Classificação Multiclasses

Suponha um problema de classificação que, em vez de cada instância (observação) pertencer a uma de duas possíveis classes, ela pertence a uma entre \(k\) possíveis classes.

Este problema também pode ser resolvido com uma rede neural Perseptron. A rede que vai atender a esse tipo de problema será muito parecida com aquelas vistas até o momento, a diferença é que as redes para esse tipo de problema são definidas com mais de um neurônio de saída.

Arquitetura do Perseptron para problemas de classificação Multiclasses

Se a variável resposta do problema de classificação puder assumir 1 entre \(k\) possíveis classes, o número de neurônios na camada de saída será \(k-1\). Dessa forma em vez de uma saída teremos \(k-1\) saídas, que indicam a probabilidade da instância pertencer a cada uma das \(k-1\) classes. Neste caso, a probabilidade da instância pertencer a classe que não está sendo representada por um neurônio é o complemntar da soma da probabilidade de pertencer às demais classes.

Perceptron com 1 camada oculta e 3  neurônios na camada de saída
Figura 1: Perceptron com 1 camada oculta e 3 neurônios na camada de saída

A Figura 1. apresenta uma rede com 3 covariáveis de entrada, 1 camada oculta com 4 neurônios e uma camada de saída com 3 neurônios. Isso indica que esta rede foi construída para um problema de classificação com 4 classes. A saída retorna um número entre 0 e 1 e este número indica a probabilidade da instância pertencer a classe correspondente. Importante: para uma isntância qualquer a soma das saídas tem que ser menor ou igual a 1, e a probabilidade de pertencer a classe não representada será o complementar desta soma.

A função de ativação nas camadas de saíde

Como comentado, a soma das variáveis de saída tem que ser \(\le 1\). Para garantir isso a função de ativação dos neurônios da camada de saída será a softmax, apesentada a seguir.

Seja \(K\) o número de classes possíveis. Vamos chamar de \(v_i\) o valor que saí do somatório do \(i\)-ésimo neurônio da camada de saída e “entra” para ser processado pela \(i\)-ésima função de ativação \(g_i\), \(i=1, 2, \ldots, k-1\). Seja \(\hat{y}_i = g_i(v_i)\) o valor que saí da \(i\)-ésima função de ativação. A função de ativação softmax \(g_i\) é definda por \[ \hat{y}_i = g_i(v_i) = \dfrac{e^{v_i}}{\sum_{k=1}^{K} e^{v_k}} \]

Veja que essa expressão supões a existência de \(k\) neurônios na camada de saída e não \(k=1\). O que acontece na prática é que o último neurônio na camada de saída é omitido do modelo, uma vez que o valor de \(\hat{y_k}\) pode ser obtido a partir dos valores das saídas dos outros \(k-1\) neurônuios.

Exemplo

Vamos usar o exemplo da base da segurado e tentar prever se um cliente é considerado de alto, médio ou baixo custo. Os clientes de baixo custo será aqueles com charges\(< 6.000\), os de custo médio serão aqueles com $6.000 $ charges\(\le 12.000\), e clientes de alto custo serão aqueles com charges\(> 12.000\).

Carregar pacotes necessários

library(caret)
library(tidyverse)
library(neuralnet)

Leitura da base de dados

base = read.csv2("insurance.csv",sep = ",",dec=".")
base = tibble::as_tibble(base)
base = base |> 
  mutate(custo = ifelse(base$charges < 6000,"baixo",
                        ifelse(base$charges > 12000,
                               "alto","medio")))
base = base |> select(-charges)

Divisão entre treino e teste

set.seed(123456789)
N = dim(base)[1]
indices_treino = caret::createDataPartition(1:N,p=0.7)[[1]]
base_treino = base[indices_treino,]
base_teste  = base[-indices_treino,]

Padronização das variáveis quantitativas

base_treino_ = base_treino |> mutate(age = scale(age),
                                     bmi = scale(bmi),
                                     children = scale(children)) 

Tratamento para as variáveis categóricas

matriz_treino_ <- model.matrix( ~ ., data = base_treino_)[,-1]

Ajuste dos modelos

modelo_1 = neuralnet(
  customedio + custobaixo ~ ., 
  data = matriz_treino_, 
  hidden = 1,
  linear.output = FALSE,
  err.fct = "ce")
plot(modelo_1,rep = "best")

modelo_2 = neuralnet(
  customedio + custobaixo ~ ., 
  data = matriz_treino_, 
  hidden = 2,
  linear.output = FALSE,
  err.fct = "ce",
  stepmax = 1e+06)
plot(modelo_2,rep = "best")

modelo_22 = neuralnet(
  customedio + custobaixo ~ ., 
  data = matriz_treino_, 
  hidden = c(2,2),
  linear.output = FALSE,
  err.fct = "ce", 
  stepmax = 10e6)
plot(modelo_22,rep = "best")

Previsão na base de treino

Agora a previsão não será mais um vetor de uma única coluna, e sim uma matriz com o número de colunas igual ao número de classes.

prev_treino_1 = modelo_1$net.result[[1]]
prev_treino_2 = modelo_2$net.result[[1]]
prev_treino_22 = modelo_22$net.result[[1]]

EC na base de treino

Para o caso do problema com mais de duas classes a expressão da entropia cruzada precisa ser generalizada.

\[ EC = - \dfrac{1}{N} \sum_{i=1}^N \sum_{k=1}^K y_{k,i}\ln(\hat{y}_{k,i}) \] sendo, \(N\) o número de instâncias (observações), \(K\) o número de classes, \(y_{k,i}\) a variável indicadora da classe \(k\), isto é, \(y_{k,i} = 1\) se a isntância \(i\) pertence a classe \(k\) e 0 caso contrário, e \(\hat{y}_{k,i}\) o valor de saída para a classe \(k\).

#real = uma matriz com N linhas e K colunas
#previsao = uma matriz com N linhas e K colunas
EC = function(real,previsao){
  N = dim(real)[1]
  K = dim(real)[2] 
  prev_classe_real = real*previsao
  prev_classe_real = apply(prev_classe_real, MARGIN = 1, FUN="sum")
  ec = -sum(log(prev_classe_real)/N)
  return(ec)
}

O comando $net.result[[1]] fornece a previsão para as duas classes com neurônios aparente. A previsão para a terceira classe, no caso a classe de alto custo, é tirada a partir do complementar da soma das classes já estimadas.

prev_treino_1 = prev_treino_1 |> cbind(1-apply(prev_treino_1, MARGIN = 1, FUN="sum"))

prev_treino_2 = prev_treino_2 |> cbind(1-apply(prev_treino_2, MARGIN = 1, FUN="sum"))

prev_treino_22 = prev_treino_22 |> cbind(1-apply(prev_treino_22, MARGIN = 1, FUN="sum"))
classe_real_treino = matriz_treino_[,c("customedio","custobaixo")]
classe_real_treino = classe_real_treino |> cbind(1-apply(classe_real_treino, MARGIN = 1, FUN="sum"))
EC_treino_1 = EC(classe_real_treino,prev_treino_1)
EC_treino_2 = EC(classe_real_treino,prev_treino_2)
EC_treino_22 = EC(classe_real_treino,prev_treino_22)
Entropia cruzada base de treino
Modelo_1 Modelo_2 Modelo_22
0.600308 0.261179 0.254207

Matriz de Confusão para a base de treino

custo_real = base_treino$custo
custo_real = as.factor(custo_real)
custo_estimado = apply(X = prev_treino_1,MARGIN = 1,FUN = "which.max")
custo_estimado =
  ifelse(custo_estimado==1,"medio",
         ifelse(custo_estimado==2,"baixo","alto"))
custo_estimado = as.factor(custo_estimado)
CMtreino1 = confusionMatrix(data = custo_estimado,reference = custo_real)
Matriz de Confusão para o Modelo 1 na base de treino
alto baixo medio
alto 314 0 298
baixo 35 285 6
medio 0 0 0
Medidas de qualidade da classificação para o Modelo 1 na base de treino
Sensitivity Specificity Pos Pred Value Neg Pred Value Precision Recall F1 Prevalence Detection Rate Detection Prevalence Balanced Accuracy
Class: alto 0.8997135 0.4940577 0.5130719 0.8926380 0.5130719 0.8997135 0.6534860 0.3720682 0.3347548 0.652452 0.6968856
Class: baixo 1.0000000 0.9372129 0.8742331 1.0000000 0.8742331 1.0000000 0.9328969 0.3038380 0.3038380 0.347548 0.9686064
Class: medio 0.0000000 1.0000000 NaN 0.6759062 NA 0.0000000 NA 0.3240938 0.0000000 0.000000 0.5000000
custo_estimado = apply(X = prev_treino_2,MARGIN = 1,FUN = "which.max")
custo_estimado = ifelse(custo_estimado==1,"medio",ifelse(custo_estimado==2,"baixo","alto"))
custo_estimado = as.factor(custo_estimado)
CMtreino2 = confusionMatrix(data = custo_estimado,reference = custo_real)
Matriz de Confusão para o Modelo 2 na base de treino
alto baixo medio
alto 283 0 0
baixo 34 285 6
medio 32 0 298
Medidas de qualidade da classificação para o Modelo 2 na base de treino
Sensitivity Specificity Pos Pred Value Neg Pred Value Precision Recall F1 Prevalence Detection Rate Detection Prevalence Balanced Accuracy
Class: alto 0.8108883 1.0000000 1.0000000 0.8992366 1.0000000 0.8108883 0.8955696 0.3720682 0.3017058 0.3017058 0.9054441
Class: baixo 1.0000000 0.9387443 0.8769231 1.0000000 0.8769231 1.0000000 0.9344262 0.3038380 0.3038380 0.3464819 0.9693721
Class: medio 0.9802632 0.9495268 0.9030303 0.9901316 0.9030303 0.9802632 0.9400631 0.3240938 0.3176972 0.3518124 0.9648950
custo_estimado = apply(
  X = prev_treino_22,
  MARGIN = 1,
  FUN = "which.max")
custo_estimado = 
  ifelse(custo_estimado==1,"medio",
         ifelse(custo_estimado==2,"baixo","alto"))
custo_estimado = as.factor(custo_estimado)
CMtreino22 = confusionMatrix(data = custo_estimado,reference = custo_real)
Matriz de Confusão para o Modelo 22 na base de treino
alto baixo medio
alto 284 0 0
baixo 33 285 6
medio 32 0 298
Medidas de qualidade da classificação para o Modelo 22 na base de treino
Sensitivity Specificity Pos Pred Value Neg Pred Value Precision Recall F1 Prevalence Detection Rate Detection Prevalence Balanced Accuracy
Class: alto 0.8137536 1.0000000 1.0000000 0.9006116 1.0000000 0.8137536 0.8973144 0.3720682 0.3027719 0.3027719 0.9068768
Class: baixo 1.0000000 0.9402757 0.8796296 1.0000000 0.8796296 1.0000000 0.9359606 0.3038380 0.3038380 0.3454158 0.9701378
Class: medio 0.9802632 0.9495268 0.9030303 0.9901316 0.9030303 0.9802632 0.9400631 0.3240938 0.3176972 0.3518124 0.9648950

Processamento na base de teste

base_teste_ = base_teste |> mutate(age = (age - mean(base_treino$age))/sd(base_treino$age) ,
                                   bmi = (bmi - mean(base_treino$bmi)/sd(base_treino$bmi)),
                                   children = (children - mean(base_treino$children))/sd(base_treino$children))
matriz_teste_ <- model.matrix( ~ ., data = base_teste_)

Previsão na base de teste

prev_teste_1 = (modelo_1 |> neuralnet::compute(matriz_teste_))$net.result
prev_teste_2 = (modelo_2 |> neuralnet::compute(matriz_teste_))$net.result
prev_teste_22 = (modelo_22 |> neuralnet::compute(matriz_teste_))$net.result
prev_teste_1 = prev_teste_1 |> cbind(1-apply(prev_teste_1, MARGIN = 1, FUN="sum"))

prev_teste_2 = prev_teste_2 |> cbind(1-apply(prev_teste_2, MARGIN = 1, FUN="sum"))

prev_teste_22 = prev_teste_22 |> cbind(1-apply(prev_teste_22, MARGIN = 1, FUN="sum"))

EC na base de teste

classe_real_teste = matriz_teste_[,c("customedio","custobaixo")]
classe_real_teste = classe_real_teste  |> cbind(1-apply(classe_real_teste , MARGIN = 1, FUN="sum"))

EC_teste_1 = EC(classe_real_teste,prev_teste_1)
EC_teste_2 = EC(classe_real_teste,prev_teste_2)
EC_teste_22 = EC(classe_real_teste,prev_teste_22)
Entropia cruzada base de teste
Modelo_1 Modelo_2 Modelo_22
0.5923592 Inf 6.972461

Matriz de Confusão para a base de teste

custo_real = base_teste$custo
custo_real = as.factor(custo_real)
custo_estimado = apply(X = prev_teste_1,MARGIN = 1,FUN = "which.max")
custo_estimado =
  ifelse(custo_estimado==1,"medio",
         ifelse(custo_estimado==2,"baixo","alto"))
custo_estimado = as.factor(custo_estimado)
CMteste1 = confusionMatrix(data = custo_estimado,reference = custo_real)
Matriz de Confusão para o Modelo 1 na base de teste
alto baixo medio
alto 138 0 103
baixo 5 144 10
medio 0 0 0
Medidas de qualidade da classificação para o Modelo 1 na base de teste
Sensitivity Specificity Pos Pred Value Neg Pred Value Precision Recall F1 Prevalence Detection Rate Detection Prevalence Balanced Accuracy
Class: alto 0.965035 0.5992218 0.5726141 0.9685535 0.5726141 0.965035 0.718750 0.3575 0.345 0.6025 0.7821284
Class: baixo 1.000000 0.9414062 0.9056604 1.0000000 0.9056604 1.000000 0.950495 0.3600 0.360 0.3975 0.9707031
Class: medio 0.000000 1.0000000 NaN 0.7175000 NA 0.000000 NA 0.2825 0.000 0.0000 0.5000000
custo_estimado = apply(X = prev_teste_2,MARGIN = 1,FUN = "which.max")
custo_estimado = ifelse(custo_estimado==1,"medio",ifelse(custo_estimado==2,"baixo","alto"))
custo_estimado = as.factor(custo_estimado)
CMteste2 = confusionMatrix(data = custo_estimado,reference = custo_real)
Matriz de Confusão para o Modelo 2 na base de teste
alto baixo medio
alto 131 0 24
baixo 8 144 29
medio 4 0 60
Medidas de qualidade da classificação para o Modelo 2 na base de teste
Sensitivity Specificity Pos Pred Value Neg Pred Value Precision Recall F1 Prevalence Detection Rate Detection Prevalence Balanced Accuracy
Class: alto 0.9160839 0.9066148 0.8451613 0.9510204 0.8451613 0.9160839 0.8791946 0.3575 0.3275 0.3875 0.9113494
Class: baixo 1.0000000 0.8554688 0.7955801 1.0000000 0.7955801 1.0000000 0.8861538 0.3600 0.3600 0.4525 0.9277344
Class: medio 0.5309735 0.9860627 0.9375000 0.8422619 0.9375000 0.5309735 0.6779661 0.2825 0.1500 0.1600 0.7585181
custo_estimado = apply(
  X = prev_teste_22,
  MARGIN = 1,
  FUN = "which.max")
custo_estimado = 
  ifelse(custo_estimado==1,"medio",
         ifelse(custo_estimado==2,"baixo","alto"))
custo_estimado = as.factor(custo_estimado)
CMteste22 = confusionMatrix(data = custo_estimado,reference = custo_real)
Matriz de Confusão para o Modelo 22 na base de teste
alto baixo medio
alto 138 64 64
baixo 2 80 12
medio 3 0 37
Medidas de qualidade da classificação para o Modelo 22 na base de teste
Sensitivity Specificity Pos Pred Value Neg Pred Value Precision Recall F1 Prevalence Detection Rate Detection Prevalence Balanced Accuracy
Class: alto 0.9650350 0.5019455 0.5187970 0.9626866 0.5187970 0.9650350 0.6748166 0.3575 0.3450 0.665 0.7334902
Class: baixo 0.5555556 0.9453125 0.8510638 0.7908497 0.8510638 0.5555556 0.6722689 0.3600 0.2000 0.235 0.7504340
Class: medio 0.3274336 0.9895470 0.9250000 0.7888889 0.9250000 0.3274336 0.4836601 0.2825 0.0925 0.100 0.6584903

Comparação entre diferentes modelos de classificação

Base de treino

Medida F1 para as três classes na base de treino
M1 M2 M22
Class: alto 0.6534860 0.8955696 0.8973144
Class: baixo 0.9328969 0.9344262 0.9359606
Class: medio NA 0.9400631 0.9400631
Tabela 1: Comparação entre modelos de classificação de acordo com a medida F1 na base de treino

Tabela 2: Comparação entre modelos de classificação de acordo com a medida F1 na base de treino

Base de teste

Medida F1 para as três classes na base de teste
M1 M2 M22
Class: alto 0.718750 0.8791946 0.6748166
Class: baixo 0.950495 0.8861538 0.6722689
Class: medio NA 0.6779661 0.4836601
Tabela 3: Comparação entre modelos de classificação de acordo com a medida F1 na base de teste

Tabela 4: Comparação entre modelos de classificação de acordo com a medida F1 na base de teste