O Stack Exchange é um conjunto de sites de Q&A (question-and-answer) que abordam diversos temas diferentes. Provavelmente você conhece e participa do mais famosos desses sites, o Stack Overflow, que aborda a temática de programação. Existem outros tópicos abordados nos diversos sites, como matemática, estatística, eletrônica, etc., cada um com seu site especializado. A maioria tem uma comunidade bem ativa e participante, como pode ser visto em seu Data Explorer.
Neste texto eu vou começar a resolver um desafio de classificação das questões do Stack Exchange proposto no HackerRank. Ele vai ser dividido em duas partes principais:
- Leitura e exploração dos datasets do desafio
- Exploração das possibilidades features que se encaixam na resolução do problema
Todo o projeto foi implementado utilizando uma stack baseada em frameworks do Python: Pandas e Scikit Learn.
Os dados estão com formatos diferentes
O desafio nos fornece o conjunto de dados de treino e de teste separadamente e em formatos ligeiramente diferentes. O arquivo de treino contém duas partes principais: a primeira linha contendo um número inteiro N indicando quantas linhas subsequentes existem e N linhas com objetos JSON que representam uma observação, composta pelas JSON keys tópico (topic), excerto (excerpt) e questão (question). Já o conjunto de teste é dividido em dois arquivos, o primeiro tem um formato semelhante ao arquivo de treino, exceto que a JSON key tópico não está presente; segundo arquivo contém uma lista de tópicos para cada entrada do arquivo anterior. O código abaixo mostra como é feita a leitura dos dados.
Neste desafio, usaremos 10 tópicos diferentes para classificação das questões e uma base de treino que contém 20219 exemplos. O código abaixo mostra como ler os arquivos de treino e teste sem a necessidade de alterar a formatação dos mesmos (dica, se você eliminar a primeira linha do arquivo de treino fica bem mais fácil de lê-lo com o Pandas).
A investigação dos dados começa aqui
Se você explorar um pouquinho o dataset de treino, vai perceber que diversas questões e excertos contém palavras-chave que nos indicam já de cara sobre qual tópico estão falando. Por exemplo, termos como “node”, “transmission”, “LEDs”, “raspberry” são bons indicativos do tópico “electronics”; já termos como “trilogy”, “wolverine”, “film”, “demendor”, “battle” nos remetem ao tópico “scifi”. Esse é um insight super interessante, pois como veremos na próxima seção, algumas features que definimos em NLP partem do princípio que existem conjuntos de palavras que têm maior “chance” de aparecer em determinados tópicos do que em outros.
Por outro lado, existem palavras que são muito comuns dentro de textos e que não necessariamente carregam um significado naquele contexto, ou seja, não são palavras-chave. Elas são geralmente preposições, artigos, advérbios, verbos, etc. Se olharmos novamente o dataset de treino, vemos alguns exemplos como “in”, “I’m”, “about”, “this”, “be”, etc. A esses termos chamamos de stopwords.
Exemplos de algumas questões presentes na base de treino:
Uma das preocupações que existem quando se cria um modelo de classificação é a do enviesamento do mesmo. Uma das formas de isso acontecer é ter, no dataset de treino uma quantidade muito maior de um dos valores da variável alvo em relação aos demais. Por exemplo, se, no dataset desse projeto, a quantidade de exemplos do tópico electronics for muito maior do que a dos demais tópicos, o modelo tenderá a classificar a maior parte dos novos documentos como electronics.
Vale a ressalva de que nem sempre esse viés do classificador é algo ruim. No exemplo citado, se o fórum do Stack Exchange for muito focado no tópico electronics, talvez o classificador só esteja refletindo uma distribuição dos dados reais, porém se isso for um problema do dataset utilizado, o classificador provavelmente errará boa parte das predições.
A partir do gráfico abaixo, é possível perceber que a quantidade de exemplos do tópico matematica é menor do que a dos demais. Por outro lado, os tópicos gis e scifi são os que tem a maior quantidade de exemplos. Esse insight pode sugerir a necessidade de realizarmos um balanceamento de classes antes de treinar o modelo.
Além da presença stopwords no texto, um outro valor que talvez influencie o desafio de classificação de texto é o tamanho dos textos em si. Se pensarmos nesse desafio de uma forma mais intuitiva, é possível imaginar que talvez alguns tipos de questão exijam respostas de tamanhos maiores ou perguntas maiores, para melhor explicitar o problema e a solução ali presentes. Dado isso, talvez essa informação esteja relacionada com algum tópico.
A partir dos gráficos acima, é possível perceber que:
- O tamanho dos excertos se concentra em torno de 200 caracteres
- O tamanho das questões tem uma variação maior. A distribuição dos tamanhos se assemelha a uma curva normal com uma cauda mais esticada para a direita
- O tamanho médio dos excertos e das questões dentro de cada um dos tópicos não tem uma variação muito grande. Dado isso, será que é relevante considerarmos o tamanho do documento como uma feature?
Quais features podemos utilizar?
No contexto de NLP, as features utilizadas são, em sua maioria, as próprias palavras que compõem o seu dataset, ou melhor, são métricas relacionadas a essas palavras, tais como contagem de aparições de palavras, frequência de aparição das palavras, etc.
Essas métricas podem ser combinadas com outros elementos para gerar novas features, tais como part-of-speech tagging, que identifica a função morfológica das palavras dentro de uma sentença. Ou seja, é possível usar a contagem de adjetivos e verbos dentro de uma frase como features.
Outro conceito importante é o de n-gramas (n-grams), que são um conjunto de palavras que co-ocorrem numa determinada janela de uma sentença textual. Por exemplo, vamos considerar a frase “tomorrow will be a cloudy day”; se n=2, teremos os seguintes n-gramas:
- tomorrow will
- will be
- be a
- a cloudy
- cloudy day
Os n-gramas com n=1 são chamados de unigramas, aqueles com n=2 são chamados de bigramas, com n=3, trigramas e por aí vai. É possível combinar as métricas citadas no primeiro parágrafo com n-gramas.
E para deixar as coisas mais claras, vou explorar abaixo como funcionam algumas desses atributos. Eles serão explicados utilizando unigramas como exemplo.
Contagem de palavras
Essa é a feature mais básica em NLP. Ela também é conhecida como bag of words ou word count e consiste basicamente em contar a quantidade de aparições de uma mesma palavra (no contexto de NLP muitas vezes chamadas de token) dentro de cada entrada do seu dataset.
Podemos ver um exemplo de como ela funciona abaixo a partir de duas questões extraídas do dataset de treino:
- Questão 01: URL displays different page (a loop)?
- Questão 02: How to Block a url not a website?
Dá pra perceber que o artigo “a” aparece duas vezes na segunda questão e os demais termos aparecem apenas uma vez ou nenhuma. Se tocaram também que existe uma quantidade enorme de brancos dentro dessa tabela? Isso acontece porque o conjunto de palavras existentes no dataset é muito maior do que a quantidade de entradas no mesmo, causando o efeito de esparsidade na tabela construída. Por isso, pode ser interessante remover as stopwords, uma vez que elas não agregam grandes informações para descobrir o tópico ao qual pertence uma frase.
Frequência de termos
Apesar de contagem de palavras ser uma feature legal, ela traz alguns vieses negativos. Um deles é que entradas do dataset que tiverem um tamanho maior de caracteres tendem a ter maiores valores de contagem, enquanto entradas menores, tendem a ter contagens menores, mesmo para um único tópico.
Uma forma de normalizar esses valores dentro de um mesmo intervalo é usando a frequência de termos (term frequency — TF), cuja fórmula é:
Vamos fazer um exemplo com as duas frases usadas anteriormente:
Tanto a contagem de palavras quanto a frequência de palavras mensuram a importância/relevância dos termos dentro de uma entrada do dataset a partir da sua abundância naquele contexto, ou seja, as palavras que aparecem mais tem um maior peso.
Frequência do termo–inverso da frequência nos documentos (TF-IDF)
A partir dos exemplos acima, note que o artigo “a” ainda tem a maior relevância dentro de toda a tabela. O TF-IDF lida exatamente com esse cenário. Ele parte do pressuposto de que palavras importantes não aparecem muito em todas as entradas do dataset. Para isso, ele combina a TF com um elemento de “peso”, o IDF, que é calculado a partir da seguinte fórmula:
O TF-IDF é resultado da multiplicação desses dois elementos:
Vamos fazer um exemplo com as duas frases usadas anteriormente:
É possível notar nesse exemplo que as palavras que são comuns aos dois documentos tiveram seu peso drasticamente reduzido a zero e as demais tiveram redução leve em seus pesos.
Resumindo…
Nesse primeiro texto lemos os dados de treino e teste em dataframes do pandas, fizemos uma exploração dos dados de treino, observando elementos-chave que compõem as questões e excertos e também como é a distribuição de exemplos para cada um dos tópicos. Além disso exploramos as possíveis features que podemos usar para resolver esse desafio de classificação de documentos.
O próximo texto vai abordar a implementação prática da parte de feature engineering bem como aplicação de alguns algoritmos de classificação.
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).