---
title: "Classificação de Imagens RGB com SVM"
subtitle: "CEAO-802 — Métodos de Análise de Dados"
author: "1T Generoso · 1T João Marcos · 1T Vitor Cesa"
date: today
lang: pt-BR
format:
revealjs:
theme: simple
embed-resources: true
slide-number: c/t
progress: true
transition: slide
transition-speed: fast
code-overflow: scroll
highlight-style: github
fig-align: center
footer: "CEAO-802 — Classificação de Imagens RGB com SVM"
width: 1280
height: 720
include-in-header:
text: |
execute:
echo: false
warning: false
message: false
---
```{r}
#| label: setup
#| include: false
library(tidyverse)
library(knitr)
library(scales)
library(caret)
library(e1071)
azul <- "#2C7FB8"
laranja <- "#E06C00"
# ── Carregar resultados do cache (sem rodar CNN nem modelos) ──────────────────
resultados_holdout <- read_csv("../outputs_parcial/resultados_modelos_rgb.csv",
show_col_types = FALSE) %>%
arrange(desc(acuracia))
resultados_kfold <- read_csv("../outputs_parcial/resultados_kfold_rgb.csv",
show_col_types = FALSE) %>%
arrange(desc(acuracia))
importancia_df <- read_csv("../outputs_parcial/importancia_variaveis_rgb.csv",
show_col_types = FALSE)
cores_modelos <- c(
"SVM linear" = "#2C7FB8",
"SVM radial" = "#AED6F1",
"SVM radial ajustado" = "#5DADE2",
"PCA + SVM radial" = "#1A5276",
"Random Forest" = "#E06C00"
)
```
```{r}
#| label: confmat
#| include: false
# ── Matriz de confusão do SVM linear — carregada do cache (.rds) ─────────────
# Não re-treina o modelo: só carrega o objeto salvo e prediz no mesmo split.
modelos_salvos <- tryCatch(
readRDS("../outputs_parcial/modelos_rgb.rds"),
error = function(e) NULL
)
dados_cache <- tryCatch(
read_csv("../outputs_parcial/features_rgb.csv", show_col_types = FALSE) %>%
mutate(classe = factor(classe, levels = paste0("C", 1:13))),
error = function(e) NULL
)
cm_plot <- NULL
if (!is.null(modelos_salvos) && !is.null(dados_cache)) {
set.seed(123)
idx_tr <- createDataPartition(dados_cache$classe, p = 0.70, list = FALSE)
teste <- dados_cache[-idx_tr, ]
cols_meta <- c("arquivo", "classe", "tamanho_mb")
x_te <- teste %>% select(-all_of(cols_meta))
y_te <- factor(teste$classe, levels = levels(dados_cache$classe))
# Selecionar as colunas que o preproc conhece (após nearZeroVar)
features_usadas <- names(modelos_salvos$preproc$mean)
x_te_sub <- x_te[, features_usadas, drop = FALSE]
x_te_norm <- predict(modelos_salvos$preproc, x_te_sub)
pred <- predict(modelos_salvos$modelo_svm_linear, x_te_norm)
cm_tbl <- as.data.frame(table(Predicao = pred, Real = y_te))
cm_plot <- ggplot(cm_tbl, aes(x = Real, y = Predicao, fill = Freq)) +
geom_tile(color = "white", linewidth = 0.4) +
geom_text(aes(label = ifelse(Freq > 0, Freq, "")),
size = 3, color = "black") +
scale_fill_gradient(low = "#EBF5FB", high = "#1A5276") +
labs(x = "Classe real", y = "Classe prevista", fill = "n") +
theme_minimal(base_size = 11) +
theme(axis.text.x = element_text(angle = 45, hjust = 1),
axis.text = element_text(size = 9),
panel.grid = element_blank(),
legend.position = "right")
}
```
## Roteiro {.center}
::: {.incremental}
1. Contexto e objetivo
2. Fundamentos: Transfer Learning e SVM
3. Pipeline em R — código-chave
4. Cinco modelos avaliados
5. Comparação de resultados
6. Validação cruzada K-Fold
7. Discussão e conclusão
:::
## Contexto e Objetivo
:::: {.columns}
::: {.column width="55%"}
- Imagens RGB de cenas urbanas por **drone** — Guaratinguetá (SP)
- **13 classes** de cobertura do solo (C1–C13); dataset: RGB + TIR
- Este trabalho: apenas canal **RGB**
- Amostra: **50 imagens / classe** · **650 imagens totais** (de 1.248)
- Divisão: **70% treino** (~455) / **30% teste** (~195), estratificada
:::
::: {.column width="45%"}
**Objetivo**
> Classificação supervisionada de cenas urbanas usando Transfer Learning como extrator de atributos e SVM como classificador.
**Baseline aleatório** (13 classes):
7,7%
Qualquer modelo acima disso aprende algo real.
:::
::::
## As 13 Classes do Dataset
:::: {.columns}
::: {.column width="50%"}
| Classe | Cobertura |
|:------:|-----------|
| C1 | Vegetação Arbórea |
| C2 | Vegetação Rasteira |
| C3 | Solo Exposto |
| C4 | Pavimento Asfáltico |
| C5 | Solo Estabilizado |
| C6 | Telhado Cerâmico |
| C7 | Telhado de Fibrocimento |
:::
::: {.column width="50%"}
| Classe | Cobertura |
|:------:|-----------|
| C8 | Telhado Metálico |
| C9 | Placa Fotovoltaica |
| C10 | Piscina |
| C11 | Pedra Sabão |
| C12 | Caixa D'água |
| C13 | Veículos |
:::
::::
::: {.caixa-azul}
Arquivo: `C10_2023_06_M_01_RGB.tiff` → classe **C10**, junho 2023, manhã. A classe é inferida do **prefixo do nome** — sem estrutura de subpastas.
:::
## Por que Transfer Learning?
:::: {.columns}
::: {.column width="48%"}
### Abordagem tradicional
Extrair atributos estatísticos manualmente:
média, desvio-padrão, histogramas de cor
❌ Representação rasa — perde padrões espaciais e texturais complexos
:::
::: {.column width="52%"}
### Abordagem adotada
**MobileNetV2** pré-treinada no ImageNet
(>1 M imagens · 1.000 classes · Google)
- Remove a camada classificadora final
- Aplica pooling global médio
- Gera vetor de **1.280 atributos** por imagem
✅ Bordas, texturas e formas de alto nível, aprendidas em escala industrial
:::
::::
::: {.caixa-azul}
A CNN faz o trabalho de **representação**. O SVM recebe os 1.280 atributos e aprende a **fronteira de decisão** — sem re-treinar a CNN.
:::
## O que é SVM?
:::: {.columns}
::: {.column width="55%"}
**Ideia central**
Encontrar o hiperplano que **maximiza a margem** entre as classes.
**Parâmetros principais**
- `cost` — tolerância a erros; maior cost = fronteira mais rígida
- `gamma` — alcance do kernel radial; maior gamma = vizinhança menor
**Kernel trick**
Kernel radial (RBF) permite fronteiras **não-lineares** projetando os dados em espaço de alta dimensão
:::
::: {.column width="45%"}
**Dois kernels testados**
| Kernel | Fronteira | Parâmetros |
|---------|-----------|------------------|
| Linear | Hiperplano | `cost` |
| Radial | Não-linear | `cost` + `gamma` |
::: {.caixa-laranja}
SVM é sensível à escala → atributos **devem ser padronizados** antes do treino.
:::
:::
::::
## Pipeline Geral
650 imagens
RGB
→
MobileNetV2
1.280 atributos
→
center + scale
nearZeroVar
→
5 modelos
SVM · RF
→
Holdout
K-Fold k=5
## Código — Leitura e Amostra
```{r}
#| eval: false
#| echo: true
# Listar imagens e inferir classe do prefixo do nome
arquivos <- list.files(path = caminho_rgb,
pattern = "\\.(jpg|jpeg|png|bmp|tif|tiff)$",
recursive = TRUE, full.names = TRUE)
classe_inferida <- stringr::str_extract(nome_sem_extensao, "^C\\d+")
# Amostra estratificada e balanceada: 50 imagens/classe
set.seed(123)
metadados <- metadados_completo %>%
group_by(classe) %>%
group_modify(~ slice_sample(.x, n = min(50, nrow(.x)))) %>%
ungroup()
# → 650 imagens totais (50 × 13 classes)
```
**O que faz:** lista os TIFFs, extrai a classe `C1`–`C13` do prefixo e sorteia 50 imagens por classe com semente fixa.
## Código — Extração com MobileNetV2
```{r}
#| eval: false
#| echo: true
# Carregar MobileNetV2 sem a camada classificadora
modelo_base <- application_mobilenet_v2(
include_top = FALSE, # remove o bloco de 1.000 classes
weights = "imagenet", # pesos pré-treinados no ImageNet
pooling = "avg", # GlobalAveragePooling → vetor 1D
input_shape = c(224L, 224L, 3L)
)
# Normalização específica da MobileNetV2: pixels → [-1, 1]
batch_prep <- (batch_array / 127.5) - 1.0
# features: matriz n × 1.280 (processado em lotes de 16)
features <- predict(modelo_base, batch_prep, verbose = 0L)
```
**O que faz:** carrega MobileNetV2, normaliza pixels para [−1, 1] e extrai 1.280 atributos por imagem. Resultado salvo em cache `.rds`.
## Código — Pré-processamento e Treino
```{r}
#| eval: false
#| echo: true
# Divisão estratificada 70/30
idx_treino <- createDataPartition(dados$classe, p = 0.70, list = FALSE)
# Remover atributos de variância quase nula
variaveis_nzv <- nearZeroVar(x_treino)
# Padronizar: média 0, desvio 1 (essencial para o SVM)
preproc <- preProcess(x_treino, method = c("center", "scale"))
x_treino_norm <- predict(preproc, x_treino)
x_teste_norm <- predict(preproc, x_teste)
# M1 — SVM linear
modelo_svm_linear <- svm(x = x_treino_norm, y = y_treino,
kernel = "linear", cost = 1, scale = FALSE)
```
**O que faz:** divide 70/30, padroniza e treina. O mesmo fluxo se repete para cada um dos 5 modelos (parâmetros variam).
## Os 5 Modelos
| # | Modelo | Tipo | Parâmetros |
|:--:|--------|:----:|-----------|
| 1 | **SVM linear** | Linear | `cost = 1` |
| 2 | **SVM radial** | RBF | `cost = 10` · `gamma = 0,01` (fixos) |
| 3 | **SVM radial ajustado** | RBF | `cost` e `gamma` por grade 4×4 + CV |
| 4 | **PCA + SVM radial** | RBF | PCA (≥ 95% var., máx. 30 comp.) + RBF |
| 5 | **Random Forest** | Ensemble | 500 árvores · `importance = TRUE` |
::: {.caixa-azul}
O PCA reduz os 1.280 atributos aos componentes que explicam ≥ 95% da variância antes de treinar o SVM, eliminando redundâncias.
:::
## Comparação — Holdout 70/30
```{r}
#| fig-width: 10
#| fig-height: 4.5
ggplot(resultados_holdout,
aes(x = reorder(modelo, acuracia), y = acuracia, fill = modelo)) +
geom_col(show.legend = FALSE, width = 0.7) +
geom_text(aes(label = percent(acuracia, accuracy = 0.1)),
hjust = -0.12, size = 4.5, fontface = "bold", color = "grey20") +
coord_flip(ylim = c(0, 0.52)) +
scale_fill_manual(values = cores_modelos) +
labs(x = NULL, y = "Acurácia") +
theme_minimal(base_size = 14) +
theme(panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank())
```
::: {.caixa-azul}
Baseline aleatório (13 classes) = **7,7%** — SVM linear (~40%) é **5,2× melhor que o acaso**.
:::
## Melhor Modelo — SVM Linear
:::: {.columns}
::: {.column width="33%"}
| Métrica | Holdout | K-Fold |
|---------|:-------:|:------:|
| Acurácia | **40,0%** | **40,2%** |
| Kappa | 0,350 | 0,352 |
| F1 macro | 0,402 | 0,409 |
::: {.caixa-laranja}
Holdout e K-Fold **concordam** → resultado **estável**.
:::
:::
::: {.column width="67%"}
```{r}
#| fig-width: 6.8
#| fig-height: 5.2
if (!is.null(cm_plot)) {
print(cm_plot)
} else {
ggplot() +
annotate("text", x = 0.5, y = 0.5, size = 5, color = "grey50",
label = "Renderize com outputs/parcial/ no lugar certo") +
theme_void()
}
```
:::
::::
## Validação Cruzada K-Fold (k = 5)
```{r}
#| fig-width: 10
#| fig-height: 4.2
ggplot(resultados_kfold,
aes(x = reorder(modelo, acuracia), y = acuracia, fill = modelo)) +
geom_col(show.legend = FALSE, width = 0.7) +
geom_text(aes(label = percent(acuracia, accuracy = 0.1)),
hjust = -0.12, size = 4.5, fontface = "bold", color = "grey20") +
coord_flip(ylim = c(0, 0.52)) +
scale_fill_manual(values = cores_modelos) +
labs(x = NULL, y = "Acurácia média (5 folds)") +
theme_minimal(base_size = 14) +
theme(panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank())
```
> **Por que k-fold?** O holdout avalia uma única divisão aleatória. O k-fold usa **todas as imagens** para avaliação, em 5 rodadas independentes — estimativa mais robusta.
## Holdout × K-Fold
```{r}
#| fig-width: 10
#| fig-height: 5.0
bind_rows(
resultados_holdout %>% mutate(estrategia = "Holdout 70/30"),
resultados_kfold %>% mutate(estrategia = "K-Fold (k=5)")
) %>%
ggplot(aes(x = reorder(modelo, acuracia),
y = acuracia, fill = estrategia)) +
geom_col(position = position_dodge(0.72), width = 0.68) +
geom_text(aes(label = percent(acuracia, accuracy = 0.1)),
position = position_dodge(0.72),
hjust = -0.1, size = 4, color = "grey20") +
coord_flip(ylim = c(0, 0.55)) +
scale_fill_manual(values = c("Holdout 70/30" = azul,
"K-Fold (k=5)" = laranja)) +
labs(x = NULL, y = "Acurácia", fill = NULL) +
theme_minimal(base_size = 14) +
theme(legend.position = "bottom",
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank())
```
## Importância das Variáveis — Random Forest
```{r}
#| fig-width: 10
#| fig-height: 5.0
top10 <- importancia_df %>% slice_head(n = 10)
ggplot(top10,
aes(x = reorder(variavel, importancia), y = importancia)) +
geom_col(fill = azul) +
geom_col(
data = top10 %>% slice_head(n = 2),
fill = laranja
) +
geom_text(aes(label = round(importancia, 2)),
hjust = -0.12, size = 4, color = "grey20") +
coord_flip(ylim = c(0, max(top10$importancia) * 1.18)) +
labs(x = "Atributo (neurônio MobileNetV2)",
y = "Importância (MeanDecreaseGini)",
subtitle = "Laranja = 2 atributos mais discriminativos") +
theme_minimal(base_size = 13) +
theme(panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank())
```
::: {.fragment .caixa-azul}
`feat_0472` e `feat_0260` têm importância ~70% maior que a 3ª colocada. São índices de neurônios internos da CNN — sem interpretação visual direta.
:::
## Discussão
```{r}
#| include: false
melhor_modelo_kf <- resultados_kfold$modelo[which.max(resultados_kfold$acuracia)]
melhor_acc_kf <- max(resultados_kfold$acuracia)
```
**Resultado principal**
- Melhor modelo: **`r melhor_modelo_kf`** com acurácia k-fold de **`r percent(melhor_acc_kf, accuracy = 0.1)`** (~5,2× acima do acaso)
- Holdout e k-fold convergiram → resultado **estável e confiável**
::: {.caixa-laranja}
**Achado mais relevante —** **SVM linear** superou o SVM radial com parâmetros fixos (14%). Os atributos da MobileNetV2 formam um espaço *quase linearmente separável* — o kernel linear é suficiente.
:::
**Trabalhos futuros**
- Outros extratores CNN: EfficientNetB0 · ResNet50 · VGG19
- Incorporar o canal **TIR** ao vetor de atributos
- Aumentar a amostra por classe (50 imagens/classe) e fazer fine-tuning da CNN
## Conclusão {.center}
> Transfer learning combina o poder representacional das CNNs com a interpretabilidade dos classificadores clássicos — **sem treinar a rede do zero**.
::: {.caixa-azul}
A abordagem é simples, reprodutível e já supera em 5× o baseline aleatório com apenas 50 imagens por classe — base sólida para extensões futuras.
:::
## {.center}
### Obrigado — Perguntas?
**1T Generoso · 1T João Marcos · 1T Vitor Cesa**
*CEAO-802 — Métodos de Análise de Dados*