本文将讨论如何将Yandex开发的Tomita-parser集成到系统中,如何将其转换为动态库,与Java成为朋友,使其成为多线程,并使用它解决用于房地产评估的文本分类问题。

问题陈述
在房地产评估中,分析销售公告非常重要。 从公告中,您可以获得有关物业的所有必要信息,包括有关公寓维修状态的信息。 通常,此信息包含在广告文字中。 这在评估中非常重要,因为好的维修可以使每平方米的价格增加几千美元。
因此,我们有一个广告文字需要根据公寓的维修状态分为以下类别之一(未完成,一般,平均,良好,优秀,独占)。 关于维修,广告可能只说一两个句子,几个单词或什么也没说,因此对文本进行完全分类没有任何意义。 由于文本的特殊性和与维修环境相关的词语集有限,唯一合理的解决方案是从文本中提取所有必要的信息并将其分类。

现在我们需要学习如何从文本中提取有关装饰状态的所有事实。 具体而言,与维修直接相关的内容以及可以间接谈论公寓状况的所有内容-吊顶的存在,内置电器,塑料窗户,按摩浴缸,使用昂贵的装饰材料等。
在这种情况下,我们只需要提取有关公寓本身维修的信息,因为入口,地下室和阁楼的状况使我们不感兴趣。 还必须考虑到以自然语言书写的文本具有其所有固有的错误,错别字,缩写和其他特征的事实-我个人发现“油毡”和“层压板”这三个单词的拼写以及“最终”一词的五个拼写; 有些人不理解为什么单词之间需要空格,而另一些人则没有听说过逗号。 因此,具有上下文无关语法的解析器成为最简单,最合理的解决方案。
因此,在做出决定时,又形成了第二个有趣的大任务-学习如何从公告中提取有关修复的所有充分和必要的信息,即提供文本的快速句法和形态分析,该分析可以在库模式下在负载下并行工作。
继续解决
在基于可以与俄语一起使用的无上下文语法从文本中提取事实的可用方法中,我们注意到了Tomita-parser和python中的Yagry库。 Yagry立即被拒绝,因为它完全是用python编写的,并且几乎没有进行优化。 Tomita最初看起来非常吸引人:她为开发人员提供了详细的文档,并提供了许多示例,C ++承诺可以接受的速度。 不难理解编写语法的规则,分类器的第一个版本及其使用已在第二天准备就绪。
从语法中提取与修复上下文有关的形容词和动词的规则示例:
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};
用于确保不检索有关公共场所状态的信息的规则:
Repair -> StopWords Verb* Prep* Adj* RepairW; Repair -> Adj+ RepairW Prep* StopWords;
默认情况下,规则的权重为1,为规则分配较小的权重,我们设置执行顺序。
尴尬的是,只有控制台应用程序和大量的C ++代码被上传到了公众面前。 但是毫无疑问的优势是易于使用和快速的实验结果。 因此,决定考虑将其引入到更接近实现本身的系统中的可能的困难。
几乎可以立即获得关于维修的几乎所有必要信息的高质量提取。 “几乎”是因为最初在某些条件和语法下都没有提取某些单词。 但是,很难立即评估此问题的规模,它会在多大程度上影响整个分类问题的解决方案质量。
首先确保Tomita为我们提供了必要的功能,然后我们意识到,将其用作控制台应用程序不是一种选择:首先,控制台应用程序由于不稳定的原因而变得不稳定且不时崩溃,其次,它不会提供每天需要解析几百万个广告。 这样,就明确了要用什么来制作图书馆。
我们如何使Tomitha成为多线程库并与Java成为朋友
我们的系统是用Java编写的,而tomita-parser是用C ++编写的。 我们需要能够从Java调用解析广告文字。
可以有条件地将Tomita解析器的Java绑定开发分为两个部分-实现使用Tomita作为共享库的可能性,以及实际上用jvm编写集成层。 主要困难涉及第一部分。 Tomita最初是为在单独的流程中执行而设计的。 因此,在应用程序过程中使用解析器的主要障碍是两个因素。
- 数据交换是通过各种IO进行的。 需要实现通过内存与解析器交换数据的功能。 而且,有必要以最小化影响解析器本身代码的方式来执行此操作。 Tomita的体系结构提出了一种从内存中读取输入文档的方法,作为CDocStreamBase和CDocListRetrieverBase接口的实现。 输出更加困难-我不得不接触xml生成器的代码。
- 由“一个解析器-一个进程”的原理引起的第二个因素是全局状态,从解析器的不同实例进行了修改。 如果查看src / util / generic / singleton.h文件,则可以看到使用共享状态的机制。 不难想象,当在相同的地址空间中使用两个解析器实例时,就会出现竞争条件。 为了不重写整个解析器,决定修改此类,将全局状态替换为相对于线程(thread_local)的本地状态。 因此,在JTextMiner包装器中调用任何解析器之前,我们将这些thread_local变量设置为当前解析器实例,之后解析器代码将使用当前解析器实例的地址。
消除了这两个因素之后,该解析器可在任何环境中用作共享库。 编写jni-binders和Java包装器不再困难。
使用前必须配置Tomita解析器。 配置参数与调用控制台实用程序时使用的配置参数相似。 解析本身包括调用parse()方法,该方法接收用于解析的文档,并以字符串的形式返回xml,并包含解析器的结果。
Tomita的多线程版本-TomitaPooledParser用于解析以相同方式配置的TomitaParser对象池。 为了进行解析,使用了第一个免费解析器。 由于创建的解析器数量等于池中的线程数量,因此始终至少有一个解析器可用于该任务。 parse方法异步地在第一个免费解析器中解析提供的文档。
从Java调用Tomita库的示例:
tomitaPooledParser = new TomitaPooledParser(threadAmount, new File(configDirname), new String[]{tomitaConfigFilename}); Future<String> result = tomitaPooledParser.parse(documents); String response = result.get();
作为响应,带有解析结果的XML字符串。
我们遇到的问题以及我们如何解决它们
因此,该库已准备就绪,我们开始使用该服务以处理大量数据,并记住了不提取某些单词的问题,意识到这对于我们的任务非常关键。
在这些词中,有“预过滤”,“完成”,“产生”和其他删减的分词。 也就是说,广告中经常出现的字词,有时这是有关修复的唯一或非常重要的信息。 出现这种情况的原因-“预过滤”一词原来是一个形态未知的词,也就是说,富田根本无法确定语音的哪个部分,因此无法提取它。 对于删减的分词,我不得不写一个单独的规则,问题就解决了,但是花了一些时间才弄清楚这些是缩写的分词,因此需要提取一个特殊的规则。 对于长期遭受苦难的“最终”完成,我不得不为词形未知的单词写一条单独的规则。
为了解决使用语法的解析问题,我们向地名词典添加了一个形态未知的单词:
TAuxDicArticle "adjNonExtracted" { key = "" | "-" }
对于缩写分词,我们使用partcp的语法特征。
现在我们可以为这些情况编写规则:
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]>+;
我们发现的最后一个问题是对Tomita库进行多线程使用的服务会生成myStem进程,这些进程不会被破坏,并且在一段时间后会填满所有内存。 最简单的解决方案是限制Tomcat中最大和最小线程数。
关于分类的几句话
因此,现在我们从文本中提取了维修信息。 使用一种梯度提升算法对其进行分类并不难。 我们不会在这个问题上停留很长时间,对此已经有很多发言和撰写,而且我们在该领域还没有做任何根本性的新事情。 我将仅给出在测试中获得的分类的质量指标:
结论
目前,使用Tomita解析器以库模式实现的服务正在稳定运行,每天解析和分类数百万个广告。
聚苯乙烯
我们在该项目中编写的
所有Tomita代码都已上传到github。 我希望这对某人有用,并且此人将在一些更有用的事情上节省一些时间。