1. 程式人生 > >NLTK之詞性標註

NLTK之詞性標註

詞性標註重要性

回想學英語的時候,老師就開始講詞性,通過分析句子中某個單詞的詞性,我們可以推測這個詞的意思,猜測這個詞在句子中的作用,這對理解句子意思有極大的幫助。小弟也還是初學,以後若發現詞性有更多作用時會繼續補充~

標註語料庫

NLTK(3.2.5)中提供了一些已經標註好詞性的文字,通過下面程式碼可以檢視:

import nltk
nltk.corpus.brown.tagged_words()

outputs:
[(u'The', u'AT'), (u'Fulton', u'NP-TL'), ...]

這表示The 被標註為AT詞性,Fulton 被標註為NP-TL 詞性,似乎看不太懂?

下面可以把它們轉成統一詞性名稱

nltk.corpus.brown.tagged_words(tagset='universal')

outputs:
[(u'The', u'DET'), (u'Fulton', u'NOUN'), ...]

DET 是限定詞,NOUN 是名詞。

這是因為標註器本身所使用的符號統一符號 不一樣的原因,通過制定tagset 可以轉化為統一符號,而標註轉換可以在~/nltk_data/taggers/universal_tagset 中找到對應的檔案。

我調查原始碼發現,上述程式碼所使用的是en-brown.map檔案,開啟檢視可以發現:

....
36
AT DET .... 294 NP-TL NOUN ...

其他標註語料庫

nltk.corpus.sinica_treebank.tagged_words()
nltk.corpus.indian.tagged_words()
nltk.corpus.mac_morpho.tagged_words()
...

indian為例:

nltk.corpus.indian.tagged_words()

outputs:
[(u'\u09ae\u09b9\u09bf\u09b7\u09c7\u09b0', u'NN'), (u'\u09b8\u09a8\u09cd\u09a4\u09be\u09a8'
, u'NN'), ...]

似乎輸出了unicode_escape編碼,怎麼辦呢?下面可以解決:

print ', '.join([word + '/ + tag for (word, tag) in nltk.corpus.indian.tagged_words()][:100])

outputs:
মহিষের/NN, সন্তান/NN, :/SYM, তোড়া/NNP, উপজাতি/NN, ৷/SYM, বাসস্থান-ঘরগৃহস্থালি/NN, তোড়া/NNP, ভাষায়/NN, গ্রামকেও/NN, বলে/VM, `/SYM, মোদ/NN, '/SYM, ৷/SYM, মোদের/NN, আয়তন/NN, খুব/INTF, বড়ো/JJ, নয়/VM, ৷/SYM, প্রতি/QF, মোদে/NN, আছে/VM, কিছু/QF, কুঁড়েঘর/NN, ,/SYM, সাধারণ/JJ, মহিষশালা/NN, ৷/SYM, আর/CC, গ্রামের/NN, বাইরে/NST, থাকে/VM, ডেয়ারি-মন্দির/NN, ৷/SYM, আয়তনের/NN, তারতম্য/NN, অনুসারে/PSP, গ্রামগুলি/NN, দু/QC, রকমের/NN, :/SYM, এতূডমোদ/NNP, (/SYM, বড়ো/JJ, গ্রাম/NN, )/SYM, ওকিনমোদ/NNP, (/SYM, ছোট/JJ, গ্রাম/NN, )/SYM, ৷/SYM, কোন/DEM, কোন/RDP, গ্রামের/NN, আবার/CC, ধর্মীয়/JJ, বা_Cমহিষের/NN, সন্তান/NN, :/SYM, তোড়া/NNP, উপজাতি/NN, ৷/SYM, িকে/PRP, বলে/VM, `/SYM, সোতি-মোদ/NNP, '/SYM, ৷/SYM, কুঁড়েঘরগুলির/NN, আকার/NN, বাংলার/NNP, বা/CC, ভারতের/NNP, অন্য/JJ, অঞ্চলের/NN, প্রচলিত/JJ, কুঁড়ে/NN, ঘর/NN, নয়/VM, ৷/SYM, এগুলি/PRP, দেখতে/NN, শোয়ানো/JJ, পিপের/NN, মতো/PSP, ৷/SYM, এক/QC, দিকের/PSP, বাঁশের/NN, কাঠামো/NN, খিলানের/NN, মতো/PSP, বেঁকে/JJ, গিয়ে/VM, অন্যদিকের/NN, মাটিতে/NN, মিশেছে/VM

標註器

使用標註器

NLTK提供了現成的標註器,你可以直接使用:

text = nltk.word_tokenize('This beautiful future is just his imagination so far')
nltk.pos_tag(text, tagset='universal')

outputs:
[('This', u'DET'), ('beautiful', u'ADJ'), ('future', u'NOUN'), ('is', u'VERB'), ('just', u'ADV'), ('his', u'PRON'), ('imagination', u'NOUN'), ('so', u'ADV'), ('far', u'ADV')]

你覺得這個標註器的準確率怎麼樣呢?

似乎完成的還不錯,那麼我們試試另外一個句子:

text = nltk.word_tokenize('They refuse to permit us to obtain the refuse permit')
nltk.pos_tag(text, tagset='universal')

outputs:
[('They', u'PRON'), ('refuse', u'VERB'), ('to', u'PRT'), ('permit', u'VERB'), ('us', u'PRON'), ('to', u'PRT'), ('obtain', u'VERB'), ('the', u'DET'), ('refuse', u'NOUN'), ('permit', u'NOUN')]

對於文中的兩個refuse,前者被標為動詞,後者被標為名詞,完成的還不錯。

這有什麼意義呢?拿第二個句子來說,其實兩個refuse的讀音不一樣,第一個讀作refUSE,第二個讀作REFuse,所以語音系統為了正確的發音,需要先做詞性標註才行。

自動標註器

為了更好的理解標註器的原理,我們慢慢來自建構建一個詞性標註器,先載入資料:

from nltk.corpus import brown
brown_tagged_sents = brown.tagged_sents(categories='news')
brown_sents = brown.sents(categories='news')
brown_tagged_words = brown.tagged_words(categories='news')
brown_words = brown.words(categories='news')

預設標註器

這是最簡單的標註器了,它給所有的識別符號都分配同樣的詞性標記,我們先看來來哪個標記是最有可能的:

tags = [tag for (word, tag) in brown_tagged_words]
nltk.FreqDist(tags).max()

outputs:
u'NN'

說明名詞是最多的,那麼我們就生成一個標註器,它將所有詞都標註為名詞:

default_tagger = nltk.DefaultTagger('NN')
default_tagger.tag(nltk.word_tokenize('This beautiful future is just his imagination so far'))

outputs:
[('This', 'NN'), ('beautiful', 'NN'), ('future', 'NN'), ('is', 'NN'), ('just', 'NN'), ('his', 'NN'), ('imagination', 'NN'), ('so', 'NN'), ('far', 'NN')]

可以看到,已經全部標註為NN了,下面評估一下我們這個標註器:

default_tagger.evaluate(brown_tagged_sents)

outputs:
0.13089484257215028

哈哈,說明這個標註器太差了,它的標註正確率只有13.1%。

雖然如此,但碰巧的是在處理大量文字的時候,大部分新詞都是名詞,這意味著預設標註器可以幫助我們提高語言處理系統的穩定性。

正則表示式標註器

在英語單詞中,我們可以通過後綴nessinged等來推測一個單詞的詞性,那麼這樣做是否也有效呢?試試就知道啦~

patterns = [
  (r'.*ing$', 'VBG'),
  (r'.*ed$', 'VBD'),
  (r'.*es$', 'VBZ'),
  (r'.*ould$', 'MD'),
  (r'.*\'s$', 'NN$'),
  (r'.*s$', 'NNS'),
  (r'^-?[0-9]+(.[0-9]+)?$', 'CD'),
  (r'.*', 'NN')
]

按照順序匹配,當全部都不匹配時,最後會被標註為NN詞性。

regexp_tagger = nltk.RegexpTagger(patterns)
regexp_tagger.tag(brown_sents[3])

outputs:
[(u'``', 'NN'), (u'Only', 'NN'), (u'a', 'NN'), (u'relative', 'NN'), (u'handful', 'NN'), (u'of', 'NN'), (u'such', 'NN'), (u'reports', 'NNS'), (u'was', 'NNS'), (u'received', 'VBD'), (u"''", 'NN'), (u',', 'NN'), (u'the', 'NN'), (u'jury', 'NN'), (u'said', 'NN'), (u',', 'NN'), (u'``', 'NN'), (u'considering', 'VBG'), (u'the', 'NN'), (u'widespread', 'NN'), (u'interest', 'NN'), (u'in', 'NN'), (u'the', 'NN'), (u'election', 'NN'), (u',', 'NN'), (u'the', 'NN'), (u'number', 'NN'), (u'of', 'NN'), (u'voters', 'NNS'), (u'and', 'NN'), (u'the', 'NN'), (u'size', 'NN'), (u'of', 'NN'), (u'this', 'NNS'), (u'city', 'NN'), (u"''", 'NN'), (u'.', 'NN')]

評估一下:

regexp_tagger.evaluate(brown_tagged_sents)

outputs:
0.20326391789486245

比預設標註器要好點,哈哈

查詢標註器

可以發現,名詞雖然出現的頻率最高,但出現頻率最高的詞未必都是名詞,所以我們可以試試取頻率最大的前100個詞,用他們最有可能的詞性來進行標註。

fd = nltk.FreqDist(brown_words)
cfd = nltk.ConditionalFreqDist(brown_tagged_words)
most_freq_words = fd.most_common()[:100]
likely_tags = dict((word, cfd[word].max()) for (word, freq) in most_freq_words)
baseline_tagger = nltk.UnigramTagger(model=likely_tags)
baseline_tagger.evaluate(brown_tagged_sents)

outputs:
0.45578495136941344

可見,就算只取前100個詞,效率也已經比之前高很多了。

我們實地看看它的工作結果:

baseline_tagger.tag(brown_sents[3])

outputs:
[(u'``', u'``'), (u'Only', None), (u'a', u'AT'), (u'relative', None), (u'handful', None), (u'of', u'IN'), (u'such', None), (u'reports', None), (u'was', u'BEDZ'), (u'received', None), (u"''", u"''"), (u',', u','), (u'the', u'AT'), (u'jury', None), (u'said', u'VBD'), (u',', u','), (u'``', u'``'), (u'considering', None), (u'the', u'AT'), (u'widespread', None), (u'interest', None), (u'in', u'IN'), (u'the', u'AT'), (u'election', None), (u',', u','), (u'the', u'AT'), (u'number', None), (u'of', u'IN'), (u'voters', None), (u'and', u'CC'), (u'the', u'AT'), (u'size', None), (u'of', u'IN'), (u'this', u'DT'), (u'city', None), (u"''", u"''"), (u'.', u'.')]

可以看到有很多是None,說明它沒有出現在前100個詞中,這時候我們可以把它們交給預設標註器處理,也就是標記為NN,這個轉移工作叫做回退

baseline_tagger = nltk.UnigramTagger(model=likely_tags, backoff=nltk.DefaultTagger('NN'))
baseline_tagger.evaluate(brown_tagged_sents)

outputs:
0.5817769556656125

準確率瞬間提高了10%+ 有木有!!

如果取更多的詞呢?下面給出資料:

高頻詞數量 準確率
200 0.5060962269029576
800 0.6335401873620145
1600 0.7067247449131809
3200 0.7813513137219802

說明隨著數量增加,準確率還會提升~

N-gram標註

一元標註

它使用簡單的統計演算法,給每一個詞分配一個最可能的標記,不會關聯上下文。

unigram_tagger = nltk.UnigramTagger(brown_tagged_sents)
unigram_tagger.evaluate(brown_tagged_sents)

outputs:
0.9349006503968017

準確率還蠻高的,學過的同學知道,這其實是過擬合啦,不信?我們試試其他十個語料庫:

print "\n".join([cate + "\t" + str(unigram_tagger.evaluate(brown.tagged_sents(categories=cate))) for cate in brown.categories()[:10]])

outputs:
adventure       0.787891898128
belles_lettres  0.798707075842
editorial       0.813940653204
fiction         0.799147295877
government      0.807778427485
hobbies         0.771327949481
humor           0.793178151648
learned         0.772942690007
lore            0.798085204762
mystery         0.80790288443

準確率表現在80%左右,下降了10%多,影響還是蠻大的啦。

一般的N-gram的標註

它是根據上下文來推斷詞性的。比如一段句子是 wn2wn1wnwn+1,對應的詞性是tn2tn1tntn+1,三元標準器(n=3)就是考慮當前詞wn前兩個詞的標記tn2tn1 ,我們來推斷tn 的詞性。

下面是一個二元標註器(即只考慮前一個詞)

bigram_tagger = nltk.BigramTagger(brown_tagged_sents)
bigram_tagger.evaluate(brown_tagged_sents)

outputs:
0.7860751437038805

三元標註器:

trigram_tagger = nltk.TrigramTagger(brown_tagged_sents)
trigram_tagger.evaluate(brown_tagged_sents)

outputs:
0.8223641028700996

準確率要高點。我們來試試它對其他文字的準確率怎麼樣

print "\n".join([cate + "\t" + str(trigram_tagger.evaluate(brown.tagged_sents(categories=cate))) for cate in brown.categories()[:10]])

outputs:
adventure       0.0947189293646
belles_lettres  0.0632885797477
editorial       0.0675930134407
fiction         0.0889498890317
government      0.0531682758817
hobbies         0.0558503855729
humor           0.0754551740032
learned         0.0566172589726
lore            0.0623759054933
mystery         0.096993125645

好吧,我懷疑我用了假標註器!

我們組合一下所建的標註器:

t0 = nltk.DefaultTagger('NN')
t1 = nltk.UnigramTagger(brown_tagged_sents, backoff=t0)
t2 = nltk.BigramTagger(brown_tagged_sents, backoff=t1)
t2.evaluate(brown_tagged_sents)

outputs:
0.9730592517453309

嗯。。。準確率還不錯,下面試試:

print "\n".join([cate + "\t" + str(t2.evaluate(brown.tagged_sents(categories=cate))) for cate in brown.categories()[:10]])

outputs:
adventure       0.835626315941
belles_lettres  0.840522022462
editorial       0.849977274203
fiction         0.84151968228
government      0.844089165252
hobbies         0.825283866659
humor           0.839041253745
learned         0.836756685433
lore            0.844033037471
mystery         0.85128303801

em………還不錯吧,下降也有10%,但準確率還有85%左右