如何构建自然语言处理应用

本文概述

在过去的几年中, 自然语言处理(一种允许软件应用程序处理人类语言的技术)变得相当普遍。

Google搜索越来越能够回答听起来自然的问题, Apple的Siri能够理解各种各样的问题, 并且越来越多的公司正在(合理地)使用智能聊天和电话bot与客户进行交流。但是, 这种看似”智能”的软件如何真正起作用?

如何构建自然语言处理应用1

在本文中, 你将学习使这些应用程序运转的技术, 并且将学习如何开发自己的自然语言处理软件。

本文将引导你完成构建新闻相关性分析器的示例过程。想象一下, 你有一个股票投资组合, 并且你想要一个应用程序自动浏览热门新闻网站, 并识别与你的投资组合相关的文章。例如, 如果你的股票投资组合包括Microsoft, BlackStone和Luxottica之类的公司, 那么你可能希望看到提及这三家公司的文章。

斯坦福大学自然语言处理图书馆入门

与其他任何机器学习应用程序一样, 自然语言处理应用程序也基于大量相对较小, 简单, 直观的算法协同工作。使用所有已经实现并集成了所有算法的外部库通常很有意义。

对于我们的示例, 我们将使用Stanford NLP库, 这是一个功能强大的基于Java的自然语言处理库, 它支持多种语言。

我们对此库感兴趣的一种特殊算法是词性(POS)标记器。 POS标记器用于自动将词性分配给一段文本中的每个单词。该POS标记器根据词法特征对文本中的单词进行分类, 并针对它们周围的其他单词进行分析。

POS标记器算法的确切机制超出了本文的范围, 但是你可以在此处了解更多信息。

首先, 我们将创建一个新的Java项目(你可以使用自己喜欢的IDE), 并将Stanford 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库基于样板算法提供了不同的提取器, 其中ArticleExtractor特别针对HTML格式的新闻文章进行了优化。 ArticleExtractor专门针对每个内容块中使用的HTML标签和出站链接密度。这比更快但更简单的DefaultExtractor更适合我们的任务。

内置函数为我们处理所有事情:

  • HTMLFetcher.fetch获取HTML文档
  • getTextDocument提取文本文档
  • CommonExtractors.ARTICLE_EXTRACTOR.getText使用样板算法从文章中提取相关文本

现在, 你可以在示例文章中找到有关光学巨头Essilor和Luxottica合并的示例, 可以在此处找到。你可以将此URL馈入函数, 然后查看结果。

将以下代码添加到你的主要功能中:

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-m-a-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 a.m. 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)标记器。特别是, 我们可以使用POS标记器在文章中找到所有专有名词, 并将它们与我们的有趣股票组合进行比较。

通过结合NLP技术, 我们不仅可以提高标记器的准确性, 并最大程度地减少上述错误肯定和否定的情况, 而且由于专有名词仅占很小的一部分, 我们还可以极大地减少与股票投资组合相比所需的文字量文章全文。

通过将投资组合预处理为具有低成员资格查询成本的数据结构, 我们可以大大减少分析文章所需的时间。

Stanford 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将字符串作为输入, 并输出一个字符串, 其中包含原始字符串中的单词以及相应的语音部分。在你的主要功能中, 实例化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 a.m._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), 则此实现将无法捕获它。在卡尔·蔡司的示例中, “卡尔”和”蔡司”将分别插入到集合中, 因此将永远不会包含单个字符串”卡尔·蔡司”。

为了解决这个问题, 我们可以收集所有连续的专有名词并将它们与空格连接起来。这是完成此操作的更新的实现:

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

运行此命令, 应用程序应显示”文章提及投资组合公司”。

将投资组合公司从Luxottica更改为本文中未提及的公司(例如” Microsoft”), 然后该应用程序应显示”文章未提及投资组合公司”。

编写NLP应用程序并不困难

在本文中, 我们逐步完成了构建应用程序的过程, 该应用程序从URL下载文章, 使用Boilerpipe清理文章, 使用Stanford NLP处理文章, 并检查文章是否引用了特定的参考文献(在我们的案例中, 我们公司投资组合)。如图所示, 利用这一系列技术会使原本艰巨的任务变成相对简单的任务。

我希望本文向你介绍了自然语言处理中的有用概念和技术, 并启发了你编写自己的自然语言应用程序。

[注意:你可以在此处找到本文引用的代码的副本。]

微信公众号
手机浏览(小程序)
0
分享到:
没有账号? 忘记密码?