RECONHECIMENTO DE DÍGITOS ESCRITOS A MÃO – PARTE 3

Na Parte 1 desse post (que já publiquei faz um tempão!) eu fiz uma classificação de imagens de dígitos escritos a mão usando o k-nn (algoritmo dos vizinhos mais próximos) usando as informações das imagens sem nenhum tipo de tratamento, isto é, sem nenhum método de preparação. Como foi mostrado, o k-nn só foi capaz de classificar razoavelmente bem com com k = 1 e conseguiu uma acurácia de apenas 78%, algo muito distante do que ainda pode ser conseguido.

Na Parte 2 eu trabalhei com um método de redução de dimensionalidade, o PCA, e também foram explorados diversos outros classificadores, como o Random Forest, o SVM e etc. O resumo dos resultados foi o seguinte:

  • k-nn com k = 1: 84%
  • Regressão linear: 14%
  • Regressão Logística Multinomial: 64%
  • Árvores de decisão: 24%
  • RandomForest: 72%

E a conclusão geral foi que não foi possível bater o k-NN ou ainda mais chegar aos resultados reportados na literatura, superiores a 95% de acurácia na tarefa. Como foi mencionado anteriormente, os possíveis problemas foram:

  1. Conjunto pequeno de imagens.
  2. Modelos com parâmetros default.

Assim, nessa última parte vou mostrar como é possível treinar melhores modelos com muito mais dados e como é possível melhorar a performance dos algoritmos com melhores hiperparâmetros. Outro ponto importante que eu queria mostrar também é o uso do pacote caret para automatizar diversas tarefas desse processo.

1.Dados e visualização

Como eu comentei antes, vamos nessa parte tentar utilizar um conjunto maior de imagens. Para tanto, ao invés desse pequeno conjunto de teste e treino que foi fornecido na Parte 1, vamos utilizar um conjunto muito maior de images de dígitos escritos a mão, o famoso data set MNIST. Se você verificar nesse link, você vai encontrar diversas informações com respeito a esse conjunto de dados e também você pode baixa-lo e utilizar nas análises que se seguem, caso queira reproduzir o que você está vendo aqui. ENTRETANTO, para poupar o seu trabalho e o meu, ao invés de pegar os dados diretamente deste site, vamos utilizar esse mesmo conjunto de dados já tratado e preparado pela equipe do Kaggle. A vantagem é que o arquivos já estão no formato csv e não será mais necessária a etapa de preparação realizada na Parte 1 e na Parte 2 desta série. Outra vantagem é que após você rodar estes modelos, se você quiser, você pode submeter seus resultados no Leaderboard para experimentar como funciona esse site de competição.

Na Parte 2, como eu peguei diretamente as imagens e converti em uma matriz, agora você poderia ficar confuso e se perguntar: mas e aí, como são estas imagens? como eu vou ver se já está em csv? Para você não ficar com dúvida, vamos “imprimir” as imagens.

############ Explorando as imagens ###############################
## Contando o número de imagens por dígito
barplot(table(treino$label), ylim = c(0,5000))

## Transformando em uma matriz
treino <- as.matrix(treino)

## Imprimindo uma imagem
matriz_imagem <- matrix(treino[1000,-1], ncol = 28)
matriz_imagem <- matriz_imagem[,28:1] ## invertendo a imagem
image(1:28, 1:28, matriz_imagem, col = c('white', 'black'))

digito4

Veja que eu peguei a primeira linha do conjunto de treino, transformei em uma matriz e imprimi a matriz como uma imagem. Nesse caso é o dígito 4. Assim, apesar de agora você estar usando um arquivo em csv preparado eles fizeram a mesma coisa que eu fiz anteriormente. Se você quiser entender melhor como cada imagem virou uma linha dessa tabela dá uma olhada na Parte 1 dessa série. Só para mostrar que os dígitos estão ok, eu vou imprimir uma “imagem média”, onde em cada imagem eu tenho um valor médio em cada píxel considerando todas as imagens do conjunto de dados.

## Plotando uma imagem média para cada dígito
## Definindo uma escala de cor, indo do branco ao preto
colors <- c('white','black')
cus_col <- colorRampPalette(colors=colors)

## Plot de cada imagem média
## Divindo a tela
png('todos_digitos.png')
par(mfrow=c(4,3),pty='s',mar=c(1,1,1,1),xaxt='n',yaxt='n')

## Criando um array para armazenar as matrizes de cada imagem média
all_img <- array(dim=c(10,28*28))

## Recuperando todas as imagens por dígito e calculando a média
for(di in 0:9) {
  print(di)
  all_img[di+1,] <- apply(treino[treino[,1]==di,-1],2,sum)
  all_img[di+1,] <- all_img[di+1,]/max(all_img[di+1,])*255
  
  z<-array(all_img[di+1,],dim=c(28,28))
  z<-z[,28:1] ##right side up
  image(1:28,1:28,z,main=di,col=cus_col(256))
}

todos_digitos

Até há uma certa variação (por isso que há uma sombra) mas no geral os mesmos píxels tem uma intensidade maior considerando cada dígito diferente que foi escrito na imagem. Isso nos leva a crer que os modelos devem conseguir distinguir um dígito do outro.

2. Preparação com PCA

Depois que você baixar os arquivos train.csv e test.csv do Kaggle já podemos efetuar a leitura dos arquivos e a preparação por meio do PCA. O que vamos fazer é aplicar o PCA e retendo somente o número de componentes necessário para alcançar 95% da variância total. Os detalhes sobre isso eu discuti na Parte 2.

## Leitura dos conjuntos de dados de treino e de teste
treino = read.csv('train.csv', header = T)
teste = read.csv('test.csv', header = T)

############ APlicação do PCA ####################################
## Obtendo componentes principais
pc <- prcomp(treino[,-1])
treino_pc <- pc$x

## Obtendo as variâncias acumuladas
vars = pc$sdev^2
props = vars/sum(vars)
varAcum = cumsum(props)
which.min(varAcum < 0.90)

## Aplicando a rotação nos dados de teste
teste_pc <- predict(pc, newdata = teste)

## Salvando treino e teste com PCA
save(treino_pc, file = 'treino_pc.rda')
save(teste_pc, file = 'teste_pc.rda')

eu costumo salvar os arquivos após cada etapa de preparação de forma a não precisar realizar o processo posteriormente. Outro ponto importante é que salvando os objetos no formato nativo do R, caso você precise recarregar os dados, o processo é muito mais rápido que a leitura em csv.

3. Árvore de Decisão

Como uma primeira tentativa, vamos utilizar o algoritmo para árvores de decisão do pacote rpart. Vamos utilizar todas as PC’s e treinar a árvore em 3/4 dos dados. O teste será realizado no 1/4 que foi separado.

#################################################################
## Teste com árvore de decisão
library(rpart)

## Separando o conjunto treino em dois para avaliação
set.seed(1)
inTrain <- createDataPartition(treino$label, p = 3/4, list = F)
train <- treino_pc[inTrain,]
evaluation <- treino_pc[-inTrain,]

## Data frame de treino e teste
treino_arvore = as.data.frame(cbind(train[,1:784]))
treino_arvore$classes = as.factor(treino$label[inTrain])

## Conjunto de teste para avaliação
teste_arvore = as.data.frame(evaluation[,1:784])

## Criando uma árvore
arvore <- rpart(classes ~ ., data = treino_arvore)

## Calculando a matriz de confusão
confusionMatrix(predict(arvore, teste_arvore, type = 'class'), treino$label[-inTrain])

e o resultado da matriz de confusão:

Confusion Matrix and Statistics

          Reference
Prediction   0   1   2   3   4   5   6   7   8   9
         0 637   2  13  20   3  66  17  12   0   2
         1   0 995   9   7  28   7   3  75   6  53
         2  60  54 741  61  30  97 130  16  93  10
         3 185  45 111 803  13 270 106  29  89  30
         4   3   0  16  10 796  73  16  82  16 471
         5  86  13  46  88  36 341  31  61 185  49
         6  24  16  47  45  21  40 725   4  13  43
         7  17   0   4   2  20  16   2 652  10  85
         8  18  46  40  27  11  60  12  18 532  13
         9  14   0   6  10  74   6  13 103  57 305

Overall Statistics
                                         
               Accuracy : 0.6217         
                 95% CI : (0.6124, 0.631)
    No Information Rate : 0.1115         
    P-Value [Acc > NIR] : < 2.2e-16      
                                         
                  Kappa : 0.5796         
 Mcnemar's Test P-Value : < 2.2e-16

mostra que o desempenho ainda está longe do satisfatório. Com uma acurácia global de apenas 62% estamos ainda muito longe da meta de 95%. Veja que utilizamos a estratégia holdout, e com relação a Parte 2 desta série a única mudança é o fato de estarmos trabalhando com imagens com mais resolução e um conjunto maior. Parece que isso ainda não é o suficiente, assim vamos explorar outros algoritmos e vamos utilizar métodos de validação cruzada para encontrar os melhores hiperparâmetros.

4. RandomForest

O randomforest é um dos algoritmos de machine learning mais utilizados na indústria. Seu sucesso advém do fato de ser robusto, facilmente paralelizável e apresentar um desempenho muito bom em uma grande quantidade de problemas diferentes. Assim, vamos experimentar esse classificador procurando ajustar os melhores hiperperâmetros por validação cruzada. No caso do RF temos que definir qual o melhor m, um parâmetro que determina quantas variáveis são sorteadas na escolha do split em cada nó, de cada árvore de decisão do comitê. Se não ficou claro para você o que significa este hiperparâmetro não tem problema, não é difícil encontrar material onde você pode entender os detalhes do RF. O importante aqui é você entender que o valor do hiperparâmetro será escolhido com base no próprio conjunto de dados, utilizando validação cruzada.

#################################################################
## Teste com RandomForest
library(randomForest)

## Separando o conjunto treino em dois para avaliação
set.seed(1)
inTrain <- createDataPartition(treino$label, p = 3/4, list = F)
train <- treino_pc[inTrain,]
evaluation <- treino_pc[-inTrain,]

## Data frame de treino e teste, aqui retendo somente 160 PC's, equivalente a 95% de ## variância.
treino_arvore = as.data.frame(cbind(train[,1:160]))
treino_arvore$classes = as.factor(treino$label[inTrain])

## Conjunto de teste para avaliação
teste_arvore = as.data.frame(evaluation[,1:160])

## Modelagem
fitControl <- trainControl(method = "oob", verboseIter = T,
                           
                           ## Estimate class probabilities
                           classProbs = F)

set.seed(825)
rfFit <- train(classes ~ ., data = treino_arvore, verbose = T,
                method = "rf",
                trControl = fitControl,
                tuneLength = 8,
                metric = "Accuracy")
save(rfFit, file = 'rfFit.rda')
                
## Calculando a matriz de confusão
confusionMatrix(predict(rfFit, teste_arvore, type = 'raw'), treino$label[-inTrain])

e a matriz de confusão:

Confusion Matrix and Statistics

          Reference
Prediction    0    1    2    3    4    5    6    7    8    9
         0 1048    0    3    0    1    1    1    0    0    1
         1    0 1130    2    0    1    1    0    2    3    0
         2    0    3 1021   10    4    2    1    6    2    0
         3    0    1    5 1073    3   12    0    1    8   11
         4    1    0    6    0  970    4    1    1    4   18
         5    0    0    0    8    2  932    6    0    4    6
         6    6    1    3    2    5    3 1023    1    2    0
         7    1    1    4    4    0    0    0 1077    3    7
         8    0    0    5    4    5    2    1    2  967    4
         9    2    2    3    2   11    2    0    1    2 1020

Overall Statistics
                                          
               Accuracy : 0.9774          
                 95% CI : (0.9744, 0.9802)
    No Information Rate : 0.1084          
    P-Value [Acc > NIR] : < 2.2e-16       
                                          
                  Kappa : 0.9749

e enfim chegamos a 97%!

5. SVM (Máquina de vertores de suporte)

O SVM é um algoritmo do tipo “caixa preta”. O princípio por detrás do algoritmo é criar hiperplanos separadores em dimensões maiores do que as presentes no conjunto de dados. A ideia é que se os pontos são linearmente separáveis, isto é, se um hiperplano como fronteira de decisão conseguiria separar completamente as classes, então o SVM é um método que pode ser utilizado para encontrar esse hiperplano. ENTRETANTO, ocorre que muitos problemas não são linearmente separáveis, e ainda que fossem não valeria a pena usar o SVM. Quando o problema não é linearmente separável o SVM, de uma certa forma, projeta os dados em um espaço onde é possível criar um hiperplano separador. Também, ele aceita um certo grau de “impurezas” dentro das fronteiras de decisão. Enfim, é um algoritmo do tipo “caixa preta”, que não tem origem na estatística já que é basicamente um algoritmo de otimização. Mas o fato é que o SVM apresenta resultados muito bons e uma grande quantidade de problemas e vamos ver isso aqui nesse teste.

#################################################################
## Teste com SVM RBF
## Carregando os pacotes
library(caret)

## Modelagem
fitControl <- trainControl(method = "cv", verboseIter = T,
                           
                           ## Estimate class probabilities
                           classProbs = F)

set.seed(825)
svmFit <- train(classes ~ ., data = treino_arvore,
                method = "svmRadial",
                trControl = fitControl,
                preProc = c("center", "scale"),
                tuneLength = 8,
                metric = "Accuracy")
save(svmFit, file = 'svmFit2.rda')

## Calculando a matriz de confusão
confusionMatrix(predict(svmFit, teste_arvore, type = 'raw'), treino$label[-inTrain])

e avaliando no conjunto de avaliação:

Confusion Matrix and Statistics

          Reference
Prediction    0    1    2    3    4    5    6    7    8    9
         0 1046    0    3    0    1    3    5    1    3    6
         1    1 1128    1    2    2    0    1    3    2    1
         2    0    3 1015   13    4    3    1   15    4    1
         3    1    1    4 1058    0   13    0    1    7    9
         4    2    2   10    1  972    1    4   10    1   21
         5    0    1    2   12    0  924    7    0    3    5
         6    5    0    2    1    5   10 1010    0    4    0
         7    0    0    5    7    0    0    0 1051    0   13
         8    3    2    9    4    0    2    5    1  967    3
         9    0    1    1    5   18    3    0    9    4 1008

Overall Statistics
                                          
               Accuracy : 0.9696          
                 95% CI : (0.9661, 0.9728)
    No Information Rate : 0.1084          
    P-Value [Acc > NIR] : < 2.2e-16       
                                          
                  Kappa : 0.9662

e novamente conseguimos algo em torno de 97%. Para ser honesto, depois que eu criei os modelos com os melhores hiperparâmetros, submetendo no Kaggle o SVM supera os 98%. Foi o modelo com melhor desempenho nessa tarefa.

Conclusão

Acho que depois da Parte 1, Parte 2 e Parte 3 (esta aqui!) você viu como se trabalha com classificação de imagens, como se prepara esse tipo de dado e como é possível alcançar altíssima acurácia utilizando os modelos de machine learning que você encontra por aí. Se você for reproduzir os exemplos, fique ciente que a etapa de modelagem pode demorar muito. No meu caso alguns destes modelos demoraram mais de 8 horas para o ajuste com os melhore hiperparâmetros! Outro ponto que eu não abordei é como o caret seleciona os melhores hiperparâmetros por validação cruzada. Isso vou deixar para falar com maior detalhe em outra oportunidade. Também gostaria de salientar que uma modelagem como essa é algo tipicamente diferente do que se espera em uma análise estatística tradicional. Aqui não estávamos interessados em inferência, mas sim em produzir modelos com o maior poder preditivo possível. Em casos como esse, trabalhar com métodos “caixa preta” não é em si um problema. O ponto principal é ter certeza que seu modelo apresentará um bom desempenho no futuro, com novos dados. POR FIM, esses modelos não são o estado da arte nesta tarefa, já que com deep learning é possível passar dos 99% de acurácia.

COMMENTS

Comments are closed.