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