Aprendizado de Máquina com Weka
Este texto é parte da disciplina de Fundamentos de Inteligência Artificial do Programa de Pós-Graduação em Computação da Universidade Federal de Pelotas.
Neste texto, vamos introduzir alguns conceitos fundamentais de Aprendizado de Máquina ao longo de uma aplicação por meio do Weka. Assume-se que o texto anterior foi já lido na integra. O Weka é uma biblioteca Java com diversos modelos e métodos de aprendizado de máquina, oferecendo uma interface gráfica para o uso direto da biblioteca. A interface é adequada para prototipações ou testes de conceito quando se trabalha com dados tabulares e problemas de classificação, regressão ou clusterização, mas para ambientes de produção assume-se que se utilizará a biblioteca diretamente ou outros meios.
O primeiro passo é baixar e instalar o Weka. Isso pode ser feito neste link. Na sequência, é útil já baixarmos um conjunto de dados (dataset) para experimentação. Vamos utilizar o conjunto Titanic (do OpenML, um bom repositório de datasets). Este link é para a versão CSV, que é um arquivo texto que pode facilmente ser visualizado e importado para uso. A tabela completa também se encontra abaixo.
Este dataset contém informações de passageiros do Titanic. Cada linha é um passageiro e cada coluna contém alguma informação como nome, sexo, idade, tipo de passagem etc. De especial interesse, temos um campo (survived) que indica se o passageiro sobreviveu (1) ou não (0) ao acidente. Nossa tarefa aqui será inferir se um passageiro sobreviveu ou não a partir das demais informações disponíveis sobre ele.
Este formato é chamado tabular ou estruturado e está essencialmente pronto para consumo por uma aplicação tradicional de aprendizado de máquina. Cada linha é um exemplo, a coluna survived é nosso atributo-alvo, ou classe, e as demais colunas são nossos atributos. Queremos treinar um modelo que aprenda um mapeamento atributos→ classe que seja consistente com os exemplos conhecidos — i.e. que faça o mapeamento correto para os exemplos dados. Supomos, pela hipótese indutiva de aprendizado, que o modelo se sairá bem em novos exemplos se for consistente com os conhecidos: por exemplo, bizarramente um novo passageiro é descoberto mas não se sabe se ele sobreviveu; utilizaríamos nosso modelo para inferir esta informação desconhecida.
Note que apesar de estarmos definindo que o atributo-alvo é a coluna survived, não há nada inerente ao problema que exija que se encontre este mapeamento. Podemos tentar inferir qualquer coluna da tabela, com maior ou menor dificuldade, a partir das demais. De fato, encorajo a tentarem ao final.
Antes de carregar os dados no Weka, um bom primeiro passo sempre é tentar observar o dataset e pelo menos vários exemplos para ter uma boa ideia do que nos espera. Neste ponto é essencial compreender a semântica de cada atributo — o que a informação representa? A maior parte dos nomes dos atributos é relativamente auto-explicativa, mas é importante buscar as definições exatas. Aqui temos as definições das colunas no dataset, mas situações mais reais envolvem desde compreender construções SQL que geraram os dados até conversar com especialistas ou resgatar registros históricos. Qualquer modelagem começa compreendendo os dados que se tem (ou se deseja ter).
O dataset possui 1309 exemplos e 14 colunas. Como uma das colunas é nossa classe, temos 13 atributos. Atributos podem ser de muitos tipos, desde Verdadeiro/Falso até imagens, sons e texto. Alguns tipos são bastante comuns e importantes de serem tratados adequadamente. Um atributo numérico é um cujos valores são, você pode adivinhar, números. É o caso do atributo age. Já um atributo categórico pode assumir apenas uma quantidade finita (e relativamente pequena) de valores definidos, como no caso da coluna sex. Alguns atributos podem ser categóricos apesar de conterem números: pclass é representado com 1, 2 ou 3, mas apenas estes três valores são possíveis, configurando o tipo categórico (para pensar: e age?). Atributos categóricos podem ainda ser ordinais ou nominais. Um atributo é ordinal quando existe uma ordem natural dos seus valores — pclass pode ser considerado ordinal neste caso. Um atributo nominal não possui esta ordem — sex é nominal, pois não possui uma ordem natural entre os valores possíveis. Estas coisas são importantes para poder selecionar o modelo adequado posteriormente.
O Weka nos ajuda com a próxima etapa: a análise exploratória dos dados. Uma vez compreendida a semântica dos dados, é importante dar atenção a como os dados se apresentam. Por exemplo, entendemos que age é a idade em anos da pessoa, mas qual é a menor e maior idades presentes no dataset? Como se distribuem? Nesta etapa poderíamos, por exemplo, descobrir que uma coluna aparentemente numérica é na verdade categórica, permitindo adaptar os dados para representarem melhor a realidade.
É, então, hora de carregar os dados no Weka. Se tentarmos carregar o CSV diretamente, o que é possível, teremos um problema: por padrão o Weka tenta inferir o tipo dos dados em cada coluna e números são tratados como atributos numéricos mesmo se forem categóricos. É possível alterar o tipo diretamente dentro do Weka, mas para este dataset temos uma solução mais prática: baixar a versão ARFF do dataset. O ARFF é um formato de arquivo que, além dos dados, contém o tipo de cada atributo.
De posse do arquivo ARFF, rodamos o Weka e nos deparamos com o Weka GUI Chooser. O Weka é, na verdade, uma coleção de aplicações. Estamos interessados no primeiro, o Explorer, então entramos nele.
A janela que se abre é o Weka Explorer. Esta é a aplicação que permite explorar e modelar os dados com uma interface gráfica razoável (ainda que com toda cara de aplicação Java clássica do final dos anos 90).
Observe as abas no topo da janela. Elas oferecem diferentes funcionalidades. A chamada Preprocess, selecionada inicialmente por padrão, é a que permite carregar e transformar os dados que serão utilizados pelas demais abas. Aqui apenas usaremos esta e a aba Classify, mas as demais permitem trabalhar com clusterização (Clustering), criação de regras de associação (Associate), seleção de atributos (Select attributes) e visualização dos dados (Visualize).
Para carregar um arquivo, clica-se no botão "Open file…" e, utilizando a janela de diálogo tipicamente inconveniente do Java, escolhe-se o arquivo (lembre-se de carregar o arquivo ARFF e não o CSV).
Agora diversas partes da interface foram habilitadas. Começando do topo e da esquerda, temos Current relation. Ali podemos ver o número de exemplos (o Weka chama de instância — Instances), 1309, e o número de atributos, 14. Logo abaixo, temos a lista dos atributos com a possibilidade de remover alguns caso desejado.
Selecionando algum atributo, um resumo sobre ele aparece na direita. No quadro superior da direita, podemos ver o tipo do atributo (na imagem, Numeric), se há dados faltando (em %), quantos valores distintos existem (Distinct) e quantos valores aparecem uma única vez (Unique). Logo abaixo, um resumo de algumas estatísticas (mínimo, máximo, média e desvio padrão), mas esta área se altera dependendo do tipo de atributo — tente selecionar um atributo categórico, como sex, e o resumo passa a ser uma contagem de cada valor. Para tipos texto, como name, não há resumo. Logo abaixo, há a opção de selecionar uma classe (i.e. o atributo-alvo). Por padrão o Weka seleciona a última coluna, o que é incorreto no nosso exercício. Deve ser selecionado, então, o atributo survived. Aproveite e também já selecione o atributo sex no painel da esquerda.
O gráfico de barra embaixo da caixa de seleção de classe oferece uma contagem do atributo selecionado no painel da esquerda em função da classe selecionada. O que vemos ali é que de todas 466 mulheres, a maior parte sobreviveu (em vermelho — clique no atributo survived para ver o que cada cor indica). Já entre os 843 homens, uma minoria sobreviveu. Isto já é uma indicação de que o atributo sex é um indicador de sobrevivência. Você pode explorar atributo por atributo, ou clicar em Visualize All para ver todos.
Vamos adiante e tentar treinar um classificador sobre estes dados. Clicando na aba "Classify" no topo, passamos para a seção onde podemos treinar e avaliar modelos.
Novamente, começamos do topo. A área Classifier permite escolher o modelo. O nome não é adequado, pois o Weka suporta também problemas de regressão. O botão Choose permite selecionar o modelo (chegaremos lá), enquanto o nome do modelo selecionado e seus parâmetros aparecem logo ao lado (ZeroR está selecionado por padrão).
Logo abaixo, temos Test Options, onde selecionamos que dados vamos usar para treinar e avaliar o modelo treinado. Temos quatro opções aqui. A primeira, Use Training set, utiliza todos os dados disponíveis para treinar o modelo e usa os mesmos dados para avaliá-lo. Ainda que esta modalidade tenha usos, a avaliação que ela retorna será excessivamente otimista. O motivo disto é que ao avaliar um modelo usando dados que foram utilizados no seu treinamento não estamos avaliando a capacidade de generalização do modelo, apenas sua capacidade de decorar os dados. Considere que um mero banco de dados, utilizando este método de treino e avaliação, conseguiria classificar perfeitamente todos os exemplos.
Este é o motivo pelo qual professores não colocam na prova os mesmos exercícios vistos em aula. Se o fizessem, estariam avaliando não a capacidade de aprendizado do aluno, mas sua capacidade de decorar exercícios passados. Quando um modelo decora os dados de treinamento, chamamos de sobreajuste ou overfitting do modelo.
Logo, uma melhor ideia é separar os dados em pelo menos dois conjuntos. Esta é a opção Percentage split, onde especificamos um valor que indica como a divisão é feita. Por padrão, esta valor é 66%, indicando que 2/3 dos dados serão utilizados para treinamento e 1/3 será reservado para teste. Assim, durante o treinamento o modelo jamais verá os dados de teste. Da mesma forma, o teste jamais será feito sobre os dados de treinamento.
Há uma troca quando adotamos esta abordagem. Por um lado, gostaríamos de disponibilizar a maior quantidade possível para o modelo aprender. Porém, isso significa ter menos dados para avaliação, o que implica uma menor confiança nesta avaliação (considere: se deixarmos apenas UM exemplo de teste, o que o modelo acertar ou errar este único exemplo nos diz exatamente?). A troca é entre um modelo melhor e uma avaliação melhor do modelo. Isso apenas não é um grande problema quando temos muitos dados.
Quando temos poucos dados, uma solução é o que o Weka chama de Cross-validation, ou validação cruzada. A terminologia não é bem adequada, já que o tipo de teste feito com Percentage split é também um tipo de validação cruzada. De qualquer forma, nesta metodologia indicamos um valor inteiro K e o dataset é dividido em K partições (por padrão, K=10). Então, treinamos o modelo K vezes, sempre tirando uma partição para servir de teste, com as K-1 restantes usadas para treino. Com isso, garantimos que todos exemplos eventualmente participam tanto do treino como do teste e o modelo é treinado com uma quantidade maior de dados por vez. Tipicamente queremos usar o maior K possível (qual o maior K teoricamente possível?), mas para modelos complexos o custo computacional pode ser considerável e valores entre 3 e 20 são típicos na literatura.
Vamos manter a opção de teste como validação cruzada e K=10. Logo abaixo desta área, temos a seleção do atributo-alvo. Infelizmente a seleção feita na aba anterior não se transfere para essa (🤷♂️) e temos que novamente selecionar a classe survived. Façamos isso e o botão Start deve se habilitar. Vamos adiante clicando no botão e algo como a tela abaixo deve ser visível:
O que fizemos foi rodar o modelo ZeroR, usando cross-validation como metodologia de treino e avaliação e classe survived. Tudo que o modelo ZeroR faz é determinar qual é a classe majoritária (i.e. a classe que possui mais exemplos nos dados) e sempre dar como resultado essa classe, para qualquer entrada. Para tarefas de classificação, este é um ótimo baseline — se seu modelo não consegue se sair melhor que o ZeroR é por que nada está sendo aprendido.
A área Classifier Output mostra o resultado da execução, incluindo a avaliação do modelo de acordo com a metodologia escolhida. Role pelo texto para ver a totalidade das informações, mas as principais estão na imagem acima. São três seções principais. A primeira, no topo da área na imagem, dá um resumo da avaliação do modelo com métricas globais — i.e. sobre todas as classes. Vemos que 61,8% das instâncias (exemplos) foram corretamente classificadas. Esta métrica é conhecida como acurácia e é calculada contando todos acertos e dividindo pelo número total de exemplos. De forma complementar, 38,2% foram incorretamente classificadas. As linhas logo abaixo representam outras estatísticas sobre o resultado e não vamos lidar com elas agora.
A próxima seção é a Detailed Accuracy By Class. Nesta seção temos uma linha por classe e uma linha adicional com médias. Cada coluna demonstra uma métrica para cada classe. De interesse aqui:
- TP Rate: True Positive Rate, ou Taxa de Positivos Verdadeiros. É um valor de 0 a 1 que indica, de todos os exemplos classificados pelo modelo como pertencendo à classe da linha em análise (os chamados exemplos positivos, em contraponto aos exemplos negativos que compõem todas classes que não a sendo analisada), quantos foram classificados corretamente. Por exemplo, a primeira linha é relativo a classe "0" (i.e. não sobreviveu). Temos TP Rate = 1.000, indicando que todos exemplos classificados como 0 eram de fato 0.
- FP Rate: False Positive Rate, ou Taxa de Falsos Positivos. De forma análoga a anterior, este valor indica a fração de exemplos que foram falsos positivos —um exemplo é um falso positivo se o modelo indicou ser um exemplo positivo mas não era. O FPR é calculado pegando todos exemplos negativos que o modelo indicou serem positivos e dividindo pelo número de exemplos negativos. Como nosso modelo dá "0" como resposta sempre, ele classificará incorretamente todos 500 exemplos rotulados como "1", resultando no FPR=1 observado.
- Precision: indica o quão preciso é o modelo, definido como sendo o número de exemplos classificados corretamente para esta class (positivos verdadeiros) dividido pelo número total de exemplos classificados como positivos. Uma alta precisão indica que há poucos falsos positivos. Aqui temos 0,618 de precisão pois todos exemplos são classificados como "0" (o denominador é a totalidade dos exemplos de teste), mas apenas 618 deles são de fato da classe "0".
- Recall: ou Revocação, é complementar à precisão e indica, entre todos os exemplos que são de fato positivos, quantos foram classificados como tal pelo modelo. Novamente, o modelo sempre resulta em "0", logo todos exemplos "0" são classificados corretamente, resultando em um Recall=1.
- F-Meaure: ou F1-Score, é uma média harmônica entre Precision e Recall, dado por 2*(precision*recall)/(precision+recall). É uma forma de combinar as duas métricas em uma única, algo útil para criar rankings.
As demais métricas ficam para outro momento, mas vale a pena observar a segunda linha, para a classe "1", e compreender por que os valores são os que aparecem ali (e.g. por que a precisão para a classe "1" é zero?).
Cada métrica captura algum aspecto do desempenho do modelo, e nenhuma única métrica totalmente permite compreender o quão bom (ou ruim) é o modelo. Por exemplo, se olharmos apenas para acurácia, 61,8%, podemos pensar que não é muito ruim — o raciocínio usualmente envolve pensar, incorretamente, que havendo apenas duas opções qualquer coisa acima de 50% já é melhor do que simplesmente chutar um valor, um indicativo de aprendizado. É incorreto pois as classes aqui são desbalanceadas: há mais sobreviventes do que mortos, logo escolha aleatória resultará em mais sobreviventes sendo classificados corretamente. Se 99% dos exemplos pertencerem a uma classe, podemos obter 99% de acurácia trivialmente e isso não significará que o modelo é bom. Da mesma forma, se olharmos apenas o recall para a classe "0" (que é igual a 1), podemos acabar concluindo que o modelo é perfeito.
Estas múltiplas métricas tornam a comparação entre modelos não-trivial. Se treinarmos dois modelos e eles obtiverem resultados conflitantes (talvez um tenha maior precisão, enquanto outro tem maior recall), teremos dificuldade em decidir qual o melhor. Para criar rankings de modelos e algoritmos, com alguma frequência se utiliza uma única métrica de interesse. Acurácia é comum em problemas balanceados ou multi-classe. Para problemas binários desbalanceados, F1-Score e ROC Area são comuns. Ainda que seja aceitável utilizar uma métrica única, é importante reconhecer que elas frequentemente escondem detalhes importantes dos resultados. No mínimo se deseja utilizar sempre que possível um par de métricas, como precisão e recall ou especificidade e sensibilidade.
Por fim, a última seção da área de resultados é a confusion matrix (matriz de confusão). É incerto de onde veio o termo "confusão", ou por que ainda usamos este termo, mas tudo indica que é uma herança da área psicologia. A essência da matriz de confusão é contabilizar com precisão os erros e acertos do modelo. Ela é uma matriz numérica, onde os números são contagens de exemplos em diferentes categorias. Para um problema binário (com duas classes), ela é interpretada como abaixo:
Mapeando esta tabela para o resultado dado pelo Weka, vemos que temos 809 Verdadeiros Positivos, 500 Falsos Positivos, zero Falsos Negativos e zero Verdadeiros Negativos. Note que para este dataset, um exemplo é considerado Positivo se ele é "0", o que pode ser contrário à intuição.
Todas métricas mencionadas antes podem ser derivadas da matriz de confusão e esta fornece um panorama detalhado que nos permite observar com precisão onde os erros estão sendo cometidos. O ideal é que a contagem se concentre na diagonal principal da matriz. Para múltiplas classes é útil utilizar cores mapeadas para as contagens, como abaixo.
Sempre deve-se olhar a matriz de confusão para problemas de classificação. Métricas apenas contam parte da história. Um exemplo de onde a matriz de confusão ajuda: verificar, em um problema com múltiplas classes, quais são confundidas pelo modelo (talvez este seja o contexto da origem do nome 🤔).
Treinando um modelo
Ainda que tecnicamente já tenhamos treinado um modelo, este é excessivamente simples. Vamos tentar algo mais sofisticado. Clicando no botão Choose na área Classifier no topo, temos uma lista de modelos disponíveis. Outros podem ser instalados facilmente no Weka. Os modelos estão separados em categorias representadas pela estrutura de diretórios.
O Weka já seleciona os modelos que são aplicáveis aos dados, considerando os tipos dos atributos e da classe. Alguns modelos se aplicam apenas se todos atributos forem categóricos, por exemplo. Outros apenas lidam com problemas de regressão e não classificação. Os modelos em cinza não são aplicáveis, enquanto os em preto são. Você deve notar que não há muitos classificadores aplicáveis no momento e isso é principalmente devido a presença de atributos textuais no dataset. Vamos corrigir isso antes de ir adiante.
Retornando à aba Preprocess, selecione todos os campos do tipo texto (itens 3, 8, 10, 12 e 14) e clique no botão de remoção. Isso remove os atributos da base de dados carregada. Note que idealmente transformaríamos estes atributos em informações úteis, o que pode ser feito usando filtros no Weka mas que tipicamente é melhor fazer antes de carregar os dados. Por hora, vamos apenas remover estas informações e torcer pelo melhor.
Podemos agora retornar à aba Classifier e escolher outro classificador. Agora vários estarão disponíveis. Em trees, vamos escolher o modelo J48. Este é um modelo baseado em árvores de decisão. Após selecionar o modelo, podemos clicar no nome dele do lado do botão para ajustar os hiperparâmetros do treinamento. Convencionalmente usamos o termo parâmetro para indicar uma variável treinável no modelo e hiperparâmetro para indicar um variável ajustável no algoritmo de treinamento do modelo. O ajuste de hiperparâmetros é com frequência fundamental para obter um bom desempenho, mas deve ser feito com cuidado para evitar sobreajustes — se testarmos uma quantidade muito grande de hiperparâmetros, corremos o risco de ajustar excessivamente o modelo ao conjunto de testes, obtendo uma avaliação viciada.
Vamos evitar este problema mantendo os hiperparâmetros padrões e rodando o modelo. Temos novamente que alterar a classe para survived, logo acima do botão Start. O Weka não é conhecido por ter boa memória. Clicando em Start o modelo é treinado e avaliado.
Observe as métricas na área de resultados. Começando pela acurácia, mede-se 80,75%, bem acima dos 61,8% anteriores. Observa-se que algumas métricas por classe pioraram — agora não temos mais uma precisão perfeita para a classe "0", mas deve estar claro que isso não significa que o modelo é pior; pelo contrário, agora ele está de fato aprendendo a diferenciar as duas classes e isso implica cometer alguns erros em ambas as classes. O melhor resultado é evidente olhando a matriz de confusão. A maior parte das contagens em cada linha está na diagonal principal, indicando que nosso modelo mais acerta do que erra para todas as classes, ainda que seja melhor para a classe "0". É possível que este melhor resultado em uma classe seja devido ao desbalanceamento do dataset e medidas para balanceá-lo podem ser empregadas, como remover exemplos da classe majoritária até que ambas tenham o mesmo tamanho, ou é uma característica do problema.
Neste momento, é interessante testar modelos diferentes. O Weka torna isso simples, basta selecionar um novo modelo e treinar. O histórico dos modelos treinados permanece na área inferior esquerda. Experimente com alguns, observando as diferenças nas métricas e no tempo de execução. Alguns podem dar erro, mas a maioria deve funcionar.
Por fim, é importante tentar utilizar outros datasets. Existem vários repositórios de dados especificamente voltados para aprendizado de máquina. Por exemplo, escolha alguns do UCI ML Repository e tente treinar modelos e comparar os resultados obtidos contra o que é disponibilizado na literatura para o mesmo dataset (esses resultados se encontram na própria descrição para a maioria dos datasets).
Como conclusão, é importante ressaltar que o Weka, com uso de interface gráfica, é bastante instrutivo mas bem rígido na sua forma e muitas coisas são praticamente impossíveis de serem feitas. Ademais, datasets muito grandes podem dar problema e o desempenho dos algoritmos de treinamento não são os melhores em muito caso. O próximo passo natural é progredir para uma biblioteca mais moderna e flexível, como scikit-learn para Python.