本系列博客的目地是通過(guò)使用兩個(gè)領(lǐng)先的生產(chǎn)級(jí)語(yǔ)言處理庫(kù)(John Snow Labs的Apache Spark NLP和Explosion AI的spaCy)來(lái)處理真實(shí)的自然語(yǔ)言處理(NLP)場(chǎng)景,從而對(duì)他們做一個(gè)比較。這兩個(gè)庫(kù)都是開源的,有商業(yè)使用許可證(分別是Apache 2.0和MIT)。兩者的發(fā)展都很活躍,發(fā)布也很頻繁,且社區(qū)不斷增長(zhǎng)。
我希望能分析和識(shí)別這兩個(gè)庫(kù)的優(yōu)點(diǎn),發(fā)現(xiàn)對(duì)數(shù)據(jù)科學(xué)家和開發(fā)人員而言它們有什么區(qū)別,以及在哪些情況下使用其中一種或另一種更方便。本次分析希望進(jìn)行一次客觀的探索,并在幾個(gè)階段加入一定量的主觀決定(就像每個(gè)自然語(yǔ)言理解應(yīng)用中那樣)。
盡管聽起來(lái)很簡(jiǎn)單,但比較兩個(gè)不同的庫(kù),并做出可比較的基準(zhǔn)測(cè)試是非常具有挑戰(zhàn)性的。請(qǐng)記住,你的應(yīng)用程序會(huì)與這里所做的有著不同的場(chǎng)景、數(shù)據(jù)管道、文本特性、硬件設(shè)置和一些非功能性要求。
我會(huì)假定讀者已經(jīng)熟悉NLP的概念和編程。你們可能沒(méi)有這兩個(gè)工具的相關(guān)知識(shí),不過(guò)我的目標(biāo)是使代碼盡可能自我說(shuō)明,提高讀性,從而不會(huì)讓讀者陷入太多的細(xì)節(jié)里。這兩個(gè)庫(kù)都有公開的文檔,并且是完全開源的。所以我建議你先看一下spaCy 101和Spark-NLP快速入門文檔。
關(guān)于這兩個(gè)庫(kù)
Spark-NLP于2017年10月被開源。作為一個(gè)Spark庫(kù),它是Apache Spark的原生擴(kuò)展。它以估計(jì)器(estimator)和轉(zhuǎn)換器(transformer)的形式引入了一套Spark ML Pipeline 階段(stage),用來(lái)處理分布式數(shù)據(jù)集。Spark NLP Annotators不僅包括如分詞、標(biāo)準(zhǔn)化和詞性標(biāo)注等基本功能,還有諸如高級(jí)情感分析、拼寫檢查、斷言狀態(tài)等其他高級(jí)功能。這些都在工作在Spark ML框架內(nèi)。Spark-NLP用Scala編寫,在JVM中運(yùn)行,并利用了Spark的優(yōu)化和執(zhí)行計(jì)劃。該庫(kù)目前提供Scala和Python的API。
spaCy是一個(gè)流行且易于使用的自然語(yǔ)言處理的Python庫(kù)。它最近發(fā)布了2.0版,其中包含了神經(jīng)網(wǎng)絡(luò)、實(shí)體識(shí)別等非常多的模型。它提供了目前業(yè)界領(lǐng)先的準(zhǔn)確性和速度,并且擁有一個(gè)活躍的開源社區(qū)。spaCy的出現(xiàn)至少有三年的時(shí)間了,它GitHub上的第一個(gè)版本可以追溯到2015年初。
Spark-NLP目前還沒(méi)有包括一套預(yù)訓(xùn)練的模型。而spaCy對(duì)七種(歐洲)語(yǔ)言提供了預(yù)先訓(xùn)練的模型,因此用戶可以快速注入目標(biāo)句子并在無(wú)需訓(xùn)練模型的情況下返回結(jié)果,包括分詞、詞條、詞類(POS)、相似性、實(shí)體識(shí)別等。
這兩個(gè)庫(kù)都提供了通過(guò)參數(shù)在某些級(jí)別的自定義,允許在磁盤中保存訓(xùn)練過(guò)的管道,并需要開發(fā)人員在特定使用案例中開發(fā)使用這些庫(kù)的程序。Spark NLP讓把一個(gè)NLP管道作為Spark ML機(jī)器學(xué)習(xí)管道(從數(shù)據(jù)加載、NLP、特征工程、模型訓(xùn)練、超參數(shù)調(diào)優(yōu)以及評(píng)估)的一部分來(lái)嵌入其中更容易。同時(shí)由于Spark可以優(yōu)化整個(gè)管道執(zhí)行過(guò)程,從而使Spark NLP的執(zhí)行速度更快。
基準(zhǔn)應(yīng)用
我在這里編寫的程序?qū)㈩A(yù)測(cè)原始.txt文件中的POS標(biāo)記。很多數(shù)據(jù)清理和準(zhǔn)備工作都是按順序進(jìn)行的。兩個(gè)應(yīng)用程序都將使用相同的數(shù)據(jù)進(jìn)行訓(xùn)練,并對(duì)同一數(shù)據(jù)進(jìn)行預(yù)測(cè),以實(shí)現(xiàn)最大可能的可比性。
我的目的是驗(yàn)證任何統(tǒng)計(jì)性程序的兩大支柱:
1.準(zhǔn)確性,衡量一個(gè)程序能夠正確預(yù)測(cè)語(yǔ)言特征的程度
2.性能,這意味著我需要等待多長(zhǎng)時(shí)間才能達(dá)到這樣的準(zhǔn)確度。以及在程序崩潰或我的孫子長(zhǎng)大之前,我可以向程序輸入多少輸入數(shù)據(jù)。
為了比較這些指標(biāo),我需要確保兩個(gè)庫(kù)有最大的可比性。我用了以下配置:
1.一臺(tái)臺(tái)式機(jī),操作系統(tǒng)是Linux Mint。帶有SSD硬盤和16GB內(nèi)存,以及4核3.5 GHz的英特爾i5-6600K處理器。
2.訓(xùn)練、測(cè)試和帶正確結(jié)果的數(shù)據(jù),這些數(shù)據(jù)遵循NLTK POS格式(見(jiàn)下文)。
3.安裝了spaCy 2.0.5的Jupyter Python 3 Notebook。
4.安裝了Spark-NLP 1.3.0和Apache Spark 2.1.1的Apache Zeppelin 0.7.3 Notebook。
數(shù)據(jù)
用于訓(xùn)練、測(cè)試和比對(duì)的數(shù)據(jù)來(lái)自?National American Corpus。我用了其中的報(bào)紙部分的MASC 3.0.2書面語(yǔ)料庫(kù)。
我用語(yǔ)料庫(kù)提供的工具(ANCtool)對(duì)數(shù)據(jù)進(jìn)行了整理。雖然我可以使用CoNLL數(shù)據(jù)格式,其中包含很多標(biāo)記信息,如詞條、索引和實(shí)體識(shí)別。但我更喜歡使用NLTK數(shù)據(jù)格式,其中包括Penn POS標(biāo)簽。它足以滿足我的目的。數(shù)據(jù)看起來(lái)像這樣:
Neither|DT?Davison|NNP?nor|CC?most|RBS?other|JJ?RxP|NNP?opponents|NNSdoubt|VBP?the|DT?efficacy|NN?of|IN?medications|NNS?.|.
如你所見(jiàn),訓(xùn)練數(shù)據(jù)中的內(nèi)容是:
- 檢測(cè)到的句子的邊界(新一行,新的句子)
- 分詞的結(jié)果(用空格分隔)
- 檢測(cè)到的POS(用“|”分隔)
在原始文本文件中,所有內(nèi)容都混在一起、混亂并且沒(méi)有任何標(biāo)準(zhǔn)邊界。
以下是我們要運(yùn)行的基準(zhǔn)測(cè)試的關(guān)鍵指標(biāo)。
基準(zhǔn)測(cè)試數(shù)據(jù)集
在本文中,我們將使用兩個(gè)基準(zhǔn)數(shù)據(jù)集。 第一個(gè)非常小,用來(lái)進(jìn)行交互式調(diào)試和實(shí)驗(yàn):
- 訓(xùn)練數(shù)據(jù):36個(gè).txt文件,總共77 KB
- 測(cè)試數(shù)據(jù):14個(gè).txt文件,共114 KB
- 需要預(yù)測(cè)21362個(gè)詞
第二組數(shù)據(jù)仍然不是“大數(shù)據(jù)”,而是一個(gè)相對(duì)大的數(shù)據(jù)集,用于評(píng)估典型的單機(jī)應(yīng)用場(chǎng)景:
- 訓(xùn)練數(shù)據(jù):72個(gè).txt文件,總共150 KB
- 兩個(gè)測(cè)試數(shù)據(jù)集:9225個(gè).txt文件,75 MB; 1125個(gè)文件,15 MB
- 需要預(yù)測(cè)1千3百萬(wàn)個(gè)詞
需要注意的是,我們這里并沒(méi)有評(píng)估“大數(shù)據(jù)”數(shù)據(jù)集。這是因?yàn)殡m然spaCy可以利用多核CPU,但它不能像Spark NLP那樣原生就可以使用集群。 因此,Spark NLP在使用集群的TB級(jí)數(shù)據(jù)集上的速度要比spaCy快幾個(gè)數(shù)量級(jí)。同樣,大型機(jī)上的數(shù)據(jù)庫(kù)的性能也會(huì)超過(guò)我這里本地安裝的MySQL數(shù)據(jù)庫(kù)。我的目標(biāo)是在單機(jī)上評(píng)估這兩個(gè)庫(kù),并使用這兩個(gè)庫(kù)的多核功能。這是開發(fā)時(shí)常見(jiàn)的情況,也適用于不需要處理大型數(shù)據(jù)集的應(yīng)用程序。
開始吧
那么讓我們動(dòng)手吧。 首先,我們必須要導(dǎo)入相關(guān)的庫(kù)并啟動(dòng)。?
spaCy
import os
import io
import time
import re
import random
import pandas as pd
import spacy
nlp_model = spacy.load(‘en’, disable=[‘parser’, ‘ner’])
nlp_blank = spacy.blank(‘en’, disable=[‘parser’, ‘ner’])
我禁用了spaCy中的一些管道,以避免不必要的解析器使它過(guò)于臃腫。我還使用了一個(gè)nlp_model作為參考,這是spaCy提供的一個(gè)預(yù)先訓(xùn)練好的NLP模型。但我將使用nlp_blank,這將更具代表性,它將是我自行訓(xùn)練的模型。
Spark-NLP
import org.apache.spark.sql.expressions.Window
import org.apache.spark.ml.Pipeline
import com.johnsnowlabs.nlp._
import com.johnsnowlabs.nlp.annotators._
import com.johnsnowlabs.nlp.annotators.pos.perceptron._
import com.johnsnowlabs.nlp.annotators.sbd.pragmatic._
import com.johnsnowlabs.nlp.util.io.ResourceHelper
import com.johnsnowlabs.util.Benchmark
我面臨的第一個(gè)挑戰(zhàn)是我要處理三種完全不同的分詞結(jié)果,這會(huì)導(dǎo)致難以確定一個(gè)詞是否與分詞和POS標(biāo)記相匹配:
1.spaCy的分詞器采用基于規(guī)則的方法,并且已經(jīng)包含了一個(gè)詞匯表,其中保存了許多常見(jiàn)的縮略語(yǔ)用于分詞。
2.SparkNLP的分詞器有自己的分詞規(guī)則。
3.我的訓(xùn)練和測(cè)試數(shù)據(jù)。這些數(shù)據(jù)按照ANC的標(biāo)準(zhǔn)進(jìn)行分詞。在很多情況下,它分割詞的方式與這兩個(gè)庫(kù)的分詞器完全不同。
所以,為了克服這個(gè)問(wèn)題,我需要決定如何比較一組完全不同的標(biāo)簽的POS標(biāo)簽。對(duì)于Spark-NLP,我將保持原樣。它的默認(rèn)規(guī)則與ANC開放標(biāo)準(zhǔn)分詞格式基本匹配。對(duì)于spaCy,我需要放松中綴規(guī)則,以便通過(guò)不使用“ – ”分割詞來(lái)增加分詞的匹配準(zhǔn)確度。
spaCy
class DummyTokenMatch:
? ? def __init__(self, content):
? ? ? ? self.start = lambda : 0
? ? ? ? self.end = lambda : len(content)
def do_nothing(content):
? ? return [DummyTokenMatch(content)]
model_tokenizer = nlp_model.tokenizer
nlp_blank.tokenizer = spacy.tokenizer.Tokenizer(nlp_blank.vocab, prefix_search=model_tokenizer.prefix_search,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? suffix_search=model_tokenizer.suffix_search,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? infix_finditer=do_nothing,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? token_match=model_tokenizer.token_match)
請(qǐng)注意:我向nlp_blank傳遞了vocab對(duì)象,因此nlp_blank并不是真的空。 這個(gè)vocab詞匯對(duì)象包含有英語(yǔ)語(yǔ)言規(guī)則和策略,可幫助我們的空白模型標(biāo)記POS,并對(duì)英語(yǔ)單詞進(jìn)行分詞。因此,spaCy開始是有一點(diǎn)小優(yōu)勢(shì)的,而Spark-NLP事先并“不懂”英語(yǔ)。
訓(xùn)練管道
現(xiàn)在來(lái)到訓(xùn)練這一步。在spaCy里,我需要提供一個(gè)指定的訓(xùn)練數(shù)據(jù)格式,它的格式如下所示:
TRAIN_DATA = [
(“I like green eggs”, {‘tags’: [‘N’, ‘V’, ‘J’, ‘N’]}),
(“Eat blue ham”, {‘tags’: [‘V’, ‘J’, ‘N’]})
]
而在Spark-NLP中,我必須提供一個(gè)文件夾,其中包含帶“分隔詞|標(biāo)記”格式的.txt數(shù)據(jù)文件,它看起來(lái)就像ANC訓(xùn)練數(shù)據(jù)。所以,我只需將文件夾路徑傳遞給POS標(biāo)記器(即PerceptronApproach)。
讓我們加載spaCy的訓(xùn)練數(shù)據(jù)。在下面代碼里,我必須添加一些手工生成的例外、規(guī)則與一些字符,因?yàn)閟paCy的訓(xùn)練集需要干凈的數(shù)據(jù)。
spaCy
start = time.time()
train_path = “./target/training/”
train_files = sorted([train_path + f for f in os.listdir(train_path) if os.path.isfile(os.path.join(train_path, f))])
TRAIN_DATA = []
for file in train_files:
? ? fo = io.open(file, mode=’r’, encoding=’utf-8′)
? ? for line in fo.readlines():
? ? ? ? line = line.strip()
? ? ? ? if line == ”:
? ? ? ? ? ? continue
? ? ? ? line_words = []
? ? ? ? line_tags = []
? ? ? ? for pair in re.split(“\\s+”, line):
? ? ? ? ? ? tag = pair.strip().split(“|”)
? ? ? ? ? ? line_words.append(re.sub(‘(\w+)\.’, ‘\1’, tag[0].replace(‘$’, ”).replace(‘-‘, ”).replace(‘\”, ”)))
? ? ? ? ? ? line_tags.append(tag[-1])
? ? ? ? TRAIN_DATA.append((‘ ‘.join(line_words), {‘tags’: line_tags}))
? ? fo.close()
TRAIN_DATA[240] = (‘The company said the one? time provision would substantially eliminate all future losses at the unit .’, {‘tags’: [‘DT’, ‘NN’, ‘VBD’, ‘DT’, ‘JJ’, ‘-‘, ‘NN’, ‘NN’, ‘MD’, ‘RB’, ‘VB’, ‘DT’, ‘JJ’, ‘NNS’, ‘IN’, ‘DT’, ‘NN’, ‘.’]})
n_iter=5
tagger = nlp_blank.create_pipe(‘tagger’)
tagger.add_label(‘-‘)
tagger.add_label(‘(‘)
tagger.add_label(‘)’)
tagger.add_label(‘#’)
tagger.add_label(‘…’)
tagger.add_label(“one-time”)
nlp_blank.add_pipe(tagger)
optimizer = nlp_blank.begin_training()
for i in range(n_iter):
? ? random.shuffle(TRAIN_DATA)
? ? losses = {}
? ? for text, annotations in TRAIN_DATA:
? ? ? ? nlp_blank.update(, [annotations], sgd=optimizer, losses=losses)
? ? print(losses)
print (time.time() – start)
運(yùn)行時(shí)間
{‘tagger’: 5.773235303101046}
{‘tagger’: 1.138113870966123}
{‘tagger’: 0.46656132966405683}
{‘tagger’: 0.5513760568314119}
{‘tagger’: 0.2541630900934435}
Time to run: 122.11359786987305 seconds
為了繞過(guò)一些坑,我不得不做了一些額外工作。spaCy不讓我使用我的分詞器的詞匯,因?yàn)樗锩姘艘恍┏舐淖址@纾谴嬖谟趘ocab標(biāo)簽中,否則spaCy不會(huì)訓(xùn)練帶有“l(fā)arge-screen”或“No”標(biāo)記的句子。我必須將這些字符添加到vocab列表中,以便在訓(xùn)練期間spaCy可以找到它們。
現(xiàn)在,讓我們看看在Spark-NLP中如何構(gòu)建管道的。
Spark-NLP
val documentAssembler = new DocumentAssembler()
? ? .setInputCol(“text”)
? ? .setOutputCol(“document”)
val tokenizer = new Tokenizer()
? ? .setInputCols(“document”)
? ? .setOutputCol(“token”)
? ? .setPrefixPattern(“\\A([^\\s\\p{L}\\d\\$\\.#]*)”)
? ? .addInfixPattern(“(\\$?\\d+(?:[^\\s\\d]{1}\\d+)*)”)
?? ?
val posTagger = new PerceptronApproach()
? ? .setInputCols(“document”, “token”)
? ? .setOutputCol(“pos”)
? ? .setCorpusPath(“/home/saif/nlp/comparison/target/training”)
? ? .setNIterations(5)
?? ?
val finisher = new Finisher()
? ? .setInputCols(“token”, “pos”)
? ? .setOutputAsArray(true)
val pipeline = new Pipeline()
? ? .setStages(Array(
? ? ? ? documentAssembler,
? ? ? ? tokenizer,
? ? ? ? posTagger,
? ? ? ? finisher
? ? ))
val model = Benchmark.time(“Time to train model”) {
? ? pipeline.fit(data)
}
正如你所看到的,構(gòu)建一個(gè)管道是一個(gè)非常線性的過(guò)程:設(shè)置文檔組裝器,這使得目標(biāo)文本列成為后續(xù)注釋器(即分詞器)的目標(biāo);接著PerceptronApproach就是POS模型,它將同時(shí)接收文檔文本和符號(hào)化表單作為輸入。
我必須更新前綴模式,并添加一個(gè)新的中綴模式,以便使用和ANC相同的方式匹配日期和數(shù)字(這可能會(huì)在Spark NLP的下一版本中成為默認(rèn)模式)。正如你所看到的,管道的每個(gè)組件都在用戶的控制之下; 沒(méi)有隱含的vocab或英語(yǔ)知識(shí),這和spaCy不同。
來(lái)自PerceptronApproach的corpusPath被傳遞到包含管道分隔文本文件的文件夾。finisher注釋器包裝POS和分詞的結(jié)果,以便下一步使用它。正如SetOutputAsArray()的名字所表示的,它會(huì)返回一個(gè)數(shù)組而不是一個(gè)拼接起來(lái)的字符串,不過(guò)這在處理時(shí)會(huì)有一定的計(jì)算代價(jià)。
傳遞給fit()的數(shù)據(jù)并不重要,因?yàn)槲ㄒ槐挥?xùn)練的NLP標(biāo)注器是PerceptronApproach。而且這個(gè)標(biāo)注器是用外部POS Corpora進(jìn)行訓(xùn)練的。
運(yùn)行時(shí)間
Time to train model: 3.167619593sec
作為一個(gè)附注,可以在管道中注入SentenceDetector或SpellChecker。這樣在某些情況下,可以通過(guò)讓模型知道句子結(jié)束的位置來(lái)幫助提升POS的準(zhǔn)確性。
接下來(lái)做什么?
到目前為止,我們已經(jīng)初始化了庫(kù),加載了數(shù)據(jù),并且用兩個(gè)庫(kù)都訓(xùn)練了一個(gè)分詞器模型。需要注意的是,spaCy帶有一個(gè)預(yù)先訓(xùn)練好的分詞器,因此如果你的文本數(shù)據(jù)來(lái)自于spaCy訓(xùn)練過(guò)的語(yǔ)言(例如英語(yǔ))和領(lǐng)域(例如新聞報(bào)道),這一步可能不是必需的。但為了讓生成的符號(hào)與我們的ANC語(yǔ)料庫(kù)更匹配,對(duì)分詞中綴的修改是非常重要的。在迭代5次的情況下,Spark-NLP的訓(xùn)練速度比spaCy快了38倍多。
在此系列的下一篇中我們將通過(guò)使用剛剛訓(xùn)練出的模型來(lái)運(yùn)行NLP管道,介紹代碼、準(zhǔn)確性和性能。
相關(guān)資料:
Saif Addin Ellafi
Saif Addin Ellafi是一名軟件開發(fā)者、分析師、數(shù)據(jù)科學(xué)家,并永遠(yuǎn)是一名學(xué)生。他同時(shí)還是一名極限運(yùn)動(dòng)和游戲愛(ài)好者。他在銀行和金融行業(yè)的數(shù)據(jù)領(lǐng)域擁有豐富的解決問(wèn)題和測(cè)試的經(jīng)驗(yàn)。現(xiàn)在他在John Snow Labs,并是Spark-NLP的主要貢獻(xiàn)者。

