CODINGNEWS

Boletín oficial analizado con CoreNLP

Aug 10

En el post anterior analizamos los documentos extraidos del boletín oficial con una herramienta llamada Freeling. En este post vamos a repetir el mismo análisis pero usando CoreNLP.

CoreNLP es la suite de NLP desarrollada en Stanford. Por suerte para nosotros, cuenta con modelos en Español que si bien no son de lo más completos, nos van a permitir experimentar un poco.

Instalación

CoreNLP esta basado en Java (versión 8), es decir que vamos a necesitar tener Java instalado. Luego bajamos CoreNLP del sitio de descargas y bajamos los archivos de los modelos en Español de la misma página.

CoreNLP cuenta con una API RESTful, pero no es muy fácil de usar por lo que aquí vamos a usar un wrapper basado en NodeJS. Podemos encontrar el wrapper aquí. La API va a ser una simple app basada en ExpressJS. Los principales snippets (recortes) estan aquí:

//...
var NLPconfig = {
  nlpPath: nlpPath,
  version: '3.6.0',
  annotators: ['tokenize', 'lemma', 'ner', 'parse'],
  language: {
    jar: nlpPath + '/stanford-spanish-corenlp-2015-10-14-models.jar',
    properties: 'StanfordCoreNLP-spanish.properties',
  },
};
//..
var sendJSONresponse = function (res, status, content) {
  res.status(status);
  res.json(content);
};

var processAPI = function (req, res) {
  var txt = req.body.txt || '';
  console.log('Processing:', txt.slice(0, 10));
  app.coreNLP.process(txt, function (err, result) {
    sendJSONresponse(res, 200, result);
  });
};

router.post('/process', processAPI);
//...

Necesitamos la ubicación de CoreNLP y un archivo de configuración StanfordCoreNLP-spanish.properties cuyo contenido es:

annotators = tokenize, ssplit, pos, ner, regexner

tokenize.language = es

pos.model = edu/stanford/nlp/models/pos-tagger/spanish/spanish-distsim.tagger

ner.model = edu/stanford/nlp/models/ner/spanish.ancora.distsim.s512.crf.ser.gz
ner.applyNumericClassifiers = false
ner.useSUTime = false

parse.model = edu/stanford/nlp/models/lexparser/spanishPCFG.ser.gz

Nota: La información al respecto del docker container está en dockerhub.

Usar un único container puede ser muy limitante, por lo que decidimos lanzar 4 containers y un Nginx actuando como load balancer. El archivo de docker-compose usado es el siguiente:

version: '2'
services:
  app1:
    build: .
    image: malev/corenlp
    hostname: app1
    ports:
      - 3001:3001
    environment:
      PORT: 3001
    restart: always
# ... (Repite 3 veces)
loadbalancer:
    image: nginx
    ports:
        - 8080:80
    volumes:
        - ./corenlp.conf:/etc/nginx/conf.d/default.conf:ro

Analizando documentos

Desde el otro lado, vamos a usar un simple script python para enviar uno a uno los documentos del boletín oficial y vamos a almacenar los resultados en una base de datos Mongo.

import os
import sys
import json
import requests
from pymongo import MongoClient

client = MongoClient()
db = client.corenlp

filename = sys.argv[1]
corenlp_url = os.environ.get('CORENLP_URL', 'localhost:8080')

print("Working with {}".format(filename))


def get_tokens(data):
    tokens = []
    r = requests.post(
        "http://{}/api/process".format(corenlp_url),
        data={'txt': data['parsedText'].encode('utf-8')}
    )
    payload = json.loads(r.text)
    for sentence in payload['document']['sentences']['sentence']:
        for token in sentence['tokens']['token']:
            if type(token) == dict:
                token['idTramite'] = data['idTramite']
                token['sentenceId'] = sentence['$']['id']
                token['tokenId'] = token['$']['id']
                del token['$']
                tokens.append(token)
    return tokens


def already_in_db(id_tramite):
    return db.tokens.find_one({'idTramite': id_tramite})


def store_tokens(tokens):
    for token in tokens:
        db.tokens.insert_one(token)


with open(filename) as dfile:
    for element in dfile:
        try:
            data = json.loads(element)
            if not already_in_db(data['idTramite']):
                print("Working on {}".format(data['idTramite']))
                store_tokens(get_tokens(data))
        except Exception, e:
            print("Error in {}. {}".format(data['idTramite'], e.message))
            continue

print("Done!")

Para “acelerar los trámites”, podemos partir el archivo en partes y correr el script con cada una de ellas en un proceso nuevo:

split -n 20000 output.dat
python tokens.py output.0.dat
# En otra terminal:
python tokens.py output.1.dat

Y a esperar. La verdad que CoreNLP resultó ser muy lento. Al menos con la configuración que usamos aquí. Una vez que terminó de procesar todo podemos exportar los datos con:

mongoexport --db corenlp --collection tokens --out tokens.json

El problema de mongoexport es que va a incluir el _id que es específico de mongo e innecesario ara nosotros. Lo vamos a eliminar usando ramda-cli:

npm install -g ramda-cli
cat tokens.json | ramda-cli 'omit ["_id"]' -c > tokens_without_id.json

El código del proyecto se encuentra en Github y los resultados se pueden bajar de aquí . Seguiremos analizando el Boletín Oficial en futuros posts.

Fun Fact: El archivo exportado por mongodb pesa 7.1Gb. Luego de eliminar los ids de todos los documentos logramos reducir el tamaño del archivo a 5.1Gb. Victoria!

comments powered by Disqus