nltk e processamento de linguagem natural
Tags: python, beginners, nltk
Published: 2021-07-08 15:10:18

> *"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.

## instalação

```
sudo pip3 install -U nltk
```
## acessando os dados

Vamos analisar o livro *Alice no País das Maravilhas*, de Lewis Carroll, disponível pelo projeto *Gutenberg* neste [link](http://www.gutenberg.org/files/11/11-0.txt).

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](http://www.nltk.org/nltk_data/). O pacote não vem habilitado por padrão, para instalá-lo, faça:

```python
>>> 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](https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch04s11.html)). 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:

```bash
$ 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`:

```python
>>> 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.

```python
>>> 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:

```python
>>> 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()`:

```python
>>> 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

*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:

```python
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()`:

```python
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()`:

```python
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:

```python
>>> 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']], ...]
```

```python
>>> sentencas = corpus.gutenberg.sents('carroll-alice.txt')
>>> sentencas
[['[', 'Alice', "'", 's', 'Adventures', 'in', 'Wonderland', 'by', 'Lewis'
, 'Carroll', '1865', ']'], ['CHAPTER', 'I', '.'], ...]
```

```python
>>> palavras = corpus.gutenberg.words('carroll-alice.txt')
>>> palavras
['[', 'Alice', "'", 's', 'Adventures', 'in', ...]
```

```python
>>> 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*.

## primeiras análises

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:

```python
>>> palavras.count('Alice')
396
```

Vamos fazer a contagem de mais alguns personagens:

```python
>>> 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`:

```python
>>> palavras_text = nltk.Text(palavras)
```

Vamos checar a frequência da palavra "Alice" ao longo do texto:

```python
>>> palavras_text.dispersion_plot(['Alice'])
```

![Gráfico dispersão da palavra Alice](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h0p2ydzq9egdsqammymx.png)

Agora vamos visualizar as partes em que os personagens que definimos anteriormente aparecem no livro através de um gráfico de dispersão:

```python
palavras_text.dispersion_plot(personagens)
```

![Gráfico dispersão dos personagens](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o680nujgeehopo9q7ej6.png)

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:

```python
>>> freq = nltk.FreqDist(palavras_text)
>>> freq.plot(20)
```

![Gráfico das 20 palavras mais frequentes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rt6m52m2q45iouf4cbfk.png)

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:

```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)")
```

![Gráfico das 20 palavras mais frequentes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9lzsjyrxz9kp6b9hmsuq.png)

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`:

```python
>>> stopwords = nltk.corpus.stopwords.words('english')
```

E filtramos também por essas *stopwords* (no caso, sem elas):

```python
>>> 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)")
```

![Gráfico das 20 palavras mais frequentes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oy7e19v7dnz8ttfeid8t.png)

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.

## explorando o tagger

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:

```python
>>> 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:

```python
>>> nltk.help.upenn_tagset()
```

Estamos interessados nos substativos próprios no singular, ou seja, na *tag* `NNP`:

```python
>>> 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:

```python
>>> freq_substantivos_proprios=nltk.FreqDist(substantivos_proprios)
>>> freq_substantivos_proprios.plot(20, title='20 substantivos próprios m
ais frequentes')
```
![Gráfico substantivos mais frequentes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wacmoohxnv423d17xac9.png)

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.

```python
>>> 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)
```

![Gráfico dispersão 20 personagens mais frequentes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oo8i3bij4n6mk2cjk0g8.png)

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`:

```python
>>> 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')
```

![Gráfico 20 verbos mais frequentes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/36dz55b8ytqdg33x1qis.png)

E ver analisar sua dispersão pelo texto:

```python
>>> 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)
```

![Gráfico dispersão 20 verbos mais frequentes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/50omj3miouugw0ri9vl6.png)

Tente fazer o mesmo com os adjetivos, advérbios e substantivos.

## análise de sentimento

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](https://www.nltk.org/_modules/nltk/sentiment/vader.html).

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:

```python
>>> 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:

```python
>>> 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:

```python
>>> 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:

```python
>>> 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:

```python
>>> 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*:

```python
>>> 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 :

```python
>>> 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:

```python
>>> 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:

```python
>>> 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:

```python
>>> 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)
```

![Gráfico da pontuação de análise de sentimento](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qsl80kag5e80do9v8c0w.png)

## referências

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/