當(dāng)我們處理自然語言(相對于程序語言而言)的時候會遇到一項挑戰(zhàn),即涵義模棱兩可。程序語言是被設(shè)計成為有且只有一個可能解釋的語言,而人類語言可能由于模糊性和不確定性衍生出很多問題。這是由于有時候你并不想確切地告訴別人你對某事物的想法。在社交場合這完全沒有問題,但是當(dāng)你試圖使用計算機來處理人類語言的話,就會非常痛苦。
詞法標識(token)就是一個簡單的例子。程序語言的詞法分析對于標識表示什么,它是什么類型(語句分隔符,標識符,保留關(guān)鍵字等等)是什么有著明確的規(guī)則。而自然語言則遠不能如此清晰可辯。can’t 是一個還是兩個標識?并且根據(jù)你做出的判斷,cannot 或者 can not 這兩個應(yīng)該是相同意思的詞又各是幾個標識呢?很多復(fù)合詞都可以寫成一個詞(比如:bookshelf),或者兩個詞(比如:lawn mower),甚至還可以用連字符來連接(比如:life-cycle)。有些字符 (比如說連字符或者右肩單撇號),可以有很多種解釋,而如何選擇正確字符往往取決于上下文語言環(huán)境(撇號在一個單詞的最后是表示所有格符號還是后單引號?)
句子的情況同樣不怎么好:如果簡單認為句號是用來結(jié)束一個句子的話,在我們使用縮寫或是序數(shù)的時候就悲劇了。雖然通常情況下,我們是可以解決這個問題的,但是對有些句子而言,除非將整個段落徹底分析,否則無法真正確定這些句子的意思。我們?nèi)祟惿踔烈矡o法有意識地考慮這些問題。
不過我們希望能夠處理人類語言,因為在跟軟件交流的時候,使用人類語言對用戶更加友好。我們更愿意直接告訴計算機要做什么,讓計算機為我們分析報紙文章,并對我們感興趣的新聞做個總結(jié),而不是通過敲擊鍵盤或者點擊小小的按鈕(或者在小小的虛擬鍵盤上打字)來讓計算機為我們做這些事。其中有些還在我們的能力范圍之外(至少在蘋果為我們提供與 Siri 交互的 API 之前)。但是有些已經(jīng)成為可能,那就是 NSLinguisticTagger。
NSLinguisticTagger 是 Foundation 框架中命名極為不當(dāng)?shù)念愔?,這是因為它遠遠不止是一個小小的詞性 tagger,而是集詞法分析,分詞器,命名實體識別及詞性標注為一體的類。換句話說,它幾乎可以滿足你處理某些計算機語言處理的全部要求。
為了展示 NSLinguisticTagger 類的用法,我們會開發(fā)一個靈活的工具用來搜索。我們有一個充滿了文本(比如新聞,電郵,或者其他的任意文本)的集合,然后我們輸入一個單詞,這個單詞將返回所有包含這個單詞的句子。我們會忽略功能詞(比如 the,of 或者 and),因為它們在這個語言環(huán)境中太過于常見,沒有什么用處。我們目前要實現(xiàn)的是第一步:從一個單獨文件中提取相關(guān)單詞。由此可以迅速地擴展到提供完整功能。
GitHub 上有源代碼和樣本文本。這是《衛(wèi)報》上一篇關(guān)于中英貿(mào)易的文章。當(dāng)用軟件分析這份文本時,你會發(fā)現(xiàn),它并不是總是運行良好,不過,出現(xiàn)運行故障完全正常:人類語言和任何正式語言都不同,人類語言凌亂復(fù)雜,無法簡單劃歸到整齊劃一的規(guī)則系統(tǒng)。很多理論問題(哪怕就像詞性一樣基礎(chǔ)的問題)在某種程度上是無法解決的,這是由于我們?nèi)匀粚θ绾尾拍茏詈玫孛枋稣Z言還所知甚少。比如說,詞的分類是以拉丁語為依據(jù)的,但這并不意味著就必定適合英語。它們充其量只是大概近似而已。不過從很多實際的目的來看,這樣就已經(jīng)足夠了,不需要讓人怎么擔(dān)心了。
注釋和標記文本的核心方法就是標簽體系的核心方法。以下是幾個可用的標簽體系:
NSLinguisticTagSchemeTokenTypeNSLinguisticTagSchemeLexicalClassNSLinguisticTagSchemeNameTypeNSLinguisticTagSchemeNameTypeOrLexicalClassNSLinguisticTagSchemeLemmaNSLinguisticTagSchemeLanguageNSLinguisticTagSchemeScriptNSLinguisticTagger 實例掃描文本中的所有條目,并調(diào)用一個包含被請求的標簽體系值的 block。最基礎(chǔ)的是 NSLinguisticTagSchemeTokenType:詞,標點,空格,或是“其他”。我們可以使用這個來識別哪些是真正的詞,那么我們在應(yīng)用程序中就可以簡單地忽略其他那些不是有效詞的語素。NSLinguisticTagSchemeLexicalClass 和詞性有關(guān),是一組非常基礎(chǔ)的標簽(就嚴格意義上的語言分析而言,這組標簽還遠遠不夠精細),我們可以使用這組標簽來分辨我們想要的實詞(名詞,動詞,形容詞,副詞)和我們想忽略的虛詞(連詞,介詞,冠詞等等)。在 NSLinguisticTagger 類的文檔中寫明了全套可能值。
NSLinguisticTagSchemeNameType 是指命名實體識別:我們可以知道一個詞是不是表示人物,地點或者組織。同樣的,這相對于自然語言的處理而言是相當(dāng)基本,但卻非常有用的,比如說你想搜索一個特定的人物或者地點。還有一種潛在的應(yīng)用是“給我一份文本中所提到的所有政治家的名錄”,你可以瀏覽這份文本中的人名,然后查閱數(shù)據(jù)庫(比如維基)來核對他們是否確實是政治家。這也可以跟 lexical 類相結(jié)合,因為這往往包含一個分類叫做“名字”。
NSLinguisticTagSchemeLemma 是詞匯的標準形式,或者說是其基本形式。對英語而言,這不是什么大問題,不過對于其它語言而言卻重要得多。原型基本上就是你在詞典中查的到的那個形式。比如說,tables 是一個復(fù)數(shù)名詞,它的基本形式是單數(shù)的 table。同樣的,動詞 running 是由 run 變形而來的不定式。如果你想要以同樣的方式處理各種詞類的變形,使用原形就非常有用,事實上這也是我們要為我們的示例應(yīng)用程序所做的 (因為這可以有助于保持索引不過于龐大)。
NSLinguisticTagSchemeLanguage 和我們所使用的語言相關(guān)。如果你使用iOS(截至iOS7),目前只能處理英語。使用OS X(截至10.9 / Mavericks)你可以稍微多幾種語言可以選擇。+[NSLinguisticTagger availableTagSchemesForLanguage:] 方法為我們列舉了對于給定語言的所有可用體系。對于在 iOS 中對應(yīng)語言數(shù)量限制的原因很可能是資源文件要占用大量空間。在筆記本或者臺式電腦上不是什么大問題,但是在手機或者平板上的話就不太妙了。
NSLinguisticTagSchemeScript 是書寫體系,比如拉丁字母 (Latin),西里爾字母 (Cyrillic) 等等。對于英語,我們將使用拉丁字母。如果你知道你將處理哪種語言,使用 setOrthography 方法可以改善標簽的結(jié)果,特別對相對較短的字符而言更是如此。
目前我們已經(jīng)知道 NSLinguisticTagger 可以為我們識別什么了,我們需要告訴它我們想要什么,以及我們想如何獲得。這里有幾個可以定義 tagger 行為的選項,它們都是 NSUInteger 類型的,并且可以使用位運算 OR 組合使用。
第一個選項是“省略單詞”,除非你只想看標點或者其它非詞類,否則這個選項毫無意義。比較有用的是下面的三個選項:“省略標點(omit punctuation)”,“省略空格(omit whitespace)”以及“省略其他(omit other)”。除非你想要對文本做全面語言分析,否則你基本上只會對單詞感興趣,而對其中的逗號句號則興趣不大。有了這些選項,就可以輕輕松松讓 tagger 對單詞作出限制,再也不用掛慮在心。最后一個選項是“連接名字(join names)”,因為名字有時不僅僅是一個標識。這個選項會將它們結(jié)合在一起,作為一個獨立的語言單位來處理。這個選項可能不會總是用得上,但是確實非常有用。舉個例子,在樣本文本中,字符串“Owen Patterson”被識別為一個名稱,并且作為一個獨立的語言單位被返回。
程序會給一定數(shù)量的文本在獨立文件中建立索引(我們假設(shè)是使用UTF-8編碼)。我們將使用一個 FileProcessor 類來處理一個單獨文件,將文件內(nèi)容分為一個一個單詞,再把這些單詞傳遞給另一類來進行處理。后一個類將實現(xiàn) WordReceiver 接口,其中包括一個方法:
-(void)receiveWord:(NSDictionary*)word
我們不是使用 NSString 來表示單詞,而是使用字典,這是因為一個單詞會有很多屬性,包括實際標識,詞性或名稱類型,原型,所在句子的數(shù)目,句子中的位置等。為了建立索引,我們還想儲存文件名。調(diào)用 FileProcessor 的這個方法:
- (BOOL)processFile:(NSString*)filename
將觸發(fā)分析,如果一切進行順利的話,返回 YES,在出現(xiàn)錯誤的時候返回 NO。它首先由文件創(chuàng)建一個 NSString,然后將其傳遞給一個 NSLinguisticTagger 實例來處理。
NSLinguisticTagger 主要做的是的在一個 NSString 中進行掃描并對尋找到的每一個元素調(diào)用 block。為了稍作簡化,我們首先將文本分解為一個個的句子,然后分別掃描每一個句子。這樣比較容易追蹤句子的 ID。至于標簽,我們會處理大量的 NSRange,它們可以被用來界定源文件中文本的注解。我們從在第一個句子范圍內(nèi)創(chuàng)建一個搜索范圍開始,并使用其在最大程度上獲得初始語句的標簽。
NSRange currentSentence = [tagger sentenceRangeForRange:NSMakeRange(0, 1)];
一旦句子處理結(jié)束,就檢查是否成功完成全部的文本,或者是否還有更多的句子等待處理:
if (currentSentence.location + currentSentence.length == [fileContent length]) {
currentSentence.location = NSNotFound;
} else {
NSRange nextSentence = NSMakeRange(currentSentence.location + currentSentence.length + 1, 1);
currentSentence = [tagger sentenceRangeForRange:nextSentence];
}
如果已經(jīng)到了文本的末尾,我們將使用 NSNotFound 來對 while 循環(huán)發(fā)出終止信號。如果我們使用一個超出文本之外的范圍,NSLinguisticTagger 將拋出一個異常并且直接崩潰。
句子處理循環(huán)中的主要方法調(diào)用如下:
while (currentSentence.location != NSNotFound) {
__block NSUInteger tokenPosition = 0;
[tagger enumerateTagsInRange:currentSentence
scheme:NSLinguisticTagSchemeNameTypeOrLexicalClass
options:options
usingBlock:^(NSString *tag, NSRange tokenRange, NSRange sentenceRange, BOOL *stop)
{
NSString *token = [fileContent substringWithRange:tokenRange];
NSString *lemma = [tagger tagAtIndex:tokenRange.location
scheme:NSLinguisticTagSchemeLemma
tokenRange: NULL
sentenceRange:NULL];
if (lemma == nil) {
lemma = token;
}
[self.delegate receiveWord:@{
@"token": token,
@"postag": tag,
@"lemma": lemma,
@"position": @(tokenPosition),
@"sentence": @(sentenceCounter),
@"filename": filename
}];
tokenPosition++;
}];
}
我們讓 tagger 處理 NSLinguisticTagSchemeNameTypeOrLexicalClass,指定一組選項(連接名字,省略標點和空格)。然后我們獲取這個標簽,以及搜索到的每一項條目的范圍,并進一步檢索信息。標識(token)是字符串一部分,僅僅由字符范圍來描述。lemma 是基本形式,如果不可能用的這個值會是 nil,所以我們需要做檢查,并使用標識字符串作為候補值。一旦收集到這個信息,我們就可以將其打包到一個字典中,然后發(fā)送給 delegate 進行處理。
在我們的示例應(yīng)用中,我們僅僅輸出了我們接收到的單詞,但是我們在這里基本上可以做任何我們想做的一切。為了實現(xiàn)搜索,我們可以過濾掉除了名詞,動詞,形容詞,副詞和名字以外的所有詞,并且在索引數(shù)據(jù)庫中儲存這些單詞的位置。使用原形,而不使用標識值,可以使我們合并各種詞的變形 (pig 和 pigs),這可以保持索引不過于龐大,并且與僅只匹配實際標識詞相比,也可以檢索出更相關(guān)的詞。請記住,你可能還要將所有查詢按照原形變化進行歸類,否則,搜索 pigs 的話將不會返回任何結(jié)果。
為了更加真實,我在樣本文本頭部信息中加進了一些基本 HTML 標簽,比如確定標題,署名,日期。在通過 tagger 運行的時候出現(xiàn)一個問題,即 NSLinguisticTagger 是不知道關(guān)于 HTML 的東西的,并試圖將這些 HTML 標記當(dāng)做文本來處理。下面是最前面的三個檢索詞。
{
filename = "/Users/oliver/tmp/guardian-article.txt";
lemma = "<";
position = 0;
postag = Particle;
sentence = 0;
token = "<";
}
{
filename = "/Users/oliver/tmp/guardian-article.txt";
lemma = h1;
position = 1;
postag = Verb;
sentence = 0;
token = h1;
}
{
filename = "/Users/oliver/tmp/guardian-article.txt";
lemma = ">";
position = 2;
postag = Adjective;
sentence = 0;
token = ">";
}
不僅僅是標簽被分成了幾個部分,被當(dāng)做詞來處理,而且還得到了奇怪和完全錯誤的標簽。所以,如果你在處理包含標記的文件,最好先將其過濾出來?;蛟S,你想要識別出標簽,并返回覆蓋標簽區(qū)域的 NSRange,而不是像我們之前處理示例應(yīng)用一樣將整個文本分成一個個句子。或者說,如果存在內(nèi)嵌標簽(比如加粗,斜體,超鏈接),將標簽全部剔除出來會更好些。
就算是用 tagger 來處理通用語言,其表現(xiàn)也出人意料的優(yōu)秀。如果你僅僅處理某一個領(lǐng)域(比如技術(shù)文本)的話,你可以做出一些在處理不受限制的文本時無法做到的假設(shè)。但是蘋果的 tagger 必須在無法預(yù)知會遇到什么的情況下也能工作,鑒于如此,它偶爾也會出錯,不過相對來說是非常少的。很顯然,很多名稱無法識別,比如說 Chengdu 這樣的地名。但另一方面,文本中大多數(shù)人名的處理都是非常不錯的。由于某些原因,日期(Wednesday 4 December 2013 10.35 GMT)被當(dāng)做了人名來處理,可能是來源于魯賓遜?克魯索的命名習(xí)慣吧。環(huán)境大臣 Owen Patterson 可以被識別出來,但是,一般被認為更加重要的首相 David Cameron 卻沒有被識別出來,盡管 David 是個更為常見的名字。
這是概率 tagger 的問題:有時候很難理解為什么某些詞以特定的方式被加上標簽。也沒有什么像鉤子一樣的東西可以掛靠 tagger,可以讓你提供比如說已知的地點,人物或者組織的名稱列表。你只能用默認設(shè)置進行處理。因此,最好使用大量數(shù)據(jù)來測試那些帶有 tagger 的應(yīng)用程序,通過觀察結(jié)果,你可以大概知道哪些可以正常運行,哪些會遇到問題。
有很多種方法來實現(xiàn)詞性標簽:兩個主要的途徑,一個是規(guī)則性的,一個是隨機性。兩種途徑都有一套相當(dāng)龐大的規(guī)則來告訴你,形容詞的后面是名詞,而不是冠詞,或者有一個概率矩陣告訴你某一個特定的標簽會出現(xiàn)在一個特定的語言環(huán)境中的可能性有多大。你也可以使用基于概率性的模型,同時添加一些規(guī)則來修正反復(fù)出現(xiàn)的典型錯誤,這就是所謂的混合 tagger。由于為不同語言開發(fā)規(guī)則集比自動學(xué)習(xí)隨機語言模型的成本要高得多,所以我猜測 NSLinguisticTagger 應(yīng)該是基于完全的隨機模型。這個實現(xiàn)細節(jié)也可以從下面的方法中窺探一二:
- (NSArray *)possibleTagsAtIndex:(NSUInteger)charIndex
scheme:(NSString *)tagScheme
tokenRange:(NSRangePointer)tokenRange
sentenceRange:(NSRangePointer)sentenceRange
scores:(NSArray **)scores
這說明了一個事實,那就是有時候(其實是大多數(shù)時候)會出現(xiàn)多個可能的標簽值,tagger 必須判斷哪個可能是錯誤的。使用這個方法,你可以獲得一份選項列表和概率得分。得分最高的詞則被 tagger 選中,但是如果你想要創(chuàng)建一套基于規(guī)則的后處理來改善 tagger 工作,你依然可以訪問得分第二的詞或者其他候選項。
對于這個方法要提高警惕,其中有個 bug,實際上它并沒有返回任何的分數(shù)。不過在 OS X 10.9 / Mavericks 中這個 bug 已被修復(fù)。所以,如果你需要支持 OS X 10.9 / Mavericks 之前的版本,會提示你無法使用這個方法。順帶一提,在 iOS 7 中這個方法可以良好運行。
下面是幾個 When is the next train…: 的輸出案例:
| When | is | the | next | train |
|---|---|---|---|---|
| Pronoun, 0.9995162 | Verb, 1 | Determiner, 0.9999986 | Adjective, 0.9292629 | Noun, 0.8741992 |
| Conjunction, 0.0004337671 | Adverb, 1.344403e-06 | Adverb, 0.0636334 | Verb, 0.1258008 | |
| Adverb, 4.170838e-05 | Preposition, 0.007003677 | |||
| Noun, 8.341675e-06 | Noun, 0.0001000525 |
正如你所見,在這個例子中到現(xiàn)在為止,正確的 tag 擁有最高的概率。對于大多數(shù)應(yīng)用程序而言,你可以保持程序簡單,并認可 tagger 所提供的標簽,而不對概率進行深究。不過你得承認 tagger 偶然也是會出錯的,而你也可以訪問到這些識別結(jié)果,并做出相應(yīng)處理。 當(dāng)然,如果你不親自檢查的話,你就不會知道 tagger 什么時候會出錯。然而,其中一個線索是概率差:如果概率非常接近(和上面的例子不同),說不定就表示可能出錯了。
處理自然語言是很困難的,蘋果給我們提供了一個非常好的工具,這個工具可以簡便地支持絕大多數(shù)使用情況。當(dāng)然,它也不是完美無缺的,即使最先進的語言處理工具也不是完美無缺的。iOS 目前只支持英語,不過隨著技術(shù)改善,以及如果有足夠大的內(nèi)存來儲存(毫無疑問會很大的)語言模型的話,這將有所改變。在此之前,我們會受到一些限制。不過還是有很多方法可以給應(yīng)用程序添加語言支持。在文本編輯器中突出動詞,理解用戶鍵入的內(nèi)容,或者處理外部數(shù)據(jù)文件等工作還是很簡單的,NSLinguisticTagger 可以幫助你做到這一點。