¿Conoces la librería CoreNLP de Stanford de procesamiento de lenguaje natural?

17/07/2017

Desde hace unos años Stanford ha liderado el procesamiento del lenguaje natural. La BVMC se ha interesado desde siempre por esta librería y la ha puesto en práctica, por ejemplo, en el analizador sintáctico. Sin embargo, deseábamos adentrarnos un poco más en su funcionamiento y ha llegado el momento 🙂

La aplicación geosearch que algunos de vosotros conoceréis incluye enlaces a Geonames que se crearon de forma automática y la investigación fue presentada en el congreso DATeCH 2017. La desambiguación de lugares como Guadalajara en México y en España o Córdoba en Argentina y España es un claro ejemplo del procesamiento del lenguaje natural. En la investigación presentada en el congreso no se tuvo en cuenta la desambiguación y se solucionó de forma manual para no retrasarnos en la entrega del artículo. Sin embargo, ahora es el momento de detectar esas localizaciones y analizarlas más profundamente para comprobar si realmente podemos realizar una aplicación automática que funcione en un rango aceptable de acierto.

En primer lugar,  CoreNLP permite detectar localizaciones en la mención de la publicación original de nuestras obras, por ejemplo:

Madrid, por Iuan de la Cuesta, 1605

El apartado que nos interesa es el reconocedor de entidades de CoreNLP y aquí os dejo la documentación en inglés. Al empezar a revisar la documentación no encontré demasiada información al respecto y por eso hemos decidido crear este pequeño manual. En primer lugar, nos creamos un proyecto Java Maven en nuestro caso e incorporamos al fichero pom.xml las librerías correspondientes en su última versión, junto con los modelos en español:

<dependency>
<groupId>edu.stanford.nlp</groupId>
<artifactId>stanford-corenlp</artifactId>
<version>3.8.0</version>
</dependency>

<dependency>
<groupId>edu.stanford.nlp</groupId>
<artifactId>stanford-corenlp</artifactId>
<version>3.8.0</version>
<classifier>models-spanish</classifier>
</dependency>

CoreNLP nos permite configurar el idioma con el que vamos a trabajar y lo podemos configurar de la siguiente forma:

Properties props = new Properties();
props.put("annotators", "tokenize, ssplit, pos, lemma, ner");

props.setProperty("tokenize.language", "es");
props.setProperty("pos.model", "edu/stanford/nlp/models/pos-tagger/spanish/spanish-distsim.tagger");
props.setProperty("ner.model", "edu/stanford/nlp/models/ner/spanish.ancora.distsim.s512.crf.ser.gz");

StanfordCoreNLP pipeline = new StanfordCoreNLP(props);

Por defecto, CoreNLP utiliza el modelo de reconocimiento de entidades del corpus Ancora para el idioma español. Sin embargo, al hacer pruebas probablemente no obtendréis los resultados esperados porque el corpus no contempla todos los casos para vuestro contexto. Por ello, CoreNLP permite crear nuevos modelos de una forma flexible y sencilla para que puedan ser entrenados con contenido propio.

En primer lugar, debemos filtrar la información con la que queremos trabajar. En nuestro caso, utilizaremos la información en el campo lugar publicación original que podemos recoger desde nuestro punto de acceso SPARQL con el endpoint de geosearch:

SELECT DISTINCT ?p
WHERE { ?s &lt;http://data.cervantesvirtual.com/publicationStatement/#hasDescription&gt; ?p}

Obtendremos como resultado un fichero descriptions-ps-bvmc.txt con sentencias del tipo:

...
Palma de Mallorca, Hijas de Colomar, 1901
Palma de Mallorca, Hijas de Colomar, 1901
Palma de Mallorca, Hijas de Colomar, 1901
París, Librería Garnier Hermanos, 1883.
Leipzig, Romanische Studien, 1895.
Barcelona, Llibreria Millà, 1933.
Barcelona, Imprempta de Salvador Bonavía, 1908..
Barcelona, Biblioteca deLo Teatro Regional", 1898.
Barcelona, Llibrería D' Eudalt Puig, 1885.
Barcelona, Impremta de Salvador Bonavía, 1907.
México, Henrico Martínez, 1606
Barcelona, Impremta de Salvador Bonavía, 1911.
Barcelona, Antoni López ; Llibrería Espanyola, ca. 1864-1866.
Barcelona, Llibreria Espanyola de López, ca.1890.
Barcelona, Llibreria Espanyola de López, [ca. 1890].
Barcelona, Impremta de Salvador Bonavía, 1908.
...

CoreNLP además de facilitar el reconocimiento de entidades, también provee muchas más funcionalidades que nos pueden ayudar en nuestro trabajo como por ejemplo el tokenizador. Trabajar con las frases directamente es extremadamente complicado, y por ello la primera operación que debemos realizar es la tokenización y anotado de cada uno de los tokens. En primer lugar, descargamos la librería a nuestro ordenador. La descomprimimos y accedemos por consola a la ruta donde lo hemos descomprimido. A continuación, debemos tokenizar (convertir a tokens) cada una de las sentencias recogidas en el fichero descriptions-ps-bvmc.txt con el siguiente comando:

java -cp stanford-ner.jar edu.stanford.nlp.process.PTBTokenizer descriptions-ps-bvmc.txt > descriptions-ps-bvmc.tok

CoreNLP nos permite marcar cada uno de los tokens con unos valores predefinidos PERS (persona), LOC (Lugar), ORG (Organización) y O (Otro). A continuación marcamos cada uno los tokens con estos valores. Este proceso puede llevar mucho tiempo, pero para que CoreNLP funcione correctamente debemos proporcionarle una cantidad suficiente de tokens anotados. Os podéis ayudar de cualquier editor de texto y de las funciones buscar y reemplazar para trabajar más rápido. Los tokens y los valores predefinidos deben estar separados por un tabulador, en caso contrario CoreNLP no funcionará correctamente. El resultado lo almacenamos en un nuevo fichero con el nombre descriptions-ps-bvmc.tsv dentro de la carpeta de CoreNLP. Nos debe quedar algo parecido al siguiente ejemplo:

...
Impressa	O
en	O
Valencia	LOC
:	O
por	O
Gabriel	PERS
Ribas	PERS
...	O
,	O
1594	O
...

Una vez generado el fichero de tokens anotado, es necesario crear el modelo y para ello ejecutamos el siguiente comando:
java -cp stanford-ner.jar edu.stanford.nlp.ie.crf.CRFClassifier -prop locations-bvmc.prop

Donde locations-bvmc.prop es el fichero de configuración para crear el modelo:

#location of the training file
trainFile = descriptions-ps-bvmc.tsv 

#location where you would like to save (serialize to) your
#classifier; adding .gz at the end automatically gzips the file,
#making it faster and smaller
serializeTo = ner-model.ser.gz

#structure of your training file; this tells the classifier
#that the word is in column 0 and the correct answer is in
#column 1
map = word=0,answer=1

#these are the features we'd like to train with
#some are discussed below, the rest can be
#understood by looking at NERFeatureFactory
useClassFeature=true
useWord=true
useNGrams=true
#no ngrams will be included that do not contain either the
#beginning or end of the word
noMidNGrams=true
useDisjunctive=true
maxNGramLeng=6
usePrev=true
useNext=true
useSequences=true
usePrevSequences=true
maxLeft=1
#the next 4 deal with word shape features
useTypeSeqs=true
useTypeSeqs2=true
useTypeySequences=true
wordShape=chris2useLC

Tras un período de tiempo que dependerá del tamaño del fichero tsv y de las anotaciones realizadas, el modelo se habrá generado en la carpeta de CoreNLP con el nombre ner-model.ser.gz. Para poder trabajar con este nuevo modelo debemos copiarlo a nuestro proyecto a la carpeta src/main/resources:

model-proyecto-maven
model-proyecto-maven

Y a continuación configuramos el código Java para que utilice el nuevo modelo en lugar del inicial que nos provee CoreNLP:

props.setProperty("ner.model", "src/main/resources/ner-model.ser.gz");

Finalmente el código nos debe quedar de la siguiente forma:


public class NLPTest {

  @Test
  public void basic() {
    System.out.println("Starting Stanford NLP");

    Properties props = new Properties();
    props.put("annotators", "tokenize, ssplit, pos, lemma, ner");
 
    props.setProperty("tokenize.language", "es");
    props.setProperty("pos.model", "edu/stanford/nlp/models/pos-tagger/spanish/spanish-distsim.tagger");
    props.setProperty("ner.model", "src/main/resources/ner-model.ser.gz");
    props.setProperty("ner.applyNumericClassifiers", "false");
    props.setProperty("ner.useSUTime", "false");

    StanfordCoreNLP pipeline = new StanfordCoreNLP(props);

    String[] tests =
    {
      "Guadalajara : Imprenta Daniel Ramírez, [1900?]",
      "Guadalajara : Imp. de Ramírez, 1909",
      "Guadalajara",
      "[Guadalaxara? : s.n., 1669?]",
      "Guadalajara : Impr. Provincial, 1884",
      "Guadalajara, Tipografía de Luis Pérez Verdía, 1883",
      "Guadalajara : Imprenta y Encuadernacion Provincial, 1884",
      "Impresso en Guadalajara : por Pedro de Robles y Francisco de Cormellas, 1564",
      "Guadalajara [Jalisco] : en la Oficina de Don José Fruto Romero, Año de 1811",
      "Guadalajara, Imprenta de J. Cabrera, 1895",
      "Guadalajara : Imprenta del Gobierno, a cargo de J. Santos Orozco, 1848"
    };
 
    for (String s : tests) {

     Annotation document = new Annotation(s);
     pipeline.annotate(document);

     List<CoreMap> sentences = document.get(SentencesAnnotation.class);
 
     for(CoreMap sentence: sentences) {
         for (CoreLabel token: sentence.get(TokensAnnotation.class)) {
             String word = token.get(TextAnnotation.class);
             String pos = token.get(PartOfSpeechAnnotation.class);
             String ne = token.get(NamedEntityTagAnnotation.class);
 
             if(ne.equals("LOC"))
                 System.out.println("Lugar: " + word + " pos: " + pos + " ne:" + ne);
             }
         }
     }
   }
}

Y como resultado obtendremos por pantalla las siguiente líneas que corresponden a localizaciones, en concreto a diferentes ejemplos de Guadalajara:

Lugar: Guadalajara pos: np00000 ne:LOC
Lugar: Guadalajara pos: np00000 ne:LOC
Lugar: Guadalajara pos: np00000 ne:LOC
Lugar: Guadalaxara pos: np00000 ne:LOC
Lugar: Guadalajara pos: np00000 ne:LOC
Lugar: Guadalajara pos: np00000 ne:LOC
Lugar: Guadalajara pos: np00000 ne:LOC
Lugar: Guadalajara pos: np00000 ne:LOC
Lugar: Guadalajara pos: np00000 ne:LOC
Lugar: Jalisco pos: np00000 ne:LOC
Lugar: Guadalajara pos: np00000 ne:LOC
Lugar: Guadalajara pos: np00000 ne:LOC

¡Espero que os haya gustado y os animo a practicar con esta librería para vuestros futuros desarrollos!