Files
sensoriamento-rgb-svm/apresentacao_svm_rgb.qmd
2026-06-16 20:01:36 -03:00

368 lines
11 KiB
Plaintext
Raw 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: "Classificacao de Imagens RGB com SVM"
subtitle: "CEAO-802 — Metodos de Analise de Dados"
author:
- "1T Generoso"
- "1T João Marcos"
- "1T Vitor Cesa"
format:
pptx:
fig-width: 9
fig-height: 4.5
execute:
warning: false
message: false
echo: false
---
```{r setup}
library(tidyverse)
library(knitr)
library(scales)
resultados_holdout <- read_csv("outputs/resultados_modelos_rgb.csv", show_col_types = FALSE)
resultados_kfold <- read_csv("outputs/resultados_kfold_rgb.csv", show_col_types = FALSE)
importancia_df <- read_csv("outputs/importancia_variaveis_rgb.csv", show_col_types = FALSE)
cores_modelos <- c(
"SVM linear" = "#2C7FB8",
"SVM radial" = "#7FCDBB",
"SVM radial ajustado" = "#41B6C4",
"PCA + SVM radial" = "#1D91C0",
"Random Forest" = "#225EA8"
)
```
# Introducao e Dataset
## Contexto do Problema
- Imagens aéreas coletadas por **drone** sobre **Guaratinguetá (SP)**
- Objetivo: classificar cada recorte em uma das **13 classes de cobertura urbana**
- Dataset disponibiliza 4 modalidades: **RGB dia**, **RGB noite**, **TIR dia**, **TIR noite**
- Este trabalho utiliza somente as imagens **RGB**
- As imagens já vinham **pré-rotuladas** pelo nome do arquivo
Exemplo: `C10_2023_06_M_01_RGB.tiff` → classe **C10 (Piscina)**, junho 2023, manhã
## As 13 Classes
:::: {.columns}
::: {.column width="50%"}
| Classe | Descrição |
|:------:|-----------|
| 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 | Descrição |
|:------:|-----------|
| C8 | Telhado Metálico |
| C9 | Placa Fotovoltaica |
| C10 | Piscina |
| C11 | Pedra Sabão |
| C12 | Caixa D'água |
| C13 | Veículos |
:::
::::
## Amostra Balanceada
```{r dataset-plot}
tibble(
classe = factor(paste0("C", 1:13), levels = paste0("C", 1:13)),
n = 50
) |>
ggplot(aes(x = classe, y = n)) +
geom_col(fill = "#2C7FB8", width = 0.7) +
geom_text(aes(label = n), vjust = -0.5, size = 4, fontface = "bold") +
scale_y_continuous(limits = c(0, 62)) +
labs(
title = "50 imagens por classe — amostra estratificada e balanceada",
subtitle = "Dataset completo: 1.248 imagens TIFF | Amostra utilizada: 650 imagens",
x = "Classe", y = "Imagens na amostra"
) +
theme_minimal(base_size = 14) +
theme(panel.grid.major.x = element_blank())
```
# Metodologia
## Pipeline de Classificação
```{r pipeline, fig.height=3.2}
tibble(
x = 1:5,
label = c("Imagens\nRGB", "MobileNetV2\n1280 features",
"Normalização\n+ PCA (opt.)", "SVM / RF",
"Avaliação\nHoldout / K-Fold")
) |>
ggplot(aes(x = x, y = 1)) +
geom_point(size = 20, color = "#2C7FB8") +
geom_text(aes(label = label), size = 3.8, color = "white",
fontface = "bold", lineheight = 1.0) +
annotate("segment",
x = c(1.38,2.38,3.38,4.38), xend = c(1.62,2.62,3.62,4.62),
y = 1, yend = 1, linewidth = 1.4, color = "grey40",
arrow = arrow(length = unit(0.3, "cm"), type = "closed")) +
scale_x_continuous(limits = c(0.5, 5.5)) +
scale_y_continuous(limits = c(0.6, 1.4)) +
theme_void()
```
## Transfer Learning — MobileNetV2
:::: {.columns}
::: {.column width="55%"}
**O que é transfer learning?**
A rede MobileNetV2 foi treinada no **ImageNet** (1,2 milhão de imagens, 1000 categorias). Reutilizamos essa rede como **extrator de atributos**, sem retreiná-la.
- Imagem redimensionada para **224 × 224 px**
- Camada de classificação removida
- Saída: **vetor de 1280 valores** por imagem
- Cada valor representa um padrão visual aprendido
:::
::: {.column width="45%"}
| | Tradicional | Transfer |
|--|:-----------:|:-------:|
| Nº features | ~60 | **1280** |
| Origem | Manual | CNN |
| Treinamento | Não | Pré-treinado |
| Padrões | Cor/textura | Alto nível |
Não é necessário treinar a rede do zero — apenas **reutilizamos** o que ela já aprendeu.
:::
::::
## Código — Carregando o Modelo
```r
library(keras3)
# Carregar MobileNetV2 sem a camada de classificação
modelo_base <- application_mobilenet_v2(
include_top = FALSE, # remove a camada final de 1000 classes
weights = "imagenet", # pesos pré-treinados no ImageNet
pooling = "avg", # GlobalAveragePooling → vetor 1D
input_shape = c(224L, 224L, 3L)
)
# Saída: vetor de 1280 atributos por imagem
```
## Código — Extraindo Features por Imagem
```r
# Para cada imagem da amostra:
img <- image_load(arquivo, target_size = c(224L, 224L))
arr <- image_to_array(img) # array 224x224x3
arr <- array_reshape(arr, c(1L, 224L, 224L, 3L)) # batch de 1 imagem
arr <- (arr / 127.5) - 1.0 # normalização padrão do MobileNetV2 [-1, 1]
feat <- predict(modelo_base, arr, verbose = 0L)
# feat: vetor de 1280 valores — atributos visuais de alto nível
```
Processamento em **lotes de 16 imagens** para eficiência, com **cache em disco** para evitar reprocessamento.
# Modelos Avaliados
## Os 5 Modelos
| # | Modelo | Tipo | Parâmetros |
|:--:|--------|:----:|-----------|
| 1 | **SVM linear** | Linear | `cost = 1` |
| 2 | **SVM radial** | RBF | `cost = 10`, `gamma = 0.01` |
| 3 | **SVM radial ajustado** | RBF | `cost` e `gamma` via grid search |
| 4 | **PCA + SVM radial** | RBF | PCA (≥ 95% variância) + RBF |
| 5 | **Random Forest** | Ensemble | 500 árvores |
**SVM** encontra o hiperplano que **maximiza a margem** entre as classes. Com kernel **RBF**, fronteiras não-lineares são possíveis via *kernel trick*.
## Código — Treinamento dos Modelos
```r
library(e1071); library(caret); library(randomForest)
# Pré-processamento: remover variância nula e normalizar
preproc <- preProcess(x_treino, method = c("center", "scale"))
x_treino_n <- predict(preproc, x_treino)
# Modelo 1: SVM linear
modelo_linear <- svm(x = x_treino_n, y = y_treino,
kernel = "linear", cost = 1, scale = FALSE)
# Modelo 3: Ajuste de hiperparâmetros (grid search com CV interna)
ajuste <- tune.svm(x = x_treino_n, y = y_treino, kernel = "radial",
cost = c(0.1, 1, 10, 100),
gamma = c(0.001, 0.01, 0.05, 0.1),
tunecontrol = tune.control(cross = 5))
# Modelo 5: Random Forest
modelo_rf <- randomForest(x = x_treino_n, y = y_treino,
ntree = 500, importance = TRUE)
```
# Avaliacao dos Modelos
## Estratégia 1 — Holdout 70/30
:::: {.columns}
::: {.column width="38%"}
**Divisão única e aleatória**
- 70% treino (~455 imagens)
- 30% teste (~195 imagens)
- Estratificada por classe
**Limitação:** com datasets menores, o resultado pode variar conforme a divisão escolhida.
:::
::: {.column width="62%"}
```{r holdout-plot}
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") +
coord_flip(ylim = c(0, 0.53)) +
scale_fill_manual(values = cores_modelos) +
labs(title = "Acurácia — Holdout 70/30", x = NULL, y = "Acurácia") +
theme_minimal(base_size = 13)
```
:::
::::
## Estratégia 2 — Validação Cruzada K-Fold (k=5)
:::: {.columns}
::: {.column width="38%"}
**5 partições estratificadas**
- Cada parte usada **uma vez** como teste
- Acurácia final = **média das 5 rodadas**
- Toda imagem aparece no teste
**Vantagem:** estimativa mais **robusta** — não depende de uma única divisão.
:::
::: {.column width="62%"}
```{r kfold-plot}
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") +
coord_flip(ylim = c(0, 0.53)) +
scale_fill_manual(values = cores_modelos) +
labs(title = "Acurácia média — K-Fold (k=5)", x = NULL, y = "Acurácia") +
theme_minimal(base_size = 13)
```
:::
::::
## Código — Validação Cruzada K-Fold
```r
folds <- createFolds(dados$classe, k = 5, list = TRUE, returnTrain = FALSE)
for (fold_i in seq_len(5)) {
idx_teste <- folds[[fold_i]]
idx_treino <- setdiff(seq_len(nrow(dados)), idx_teste)
# Pré-processar apenas com dados do treino deste fold
pp <- preProcess(x[idx_treino, ], method = c("center", "scale"))
x_tr <- predict(pp, x[idx_treino, ])
x_te <- predict(pp, x[idx_teste, ])
y_tr <- dados$classe[idx_treino]
y_te <- dados$classe[idx_teste]
modelo <- svm(x = x_tr, y = y_tr, kernel = "linear", cost = 1)
pred <- predict(modelo, x_te)
cm <- confusionMatrix(pred, y_te)
metricas_folds[[fold_i]] <- extrair_metricas("SVM linear", cm)
}
# Média das 5 rodadas
resultados_kfold <- bind_rows(metricas_folds) |>
group_by(modelo) |> summarise(across(everything(), mean))
```
## Comparação: Holdout vs K-Fold
```{r comparacao-plot, fig.height=5}
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(width = 0.72), width = 0.68) +
geom_text(aes(label = percent(acuracia, accuracy = 0.1)),
position = position_dodge(width = 0.72),
hjust = -0.1, size = 4) +
coord_flip(ylim = c(0, 0.55)) +
scale_fill_manual(values = c("Holdout 70/30" = "#2C7FB8",
"K-Fold (k=5)" = "#E06C00")) +
labs(title = "Holdout 70/30 vs K-Fold (k=5)",
x = NULL, y = "Acurácia", fill = NULL) +
theme_minimal(base_size = 13) +
theme(legend.position = "top")
```
## Variáveis Mais Importantes (Random Forest)
```{r importancia-plot, fig.height=5}
importancia_df |>
slice_head(n = 15) |>
ggplot(aes(x = reorder(variavel, importancia), y = importancia)) +
geom_col(fill = "#2C7FB8", width = 0.75) +
geom_text(aes(label = round(importancia, 2)), hjust = -0.15, size = 4) +
coord_flip(ylim = c(0, max(importancia_df$importancia[1:15]) * 1.18)) +
labs(
title = "Top 15 features mais importantes — Random Forest",
subtitle = "feat_XXXX = neurônio da camada final do MobileNetV2",
x = NULL, y = "Importância (Mean Decrease Gini)"
) +
theme_minimal(base_size = 13)
```
# Conclusao
## Conclusão
```{r conclusao-calc}
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 de **`r percent(melhor_acc_kf, accuracy = 0.1)`** (k-fold)
- Acerto aleatório com 13 classes ≈ **7,7%** → resultado ~5× melhor
- Holdout e k-fold convergiram → estimativa **estável e confiável**
**Por que SVM linear ganhou do radial?**
Os 1280 atributos da MobileNetV2 já tornam as classes aproximadamente **linearmente separáveis** — o kernel RBF não agrega ganho relevante neste caso.
**Trabalhos futuros**
- Outros extratores CNN: EfficientNetB0, ResNet50, VGG19
- Incorporar canal **TIR** (térmico) ao vetor de atributos
- Aumentar amostra por classe (atualmente 50 imagens)