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