CoreNLP: Un tutorial
El procesamiento de lenguaje natural (NLP), uno de las áreas de Inteligencia Artificial (AI), tiene un gran crecimiento en en el desarrollo de aplicaciones: las vemos en Bots, en los asistentes personales (como Siri). Una de las aplicaciones muy utiles es el procesamiento de información, obtener "insights". Este post trata de explicar como usar NLP para extraer información de periodicos, imaginate desarrollar una aplicación que lea noticias de diarios como Washington Post o el New York Times y de ello poder sacar "insights". Sin embargo, en vez de ello la usaremos para analizar noticias de Perú, un país con una muy buena comida pero con muchos problemas politicos (sus ultimos 3 presidentes procesados judicialmente 😂). Imaginate analizar las redes de politica de tu país, pues bien, eso es lo que haremos con Perú!
Bueno no tanto, eso tomaría mucho tiempo. En este tutorial, solo analizaremos los textos de ciertas paginas web sobre política (El comercio), extraeremos las entidades (por ejemplo nombre de personas, lugares, organizaciones) y con ello mostraremos las relaciones entre ellas, formando una red! 😄. Tomaremos un enfoque totalmente práctico y usaremos muchas librerías en diferentes lenguajes de programación.
En sí el proceso que haremos es el siguiente:
- Obtener los textos a procesar
- Reconocer las entidades y relaciones en ella
- Mostrar un grafo (de relaciones)
Comenzemos por el paso 1:
1.- Obtener los textos a procesar
Primero lo primero, identificar de donde sacar las noticias, como dijimos la sacaremos de diarios de Perú, por ejemplo:
- Links de noticias que se muestran en: https://elcomercio.pe/politica
- Url 1
- Url 2
- ...
Bueno ahora ¿Como obtendremos el texto de esas noticias?
Para esto, el concepto de Web Scraping nos sera de ayuda, básicamente es hacer una petición Http a estos sitios web, obtener el html y mediante librerias de analisis de documentos HTML (por ejemplo, Beautiful Soup si usas Python), obtener el texto.
Felizmente existe una libreria en python que incluye todo esto, Newspaper3k: Article scraping & curation 👍
Lo único que debemos hacer es descargar la librería ejecutando en nuestra terminal pip3 install newspaper3
y crear un archivo (por ejemplo, news.py
) que contenga algo como:
from newspaper import Article
def extract_text(url):
article = Article(url)
article.download()
article.parse()
return article.text
urls = ['url 1','url2',...]
texts = list(map( extract_text, urls ))
Con lo siguiente descargariamos los textos de una lista de urls
(nuestras noticias), con tan solo muy pocas lineas de código!
Con ello, hemos realizado la primera tarea, vayamos al siguiente paso.
2.- Reconocer las entidades y relaciones en el texto
El texto de por si no es útil si es que no es interpretado, datos no es lo mismo que información. Es aquí donde usaremos NLP para interpretar el texto, y de ello obtener las entidades (como las personas o lugares que aparecen en las noticias) y también que relaciones a las entidades.
Pero la pregunta ahora es ¿Como obtendremos las entidades y relaciones en los textos? Aqui viene CoreNLP al rescate.
CoreNLP, es una librería desarrollada por la Universidad de Standford en Java, que integra muchas herramientas de Procesamiento de Lenguaje Natural, entre ellas, nos serán de utilidad: Named Entity Recognizer (NER), que se encarga de reconocer las entidades, y el Relation Extractor Annotator, encargado de reconocer las relaciones.
Una de las características muy potentes de CoreNLP es su capacidad ser usada como un servidor, es decir podemos hacerle peticiones HTTP. Por ejemplo solicitarle las entidades de un texto (GET entities?text='.......'), de esta forma podríamos integrarla con cualquier lenguaje de programación.
Nosotros usaremos CoreNLP de esa forma.
Así que primero levantemos CoreNLP, primero lo primero, dado que CoreNLP está desarrollado en Java, lo primero es descargarlo al menos en su versión 8, puedes verificarlo asi: java -version
Luego descargar en sí CoreNLP, esto requiere que descarguemos: corenlp .jar, y dado que analizaremos noticias en español, necesitamos descargar el .jar que contiene el modelo entrenado para el lenguaje español ; luego pondremos estos jar en una carpeta llamada corenlp (o el nombre que prefieras).
Luego de esto, nos ubicaremos en la carpeta corenlp, y ejecutaremos lo siguiente:
java -cp "\*" -Xmx4g edu.stanford.nlp.pipeline.StanfordCoreNLPServer \
-serverProperties StanfordCoreNLP-spanish.properties \
-timeout 30000 -quiet \
-preload "tokenize,ssplit,pos,lemma,ner,depparse,coref,kbp" \
-annotators "ner,kbp" \
-maxCharLength 1000000 -port 9000
Esto levanta el servicio de CoreNLP en el puerto 9000, con una serie de opciones, por ejemplo el flag -preload
, indica que debemos tokenizar ('tokenize' - Tokeniza el texto a procesar y devuelve la lista de 'tokens' en el Response), puedes ver más acerca de estas opciones [en su documentación.]
Una vez ejecutado este comando, podremos ver en localhost:9000, un contenido igual a este link.
Ahora podemos hacer peticiones web para obtener las entidades y relaciones, y dado que usamos python usaremos la librería pycorenlp para que CoreNLP lea los textos texts
que hemos descargado en el paso anterior:
# código del paso 1.
# ...
texts = list(map( extract_text, urls ))
from pycorenlp import StanfordCoreNLP
nlp = StanfordCoreNLP('http://localhost:9000')
def recognizer(text):
return nlp.annotate(text,
properties={
'annotators': 'tokenize,ssplit,pos,ner,depparse,openie,kbp',
'outputFormat': 'json',
'timeout': 90000,
})
response = list(map(recognizer, texts))
En la solicitud web (request) se le especifica una lista de annotators, que son tipos de analisis que se le aplicara al texto a procesar: tokenize,ssplit,pos,ner,depparse,openie,kbp.
La respuesta a la peticion web (response
) veremos la propiedad sentences, el cual contiene un arreglo de oraciones del texto a procesar, cada oracion contiene estas propiedades:
"tokens", "index", "basicDependencies", "enhancedDependencies", "enhancedPlusPlusDependencies","entitymentions","openie", "kbp".
Para este caso solo usaremos entitymentions (que nos da a conocer las entidades) y openie (que nos da a conocer las relaciones).
Por ejemplo, haciendole peticion a https://corenlp.run/, con el texto: Donald Trump is president USA
obtendremos:
{
"docDate": "2019-08-18T22:43:06",
"sentences": [
{
"index": 0,
"openie": [
{
"subject": "Donald Trump",
"subjectSpan": [0, 2],
"relation": "is",
"relationSpan": [2, 3],
"object": "president USA",
"objectSpan": [3, 5]
}
],
"entitymentions": [
{
"docTokenBegin": 0,
"docTokenEnd": 2,
"tokenBegin": 0,
"tokenEnd": 2,
"text": "Donald Trump",
"characterOffsetBegin": 0,
"characterOffsetEnd": 12,
"ner": "PERSON"
},
{
"docTokenBegin": 3,
"docTokenEnd": 4,
"tokenBegin": 3,
"tokenEnd": 4,
"text": "president",
"characterOffsetBegin": 16,
"characterOffsetEnd": 25,
"ner": "TITLE"
},
{
"docTokenBegin": 4,
"docTokenEnd": 5,
"tokenBegin": 4,
"tokenEnd": 5,
"text": "USA",
"characterOffsetBegin": 26,
"characterOffsetEnd": 29,
"ner": "COUNTRY"
}
]
}
]
}
El texto de ejemplo solo contiene una oracion, pero en casos reales, el texto contendra muchas oraciones, por lo que la propiedad sentences
sera un arreglo con muchos elementos.
Bueno ya obtuvimos las entidades y relaciones, ahora nos toca el ultimo paso, mostrar el grafo.
Mostrar el grafo
Mostramos un ejemplo del resultado de una oracion, sabemos que por texto CoreNLP retorna muchas oraciones, bueno el resultado final tomara este formato:
{ "data": {
"nodes": {
"0": [
{
"name": "PNP",
"type": "ORGANIZATION",
"color": "#527f1c"
},
{
"name": "Luis Muguruza Delgado",
"type": "PERSON",
"color": "#04dbf4
} ...
]
"1": [ ... ],
"2": [ ... ],
...
}
"edges": {
"0": [
{
"from": "efectivos",
"to": "armas",
"label": "empleando"
},..
]
"1": [ ...],
"2": [ ...],
}
}
Dado este formato se visualizara el grafo usando jsnetworkx.js
function draw(nodes, edges) {
var G = new jsnx.DiGraph();
nodes_graph = nodes.map((node) => [
node["name"],
{ color: node["color"], size: 30 },
]);
edges_graph = edges.map((node) => [node["from"], node["to"]]);
G.addNodesFrom(nodes_graph);
G.addEdgesFrom(edges_graph);
jsnx.draw(G, {
element: "#canvas",
withLabels: true,
nodeAttr: {
r: function (d) {
return d.data.size;
},
},
nodeStyle: {
fill: function (d) {
return d.data.color.replace("0", "5");
},
},
node_style: {
fill: "#999",
cursor: "pointer",
"stroke-opacity": "0",
stroke: "white",
},
labelStyle: { fill: "black", "font-weigth": "bold" },
stickyDrag: true,
});
}
Basandonos en la variable data mostrado anteriormente en formato json se mostrara el grafo:
index = 0; // el primer texto de data
draw(data["nodes"][index], data["edges"][index]);
Luego de todo este trabajo, obtendremos un resultado similar a la siguiente image.
Listo terminamos 😄 😄