Treinando modelos do parser de Stanford 
Autor: Erick | Publicado: 2016-03-01T19:44:00-08:00

O Stanford CoreNLP é um conjunto de ferramentas para o processamento de línguas naturais bastante utilizado por pesquisadores e desenvolvedores.&nbsp;Modelos treinados para inglês para todas as funcionalidades do CoreNLP estão disponíveis para download, além de modelos para outras línguas com algumas funcionalidades.&nbsp;Neste post, vou mostrar como treinar um POS Tagger e um parser de dependências para o português.


Quem está acostumado com o meio acadêmico da computação sabe que são raros os casos em que um software desenvolvido por pesquisadores é fácil de se usar ou tem uma boa documentação; e isso quando o mesmo está disponível para uso. Muitas vezes, os pesquisadores querem apenas avaliar o desempenho de algoritmos, sem ter em mente sua aplicação posterior.&nbsp;Felizmente, o CoreNLP é uma exceção: funciona bem e sua documentação é razoavelmente boa.&nbsp;De qualquer forma,&nbsp;com a popularidade do GitHub, parece que estamos tendo uma melhora nesse tendência.



Breve explicação de arvores de dependência

Árvores de dependência são uma forma de representação da estrutura sintática de uma sentença, seguindo a gramática de dependências, e são úteis no pré-processamento de diversas aplicações de PLN. Numa árvore sintática, cada token&nbsp;está ligado a um outro,&nbsp;que é dito seu head.&nbsp;Um exemplo da Wikipedia:





No exemplo, "convention"&nbsp;e "vary"&nbsp;têm "can" como head, e por isso são considerados modificadores deste token. O token&nbsp;"can"&nbsp;não tem um head; portanto é a raiz da árvore. Normalmente, a raiz é um verbo. Além disso, cada relação entre dois tokens, chamada de dependência, pode ter um rótulo, indicando conceitos como adjunto adnominal, sujeito, objeto direto, determinante etc.



Treinamento do modelo

Corpus

Para treinarmos um novo modelo, a primeira coisa que precisamos é de um corpus anotado com árvores sintáticas - e essa tem sido uma grande carência da língua portuguesa.&nbsp;



Durante anos, tínhamos somente a&nbsp;Floresta Sintá(c)tica,&nbsp;composta por textos brasileiros e portugueses e compilada pela equipe da Linguateca. O tamanho é bem limitado: a parte revisada tem cerca de 215 mil tokens, contra mais de quatro milhões do Penn Treebank.



O problema maior não é nem o tamanho do corpus, mas a tokenização usada. Na Floresta Sintá(c)tica, expressões como pelo menos, em frente a, por causa de&nbsp;e várias outras são tratadas como um único token.&nbsp;Dessa forma, um parser treinado nesse corpus fica "acostumado" a tratar essas expressões como uma única palavra, e&nbsp;cometeria erros ao analisar um texto que não tivesse passado por esse processo (nada simples) de pré-processamento.



Há poucos anos, porém, foi lançado o Universal Dependencies&nbsp;(UD), um projeto que apresenta corpora em diversas línguas anotados segundo o mesmo formalismo. O corpus em português do UD não é muito maior que a porção anotada da Floresta Sintá(c)tica, mas não tem o problema dos multitokens.


Se você pretende usar esse corpus, aconselho pegar a versão editada pelo Pedro Balage (colega de laboratório), onde alguns erros no corpus foram corrigidos. Ela pode ser encontrada nesse repositório, na pasta pt-br-corrected.



POS Tagger
A maioria dos parsers sintáticos precisa que os textos de entrada já estejam anotados com classes gramaticais, ou part-of-speech&nbsp;(POS) tags. O corpus de treinamento já possui esse tipo de anotação, mas para usarmos o parser com novos textos que precisarmos processar, um POS tagger será necessário.

Eu desenvolvi a nlpnet, uma biblioteca que usa redes neurais para algumas tarefas em PLN e adoraria poder usá-la aqui também, já que tem uma excelente performance com POS em português. No entanto, o CoreNLP precisa que o POS tagger seja chamado dentro do seu código, em Java. Criar uma versão da nlpnet dentro da estrutura do CoreNLP é perfeitamente possível, mas também seria bem trabalhoso. No fim das contas, é bem mais fácil usar o tagger do próprio CoreNLP (eu cheguei a comparar a performance dele com a da nlpnet treinada no mesmo corpus, e a nlpnet de fato é um pouco melhor).

Para treinar um modelo do tagger, é preciso criar um arquivo de propriedades (que indica parâmetros de treinamento). Use o seguinte comando no terminal (considerando que esteja no diretório do CoreNLP):

java &nbsp;-cp "*;lib/*" edu.stanford.nlp.tagger.maxent.MaxentTagger -genprops
A sintaxe acima para o -cp se aplica ao Windows. Em Linux (e imagino que também Mac, mas não testei), o argumento é&nbsp;"*:lib/*"&nbsp;(usa dois pontos em vez de ponto-e-vírgula para juntar os diretórios do classpath).

Gerado o arquivo, editei os seguintes campos:

model = pt-model/pt-pos-tagger.dat
arch = left3words,bidirectional,generic,words(-1,1), unicodeshapes(-1,1), suffix(4), prefix(4)
trainFile = format=TSV,wordColumn=1,tagColumn=4,pt-model/pt-br-universal-train.conll
encoding = UTF-8

As linhas começando com # são comentários. model indica o nome do arquivo do modelo a ser salvo. arch é uma série de diferentes parâmetros para o funcionamento do tagger, e talvez examinar diferentes combinações leve a melhores resultados. trainFile especifica o nome e formato do arquivo com os dados de treino, e o formato TSV indica Tab Separated Values. Note que esse formato é válido para o corpus do Universal Dependencies, mas se você estiver usando um corpus diferente, talvez tenha que mudar. Por fim, encoding é sempre bom se usar UTF-8.

Pronto o arquivo, é só rodar o tagger:

java -cp "javanlp-core.jar;lib/*" &nbsp;edu.stanford.nlp.tagger.maxent.MaxentTagger -prop tagger-props.txt

O CoreNLP vai mostrar na tela o progresso do treinamento. Para avaliar o modelo treinado, basta trocar a linha do trainFile por testFile e dar o nome do arquivo com dados de teste. O modelo que treinei obteve uma acurácia geral de&nbsp;96,83%, sendo&nbsp;91,38% das palavras desconhecidas (ausentes do conjunto de treino) corretas. Nada mal.



Parser

Com o tagger treinado, podemos treinar o parser. Note que as ferramentas do CoreNLP não seguem sempre o mesmo padrão de configuração: enquanto o POS tagger usa um arquivo com os parâmetros, para o parser de dependências é tudo passado via linha de comando. Aqui está a chamada que fiz:

java -cp "javanlp-core.jar;lib/*" edu.stanford.nlp.parser.nndep.DependencyParser -trainFile pt-model/pt-br-universal-train.conll -devFile pt-model/pt-br-universal-dev.conll -embedFile pt-model/embeddings-pt.txt -embeddingSize 50 -model&nbsp;pt-model/dep-parser&nbsp;

A opção -embedFile requer um arquivo de word embeddings, os vetores numéricos representando palavras, algo que tem se tornado bastante comum e útil no PLN nos últimos anos. Se você não tiver um modelo já pronto, pode usar o que eu treinei&nbsp;sobre a Wikipedia e sites de notícias. É bem abrangente e deu ótimos resultados.&nbsp;

Caso use o meu modelo, veja que ele está no formato do NumPy, acompanhado por um outro arquivo com o vocabulário, e o formato para o CoreNLP tem que ter, em cada linha, uma palavra e os valores do seu vetor, tudo separado por tabulação. É uma conversão bem fácil de se fazer.

Além disso, é preciso dizer&nbsp;no argumento&nbsp;-embeddingSize&nbsp;o tamanho de cada vetor.&nbsp;Veja também que, ao contrário do tagger, o parser já aproveita o arquivo de validação (o devFile) durante o treinamento.

Por último, um aviso: a configuração padrão do CoreNLP treina o parser por 20.000 iterações, o que levou praticamente 24 horas no meu computador. Caso queira acelerar o processo (e naturalmente, perder um pouco de performance), pode passar um número menor para o argumento&nbsp;-maxIter.



Avaliação e Execução

Para avaliar a performance do parser, é só chamar na linha de comando:

java -cp "javanlp-core.jar;lib/*" edu.stanford.nlp.parser.nndep.DependencyParser -model pt-model/dep-parser -testFile pt-model/pt-br-universal-test.conll


A avaliação de parsers de dependência é normalmente medida em UAS (Unlabeled Attachment Score) e LAS (Labeled Attachment Score). O primeiro diz respeito à proporção de tokens que tiveram seu head&nbsp;corretamente identificado, e o segundo considera, dentre esses, quais tiveram seu tipo de relação etiquetada corretamente. Meu modelo obteve 88,60% de UAS e 86,96% de LAS. São valores claramente abaixo do que se consegue em inglês, mas considerando o tamanho do corpus em que treinei, estão muito bons.&nbsp;

O uso das word embeddings já fornece ao parser um pouco de conhecimento prévio, de modo que ele não comece o treinamento totalmente sem conhecimento. Por isso, seria muito difícil que um outro parser, sem usar embeddings, alcançasse um desempenho igual ou maior sendo treinado apenas nesse corpus.

Para rodar o parser com um texto qualquer, é só executar o comando:

java -cp "javanlp-core.jar;lib/*" edu.stanford.nlp.parser.nndep.DependencyParser -model pt-model/dep-parser -tagger.model pt-model/pt-pos-tagger.dat -textFile arquivo.txt

Ele vai ler o arquivo passado como argumento de -textFile, executar o POS tagger e em seguida o parser. Lembrando que essa configuração que apresentei aqui não&nbsp;usa tokenização, ou seja: o conteúdo do seu arquivo de entrada já deve ter todos os tokens separados por espaço em branco.



Enfim!

Em tempo,&nbsp;esse post&nbsp;me ajudou bastante a entender como configurar o parser e fazer a coisa toda funcionar bem.&nbsp;

Como provavelmente algumas pessoas prefeririam rodar um parser já treinado (ainda mais com o tempo demorado de treinamento), pretendo disponibilizar em breve um link com o modelo que treinei&nbsp;o modelo treinado pode ser baixado aqui.&nbsp;

Espero que esta postagem tenha sido útil. Bom trabalho com o parser agora!