SUMÁRIO

🗒️ Resumo

🗒️ 1. Introdução

🗒️ 2. Fundamentação teórica

🗒️ 3. Primeiros passos Hands-on

🗒️ 4. Aplicações com SVM

🗒️ 5. Outros tipos de Modelos de Vetores de Suporte

🗒️ 6. Exercícios

🗒️ Apêndice


4.1 Aplicações com SVM em Linguagem R

Máquinas de Vetores de Suporte (MVS) - também conhecido como Support Vector Machines (SVM) - é um poderoso modelo de aprendizado estatístico de máquina que tem sido amplamente utilizado para resolver problemas de classificação. Como abordado na seção teórica, objetivo das SVMs, no contexto de classificação, é encontrar um hiperplano de separação ótimo, capaz de separar duas classes de dados. Um dos diferenciais das SVMs está em sua capacidade de lidar com dados linearmente separáveis ou não linearmente separáveis, graças ao uso do truque do kernel. Neste primeiro exemplo, iremos explorar a utilização do SVM para classificar cédulas em duas classes: fraudulentas ou autênticas. Entretanto, antes de mergulhar no modelo em si, é essencial investigar e analisar a base de dados em questão.

Autenticação de cédulas:

Os dados foram extraídos de imagens capturadas para a avaliação de um procedimento de autenticação de cédulas bancárias. Durante esse processo, as imagens foram utilizadas para obter informações e características relevantes das cédulas, a fim de verificar sua autenticidade. As imagens foram processadas utilizando a Transformada Wavelet, decompondo a imagem em versões deslocadas e escalonadas de uma wavelet. O resultado final foi sumarizado nos cinco preditores que compõe a base de dados:

1.Variância da imagem após a Transformada Wavelet (\(X_{1}\)).

2.Assimetria da imagem após a Transformada Wavelet (\(X_{2}\)).

3.Curtosis da imagem após a Transformada Wavelet (\(X_{3}\)).

4.Entropia da imagem após a Transformada Wavelet (\(X_{4}\)).

A variável dependente \(Y\) está codificada nos valores inteiros \(\{0,1\}\) sendo cédulas falsas e verdadeiras respectivamente. A base de dados pode ser obtida através do repositório de aprendizado estatístico da UC Ivirne University (Lohweg 2013), assim como maiores informações sobre a aplicação.

Análise Exploratória:

Inicialmente carrega-se a base de dados presente no link.

library(tidyverse)
banknote <- read.csv("data_banknote_authentication.txt", header=FALSE)

Pode-se verificar as estatísticas descritivas do conjunto de dados em questão através do summary.

summary(banknote)
##        V1                V2                V3                V4         
##  Min.   :-7.0421   Min.   :-13.773   Min.   :-5.2861   Min.   :-8.5482  
##  1st Qu.:-1.7730   1st Qu.: -1.708   1st Qu.:-1.5750   1st Qu.:-2.4135  
##  Median : 0.4962   Median :  2.320   Median : 0.6166   Median :-0.5867  
##  Mean   : 0.4337   Mean   :  1.922   Mean   : 1.3976   Mean   :-1.1917  
##  3rd Qu.: 2.8215   3rd Qu.:  6.815   3rd Qu.: 3.1793   3rd Qu.: 0.3948  
##  Max.   : 6.8248   Max.   : 12.952   Max.   :17.9274   Max.   : 2.4495  
##        V5        
##  Min.   :0.0000  
##  1st Qu.:0.0000  
##  Median :0.0000  
##  Mean   :0.4446  
##  3rd Qu.:1.0000  
##  Max.   :1.0000

Como podemos observar, a V5 está no formato de inteiro. Portanto, é necessário convertê-la em factor, uma vez que representa a variável dependente \(y \in \{0,1\}\) e, que, posteriormente, será tratada em problema de classificação.

banknote <- banknote |> dplyr::mutate(V5 = as.factor(V5)) |> dplyr::rename(y = V5)

summary(banknote)
##        V1                V2                V3                V4         
##  Min.   :-7.0421   Min.   :-13.773   Min.   :-5.2861   Min.   :-8.5482  
##  1st Qu.:-1.7730   1st Qu.: -1.708   1st Qu.:-1.5750   1st Qu.:-2.4135  
##  Median : 0.4962   Median :  2.320   Median : 0.6166   Median :-0.5867  
##  Mean   : 0.4337   Mean   :  1.922   Mean   : 1.3976   Mean   :-1.1917  
##  3rd Qu.: 2.8215   3rd Qu.:  6.815   3rd Qu.: 3.1793   3rd Qu.: 0.3948  
##  Max.   : 6.8248   Max.   : 12.952   Max.   :17.9274   Max.   : 2.4495  
##  y      
##  0:762  
##  1:610  
##         
##         
##         
## 

Dois pressupostos importantes, e muitas vezes negligenciados, para a utilização do SVM e de outros modelos de aprendizado estatístico de máquina são:

  1. As observações precisam ser amostradas de maneira independente e de uma mesma população (i.i.d).
  2. Os preditores precisam ser independentes.

A verificação e validação desses pressupostos nem sempre são tão simples. No entanto, eles podem ser facilmente invalidados sendo necessárias abordagens diferentes para cada caso. Dados de séries temporais facilmente violam a primeira condição, e vamos demonstrar como lidar com eles em futuros exemplos. Para o caso do banknote, vamos assumir como verdadeiro uma vez que cada cédula não depende da outra e todas fazem parte da mesma população. Para verificar o segundo pressuposto, podemos analisar, pelo menos, os valores de correlação linear para ter uma rápida avaliação de que não há nenhum preditor dependente do outro.

#Podemos analisar essa relação inicialmente através da função pairs.
pairs(banknote[,1:4], col = banknote[,5])

# E também através de uma representação gráfica da matriz de correlação linear.
library(corrplot)
cor_m <- cor(banknote[,1:4,])

corrplot(cor_m, method = "circle", type = "full", tl.cex = 0.8)

Como podemos perceber, há uma forte correlação linear entre V2 e V3, o que pode indicar que uma transformação nas variáveis pode ser necessária a fim de obter uma melhor performance do modelo, por exemplo, Análise de Componentes Principais (ACP).

Validação Cruzada (VC):

Com o objetivo de avaliar a capacidade preditiva do modelo, é fundamental verificar sua capacidade de generalização, ou seja, a capacidade de fazer previsões precisas para novas cédulas que não foram previamente classificadas como autênticas ou não. Uma maneira simples de obter uma base de dados considerada “nova” é através da utilização da validação cruzada (também conhecida como cross-validation (CV)), onde a base de dados é dividida em conjuntos de treinamento e teste. A forma como essa divisão é feita resulta em diferentes técnicas de validação cruzada, tais como: holdout, holdout repetido, k-fold, k-fold repetido e leave-one-out. Para fins ilustrativos, será utilizada a abordagem de holdout repetido. No entanto, para determinar a melhor técnica para o conjunto de dados e modelo em questão, é recomendado realizar uma análise mais detalhada, como apresentado no estudo de Kim (2009).

A estrutura de holdout repetido será definida utilizando uma função que retornará uma lista com os conjuntos de treinamento e teste.

# Definindo uma semente para fins de reprodutibilidade.
set.seed(42)

# Definindo parâmetros do CV
n_rep <- 30
cv <- vector("list", n_rep)

for(i in 1:n_rep){
  train_index <- sample(1:nrow(banknote),
                        size = round(0.7*nrow(banknote)))
  cv[[i]]$train_set <- banknote[train_index ,]
  cv[[i]]$test_set <- banknote[-train_index,]
}

kernlab: utilizando o SVM no R

A biblioteca kernlab abrange métodos de aprendizado estatístico que variam de modelos de classificação, regressão, clusterização, entre outros. O pacote kernlab será utilizado no R, uma vez que sua implementação é otimizada, eficiente e flexível para a utilização das mais diversas funções de kernel. Para verificar todos os detalhes do pacote, é possível acessar a sua documentação completa. Outra alternativa para a utilização do SVM é através do pacote e1071.

A principal função do kernlab para a utilização do SVM chama-se ksvm, e para gerar um modelo, basta utilizar a mesma sintaxe presente na maioria das funções em R.

library(kernlab)
svm_mod <- kernlab::ksvm(y~., data = cv[[1]]$train_set)

Para observar o resultado do modelo, basta rodar novamente o objeto svm_mod no console, e será apresentado:

svm_mod
## Support Vector Machine object of class "ksvm" 
## 
## SV type: C-svc  (classification) 
##  parameter : cost C = 1 
## 
## Gaussian Radial Basis kernel function. 
##  Hyperparameter : sigma =  0.429639853235315 
## 
## Number of Support Vectors : 80 
## 
## Objective Function Value : -35.8533 
## Training error : 0

Isso exibirá as informações do modelo SVM, incluindo os parâmetros configurados, as funções de kernel utilizadas, e outras informações relevantes.

Com relação ao resultado mostrado anteriormente, pode-se observar que o ksvm padrão apresenta a seguinte parametrização:

  • C: é o parâmetro de “custo” do SVM, responsável por controlar a penalidade das violações das margens. Valores maiores de C aplicam penalidades mais rigorosas às violações, resultando em uma margem “mais rígida”. Por outro lado, valores menores de C tornam a margem mais flexível. No entanto, valores muito altos ou muito baixos podem levar a problemas de ajuste inadequado do modelo, e escolher o parâmetro adequado é fundamental para o bom funcionamento do modelo.

  • Função de kernel de base radial gaussiana (Gaussian Radial Basis kernel function): o ksvm utiliza, por padrão, o kernel gaussiano como função de kernel. É possível explorar a lista completa de funções de kernel disponíveis no pacote kernlab utilizando ?kernels no console e modificá-lo através do argumento kernel. Caso a função de kernel possua hiperparâmetros, estes podem ser ajustados utilizando o argumento kpar. A escolha da função de kernel e seus hiperparâmetros é um aspecto crucial do SVM, pois o truque do kernel permite encontrar um hiperplano de separação ótimo, mesmo em casos complexos e não lineares. Assim como o parâmetro de custo C, a escolha da função de kernel pode afetar significativamente o desempenho e a capacidade de ajuste do modelo ao conjunto de dados.

Para obter mais detalhes sobre a função ksvm, acessa-se a documentação usando ?ksvm.

Por fim, a previsão do modelo pode ser obtida de maneira simples usando a função predict.

ytest_hat <- predict(svm_mod,newdata = cv[[1]]$test_set)

Para visualizar a performance do modelo, na primeira repetição do holdout pode-se construir uma matriz de confusão para uma rápida verificação.

table(obs = cv[[1]]$test_set$y, pred = ytest_hat)
##    pred
## obs   0   1
##   0 233   0
##   1   0 179

Surpreendentemente, observa-se que o SVM, com os valores padrões, alcança 100% de acurácia na amostra de teste de uma das partições. Para uma avaliação abrangente, esse processo é repetido em todas as repetições do holdout, armazenando os valores de acurácia do modelo. Além disso, o F1-score é utilizado para penalizar os falsos positivos (detectando cédulas falsas como autênticas) e os falsos negativos (detectando cédulas autênticas como falsas), uma vez que ambos os casos implicam em perdas monetárias. Devido ao equilíbrio relativo da base de dados “banknote”, espera-se que as métricas de acurácia e F1-score não apresentem diferenças significativas.

# Criando o vetor de acurácia
acc_default <- numeric(n_rep)
f1_default <- numeric(n_rep)
for(i in 1:n_rep){
  # Gerando o modelo
  svm_mod <- ksvm(y~., data = cv[[i]]$train_set)
  # Predizendo novas observaoes
  yhat_test <- predict(svm_mod, newdata = cv[[i]]$test_set)
  # Criando a matriz de confusao
  mc <- table(obs = cv[[i]]$test_set$y, pred = ytest_hat)
  acc_default[i] <- sum(diag(mc))/sum(mc)
  f1_default[i] <- 2*mc[2,2]/(2*mc[2,2]+mc[1,2]+mc[2,1])
}

Ao analisar o resultado final, constata-se que a acurácia média é de 0.981, e o F1-score médio é de 0.979. O próximo passo será aprimorar esses valores por meio do processo de tuning.

Tuning: explorando hiperparâmetros e funções de kernel.

Para decidir qual o modelo SVM apresenta o melhor ajuste, os seguintes três parâmetros do modelo serão variados:

  1. Função de Kernel: existem diversas opções para escolher o kernel a ser utilizado, dependendo do conjunto de dados em questão. A criação de funções de kernel é um campo de estudo ativo atualmente, por isso, vamos limitar a utilização a quatro funções amplamente utilizadas na literatura: linear, polinomial, gaussiano e laplaciano. Na tabela a seguir, estão apresentados os valores e seus hiperparâmetros correspondentes.
Kernel K(x,y) Parâmetros
Kernel Linear \((x·y)\) -
Kernel Polinomial \((x·y)^d\) \(d\)
Kernel Gaussiano \(e^{-\sigma||x-y||^{2}}\) \(\sigma\)
Kernel Laplacian \(e^{-\sigma||x-y||^{2}}\) \(\sigma\)
  1. Hiperparâmetros dos kernels: como mostrado na tabela anterior, os kernels polinomial, gaussiano e laplaciano possuem hiperparâmetros, e a escolha adequada desses hiperparâmetros pode definir o melhor modelo a ser ajustado.

  2. C: o parâmetro de custo.

Para selecionar a combinação com o menor erro nas amostras de teste, a técnica de grid-search será utilizada. Essa abordagem consiste em criar um grid com todas as combinações possíveis dos hiperparâmetros, calcular as métricas de desempenho e selecionar aquela que apresentar o melhor valor da métrica escolhida para avaliar o desempenho do modelo. O tamanho do grid deve ser escolhido cuidadosamente, buscando um equilíbrio entre uma busca refinada e o custo computacional envolvido. Dessa forma, garante-se que nenhuma combinação seja deixada de fora sem tornar o processo computacionalmente inviável.

Para criar o grid de valores, será realizado o seguinte procedimento:

# Criando um grid para o vetor de custo
c_grid <- c(0.01,0.1,1,10,100)
sigma_grid <- c(0.01,0.1,0.25,0.5,1,5,10,100)
d_grid <- c(2,3)

# Criando o grid completo para cada uma das funcoes de kernel
lin_grid <- expand.grid(C = c_grid,kernel = "vanilladot",sigma_grid=NA,d_grid = NA)
pol_grid <- expand.grid(C = c_grid,kernel = "polydot",sigma_grid=NA,d_grid = d_grid)
gau_grid <- expand.grid(C = c_grid,kernel = "rbfdot",sigma_grid=sigma_grid,d_grid = NA)
lap_grid <- expand.grid(C = c_grid,kernel = "laplacedot",sigma_grid=sigma_grid,d_grid = NA)

Será criada uma matriz para cada uma das funções de kernel, na qual as linhas correspondem ao número da repetição do holdout e as colunas correspondem à linha do grid criado.

# Criando matrizes para armazenar os valores da acuracia sobre a amostra de test
lin_acc <- lin_f1 <- matrix(NA,nrow = n_rep, ncol = nrow(lin_grid))
pol_acc <- pol_f1 <- matrix(NA,nrow = n_rep, ncol = nrow(pol_grid))
gau_acc <- gau_f1 <- matrix(NA, nrow = n_rep, ncol = nrow(gau_grid))
lap_acc <- lap_f1 <- matrix(NA, nrow = n_rep, ncol = nrow(lap_grid))

# Iterando sobre todas os grids e todas repeticoes do holdout teremos

for(i in 1:n_rep){
  
  for(j in 1:nrow(lin_grid)){
    
      # Gerando o modelo linear
      svm_mod <- ksvm(y~., data = cv[[i]]$train_set,
                      kernel = "vanilladot",
                      C = lin_grid$C[j])
      # Predizendo novas observaoes
      yhat_test <- predict(svm_mod, newdata = cv[[i]]$test_set)
      # Criando a matriz de confusao
      mc <- table(obs = cv[[i]]$test_set$y, pred = yhat_test)
      lin_acc[i,j] <- sum(diag(mc))/sum(mc)
      lin_f1[i,j] <- 2*mc[2,2]/(2*mc[2,2]+mc[1,2]+mc[2,1])

      
  }
  
  for(j in 1:nrow(pol_grid)){
    
      # Gerando o modelo polear
      svm_mod <- ksvm(y~., data = cv[[i]]$train_set,
                      kernel = "polydot",
                      C = pol_grid$C[j],
                      kpar = list(degree = pol_grid$d_grid[j]))
      # Predizendo novas observaoes
      yhat_test <- predict(svm_mod, newdata = cv[[i]]$test_set)
      # Criando a matriz de confusao
      mc <- table(obs = cv[[i]]$test_set$y, pred = yhat_test)
      pol_acc[i,j] <- sum(diag(mc))/sum(mc)
      pol_f1[i,j] <- 2*mc[2,2]/(2*mc[2,2]+mc[1,2]+mc[2,1])

  }
  
  for(j in 1:nrow(gau_grid)){
    
      # Gerando o modelo com o Kernel Gaussiano
      svm_mod <- ksvm(y~., data = cv[[i]]$train_set,
                      kernel = "rbfdot",
                      C = gau_grid$C[j],
                      kpar = list(sigma = gau_grid$sigma_grid[j]))
      # Predizendo novas observaoes
      yhat_test <- predict(svm_mod, newdata = cv[[i]]$test_set)
      # Criando a matriz de confusao
      mc <- table(obs = cv[[i]]$test_set$y, pred = yhat_test)
      gau_acc[i,j] <- sum(diag(mc))/sum(mc)
      gau_f1[i,j] <- 2*mc[2,2]/(2*mc[2,2]+mc[1,2]+mc[2,1])
  }
  
  for(j in 1:nrow(lap_grid)){
    
      # Gerando o modelo com o Kernel Laplaciano
      svm_mod <- ksvm(y~., data = cv[[i]]$train_set,
                      kernel = "laplacedot",
                      C = lap_grid$C[j],
                      kpar = list(sigma = lap_grid$sigma_grid[j]))
      # Predizendo novas observaoes
      yhat_test <- predict(svm_mod, newdata = cv[[i]]$test_set)
      # Criando a matriz de confusao
      mc <- table(obs = cv[[i]]$test_set$y, pred = yhat_test)
      lap_acc[i,j] <- sum(diag(mc))/sum(mc)
      lap_f1[i,j] <- 2*mc[2,2]/(2*mc[2,2]+mc[1,2]+mc[2,1])

  }
  
  cat(paste0("Running repetition number: ",i, "\n"))
  
  
}

Para fazer uma comparação geral, devem ser escolhidas as colunas que produzem os maiores valores médios de ACC e F1-score.

# Obtendo os valores maximos com respeito à ACC.
lin_max_acc <- which(apply(lin_acc,2,mean) == max(apply(lin_acc,2,mean)))
pol_max_acc <- which(apply(pol_acc,2,mean) == max(apply(pol_acc,2,mean)))
gau_max_acc <- which(apply(gau_acc,2,mean) == max(apply(gau_acc,2,mean)))
lap_max_acc <- which(apply(lap_acc,2,mean) == max(apply(lap_acc,2,mean)))

# Obtendo os valores maximos com respeito ao F1.
lin_max_f1 <- which(apply(lin_f1,2,mean) == max(apply(lin_f1,2,mean)))
pol_max_f1 <- which(apply(pol_f1,2,mean) == max(apply(pol_f1,2,mean)))
gau_max_f1 <- which(apply(gau_f1,2,mean) == max(apply(gau_f1,2,mean)))
lap_max_f1 <- which(apply(lap_f1,2,mean) == max(apply(lap_f1,2,mean)))

Para este caso, pode-se verificar se os hiperparâmetros que produziram os valores máximos de acurácia são os mesmos para o F1-score.

# Retorna TRUE caso todos indices de
all(all(lin_max_acc==lin_max_f1),
    all(pol_max_acc==pol_max_f1),
    all(gau_max_acc==gau_max_f1),
    all(lap_max_acc==lap_max_f1))
## [1] TRUE

Como os valores são iguais, é possível selecionar uma combinação dos hiperparâmetros para plotar os valores obtidos na validação cruzada.

Ao plotar os resultados em um boxplot final e compará-los com o anterior, têm-se:

# Criando uma base para armazenar todas as metricas dos resultados finais
plot_data <- rbind(data.frame(metric = "ACC",
                                         kernel = "Default",
                                         val = acc_default),
                    data.frame(metric = "ACC",
                                         kernel = "Linear",
                                         val = lin_acc[,lin_max_acc[1]]),
                   data.frame(metric = "ACC",
                                         kernel = "Polinomial",
                                         val = pol_acc[,pol_max_acc[1]]),
                   data.frame(metric = "ACC",
                                         kernel = "Gaussian",
                                         val = gau_acc[,gau_max_acc[1]]),
                   data.frame(metric = "ACC",
                                         kernel = "Laplacian",
                                         val = lap_acc[,lap_max_acc[1]]),
                   data.frame(metric = "F1",
                                         kernel = "Default",
                                         val = f1_default),
                   data.frame(metric = "F1",
                                         kernel = "Linear",
                                         val = lin_f1[,lin_max_f1[1]]),
                   data.frame(metric = "F1",
                                         kernel = "Polinomial",
                                         val = pol_f1[,pol_max_f1[1]]),
                   data.frame(metric = "F1",
                                         kernel = "Gaussian",
                                         val = gau_f1[,gau_max_f1[1]]),
                   data.frame(metric = "F1",
                                         kernel = "Laplacian",
                                         val = lap_f1[,lap_max_f1[1]])) %>% 
                  mutate(kernel = factor(kernel,levels = c("Default", "Linear", "Polinomial",
                                                           "Gaussian", "Laplacian")))
                   
# Importando o GGplot
library(ggplot2)
ggplot(plot_data)+
  geom_boxplot(mapping = aes(x = kernel, y = val, col = metric))+
  guides(color = guide_legend(title = "Métrica"))+
  xlab("Kernel")+
  ylab("")+
  theme_classic()

A partir dos resultados obtidos, é possível observar que a abordagem de tuning melhorou significativamente a performance do modelo SVM. Todos os kernels, além do linear (polinomial, gaussiano e laplaciano), alcançaram uma acurácia e F1 de 100% sobre a amostra de teste, após o ajuste dos hiperparâmetros ideais. Isso indica que o modelo foi capaz de generalizar corretamente para novas amostras não vistas durante o treinamento.

Para realizar a mesma análise utilizando a linguagem Python, a documentação e explicação estão presentes no capítulo seguinte.

Apêndice

Resultados do tuning:

Apesar de selecionarmos os parâmetros que produziram os valores máximos de acurácia e F1, esses valores não foram apresentados. As tabelas abaixo resumem todas as combinações de hiperparâmetros para cada uma das funções de kernel.

Hiperparâmetros que produziram valores máximos de ACC e F1 para o kernel Linear utilizando kernlab.
C
10
Hiperparâmetros que produziram valores máximos de ACC e F1 para o kernel Polinomial utilizando kernlab.
C d
1.0 2
0.1 3
Hiperparâmetros que produziram valores máximos de ACC e F1 para o kernel Gaussiano utilizando kernlab.
C Sigma
10 0.10
100 0.10
10 0.25
100 0.25
10 0.50
100 0.50
1 5.00
10 5.00
100 5.00
Hiperparâmetros que produziram valores máximos de ACC e F1 para o kernel Laplaciano utilizando kernlab.
C Sigma
100 0.01
10 0.10
100 0.10
1 0.25
10 0.25
100 0.25
1 0.50
10 0.50
100 0.50
1 5.00
10 5.00
100 5.00

Referências

Kim, Ji-Hyun. 2009. «Estimating classification error rate: Repeated cross-validation, repeated hold-out and bootstrap». Computational statistics & data analysis 53 (11): 3735–45.
Lohweg, Volker. 2013. «Banknote Authentication». UCI Machine Learning Repository.