20
nltk e processamento de linguagem natural
"And what is the use of a book", thought Alice, "without pictures or conversations?"
nltk
(Natural Language Toolkit) é um pacote para trabalhar com Processamento de Linguagem Natural (NLP) no Python. Por linguagem natural entende-se a linguagem usada no dia a dia na comunicação entre pessoas.
Muitos dados são gerados, dia a dia, por blogs, redes sociais e páginas web. A maioria destes dados estão em formato de texto não estruturado e são usados por empresas para entender seus clientes e melhorar (e produzir novos) produtos. NLP é útil em detecção de erros gramaticais e traduções automáticas, análise de sentimento em discursos, motores de busca, filtros de spam, etc…
O nltk
possui muitos recursos para análise de texto e conheceremos alguns deles.
sudo pip3 install -U nltk
Vamos analisar o livro Alice no País das Maravilhas, de Lewis Carroll, disponível pelo projeto Gutenberg neste link.
O nltk
possui um pacote com amostras de dados para uso, inclusive alguns do próprio projeto Gutenberg. A lista completa dos dados disponíveis pode ser acessada aqui. O pacote não vem habilitado por padrão, para instalá-lo, faça:
>>> import nltk
>>> nltk.download()
Uma janela deve abrir. É recomendado que você modifique o local do download para usr/share/nltk_data
- diretório do Linux para armazenar dados que não serão modificados (mais informações aqui). Caso esteja utilizando Windows, a recomendação é salvar no diretório C:\nltk_data
e no Mac em /usr/local/share/nltk_data
.
Também é possível instalar via linha de comando:
$ sudo python3 -m nltk.downloader -d /usr/local/share/nltk_data all
Uma vez que o download dos dados esteja concluído, podemos carregar e usar seu pacotes através do comando import
:
>>> import nltk.corpus as corpus
A palavra corpus é utilizada na língua inglesa para denotar uma coleção de material escrito ou falado em formato legível por máquina, montado para fins de pesquisa lingüística. O pacote corpus
possui exatamente isso, várias amostras de textos que podemos utilizar para análise. Usaremos o atributo gutenberg
para acessar alguns textos do projeto Gutenberg que carregamos com o download dos dados do nltk.
>>> type(corpus.gutenberg)
<class 'nltk.corpus.reader.plaintext.PlaintextCorpusReader'>
corpus.gutenberg
é um atributo do tipo PlaintextCorpusReader
. Este objeto, segundo a documentação (acesse pelo comando help(corpus.gutenberg)
), é um leitor para corpus de texto plano - é assumido que parágrafos são divididos usando quebra de linha, as frases
finalizam com pontuações específicas (como ponto, exclamação ou interrogação) e palavras podem ser dividas em unidades.
Vamos checar os textos disponíveis neste objeto:
>>> corpus.gutenberg.fileids()
['austen-emma.txt', 'austen-persuasion.txt', 'austen-sense.txt', 'bible-k
jv.txt', 'blake-poems.txt', 'bryant-stories.txt', 'burgess-busterbrown.tx
t', 'carroll-alice.txt', 'chesterton-ball.txt', 'chesterton-brown.txt',
'chesterton-thursday.txt', 'edgeworth-parents.txt', 'melville-moby_dick.t
xt', 'milton-paradise.txt', 'shakespeare-caesar.txt', 'shakespeare-hamle
t.txt', 'shakespeare-macbeth.txt', 'whitman-leaves.txt']
A função fileids()
mostra os arquivos disponíveis. Para acessar o livro Alice no País das Maravilhas, usaremos o seu fileid
que é o carroll-alice.txt
através do método raw()
:
>>> alice = corpus.gutenberg.raw('carroll-alice.txt')
>>> type(alice)
<class 'str'>
O método raw()
retorna uma string com todo o conteúdo do livro. Quem já trabalhou com strings conhece a dificuldade de trabalhar com estes objetos. Outros objetos do nltk
irão nos ajudar para conseguir analisar melhor este conteúdo.
Tokenization é o nome dado para o processo de dividir uma grande quantidade de texto em pequenas quantidades - essas pequenas quantidades são chamadas de tokens. Essa é uma divisão importante para fazer análises textuais.
O nltk
possui um módulo chamado tokenize
que facilita o processo de divisão de um texto. A função word_tokenize()
divide um texto por palavras e pontuações:
from nltk.tokenize import word_tokenize
>>> texto = 'Hello, how are you?'
>>> word_tokenize(texto)
['Hello', ',', 'how', 'are', 'you', '?']
Além da divisão por palavras, podemos querer dividir um texto em sentenças para calcular a média de palavras por sentenças, por exemplo. Para isso usamos a função sent_tokenize()
:
from nltk.tokenize import sent_tokenize
>>> texto = 'Hello, how are you? Fine, thanks!'
>>> sent_tokenize(texto)
['Hello, how are you?', 'Fine, thanks!']
E para dividir em parágrafos, usamos line_tokenize()
:
from nltk.tokenize import line_tokenize
>>> texto = 'Lorem ipsum dolor sit amet, amet blandit suscipit quam tellu
s vitae mauris, ut metus tellus, curabitur et elit, in volutpat, fringill
a aliquam. \nDonec cras tristique, quis eu. Ipsum integer quis sapien ves
tibulum wisi vel, scelerisque justo massa nulla tempor in, placerat ut se
m nunc ultrices ac, quis conubia.'
>>> line_tokenize(texto)
>>> ['Lorem ipsum dolor sit amet, amet blandit suscipit quam tellus vitae
mauris, ut metus tellus, curabitur et elit, in volutpat, fringilla aliqua
m.',
'Donec cras tristique, quis eu. Ipsum integer quis sapien vestibulum wis
i vel, scelerisque justo massa nulla tempor in, placerat ut sem nunc ultr
ices ac, quis conubia.']
No próprio objeto gutenberg
, temos funções prontas como paras()
, sents()
e words()
que retornam o conteúdo do texto separados por parágrafos, sentenças e palavras, respectivamente. Você pode testar cada um deles:
>>> paragrafos = corpus.gutenberg.paras('carroll-alice.txt')
>>> paragrafos
[[['[', 'Alice', "'", 's', 'Adventures', 'in', 'Wonderland', 'by', 'Lewi
s', 'Carroll', '1865', ']']], [['CHAPTER', 'I', '.'], ['Down', 'the', 'Ra
bbit', '-', 'Hole']], ...]
>>> sentencas = corpus.gutenberg.sents('carroll-alice.txt')
>>> sentencas
[['[', 'Alice', "'", 's', 'Adventures', 'in', 'Wonderland', 'by', 'Lewis'
, 'Carroll', '1865', ']'], ['CHAPTER', 'I', '.'], ...]
>>> palavras = corpus.gutenberg.words('carroll-alice.txt')
>>> palavras
['[', 'Alice', "'", 's', 'Adventures', 'in', ...]
>>> type(paragrafos)
<class 'nltk.corpus.reader.util.StreamBackedCorpusView'>
>>> type(sentencas)
<class 'nltk.corpus.reader.util.StreamBackedCorpusView'>
>>> type(palavras)
<class 'nltk.corpus.reader.util.StreamBackedCorpusView'>
Todos eles retornam um objeto do tipo StremBackedCorpusView
que representa uma visualização do corpus através de uma sequência de tokens iterável. Este objeto também possui métodos, como o count()
para contar o número de vezes que um elemento aparece na sequência de tokens.
Vamos usar o count()
para checar o número de vezes que determinados personagens são citados no texto. Por exemplo, se queremos saber quantas vezes a palavra "Alice" aparece, fazemos:
>>> palavras.count('Alice')
396
Vamos fazer a contagem de mais alguns personagens:
>>> personagens = ['Alice', 'Rabbit', 'Caterpillar', 'Cat', 'Hatter', 'Do
rmouse', 'Hare', 'Queen', 'King', 'Gryphon' ]
>>> for p in personagens:
... print('{}: {}'.format(p, palavras.count(p)))
...
Alice: 396
Rabbit: 45
Caterpillar: 0
Cat: 26
Hatter: 55
Dormouse: 40
Hare: 31
Queen: 74
King: 61
Gryphon: 55
Legal, obtemos a frequência de aparição de cada personagem no livro. Mas e se quisermos saber em quais partes do livro estes personagens aparecem? O nltk
facilita essa visualização através da classe Text
.
A classe Text
funciona como um wrapper de uma sequência de tokens (como nossa variável palavras
) e ajuda na exploração de textos. Seus métodos executam várias análises e exibe seus resultados, como é o caso do método dispersion_plot()
, que plota um gráfico de dispersão. Primeiro precisamos de uma instância de Text com o conteúdo do livro, e obteremos através da variável palavras
:
>>> palavras_text = nltk.Text(palavras)
Vamos checar a frequência da palavra "Alice" ao longo do texto:
>>> palavras_text.dispersion_plot(['Alice'])
Agora vamos visualizar as partes em que os personagens que definimos anteriormente aparecem no livro através de um gráfico de dispersão:
palavras_text.dispersion_plot(personagens)
Essa imagem, além de mostrar a frequência que cada personagem aparece na história, permite visualizar os momentos que eles interagem. O eixo x representa o deslocamento das palavras referenciando o índice da sequência do texto.
"Alice", sendo a protegonista, aparece com mais frequência e por toda a história do livro. Vemos que "Caterpillar" (a Lagarta Azul) interage pouco - apenas no início da história - e "Hatter" (Chapeleiro), "Dormouse" (Arganaz) e "Hare" (Lebre de Março) aparecem com uma frequência parecida no meio da história, no momento do famoso chá. O "Rabbit" (Coelho Branco) já interage com "Alice" em vários momentos da história (começo, meio e fim) mas com um frequência pequena.
Podemos usar a função de distribuição de frequência do nltk
para determinar as palavras mais frequentes (especificamente tokens), que são usadas em um determinado texto. Para facilitar a visualização, vamos utilizar a classe FreqDist
que representa a distribuição dos valores que aparecem com maior frequência em uma amostra. Essa classe possui um método plot()
que recebe um número inteiro como parâmetro representando a quantidade dos valores que queremos mostrar. Vamos gerar um gráfico com as 20 palavras mais frequentes no
texto:
>>> freq = nltk.FreqDist(palavras_text)
>>> freq.plot(20)
Esse gráfico não está bom. Veja que ele está contando as palavras que são pontuações (como ponto final e exclamações). Vamos eliminar as pontuações. Podemos filtrar pela função isalpha()
do objeto str
do próprio Python:
>>> freq_sem_pontuacao = nltk.FreqDist(dict((word, freq) for word, freq i
n freq.items() if word.isalpha()))
>>> freq_sem_pontuacao.plot(20, title="20 palavras mais populares (sem po
ntuação)")
Já está um pouco melhor! Ainda assim, o gráfico traz palavras comuns como “a”, “the”, “to”, etc… Queremos eliminar este tipo de palavra que é irrelevante para nossa busca e aparece com frequência em qualquer texto. Para isso, o nltk
possui um recurso chamado de stopwords (palavras de parada). Vamos eliminar também essas stopwords.
Primeiro guardaremos as stopwords da língua inglesa - já que o texto está em inglês - em uma variável chamada stopwords
:
>>> stopwords = nltk.corpus.stopwords.words('english')
E filtramos também por essas stopwords (no caso, sem elas):
>>> freq_sem_pontuacao_e_sem_stopwords = nltk.FreqDist(dict((word, freq)
for word, freq in freq.items() if word not in stopwords and word.isalpha
()))
>>> freq_sem_pontuacao_e_sem_stopwords.plot(20, title="20 palavras mais p
opulares (sem stopwords ou pontuações)")
Agora está bem melhor. Vemos que a personagem mais citada no texto, depois de "Alice" é a "Queen" (a Rainha). Podemos facilmente plotar esse gráfico com todos os nomes próprios (ou o que a morfologia chama de substantivos próprios) se existisse uma maneira fácil de identificá-los pelo texto. O nltk
possui esse recurso.
O nltk
possui uma função chamada pos_tag()
que infere uma classe (substantivo, adjetivo, advérbio, verbo, etc..) para cada palavra. Como esta função recebe uma lista de tokens, vamos usar a função word_tokenize()
com o texto puro:
>>> alice_txt = nltk.corpus.gutenberg.raw('carroll-alice.txt')
>>> words = nltk.word_tokenize(alice_txt)
>>> tags = nltk.pos_tag(words)
>>> tags
[('[', 'JJ'), ('Alice', 'NNP'),("'s", 'POS'), ('Adventures', 'NNS'), ('i
n', 'IN'), ('Wonderland', 'NNP'), ('by', 'IN'), ('Lewis', 'NNP'), ('Carro
ll', 'NNP'), ('1865', 'CD'), (']', 'NNP'), ('CHAPTER', 'NNP'), ('I', 'PR
P'), ('.', '.'), ('Down', 'RP'), ('the', 'DT'), ('Rabbit-Hole', 'JJ'), (
'Alice', 'NNP'), ('was', 'VBD'), ...
Veja que para cada token foi gerada uma tag. A lista completa com a definição de cada uma delas pode ser acessada através do código abaixo:
>>> nltk.help.upenn_tagset()
Estamos interessados nos substativos próprios no singular, ou seja, na tag NNP
:
>>> substantivos_proprios= []
>>> for word,tag in tags:
... if tag in ['NNP'] and word.isalpha():
... substantivos_proprios.append(word)
...
>>>
Agora que temos a lista de todos os substantivos próprios, vamos plotar o gráfico dos 20 mais frequentes:
>>> freq_substantivos_proprios=nltk.FreqDist(substantivos_proprios)
>>> freq_substantivos_proprios.plot(20, title='20 substantivos próprios m
ais frequentes')
Agora sabemos mais claramente os 20 personagens da história mais citados no texto. E podemos plotar o gráfico da dispersão de com eles, da mesma maneira que fizemos anteriormente com nossa lista de personagens.
>>> mais_comuns = freq_substantivos_proprios.most_common(n=20)
>>> personagens_mais_comuns = []
>>> for nome,freq in mais_comuns:
... personagens_mais_comuns.append(nome)
...
>>> palavras_text.dispersion_plot(personagens_mais_comuns)
Agora, além dos 20 personagens que aparecem com maior frequência no texto, temos como eles estão dispersos no texto. Note que as palavras "Rabbit" e "White" aparecem com uma frequência parecida, já que fazem referência ao "Coelho Branco", mas nem sempre aparecem juntas. O mesmo acontece com "Hare" e "March" - já que essas palavras fazem referência a "Lebre de Março" . Neste caso, poderíamos excluir desta lista as palavras "White" e "March" para uma análise mais precisa.
Da mesma maneira, podemos querer plotar o gráfico de frequência dos 20 verbos mais usados no texto. Usamos, desta vez, a tag VB
:
>>> verbos = []
>>> for word,tag in tags:
... if tag in ['VB']:
... verbos.append(word)
...
>>> freq_verbos=nltk.FreqDist(verbos)
>>> freq_verbos.plot(20, title='20 verbos mais frequentes')
E ver analisar sua dispersão pelo texto:
>>> mais_comuns = freq_verbos.most_common(n=20)
>>> verbos_mais_comuns = []
>>> for nome,freq in mais_comuns:
... verbos_mais_comuns.append(nome)
...
>>> palavras_text.dispersion_plot(verbos_mais_comuns)
Tente fazer o mesmo com os adjetivos, advérbios e substantivos.
O nltk
é um pacote bastante completo e possui subpacotes para analisar o sentimento de frases. Será que o texto possui mais sentenças que passam sentimentos negativos, positivos ou
neutros? Vamos analisar isso.
O subpacote para analisar sentimento é o nltk.sentiment
que possui o módulo vader
com diversas funcionalidade para obter métricas de sentimento de uma sentença. VADER
é acrônimo de Valence Aware Dictionary and Sentiment Reasoner, ou seja, um dicionário de valência e de raciocínio de sentimento. A palavra valência , em linguística, é o número de elementos gramaticais com os quais uma determinada palavra, especialmente um verbo, pode combinar em uma sentença.
Análise de sentimentos em um texto é uma tarefa complexa e exige muito estudo de linguística. Sorte que o nltk
já fez todo este trabalho por nós. O código deste módulo pode ser acessado através deste link.
Para analisar o sentimento de uma sentença, vamos utilizar a classe SentimentIntensityAnalyzer
que nos dá uma pontuação de intensidade de sentimento de determinadas sentenças.
Primeiro, vamos importar essa classe e instanciá-la:
>>> from nltk.sentiment.vader import SentimentIntensityAnalyzer
>>> sa = SentimentIntensityAnalyzer()
Esta classe possui o método polarity_scores()
que vai gerar as pontuações. Vejamos um exemplo:
>>> frase = 'Good job, nltk!'
>>> sa.polarity_scores(frase)
{'neg': 0.0, 'neu': 0.385, 'pos': 0.615, 'compound': 0.4926}
O retorno é um dicionários com pontuações de -1 a 1, do tipo float, para as chaves neg
(sentimento negativo), neu
(sentimento neutro), pos
(sentimento positivo) e compound
(sentimento dos três anteriores combinados). Esta pontuação é exatamente o cálculo da valência.
Veja que a frase "God Job, nltk!" ("Bom trabalho, nltk!") tem uma pontuação positiva alta e a pontuação composta de 0.4926 - ou seja, acima de 0, portanto é uma frase que passa um sentimento positivo.
O vader
é muito utilizado para analisar sentimentos de posts em redes sociais. É extremamente rápido e não necessita de treinamento.
Vamos analisar nosso texto para ter uma métrica do número de sentenças positivas, neutras e negativas que ele possui. Para isso utilizaremos a pontuação composta.
Primeiro, vamos dividir nosso texto em sentenças:
>>> alice_txt = nltk.corpus.gutenberg.raw('carroll-alice.txt')
>>> sentencas = nltk.sent_tokenize(alice_txt)
>>> sentencas
Agora, vamos criar duas listas, uma para as sentenças e outra para as pontuações:
>>> lista_pontuacoes = []
>>> lista_sentencas = []
>>> for sentenca in sentencas:
... lista_sentencas.append(sentenca)
... pontuacao = sa.polarity_scores(sentenca)
... lista_pontuacoes.append(pontuacao['compound'])
...
Para plotar o gráfico de quantidade de cada uma das classificações (neutras, negativas e positivas), vamos usar o pandas
, uma biblioteca do Python bastante usada para análise de dados. Para instalar o pandas
, execute o código abaixo no terminal:
$ pip3 install pandas
Vamos criar um dataframe com estes dados:
>>> import pandas as pd
>>> df = pd.DataFrame({'sentenca': lista_sentencas, 'pontuacao': lista_po
ntuacoes})
A variável df
guarda um dataframe, um tipo de dado retangular que representa uma tabela com as colunas senteca
e pontuacao
. Cada linha dessa tabela, portanto, possui o valor da sentença e sua respectiva pontuação de análise de sentimento (no caso, a pontuação composta).
Você pode checar esta estrutura através da função head()
do dataframe:
>>> df.head(10)
Após executado, vai mostrar as primeiras 10 linhas desta tabela. Agora, vamos selecionar apenas as sentenças negativas. O pandas
consegue selecionar isso muito facilmente. Uma sentença com sentimento negativo é aquela que possui pontuação menor do que 0 :
>>> selecao_neg = df.pontuacao < 0
>>> negativas = df[selecao_neg]
>>> negativas.head(10)
Selecionamos e guardamos apenas as sentenças negativas em outro dataframe que nomeamos de negativas
. Vamos ver sua quantidade:
>>> len(negativas)
394
Legal! Agora sabemos que 394 sentenças do livro Alice no País das Maravilhas passa um sentimento negativo. Vamos fazer o mesmo para os positivos e neutros:
>>> selecao_pos = df.pontuacao > 0
>>> positivas = df[selecao_pos]
>>> len(positivas)
443
>>> selecao_neu = df.pontuacao == 0
>>> neutras = df[selecao_neu]
>>> len(neutras)
788
Sabemos que o texto possui mais sentenças que passam um sentimento positivo do que negativo, mas as neutras superam. Vamos plotar um gráfico de barras para visualizar melhor os resultados:
>>> sentimentos = pd.DataFrame({'sentimentos':['negativo', 'neutro', 'pos
itivo'], 'quantidade':[len(negativas), len(neutras), len(positivas)]})
>>> sentimentos.plot(kind='bar', x='sentimentos', y='quantidade', color=[
'lightblue'], rot=0)
Documentação do nltk. http://www.nltk.org
Steve Bird, Edwan Klein e Edward Loper; Processamento de Linguagem Natural com Python - Analizando textos com Natural Language Toolkit. http://www.nltk.org/book/
20