Back-End

2 fev, 2017

Um pouco de Machine Learning com Python

Publicidade

E aí pessoal, tudo bem?

É quase um milagre este artigo sair com tão pouco tempo desde o último, mas o fato é que estou muito empolgado com meus estudos e o curso da Udemy que comentei no último artigo, tanto que comecei a aplicar algumas coisas na prática!

Hoje quero mostrar um pequeno programa que criei para testar e praticar as técnicas de Machine Learning na categorização de textos. É apenas um protótipo que usa matérias jornalísticas, mas você pode baixá-lo para testar e estudar!

Importante: Meu objetivo não é abordar profundamente as diferentes técnicas, abordagens e algoritmos disponíveis, pois meu conhecimento ainda não me permite. Ao invés disso, quero te mostrar como é possível aplicar tais conhecimentos e, quem sabe, te estimular a também estudar o assunto!

Como você pode ver no aquivo em meu Github, o programa ficou com 152 linhas de código, o que é pouco mas vou me concentrar nas partes relacionadas ao processamento dos dados, treinamento e predição que são o foco do artigo de hoje.

Acabei criando um repositório em meu github para este projeto, assim fica mais fácil baixar, instalar e testar o código!

O programa pode ser dividido nas seguintes etapas:

  1. Baixar e extrair o conteúdo das matérias a partir de um CSV com links e categorias;
  2. Limpar o conteúdo e deixar apenas o que é relevante para o treinamento do modelo;
  3. Criar o Bag of Words e treinar o modelo que fará a categorização;
  4. Categorizar novos links com o modelo treinado.
Com estas etapas em mente, vamos ao que interessa!

1. Baixar e extrair o conteúdo (Goose, o achado!)

Se você já criou algum tipo de web scraping, sabe como pode ser chato trabalhoso. Para os meus propósitos, era necessário extrair das páginas HTML apenas o conteúdo da matéria sem poluir o resultado com tags ou textos periféricos. Depois de muitas tentativas usando requests, lxml e regex, encontrei o projeto goose-extractor no pypi.

Goose, muito amor!

Em uma olhada rápida na documentação, percebemos que esse projeto é perfeito para o nosso caso e em 3 linhas ele resolve o problema:

from goose import Goose
....
goose = Goose()
article = goose.extract(link)
text = article.cleaned_text

Basta passar o link da página e o Goose faz o resto! Ele também permite acessar outros atributos da página, como o título da matéria, descriptors, etc. Vale a pena conferir!

Por se tratar de uma tarefa I/O Bound e serem mais de 700 links para baixar, estou usando a classe ThreadPoolExecutor do backport futures. O uso é muito simples e você pode saber mais na documentação do projeto!

2. Limpar o conteúdo (NLTK)

Com o texto das matérias em mãos, precisamos remover caracteres e palavras que podem atrapalhar o algoritmo de categorização, deixando apenas as palavras que possam contribuir com o “entendimento” do texto. Na primeira etapa, removo praticamente tudo que não for texto:

import unicodedata

...

def remove_nonlatin(string): 
    new_chars = []
    for char in string:
        if char == '\n':
            new_chars.append(' ')
            continue
        try:
            if unicodedata.name(unicode(char)).startswith(('LATIN', 'SPACE')):
                new_chars.append(char)
        except:
            continue
    return ''.join(new_chars)

...

text = remove_nonlatin(to_unicode(text))

Depois disso, precisamos remover as chamadas stop words que, em resumo, são palavras que se repetem muito em qualquer texto e podem prejudicar a análise feita pelo algoritmo. Isso é feito com a ajuda do projeto NLTK.

NLTK pode ser instalado via pip mas depois da sua instalação é preciso baixar o pacote stopwords pelo gerenciador do próprio NLTK. Vou deixar lincadas as instruções das 2 etapas.

Depois de tudo instalado, é fácil remover as stop words dos nossos textos:

from nltk.corpus import stopwords
...
stops = set(stopwords.words("portuguese"))
words = ' '.join([w for w in words if not w in stops])

Por fim, criamos um DataFrame pandas para reunir os links, as categorias e os textos processados:

from pandas import DataFrame
...
lines = []
words = pre_processor(link)
lines.append((link, categ, words))
...
df = DataFrame(lines)
df.columns = ['link', 'categoria', 'texto']

3. Bag of words

Em resumo rápido, bag of words é um modelo de representação textual que ignora a ordem e a gramática das palavras, mas preserva sua multiplicidade. Um exemplo pode nos ajudar:

O texto: “Rafael gosta muito de assistir filmes. A Ana gosta de filmes também” é transformado em uma lista de palavras:

["Rafael", "gosta", "muito", "de", "assistir", "filmes", "a", "Ana", "também"]

Depois disso, o modelo gera uma lista com a frequência que cada palavra aparece no texto : [1, 2, 1, 2, 1, 2, 1, 1, 1].

Este processo, de traduzir palavras em números, é necessário, pois os algoritmos que vamos usar para classificar os textos só aceitam/”entendem” números. Para nossa sorte, o scikit-learn, uma das principais bibliotecas do tipo em Python, já tem uma classe para ajudar neste processo.

Abaixo, vou mostrar a função de treinamento por completo. Acredito que seja mais fácil explicar assim:

import sklearn
from sklearn.externals import joblib
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.feature_extraction.text import CountVectorizer

def train(df, fit_file):
    print "\nTraining..."
    df = df.dropna()
    train_size = 0.8
    vectorizer = CountVectorizer(
        analyzer="word",
        tokenizer=None,
        preprocessor=None,
        stop_words=None
    )
    logreg = LogisticRegression()
    pipe = Pipeline([('vect', vectorizer), ('logreg', logreg)])
    X_train, X_test, Y_train, Y_test = sklearn.model_selection.train_test_split(
        df.texto, df.categoria, train_size=train_size
    )
    pipe.fit(X_train, Y_train)
    accuracy = pipe.score(X_test, Y_test)
    msg = "\nAccuracy with {:.0%} of training data: {:.1%}\n".format(train_size, accuracy)
    print msg
    pipe.fit(df.texto, df.categoria)
    joblib.dump(pipe, fit_file)

Das linhas 11 a 16, criamos a instância que vai montar o modelo bag of words dos textos que baixamos, mas por enquanto nada foi feito.

Na linha 17, é criada a instância que efetivamente vai analisar nossos dados e fazer predições. Como o nome sugere, será usada uma técnica chamada Regressão Logística para fazer a classificação dos textos. Eu testei algumas técnicas diferentes de classificação, mas esta apresentou os melhores resultados, chegando a 84% de acerto! Você pode conferir os teste que fiz neste link aqui!

Na linha 18, reunimos os 2 processos anteriores em um pipeline. Isto foi necessário para facilitar a preservação e armazenamento do modelo treinado. Vamos falar disso daqui a pouco!

Da linha 19 a 21 usamos uma função do scikit-learn para dividir nossa massa de dados em 80% para treino e 20% para testar a precisão do modelo.

Da linha 22 a 25, nós treinamos nosso modelo, avaliamos a precisão do mesmo e mostramos essa informação no terminal.

Finalizando, na linha 26 o modelo é treinado novamente, mas agora com 100% dos dados, e na linha 27 usamos outra ferramenta do scikit-learn para salvar o modelo treinado em disco, pois não precisamos refazer todo este processo toda vez que o programa for usado.

4. Pronto para uso!

Finalmente! O programa está pronto para categorizar novos textos! Vamos ver a função predict!

def predict(url, fit_file):
    pipe = joblib.load(fit_file)
    words = pre_processor(url)
    resp = pipe.predict([words])
    print "\nCategory: %s \n" % resp[0]
    resp = zip(pipe.classes_, pipe.predict_proba([words])[0])
    resp.sort(key=lambda tup: tup[1], reverse=True)
    for cat, prob in resp:
        print "Category {:16s} with {:.1%} probab.".format(cat, prob)

A função recebe como parâmetros uma URL (que vamos categorizar) e o caminho para o arquivo salvo em disco com nosso modelo treinado.

A linha 2 carrega o pipeline que salvamos no disco como o CountVectorizer e o LogisticRegression.

A linha 3 usa uma função para baixar e processar o texto da URL fornecida. Basicamente, os passos 1 e 2 desse artigo.

A linha 4 usa nosso pipeline para criar o bag of words do texto e fazer a predição de qual a categoria que melhor representa este texto.

As linhas de 6 a 9 mostram todas as categorias que nosso modelo conhece e as respectivas probabilidades em relação ao texto que está sendo categorizado.

Vamos ver como funciona!

Agora que já falamos sobre as principais partes do programa, vamos ve-lo em ação!
Para testar no seu computador, alem de baixar o arquivo do programa e a lista de links, você precisará instalar as dependências, tudo via pip install:

  • futures
  • goose-extractor
  • pandas
  • nltk
  • scikit-learn

O programa usa 3 parâmetros, FILE é o caminho para o arquivo CSV com os links das matérias que você pode baixar aqui. O formato do arquivo é muito simples e você pode montar o seu com outros links e categorias.

TRAIN é o nome do arquivo com o modelo treinado. Se o arquivo já existir, o programa usa o modelo existente; caso contrário, ele vai baixar os dados e processar.

Primeiro, vamos baixar os dados, processá-los e treinar nosso modelo. Esta é a saída do programa mostrando a quantidade de palavras em cada matéria:

Depois de algum tempo baixando e processando os dados, o programa nos mostra que ele salvou o arquivo bag_words.csv com os dados processados e a precisão do modelo após o treinamento. Chegamos a 79%!

Agora vamos testar se ele acerta a categoria desta matéria do G1 sobre tecnologia:

Concluindo (finalmente!)

Eu adoraria ficar aqui e te mostrar vários exemplos de como o programa acerta a categoria de várias matérias, mas você deve estar bem cansado – eu estou! rs

Bom, espero ter atingido meu objetivo e, pelo menos, motivado você a também estudar o assunto. Deixei vários links ao longo do texto e nas referências para te ajudar a entender melhor do que estamos falando.

Fique à vontade e comente o que achou aqui em baixo e se você souber como posso melhorar a precisão dos modelos usados, será de grande ajuda!

Referências