本章關注的問題:
1. 什麼是lexical categories(詞彙分類),在NLP中如何使用它們?
2. 什麼樣的Python數據結構適合存儲詞彙與它們的類別?
3. 如何自動標註文本中詞彙的詞類?
將詞彙按它們的 詞性(parts-of-speech,POS)分類以及相應的標註它們的過程被稱爲 詞性標註(part-of-speech tagging, POS tagging)或乾脆簡稱 標註。
使用詞性標註器
一個 詞性標註器(parts-of-speech tagger 或 POS tagger)處理一個詞序列,爲每個詞附加一個詞性標記(不要忘記 import nltk):
>>> import nltk
>>> text = nltk.word_tokenize("And now for something completely different")
>>> nltk.pos_tag(text)
[('And', 'CC'), ('now', 'RB'), ('for', 'IN'), ('something', 'NN'),('completely', 'RB'), ('different', 'JJ')]
在這裏我們看到 and 是 CC,並列連詞;now 和 completely 是 RB,副詞;for 是 IN,介詞;something 是 NN,名詞;different 是 JJ ,形容詞。
NLTK 中提供了每個標記的文檔,可以使用標記來查詢,如:nltk.help.u
penn_tagset(‘RB’),
標註語料庫
表示已標註的標識符
按照 NLTK 的約定,一個已標註的標識符使用一個由標識符和標記組成的元組來表示 。我們可以使用函數str2tuple()從表示一個已標註的標識符的標準字符串創建一個這樣的特殊元組:
>>> tagged_token = nltk.tag.str2tuple('fly/NN')
>>> tagged_token
('fly', 'NN')
>>> tagged_token[0]
'fly'
>>> tagged_token[1]
'NN'
讀取已標註的語料庫
NLTK 中包括的若干語料庫 已標註了詞性。
>>> nltk.corpus.brown.tagged_words()
[('The', 'AT'), ('Fulton', 'NP-TL'), ('County', 'NN-TL'), ...]
>>> nltk.corpus.brown.tagged_words(tagset='universal')
[('The', 'DET'), ('Fulton', 'N'), ('County', 'N'), ...]
中文語料庫:
>>> nltk.corpus.sinica_treebank.tagged_words()
[('一', 'Neu'), ('友情', 'Nad'), ('嘉珍', 'Nba'), ...]
簡化的詞性標記集
簡化的標記集
標記 | 含義 | 例子 |
---|---|---|
ADJ | 形容詞 | new, good, high, special, big, local |
ADV | 動詞 | really, already, still, early, now |
CNJ | 連詞 | and, or, but, if, while, although |
DET | 限定詞 | the, a, some, most, every, no |
EX | 存在量詞 | there, there’s |
FW | 外來詞 | dolce, ersatz, esprit, quo, maitre |
MOD | 情態動詞 | will, can, would, may, must, should |
N | 名詞 | year, home, costs, time, education |
NP | 專有名詞 | Alison, Africa, April, Washington |
NUM | 數詞 | twenty-four, fourth, 1991, 14:24 |
PRO | 代詞 | he, their, her, its, my, I, us |
P | 介詞 | on, of, at, with, by, into, under |
TO | 詞 to | to |
UH | 感嘆詞 | ah, bang, ha, whee, hmpf, oops |
V | 動詞 | is, has, get, do, make, see, run |
VD | 過去式 | said, took, told, made, asked |
VG | 現在分詞 | making, going, playing, working |
VN | 過去分詞 | given, taken, begun, sung |
WH | Wh 限定詞 | who, which, when, what, where, how |
. | 標點符號 | . , ; ! |
讓我們來看看這些標記中哪些是布朗語料庫的新聞類中最常見的:
>>> from nltk.corpus import brown
>>> brown_news_tagged = brown.tagged_words(categories='news', tagset='universal')
>>> tag_fd = nltk.FreqDist(tag for (word, tag) in brown_news_tagged)
>>> tag_fd.most_common()
[('NOUN', 30640), ('VERB', 14399), ('ADP', 12355), ('.', 11928), ('DET', 11389), ('ADJ', 6706), ('ADV', 3349), ('CONJ', 2717), ('PRON', 2535), ('PRT', 2264), ('NUM', 2166), ('X', 106)]
名詞
名詞一般指的是人、地點、事情或概念,例如:女人、蘇格蘭、圖書、情報。
動詞
動詞是用來描述事件和行動的詞,例如:fall 和 eat。
形容詞和副詞
形容詞修飾名詞,副詞修飾動詞。
Python字典
定義字典
我們可以使用鍵-值對格式創建字典。有兩種方式做這個,我們通常會使用第一個:
>>> pos = {'colorless': 'ADJ', 'ideas': 'N', 'sleep': 'V', 'furiously': 'ADV'}
>>> pos = dict(colorless='ADJ', ideas='N', sleep='V', furiously='ADV')
NOTE:字典的鍵必須是不可改變的類型,如字符串和元組。如果我們嘗試使用可變鍵定義字典會得到一個 TypeError:
>>> pos = {['ideas', 'blogs', 'adventures']: 'N'}
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: list objects are unhashable
默認字典
使用defaultdict,我們必須提供一個參數,用來創建默認值,如:int、float、str、list、dict、tuple。
>>> from collections import defaultdict
>>> frequency = defaultdict(int)
>>> frequency['colorless'] = 4
>>> frequency['ideas']
0
>>> pos = defaultdict(list)
>>> pos['sleep'] = ['NOUN', 'VERB']
>>> pos['ideas']
[]
顛倒字典
>>> pos = {'colorless': 'ADJ', 'ideas': 'N', 'sleep': 'V', 'furiously': 'ADV'}
>>> pos2 = dict((value, key) for (key, value) in pos.items())
>>> pos2['N']
'ideas'
Python字典方法:
示例 | 說明 |
---|---|
d = {} | 創建一個空的字典,並將分配給 d |
d[key] = value | 分配一個值給一個給定的字典鍵 |
d.keys() | 字典的鍵的鏈表 |
list(d) | 字典的鍵的鏈表 |
sorted(d) | 字典的鍵,排序 |
key in d | 測試一個特定的鍵是否在字典中 |
for key in d | 遍歷字典的鍵 |
d.values() | 字典中的值的鏈表 |
dict([(k1,v1), (k2,v2), …]) | 從一個鍵-值對鏈表創建一個字典 |
d1.update(d2) | 添加 d2 中所有項目到 d1 |
defaultdict(int) | 一個默認值爲 0 的字典 |
自動標註
我們已加載將要使用的數據開始:
>>> from nltk.corpus import brown
>>> brown_tagged_sents = brown.tagged_sents(categories='news')
>>> brown_sents = brown.sents(categories='news')
默認標註器
最簡單的標註器是爲每個標識符分配同樣的標記。但是不可能每個標識符都有相同的標記。爲了得到最好的效果,我們用最有可能的標記標註每個詞。
>>> tags = [tag for (word, tag) in brown.tagged_words(categories='news')]
>>> nltk.FreqDist(tags).max()
'NN'
現在我們可以創建一個將所有詞都標註成 NN 的標註器。
>>> raw = 'I do not like green eggs and ham, I do not like them Sam I am!'
>>> tokens = nltk.word_tokenize(raw)
>>> default_tagger = nltk.DefaultTagger('NN')
>>> default_tagger.tag(tokens)
[('I', 'NN'), ('do', 'NN'), ('not', 'NN'), ('like', 'NN'), ('green', 'NN'),('eggs', 'NN'), ('and', 'NN'), ('ham', 'NN'), (',', 'NN'), ('I', 'NN'),('do', 'NN'), ('not', 'NN'), ('like', 'NN'), ('them', 'NN'), ('Sam', 'NN'),('I', 'NN'), ('am', 'NN'), ('!', 'NN')]
不出所料,這種方法的表現相當不好。在一個典型的語料庫中,它只標註正確了八分之一的標識符,正如我們在這裏看到的:
>>> default_tagger.evaluate(brown_tagged_sents)
0.13089484257215028
正則表達式標註器
正則表達式標註器基於匹配模式分配標記給標識符。例如:我們可能會猜測任一以 ed結尾的詞都是動詞過去分詞,任一以’s 結尾的詞都是名詞所有格。可以用一個正則表達式的列表表示這些:
>>> patterns = [
... (r'.*ing$', 'VBG'), # gerunds
... (r'.*ed$', 'VBD'), # simple past
... (r'.*es$', 'VBZ'), # 3rd singular present
... (r'.*ould$', 'MD'), # modals
... (r'.*\'s$', 'NN$'), # possessive nouns
... (r'.*s$', 'NNS'), # plural nouns
... (r'^-?[0-9]+(.[0-9]+)?$', 'CD'), # cardinal numbers
... (r'.*', 'NN') # nouns (default)
... ]
請注意,這些是順序處理的,第一個匹配上的會被使用。現在我們可以建立一個標註器 ,
並用它來標記一個句子。做完這一步會有約五分之一是正確的。
>>> regexp_tagger = nltk.RegexpTagger(patterns)
>>> regexp_tagger.tag(brown_sents[3])
[('``', 'NN'), ('Only', 'NN'), ('a', 'NN'), ('relative', 'NN'), ('handful', 'NN'),('of', 'NN'), ('such', 'NN'), ('reports', 'NNS'), ('was', 'NNS'), ('received', 'VBD'),
("''", 'NN'), (',', 'NN'), ('the', 'NN'), ('jury', 'NN'), ('said', 'NN'), (',', 'NN'),('``', 'NN'), ('considering', 'VBG'), ('the', 'NN'), ('widespread', 'NN'), ...]
>>> regexp_tagger.evaluate(brown_tagged_sents)
0.20326391789486245
最終的正則表達式 .* 是一個全面捕捉的,標註所有詞爲名詞。
查詢標註器
很多高頻詞沒有 NN 標記。讓我們找出 100 個最頻繁的詞,存儲它們最有可能的標記 。然後我們可以使用這個信息作爲“查找標註器”(NLTK UnigramTagger)的模型:
>>> fd = nltk.FreqDist(brown.words(categories='news'))
>>> cfd = nltk.ConditionalFreqDist(brown.tagged_words(categories='news'))
>>> most_freq_words = fd.keys()[:100]
>>> likely_tags = dict((word, cfd[word].max()) for word in most_freq_words)
>>> baseline_tagger = nltk.UnigramTagger(model=likely_tags)
>>> baseline_tagger.evaluate(brown_tagged_sents)
0.45578495136941344
僅僅知道 100 個最頻繁的詞的標記就使我們能正確標註很大一部分標識符(近一半,事實上)。讓我們來看看它在一些未標註的輸入文本上做的如何:
>>> sent = brown.sents(categories='news')[3]
>>> baseline_tagger.tag(sent)
[('``', '``'), ('Only', None), ('a', 'AT'), ('relative', None),('handful', None), ('of', 'IN'), ('such', None), ('reports', None),('was', 'BEDZ'), ('received', None), ("''", "''"), (',', ','),('the', 'AT'), ('jury', None), ('said', 'VBD'), (',', ','),('``', '``'), ('considering', None), ('the', 'AT'), ('widespread', None),('interest', None), ('in', 'IN'), ('the', 'AT'), ('election', None),
(',', ','), ('the', 'AT'), ('number', None), ('of', 'IN'),
('voters', None), ('and', 'CC'), ('the', 'AT'), ('size', None),('of', 'IN'), ('this', 'DT'), ('city', None), ("''", "''"), ('.', '.')]
許多詞都被分配了一個 None 標籤,因爲它們不在 100 個最頻繁的詞之中。在這些情況下,我們想分配默認標記 NN。我們可以通過指定一個標註器作爲另一個標註器的參數做到這個,如下所示。
>>> baseline_tagger = nltk.UnigramTagger(model=likely_tags,
... backoff=nltk.DefaultTagger('NN'))
N-gram 標註
一元標註(Unigram Tagging)
一元標註器基於一個簡單的統計算法:對每個標識符分配這個標識符最有可能的標記。我們訓練一個一元標註器,用它來標註一個句子,然後評估:
>>> from nltk.corpus import brown
>>> brown_tagged_sents = brown.tagged_sents(categories='news')
>>> brown_sents = brown.sents(categories='news')
>>> unigram_tagger = nltk.UnigramTagger(brown_tagged_sents)
>>> unigram_tagger.tag(brown_sents[2007])
[('Various', 'JJ'), ('of', 'IN'), ('the', 'AT'), ('apartments', 'NNS'),('are', 'BER'), ('of', 'IN'), ('the', 'AT'), ('terrace', 'NN'), ('type', 'NN'),(',', ','), ('being', 'BEG'), ('on', 'IN'), ('the', 'AT'), ('ground', 'NN'),('floor', 'NN'), ('so', 'QL'), ('that', 'CS'), ('entrance', 'NN'), ('is', 'BEZ'),('direct', 'JJ'), ('.', '.')]
>>> unigram_tagger.evaluate(brown_tagged_sents)
0.9349006503968017
分離訓練數據和測試數據
一般將數據集分開,訓練90%測試10%。
>>> size = int(len(brown_tagged_sents) * 0.9)
>>> size
4160
>>> train_sents = brown_tagged_sents[:size]
>>> test_sents = brown_tagged_sents[size:]
>>> unigram_tagger = nltk.UnigramTagger(train_sents)
>>> unigram_tagger.evaluate(test_sents)
0.811721...
一般的N-gram標註
一個 n-gram 標註器是一個 unigram 標註器的一般化,它的上下文是當前詞和它前面 n-1 個標識符的詞性標記。
1-gram標註器是一元標註器(unigram tagger),2-gram 標註器也稱爲二元標註器(bigram taggers),3-gram 標註器也稱爲三元標註器(trigram taggers)。
>>> bigram_tagger = nltk.BigramTagger(train_sents)
>>> bigram_tagger.tag(brown_sents[2007])
[('Various', 'JJ'), ('of', 'IN'), ('the', 'AT'), ('apartments', 'NNS'),('are', 'BER'), ('of', 'IN'), ('the', 'AT'), ('terrace', 'NN'),('type', 'NN'), (',', ','), ('being', 'BEG'), ('on', 'IN'), ('the', 'AT'),('ground', 'NN'), ('floor', 'NN'), ('so', 'CS'), ('that', 'CS'),
('entrance', 'NN'), ('is', 'BEZ'), ('direct', 'JJ'), ('.', '.')]
>>> unseen_sent = brown_sents[4203]
>>> bigram_tagger.tag(unseen_sent)
[('The', 'AT'), ('population', 'NN'), ('of', 'IN'), ('the', 'AT'), ('Congo', 'NP'),('is', 'BEZ'), ('13.5', None), ('million', None), (',', None), ('divided', None),('into', None), ('at', None), ('least', None), ('seven', None), ('major', None),('``', None), ('culture', None), ('clusters', None), ("''", None), ('and', None),('innumerable', None), ('tribes', None), ('speaking', None), ('400', None),('separate', None), ('dialects', None), ('.', None)]
bigram 標註器能夠標註訓練中它看到過的句子中的所有詞,但對一個沒見過的句子表現很差。只要遇到一個新詞,就無法給它分配標記。它的整體準確度得分非常低:
>>> bigram_tagger.evaluate(test_sents)
0.10276088906608193
組合標註器
解決精度和覆蓋範圍之間的權衡的一個辦法是儘可能的使用更精確的算法,但卻在很多時候落後於具有更廣覆蓋範圍的算法。例如:我們可以按如下方式組合 bigram 標註器、unigram 標註器和一個默認標註器:
1. 嘗試使用 bigram 標註器標註標識符。
2. 如果 bigram 標註器無法找到一個標記,嘗試 unigram 標註器。
3. 如果 unigram 標註器也無法找到一個標記,使用默認標註器。
大多數 NLTK 標註器允許指定一個回退標註器。回退標註器自身可能也有一個回退標註器:
>>> t0 = nltk.DefaultTagger('NN')
>>> t1 = nltk.UnigramTagger(train_sents, backoff=t0)
>>> t2 = nltk.BigramTagger(train_sents, backoff=t1)
>>> t2.evaluate(test_sents)
0.844513...
如何確定一個詞的分類
在一般情況下,語言學家使用形態學、句法和語義線索確定一個詞的類
別。
形態學線索
一個詞的內部結構可能爲這個詞分類提供有用的線索。舉例來說:-ness是一個後綴,與形容詞結合產生一個名詞,如 happy→happiness, ill →illness。
句法線索
另一個信息來源是一個詞可能出現的典型的上下文語境。
語義線索
最後,一個詞的意思對其詞彙範疇是一個有用的線索。