terça-feira, 1 de março de 2016

Treinando modelos do parser de Stanford

O Stanford CoreNLP é um conjunto de ferramentas para o processamento de línguas naturais bastante utilizado por pesquisadores e desenvolvedores. 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. 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. Felizmente, o CoreNLP é uma exceção: funciona bem e sua documentação é razoavelmente boa. De qualquer forma, 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 está ligado a um outro, que é dito seu head. Um exemplo da Wikipedia:



No exemplo, "convention" e "vary" têm "can" como head, e por isso são considerados modificadores deste token. O token "can" 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. 

Durante anos, tínhamos somente a Floresta Sintá(c)tica, 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 e várias outras são tratadas como um único token. Dessa forma, um parser treinado nesse corpus fica "acostumado" a tratar essas expressões como uma única palavra, e 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 (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 (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  -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 é "*:lib/*" (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/*"  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 96,83%, sendo 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 pt-model/dep-parser 

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 sobre a Wikipedia e sites de notícias. É bem abrangente e deu ótimos resultados. 

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 no argumento -embeddingSize o tamanho de cada vetorVeja 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 -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 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. 

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 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, esse post me ajudou bastante a entender como configurar o parser e fazer a coisa toda funcionar bem. 

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 o modelo treinado pode ser baixado aqui

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

18 comentários:

  1. alo, muito legal esse post. me ajudou a entender varias interligacoes que nao estavam claras. obrigada!

    ResponderExcluir
    Respostas
    1. Que bom, Valeria! Às vezes nem tenho noção de que tipos de postagem são mais úteis.

      Excluir
  2. Poderia disponibilizar o link com o modelo treinado por favor?

    ResponderExcluir
  3. Vocês sabem se existe algum artigo que trata das abordagens existentes de Dependency Parser para Português do Brasil? Estou estudando a respeito e não acho nada para nosso idioma. Parabéns pelo post ;)

    ResponderExcluir
    Respostas
    1. Oi Leandro! Não tenho acompanhado essa área muito de perto, mas não conheço nenhum trabalho recente específico para o português. A tendência que existe é aproveitar os mesmos algoritmos de parsing com dados de treinamento em diferentes línguas.

      Excluir
  4. Erick, Parabéns pelo artigo. Sobre o comando abaixo: "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" Onde posso encontrar os arquivos que ficam no diretório "pt-model" que você passa como parâmetro no comando acima. Gostaríamos de utilizar sua compilação. Desde já obrigado!

    ResponderExcluir
    Respostas
    1. Obrigado! No final do post tem o link para um arquivo contendo o modelo do tagger e do parser.

      Excluir
  5. Erick, parabéns pela iniciativa, eu sou mestrando em computação na Universidade estadual do ceará seu blog vai me ajudar muito a entender alguns conceitos.

    É possível utilizar este modelo treinado junto com o NLTK, em python?
    Se puder me ajudar (no mestrado, ajuda é sempre bem vinda :D), eu te explico meu projeto e a gente troca umas ideias. meu email é: maikonigor@gmail.com

    Obrigado!

    ResponderExcluir
    Respostas
    1. Oi Maikon! Legal ter sido útil. Dá uma olhada no novo post, ele explica como usar o parser pelo Python. Depois podemos conversar sim!

      Excluir
  6. Olá,tudo bem? Gostei muito do seu post. Fiquei com uma dúvida. Eu gostaria de classificar textos para recomendar para os usuários de um portal de acordo com o perfil do usuário. Gostaria de saber onde entra o seu modelo treinado em português. Eu usaria ele antes de usar um classificador, por exemplo, naive bayes? Obrigado. Everton

    ResponderExcluir
    Respostas
    1. Olá! A ideia do parser é enriquecer a representação de um texto com as estruturas sintáticas. No caso de classificação de texto por assunto/perfil, normalmente não é tão interessante, pois só as palavras já são suficientes para uma boa performance.

      Excluir
  7. Boa tarde Erick.

    Parabéns pelo blog, é de grande ajuda para os iniciantes, meu muito obrigado.

    Você falou que seu amigo modificou o UD, saberia dizer como ficaria a referência do novo dataset?

    Abraços

    ResponderExcluir
    Respostas
    1. Olá, que bom que está ajudando! Referência como de publicação? Se for isso, não tem nenhuma.

      De qualquer forma, o UD já tem versões novas desse corpus e também de outro em português (baseado no corpus Bosque), mas da última vez que olhei ainda tinha aguns problemas.

      Excluir

    2. Acho que é um bom trabalho e pode usar em alguma publicação, ou algum curso também, o mercado de aprendizado anda em alta.

      Uma curiosidade, é possível utilizar o parser/tagger para fazer implicações A -> b o famoso de para?

      Exemplo usando o NER eu identifico as entidades:


      Boa noite. Quero uma passagem do Rio de Janeiro para Natal, com saída nesse domingo, às 23:00.

      Boa noite. Quero uma do para , com saída nesse .

      O NER em sí identifica os dados, como faz a inferência do Local ser origem e destino? Para isso usamos o tagger e o parser?

      Abraços

      Excluir
    3. As entidades ficariam assim:

      Boa noite. Quero uma {Produto= passagem} do {Local=Rio de Janeiro}
      para {Local=Natal}, com saída nesse {Data=domingo, às 23:00}.

      O NER em sí identifica os dados, como faz a inferência do Local ser origem e destino? Para isso usamos o tagger e o parser?

      Abraços

      Excluir