368 lines
11 KiB
Plaintext
368 lines
11 KiB
Plaintext
---
|
||
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)
|