Marcelo Celeghini - Natural Language Processing (NLP)

51
8 Marcelo Celeghini PROJETO SOBRE AS PRINCIPAIS TÉCNICAS PARA PROCESSAMENTO DE LINGUAGEM NATURAL (PLN), UTILIZANDO PYTHON E A BIBLIOTECA DE CÓDIGO ABERTO, NATURAL LANGUAGE TOOLKIT (NLTK) SÃO PAULO 2012 Agradeço a todos e a tudo que direta ou indiretamente contribuíram para a realização deste trabalho.

Transcript of Marcelo Celeghini - Natural Language Processing (NLP)

8

Marcelo Celeghini

PROJETO SOBRE AS PRINCIPAIS TÉCNICAS PARA PROCESSAMENTO DE

LINGUAGEM NATURAL (PLN), UTILIZANDO PYTHON E A BIBLIOTECA DE

CÓDIGO ABERTO, NATURAL LANGUAGE TOOLKIT (NLTK)

SÃO PAULO

2012

Agradeço a todos e a tudo que direta ou indiretamente contribuíram para a

realização deste trabalho.

9

RESUMO

O Processamento de Linguagem Natural (PLN) é um campo da ciência da

computação que se preocupa com o processamento da linguagem humana para a

entrada e saída de dados em sistemas computacionais. Ao invés de usar uma

linguagem de programação convencional, o usuário simplesmente pode usar a

linguagem do seu dia-a-dia, como se ele estivesse se comunicando com outra

pessoa.

Um dos problemas mais desafiadores na área de ciência da computação é

desenvolver computadores que possam entender a linguagem natural e devolver

respostas corretas para ela.

Neste trabalho, utilizaremos a linguagem de programação Python juntamente

com a biblioteca de código aberto, Natural Language Toolkit (NLTK) para:

- Explicar como a linguagem Python trata um texto.

- Extrair informações de textos não estruturados.

- Analisar a estrutura linguística em um texto, incluindo análise sintática e

semântica.

- Escrever programas para acessar textos em arquivos.

- Construir modelos de linguagem que possam ser usados em execução

automática de tarefas de processamento de linguagem.

10

CONVENÇÕES ADOTADAS

Com o intuito de facilitar o entendimento desse trabalho, a fonte utilizada nos

exemplos de códigos será a fonte Courier New, tamanho 12, e os resultados

dos códigos executados serão escritos com a fonte Courier New, Tamanho 12,

em Negrito e na cor azul.

Como a biblioteca NLTK foi desenvolvida baseada majoritariamente no

padrão ASCII e em textos da língua inglesa, para evitar resultados inesperados na

execução dos códigos sobre textos em português baseados em caracteres Unicode,

em alguns dos textos as letras acentuadas e caracteres especiais da língua

portuguesa foram substituídos por caracteres não acentuados, ex. ‘á’ por ‘a’, ‘ê’ por

‘e’, ‘ç’ por ‘c’, e assim por diante.

11

Sumário INTRODUÇÃO ....................................................................................................................................... 12

1. PLN E PYTHON .................................................................................................................................. 14

1.1. O Processamento de Linguagem Natural .................................................................................. 14

1.2. A linguagem de programação Python ....................................................................................... 15

1.3. O Interpretador interativo ......................................................................................................... 16

1.4. Utilizando simples técnicas para processamento de textos ...................................................... 17

1.4.1. O uso de funções ................................................................................................................ 20

1.5. A biblioteca NLTK ...................................................................................................................... 22

1.6. Alguns desafios na área de Processamento de Linguagem Natural .......................................... 24

2. PROCESSAMENTO DE TEXTO BRUTO E O USO DE RECURSOS LÉXICOS ............................................ 26

2.1. Transformando um texto em tokens ......................................................................................... 26

2.2. Recursos léxicos ........................................................................................................................ 28

2.3. Stopwords ................................................................................................................................. 32

2.4. Expressões regulares e detecção de padrões ............................................................................ 33

3. AS CATEGORIAS LÉXICAS .................................................................................................................. 36

3.1. O processo de etiquetação ........................................................................................................ 38

3.1.1. Etiquetador padrão ............................................................................................................ 38

3.1.2. Avaliando a precisão de um etiquetador ............................................................................ 39

3.1.3. Etiquetador Unigram .......................................................................................................... 39

3.1.4. Combinando etiquetadores ................................................................................................ 42

4. ANÁLISE SINTÁTICA PARCIAL ............................................................................................................ 44

4.1. Expressões regulares para a identificação de padrões .............................................................. 44

4.2. Utilizando gramáticas para agrupar e desagrupar palavras....................................................... 46

5. CONSTRUINDO UM SISTEMA PARA CORREÇÃO VERBAL .................................................................. 48

5.1. Dicionários em Python .............................................................................................................. 49

5.2. Definindo os dicionários ............................................................................................................ 49

5.3. Definindo uma função auxiliar .................................................................................................. 51

5.4. Definindo uma função para correção Verbal ............................................................................. 52

6. CONCLUSÃO ..................................................................................................................................... 57

REFERÊNCIAS BIBLIOGRÁFICAS ............................................................................................................ 58

12

INTRODUÇÃO

A partir da Revolução Industrial, a humanidade intensificou a criação de meios e dispositivos para automatizar o trabalho mecânico. Na segunda metade do século XX, iniciou-se uma nova revolução, dessa vez com a atenção dos humanos voltada para a automatização de atividades intelectuais, desde então e cada vez mais, restarão aos homens apenas tarefas ligadas às tomadas de decisões.

Não seria bom poder dirigir um carro sem o uso das mãos, apenas "conversando" com o automóvel?

E sobre viajar para a China e poder comunicar-se com os seus habitantes,

sem necessariamente saber falar chinês?

Apesar da primeira situação ainda ser um caso de ficção científica, a segunda não é.

Em reportagem do site UOL Tecnologia1 de 23 de maio de 2011, foi listada uma série de aplicativos para celulares que permitem que uma pessoa viaje para a China e que se comunique de forma satisfatória sem saber o idioma mandarim, a língua mais falada por lá. Entre esses aplicativos estão dicionários em mandarim, reconhecedores de caracteres, tradutores automáticos, pronunciadores de frases e palavras, leitores de texto, entre outros.

Figura 1.1: Aplicativo que reconhece desenhos de caracteres em mandarim feitos na tela de um dispositivo móvel.

Aplicativos como esses ainda apresentam falhas e inconsistências, mas já

1 http://tecnologia.uol.com.br/album/2011_falar_chines_aplicativos_album.jhtm?

13

estão nos auxiliando na resolução de problemas na área de PLN (Processamento de Linguagem Natural), que praticamente eram insolúveis há 50 anos, quando começaram a surgir os primeiros computadores eletrônicos.

No processamento de linguagem natural, um desafio ainda não vencido

totalmente é saber qual o real sentido de uma sentença, pois a mesma pode assumir vários significados, mas geralmente apenas um é o aceitável. Para isso utiliza-se um conjunto de regras para tentar compreender, sem ambiguidades, as informações contidas na linguagem falada ou escrita.

Nesse trabalho, concentraremos os nossos esforços no sentido de explicar as

principais técnicas de PLN utilizadas para a manipulação de textos escritos. Para tanto, utilizaremos a linguagem de programação Python para implementar os algoritmos estudados.

Por que Python? Porque é uma linguagem poderosa, de fácil entendimento, e

com uma especial característica, que é o fato de possuir à sua disposição uma biblioteca de código livre chamada Natural Language Toolkit (NLTK), com várias funcionalidades especificamente voltadas para o processamento de dados linguísticos.

Os livros que nos serviram de base para esse estudo foram “Natural

Language Processing with Python”, “Python Text Processing with NLTK 2.0 Cookbook”, “Mining the Social Web” e “Python 2.6 Text Processing Beginner's Guide”.

O interpretador Python para diversas plataformas pode ser obtido

gratuitamente em http://www.python.org/, e a biblioteca NLTK também pode ser baixada sem custo em http://www.nltk.org/. Nesses dois endereços existe vasta documentação, dados e exemplos que podem ser livremente consultados.

Na primeira parte do nosso trabalho começaremos com uma introdução à

Python e ao PLN, e demonstraremos o que pode ser alcançado com a combinação de simples técnicas de programação com grandes quantidades de texto, quais são as técnicas e ferramentas que Python disponibiliza para esse tipo de trabalho e quais são alguns dos desafios do processamento de linguagem natural.

Na segunda parte falaremos sobre o uso de recursos léxicos, e como separar

palavras, símbolos e pontuações em um texto para que possam ser usados em diferentes tipos de análise.

Na terceira parte definiremos as categorias léxicas e mostraremos como elas

podem ser usadas no processamento de linguagem natural. Na quarta parte veremos como identificar e classificar as características

relevantes de uma linguagem. E finalmente na quinta parte, construiremos um sistema para manipular

informações em dados não estruturados, utilizando algumas das técnicas vistas ao longo do nosso estudo.

14

1. PLN E PYTHON

1.1. O Processamento de Linguagem Natural

O Processamento de Linguagem Natural é um campo de estudo altamente

interdisciplinar que engloba conceitos de Linguística2, Matemática e Ciência da

Computação.

A utilização da linguagem humana, seja em texto escrito ou falado, está

crescendo a taxas exponenciais e as pessoas estão cada vez mais confiando em

serviços da internet para a busca, filtragem e processamento de conteúdo. Esses

serviços que nos permitem fazer tudo isso com a linguagem cotidiana fazem parte

dos problemas compreendidos pelo PLN.

Para explicar melhor daremos alguns exemplos. Digamos que um blogger

esteja tentando obter informações sobre uma erupção vulcânica no Chile.

O seu fluxo de trabalho pode consistir numa sequência de tarefas baseada na

Web. Para cada tarefa que está sendo executada, incluiremos o nome do problema

específico em PLN que está sendo resolvido:

-“Mostre-me os dez documentos mais relevantes da internet sobre a erupção

vulcânica no Chile.” (Recuperação de Informação).

- “Faça um resumo sobre esses duzentos artigos sobre a erupção no Chile.”

(Resumo Automático de Documento).

- “Traduza esse blog do espanhol para o português, para que eu possa obter

as últimas informações sobre a erupção no Chile.” (Tradução Automática).

O PLN como área de estudos acadêmicos nunca foi mais relevante do que é

atualmente. E os sucessos obtidos nessa área devem-se à utilização de métodos

que são dirigidos por dados de linguagem natural em vez de usar métodos

puramente baseados no conhecimento ou baseados em regras, que além de não

serem tão robustas, possuem alto custo computacional.

Com o crescimento dessa tendência, as técnicas de PLN dirigidas a dados

vêm se tornando cada vez mais sofisticadas, empregando vários conhecimentos das

2 Linguística é a ciência que estuda a linguagem verbal humana.

15

áreas de Estatística e de Aprendizado de Máquinas. Tais técnicas exigem grandes

quantidades de dados para poderem construir um modelo razoavelmente

semelhante à linguagem humana.

Em PLN e em Linguística usa-se com frequência os termos corpus ou

corpora (corpus no plural). Uma coleção com as diversas obras de Machado de

Assis pode ser chamada de um corpus, e uma coleção com várias obras de

diversos autores chamamos de corpora. Corpus e corpora em nosso trabalho

representarão uma grande coleção finita de textos que sirvam para o propósito de

análise. Podemos usar um corpus para medir a frequência de uma palavra em um

idioma, ou para criar um dicionário de uma língua, por exemplo.

1.2. A linguagem de programação Python

Python é uma linguagem interpretada, de código aberto, de alto nível e de uso

geral. Foi criada no início da década de 1990, por Guido van Rossum no CWI

(Centrum Wiskunde & Informatica) na Holanda. O nome veio de uma homenagem a

um programa televisivo humorístico chamado Monty Python, que foi ao ar pelo canal

de TV britânico BBC, entre os anos de 1969 e 1974.

É considerada uma linguagem de programação multiparadigma, ou seja, ao

invés de obrigar o programador a adotar um estilo específico de programação, ela

permite que sejam usados estilos diferentes, como programação estruturada ou

orientada a objetos, misturando livremente construtores de diferentes paradigmas.

Desse modo o programador fica livre para utilizar diferentes ferramentas conforme a

sua necessidade.

Em Python não existem tipos primitivos, o conceito de variável é sempre

representado por um objeto.

Python é fácil de se aprender, é versátil, possue uma sintaxe simples, e além

disso é uma linguagem poderosa.

Várias linguagens de programação tem sido utilizadas para PLN, como por

exemplo Pearl, Prolog, Java, C ou Ruby. Mas essas linguagens apresentam uma

sintaxe mais complexa e portanto, uma curva de aprendizado mais acentuada.

Apesar de Python não ser tão veloz como linguagens compiladas como C ou

C++, a menor velocidade de execução não é percebida na maioria dos programas,

desse modo o menor tempo gasto em programação pode compensar o maior tempo

com a execução de código.

16

A combinação entre o poder e a simplicidade, foi o principal critério que nos

guiou no sentido de eleger Python para o nosso estudo sobre Processamento de

Linguagem Natural.

1.3. O Interpretador interativo

Uma das facilidades de Python é o seu interpretador interativo. Com ele é

possível testar e modificar comandos, funções e outros trechos de código antes de

incluí-los em um programa. Essa característica nos ajuda a aumentar a velocidade

de aprendizado e portanto, esse é mais um dos pontos positivos de Python com

relação às outras linguagens.

O interpretador interativo pode ser acessado usando a interface gráfica

chamada IDLE (Interactive DeveLopment Environment), que pode ser observada na

figura abaixo.

Figura 1.2: Interactive Development Environment – IDLE. O símbolo ">>>",

chamado de prompt, indica que o interpretador Python está pronto para receber um

comando.

Para mostrar o funcionamento do interpretador interativo digitaremos um

simples comando para imprimir uma string (sentença) na tela:

17

>>> print “Ciência da Computação”

Ciência da Computação

>>>

Após digitarmos o comando e pressionarmos a tecla Enter, o interpretador

executa a instrução e o resultado aparece na linha seguinte. Logo abaixo do

resultado também reaparece o prompt, indicando que o interpretador está pronto

para receber uma nova instrução.

Python consegue manipular strings de várias maneiras, como concatenar

elementos:

>>> frase = 'Ciência ' + 'da ' + 'Computação.'

>>> print frase

Ciência da Computação.

>>>

O interpretador Python possui uma série de funções embutidas que estão

disponíveis a qualquer instante. No decorrer do trabalho utilizaremos várias dessas

funções em nossos exemplos.

1.4. Utilizando simples técnicas para processamento de textos

Já vimos como é possível imprimir uma string na tela utilizando apenas um

comando. Veremos agora o que pode ser feito com grandes quantidades de texto,

utilizando simples técnicas de programação.

A string do exemplo a seguir foi retirada do site da UNIP, na página de

Objetivos do Curso de Ciência da Computação:

>>> objetivos_curso = "Com o intuito de estimular e

contribuir para a preparação de mão-de-obra especializada

e indispensável à política de desenvolvimento nacional, o

bacharel em Ciência da Computação tem sua formação focada,

principalmente, para o projeto e desenvolvimento de

produtos de software...”

>>>

>>> print objetivos_curso

Com o intuito de estimular e contribuir para a preparação

de mão-de-obra especializada e indispensável à política de

desenvolvimento nacional, o bacharel em Ciência da

Computação tem sua formação focada, principalmente, para o

18

projeto e desenvolvimento de produtos de software...

>>>

Nesse exemplo atribuímos à variável objetivos_curso, uma string que

contém o trecho da descrição dos objetivos do curso de Ciência da Computação.

Quando executamos o comando print, o valor da variável é impresso na tela.

Vejam o exemplo a seguir:

>>> len(objetivos_curso)

279

>>>

A função len() retorna o número de caracteres ou o comprimento da string,

incluindo espaços em branco, caracteres especiais e pontuação. O nosso exemplo

possui 279 caracteres.

Veremos agora se a função len() também é eficiente para textos maiores,

para tanto criamos um arquivo de texto chamado dom_casmurro.txt, que contém

todo o conteúdo da respectiva obra de Machado de Assis, e o salvamos na mesma

pasta onde Python foi instalado.

>>> dc = open(“dom_casmurro.txt”)

>>> texto = dc.read()

>>> len(texto)

373504

>>>

Na primeira linha desse exemplo a função open() recebe o argumento

“dom_casmurro.txt”, que é o nome do arquivo. As aspas indicam que

queremos abrir o texto no formato string retornado como um “objeto arquivo” que é

atribuído à variável dc. Como dc agora é um objeto de Python, ele pode acessar o

método read() que lê o conteúdo desse objeto na forma de uma longa string. Na

linha seguinte usamos a função len(texto) para medir o comprimento da string

lida, e o resultado obtido foi de 373504 caracteres.

Uma string pode ser indexada e isso nos permite “fatiar” um texto. O índice

zero corresponde ao primeiro caractere da string e no nosso exemplo, o índice

373503 corresponde ao seu último caractere. Verificaremos agora quais são os

primeiros duzentos caracteres do nosso texto, os caracteres ‘\n’ representam uma

nova linha, e por convenção m:n significa o intervalo iniciando com o elemento m

até n-1, o intervalo abaixo inclui o elemento 0 até o elemento 199 (200 -1):

>>> texto[0:200]

'Romance, Dom Casmurro, 1899\n\nDom Casmurro\n\nTexto de

refer\xeancia:\n\nObras Completas de Machado de

Assis,\nvol. I,\n\nNova Aguilar, Rio de\nJaneiro,

19

1994.\n\n\xa0Publicado originalmente\npela Editora

Garnier, Rio d'

Percebemos que o texto obtido não está formatado de maneira apropriada

para os padrões humanos de leitura, e que a palavra referência não está totalmente

legível (refer\xeancia), isso porque o caractere especial ê está representado

internamente no formato hexadecimal (onde \x indica que o valor a seguir, ea, é o

caractere ê no valor hexadecimal). Para contornar essa situação devemos utilizar o

comando print para transformar o conteúdo da variável texto em um padrão de

leitura apropriado:

>>> print texto[0:200]

Romance, Dom Casmurro, 1899

Dom Casmurro

Texto de referência:

Obras Completas de Machado de Assis,

vol. I,

Nova Aguilar, Rio de

Janeiro, 1994.

Publicado originalmente

pela Editora Garnier, Rio d

>>>

Strings podem ser manipuladas de diversas maneiras, vimos como medir o

seu comprimento, atribuí-las a nomes de variáveis, concatená-las, indexá-las e fatiá-

las. Podemos também substituir os seus caracteres, descobrir a posição de suas

palavras, transformá-las para letras maiúsculas ou minúsculas, encontrar e isolar

caracteres numéricos ou especiais contidos nela, entre vários outros tipos de

manipulações.

Essas operações com strings são importantes para o PLN, pois em muitos

casos teremos que fazer uso de expressões regulares nos algoritmos utilizados.

Para um aprofundamento no conhecimento das técnicas utilizadas para a

manipulação de strings e outras técnicas de programação em Python, pode-se

consultar a documentação em http://www.python.org.br/wiki/DocumentacaoPython.

20

1.4.1. O uso de funções

O interpretador Python possui uma série de funções embutidas para ajudar no

nosso trabalho. Elas basicamente são blocos de código contendo comandos e

declarações.

Já vimos algumas delas como a função len() que retorna o comprimento de

uma lista, a função open() que abre um arquivo e a função read() que lê o conteúdo

de um arquivo.

Funções também podem ser chamadas de métodos, quando elas pertencem

a alguma classe.

A característica mais importante de uma função é a facilidade na reutilização

de código. Além disso, o uso de funções torna um programa mais legível, pois

abstrai detalhes do código, e também mais confiável, pois geralmente são criadas a

partir de códigos exaustivamente testados.

No entanto, as funções pré-definidas em Python, às vezes não são suficientes

para atingir os nossos propósitos de maneira satisfatória, e por esse motivo, em

várias situações é muito útil saber criar nossas próprias funções.

Para definir uma função, nós usamos a palavra reservada ‘def’, seguida

pelo nome da função, e seguida por um par de parênteses que podem conter ou não

parâmetros (dados para serem usados pela função). Para finalizar a definição da

função usamos o símbolo de dois pontos.

Na seção 1.4.1. usamos os seguintes comandos e declarações:

>>> dc = open("dom_casmurro.txt"), para abrir um texto

>>> texto = dc.read() para ler o texto aberto ,

e o comando >>> len(texto) para obter o número de caracteres do

texto.

Poderíamos combinar todas essas três linhas de código em apenas uma

função, desse modo a função recém criada poderia ser usada para qualquer outro

texto.

Nomearemos essa função de open_read_len(), que traduzindo significa

abrir, ler, e medir o tamanho do texto que estiver entre os parênteses. Esse texto é o

parâmetro da função:

>>> def open_read_len(texto):

texto_aberto = open(texto)

texto_lido = texto_aberto.read()

return len(texto_lido)

21

Agora que a função já está definida, podemos usá-la simplesmente

chamando-a pelo seu nome, seguido por um argumento entre parênteses:

>>> open_read_len("dom_casmurro.txt")

373504

Denominamos de argumento o valor passado para o parâmetro da função.

Nesse exemplo, o argumento passado foi o arquivo

("dom_casmurro.txt"). Na última linha de código da função usamos a palavra

reservada return, que irá devolver um resultado assim que a função for chamada.

Nesse exemplo o valor retornado pela função foi 373504.

Se quisermos reutilizar a mesma função, mas com outro texto, basta passar

um diferente argumento no momento da chamada da função, como no exemplo

abaixo:

>>> open_read_len("Memórias Póstumas de Brás Cubas.txt")

355706

Dessa vez passamos como argumento outro arquivo, contendo uma diferente

obra de Machado de Assis, o romance Memórias Póstumas de Brás Cubas. E o

valor retornado pela função foi o de 355706 caracteres.

Outra facilidade da linguagem de programação Python é o uso de funções

anônimas. Funções anônimas utilizam um construtor chamado de lambda.

Funções lambda permitem a criação de uma função em apenas uma linha. O

código a seguir exemplifica a diferença entre uma função normal e uma função

lambda. Textos seguidos por # são comentários:

>>> # Atribuindo uma sentença à variável texto:

>>> texto = "Lambda é uma função anônima composta apenas

por expressões."

>>> # Definindo uma função normal:

>>> def retorna_comprimento(x):

return len(x)

>>> # Chamando a função e passando a variável ‘texto’ como

argumento:

>>> retorna_comprimento(texto)

59

>>> # Criando uma função lambda:

>>> comprimento = lambda x: len(x)

>>> # Chamando uma função lambda dentro de um comando

print:

>>> print comprimento(texto)

59

22

Podemos observar que retorna_comprimento() e comprimento() fazem

exatamente a mesma coisa, no entanto a função lambda não possui o comando

return, ela sempre retornará o valor da expressão depois do símbolo :.

No decorrer deste trabalho, em algumas situações usaremos funções lambda

para simplificar a elaboração dos nossos programas.

1.5. A biblioteca NLTK

Apesar de Python ser considerada uma linguagem fácil para implementar

algoritmos na área de PLN, o estudo de Processamento de Linguagem Natural é um

tema complexo. Com o intuito de facilitar o entendimento desse assunto, em 2001

um grupo de desenvolvedores liderados por Steven Bird e Edward Loper da

Universidade da Pensilvânia iniciou a criação de uma “caixa de ferramentas” na

forma de uma eficiente biblioteca com vários métodos, funções e dados

especialmente voltados para estudos na área de linguística computacional. Essa

biblioteca de código aberto chamada NLTK (Natural Language Toolkit), possui

dezenas de corpora em diferentes idiomas e várias ferramentas para processamento

de textos, como tokenizadores3, classificadores de palavras, analisadores sintáticos,

assim como interfaces para bibliotecas de aprendizado de máquina.

NLTK é um software de código aberto e pode ser baixado gratuitamente em

http://www.nltk.org, além do software, uma vasta documentação está disponível no

site, como artigos, livros, tutoriais, projetos e ideias relacionadas à área de PLN.

Para a realização do nosso trabalho, além de Python e de NLTK, algumas

outras ferramentas também foram utilizadas como “NumPy” que é uma biblioteca

para manipular vetores e matrizes multidimensionais, e executar tarefas de

classificação e álgebra linear para cálculos de probabilidade e a biblioteca

“Matplotlib” que foi utilizada para as visualizações de dados em duas dimensões

como gráficos de linhas e de barras. Essas duas ferramentas também podem ser

obtidas gratuitamente no site do NLTK.

Para utilizar a biblioteca NLTK em nossos programas, primeiramente

devemos importá-la utilizando o comando import NLTK. Para importar apenas

determinados recursos dessa ferramenta, usamos o comando from ... import

..., dependendo do recurso desejado. Veremos a seguir alguns exemplos de

corpus em português que estão inclusos no pacote NLTK:

3 Quebra uma string em tokens, que são os segmentos de texto ou símbolos que serão manipulados pelo Analisador Sintático.

23

>>> from nltk.examples.pt import *

*** Introductory Examples for the NLTK Book ***

Loading ptext1, ... and psent1, ...

Type the name of the text or sentence to view it.

Type: 'texts()' or 'sents()' to list the materials.

ptext1: Memórias Póstumas de Brás Cubas (1881)

ptext2: Dom Casmurro (1899)

ptext3: Gênesis

ptext4: Folha de Sau Paulo4 (1994)

>>>

Nesse comando o símbolo “ * ” determina que se importe todos os recursos

do módulo pt, pertencente ao pacote examples da biblioteca NLTK.

As primeiras quatro linhas do resultado se referem a uma breve descrição do

módulo, e a algumas instruções para usar certas funções pertencentes a ele e, nas

quatro últimas linhas aparecem os quatro textos importados e que formam a corpora

do pacote de exemplos.

Esses quatro textos são chamados de “textos NLTK”, pois já estão

formatados, ou seja, já estão tokenizados, suas palavras e pontuações estão

separadas em grupos e, portanto, estão prontos para receber os procedimentos para

o estudo linguístico. Em breve veremos como tokenizar os nossos próprios textos.

O texto ptext2 se refere ao livro Dom Casmurro. Só que nesse caso, como

já explicamos todas as suas palavras, símbolos e pontuações estão isolados na

forma de tokens, diferentemente do texto dom_casmurro.txt que foi usado na

seção 1.4 e que se refere a uma longa string.

Vejamos a diferença entre os arquivos dom_casmurro.txt e ptext2:

>>> dom_casmurro = open("dom_casmurro.txt").read()

>>> len(dom_casmurro)

373504

>>> len(ptext2)

82088

Como o arquivo ptext2 representa uma lista de tokens, a função len()ao

invés de medir o seu comprimento em caracteres, medirá o seu comprimento em

tokens.

Com o auxílio da função count() que conta o número de vezes que uma

específica string aparece numa lista, verificaremos agora a porcentagem que a

palavra “Deus” ocupa nos livros Gênesis e Dom Casmurro respectivamente:

4 A palavra ‘São’ de São Paulo está grafada incorretamente como ‘Sau’, provavelmente devido a uma falta de atenção dos desenvolvedores do NLTK.

24

>>> from __future__ import division

>>> 100 * ptext3.count('Deus') / len(ptext3)

0.53222158158513333

>>> 100 * ptext2.count('Deus') / len(ptext2)

0.08283793002631322

>>>

O comando from __future__ import division é usado para garantir

que Python use divisão com ponto flutuante.

É fácil perceber apenas comparando os dois resultados que quando dividimos

o número de ocorrências da palavra Deus pelo número total de tokens do texto, que

o texto 3 tem um caráter bem mais religioso do que o texto 2, pois a palavra “Deus”

representa 0,53% dos tokens do livro Gênesis, enquanto que na obra Dom

Casmurro, a mesma palavra representa apenas 0,08% dos tokens do livro.

Na segunda parte desse trabalho explicaremos melhor a importância do uso

de tokens para o estudo de PLN.

1.6. Alguns desafios na área de Processamento de Linguagem Natural

Poderemos no futuro criar um sistema computacional que seja capaz de se

comunicar conosco de maneira natural, e seremos capazes de não perceber se

estamos ou não conversando com uma máquina?

Uma máquina poderá um dia pensar? Esse é uma questão clássica na área

de inteligência artificial proposta por Alan Turing em 1950.

Em filmes de ficção científica como “2001 - Uma Odisseia no Espaço”5 e em

“Blade Runner”6 esse problema parece ter sido superado. Mas nos atuais sistemas

comerciais de conversação ainda existem muitas limitações, sendo que alguns já

apresentam resultados significativos quando atuando de modo bem específico.

5 2001: A Space Odyssey (br: 2001: Uma Odisseia no Espaço) é um filme americano de 1968 dirigido e produzido por Stanley Kubrick, co-escrito por Kubrick e Arthur C. Clarke. O filme lida com os elementos temáticos da evolução humana, tecnologia, inteligência artificial e vida extraterrestre. 6 Blade Runner (br: Blade Runner: O Caçador de Andróides ) é um filme americano de 1982 dirigido por Ridley Scott. No início do século XXI, uma grande corporação desenvolve um robô que é mais forte e ágil que o ser humano e se equiparando em inteligência. São conhecidos como replicantes e utilizados como escravos na colonização e exploração de outros planetas.

25

Sistema: Bom dia, em que posso ajudá-lo?

Usuário: Qual é a previsão do tempo para hoje?

Sistema: Para o município de qual estado?

Usuário: Para Porto Alegre no Rio Grande do Sul.

Sistema: A temperatura mínima será de 17ºC e a máxima de 29ºC, com sol e

aumento de nuvens de manhã e com pancadas de chuva à tarde e à noite.

Na situação acima o sistema se comportou de maneira satisfatória, de acordo

com a expectativa do usuário. E se o usuário desse uma resposta um pouco

diferente para a segunda pergunta do sistema:

Sistema: Para o município de qual estado?

Usuário: Para a capital de São Paulo.

Sistema: Por favor, reformule a sua resposta.

Aparentemente o sistema não entendeu a resposta do usuário que, para nós

humanos, foi perfeitamente inteligível. Se esse sistema tivesse sido programado

para entender que em determinadas situações o usuário pode simplesmente

responder “para a capital de tal estado”, não haveria um comportamento inesperado

da máquina.

O que esperar então de um sistema para uma pergunta mais elaborada como:

Qual é a melhor época do ano para tirar férias e viajar para o Nordeste?

Não é preciso ser um agente de viagens para saber que a melhor época do

ano para aproveitar as férias no Nordeste é fora do conhecido período de chuvas.

Em uma conversa entre humanos nem precisaríamos citar que estamos nos

referindo ao nordeste brasileiro. Mas uma máquina precisaria armazenar e

manipular conhecimento para isso, ela teria que saber que durante as férias a

maioria dos brasileiros procura sol e calor e que isso corresponde ao adjetivo

“melhor” da pergunta. Também teria que saber em quais meses chove menos na

região nordeste do Brasil.

Em sistemas de computação é preciso habilidade, conhecimento e até

alguma sorte para obter respostas sensatas para algumas perguntas. Obter um

sistema que responda automaticamente esses tipos de questões envolve uma série

de tarefas de processamento de linguagem como extração de informações,

inferência7 e sumarização8 (Bird et al., 2009).

7 Inferência é o processo pela qual concluímos algo por meio de um raciocínio. De várias proposições nós inferimos uma conclusão. Inferir é, portanto, chegar a uma resposta a partir de juízos anteriores.

26

Apesar de todos os avanços realizados, os atuais sistemas de linguagem

natural ainda não conseguem obter um desempenho robusto satisfatório.

Esperamos que um dia esses difíceis problemas na área de inteligência artificial

possam ser superados, mas por enquanto teremos que conviver com essas graves

limitações.

No entanto, acreditamos que até o fim desse trabalho, poderemos construir

um sistema útil para PLN, utilizando técnicas simples e poderosas, que contribua

para a construção de máquinas mais inteligentes.

2. PROCESSAMENTO DE TEXTO BRUTO E O USO DE RECURSOS LÉXICOS

2.1. Transformando um texto em tokens

Já sabemos que a biblioteca NLTK possui vários textos especialmente

tratados para o estudo de PLN.

No entanto, às vezes queremos usar os nossos próprios textos, e para que

isso seja possível é necessário prepará-los para as nossas finalidades de análise

linguística.

Como exemplo, extraímos do website IDG Now!9, uma curta reportagem do

dia 6 de agosto de 2010, que diz que os sistemas de reconhecimento de voz ficarão

mais inteligentes. O conteúdo da reportagem foi copiado em um arquivo chamado

idg_voz.txt e salvo na mesma pasta onde Python está instalado.

Vamos abrir esse arquivo e efetuar algumas operações com as quais já

estamos familiarizados:

8 A sumarização compreende dois processos: a seleção do conteúdo relevante de uma mensagem fonte primária e sua organização coerente. 9 http://idgnow.uol.com.br/mercado/2010/08/06/reconhecimento-de-voz-vai-ficar-mais-esperto-promete-pesquisador/#&panel2-1

27

>>> texto_bruto = open("idg_voz.txt").read()

>>> len(texto_bruto)

2018

>>> texto_bruto[0:50]

'Reconhecimento de voz vai ficar mais esperto, prom'

>>> type(texto_bruto)

<type 'str'>

>>>

A função type() retorna o tipo de objeto. No nosso exemplo o objeto variável

texto_bruto é uma string. Com o uso da função len() podemos ver que essa

string possui um total de 2018 caracteres.

Para continuar nosso estudo, precisamos transformar esse texto, que é uma

string (lista de caracteres), em uma lista de tokens. Esse processo é chamado de

tokenização e faz uso da função word_tokenize().

Um token é o nome técnico para uma sequência de caracteres - como “carro”,

“elas” ou “#” – que queremos tratar como um grupo (Bird et al., 2009).

>>> tokens = nltk.word_tokenize(texto_bruto)

>>> type(tokens)

<type 'list'>

>>> len(tokens)

423

>>> tokens[0:10]

['Reconhecimento', 'de', 'voz', 'vai', 'ficar', 'mais',

'esperto', ',', 'promete', 'pesquisador']

>>>

Dessa vez quando usamos a função type() e passamos tokens como

argumento, o objeto retornado é do tipo lista com o comprimento de 423 tokens.

No comando tokens[0:10], fatiamos essa lista para exibir os seus 10

primeiros itens.

E finalmente, para poder utilizar os recursos da biblioteca NLTK,

transformaremos essa lista de tokens em um “texto NLTK” com o uso do método

nltk.Text():

>>> texto = nltk.Text(tokens)

>>> type(texto)

<class 'nltk.text.Text'>

>>>

Obtemos agora um objeto da classe “texto NLTK”, pronto para o estudo dos

recursos léxicos da biblioteca NLTK.

28

2.2. Recursos léxicos

É possível efetuar em nossos textos diversas manipulações, que fazem parte

de um conjunto de operações chamado de “Recursos Léxicos”.

Um recurso léxico é uma coleção de palavras ou frases que possuem algum

tipo de informação associada, ele é secundário ao texto e, portanto, é criado com o

auxílio de textos (Bird et al., 2009). Um dicionário é um tipo de recurso léxico, pois é

uma coleção de palavras com suas respectivas definições e sentidos.

Nos exemplos anteriores quando usamos o comando:

>>> tokens = nltk.word_tokenize(texto_bruto),

também fizemos uso de um recurso léxico simples, declarado como tokens, que

carrega consigo informações associadas ao seu nome.

Podemos criar nossos próprios recursos léxicos dependendo do objetivo a ser

alcançado no estudo de um texto.

Para o próximo exemplo usaremos um texto que não sabemos sobre qual

assunto trata, e com o uso de um recurso léxico simples chamado Distribuição de

Frequência, tentaremos descobrir o assunto do texto.

Uma distribuição de frequência verifica quantas vezes cada token aparece no

texto. Ao identificarmos as palavras mais frequentes, poderemos deduzir em boa

parte dos casos, sobre qual assunto o texto trata:

>>> texto_bruto = open("texto_desconhecido.txt").read()

>>> len(texto_bruto)

15914

>>> tokens = nltk.word_tokenize(texto_bruto)

>>> texto = nltk.Text(tokens)

>>> dist_freq = FreqDist(texto)

>>> dist_freq

<FreqDist with 2805 outcomes>

>>> vocabulario = dist_freq.keys()

>>> vocabulario[0:50]

['de', ',', 'e', 'o', 'que', 'a', '"', 'um', 'para', '.',

'com', 'em', 'do', 'os', 'uma', 'reconhecimento', 'como',

'palavras', 'voz', 'ou', 'da', 'sistema', 'mais', 'fala',

'na', 'programa', 'sao', 'O', 'as', 'sistemas',

'computador', 'nao', 'se', 'tambem', 'tem', 'usuarios',

'das', 'no', 'ser', 'som', '(', ')', 'pode', 'por',

'programas', 'E', 'Os', 'cada', 'entre', 'isso']

>>>

29

Analisando o resultado:

O primeiro valor retornado, quando utilizamos a função len(), nos indica

que esse texto é uma string com 15914 caracteres.

Depois tokenizamos texto_bruto, e em seguida o transformamos em um

texto NLTK.

Na sexta linha, utilizamos a função FreqDist() para transformar o objeto

texto em uma distribuição de frequência, e atribuímos o resultado à uma “variável

objeto” dist_freq. Quando chamamos o objeto dist_freq obtemos como

resposta “<FreqDist with 2805 outcomes>”, ou seja, uma lista de distribuição

de frequência com 2805 resultados ou tokens.

Na linha seguinte, quando usamos o comando vocabulario =

dist_freq.keys(), a função keys() organiza a lista de distribuição de modo

que os tokens mais frequentes apareçam distintamente no início da lista. O resultado

é atribuído à variável vocabulario. No último comando solicitamos que sejam

exibidos os 50 tokens mais frequentes do texto.

Obtemos alguns símbolos de pontuação como a vírgula ',' e o ponto '.'.

Esses tokens não nos dão o sentido do texto e, portanto são praticamente inúteis.

Palavras como 'e' e 'o' minúsculos, e 'E' e 'O' maiúsculos são

redundâncias, pois significam a mesma coisa. Podemos eliminar os símbolos de

pontuação e as palavras redundantes convertendo todos os tokens alfabéticos do

texto para caracteres minúsculos:

>>> vocab_minusculas = [w.lower() for w in vocabulario if

w.isalpha()]

>>> vocab_minusculas[0:50]

['de', 'e', 'o', 'que', 'a', 'um', 'para', 'com', 'em',

'do', 'os', 'uma', 'reconhecimento', 'como', 'palavras',

'voz', 'ou', 'da', 'sistema', 'mais', 'fala', 'na',

'programa', 'sao', 'o', 'as', 'sistemas', 'computador',

'nao', 'se', 'tambem', 'tem', 'usuarios', 'das', 'no',

'ser', 'som', 'pode', 'por', 'programas', 'e', 'os',

'cada', 'entre', 'isso', 'modelos', 'podem', 'atuais',

'ja', 'mas']

>>>

Nesse comando apenas os tokens constituídos por caracteres alfabéticos

foram convertidos para tokens formados por caracteres minúsculos. A função lower()

é responsável por essa conversão e a função isalpha() verifica se o token é formado

apenas por letras.

30

Em outras palavras, a variável 'w' que aqui poderia ser qualquer outro nome,

percorre cada item da lista vocabulario por intermédio do comando for, e se o

token é formado só por letras, ele é então convertido para minúsculas e o resultado

é atribuído à variável vocab_minusculas.

Além dos pontos e palavras redundantes, percebemos a ocorrência de outros

tokens não muito úteis para a compreensão do texto como ‘de’, ‘que’, ‘para’

e ‘uma’. Vamos eliminar também as palavras com menos de quatro caracteres:

>>> vocab_maior_3 = [w for w in vocab_minusculas if len(w)

> 3]

>>> vocab_maior_3[0:50]

['para', 'reconhecimento', 'como', 'palavras', 'sistema',

'mais', 'fala', 'programa', 'sistemas', 'computador',

'tambem', 'usuarios', 'pode', 'programas', 'cada',

'entre', 'isso', 'modelos', 'podem', 'atuais', 'muito',

'usuario', 'computadores', 'diferentes', 'estatisticos',

'frases', 'maneira', 'mesmo', 'palavra', 'quando',

'treinamento', 'dados', 'entanto', 'esses', 'falar',

'fonemas', 'grande', 'maior', 'pelo', 'ruido', 'voce',

'garofolo', 'ainda', 'algum', 'anos', 'bastante',

'comandos', 'criar', 'desempenho', 'duas']

>>>

Verificamos a ocorrência frequente de palavras como ‘reconhecimento’,

‘palavras’, ‘sistemas’, ‘fala’, ‘programa’, ‘computador’ e

‘frases’. Apenas constatando a ocorrência dessas palavras, podemos concluir

que o texto deve falar sobre algum assunto relacionado ao reconhecimento de fala

em sistemas computacionais, portanto essa simples técnica de Distribuição de

Frequência nos auxiliou a dar sentido a um texto desconhecido.

O texto original pode ser encontrado em:

http://informatica.hsw.uol.com.br/reconhecimento-de-voz.htm

Se quisermos saber quantas vezes um determinado token aparece no texto,

basta fornecer a palavra procurada como um argumento ao objeto dist_freq:

>>> dist_freq['reconhecimento']

24

>>> dist_freq['palavras']

20

>>>

As palavras ‘reconhecimento’ e ‘palavras’ formam 44 dos 2805

tokens do texto, ou seja, elas correspondem juntas a aproximadamente 1,57% do

texto.

31

Com o uso da função plot(), que representa graficamente a participação

dos tokens mais frequentes, é possível analisar visualmente a distribuição de

frequência de um texto.

A função plot() a seguir receberá dois argumentos, o primeiro indica a

quantidade de tokens considerados, e o segundo argumento, cumulative=True,

solicita que os resultados sejam acumulados:

>>> dist_freq.plot(40, cumulative=True)

Figura 2.1: Gráfico com a frequência acumulada dos 40 tokens mais usados no

texto, que juntos correspondem a quase metade dos 2805 tokens identificados.

32

Observamos que apenas 40 dos tokens mais frequentes correspondem a

mais de 1200 ocorrências, quase a metade do texto.

2.3. Stopwords

Mais uma das ferramentas úteis do NLTK é o corpus chamado Stopwords,

que nada mais é do que uma coleção de listas de palavras com altas frequências em

vários idiomas, como alemão, inglês, italiano, francês, português, sueco, etc.

A lista NLTK de stopwords em português contem 203 palavras mais utilizadas

em nosso idioma como que, para, uma, se, quando, muito, etc.

Stopwords são geralmente palavras com pouca significância léxica, e a sua

presença em um texto dificulta que se percebam as outras palavras com maior

importância, criando o que poderíamos chamar de poluição visual.

Com o uso dessa ferramenta podemos filtrar um texto e assim obter um

conjunto de palavras com maior significância.

>>> from nltk.corpus import stopwords

>>> stopwords.words('portuguese')[0:10]

['de', 'a', 'o', 'que', 'e', 'do', 'da', 'em', 'um',

'para']

>>>

No primeiro comando importamos o corpus de stopwords da biblioteca NLTK

e no segundo, acessamos os 10 primeiros itens da lista de stopwords em português.

Agora iremos abrir o arquivo texto_desconhecido.txt, já usado nos

exemplos anteriores, e filtrá-lo usando stopwords:

>>> texto_bruto = open("texto_desconhecido.txt").read()

>>> tokens = nltk.word_tokenize(texto_bruto)

>>> texto = nltk.Text(tokens)

>>> df = nltk.FreqDist(w.lower() for w in texto if w not

in stopwords)

>>> for word in df.keys()[:20]:

print word, df[word]

33

, 123

" 46

. 34

reconhecimento 24

palavras 20

voz 19

sistema 17

fala 15

programa 15

sao 14

nao 13

sistemas 13

tambem 13

computador 11

usuarios 11

ser 10

som 10

( 9

) 9

>>>

As três primeiras linhas do código já são familiares. Na primeira abrimos e

fizemos a leitura do arquivo texto_desconhecido.txt e atribuímos o resultado à

variável texto_bruto. Na linha seguinte tokenizamos texto_bruto, na terceira

linha transformamos tokens em um texto NLTK e atribuímos o resultado à variável

texto.

Na quarta linha atribuímos à variável df o resultado de uma distribuição de

frequência onde percorremos cada token da variável texto, transformamos cada item

em letras minúsculas para que não exista redundância de palavras e verificamos se

cada token não está na lista de stopwords. É assim que ocorre a filtração do texto.

No último comando solicitamos a impressão dos 20 primeiros tokens mais

frequentes da distribuição de frequência, juntamente com o número acumulado de

vezes em que o token aparece na lista.

2.4. Expressões regulares e detecção de padrões

Expressões Regulares são às vezes consideradas o “Canivete Suíço” do

processamento de textos (McNeil, 2010, p.137).

34

Uma expressão regular pode ser vista como um conjunto de caracteres que

especificam um padrão. É uma maneira que o programador usa para dizer como o

computador deve procurar por certos formatos dentro do texto, e o que fazer após

encontrá-los.

Em várias situações, quando realizamos o estudo linguístico de um texto,

procuramos por determinados padrões da linguagem. Por exemplo, em alguns casos

precisamos saber quais e quantas palavras no texto terminam com as letras “ente’,

como em finalmente, regularmente, geralmente, concorrente, saliente, etc. Com o

uso de Expressões Regulares, podemos identificar esses e outros padrões.

No exemplo a seguir usaremos um dos corpus disponíveis pelo NLTK

chamado ptext4, que se refere a artigos retirados do jornal folha de São Paulo de

1994.

>>> import re

>>> final_ido = [w for w in ptext4 if re.search('ido$',

w)][:20]

>>> type(final_ido)

<type 'list'>

>>> final_ido

[u'reduzido', u'abolido', u'abolido', u'pedido', u'sido',

u'marido', u'apreendido', u'r\xe1pido', u'r\xe1pido',

u'Devido', u'r\xe1pido', u'sido', u'r\xe1pido',

u'Partido', u'Unido', u'Partido', u'Unido', u'prometido',

u'sido', u'assumido']

>>>

Na primeira linha importamos o módulo re de Python, que nos permite

trabalhar com expressões regulares.

O segundo comando pede para percorrer todos os tokens (aqui chamados de

w) do arquivo ptext4, e se a busca pela expressão regular ido$ retornar True, o

valor dos primeiros 20 itens da lista será atribuído à variável final_ido.

Em re.search a função search (busca), do módulo re (regular

expressions), é utilizada para procurar por padrões de expressões regulares, nesse

caso a expressão regular procurada é definida como 'ido$'. O símbolo $ indica o

final da string, isso quer dizer que e a expressão regular pode ser lida como

qualquer string que tenha o final ido.

No terceiro comando type(final_ido,)verificamos que a variável

final_ido se trata de uma lista de strings.

No último comando podemos ver como a variável é representada

internamente. A letra u, antes de cada string, indica que ela está representada no

35

formato Unicode10. É importante representar textos em português no formato

Unicode, para evitar problemas com a leitura de caracteres especiais como ç, á, é,

etc.

Para imprimir o conteúdo na tela podemos usar o código abaixo:

>>> for palavra in final_ido:

print palavra,

reduzido abolido abolido pedido sido marido apreendido

rápido rápido Devido rápido sido rápido Partido Unido

Partido Unido prometido sido assumido

>>>

De forma geral, uma expressão regular é composta por caracteres e

metacaracteres, que nada mais são do que caracteres especiais como ^ $ * +? [ ] .

Juntos, os caracteres e metacaracteres formam um padrão de texto.

Demonstraremos agora como podemos encontrar caracteres específicos em

uma frase :

>>> frase = 'Expressão Regular: Uma composição de

símbolos, caracteres com funções especiais, que, agrupados

entre si e com caracteres literais, formam uma seqüência,

uma expressão. Essa expressão é interpretada como uma

regra, que indicará sucesso se uma entrada de dados

qualquer casar com essa regra, ou seja, obedecer

exatamente a todas as suas condições.'11

>>> re.findall(r'[aáãeéêiíoõuü]', frase)

['e', '\xe3', 'o', 'e', 'u', 'a', 'a', 'o', 'o', 'i',

'\xe3', 'o', 'e', '\xed', 'o', 'o', 'a', 'a', 'e', 'e',

'o', 'u', '\xf5', 'e', 'e', 'e', 'i', 'a', 'i', 'u', 'e',

'a', 'u', 'a', 'o', 'e', 'e', 'i', 'e', 'o', 'a', 'a',

'e', 'e', 'i', 'e', 'a', 'i', 'o', 'a', 'u', 'a', 'e',

'\xfc', '\xea', 'i', 'a', 'u', 'a', 'e', 'e', '\xe3', 'o',

'a', 'e', 'e', '\xe3', 'o', '\xe9', 'i', 'e', 'e', 'a',

'a', 'o', 'o', 'u', 'a', 'e', 'a', 'u', 'e', 'i', 'i',

'a', '\xe1', 'u', 'e', 'o', 'e', 'u', 'a', 'e', 'a', 'a',

'e', 'a', 'o', 'u', 'a', 'u', 'e', 'a', 'a', 'o', 'e',

'a', 'e', 'a', 'o', 'u', 'e', 'a', 'o', 'e', 'e', 'e',

'e', 'a', 'a', 'e', 'e', 'a', 'o', 'a', 'a', 'u', 'a',

'o', 'i', '\xf5', 'e']

>>>

10 O Padrão Unicode especifica a representação do texto em software e padrões modernos. O Unicode fornece um único número para cada caractere, não importa a plataforma, não importa o programa, não importa a língua. (Em: <http://unicode.org/standard/translations/portuguese.html>. Acesso em: 09 maio 2012.) 11 http://aurelio.net/regex/

36

Na primeira linha do exemplo atribuímos uma string à variável frase.

No segundo comando utilizamos a função findall (encontre tudo), seguida

por dois parâmetros entre parênteses. No primeiro parâmetro r'[aáãeéêiíoõuü]'

é lido como uma “expressão regular r formada pelos caracteres entre os

colchetes”. E o segundo parâmetro é a variável frase.

Como resultado, obtemos uma lista com todas as ocorrências das vogais

minúsculas definidas entre os colchetes e encontradas na variável frase, incluindo

caracteres acentuados como á ou é, representados internamente nos formatos

hexadecimais e3 e e9, respectivamente. Os caracteres \x representam um escape

sequence (sequência de escape), como já vimos anteriormente na seção 1.4 deste

trabalho.

No processamento de linguagem natural algumas das utilidades para as

expressões regulares são: tokenizar sentenças, isolar e substituir caracteres e

palavras, validar campos em formulários, traduzir palavras, remover caracteres

repetidos e verificar ortografia. No entanto devemos ser cautelosos e específicos no

seu uso, pois elas podem se tornar complexas rapidamente.

Para saber mais sobre o uso de expressões regulares em Python visitem a

página: http://docs.python.org/library/re.html

3. AS CATEGORIAS LÉXICAS

Palavras podem ser divididas em grupos chamados de Classes Gramaticais,

como verbos, adjetivos, pronomes, advérbios, preposições, substantivos, etc. Em

Python esses grupos são conhecidos como Categorias Lexicais

Uma das técnicas utilizadas em Python para classificar as palavras em um

texto é chamada de tagging ou etiquetação. Na etiquetação é atribuída uma etiqueta

para cada palavra, como N para substantivo, V para verbo, PREP para preposição,

ART para artigo, e assim por diante.

No próximo exemplo veremos o trecho de um texto já etiquetado que faz parte

do corpus Mac-Morpho12 do NLTK:

>>> import nltk

>>> nltk.corpus.mac_morpho.tagged_words()[:10]

12 Mac-Morpho se trata de uma coleção de textos em português do Brasil, extraídos de diferentes seções do jornal Folha de São Paulo de 1994. Esse corpus possui mais de um milhão de palavras já etiquetadas.

37

[(u'Jersei', u'N'), (u'atinge', u'V'), (u'm\xe9dia',

u'N'), (u'de', u'PREP'), (u'Cr$', u'CUR'), (u'1,4',

u'NUM'), (u'milh\xe3o', u'N'), (u'em', u'PREP|+'), (u'a',

u'ART'), (u'venda', u'N')]

>>>

No comando >>> nltk.corpus.mac_morpho.tagged_words()[:10],

solicitamos as primeiras dez palavras etiquetadas do corpus mac_morpho, e como

resultados obtemos uma lista de dez tuplas, contendo a palavra e sua respectiva

etiqueta.

Uma tupla é uma sequência, assim como listas e strings. A diferença é que

ela é imutável, portanto, uma vez criada, não pode ser modificada. Ela também pode

ser indexada e isso permite que os seus elementos sejam acessados por índices. As

tuplas são criadas usando parênteses e seus elementos são separados por vírgulas.

Vamos imprimir o resultado do exemplo anterior em uma forma mais fácil para

leitura, usando o comando print:

>>> tuplas = nltk.corpus.mac_morpho.tagged_words()[:10]

>>> for x, y in tuplas:

print x, y

Jersei N

atinge V

média N

de PREP

Cr$ CUR

1,4 NUM

milhão N

em PREP|+

a ART

venda N

>>>

Primeiro atribuímos à variável tuplas uma lista com as dez primeiras tuplas

do corpus Mac-Morho. Posteriormente utilizamos o comando for para percorrer

todos os elementos de tuplas e atribuímos o primeiro elemento de cada uma à

variável x, e o segundo a variável y. Na sequência usamos o comando print para

imprimir x (a palavra) e y (a etiqueta).

No resultado, ‘Jersei’ possui a etiqueta N, que significa substantivo,

‘atinge’ possui a etiqueta V, que significa um verbo, as palavras ‘média’,

‘milhão’ e ‘venda’ também estão etiquetadas como substantivos.

38

3.1. O processo de etiquetação

Etiquetação é o processo de converter uma sentença, que é uma lista de

palavras, em uma lista de tuplas, onde cada tupla se encontra na formato (palavra,

etiqueta). A etiqueta indica se a palavra é um substantivo, um adjetivo, um artigo,

etc.

Após a tokenização, o processo de etiquetação é a segunda etapa no

processamento de linguagem natural. Ao etiquetar as palavras podemos extrair

frases com significados de um texto.

Para etiquetar palavras usamos etiquetadores. A maioria dos etiquetadores

são treináveis. Eles usam uma lista de sentenças já etiquetadas chamadas de

sentenças treinadoras. Nos nossos exemplos usaremos as sentenças treinadoras do

corpus Mac-Morpho.

3.1.1. Etiquetador padrão

O modo mais simples para etiquetar palavras é usando o etiquetador padrão.

O NLTK possui um etiquetador padrão chamada DefaultTagger que atribui a

mesma etiqueta para todas as palavras encontradas em um texto.

Apesar de não fazer muito sentido marcar toda palavra com a mesma

etiqueta, o etiquetador padrão é útil para etiquetar palavras desconhecidas. Assim,

ao combinarmos o etiquetador padrão com etiquetadores mais robustos, poderemos

agrupar todas as palavras desconhecidas em uma mesma categoria.

>>> from nltk.tag import DefaultTagger

>>> etiquetador = DefaultTagger('N')

>>> etiquetador.tag(['processamento', 'de', 'linguagem',

'natural', u'é', 'uma', u'área', 'da', u'inteligência',

'artificial'])

[('processamento', 'N'), ('de', 'N'), ('linguagem', 'N'),

('natural', 'N'), (u'\xe9', 'N'), ('uma', 'N'),

(u'\xe1rea', 'N'), ('da', 'N'), (u'intelig\xeancia', 'N'),

('artificial', 'N')]

>>>

No primeiro comando importamos a classe DefaultTagger do pacote tag.

Na segunda linha criamos um objeto etiquetador padrão, chamado de

etiquetador, que recebe a etiqueta ‘N’ como argumento.

39

No terceiro comando o método tag do objeto etiquetador recebe como

argumento uma lista de palavras e a converte em uma lista de tuplas, contendo as

palavras e suas respectivas etiquetas. Todas as palavras receberam a etiqueta 'N'

que significa substantivo.

3.1.2. Avaliando a precisão de um etiquetador

Para saber o quão preciso é um etiquetador, usamos o método

evaluate(), que recebe uma lista de sentenças treinadoras como as sentenças

do corpus Mac-Morpho, que já se encontram corretamente etiquetadas.

Vamos verificar a precisão do nosso etiquetador padrão criado no exemplo

anterior:

>>> from nltk.corpus import mac_morpho

>>> sentencas_treinadoras =

mac_morpho.tagged_sents()[0:15000]

>>> etiquetador.evaluate(sentencas_treinadoras)

0.20727113660113247

>>>

No primeiro comando importamos o corpus Mac-Morpho.

No segundo, atribuímos à variável sentencas_treinadoras as primeiras

15 mil sentenças do corpus mac_morpho. Nesse momento é criado um subconjunto

do corpus mac_morpho, contendo 15 mil sentenças corretamente etiquetas.

No terceiro comando, passamos a variável sentencas_treinadoras como

argumento para o método evaluate() do objeto etiquetador, para verificar a

precisão do nosso etiquetador. Como resultado obtemos um valor de 20, 72%. Ou

seja, nas 15 mil primeras sentenças do corpus Mac-Morpho, apenas

aproximadamente 20% das palavras são realmente substantivos, e portanto

receberam a etiqueta 'N'corretamente.

3.1.3. Etiquetador Unigram

Um etiquetador Unigram é baseado simplesmente em dados estatísticos. Ele

atribui uma etiqueta mais provável para cada token. Por exemplo, a palavra “azul”

40

pode ser classificada como um adjetivo, como em “O céu é azul”, ou como

substantivo em “A cor azul”. No etiquetador Unigram a palavra “azul” será sempre

representada como um adjetivo, pois baseado em dados estatísticos, essa palavra

aparece na maioria dos textos como um adjetivo, e por isso receberá a etiqueta ADJ.

Usaremos a mesma lista de palavras do exemplo anterior para receber

etiquetas com o uso do etiquetador Unigram, e depois mediremos a precisão desse

etiquetador.

>>> from nltk.tag import UnigramTagger

>>> sentencas_treinadoras =

mac_morpho.tagged_sents()[0:15000]

>>> etiquetador = UnigramTagger(sentencas_treinadoras)

>>> etiquetador.tag(['processamento', 'de', 'linguagem',

'natural', u'é', 'uma', u'área', 'da', u'inteligência',

'artificial'])

[('processamento',u'N'),('de',u'PREP'), ('linguagem',u'N'),

('natural',u'ADJ'),(u'\xe9',u'V'),('uma',u'ART'),(u'\xe1rea

',u'N'),('da',u'NPROP'),(u'intelig\xeancia','N'),('artifici

al',u'ADJ')]

>>>

No primeiro comando importamos a classe UnigramTagger do pacote tag.

No segundo, atribuímos à variável sentencas_treinadoras as primeiras

15 mil sentenças etiquetadas do corpus mac_morpho.

No terceiro comando, passamos o argumento sentencas_treinadoras

para treinar a classe UnigramTagger(), e o resultado desse treinamento é atribuído

à variável-objeto etiquetador. Nesse momento o objeto etiquetador tem uma

referência baseada em 15 mil frases já corretamente etiquetadas.

No último comando o método tag do objeto etiquetador recebe como

argumento uma lista de palavras e a converte em uma lista de tuplas, contendo as

palavras e suas respectivas etiquetas.

Para imprimir o resultado em uma forma mais legível podemos usar o comando

print, como já foi explicado nos capítulos anteriores.

>>> tuplas = etiquetador.tag(['processamento', 'de',

'linguagem','natural',u'é','uma',u'área','da',u'inteligênci

a', 'artificial'])

>>> for x, y in tuplas:

print x, y

41

processamento N

de PREP

linguagem N

natural ADJ

é V

uma ART

área N

da NPROP

inteligência N

artificial ADJ

>>>

No resultado as palavras processamento, linguagem, área e

inteligência foram corretamente marcadas como substantivos, a maioria das

outras também foram, como 'de' (preposição), 'natural' (adjetivo), 'é'

(verbo), 'uma' (artigo), e 'artificial' (adjetivo). A palavra 'da' foi etiquetada

como nome próprio 'NPROP', o que é um erro, pois 'da' é uma contração entre a

preposição ‘de’ e o artigo feminino ‘a’.

Os pesquisadores que criaram o corpus Mac-Morho substituíram todas as

contrações ‘da’ por de_PREP| + a_ART, e é por isso que a palavra ‘da’ do nosso

exemplo foi marcada incorretamente. Se quiséssemos que ela fosse devidamente

etiquetada, deveríamos mudar a nossa frase para: “processamento de linguagem

natural é uma área de a inteligência artificial”.

Mesmo assim, conseguimos desta vez um etiquetador bem mais preciso do

que o etiquetador padrão. Veremos agora como medir a precisão do etiquetador

Unigram:

>>> etiquetador.evaluate(sentencas_treinadoras)

0.87600245449464142

>>>

Analisando o resultado, chegamos à marca de 87% de precisão. Nenhum outro

etiquetador é mais preciso do que esse. No entanto, 13% dos tokens não foram

corretamente etiquetados, mesmo usando sentenças treinadoras idênticas às

sentenças avaliadoras. Isso ocorreu porque, como já citamos anteriormente, o

UnigramTagger se baseia em dados estatísticos e por isso não é 100% preciso.

‘Azul’ por exemplo, nem sempre é um adjetivo, a palavra ‘casa’ pode ser um

substantivo, mas às vezes é um verbo, e assim ocorre com diversas outras palavras.

42

3.1.4. Combinando etiquetadores

Podemos aumentar a precisão do processo de etiquetação quando

combinamos diferentes etiquetadores. Nessa técnica, dois ou mais etiquetadores

são utilizados em conjunto. Se um etiquetador não for capaz de etiquetar uma

palavra corretamente, a tarefa é passada para o próximo.

Nos exemplos a seguir usaremos um diferente trecho do corpus Mac-Morpho

para medir a precisão do UnigramTagger, depois o combinaremos com o

DefaultTagger para ver se conseguimos um etiquetador com maior precisão:

>>> from nltk.corpus import mac_morpho

>>> from nltk.tag import UnigramTagger

>>>sentencas_treinadoras=mac_morpho.tagged_sents()[0:1000]

>>> etiquetador = UnigramTagger(sentencas_treinadoras)

>>> sentencas_teste = mac_morpho.tagged_sents()[1000:2000]

>>> etiquetador.evaluate(sentencas_teste)

0.72472077122351042

Primeiro importamos o corpus mac_morpho e em seguida importamos a

classe UnigramTagger.

Na terceira linha atribuímos à variável sentenças_treinadoras as

primeiras 1000 sentenças etiquetadas (de 0 a 999), do corpus mac_morpho.

No quarto comando, passamos o argumento sentencas_treinadoras

para treinar a classe UnigramTagger(), e o resultado desse treinamento é atribuído

à variável-objeto etiquetador.

No quinto comando criamos uma variável chamada sentencas_teste, que

também recebe 1000 sentenças do corpus mac_morpho, só que de um diferente

trecho do corpus, iniciando na sentença de número 1000 e indo até a sentença de

número 1999.

No último comando avaliamos a precisão do nosso etiquetador usando como

argumento a variável sentenças_teste, obtendo uma precisão de 72,47%.

Agora iremos combinar o UnigramTagger com o DefaultTagger:

>>> from nltk.tag import DefaultTagger

>>> etiquetador1 = DefaultTagger('N')

43

>>> etiquetador2 = UnigramTagger(sentencas_treinadoras,

backoff=etiquetador1)

>>> etiquetador2.evaluate(sentencas_teste)

0.77564020894381447

No primeiro comando importamos a classe DefaultTagger.

No segundo comando criamos um objeto etiquetador padrão, chamado de

etiquetador1, que recebe a etiqueta ‘N’ (substantivo) como argumento.

No terceiro comando criamos um segundo objeto etiquetador chamado

etiquetador2, baseado na classe UnigramTagger, que recebe como

argumento as variáveis sentenças_treinadoras e backoff=etiquetador1.

A declaração backoff=etiquetador1 significa que o etiquetador1

marcará com a etiqueta ‘N’ todas as palavras que o etiquetador2 não conseguir

marcar. Seria como dizer: “Etiquetador2, marque as palavras que encontrar

baseando-se nas sentenças treinadoras, caso não consiga, use o etiquetador1”.

Aparentemente não há sentido em classificar tudo o que for desconhecido

como substantivo, mas baseada em dados estatísticos, a chance de uma palavra

desconhecida ser um substantivo é de aproximadamente 20%, como já foi visto na

seção 3.1.2., quando atribuímos a etiqueta ‘N’ para todas as palavras das primeiras

15 mil sentenças do corpus Mac-Morpho.

No último comando avaliamos o etiquetador2, e podemos constatar que o

seu ganho de precisão foi de aproximadamente 5% (0.77564 menos 0.72472).

Além dos etiquetadores vistos até agora, existem outros como

BigramTagger, TrigramTagger, Brill Taggers e TnT Taggers, que podem auxiliar

no aumento de precisão de um etiquetador. Também podemos utilizar outras

técnicas como o uso de expressões regulares, que em conjunto com os

etiquetadores já mencionados, podem levar a precisão de um etiquetador a mais de

95%.

44

4. ANÁLISE SINTÁTICA PARCIAL

A Análise Sintática Parcial é o processo de extrair pequenas frases de

sentenças etiquetadas. É diferente da Análise Sintática Completa, pois na primeira

estamos apenas interessados em frases curtas ao invés de Árvores de Análise

Sintáticas Completas, que serão estudadas posteriormente.

Falaremos nesse capítulo sobre algumas técnicas utilizadas em PLN para

extrair pequenas frases de um texto, apenas observando padrões particulares em

uma sequência de palavras etiquetadas.

4.1. Expressões regulares para a identificação de padrões

Com o uso de expressões regulares modificadas podemos identificar padrões

em uma linguagem. Esses padrões definem quais os tipos de palavras compõem um

bloco de uma sentença.

Expressões regulares também podem definir padrões para palavras que não

desejamos que façam parte de um bloco.

Agrupar palavras é o processo de isolar padrões em uma sentença para

manter as palavras em um bloco. O contrário disso é o processo de desagrupar.

Uma regra de agrupamento especifica quais grupos devem formar um bloco,

e uma regra de desagrupamento especifica quais blocos isolar.

Para criar uma regra, a primeira tarefa é definir os padrões do agrupamento.

Esses padrões são expressões regulares modificadas equivalentes às sequências

de palavras etiquetadas.

Para definir uma etiqueta, colocamo-la entre os sinais de menor que (<) e

maior que (>), como em <N>, que especifica uma etiqueta para um substantivo.

Várias etiquetas podem ser combinadas, como em <ART><N><ADJ>, que

equivalem a um artigo, seguido por um substantivo, seguido por um adjetivo. Uma

frase que respeita esse padrão poderia ser: O carro azul, pois ‘O’ é um artigo,

seguido por ‘carro’ que é um substantivo, seguido por ‘azul’, um adjetivo.

A sintaxe das expressões regulares pode ser utilizada tanto dentro como fora

dos símbolos < >.

45

Em <N.*> por exemplo, queremos encontrar qualquer etiqueta que inicie com

a letra N, seguida por qualquer outro símbolo (representado pelo sinal de

pontuação), zero ou mais vezes (representado pelo metacaractere asterisco).

Portanto, esse padrão reconhecerá a etiqueta que representa um substantivo

no singular, como em <N>, pois a etiqueta começa com a letra N seguida de

nenhuma letra. Esse padrão também reconhecerá a etiqueta para substantivos no

plural <NP>, pois o padrão <N.*> também reconhece a letra N seguida por qualquer

outra letra.

Os substantivos ‘computador’ e ‘computadores’ se encaixam nessa regra,

pois possuem as etiquetas <N> e <NP>, respectivamente.

Um exemplo do uso da sintaxe de expressões regulares fora dos símbolos <

> poderia ser como em <ART>?<N><ADJ>+.

O metacaractere ‘?’ especifica que a etiqueta anterior a ele (<ART>), é

opcional, e o metacaractere ‘+’ especifica que a etiqueta anterior a ele (<ADJ>),

deve aparecer uma ou muitas vezes.

As blocos de texto a seguir obedecem a esse padrão:

‘A cidade grande’: um artigo seguido por um substantivo e por um adjetivo.

‘Ciência moderna’: um substantivo seguido por um adjetivo, (a presença de

um artigo no início da frase artigo é opcional).

‘O edifício verde claro’: um artigo seguido por um substantivo, seguido por

dois adjetivos.

A criação de padrões para agrupar e desagrupar blocos de palavras em um

texto depende muito da necessidade e da criatividade do desenvolvedor. Podemos

dizer que é um processo de tentativa e acerto, e que vai se aperfeiçoando com

prática.

46

4.2. Utilizando gramáticas para agrupar e desagrupar palavras

Um padrão para agrupar blocos é definido entre chaves normais, como em

{<ART><N>}, e um padrão para desagrupá-los é definido entre chaves opostas

com em }<V>{. A regra de agrupamento ou desagrupamento que especifica o bloco

entre as chaves é chamada de gramática do bloco ou gramática da frase.

Vamos agora criar uma gramática para extrair frases sem verbos de uma

sentença, e para isso usaremos regras de agrupamento e de desagrupamento. Em

seguida construiremos uma árvore de análise para a sentença.

>>> from nltk.chunk import RegexpParser

>>> analis_gram = RegexpParser(r'''

FN:

{<ART><N><.*>*<N>}

}<V>{

''')

>>>

A primeira linha do código importa a classe RegexParser do pacote chunk

da biblioteca NLTK. O pacote chunk possui uma série de classes e interfaces para

identificar grupos linguísticos não sobrepostos, como frases verbais e nominais.

RegexParser é uma classe do pacote chunk, que usa expressões regulares para

analisar as etiquetas de um texto e transformá-lo em blocos.

Na segunda linha instanciamos um objeto da classe RegexpParser e o

chamamos de analis_gram (analisador gramatical), que recebe entre os

parênteses as regras para agrupamento e desagupamento do texto. O caractere ‘r’

especifica que devemos interpretar os códigos nas próximas linhas como

expressões regulares, e as três aspas simples colocadas antes e depois das

declarações significam que podemos escrever o código em múltiplas linhas, isso

serve apenas para melhorar a leitura do mesmo.

Na terceira linha definimos a nossa gramática como ‘FN’ (frase nominal). Uma

frase nominal é aquela que não possui verbos.

Nas linhas 4 e 5 definimos as regras para a gramática. A primeira regra, entre

chaves normais, diz que devemos agrupar o padrão formado por um artigo <ART>,

seguido por um substantivo <N>, seguido por zero ou mais palavras contendo

qualquer tipo de etiqueta <.*>*, até encontrar outro substantivo <N>.

A segunda regra, entre chaves opostas, diz que os verbos <V>, devem ser

desagrupados.

47

Utilizaremos o nosso analisador gramatical para analisar a sentença “O

mundo possui diversos idiomas”, que já está com as palavras devidamente

etiquetadas.

>>>analis_gram.parse([('O','ART'),('mundo', N'),('possui',

'V'), ('diversos', 'ADJ'), ('idiomas', 'N')])

Tree('S', [Tree('FN', [('O', 'ART'), ('mundo', 'N')]),

('possui', 'V'), Tree('FN', [('diversos','ADJ'),

('idiomas', 'N')])])

Na primeira linha de código, como o objeto analis_gram foi instanciado a

partir da classe RegexpParser, usamos o método parse dessa classe para aplicar

as regras da gramática nas palavras etiquetadas.

Como resultado, obtemos a sentença na forma de uma árvore ‘Tree’. O

primeiro nó da árvore ‘S’ significa sentença. Os nós restantes representam os blocos

encontrados e representam subárvores da sentença.

Cada subárvore é definida dentro de colchetes, e inicia com a palavra Tree,

seguida de tuplas entre parênteses que contém a palavra e sua respectiva etiqueta.

Por exemplo, no trecho [Tree('FN', [('O', 'ART'), ('mundo',

'N')], temos uma subárvore do tipo FN (frase nominal), formada pela lista de

tuplas [('O', 'ART'), ('mundo', 'N')]. Essa subárvore é denominada FN

pois atende as regras estabelecidas pela nossa gramática. Notem que a

tupla('possui', 'V'), não faz parte de uma subárvore, pois pela gramática os

verbos devem ser desagrupados e por isso ela não é precedida por Tree'FN'.

Para entendermos melhor o resultado do código, podemos desenhar a árvore

utilizando o método draw():

>>> arvore = analis_gram.parse([('O', 'ART'), ('mundo',

'N'),('possui', 'V'), ('diversos', 'ADJ'), ('idiomas',

'N')])

>>> arvore.draw()

48

Obtemos agora uma representação gráfica da sentença que é uma árvore

divida em nós e folhas. O primeiro nó ‘S’ representa toda a sentença, os nós ‘FN’

representam subárvores de frases nominais. As folhas ‘O ART’ e ‘mundo N’, por

exemplo, formam uma frase nominal e por isso pertencem ao nó FN. A folha

‘possui V’ não pertence a nenhuma subárvore, pois foi desagrupada pela regra

gramatical.

Existem outras regras para manipular blocos, como as regras de fusão, que

unem blocos distintos e as regras de divisão que dividem blocos.

Juntas, as regras de agrupamento, desagrupamento, fusão e divisão, são

suficientes para lidar com a maioria das tarefas envolvidas na análise gramatical de

blocos de sentenças.

5. CONSTRUINDO UM SISTEMA PARA CORREÇÃO VERBAL

Agora que já sabemos como extrair blocos de frases em uma sentença

etiquetada, veremos o que podemos fazer com esses grupos.

Um exemplo poderia ser o de criar um filtro para excluir palavras de pouco

significado em um texto, obtendo assim apenas as palavras mais importantes.

No texto “O parque estava lotado”, se filtrarmos o artigo ‘O’ e o verbo ‘estava’,

ficaremos apenas com o substantivo ‘parque’ e o adjetivo ‘lotado’. Mesmo assim

conseguiremos entender o significado da frase “parque lotado”. Esse tipo de

manipulação de textos é bastante usado quando é preciso extrair informações

relevantes de grandes quantidades de dados.

Em outro exemplo poderíamos criar um analisador de textos para obter todas

as palavras compostas de um documento, como ‘papel-moeda’, ‘arco-íris’, ‘meio-dia’,

‘obra-prima’, etc. Ou talvez para obter os nomes próprios contidos no documento,

como ‘Paulo’, ‘Maria’, ‘Museu do Ipiranga’ ou ‘Avenida Paulista’.

Os tipos de aplicações que podem utilizar essas técnicas são os mais

diversos e dependem muito da necessidade e da criatividade do usuário. Como a

quantidade de texto no formato eletrônico vem crescendo a taxas enormes, cada vez

mais será importante conhecer esses princípios de manipulação de textos para que

possamos ter acesso às informações desejadas.

Nesse capítulo construiremos um pequeno sistema para realizar a correção

de verbos em sentenças.

49

A ideia que nos motivou a criar um corretor verbal foi a de demonstrar como

as metodologias e técnicas aplicadas nesse sistema, podem ser utilizadas em

sistemas semelhantes, como corretores ortográficos e outros mais complexos, como

os corretores gramaticais que fazem amplo uso dos conceitos do processamento de

linguagem natural.

5.1. Dicionários em Python

Antes de tudo, precisamos introduzir brevemente um conceito pertencente à

linguagem de programação Python, chamado de dicionário.

Um dicionário em Python representa uma coleção de elementos formada por

pares constituídos por chave e valor respectivamente.

Eis um exemplo de dicionário:

nosso_dicionario = {'idade' : 20, 'estado' : ‘SP’,

8: 'oito'}

>>> print nosso_dicionario[8]

oito

# O valor da chave 8 é a string ‘oito’.

E o valor da chave ‘idade’ é o número inteiro 20.

Dicionários podem armazenar dados como inteiros, decimais, listas, tuplas,

strings, etc., e são declarados dentro dos símbolos de chaves, ‘{ }’. O símbolo de

dois pontos separa a chave do valor, e a vírgula separa os pares.

5.2. Definindo os dicionários

A primeira tarefa para criar o nosso corretor verbal será a de definir os

dicionários. Esses dicionários armazenarão a conjugação do verbo ‘ser’ nos tempos

‘presente do indicativo’ e ‘pretérito imperfeito’, e nas 3ªs. pessoas, do singular e do

plural. E cada tupla do dicionário armazenará o verbo e sua respectiva etiqueta.

A função dos dicionários é apontar a conjugação correta de um verbo que se

encontra incorretamente conjugado.

A tabela abaixo exemplifica as conjugações:

50

Verbo ‘ser’

3ª. Pessoa do Singular (VS) (ele/ela)

3ª. Pessoa do Plural (VP) (eles/elas)

Presente do Indicativo (PI) é são

Pretérito Imperfeito (PIm) era eram

Criaremos agora dois dicionários, um de ‘singular para plural’ e outro de

‘plural para singular’:

>>> singular_plural = {

('é', 'VSPI'): ('são', 'VPPI'),

('era', 'VSPIm'): ('eram', 'VPPIm')

}

>>> plural_singular = {

('são', 'VPPI'): ('é', 'VSPI'),

('eram', 'VPPIm'): ('era', 'VSPIm')

}

>>>

No primeiro bloco de código criamos um dicionário de Python chamado de

singular_plural, que indica qual a correção dos verbos que estão no singular

para suas respectivas conjugações no plural.

Entre as chaves inserimos quatro tuplas. A primeira tupla ('é', 'VSPI') é

a chave do valor ('são', 'VPPI').

Isso quer dizer que quando o corretor verbal solicitar a correção da palavra

‘é’, a chave-tupla ('é', 'VSPI') apontará para o valor ('são', 'VPPI').

A tupla ('era', 'VSPIm') apontará para ('eram', 'VPPIm').

Esse princípio também se aplica para as tuplas restantes.

No segundo bloco de código criamos outro dicionário de Python chamado de

plural_singular, que indica qual correção deverá ser feita para os verbos que

se encontram incorretamente conjugados no plural.

As etiquetas utilizadas representam uma junção das etiquetas do verbo e

suas respectivas flexões gramaticais (singular e plural), com as etiquetas dos

tempos verbais. A etiqueta VPPIm por exemplo, significa Verbo Plural Pretérito

Imperfeito. Para deduzir o significado das outras etiquetas, basta consultar a tabela.

51

5.3. Definindo uma função auxiliar

Agora que já construímos os dicionários, o próximo passo a ser tomado é o

de definir uma função auxiliar que percorrerá as tuplas de um bloco para verificar se

elas possuem um determinado atributo. Caso esse atributo seja encontrado, a

função retornará o índice da primeira tupla onde o atributo foi encontrado, caso

contrário ela retornará nada. O atributo pode ser uma etiqueta de verbo ou de

substantivo.

>>> def indice_atributo(frase, atributo, inicio=0,

incremento=1):

comprimento = len(frase)

final = comprimento if incremento > 0 else -1

for indice in range(inicio, final, incremento):

if atributo(frase[indice]):

return indice

return None

>>>

Para o texto a seguir, adotaremos como referência uma frase de comprimento

‘n’ igual a 5 tuplas.

Na primeira linha do código usamos a palavra chave def para definir uma

função que nomearemos de indice_atributo. A função possui quatro

parâmetros, o primeiro (frase) é uma lista de tuplas a ser analisada, o segundo

(atributo) é o resultado de uma função lambda que retorna a etiqueta

identificadora de um verbo ou de um substantivo. Os outros dois parâmetros

(inicio e incremento) já iniciam com valores padrões 0 e 1 respectivamente. O

parâmetro inicio se refere ao primeiro índice do bloco a ser analisado (índice 0), e

é incrementado pela variável incremento a cada looping.

Na linha comprimento = len(frase), atribuímos à variável

comprimento o tamanho da variável frase, com o auxilio da função len() de

Python. Portanto, o valor de comprimento será 5.

Na declaração final = comprimento if incremento > 0 else -1,

a variável final receberá o valor 5 se a variável incremento for positiva, caso

contrário final receberá o valor -1. Esse valor -1 determina que a lista seja

percorrida de frente para trás, pois em alguns momentos o corretor verbal precisará

conferir se a tupla que antecede um verbo é constituída por um substantivo.

Poderemos compreender melhor essa situação quando executarmos o corretor

verbal.

No comando seguinte, for, nomeamos uma variável de indice que

percorrerá a lista de tuplas. Para isso utilizamos a função embutida de Python

52

range(). A finalidade dessa função é a de interar sobre uma sequência numérica

que são os parâmetros entre parênteses. O parâmetro, inicio, determina que o

looping inicie no índice 0 da tupla, e que seja incrementado por 1

(incremento=1), até alcançar o índice final 5.

Em if atributo(frase[indice]):, é determinado que se a variável

atributo (uma etiqueta verbal ou nominal), for encontrada na tupla, o índice dessa

tupla será retornado pela função. Caso o atributo procurado não seja encontrado na

frase, a função retornará None, uma palavra reservada de Python que em português

significa Nada ou Nenhum.

5.4. Definindo uma função para correção Verbal

Após definirmos os dicionários e a função auxiliar, só nos resta agora definir a

função responsável pela correção verbal, e é isso o que faremos a seguir.

Com a intenção de facilitar a leitura do código, copiamos a tela do editor

interativo Python que contém a definição da função corretor_verbal e a

anexamos na próxima página deste trabalho.

Na sequência explicaremos o funcionamento da função.

53

54

Na primeira linha do código definimos a função corretor_verbal, que

recebe frase como parâmetro. Esse parâmetro é uma lista de tuplas da frase a ser

analisada. Cada tupla conterá a palavra e sua respectiva etiqueta.

Na linha seguinte temos:

indice_verbo = indice_atributo(frase, lambda (palavra,

etiqueta): etiqueta.startswith('V')), onde é criada uma variável

chamada indice_verbo que receberá o índice da tupla onde o primeiro verbo é

encontrado na frase.

Para que isso ocorra, a função indice_atributo percorrerá

sequencialmente as tuplas da frase até encontrar o primeiro verbo. Caso nenhum

verbo seja encontrado o valor retornado será nenhum e a frase será impressa na

tela sem modificações.

Se um verbo for encontrado, a variável indice_verbo receberá a posição

da tupla na qual o verbo se encontra.

A seguir, em verbo, etiqueta_verbo = frase[indice_verbo] é

criada uma variável do tipo tupla chamada verbo, etiqueta_verbo que recebe

o conteúdo da variável indice_verbo.

Nesse momento a tupla (verbo, etiqueta_verbo) e indice_verbo se

encontram na memória.

Agora verificaremos se encontramos um substantivo do lado direito do verbo,

caso não encontremos, verificaremos do lado esquerdo.

Em atributo_substantivo = lambda (palavra, etiqueta):

etiqueta.startswith('N'), a variável atributo_substantivo receberá o

valor ‘N’, caso uma tupla contendo um substantivo seja encontrada.

Em indice_substantivo = indice_atributo(frase,

atributo_substantivo, inicio=indice_verbo+1), a variável

indice_substantivo receberá o índice da tupla onde o substantivo é encontrado

na frase.

Caso nenhum substantivo seja encontrado ao lado direito do verbo então ele

será procurado do lado esquerdo, conforme definido a seguir:

if indice_substantivo is None:

indice_substantivo = indice_atributo(frase,

atributo_substantivo, inicio=indice_verbo-1,

incremento=-1)

55

Caso ele também não seja encontrado do lado esquerdo, a frase será

impressa na tela sem modificações.

if indice_substantivo is None:

return frase

Se um substantivo for encontrado, então o conteúdo referente ao seu índice

será atribuído à variável substantivo, etiqueta_substantivo, conforme

demonstrado a seguir:

substantivo, etiqueta_substantivo =

frase[indice_substantivo]

Em sua última etapa, a função verificará se o substantivo encontrado está no

plural. Em caso positivo, ele fará a correção do verbo, se o mesmo se encontrar no

singular, e então a frase correta será impressa na tela.

Se o substantivo encontrado estiver no singular, então ele verificará se o

verbo da frase também se encontra no singular, caso não se encontre, o verbo será

corrigido e a frase correta será impressa na tela.

if etiqueta_substantivo.endswith('S'):

frase[indice_verbo] = singular_plural.get((verbo,

etiqueta_verbo), (verbo, etiqueta_verbo))

else:

frase[indice_verbo] = plural_singular.get((verbo,

etiqueta_verbo), (verbo, etiqueta_verbo))

return frase

Para testar o nosso corretor verbal, criamos algumas frases com verbos e

substantivos no início e meio da frase:

Exemplo 1, verbo incorreto (presente/singular), no inicio da frase:

>>> frase = [('é', 'VSPI'), ('caras', 'ADV'),

('as','ART'), ('passagens', 'NS')]

>>> corretor_verbal(frase)

[('s\xe3o', 'VPPI'), ('caras', 'ADV'), ('as', 'ART'),

('passagens', 'NS')]

Para tornar o resultado mais legível usamos o comando print:

>>> for x, y in frase:

print x, y,

são VPPI caras ADV as ART passagens NS

56

Exemplo 2, verbo incorreto (presente/plural), no meio da frase:

>>> frase = [('a', 'ART'), ('cidade', 'N'),

('são','VPPI'), ('distante', 'ADJ')]

>>> corretor_verbal(frase)

[('a', 'ART'), ('cidade', 'N'), ('\xe9', 'VSPI'),

('distante', 'ADJ')]

>>> for x, y in frase:

print x, y,

a ART cidade N é VSPI distante ADJ

Exemplo 3, verbo incorreto (pretérito/singular), no meio da frase:

>>> frase = [('as', 'ART'), ('soluções', 'NS'),

('era','VSPIm'), ('muitas', 'PRONADJ')]

>>> corretor_verbal(frase)

[('as', 'ART'), ('solu\xe7\xf5es', 'NS'), ('eram',

'VPPIm'), ('muitas', 'PRONADJ')]

>>> for x, y in frase:

print x, y,

as ART soluções NS eram VPPIm muitas PRONADJ

Exemplo 4, verbo incorreto (pretérito/plural), no meio da frase:

>>> frase = [('Quando', 'ADV'), ('ela', 'PRON'),

('eram','VPPIm'), ('pequena', 'N')]

>>> corretor_verbal(frase)

[('Quando', 'ADV'), ('ela', 'PRON'), ('era', 'VSPIm'),

('pequena', 'N')]

>>>

Percebemos que, a partir dos exemplos, o nosso corretor verbal se

comportou bem em situações controladas. Apesar de ele estar longe se der

um corretor verbal real, conseguimos verificar na prática a aplicação de

alguns dos princípios do processamento de linguagem natural na prática..

57

6. CONCLUSÃO

Nesse trabalho conseguimos entender o funcionamento do

Processamento de Linguagem Natural e aprender algumas das principais

técnicas que podem auxiliar nessa área.

Encontramos algumas dificuldades como, por exemplo, a de lidar com

os caracteres da língua portuguesa, já que a grande maioria das publicações

sobre esse assunto se encontram na língua inglesa e, portanto voltadas para

o processamento de textos em inglês.

Também encontramos certa dificuldade para encontrar corpora

preparada em português. No nosso trabalho utilizamos o corpus Mac-Morpho

em boa parte dos exemplos, mas infelizmente esse corpus apesar de conter

mais de 1 milhão de palavras etiquetadas, não possui as sentenças no

formato de árvores, o que é fundamental para uma análise sintática completa.

No entanto, acreditamos que atingimos o nosso objetivo principal, que

era o de explicar como é feito o Processamento de Linguagem Natural e o

que é preciso para que ele seja realizado

58

REFERÊNCIAS BIBLIOGRÁFICAS

BIRD, Steven; KLEIN, Ewan; LOPER, Edward. Natural Language Processing with

Python. First Edition. Sebastopol, CA O’Reilly Media, Inc., 2009

PERKINS, Jacob. Python Text Processing with NLTK 2.0 Cookbook. First

Edition. Birmingham, UK. Packt Publishing Ltd.,2010

MCNEIL, Jeff. Python 2.6 Text Processing Beginner's Guide. First Edition.

Birmingham, UK. Packt Publishing Ltd.,2010

Sites na Internet:

http://www.python.org/

http://code.google.com/p/nltk/

http://nltk.org/