Files
sensoriamento-rgb-svm/trabalho/amostra/apresentacao/apresentacao_svm_rgb.qmd
Cesa-V 953758497d Reorganiza estrutura do projeto em trabalho/ e artigo/
Move arquivos de analise, apresentacao e outputs para subpastas
dedicadas; remove caches orfaos e tex nao usados.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 10:35:57 -03:00

589 lines
18 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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: |
<style>
.reveal h1, .reveal h2 { color: #2C7FB8 !important; }
.reveal h3 { color: #1a5276 !important; }
.reveal .slide-number { color: #2C7FB8; }
.reveal .progress { background: #E06C00 !important; }
.destaque { color: #E06C00; font-weight: bold; }
.azul { color: #2C7FB8; font-weight: bold; }
.reveal pre { font-size: 0.72em; line-height: 1.45; }
.reveal table { font-size: 0.78em; margin: auto; }
.reveal table th {
background: #2C7FB8; color: white; padding: 4px 10px;
}
.reveal table td { padding: 3px 10px; }
.reveal table tr:nth-child(even) td { background: #EBF5FB; }
.caixa-azul {
background: #EBF5FB;
border-left: 5px solid #2C7FB8;
padding: 0.45em 1em;
border-radius: 4px;
margin-top: 0.6em;
font-size: 0.86em;
}
.caixa-laranja {
background: #FEF9E7;
border-left: 5px solid #E06C00;
padding: 0.45em 1em;
border-radius: 4px;
margin-top: 0.6em;
font-size: 0.86em;
}
.reveal .footer { color: #aaa; font-size: 0.65em; }
/* ── Redução de tamanho base (padrão simple = ~40px) ── */
:root { --r-main-font-size: 34px; }
.reveal { font-size: 34px; }
.reveal h2 { font-size: 1.35em; margin-bottom: 0.25em; }
.reveal h3 { font-size: 1.05em; margin-bottom: 0.2em; }
.reveal ul, .reveal ol { margin: 0.25em 0 0.25em 1.2em; }
.reveal li { margin: 0.15em 0; line-height: 1.3; }
.reveal p { margin: 0.3em 0; line-height: 1.35; }
.reveal blockquote { padding: 0.3em 0.8em; margin: 0.35em 0; }
</style>
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 (C1C13); 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):
<span class="destaque" style="font-size:1.5em">7,7%</span>
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
<div style="display:flex; align-items:center; justify-content:center; gap:0; margin-top:80px">
<div style="background:#2C7FB8; color:white; border-radius:50%; width:200px; height:200px; display:flex; align-items:center; justify-content:center; text-align:center; font-weight:bold; font-size:0.85em; line-height:1.4; flex-shrink:0">650 imagens<br>RGB</div>
<span style="font-size:2.2em; color:#999; margin:0 10px; flex-shrink:0">→</span>
<div style="background:#2C7FB8; color:white; border-radius:50%; width:200px; height:200px; display:flex; align-items:center; justify-content:center; text-align:center; font-weight:bold; font-size:0.85em; line-height:1.4; flex-shrink:0">MobileNetV2<br>1.280 atributos</div>
<span style="font-size:2.2em; color:#999; margin:0 10px; flex-shrink:0">→</span>
<div style="background:#2C7FB8; color:white; border-radius:50%; width:200px; height:200px; display:flex; align-items:center; justify-content:center; text-align:center; font-weight:bold; font-size:0.85em; line-height:1.4; flex-shrink:0">center + scale<br>nearZeroVar</div>
<span style="font-size:2.2em; color:#999; margin:0 10px; flex-shrink:0">→</span>
<div style="background:#2C7FB8; color:white; border-radius:50%; width:200px; height:200px; display:flex; align-items:center; justify-content:center; text-align:center; font-weight:bold; font-size:0.85em; line-height:1.4; flex-shrink:0">5 modelos<br>SVM · RF</div>
<span style="font-size:2.2em; color:#999; margin:0 10px; flex-shrink:0">→</span>
<div style="background:#2C7FB8; color:white; border-radius:50%; width:200px; height:200px; display:flex; align-items:center; justify-content:center; text-align:center; font-weight:bold; font-size:0.85em; line-height:1.4; flex-shrink:0">Holdout<br>K-Fold k=5</div>
</div>
## 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 |
<br>
::: {.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}
<br>
> Transfer learning combina o poder representacional das CNNs com a interpretabilidade dos classificadores clássicos — **sem treinar a rede do zero**.
<br>
::: {.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}
<br><br>
### Obrigado — Perguntas?
<br>
**1T Generoso · 1T João Marcos · 1T Vitor Cesa**
*CEAO-802 — Métodos de Análise de Dados*