--- 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*