quinta-feira, 4 de janeiro de 2018

Um pouco de Machine Learning com Python

Por Rafael Novello em 02/02/2017 no site iMasters



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.
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:
1from goose import Goose
2....
3goose = Goose()
4article = goose.extract(link)
5text = 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:
1import unicodedata
2 
3...
4 
5def remove_nonlatin(string):
6    new_chars = []
7    for char in string:
8        if char == '\n':
9            new_chars.append(' ')
10            continue
11        try:
12            if unicodedata.name(unicode(char)).startswith(('LATIN', 'SPACE')):
13                new_chars.append(char)
14        except:
15            continue
16    return ''.join(new_chars)
17 
18...
19 
20text = 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:
1from nltk.corpus import stopwords
2...
3stops = set(stopwords.words("portuguese"))
4words = ' '.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:
1from pandas import DataFrame
2...
3lines = []
4words = pre_processor(link)
5lines.append((link, categ, words))
6...
7df = DataFrame(lines)
8df.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:
1["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:
1import sklearn
2from sklearn.externals import joblib
3from sklearn.pipeline import Pipeline
4from sklearn.linear_model import LogisticRegression
5from sklearn.feature_extraction.text import CountVectorizer
6 
7def train(df, fit_file):
8    print "\nTraining..."
9    df = df.dropna()
10    train_size = 0.8
11    vectorizer = CountVectorizer(
12        analyzer="word",
13        tokenizer=None,
14        preprocessor=None,
15        stop_words=None
16    )
17    logreg = LogisticRegression()
18    pipe = Pipeline([('vect', vectorizer), ('logreg', logreg)])
19    X_train, X_test, Y_train, Y_test = sklearn.model_selection.train_test_split(
20        df.texto, df.categoria, train_size=train_size
21    )
22    pipe.fit(X_train, Y_train)
23    accuracy = pipe.score(X_test, Y_test)
24    msg = "\nAccuracy with {:.0%} of training data: {:.1%}\n".format(train_size, accuracy)
25    print msg
26    pipe.fit(df.texto, df.categoria)
27    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!
1def predict(url, fit_file):
2    pipe = joblib.load(fit_file)
3    words = pre_processor(url)
4    resp = pipe.predict([words])
5    print "\nCategory: %s \n" % resp[0]
6    resp = zip(pipe.classes_, pipe.predict_proba([words])[0])
7    resp.sort(key=lambda tup: tup[1], reverse=True)
8    for cat, prob in resp:
9        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 vê-lo em ação! Para testar no seu computador, além 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

Nenhum comentário:

Postar um comentário