如何構建自然語言處理應用程序

已發表: 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 處理它,並檢查文章是否包含感興趣的特定引用(在我們的例子中,我們的公司文件夾)。 正如所展示的,利用這一系列技術可以將原本艱鉅的任務變成相對簡單的任務。

我希望這篇文章向您介紹了自然語言處理中有用的概念和技術,並啟發您編寫自己的自然語言應用程序。

[注意:您可以在此處找到本文中引用的代碼的副本。]