Cómo distinguir una buena reparación de una mala, o cómo en SRG hicimos una biblioteca Java multiproceso del analizador Tomit

Este artículo discutirá cómo integramos el analizador Tomita desarrollado por Yandex en nuestro sistema, lo convertimos en una biblioteca dinámica, nos hicimos amigos de Java, lo hicimos multiproceso y resolvimos el problema de la clasificación de texto para la valoración de bienes raíces con él.



Declaración del problema.


En la evaluación de bienes raíces, el análisis de los anuncios de venta es de gran importancia. Desde el anuncio puede obtener toda la información necesaria sobre la propiedad, incluida la información sobre el estado de reparación en el apartamento. Por lo general, esta información está contenida en el texto del anuncio. Es muy importante en la evaluación, ya que una buena reparación puede agregar varios miles al precio por metro cuadrado.

Por lo tanto, tenemos un texto de anuncio que debe clasificarse en una de las categorías de acuerdo con el estado de reparación en el apartamento (sin terminar, justo, promedio, bueno, excelente, exclusivo). Sobre las reparaciones, un anuncio puede decir una o dos oraciones, unas pocas palabras o nada, por lo que no tiene sentido clasificar el texto por completo. Debido a la especificidad del texto y a un conjunto limitado de palabras relacionadas con el contexto de reparación, la única solución razonable era extraer toda la información necesaria del texto y clasificarlo.

imagen

Ahora necesitamos aprender a extraer del texto todos los hechos sobre el estado de la decoración. Específicamente, lo que se relaciona directamente con la reparación, así como todo lo que indirectamente puede hablar sobre la condición del apartamento: la presencia de techos suspendidos, electrodomésticos empotrados, ventanas de plástico, un jacuzzi, el uso de costosos materiales de acabado, etc.

En este caso, necesitamos extraer solo información sobre reparaciones en el departamento en sí, porque la condición de las entradas, sótanos y áticos no nos interesa. También es necesario tener en cuenta el hecho de que el texto está escrito en lenguaje natural con todos sus errores, errores tipográficos, abreviaturas y otras características inherentes. Personalmente encontré tres deletreos de las palabras "linóleo" y "laminado" y cinco deletreos de la palabra "final"; Algunas personas no entienden por qué se necesitan espacios entre las palabras, mientras que otras no han oído hablar de comas. Por lo tanto, el analizador con gramáticas contextualmente libres se convirtió en la solución más simple y razonable.

Por lo tanto, a medida que se tomó la decisión, se formó una segunda tarea grande e interesante: aprender a extraer toda la información suficiente y necesaria sobre la reparación del anuncio, es decir, proporcionar un rápido análisis sintáctico y morfológico del texto, que puede funcionar en paralelo bajo carga en modo biblioteca.

Pasar a la solución


De los medios disponibles para extraer hechos del texto basado en gramáticas libres de contexto que pueden funcionar con el idioma ruso, llamamos nuestra atención sobre Tomita-parser y la biblioteca Yagry en python. Yagry fue rechazado de inmediato, ya que está escrito completamente en Python y apenas está bien optimizado. Y Tomita inicialmente parecía muy atractiva: tenía documentación detallada para el desarrollador y muchos ejemplos, C ++ prometía una velocidad aceptable. No fue difícil entender las reglas para escribir gramáticas, y la primera versión del clasificador con su uso estaba lista al día siguiente.

Ejemplos de reglas de nuestras gramáticas que extraen adjetivos y verbos relacionados con el contexto de reparación:

RepairW -> "" | "" | ""; StopWords -> "" | "" | "" | ""; Repair -> RepairW<gnc-agr[1]> Adj<gnc-agr[1]>+ interp (Repair.AdjGroup {weight = 0.5}); Repair -> Verb<gnc-agr[1]> Adj<gnc-agr[1]>* interp (Repair.Verb) RepairW<gnc-agr[1]> {weight = 0.5}; 

Reglas utilizadas para garantizar que no se recupere información sobre el estado de los espacios públicos:

 Repair -> StopWords Verb* Prep* Adj* RepairW; Repair -> Adj+ RepairW Prep* StopWords; 

Por defecto, el peso de la regla es 1, asignando un peso menor a la regla, establecemos el orden de su ejecución.

Era un poco vergonzoso que solo la aplicación de consola y una tonelada de código C ++ se subieran al público. Pero la ventaja indudable fue la facilidad de uso y los rápidos resultados en los experimentos. Por lo tanto, se decidió pensar en las posibles dificultades de introducirlo en nuestro sistema más cerca de la implementación misma.

Casi de inmediato, fue posible lograr una extracción de alta calidad de casi toda la información necesaria sobre la reparación. "Casi", porque inicialmente algunas palabras no fueron extraídas bajo ninguna condición y gramáticas. Sin embargo, fue difícil evaluar de inmediato la escala de este problema, cuánto puede afectar la calidad de la solución al problema de clasificación en su conjunto.

Después de asegurarnos de que, en una primera aproximación, Tomita nos proporciona la funcionalidad necesaria, nos dimos cuenta de que no es una opción usarla como una aplicación de consola: en primer lugar, la aplicación de consola resultó inestable y se bloqueó de vez en cuando por razones desconocidas, y en segundo lugar, no proporcionaría la carga de análisis requerida de varios millones de anuncios por día. Por lo tanto, se hizo definitivamente claro de qué hacer una biblioteca.

Cómo hicimos de Tomitha una biblioteca multiproceso y nos hicimos amigos de Java


Nuestro sistema está escrito en Java, tomita-parser en C ++. Necesitábamos poder llamar al análisis de texto de anuncios desde Java.

El desarrollo de enlaces java para Tomita-parser se puede dividir condicionalmente en dos componentes: la implementación de la posibilidad de usar Tomita como una biblioteca compartida y, de hecho, escribir una capa de integración con jvm. La principal dificultad se refería a la primera parte. La propia Tomita fue diseñada originalmente para su ejecución en un proceso separado. Se dedujo que los principales obstáculos para utilizar el analizador en el proceso de solicitud fueron dos factores.

  1. El intercambio de datos se llevó a cabo a través de varios tipos de IO. Se requería implementar la capacidad de intercambiar datos con el analizador a través de la memoria. Además, era necesario hacer esto para minimizar el código del analizador. La arquitectura de Tomita sugirió una forma de implementar la lectura de documentos de entrada de la memoria como una implementación de las interfaces CDocStreamBase y CDocListRetrieverBase. Fue más difícil con la salida: tuve que tocar el código del generador xml.
  2. El segundo factor que surge del principio de "un analizador - un proceso" es el estado global, modificado a partir de diferentes instancias del analizador. Si observa el archivo src / util / generic / singleton.h , puede ver el mecanismo para usar el estado compartido. Es fácil imaginar que cuando se usan dos instancias de analizador en el mismo espacio de direcciones, se producirá una condición de carrera. Para no reescribir todo el analizador, se decidió modificar esta clase, reemplazando el estado global con un estado local relativo al hilo (thread_local). En consecuencia, antes de cualquier llamada al analizador en el contenedor JTextMiner, establecemos estas variables thread_local en la instancia del analizador actual, después de lo cual el código del analizador funciona con las direcciones de la instancia del analizador actual.

Después de eliminar estos dos factores, el analizador estaba disponible para su uso como biblioteca compartida desde cualquier entorno. Escribir jni-binders y un java wrapper ya no era difícil.

El analizador Tomita debe configurarse antes de su uso. Los parámetros de configuración son similares a los utilizados al invocar la utilidad de la consola. El análisis en sí consiste en llamar al método parse (), que recibe documentos para analizar y devuelve xml como una cadena con los resultados del analizador.

La versión multiproceso de Tomita: TomitaPooledParser utiliza para analizar un grupo de objetos TomitaParser que se configuran de la misma manera. Para el análisis, se utiliza el primer analizador gratuito. Dado que el número de analizadores creados es igual al número de subprocesos en el grupo, siempre habrá al menos un analizador disponible para la tarea. El método de análisis analiza asincrónicamente los documentos proporcionados en el primer analizador libre.

Un ejemplo de llamar a la biblioteca Tomita desde Java:

 /** * @param threadAmount number of threads in the pool * @param tomitaConfigFilename tomita config.proto * @param configDirname dir with configs: grammars, gazetteer, facttypes.proto */ tomitaPooledParser = new TomitaPooledParser(threadAmount, new File(configDirname), new String[]{tomitaConfigFilename}); Future<String> result = tomitaPooledParser.parse(documents); String response = result.get(); 

En respuesta, una cadena XML con el resultado del análisis.

Problemas que encontramos y cómo los resolvimos


Entonces, la biblioteca está lista, comenzamos el servicio con su uso en una gran cantidad de datos y recordamos el problema de no extraer algunas palabras, dándonos cuenta de que esto es muy crítico para nuestra tarea.

Entre estas palabras estaban "prefiltrado", así como "hecho", "producido" y otros participios resumidos. Es decir, las palabras que se encuentran en el anuncio con mucha frecuencia y, a veces, esta es la información única o muy importante sobre la reparación. La razón de este comportamiento: la palabra "prefiltrado" resultó ser una palabra con una morfología desconocida, es decir, Tomita simplemente no puede determinar qué parte del discurso es, y, en consecuencia, no puede extraerlo. Y para los participios abreviados, tuve que escribir una regla separada, y el problema se resolvió, pero me llevó un tiempo descubrir que estos son participios abreviados, para cuya extracción se necesita una regla especial. Y para el final "final" sufrido, tuve que escribir una regla separada en cuanto a una palabra con una morfología desconocida.

Para resolver problemas de análisis utilizando gramáticas, agregamos una palabra con morfología desconocida al diccionario geográfico:

 TAuxDicArticle "adjNonExtracted" { key = "" | "-" } 

Para participios abreviados usamos las características gramaticales de partcp, brev.

Y ahora podemos escribir las reglas para estos casos:

 Repair -> RepairW<gnc-agr[1]> Word<gram="partcp,brev",gnc-agr[1]> interp (Repair.AdjGroup) {weight = 0.5}; Repair -> Word<kwtype="adjNonExtracted",gnc-agr[1]> interp (Repair.AdjGroup) RepairW<gnc-agr[1]> Prep* Adj<gnc-agr[1]>+; 

Y el último de los problemas que descubrimos: un servicio con uso multiproceso de la biblioteca Tomita produce procesos myStem que no se destruyen y después de un tiempo llenan toda la memoria. La solución más fácil fue limitar el número máximo y mínimo de subprocesos en Tomcat.

Algunas palabras sobre la clasificación.


Entonces, ahora tenemos la información de reparación extraída del texto. No fue difícil clasificarlo usando uno de los algoritmos de aumento de gradiente. No nos detendremos aquí por mucho tiempo sobre este tema, se ha dicho y escrito mucho sobre esto, y no hemos hecho nada radicalmente nuevo en esta área. Solo daré los indicadores de calidad de la clasificación que obtuvimos en las pruebas:

  • Precisión = 95%
  • Puntuación F1 = 93%

Conclusión


El servicio implementado que usa Tomita-parser en modo biblioteca actualmente funciona de manera constante, analizando y clasificando varios millones de anuncios por día.

PS


Todo el código de Tomita que escribimos como parte de este proyecto se carga en el github. Espero que esto sea útil para alguien, y esta persona ahorrará un poco de tiempo en algo aún más útil.

Source: https://habr.com/ru/post/439614/


All Articles