Se você tá lendo esse texto, provavelmente já deve ter lido a parte 01. Se não, corre lá pra entender o cenário do que vou escrever abaixo.
Nesse artigo vamos falar sobre três tópicos:
- Feature engineering na prática
- Algoritmos de predição escolhidos para resolver esse problema
- Validação dos resultados do modelo usando métricas
Criar as features do modelo
No último texto expliquei como funciona a contagem de termos, frequência de termos e o TF-IDF. No scikit-learn é bem fácil de construir cada uma dessas features com pouco código, basta usar uma combinação do CountVectorizercom o TfidfTransformer (há outras formas de fazer chegar ao mesmo resultado, mas esse método que vou usar é descrito na documentação do scikit-learn).
Um ponto de atenção importante o utilizar esses métodos é a parte da normalização dos dados. Nós usaremos o TfidfTransformer para calcular possui dois métodos possíveis:
- L1 (least absolute deviation — LAD): também conhecido como o método que a gente vai usar para resolver nosso desafio, ele consiste em dividirmos a contagem de cada termo numa entrada do dataset de treino pela soma da contagem de todos os termos daquela mesma entrada.
- L2 (least square error — LSE): consiste em dividirmos a contagem de cada termo numa entrada do dataset de treino pela raiz quadrada da soma dos quadrados de cada termo daquela mesma entrada
Lembram das stopwords? A gente também precisa removê-las. Mas a notícia boa é que conseguimos fazer isso dentro do CountVectorizer. Mas antes de começar a implementar os elementos citados acima, precisamos ajustar nossos dados para que eles possam ser enviados para o scikit learn. Para isso, precisaremos de duas coisas:
- Usar as colunas excerpt e question juntas, pois elas contém texto capaz de identificar o tópico. O conteúdo delas deve estar minúsculo também
- Transformar os tópicos do dataset em valores inteiros usando o LabelEncoder do scikit learn, pois a biblioteca não trabalha com strings em seus modelos
Agora sim podemos criar nossas features como mostrado no código abaixo:
A predição
Escolhi dois algoritmos para tentar resolver esse desafio: o Multinomial Naive Bayes e o Support Vector Machines. Mas por que os escolhi? Essa resposta é simples: eles são usado em pesquisas científicas de NLP.
Em toda atividade de aprendizado supervisionado (classificação e regressão) precisamos dividir nossa base em treino e teste. A priori os dados para esse problema já estão divididos, pois temos um arquivo apenas com dados de treino e outros dois com dados de teste (não lembra disso? Então refresca a memória no primeiro texto). Não vamos mexer na base de testes por enquanto; ela vai ser usada quando eu descobrir qual o melhor algoritmo de predição.
Vamos focar na base de treino então. Vou dividi-la em duas bases, uma que vai servir para treinar o algoritmo e outra que vai servir como teste; vou usar um split de 80% dos dados para treino e 20% dos dados para teste. Ora bolas, mas por que eu tô fazendo isso? Porque na maioria das situações cotidianas vividas por um cientista de dados, não existe essa separação bonitinha de base de treino e base de teste, mas sim uma base única com os dados coletados e existem aqueles dados que você nunca viu antes e nos quais você quer aplicar o algoritmo que treinado. Para a resolução desse desafio do Stack Exchange, eu tô fingindo que a base de treino corresponde a essa base única com todos os dados coletados e que a base de testes é o conjunto de dados que eu nunca vi antes.
O código abaixo mostra como fazer essa divisão nos dados de treino para cada um dos tipos de features gerados.
Desmistificando o Multinomial Naive Bayes
Os classificadores naive bayes são algoritmos probabilísticos baseados no teorema de Bayes e eles tem uma característica interessante de assumir independência total entre as features do modelo. Para entendermos o funcionamento do Multinomial Naive Bayes, vamos lembrar um pouquinho de probabilidade condicional: dada uma frase w que precisa ser classificada, ou melhor, suas features w=(w1, w2, w3, …, wn), nós temos que a probabilidade de escolhermos a classe ck , onde ck ∈ Ck (conjunto de todas as classes) dado w é p(ck | w1, w2, w3, …, wn). Podemos generalizar essa probabilidade da seguinte forma:
Não vamos entrar em detalhes matemáticos aqui, mas essa expressão pode ser representada como uma probabilidade conjunta.
Onde p(ck) é a probabilidade anterior (prior probability) da classe ck e p(wi | ck) é a probabilidade condicional da feature wi dada a classe ck . Como estamos trabalhando com um classificador, precisamos de uma regra que defina uma classe para a frase w e, para isso, usamos a maximum a posteriori, que basicamente usa a classe com a maior probabilidade de aparecer. Representamos isso com a fórmula abaixo:
O Multinomial Naive Bayes usa todos os conceitos acima, com a adição de que a probabilidade condicional de uma feature wi dada uma classe ck é calculada a partir da frequência relativa de wi nos documentos com a classe ck.
Você pode ler detalhes sobre o classificadores Naive Bayes e suas variantes neste excelente blog post, nesse aqui também e nesse artigo científico. O código abaixo mostra como treinar um classificador desse tipo:
Desmistificando o Support Vector Machines (SVM)
Por trás desse nome complicado jaz um algoritmo que não é tão difícil assim de entender e ao mesmo tempo é bem poderoso. Ele pode ser usado tanto para atividades de regressão quanto para atividades de classificação. Neste último caso o SVM encontra o melhor hiperplano que diferencia as classes do modelo de um jeito bom.
Para entender o SVM, é preciso aprender os conceitos de hiperplano, support vectors, margens e funções de kernel. Vou explicar usando a imagem abaixo:
A linha vermelha é o que chamamos de hiperplano, que basicamente um conceito que generaliza a noção de reta e plano para várias dimensões. Então, o SVM acha a melhor posição do hiperplano que divide as bolinhas laranjas dos quadrados roxos. E como ele faz isso? Baseado em duas coisas principais: os support vectors e as margens.
Os support vectors os pontos mais próximos do hiperplano e que, se forem removidos do conjunto de dados, alterariam a posição do hiperplano. Em outras palavras, são elementos críticos para que o SVM executar a classificação adequadamente. Já as margens são as distâncias entre os support vectors e os possíveis hiperplanos. O SVM seleciona o hiperplano que maximiza as distâncias entre todos os support vectors.
Mas e quando o conjunto de dados não é linearmente separável como na imagem abaixo da esquerda? É aí que entra o que chamamos de kernel trick. As funções de kernel visam transformar um espaço n-dimensional em um espaço m-dimensional, tal que m > n. Em outras palavras, elas transformam um problema que não é linearmente separável em um que seja. É o que aconteceu na imagem abaixo da direita, pois no espaço bidimensional os pontos não são separáveis linearmente, mas no espaço tridimensional são.
Você pode ler detalhes sobre o svm e suas variantes neste blog post, nesse aqui também e nesse artigo científico, que aborda classificação de texto usando esse algoritmo. Ah, você não pode esquecer de olhar rapidinho a documentação do SVM no scikit learn para entender alguns parâmetros dele.
O código abaixo mostra como treinar um classificador desse tipo. Para resolver esse desafio, usaremos um kernel linear por questões de performance (usar outros kernels faz a etapa de treino demorar muito no meu computador) e também um pequeno GridSearch para determinar o melhor valor para o parâmetro C, que trabalha a regularização dentro do modelo:
Medindo os resultados
Agora que o modelo está criado precisamos validar o quão bom ele está. Para isso, vamos usar a mesma métrica descrita no desafio do HackerRank: a acurácia. Em problemas do mundo real, a acurácia muitas vezes não é a única métrica que precisa ser avaliada para definir a qualidade de um modelo, mas vamos manter as coisas simples nesse desafio e no futuro podemos ver mais métricas. O código abaixo mostra como calcular a acurácia dos modelos que criamos:
A tabela a seguir, que resume os resultados obtidos aplicando cada uma dos classificadores, mostra que o SVM com TF-IDF foi o melhor classificador que criamos.
Agora que descobri qual o melhor modelo, eu vou usá-lo na base de testes e vou checar a acurácia final. O código abaixo mostra como fazer isso:
A acurácia final foi de: 0,92 (92%).
Resumindo…
O objetivo desse texto era mostrar, na prática, como implementar feature engineering nos dados de texto e depois usá-los com classificadores de machine learning.
O próximo texto vai abordar algumas variações que podemos aplicar na resolução desse desafio, tais como usar outras features e usar técnicas de lematização de palavras.
Ah, vale lembrar que o código completo que criei para resolver esse desafio está no meu Github (se forem reutilizar meu código, lembrem de dar uma olhadinha na licença do projeto).