Cómo construir una aplicación de procesamiento de lenguaje natural

Publicado: 2022-03-11

El procesamiento del lenguaje natural, una tecnología que permite que las aplicaciones de software procesen el lenguaje humano, se ha vuelto bastante omnipresente en los últimos años.

La búsqueda de Google es cada vez más capaz de responder preguntas que suenan naturales, Siri de Apple es capaz de comprender una amplia variedad de preguntas, y cada vez más empresas utilizan (razonablemente) chats inteligentes y bots telefónicos para comunicarse con los clientes. Pero, ¿cómo funciona realmente este software aparentemente "inteligente"?

En este artículo, aprenderá sobre la tecnología que hace que estas aplicaciones funcionen y cómo desarrollar su propio software de procesamiento de lenguaje natural.

El artículo lo guiará a través del proceso de ejemplo de creación de un analizador de relevancia de noticias. Imagine que tiene una cartera de acciones y le gustaría que una aplicación rastreara automáticamente los sitios web de noticias populares e identificara los artículos que son relevantes para su cartera. Por ejemplo, si su cartera de acciones incluye empresas como Microsoft, BlackStone y Luxottica, querrá ver artículos que mencionen a estas tres empresas.

Primeros pasos con la biblioteca de PNL de Stanford

Las aplicaciones de procesamiento de lenguaje natural, como cualquier otra aplicación de aprendizaje automático, se basan en una serie de algoritmos relativamente pequeños, simples e intuitivos que funcionan en conjunto. A menudo tiene sentido utilizar una biblioteca externa donde todos estos algoritmos ya estén implementados e integrados.

Para nuestro ejemplo, utilizaremos la biblioteca Stanford NLP, una poderosa biblioteca de procesamiento de lenguaje natural basada en Java que viene con soporte para muchos idiomas.

Un algoritmo particular de esta biblioteca que nos interesa es el etiquetador de parte del discurso (POS). Se utiliza un etiquetador POS para asignar automáticamente partes del discurso a cada palabra en un texto. Este etiquetador POS clasifica las palabras en el texto según las características léxicas y las analiza en relación con otras palabras a su alrededor.

La mecánica exacta del algoritmo del etiquetador de POS está más allá del alcance de este artículo, pero puede obtener más información al respecto aquí.

Para comenzar, crearemos un nuevo proyecto Java (puede usar su IDE favorito) y agregaremos la biblioteca Stanford NLP a la lista de dependencias. Si está utilizando Maven, simplemente agréguelo a su 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>

Dado que la aplicación deberá extraer automáticamente el contenido de un artículo de una página web, también deberá especificar 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>

Con estas dependencias agregadas, está listo para seguir adelante.

Artículos de raspado y limpieza

La primera parte de nuestro analizador consistirá en recuperar artículos y extraer su contenido de las páginas web.

Al recuperar artículos de fuentes de noticias, las páginas suelen estar plagadas de información superflua (videos incrustados, enlaces salientes, videos, anuncios, etc.) que son irrelevantes para el artículo en sí. Aquí es donde entra en juego Boilerpipe.

Boilerpipe es un algoritmo extremadamente robusto y eficiente para eliminar el "desorden" que identifica el contenido principal de un artículo de noticias mediante el análisis de diferentes bloques de contenido utilizando funciones como la longitud de una oración promedio, los tipos de etiquetas utilizadas en los bloques de contenido y la densidad de los enlaces. El algoritmo de tubo de escape ha demostrado ser competitivo con otros algoritmos mucho más costosos desde el punto de vista computacional, como los basados ​​en la visión artificial. Puede obtener más información en el sitio de su proyecto.

La biblioteca Boilerpipe viene con soporte incorporado para raspar páginas web. Puede obtener el HTML de la web, extraer texto de HTML y limpiar el texto extraído. Puede definir una función, extractFromURL , que tomará una URL y usará Boilerpipe para devolver el texto más relevante como una cadena 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 diferentes extractores basados ​​en el algoritmo de caldera, con ArticleExtractor optimizado específicamente para artículos de noticias en formato HTML. ArticleExtractor se centra específicamente en las etiquetas HTML utilizadas en cada bloque de contenido y la densidad de enlaces salientes. Esto se adapta mejor a nuestra tarea que el DefaultExtractor más rápido pero más simple.

Las funciones integradas se encargan de todo por nosotros:

  • HTMLFetcher.fetch obtiene el documento HTML
  • getTextDocument extrae el documento de texto
  • CommonExtractors.ARTICLE_EXTRACTOR.getText extrae el texto relevante del artículo utilizando el algoritmo de caldera

Ahora puede probarlo con un artículo de ejemplo sobre las fusiones de los gigantes ópticos Essilor y Luxottica, que puede encontrar aquí. Puede enviar esta URL a la función y ver qué sale.

Agregue el siguiente código a su 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-ma-luxottica-group-idUSKBN14Z110"; String text = BoilerPipeExtractor.extractFromUrl(urlString); System.out.println(text); } }

Debería ver en su salida en el cuerpo principal del artículo, sin anuncios, etiquetas HTML y enlaces salientes. Aquí está el fragmento inicial de lo que obtuve cuando ejecuté esto:

 MILAN/PARIS Italy's Luxottica (LUX.MI) and France's Essilor (ESSI.PA) have agreed a 46 billion euro ($49 billion) merger to create a global eyewear powerhouse with annual revenue of more than 15 billion euros. The all-share deal is one of Europe's largest cross-border tie-ups and brings together Luxottica, the world's top spectacles maker with brands such as Ray-Ban and Oakley, with leading lens manufacturer Essilor. "Finally ... two products which are naturally complementary -- namely frames and lenses -- will be designed, manufactured and distributed under the same roof," Luxottica's 81-year-old founder Leonardo Del Vecchio said in a statement on Monday. Shares in Luxottica were up by 8.6 percent at 53.80 euros by 1405 GMT (9:05 am ET), with Essilor up 12.2 percent at 114.60 euros. The merger between the top players in the 95 billion eyewear market is aimed at helping the businesses to take full advantage of expected strong demand for prescription spectacles and sunglasses due to an aging global population and increasing awareness about eye care. Jefferies analysts estimate that the market is growing at between...

Y ese es de hecho el cuerpo del artículo principal del artículo. Es difícil imaginar que esto sea mucho más simple de implementar.

Etiquetado de partes del discurso

Ahora que ha extraído con éxito el cuerpo principal del artículo, puede trabajar para determinar si el artículo menciona empresas que son de interés para el usuario.

Es posible que sienta la tentación de simplemente realizar una búsqueda de cadenas o expresiones regulares, pero este enfoque tiene varias desventajas.

En primer lugar, una búsqueda de cadenas puede ser propensa a falsos positivos. Un artículo que menciona a Microsoft Excel puede etiquetarse como que menciona a Microsoft, por ejemplo.

En segundo lugar, dependiendo de la construcción de la expresión regular, una búsqueda de expresiones regulares puede generar falsos negativos. Por ejemplo, un artículo que contiene la frase "Las ganancias trimestrales de Luxottica superaron las expectativas" puede pasar desapercibido en una búsqueda de expresión regular que busca "Luxottica" rodeada de espacios en blanco.

Finalmente, si está interesado en una gran cantidad de empresas y está procesando una gran cantidad de artículos, buscar en todo el cuerpo del texto para cada empresa en la cartera del usuario puede resultar extremadamente lento y producir un rendimiento inaceptable.

La biblioteca CoreNLP de Stanford tiene muchas características poderosas y proporciona una forma de resolver estos tres problemas.

Para nuestro analizador, utilizaremos el etiquetador de partes del discurso (POS). En particular, podemos usar el etiquetador POS para encontrar todos los nombres propios en el artículo y compararlos con nuestra cartera 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 mencionados anteriormente, sino que también minimizamos drásticamente la cantidad de texto que necesitamos para comparar con nuestra cartera de acciones, ya que los nombres propios solo comprenden un pequeño subconjunto. del texto completo del artículo.

Al preprocesar nuestra cartera en una estructura de datos que tiene un bajo costo de consulta de membresía, podemos reducir drásticamente el tiempo necesario para analizar un artículo.

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

Aquí hay una implementación simple:

 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 de etiquetador, tagPos , toma una cadena como entrada y genera una cadena que contiene las palabras de la cadena original junto con la parte gramatical correspondiente. En su función principal, cree una instancia de PortfolioNewsAnalyzer y alimente la salida del raspador en la función de etiquetador y debería 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 am_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...

Procesamiento de la salida etiquetada en un conjunto

Hasta ahora, hemos creado funciones para descargar, limpiar y etiquetar un artículo de noticias. Pero aún falta determinar si el artículo menciona alguna de las empresas de interés para el usuario.

Para hacer esto, debemos recopilar todos los nombres propios y verificar si las acciones de nuestra cartera están incluidas en esos nombres propios.

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

Una vez que tengamos todos los nombres propios, querremos almacenarlos en una estructura de datos que esté mejor optimizada para nuestro propósito. Para nuestro ejemplo, usaremos un HashSet . A cambio de no permitir entradas duplicadas y no realizar un seguimiento del orden de las entradas, HashSet permite consultas de membresía muy rápidas. Dado que solo estamos interesados ​​​​en consultar la membresía, el HashSet es perfecto para nuestros propósitos.

A continuación se muestra la función que implementa la división y el almacenamiento de nombres propios. Coloque esta función en su 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 empresa consta de varias palabras (p. ej., Carl Zeiss en el ejemplo de Luxottica), esta implementación no podrá detectarlo. En el ejemplo de Carl Zeiss, "Carl" y "Zeiss" se insertarán en el conjunto por separado y, por lo tanto, nunca contendrán la cadena única "Carl Zeiss".

Para resolver este problema, podemos juntar todos los nombres propios consecutivos y unirlos con espacios. Aquí está 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 devolver un conjunto con los nombres propios individuales y los nombres propios consecutivos (es decir, unidos por espacios). Si imprime propNounSet , debería 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, ...]

Comparación de la cartera con el conjunto de nombres propios

¡Casi terminamos!

En las secciones anteriores, construimos un raspador que puede descargar y extraer el cuerpo de un artículo, un etiquetador que puede analizar el cuerpo del artículo e identificar nombres propios y un procesador que toma la salida etiquetada y recopila los nombres propios en un HashSet . Ahora todo lo que queda por hacer es tomar el HashSet y compararlo con la lista de empresas que nos interesan.

La implementación es muy simple. 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); }

Poniendolo todo junto

Ahora podemos ejecutar toda la aplicación: el raspado, la limpieza, el etiquetado, la recopilación y la comparación. Esta es la función que se ejecuta en toda la aplicación. Agregue esta función a su 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 que usa el mismo artículo que el anterior y Luxottica como la compañía de cartera:

 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-ma-luxottica-group-idUSKBN14Z110"); if (mentioned) { System.out.println("Article mentions portfolio companies"); } else { System.out.println("Article does not mention portfolio companies"); } }

Ejecute esto, y la aplicación debería imprimir "El artículo menciona compañías de cartera".

Cambie la empresa de cartera de Luxottica a una empresa que no se mencione en el artículo (como "Microsoft"), y la aplicación debería imprimir "El artículo no menciona empresas de cartera".

Crear una aplicación de PNL no tiene por qué ser difícil

En este artículo, repasamos el proceso de creación de una aplicación que descarga un artículo de una URL, lo limpia con Boilerpipe, lo procesa con Stanford NLP y verifica si el artículo contiene referencias específicas de interés (en nuestro caso, empresas de nuestro portafolio). Como se demostró, aprovechar esta variedad de tecnologías convierte lo que de otro modo sería una tarea abrumadora en una tarea relativamente sencilla.

Espero que este artículo le haya presentado conceptos y técnicas útiles en el procesamiento del lenguaje natural y que lo haya inspirado a escribir sus propias aplicaciones de lenguaje natural.

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