Este artigo discutirá como integramos o Tomita-parser desenvolvido por Yandex em nosso sistema, o transformou em uma biblioteca dinâmica, fez amizade com Java, fez multithread e resolveu o problema de classificação de texto para avaliação de imóveis.

Declaração do problema
Na avaliação imobiliária, a análise dos anúncios de venda é de grande importância. A partir do anúncio, você pode obter todas as informações necessárias sobre a propriedade, incluindo informações sobre o estado de reparo no apartamento. Geralmente, essas informações estão contidas no texto do anúncio. É muito importante na avaliação, pois um bom reparo pode adicionar vários milhares ao preço por metro quadrado.
Portanto, temos um texto de anúncio que precisa ser classificado em uma das categorias de acordo com o estado de reparo no apartamento (inacabado, regular, médio, bom, excelente, exclusivo). Sobre reparos, um anúncio pode dizer uma ou duas frases, algumas palavras ou nada, por isso não faz sentido classificar o texto completamente. Devido à especificidade do texto e ao conjunto limitado de palavras relacionadas ao contexto de reparo, a única solução razoável era extrair todas as informações necessárias e classificá-las.

Agora precisamos aprender a extrair do texto todos os fatos sobre o estado da decoração. Especificamente, o que está diretamente relacionado à reparação, bem como tudo o que pode falar indiretamente sobre o estado do apartamento - a presença de tetos falsos, eletrodomésticos, janelas de plástico, jacuzzi, uso de materiais de acabamento caros, etc.
Nesse caso, precisamos extrair apenas informações sobre reparos no próprio apartamento, porque as condições das entradas, porões e sótãos não nos interessam. Também é necessário levar em consideração o fato de o texto ser escrito em linguagem natural, com todos os seus erros, erros de digitação, abreviações e outras características inerentes - encontrei pessoalmente três grafias das palavras “linóleo” e “laminado” e cinco grafias da palavra “final”; algumas pessoas não entendem por que são necessários espaços entre as palavras, enquanto outras não ouviram falar de vírgulas. Portanto, o analisador com gramáticas contextualmente livres se tornou a solução mais simples e mais razoável.
Assim, quando a decisão foi tomada, uma segunda tarefa grande e interessante foi formada - aprender a extrair do anúncio todas as informações suficientes e necessárias sobre o reparo, ou seja, fornecer uma rápida análise sintática e morfológica do texto, que pode funcionar em paralelo sob carga no modo de biblioteca.
Vá para a solução
Dos meios disponíveis para extrair fatos do texto com base em gramáticas sem contexto que podem funcionar com o idioma russo, nossa atenção foi atraída para o Tomita-parser e a biblioteca Yagry em python. Yagry foi imediatamente rejeitado, pois está escrito inteiramente em python e dificilmente está bem otimizado. E Tomita inicialmente parecia muito atraente: ela tinha documentação detalhada para o desenvolvedor e muitos exemplos, o C ++ prometeu uma velocidade aceitável. Não foi difícil entender as regras para escrever gramáticas, e a primeira versão do classificador com seu uso ficou pronta no dia seguinte.
Exemplos de regras de nossas gramáticas que extraem adjetivos e verbos relacionados ao contexto de reparo:
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};
Regras usadas para garantir que as informações sobre o status dos espaços públicos não sejam recuperadas:
Repair -> StopWords Verb* Prep* Adj* RepairW; Repair -> Adj+ RepairW Prep* StopWords;
Por padrão, o peso da regra é 1, atribuindo um peso menor à regra, definimos a ordem de sua execução.
Foi um pouco embaraçoso que apenas o aplicativo do console e uma tonelada de código C ++ tenham sido enviados ao público. Mas a vantagem indubitável era a facilidade de uso e resultados rápidos em experimentos. Portanto, decidiu-se pensar nas possíveis dificuldades de introduzi-lo em nosso sistema mais perto da implementação em si.
Quase imediatamente, foi possível obter extração de alta qualidade de quase todas as informações necessárias sobre o reparo. "Quase", porque inicialmente algumas palavras não foram extraídas sob nenhuma condição e gramática. No entanto, foi difícil avaliar imediatamente a escala desse problema, o quanto isso pode afetar a qualidade da solução do problema de classificação como um todo.
Tendo assegurado que, em uma primeira aproximação, o Tomita nos forneça a funcionalidade necessária, percebemos que não é uma opção usá-lo como um aplicativo de console: primeiro, o aplicativo do console acabou por ser instável e travou de tempos em tempos por motivos desconhecidos; em segundo lugar, não forneceria a carga de análise necessária de vários milhões de anúncios por dia. Assim, ficou definitivamente claro do que fazer uma biblioteca.
Como fizemos do Tomitha uma biblioteca multithread e fizemos amizade com Java
Nosso sistema é escrito em Java, tomita-parser em C ++. Precisávamos poder chamar a análise do texto do anúncio em Java.
O desenvolvimento de ligações java para o Tomita-parser pode ser dividido condicionalmente em dois componentes - a implementação da possibilidade de usar o Tomita como uma biblioteca compartilhada e, de fato, escrever uma camada de integração com o jvm. A principal dificuldade dizia respeito à primeira parte. A própria Tomita foi originalmente projetada para execução em um processo separado. Em seguida, os principais obstáculos ao uso do analisador no processo de aplicação foram dois fatores.
- A troca de dados foi realizada através de vários tipos de IO. Foi necessário implementar a capacidade de trocar dados com o analisador através da memória. Além disso, era necessário fazer isso de maneira a afetar minimamente o código do analisador. A arquitetura da Tomita sugeriu uma maneira de implementar a leitura de documentos de entrada da memória como uma implementação das interfaces CDocStreamBase e CDocListRetrieverBase. Foi mais difícil com a saída - tive que tocar no código do gerador xml.
- O segundo fator decorrente do princípio de "um analisador - um processo" é o estado global, modificado a partir de diferentes instâncias do analisador. Se você observar o arquivo src / util / generic / singleton.h , poderá ver o mecanismo para usar o estado compartilhado. É fácil imaginar que, ao usar duas instâncias do analisador no mesmo espaço de endereço, ocorrerá uma condição de corrida. Para não reescrever o analisador inteiro, foi decidido modificar essa classe, substituindo o estado global por um estado local relativo ao encadeamento (thread_local). Portanto, antes de qualquer chamada do analisador no wrapper JTextMiner, configuramos essas variáveis thread_local para a instância atual do analisador, após o qual o código do analisador trabalha com os endereços da instância atual do analisador.
Após eliminar esses dois fatores, o analisador ficou disponível para uso como uma biblioteca compartilhada de qualquer ambiente. Escrever jni-binders e um wrapper java não era mais difícil.
O analisador Tomita deve ser configurado antes do uso. Os parâmetros de configuração são semelhantes aos usados ao chamar o utilitário do console. A análise em si consiste em chamar o método parse (), que recebe documentos para análise e retorna xml como uma sequência com os resultados do analisador.
A versão multithread do Tomita - TomitaPooledParser usa para analisar um pool de objetos TomitaParser configurados da mesma maneira. Para análise, o primeiro analisador livre é usado. Como o número de analisadores criados é igual ao número de threads no pool, sempre haverá pelo menos um analisador disponível para a tarefa. O método de análise analisa de forma assíncrona os documentos fornecidos no primeiro analisador gratuito.
Um exemplo de chamada da biblioteca Tomita a partir de Java:
tomitaPooledParser = new TomitaPooledParser(threadAmount, new File(configDirname), new String[]{tomitaConfigFilename}); Future<String> result = tomitaPooledParser.parse(documents); String response = result.get();
Em resposta, uma sequência XML com o resultado da análise.
Problemas que encontramos e como resolvemos
Assim, a biblioteca está pronta, iniciamos o serviço com uma grande quantidade de dados e lembramos do problema de não extrair algumas palavras, percebendo que isso é muito crítico para a nossa tarefa.
Entre essas palavras, estavam “pré-filtrados”, assim como “concluídos”, “produzidos” e outros particípios resumidos. Ou seja, as palavras que são encontradas no anúncio com muita frequência e, às vezes, essas são as únicas ou muito importantes informações sobre o reparo. A razão para esse comportamento - a palavra “pré-filtrada” acabou sendo uma palavra com morfologia desconhecida, ou seja, Tomita simplesmente não pode determinar qual parte do discurso é e, portanto, não pode extraí-lo. E para particípios abreviados, tive que escrever uma regra separada, e o problema foi resolvido, mas levou algum tempo para descobrir que esses particípios são abreviados, para cuja extração é necessária uma regra especial. E para o final "sofredor", que sofria muito, tive que escrever uma regra separada, como para uma palavra com uma morfologia desconhecida.
Para resolver problemas de análise usando gramáticas, adicionamos uma palavra com morfologia desconhecida ao gazetteer:
TAuxDicArticle "adjNonExtracted" { key = "" | "-" }
Para particípios abreviados, usamos as características gramaticais do partcp, brev.
E agora podemos escrever as regras para estes 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]>+;
E o último dos problemas que descobrimos - um serviço com uso multithread da biblioteca Tomita produz processos myStem que não são destruídos e, após algum tempo, preenchem toda a memória. A solução mais fácil foi limitar o número máximo e mínimo de threads no Tomcat.
Algumas palavras sobre a classificação
Portanto, agora temos as informações de reparo extraídas do texto. Não foi difícil classificá-lo usando um dos algoritmos de aumento de gradiente. Não vamos parar por um longo tempo neste tópico, muito foi dito e escrito sobre isso, e não fizemos nada radicalmente novo nesta área. Darei apenas os indicadores de qualidade da classificação que obtivemos nos testes:
- Precisão = 95%
- Escore F1 = 93%
Conclusão
O serviço implementado usando o Tomita-parser no modo de biblioteca está atualmente trabalhando constantemente, analisando e classificando vários milhões de anúncios por dia.
PS
Todo o código Tomita que escrevemos como parte deste projeto é carregado no github. Espero que isso seja útil para alguém, e essa pessoa economize um pouco de tempo com algo ainda mais útil.