如何构建自然语言处理应用程序
已发表: 2022-03-11自然语言处理——一种允许软件应用程序处理人类语言的技术——在过去几年中变得相当普遍。
谷歌搜索越来越有能力回答听起来自然的问题,苹果的 Siri 能够理解各种各样的问题,越来越多的公司正在(合理地)使用智能聊天和电话机器人与客户交流。 但是这个看似“智能”的软件究竟是如何工作的呢?
在本文中,您将了解使这些应用程序运转的技术,您将学习如何开发自己的自然语言处理软件。
本文将引导您完成构建新闻相关性分析器的示例过程。 假设您有一个股票投资组合,并且您希望应用程序自动爬取热门新闻网站并识别与您的投资组合相关的文章。 例如,如果您的股票投资组合包括 Microsoft、BlackStone 和 Luxottica 等公司,您可能希望看到提及这三家公司的文章。
斯坦福 NLP 库入门
与任何其他机器学习应用程序一样,自然语言处理应用程序是建立在许多相对较小、简单、直观的算法协同工作的基础上的。 使用所有这些算法都已经实现和集成的外部库通常是有意义的。
对于我们的示例,我们将使用斯坦福 NLP 库,这是一个强大的基于 Java 的自然语言处理库,支持多种语言。
我们感兴趣的这个库中的一个特定算法是词性 (POS) 标记器。 词性标注器用于自动为一段文本中的每个单词分配词性。 这个词性标注器根据词汇特征对文本中的单词进行分类,并分析它们与周围其他单词的关系。
POS 标记器算法的确切机制超出了本文的范围,但您可以在此处了解更多信息。
首先,我们将创建一个新的 Java 项目(您可以使用您最喜欢的 IDE)并将斯坦福 NLP 库添加到依赖项列表中。 如果您使用的是 Maven,只需将其添加到您的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>
由于应用程序需要自动从网页中提取文章的内容,因此您还需要指定以下两个依赖项:
<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>
添加这些依赖项后,您就可以继续前进了。
刮擦和清洁用品
我们分析器的第一部分将涉及检索文章并从网页中提取其内容。
从新闻来源检索文章时,页面通常充斥着与文章本身无关的无关信息(嵌入视频、出站链接、视频、广告等)。 这就是 Boilerpipe 发挥作用的地方。
Boilerpipe 是一种非常强大且高效的算法,用于消除“杂乱”,通过使用平均句子长度、内容块中使用的标签类型和链接密度等特征分析不同的内容块来识别新闻文章的主要内容。 锅炉管算法已被证明与其他计算成本更高的算法相比具有竞争力,例如基于机器视觉的算法。 您可以在其项目网站上了解更多信息。
Boilerpipe 库带有对抓取网页的内置支持。 它可以从 Web 中获取 HTML,从 HTML 中提取文本,并清理提取的文本。 您可以定义一个函数extractFromURL
,它将接受一个 URL 并使用 Boilerpipe 将最相关的文本作为字符串返回,使用ArticleExtractor
执行此任务:
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); } }
Boilerpipe 库提供了基于boilerpipe 算法的不同提取器,其中ArticleExtractor
专门针对HTML 格式的新闻文章进行了优化。 ArticleExtractor
专门关注每个内容块中使用的 HTML 标记和出站链接密度。 这比更快但更简单的DefaultExtractor
更适合我们的任务。
内置函数为我们处理一切:
-
HTMLFetcher.fetch
获取 HTML 文档 getTextDocument
提取文本文档CommonExtractors.ARTICLE_EXTRACTOR.getText
使用boilerpipe算法从文章中提取相关文本
现在,您可以通过有关光学巨头依视路和 Luxottica 合并的示例文章进行尝试,您可以在此处找到该文章。 您可以将此 URL 提供给函数并查看结果。
将以下代码添加到您的 main 函数中:
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); } }
您应该在文章主体的输出中看到,没有广告、HTML 标记和出站链接。 这是我运行它时得到的开始片段:
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...
这确实是文章的主体。 很难想象这实现起来要简单得多。
标记词性
现在您已成功提取文章主体,您可以确定文章是否提及用户感兴趣的公司。
您可能很想简单地进行字符串或正则表达式搜索,但这种方法有几个缺点。
首先,字符串搜索可能容易出现误报。 例如,提及 Microsoft Excel 的文章可能会被标记为提及 Microsoft。
其次,根据正则表达式的构造,正则表达式搜索可能会导致漏报。 例如,包含短语“Luxottica 的季度收益超出预期”的文章可能会被用于搜索“Luxottica”并被空格包围的正则表达式搜索所遗漏。
最后,如果您对大量公司感兴趣并且正在处理大量文章,那么在整个正文中搜索用户投资组合中的每家公司可能会非常耗时,并且会产生不可接受的性能。
斯坦福的 CoreNLP 库具有许多强大的功能,并提供了解决所有这三个问题的方法。
对于我们的分析器,我们将使用词性 (POS) 标注器。 特别是,我们可以使用词性标注器来查找文章中的所有专有名词,并将它们与我们感兴趣的股票组合进行比较。
通过整合 NLP 技术,我们不仅提高了标注器的准确性并最大限度地减少了上述误报和误报,而且我们还显着减少了需要与我们的股票组合进行比较的文本数量,因为专有名词仅包含一小部分文章的全文。
通过将我们的投资组合预处理成具有低成员查询成本的数据结构,我们可以显着减少分析文章所需的时间。
斯坦福 CoreNLP 提供了一个非常方便的标注器,称为 MaxentTagger,只需几行代码即可提供 POS 标注。
这是一个简单的实现:
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); }
标注器函数tagPos
将字符串作为输入并输出一个字符串,该字符串包含原始字符串中的单词以及相应的词性。 在你的 main 函数中,实例化一个PortfolioNewsAnalyzer
并将刮板的输出输入到 tagger 函数中,你应该会看到如下内容:

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...
将标记的输出处理成一个集合
到目前为止,我们已经构建了下载、清理和标记新闻文章的功能。 但是我们仍然需要确定文章是否提到了用户感兴趣的任何公司。
为此,我们需要收集所有专有名词并检查我们投资组合中的股票是否包含在这些专有名词中。
要找到所有专有名词,我们首先要将标记的字符串输出拆分为标记(使用空格作为分隔符),然后将每个标记拆分为下划线 ( _
) 并检查词性是否为专有名词.
一旦我们拥有了所有专有名词,我们将希望将它们存储在一个更适合我们的目的的数据结构中。 对于我们的示例,我们将使用HashSet
。 作为不允许重复条目和不跟踪条目顺序的交换, HashSet
允许非常快速的成员资格查询。 由于我们只对查询成员资格感兴趣,因此HashSet
非常适合我们的目的。
下面是实现专有名词拆分和存储的函数。 将此函数放在您的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; }
但是,此实现存在问题。 如果公司名称由多个单词组成(例如,Luxottica 示例中的 Carl Zeiss),则此实现将无法识别它。 在 Carl Zeiss 的示例中,“Carl”和“Zeiss”将分别插入到集合中,因此永远不会包含单个字符串“Carl Zeiss”。
为了解决这个问题,我们可以收集所有连续的专有名词并用空格将它们连接起来。 这是完成此操作的更新实现:
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; }
现在该函数应该返回一个包含单个专有名词和连续专有名词(即,由空格连接)的集合。 如果您打印propNounSet
,您应该会看到如下内容:
[... 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, ...]
将投资组合与 PropNouns 集进行比较
我们快完成了!
在前面的部分中,我们构建了一个可以下载和提取文章正文的刮板,一个可以解析文章正文并识别专有名词的标记器,以及一个接收标记输出并将专有名词收集到HashSet
中的处理器。 现在剩下要做的就是获取HashSet
并将其与我们感兴趣的公司列表进行比较。
实现非常简单。 在您的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); }
把它们放在一起
现在我们可以运行整个应用程序——抓取、清理、标记、收集和比较。 这是贯穿整个应用程序的函数。 将此函数添加到您的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); }
最后,我们可以使用该应用程序了!
这是一个使用与上述相同的文章和 Luxottica 作为投资组合公司的示例:
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"); } }
运行此程序,应用程序应打印“文章提及投资组合公司”。
将投资组合公司从 Luxottica 更改为文章中未提及的公司(如“微软”),应用应打印“文章未提及投资组合公司”。
构建 NLP 应用程序并不难
在本文中,我们逐步完成了构建一个应用程序的过程,该应用程序从 URL 下载文章,使用 Boilerpipe 清理它,使用斯坦福 NLP 处理它,并检查文章是否包含感兴趣的特定引用(在我们的例子中,我们的公司文件夹)。 正如所展示的,利用这一系列技术可以将原本艰巨的任务变成相对简单的任务。
我希望这篇文章向您介绍了自然语言处理中有用的概念和技术,并启发您编写自己的自然语言应用程序。
[注意:您可以在此处找到本文中引用的代码的副本。]