Cómo Construir Una Aplicación de Procesamiento de Lenguaje Natural

View all articles

El procesamiento de lenguaje natural—una tecnología que permite a aplicaciones de software procesar lenguaje humano—se ha hecho un tanto ubicuo en los últimos años.

La búsqueda de Google es capaz de contestar preguntas que suenan naturales. Siri de Apple es capaz de entender una gran variedad de preguntas, por esto muchas compañías están usando (razonable) chat inteligente y teléfonos bot para comunicarse con los clientes. Pero ¿cómo funciona realmente este software aparentemente “inteligente”?

En este artículo, aprenderás sobre la tecnología que hace mover a estas aplicaciones, al igual que aprenderás cómo desarrollar tú mismo un software de procesamiento de lenguaje natural.

El artículo te guiará en un ejemplo de un proceso de construcción de un analizador digno de las noticias. Imagínate que tienes un portafolio de valores y te gustaría tener una aplicación que automáticamente pase por sitios web populares de noticia e identifique artículos que sean relevantes para tu portafolio. Por ejemplo, si tu portafolio de valores incluye compañías como Microsoft, BlackStone y Luxottica, deberías estar al tanto de artículos que mencionen estas tres compañías.

Comenzar con la Biblioteca NLP de Stanford

Las aplicaciones de procesamiento de lenguaje natural, como cualquier otra aplicación de aprendizaje de máquina, están construidas sobre una base de algoritmos relativamente pequeños, simples e intuitivos que trabajan en conjunto. A menudo, tiene más sentido usar una biblioteca externa donde todos estos algoritmos ya son implementados e integrados.

Para nuestro ejemplo, vamos a usar la biblioteca NLP de Stanford, una poderosa biblioteca de procesamiento de lenguaje natural basada en Java, ésta viene con apoyo para muchos lenguajes.

Un algoritmo en particular que nos interesa de esta biblioteca es el del etiquetado gramatical. El etiquetado gramatical se usa para asignar automáticamente partes del discurso a cada palabra en un pedazo de texto. Este etiquetado gramatical clasifica palabras en el texto basado en características de léxico, mientras las analiza en relación a otras palabras a su alrededor.

La mecánica exacta del algoritmo etiquetado gramatical va más allá del alcance de este artículo, pero puedes aprender sobre esto aquí.

Para comenzar, vamos a crear un nuevo proyecto Java (puedes usar tu Entorno de Desarrollo Interactivo IDE) y agregar la biblioteca NLP de Stanford a la lista de dependencias. Si estás usando Maven, simplemente agrégalo a tu archivo pom.xml :

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

Ya que la aplicación va a necesitar extraer automáticamente el contenido de un artículo de la página web, deberás especificar también las siguientes dos dependencias:

<dependency>
<groupId>de.l3s.boilerpipe</groupId>
<artifactId>boilerpipe</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
<version>1.9.22</version>
</dependency>

Al agregar estas dependencias, estás listo para avanzar:

Scraping y Limpieza de Artículos

La primera parte de nuestro analizador se tratará de tomar artículos y extraer su contenido de las páginas web.

Cuando tomamos artículos de fuentes de noticia, las páginas usualmente se llenan de información no pertinente (videos incrustados, enlaces salientes, videos, publicidad, etc.) que es irrelevante para el artículo como tal. Aquí es donde Boilerpipe se hace notar.

Boilerpipe es un algoritmo extremadamente importante y eficaz para remover el “desorden” que identifica el contenido principal de un artículo al analizar diferentes bloques de contenido, usando características como el largo de una oración promedio, tipos de etiqueta usados en bloques de contenido y densidad de los enlaces. El algoritmo de boilerpipe ha probado que puede competir con otros algoritmos mucho más costosos computacionalmente, tales como aquellos basados en visión de máquina. Puedes aprender más en el sitio web de su proyecto.

La biblioteca de boilerpipe viene con un apoyo instalado para el scraping de páginas web. Puede traer el HTML de la web, extraer texto de HTML y limpiar el texto extraído. Puedes definir una función, extractFromURL, eso tomará una URL y usará Boilerpipe para regresar el texto más relevante como una cadena de caracteres usando ArticleExtractor para esta tarea:

import java.net.URL;
 
import de.l3s.boilerpipe.document.TextDocument;
import de.l3s.boilerpipe.extractors.CommonExtractors;
import de.l3s.boilerpipe.sax.BoilerpipeSAXInput;
import de.l3s.boilerpipe.sax.HTMLDocument;
import de.l3s.boilerpipe.sax.HTMLFetcher;
 
public class BoilerPipeExtractor {
  public static String extractFromUrl(String userUrl)
    throws java.io.IOException,
                 org.xml.sax.SAXException,
                 de.l3s.boilerpipe.BoilerpipeProcessingException  {
      final HTMLDocument htmlDoc = HTMLFetcher.fetch(new URL(userUrl));
      final TextDocument doc = new BoilerpipeSAXInput(htmlDoc.toInputSource()).getTextDocument();
      return CommonExtractors.ARTICLE_EXTRACTOR.getText(doc);
  }
}

La biblioteca boilerpipe proporciona extractores diferentes basados en el algoritmo boilerpipe, con ArticleExtractor siendo optimizado específicamente para artículos de noticias formateados en HTML. ArticleExtractor se enfoca en etiquetas HTML utilizadas en cada bloque de contenido y la densidad de enlaces salientes. Esto es más apropiado para nuestra tarea que el más rápido pero más sencillo DefaultExtractor.

Las funciones agregadas se encargan de todo por nosotros:

  • HTMLFetcher.fetch toma el documento HTML
  • getTextDocument extrae el documento de texto
  • CommonExtractors.ARTICLE_EXTRACTOR.getText extrae el texto relevante del artículo usando el algoritmo boilerpipe

Ahora lo puedes intentar con un artículo de ejemplo en relación con las fusiones de los gigantes ópticos Essilor y Luxottica, el cual puedes encontrar aquí. Puedes agregar esta URL a la función y ver el resultado.

Agrega el siguiente código a tu función principal:

public class App
{
  public static void main( String[] args )
     throws java.io.IOException,
                  org.xml.sax.SAXException,
                  de.l3s.boilerpipe.BoilerpipeProcessingException {
      String urlString = "http://www.reuters.com/article/us-essilor-m-a-luxottica-group-idUSKBN14Z110";
      String text = BoilerPipeExtractor.extractFromUrl(urlString);
      System.out.println(text);
  }
}

Deberías ver tu información de salida en el cuerpo principal del artículo, sin publicidad, etiquetas HTML y enlaces salientes. Aquí está una pequeña parte de lo que obtuve cuando lo puse en curso:

MILAN/PARIS Luxottica de Italia (LUX.MI) y Essilor de Francia (ESSI.PA) han llegado a un acuerdo de una fusión de 46 billones de euros (49 billones de dólares) para crear *global eyewear powerhouse* con ingresos anuales de más de 15 billones de euros.  
 
El acuerdo de todo tipo de acciones es uno de los mayores vínculos transfronterizos de Europa y une a Luxottica, el mayor creador de lentes del mundo con marcas como  Ray-Ban y Oakley, con el fabricante de lentes líder Essilor.

"Finalmente... dos productos que naturalmente se complementan – es decir monturas y lentes – serán diseñados, manufacturados y distribuidos bajo el mismo techo," Leonardo Del Vecchio, fundador de Luxottica de 81 años, dijo en un comunicado el lunes.

Las acciones en Luxottica subieron un 8.6 por ciento a 53.80 euros a las 1405 GMT (9:05 a.m. ET), con Essilor arriba un 12.2 por ciento a 114.60 euros.

La fusión entre estos jugadores tan importantes en el mercado de lentes de 95 billones, se enfoca en ayudar a los negocios a aprovechar la demanda tan fuerte que se espera para lentes de prescripción y lentes de sol, debido a una población global que envejece y una toma de conciencia mayor con respecto al cuidado de los ojos.

Los analistas Jefferies estiman que el mercado está creciendo entre…

Y ese es el cuerpo del artículo principal. Es difícil imaginarse que esto sea más sencillo de implementar.

Etiquetar Partes de un Discurso

Ahora que ya has extraído exitosamente el cuerpo del artículo, puedes enfocarte en determinar si el artículo menciona compañías que sean de interés para el usuario.

Tal vez te sientas tentado a hacer simplemente una cadena de caracteres o una expresión regular de búsqueda, pero hay varias desventajas en este acercamiento.

Primero que todo, una búsqueda de cadena de caracteres puede estar expuesta a falsos positivos. Un artículo que menciona a Microsoft Excel puede estar etiquetado como si mencionara a Microsoft, por ejemplo.

Segundo, dependiendo de la construcción de la expresión regular, una búsqueda de expresión regular puede dar pie a falsos negativos. Por ejemplo, un artículo que contiene la frase “Las ganancias trimestrales de Luxottica excedieron las expectativas” se podría perder en una búsqueda de expresión regular que busca la palabra “Luxottica” rodeada de espacios blancos.

Finalmente, si estás interesado en una gran cantidad de compañías y estás procesando un gran número de artículos, buscar cada compañía en el portafolio del usuario dentro de todo el cuerpo principal del texto, puede dar como resultado un desempeño flojo y que al mismo tiempo consume mucho tiempo.

La Biblioteca CoreNLP de Stanford tiene muchas características fuertes y proporciona una manera de resolver estos tres problemas.

Para nuestro analizador, vamos a usar el etiquetado gramatical. En particular, lo podemos usar para encontrar todos los nombres propios en el artículo y compararlos en nuestro portafolio de acciones interesantes.

Al incorporar la tecnología NLP, no solo mejoramos la precisión de nuestro etiquetador y minimizamos los falsos positivos y negativos ya mencionados, también minimizamos drásticamente la cantidad de texto que necesitamos para comparar con nuestro portafolio de acciones, ya que los nombres correctos solo comprometen un pequeño subconjunto del texto completo del artículo.

Al pre-procesar nuestro artículo en una estructura de data que tiene bajo costo de consulta de membresía, podemos reducir drásticamente el tiempo necesario para analizar un artículo.

CoreNLP de Stanford facilita un etiquetador muy conveniente llamado MaxentTagger que puede proporcionar etiquetado gramatical en unas pocas líneas de código.

Aquí está una simple implementación:

public class PortfolioNewsAnalyzer {
   private HashSet<String> portfolio;
   private static final String modelPath = "edu\\stanford\\nlp\\models\\pos-tagger\\english-left3words\\english-left3words-distsim.tagger";
   private MaxentTagger tagger;
 
   public PortfolioNewsAnalyzer() {
       tagger = new MaxentTagger(modelPath);
   }
   public String tagPos(String input) {
       return tagger.tagString(input);
   }
 

La función del etiquetador, tagPos, toma una cadena de caracteres como entrada y emite una cadena de caracteres que contiene las palabras en la cadena original junto con la parte del discurso correspondiente. En tu función principal, ejemplifica un PortfolioNewsAnalyzer y provee la salida del scraper a la función del etiquetador y deberías poder ver algo como esto:

MILAN/PARIS_NN Italy_NNP 's_POS Luxottica_NNP -LRB-_-LRB- LUX.MI_NNP -RRB-_-RRB- and_CC France_NNP 's_POS Essilor_NNP -LRB-_-LRB- ESSI.PA_NNP -RRB-_-RRB- have_VBP agreed_VBN a_DT 46_CD billion_CD euro_NN -LRB-_-LRB- $_$ 49_CD billion_CD -RRB-_-RRB- merger_NN to_TO create_VB a_DT global_JJ eyewear_NN powerhouse_NN with_IN annual_JJ revenue_NN of_IN more_JJR than_IN 15_CD billion_CD euros_NNS ._. The_DT all-share_JJ deal_NN is_VBZ one_CD of_IN Europe_NNP 's_POS largest_JJS cross-border_JJ tie-ups_NNS and_CC brings_VBZ together_RB Luxottica_NNP ,_, the_DT world_NN 's_POS top_JJ spectacles_NNS maker_NN with_IN brands_NNS such_JJ as_IN Ray-Ban_NNP and_CC Oakley_NNP ,_, with_IN leading_VBG lens_NN manufacturer_NN Essilor_NNP ._. ``_`` Finally_RB ..._: two_CD products_NNS which_WDT are_VBP naturally_RB complementary_JJ --_: namely_RB frames_NNS and_CC lenses_NNS --_: will_MD be_VB designed_VBN ,_, manufactured_VBN and_CC distributed_VBN under_IN the_DT same_JJ roof_NN ,_, ''_'' Luxottica_NNP 's_POS 81-year-old_JJ founder_NN Leonardo_NNP Del_NNP Vecchio_NNP said_VBD in_IN a_DT statement_NN on_IN Monday_NNP ._. Shares_NNS in_IN Luxottica_NNP were_VBD up_RB by_IN 8.6_CD percent_NN at_IN 53.80_CD euros_NNS by_IN 1405_CD GMT_NNP -LRB-_-LRB- 9:05_CD a.m._NN ET_NNP -RRB-_-RRB- ,_, with_IN Essilor_NNP up_IN 12.2_CD percent_NN at_IN 114.60_CD euros_NNS ._. The_DT merger_NN between_IN the_DT top_JJ players_NNS in_IN the_DT 95_CD billion_CD eyewear_NN market_NN is_VBZ aimed_VBN at_IN helping_VBG the_DT businesses_NNS to_TO take_VB full_JJ advantage_NN of_IN expected_VBN strong_JJ demand_NN for_IN prescription_NN spectacles_NNS and_CC sunglasses_NNS due_JJ to_TO an_DT aging_NN global_JJ population_NN and_CC increasing_VBG awareness_NN about_IN...

Procesar la Salida Etiquetada en un Set

Hasta ahora, hemos construido funciones para descargar, limpiar y etiquetar un artículo de noticias. Pero todavía debemos determinar si el artículo menciona alguna compañía de interés para el usuario.

Para hacer esto, necesitamos recopilar todos nombres propios y revisar si algunas acciones de nuestro portafolio están incluidas en esos nombres propios.

Para encontrar todos los nombres propios, debemos, primero, separar la cadena de salida etiquetada en identificadores (usando espacios como delimitadores), luego separa cada uno de los identificadores en el guión bajo (_) y revisa si la parte del discurso es un nombre propio.

Una vez que tenemos todos los nombres propios, deberíamos guardarlos en una estructura de data, la cual está mejor optimizada para nuestro propósito. Para nuestro ejemplo, vamos a usar un HashSet. Como intercambio por prohibir las entradas duplicadas y no seguir la pista del orden de las entradas, HashSet permite consultas de membresía muy rápidas. Ya que solo estamos interesados en preguntar por membresía, el HashSet es perfecto para nuestros propósitos.

Debajo está la función que implementa la separación y almacenamiento de los nombres propios. Ubica esta función en tu clase PortfolioNewsAnalyzer:

public static HashSet<String> extractProperNouns(String taggedOutput) {
  HashSet<String> propNounSet = new HashSet<String>();
  String[] split = taggedOutput.split(" ");
  for (String token: split ){
      String[] splitTokens = token.split("_");
      if(splitTokesn[1].equals("NNP")){
          propNounSet.add(splitTokens[0]);
      }
  }
  return propNounSet;
}

Sin embargo, hay un problema con esta implementación. Si el nombre de una compañía consiste de múltiples palabras, (ej., Carl Zeiss en el ejemplo de Luxottica) esta implementación no podrá captarla. En el ejemplo de Carl Zeiss, “Carl” y “Zeiss” serán insertados en el set por separado, por ende nunca contendrá la cadena única “Carl Zeiss.”

Para resolver este problema, podemos recolectar todos los nombres propios consecutivos y unirlos con espacios. Aquí se muestra la implementación actualizada que logra esto:

public static HashSet<String> extractProperNouns(String taggedOutput) {
  HashSet<String> propNounSet = new HashSet<String>();
  String[] split = taggedOutput.split(" ");
  List<String> propNounList = new ArrayList<String>();
  for (String token: split ){
      String[] splitTokens = token.split("_");
      if(splitTokens[1].equals("NNP")){
          propNounList.add(splitTokens[0]);
      } else {
          if (!propNounList.isEmpty()) {
              propNounSet.add(StringUtils.join(propNounList, " "));
              propNounList.clear();
          }
      }
  }
  if (!propNounList.isEmpty()) {
      propNounSet.add(StringUtils.join(propNounList, " "));
      propNounList.clear();
  }
  return propNounSet;
}

Ahora la función debería regresar un set con los nombres propios individuales y los nombres propios consecutivos (ej., unidos por espacios). Si imprimes el propNounSet, deberías ver algo como lo siguiente:

[... Monday, Gianluca Semeraro, David Goodman, Delfin, North America, Luxottica, Latin America, Rossi/File Photo, Rome, Safilo Group, SFLG.MI, Friday, Valentina Za, Del Vecchio, CEO Hubert Sagnieres, Oakley, Sagnieres, Jefferies, Ray Ban, ...]

Comparar el Portafolio con el Set de Nombres Propios

¡Ya casi terminamos!

En las secciones anteriores, construimos un scraper que puede descargar y extraer el cuerpo del artículo, un etiquetador que puede analizar el cuerpo del artículo e identificar nombres propios, al igual que un procesador que toma la salida etiquetada y recolecta los nombres propios en un HashSet. Ahora lo que resta por hacer es tomar el HashSet y compararlo con la lista de compañías que nos interesa.

La implementación es muy sencilla. Agrega el siguiente código en tu clase PortfolioNewsAnalyzer:

private HashSet<String> portfolio;
public PortfolioNewsAnalyzer() {
 portfolio = new HashSet<String>();
}
public void addPortfolioCompany(String company) {
  portfolio.add(company);
}
public boolean arePortfolioCompaniesMentioned(HashSet<String> articleProperNouns){
  return !Collections.disjoint(articleProperNouns, portfolio);
}

Uniendo Todo

Ahora podemos ejecutar toda la aplicación—el scraping, limpieza, etiquetado, la recolección y comparación. Aquí está la función que se ejecuta durante toda la aplicación. Agrega esta función a tu clase PortfolioNewsAnalyzer:

public boolean analyzeArticle(String urlString) throws
     IOException,
     SAXException,
     BoilerpipeProcessingException
{
  String articleText = extractFromUrl(urlString);
  String tagged = tagPos(articleText);
  HashSet<String> properNounsSet = extractProperNouns(tagged);
  return arePortfolioCompaniesMentioned(properNounsSet);
}

Finalmente, ¡podemos usar la aplicación!

Aquí hay un ejemplo usando el mismo artículo de arriba y Luxottica como la compañía portafolio:

public static void main( String[] args ) throws
     IOException,
     SAXException,
     BoilerpipeProcessingException
{
  PortfolioNewsAnalyzer analyzer = new PortfolioNewsAnalyzer();
  analyzer.addPortfolioCompany("Luxottica");
  boolean mentioned = analyzer.analyzeArticle("http://www.reuters.com/article/us-essilor-m-a-luxottica-group-idUSKBN14Z110");
  if (mentioned) {
      System.out.println("Article mentions portfolio companies");
  } else {
      System.out.println("Article does not mention portfolio companies");
  }
}

Ejecuta esto y la aplicación debería imprimir “El artículo menciona las compañías del portafolio.”

Cambia la compañía del portafolio de Luxottica a una compañía que no se mencione en el artículo (como “Microsoft”), y la aplicación debería imprimir “El artículo no menciona las compañías del portafolio.”

Construir Una Aplicación NLP No Tiene Que Ser Difícil

En este artículo, pasamos por el proceso de construir una aplicación que descarga un artículo desde una URL, lo limpia usando Boilerpipe, lo procesa usando NLP de Stanford y revisa si el artículo hace referencias específicas de interés (en nuestro caso, compañías en nuestro portafolio). Como ya fue demostrado, manejar esta matriz de tecnologías hace lo que de otra manera sería una tarea intimidante, una tarea que es relativamente directa.

Espero que este artículo te haya enseñado conceptos útiles y técnicas en el procesamiento de lenguaje natural y que te haya inspirado a escribir aplicaciones de lenguaje natural por ti mismo.

[Nota: Puedes encontrar una copia del código al que se hace referencia en este artículo aquí.

About the author

Shanglun Wang, United States
member since April 1, 2016
Sean is a passionate polyglot developer with extensive experience in full-stack web development, system administration, and data science. He is capable of working in both Linux and Windows environments and has developed everything from machinery interface to market intelligence software. Sean is also an excellent communicator and spends his spare time coaching speech and debate. [click to continue...]
Hiring? Meet the Top 10 Freelance Algorithm Developers for Hire in November 2017

Comments

Esteban Chacho
Al construir el MaxentTagger utilizar el modelo "edu\\stanford\\nlp\\models\\pos-tagger\\english-left3words\\english-left3words-distsim.tagger" Estuve leyendo en la especificacion de la clase ( https://nlp.stanford.edu/nlp/javadoc/javanlp/edu/stanford/nlp/tagger/maxent/MaxentTagger.html ) y dice que solo utilizar librerias en Inglés para propositos generales. No hay manera de especificar Español?
comments powered by Disqus
Subscribe
The #1 Blog for Engineers
Get the latest content first.
No spam. Just great engineering posts.
The #1 Blog for Engineers
Get the latest content first.
Thank you for subscribing!
Check your inbox to confirm subscription. You'll start receiving posts after you confirm.
Trending articles
Relevant Technologies
About the author
Shanglun Wang
Python Developer
Sean is a passionate polyglot developer with extensive experience in full-stack web development, system administration, and data science. He is capable of working in both Linux and Windows environments and has developed everything from machinery interface to market intelligence software. Sean is also an excellent communicator and spends his spare time coaching speech and debate.