Isso é bem inconveniente se você já tem ou pretende desenvolver um sistema em Python e só queria incluir o parser. Uma possível solução é fazer chamadas ao Java para rodar o parser para um determinado texto, o que até funciona. O problema é que cada chamada precisa carregar o parser em memória, o que leva um certo tempo. Esse tempo de leitura pode ser menos de um minuto dependendo do hardware, mas ao rodar com vários textos diferentes, torna-se um grande gargalo.
O ideal, naturalmente, é poder carregar o parser na memória uma única vez e aproveitar o modelo para anotar textos conforme forem enviados. E é aí que entra a ideia de um servidor CoreNLP (lembrando que CoreNLP é toda a suíte de ferramentas de PLN de Stanford), que recebe e responde requisições HTTP. Esse processo pode rodar localmente, no computador do desenvolvedor que está fazendo experimentos, ou em um servidor, o que é útil para aplicações web.
Então, o que precisamos para fazer isso funcionar? Basicamente, duas partes: o servidor e o cliente.
Configurando o servidor
Boa notícia: o servidor já existe como parte do próprio CoreNLP. Mal começamos e já está 50% pronto!
Se você for rodá-lo localmente no seu computador, é só entrar no diretório do CoreNLP e fazer uma chamada Java:
A chamada solicita um máximo de 4 giga de memória, inclui os arquivos do diretório local para o classpath do Java e executa a classe StanfordCoreNLPServer.
Por padrão, o processo vai escutar a porta 9000, e você pode selecionar uma outra acrescentando a opção -port e o número desejado. Se você estiver rodando em um servidor acessível pela internet, é possível que precise de alguma configuração para liberá-la. Isso já é outra história e não vou detalhar aqui.
Chamando o servidor
Agora, precisamos preparar a chamada ao nosso servidor. Vou apresentar primeiro um exemplo pronto e funcional, e depois explicar os passos:
import json import urllib import requests def call_corenlp(text, dep_model_path, pos_model_path, ip, port): """ Chama o parser do CoreNLP em um servidor. """ properties = {'tokenize.whitespace': 'true', 'annotators': 'tokenize,ssplit,pos,depparse', 'depparse.model': dep_model_path, 'pos.model': pos_model_path, 'outputFormat': 'conllu'} # converte o dicionário em uma string properties_val = json.dumps(properties) # codifica os parâmetros com urllib para usar parâmetros GET. O conteúdo # do POST é o texto params = {'properties': properties_val} encoded_params = urllib.urlencode(params) url = 'http://{ip}:{port}/?{params}'.format(ip=ip, port=port, params=encoded_params) headers = {'Content-Type': 'text/plain;charset=utf-8'} response = requests.post(url, text.encode('utf-8'), headers=headers) response.encoding = 'utf-8' # apenas para ter certeza... output = response.text.replace('\0', '') return output
Parâmetros do CoreNLP
Os parâmetros que usei devem servir para a maioria dos casos:
- tokenize.whitespace indica que os tokens estão separados por espaço em branco (ou seja, você precisa já ter feito essa separação antes). Se não for indicado nada, é usado o tokenizador do Penn Treebank, o padrão para inglês.
- annotators informa quais módulos do CoreNLP queremos rodar. Aqui, escolhi o tokenizador (necessário para todo o resto), separador de sentenças (ssplit), POS tagger e parser de dependências. Se você tiver outros módulos treinados e prontos, como extrator de informações ou identificador de entidades, eles seriam adicionados aqui.
- depparse.model é o caminho para onde está salvo o modelo treinado do parser de dependência. Esse caminho é relativo ao diretório onde o servidor está sendo executado. Pode parecer estranho o cliente ter que saber onde o servidor armazena seus modelos, mas vamos dar um desconto - trata-se de software acadêmico, afinal!
- pos.model Idem ao anterior, mas para o POS tagger.
- outputFormat é o formato de saída, e o conllu é bem comum em PLN. Outras opções são json, xml e text. O formato text é o mais fácil de se visualizar e entender, mas não é bem estruturado como os outros.
Preparação da Request
Após definir os parâmetros, precisamos preparar a request, isso é, a requisição que o cliente vai enviar para o servidor. O servidor CoreNLP espera receber uma requisição do tipo POST, cujo conteúdo é o texto para ser anotado, mas com uma URL parametrizada como num GET.
Se você não entende bem desses conceitos, não tem problema. Simplificando, é o seguinte: uma requisição POST é uma mensagem que o cliente envia para o servidor com algum dado acoplado. No nosso caso, esse dado é o texto para ser anotado. E para enviar uma mensagem ao servidor, precisamos saber onde encontrá-lo, o que é definido pela URL, e no nosso caso vai ser composta de ip, porta e alguns parâmetros. Por exemplo:
http://10.11.12.13:4567?param1=valor1¶m2=valor2
Esses parâmetros da URL são onde vamos incluir os parâmetros do CoreNLP.
O trecho de código até a geração da variável url converte o dicionário de parâmetros para um formato compatível com uma URL.
Recebendo a resposta
O que resta agora é enviar a requisição para o servidor e aguardar a resposta. A primeira chamada pode demorar alguns segundos, pois o CoreNLP só carrega os modelos em memória quando precisa usá-los. Após isso, as próximas chamadas serão respondidas em questão de poucos milissegundos.
No final do código, tem uma linha para remover o caracter \0. Eu a incluí por conta de um antigo bug no CoreNLP Server, que enviava a resposta com alguns caracteres desse no final. Não vi mais esse problema na última versão, mas também não faz mal deixar a linha ali. Esse problema me deu uma certa dor de cabeça quando aconteceu pela primeira vez!
Testando
Agora é só fazer uma chamada com o código.
text = u'Processamento de língua natural é uma área muito interessante!' output = call_corenlp(text, 'models/pt-br/dep-parser', 'models/pt-br/pos-tagger.dat', 'localhost', 9000) print(output)
E a saída:
1 Processamento _ _ NOUN _ 5 nsubj _ _ 2 de _ _ ADP _ 1 adpmod _ _ 3 língua _ _ NOUN _ 2 adpobj _ _ 4 natural _ _ ADJ _ 3 amod _ _ 5 é _ _ VERB _ 0 root _ _ 6 uma _ _ DET _ 7 det _ _ 7 área _ _ NOUN _ 5 attr _ _ 8 muito _ _ ADV _ 9 advmod _ _ 9 interessante! _ _ ADJ _ 7 amod _ _
Espero que seja útil para os leitores!
Olá, muito obrigado pela postagem.
ResponderExcluirPra quem usa Python 3, troca 'encoded_params = urllib.urlencode(params)' por 'encoded_params = urllib.parse.urlencode(params)'.
O ssplit não estava funcionando aqui, então coloquei 'tokenize.whitespace': 'true' para 'false' e funcionou normalmente.