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%。
雖然如此,但碰巧的是在處理大量文字的時候,大部分新詞都是名詞,這意味著預設標註器可以幫助我們提高語言處理系統的穩定性。
正則表示式標註器
在英語單詞中,我們可以通過後綴ness
、ing
、ed
等來推測一個單詞的詞性,那麼這樣做是否也有效呢?試試就知道啦~
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的標註
它是根據上下文來推斷詞性的。比如一段句子是 前兩個詞
的標記
下面是一個二元標註器(即只考慮前一個詞)
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%左右