# 5\. 分類和標注詞匯
早在小學你就學過名詞、動詞、形容詞和副詞之間的差異。這些“詞類”不是閑置的文法家的發明,而是對許多語言處理任務都有用的分類。正如我們將看到的,這些分類源于對文本中詞的分布的簡單的分析。本章的目的是要回答下列問題:
1. 什么是詞匯分類,在自然語言處理中它們是如何使用?
2. 一個好的存儲詞匯和它們的分類的 Python 數據結構是什么?
3. 我們如何自動標注文本中詞匯的詞類?
一路上,我們將介紹 NLP 的一些基本技術,包括序列標注、N-gram 模型、回退和評估。這些技術在許多方面都很有用,標注為我們提供了一個表示它們的簡單的上下文。我們還將看到,在典型的 NLP 處理流程中,標注為何是位于分詞之后的第二個步驟。
將單詞按它們的詞性分類并進行相應地標注的過程,稱為詞語性質標注、詞性標注或簡稱標注。詞性也稱為詞類或詞匯類別。用于特定任務的標記的集合被稱為一個標記集。我們在本章的重點是運用標記和自動標注文本。
## 1 使用詞性標注器
一個詞語性質標注器或者詞性標注器處理一個單詞序列,為每個詞附加一個詞性標記(不要忘記`import nltk`):
```py
>>> text = 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.upenn_tagset('RB')`,或者一個正則表達,如`nltk.help.upenn_tagset('NN.*')`。一些語料庫有標記集文檔的 README 文件,見`nltk.corpus.???.readme()`,用語料庫的名稱替換。
讓我們來看看另一個例子,這次包括一些同形同音異義詞:
```py
>>> text = word_tokenize("They refuse to permit us to obtain the refuse permit")
>>> nltk.pos_tag(text)
[('They', 'PRP'), ('refuse', 'VBP'), ('to', 'TO'), ('permit', 'VB'), ('us', 'PRP'),
('to', 'TO'), ('obtain', 'VB'), ('the', 'DT'), ('refuse', 'NN'), ('permit', 'NN')]
```
請注意 refuse 和 permit 都以現在時動詞(`VBP`)和名詞(`NN`)形式出現。例如 refUSE 是一個動詞,意為“拒絕”,而 REFuse 是一個名詞,意為“垃圾”(即它們不是同音詞)。因此,我們需要知道正在使用哪一個詞以便能正確讀出文本。(出于這個原因,文本轉語音系統通常進行詞性標注。)
注意
**輪到你來:** 很多單詞,如 ski 和 race,可以用作名詞或動詞而發音沒有區別。你能想出其他的嗎?提示:想想一個常見的東西,嘗試把詞 to 放到它前面,看它是否也是一個動詞;或者想想一個動作,嘗試把 the 放在它前面,看它是否也是一個名詞。現在用這個詞的兩種用途造句,在這句話上運行詞性標注器。
詞匯類別如“名詞”和詞性標記如`NN`,看上去似乎有其用途,但在細節上將使許多讀者感到晦澀。你可能想知道要引進這種額外的信息的理由是什么。很多這些類別源于對文本中單詞分布的粗略分析。考慮下面的分析,涉及 woman(名詞),bought(動詞),over(介詞)和 the(限定詞)。`text.similar()`方法接收一個單詞 w,找出所有上下文 w<sub>1</sub>w w<sub>2</sub>,然后找出所有出現在相同上下文中的詞 w',即 w<sub>1</sub>w'w<sub>2</sub>。
```py
>>> text = nltk.Text(word.lower() for word in nltk.corpus.brown.words())
>>> text.similar('woman')
Building word-context index...
man day time year car moment world family house boy child country job
state girl place war way case question
>>> text.similar('bought')
made done put said found had seen given left heard been brought got
set was called felt in that told
>>> text.similar('over')
in on to of and for with from at by that into as up out down through
about all is
>>> text.similar('the')
a his this their its her an that our any all one these my in your no
some other and
```
可以觀察到,搜索 woman 找到名詞;搜索 bought 找到的大部分是動詞;搜索 over 一般會找到介詞;搜索 the 找到幾個限定詞。一個標注器能夠正確識別一個句子的上下文中的這些詞的標記,例如 The woman bought over $150,000 worth of clothes。
一個標注器還可以為我們對未知詞的認識建模,例如我們可以根據詞根 scrobble 猜測 scrobbling 可能是一個動詞,并有可能發生在 he was scrobbling 這樣的上下文中。
## 2 已經標注的語料庫
## 2.1 表示已經標注的詞符
按照 NLTK 的約定,一個已標注的詞符使用一個由詞符和標記組成的元組來表示。我們可以使用函數`str2tuple()`從表示一個已標注的詞符的標準字符串創建一個這樣的特殊元組:
```py
>>> tagged_token = nltk.tag.str2tuple('fly/NN')
>>> tagged_token
('fly', 'NN')
>>> tagged_token[0]
'fly'
>>> tagged_token[1]
'NN'
```
我們可以直接從一個字符串構造一個已標注的詞符的列表。第一步是對字符串分詞以便能訪問單獨的`單詞/標記`字符串,然后將每一個轉換成一個元組(使用`str2tuple()`)。
```py
>>> sent = '''
... The/AT grand/JJ jury/NN commented/VBD on/IN a/AT number/NN of/IN
... other/AP topics/NNS ,/, AMONG/IN them/PPO the/AT Atlanta/NP and/CC
... Fulton/NP-tl County/NN-tl purchasing/VBG departments/NNS which/WDT it/PPS
... said/VBD ``/`` ARE/BER well/QL operated/VBN and/CC follow/VB generally/RB
... accepted/VBN practices/NNS which/WDT inure/VB to/IN the/AT best/JJT
... interest/NN of/IN both/ABX governments/NNS ''/'' ./.
... '''
>>> [nltk.tag.str2tuple(t) for t in sent.split()]
[('The', 'AT'), ('grand', 'JJ'), ('jury', 'NN'), ('commented', 'VBD'),
('on', 'IN'), ('a', 'AT'), ('number', 'NN'), ... ('.', '.')]
```
## 2.2 讀取已標注的語料庫
NLTK 中包括的若干語料庫已標注了詞性。下面是一個你用文本編輯器打開一個布朗語料庫的文件就能看到的例子:
> The/at Fulton/np-tl County/nn-tl Grand/jj-tl Jury/nn-tl said/vbd Friday/nr an/at investigation/nn of/in Atlanta's/np$ recent/jj primary/nn election/nn produced/vbd `/` no/at evidence/nn ''/'' that/cs any/dti irregularities/nns took/vbd place/nn ./.
其他語料庫使用各種格式存儲詞性標記。NLTK 的語料庫閱讀器提供了一個統一的接口,使你不必理會這些不同的文件格式。與剛才提取并顯示的上面的文件不同,布朗語料庫的語料庫閱讀器按如下所示的方式表示數據。注意,詞性標記已轉換為大寫的,自從布朗語料庫發布以來這已成為標準的做法。
```py
>>> nltk.corpus.brown.tagged_words()
[('The', 'AT'), ('Fulton', 'NP-TL'), ...]
>>> nltk.corpus.brown.tagged_words(tagset='universal')
[('The', 'DET'), ('Fulton', 'NOUN'), ...]
```
只要語料庫包含已標注的文本,NLTK 的語料庫接口都將有一個`tagged_words()`方法。下面是一些例子,再次使用布朗語料庫所示的輸出格式:
```py
>>> print(nltk.corpus.nps_chat.tagged_words())
[('now', 'RB'), ('im', 'PRP'), ('left', 'VBD'), ...]
>>> nltk.corpus.conll2000.tagged_words()
[('Confidence', 'NN'), ('in', 'IN'), ('the', 'DT'), ...]
>>> nltk.corpus.treebank.tagged_words()
[('Pierre', 'NNP'), ('Vinken', 'NNP'), (',', ','), ...]
```
并非所有的語料庫都采用同一組標記;看前面提到的標記集的幫助函數和`readme()`方法中的文檔。最初,我們想避免這些標記集的復雜化,所以我們使用一個內置的到“通用標記集“的映射:
```py
>>> nltk.corpus.brown.tagged_words(tagset='universal')
[('The', 'DET'), ('Fulton', 'NOUN'), ...]
>>> nltk.corpus.treebank.tagged_words(tagset='universal')
[('Pierre', 'NOUN'), ('Vinken', 'NOUN'), (',', '.'), ...]
```
NLTK 中還有其他幾種語言的已標注語料庫,包括中文,印地語,葡萄牙語,西班牙語,荷蘭語和加泰羅尼亞語。這些通常含有非 ASCII 文本,當輸出較大的結構如列表時,Python 總是以十六進制顯示這些。
```py
>>> nltk.corpus.sinica_treebank.tagged_words()
[('?', 'Neu'), ('??', 'Nad'), ('??', 'Nba'), ...]
>>> nltk.corpus.indian.tagged_words()
[('??????', 'NN'), ('??????', 'NN'), (':', 'SYM'), ...]
>>> nltk.corpus.mac_morpho.tagged_words()
[('Jersei', 'N'), ('atinge', 'V'), ('m\xe9dia', 'N'), ...]
>>> nltk.corpus.conll2002.tagged_words()
[('Sao', 'NC'), ('Paulo', 'VMI'), ('(', 'Fpa'), ...]
>>> nltk.corpus.cess_cat.tagged_words()
[('El', 'da0ms0'), ('Tribunal_Suprem', 'np0000o'), ...]
```
如果你的環境設置正確,有適合的編輯器和字體,你應該能夠以人可讀的方式顯示單個字符串。例如,[2.1](./ch05.html#fig-tag-indian)顯示的使用`nltk.corpus.indian`訪問的數據。

圖 2.1:四種印度語言的詞性標注數據:孟加拉語、印地語、馬拉地語和泰盧固語
如果語料庫也被分割成句子,將有一個`tagged_sents()`方法將已標注的詞劃分成句子,而不是將它們表示成一個大列表。在我們開始開發自動標注器時,這將是有益的,因為它們在句子列表上被訓練和測試,而不是詞。
## 2.3 通用詞性標記集
已標注的語料庫使用許多不同的標記集約定來標注詞匯。為了幫助我們開始,我們將看一看一個簡化的標記集([2.1](./ch05.html#tab-universal-tagset)中所示)。
表 2.1:
通用詞性標記集
```py
>>> 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)]
```
注意
**輪到你來:**使用`tag_fd.plot(cumulative=True)`為上面顯示的頻率分布繪圖。標注為上述列表中的前五個標記的詞的百分比是多少?
我們可以使用這些標記做強大的搜索,結合一個圖形化的詞性索引工具`nltk.app.concordance()`。用它來尋找任一單詞和詞性標記的組合,如`N N N N`, `hit/VD`, `hit/VN`或者`the ADJ man`。
## 2.4 名詞
名詞一般指的是人、地點、事情或概念,例如: woman, Scotland, book, intelligence。名詞可能出現在限定詞和形容詞之后,可以是動詞的主語或賓語,如[2.2](./ch05.html#tab-syntax-nouns)所示。
表 2.2:
一些名詞的句法模式
```py
>>> word_tag_pairs = nltk.bigrams(brown_news_tagged)
>>> noun_preceders = [a[1] for (a, b) in word_tag_pairs if b[1] == 'NOUN']
>>> fdist = nltk.FreqDist(noun_preceders)
>>> [tag for (tag, _) in fdist.most_common()]
['NOUN', 'DET', 'ADJ', 'ADP', '.', 'VERB', 'CONJ', 'NUM', 'ADV', 'PRT', 'PRON', 'X']
```
這證實了我們的斷言,名詞出現在限定詞和形容詞之后,包括數字形容詞(數詞,標注為`NUM`)。
## 2.5 動詞
動詞是用來描述事件和行動的詞,例如[2.3](./ch05.html#tab-syntax-verbs)中的 fall, eat。在一個句子中,動詞通常表示涉及一個或多個名詞短語所指示物的關系。
表 2.3:
一些動詞的句法模式
```py
>>> wsj = nltk.corpus.treebank.tagged_words(tagset='universal')
>>> word_tag_fd = nltk.FreqDist(wsj)
>>> [wt[0] for (wt, _) in word_tag_fd.most_common() if wt[1] == 'VERB']
['is', 'said', 'are', 'was', 'be', 'has', 'have', 'will', 'says', 'would',
'were', 'had', 'been', 'could', "'s", 'can', 'do', 'say', 'make', 'may',
'did', 'rose', 'made', 'does', 'expected', 'buy', 'take', 'get', 'might',
'sell', 'added', 'sold', 'help', 'including', 'should', 'reported', ...]
```
請注意,頻率分布中計算的項目是詞-標記對。由于詞匯和標記是成對的,我們可以把詞作作為條件,標記作為事件,使用條件-事件對的鏈表初始化一個條件頻率分布。這讓我們看到了一個給定的詞的標記的頻率順序列表:
```py
>>> cfd1 = nltk.ConditionalFreqDist(wsj)
>>> cfd1['yield'].most_common()
[('VERB', 28), ('NOUN', 20)]
>>> cfd1['cut'].most_common()
[('VERB', 25), ('NOUN', 3)]
```
我們可以顛倒配對的順序,這樣標記作為條件,詞匯作為事件。現在我們可以看到對于一個給定的標記可能的詞。我們將用《華爾街日報 》的標記集而不是通用的標記集來這樣做:
```py
>>> wsj = nltk.corpus.treebank.tagged_words()
>>> cfd2 = nltk.ConditionalFreqDist((tag, word) for (word, tag) in wsj)
>>> list(cfd2['VBN'])
['been', 'expected', 'made', 'compared', 'based', 'priced', 'used', 'sold',
'named', 'designed', 'held', 'fined', 'taken', 'paid', 'traded', 'said', ...]
```
要弄清`VBD`(過去式)和`VBN`(過去分詞)之間的區別,讓我們找到可以同是`VBD`和`VBN`的詞匯,看看一些它們周圍的文字:
```py
>>> [w for w in cfd1.conditions() if 'VBD' in cfd1[w] and 'VBN' in cfd1[w]]
['Asked', 'accelerated', 'accepted', 'accused', 'acquired', 'added', 'adopted', ...]
>>> idx1 = wsj.index(('kicked', 'VBD'))
>>> wsj[idx1-4:idx1+1]
[('While', 'IN'), ('program', 'NN'), ('trades', 'NNS'), ('swiftly', 'RB'),
('kicked', 'VBD')]
>>> idx2 = wsj.index(('kicked', 'VBN'))
>>> wsj[idx2-4:idx2+1]
[('head', 'NN'), ('of', 'IN'), ('state', 'NN'), ('has', 'VBZ'), ('kicked', 'VBN')]
```
在這種情況下,我們可以看到過去分詞 kicked 前面是助動詞 have 的形式。這是普遍真實的嗎?
注意
**輪到你來:** 通過`list(cfd2['VN'])`指定一個過去分詞的列表,嘗試收集所有直接在列表中項目前面的詞-標記對。
## 2.6 形容詞和副詞
另外兩個重要的詞類是形容詞和副詞。形容詞修飾名詞,可以作為修飾語(如 the large pizza 中的 large),或者謂語(如 the pizza is large)。英語形容詞可以有內部結構(如 the falling stocks 中的 fall+ing)。副詞修飾動詞,指定動詞描述的事件的時間、方式、地點或方向(如 the stocks fell quickly 中的 quickly)。副詞也可以修飾的形容詞(如 Mary's teacher was really nice 中的 really)。
英語中還有幾個封閉的詞類,如介詞,冠詞(也常稱為限定詞)(如 the、a),情態動詞(如 should、may)和人稱代詞(如 she、they)。每個詞典和語法對這些詞的分類都不同。
注意
**輪到你來:**如果你對這些詞性中的一些不確定,使用`nltk.app.concordance()`學習它們,或看 _Schoolhouse Rock!_ 語法視頻于 YouTube,或者查詢本章結束的進一步閱讀一節。
## 2.7 未簡化的標記
讓我們找出每個名詞類型中最頻繁的名詞。[2.2](./ch05.html#code-findtags)中的程序找出所有以`NN`開始的標記,并為每個標記提供了幾個示例單詞。你會看到有許多`NN`的變種;最重要有`此外,大多數的標記都有后綴修飾符:`-NC`表示引用,`-HL`表示標題中的詞,`-TL`表示標題(布朗標記的特征)。
```py
def findtags(tag_prefix, tagged_text):
cfd = nltk.ConditionalFreqDist((tag, word) for (word, tag) in tagged_text
if tag.startswith(tag_prefix))
return dict((tag, cfd[tag].most_common(5)) for tag in cfd.conditions())
>>> tagdict = findtags('NN', nltk.corpus.brown.tagged_words(categories='news'))
>>> for tag in sorted(tagdict):
... print(tag, tagdict[tag])
...
NN [('year', 137), ('time', 97), ('state', 88), ('week', 85), ('man', 72)]
NN$ [("year's", 13), ("world's", 8), ("state's", 7), ("nation's", 6), ("company's", 6)]
NN$-HL [("Golf's", 1), ("Navy's", 1)]
NN$-TL [("President's", 11), ("Army's", 3), ("Gallery's", 3), ("University's", 3), ("League's", 3)]
NN-HL [('sp.', 2), ('problem', 2), ('Question', 2), ('business', 2), ('Salary', 2)]
NN-NC [('eva', 1), ('aya', 1), ('ova', 1)]
NN-TL [('President', 88), ('House', 68), ('State', 59), ('University', 42), ('City', 41)]
NN-TL-HL [('Fort', 2), ('Dr.', 1), ('Oak', 1), ('Street', 1), ('Basin', 1)]
NNS [('years', 101), ('members', 69), ('people', 52), ('sales', 51), ('men', 46)]
NNS$ [("children's", 7), ("women's", 5), ("janitors'", 3), ("men's", 3), ("taxpayers'", 2)]
NNS$-HL [("Dealers'", 1), ("Idols'", 1)]
NNS$-TL [("Women's", 4), ("States'", 3), ("Giants'", 2), ("Bros.'", 1), ("Writers'", 1)]
NNS-HL [('comments', 1), ('Offenses', 1), ('Sacrifices', 1), ('funds', 1), ('Results', 1)]
NNS-TL [('States', 38), ('Nations', 11), ('Masters', 10), ('Rules', 9), ('Communists', 9)]
NNS-TL-HL [('Nations', 1)]
```
當我們開始在本章后續部分創建詞性標注器時,我們將使用未簡化的標記。
## 2.8 探索已標注的語料庫
讓我們簡要地回過來探索語料庫,我們在前面的章節中看到過,這次我們探索詞性標記。
假設我們正在研究詞 often,想看看它是如何在文本中使用的。我們可以試著看看跟在 often 后面的詞匯
```py
>>> brown_learned_text = brown.words(categories='learned')
>>> sorted(set(b for (a, b) in nltk.bigrams(brown_learned_text) if a == 'often'))
[',', '.', 'accomplished', 'analytically', 'appear', 'apt', 'associated', 'assuming',
'became', 'become', 'been', 'began', 'call', 'called', 'carefully', 'chose', ...]
```
然而,使用`tagged_words()`方法查看跟隨詞的詞性標記可能更有指導性:
```py
>>> brown_lrnd_tagged = brown.tagged_words(categories='learned', tagset='universal')
>>> tags = [b[1] for (a, b) in nltk.bigrams(brown_lrnd_tagged) if a[0] == 'often']
>>> fd = nltk.FreqDist(tags)
>>> fd.tabulate()
PRT ADV ADP . VERB ADJ
2 8 7 4 37 6
```
請注意 often 后面最高頻率的詞性是動詞。名詞從來沒有在這個位置出現(在這個特別的語料中)。
接下來,讓我們看一些較大范圍的上下文,找出涉及特定標記和詞序列的詞(在這種情況下,`"<Verb> to <Verb>"`)。在 code-three-word-phrase 中,我們考慮句子中的每個三詞窗口[![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)](./ch05.html#three-word),檢查它們是否符合我們的標準[![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)](./ch05.html#verb-to-verb)。如果標記匹配,我們輸出對應的詞[![[3]](https://img.kancloud.cn/69/fc/69fcb1188781ff9f726d82da7988a139_15x15.jpg)](./ch05.html#print-words)。
```py
from nltk.corpus import brown
def process(sentence):
for (w1,t1), (w2,t2), (w3,t3) in nltk.trigrams(sentence): ![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)
if (t1.startswith('V') and t2 == 'TO' and t3.startswith('V')): ![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)
print(w1, w2, w3) ![[3]](https://img.kancloud.cn/69/fc/69fcb1188781ff9f726d82da7988a139_15x15.jpg)
>>> for tagged_sent in brown.tagged_sents():
... process(tagged_sent)
...
combined to achieve
continue to place
serve to protect
wanted to wait
allowed to place
expected to become
...
```
最后,讓我們看看與它們的標記關系高度模糊不清的詞。了解為什么要標注這樣的詞是因為它們各自的上下文可以幫助我們弄清楚標記之間的區別。
```py
>>> brown_news_tagged = brown.tagged_words(categories='news', tagset='universal')
>>> data = nltk.ConditionalFreqDist((word.lower(), tag)
... for (word, tag) in brown_news_tagged)
>>> for word in sorted(data.conditions()):
... if len(data[word]) > 3:
... tags = [tag for (tag, _) in data[word].most_common()]
... print(word, ' '.join(tags))
...
best ADJ ADV NP V
better ADJ ADV V DET
close ADV ADJ V N
cut V N VN VD
even ADV DET ADJ V
grant NP N V -
hit V VD VN N
lay ADJ V NP VD
left VD ADJ N VN
like CNJ V ADJ P -
near P ADV ADJ DET
open ADJ V N ADV
past N ADJ DET P
present ADJ ADV V N
read V VN VD NP
right ADJ N DET ADV
second NUM ADV DET N
set VN V VD N -
that CNJ V WH DET
```
注意
**輪到你來:**打開詞性索引工具`nltk.app.concordance()`并加載完整的布朗語料庫(簡化標記集)。現在挑選一些上面代碼例子末尾處列出的詞,看看詞的標記如何與詞的上下文相關。例如搜索`near`會看到所有混合在一起的形式,搜索`near/ADJ`會看到它作為形容詞使用,`near N`會看到只是名詞跟在后面的情況,等等。更多的例子,請修改附帶的代碼,以便它列出的詞具有三個不同的標簽。
## 3 使用 Python 字典映射單詞到其屬性
正如我們已經看到,`(word, tag)`形式的一個已標注詞是詞和詞性標記的關聯。一旦我們開始做詞性標注,我們將會創建分配一個標記給一個詞的程序,標記是在給定上下文中最可能的標記。我們可以認為這個過程是從詞到標記的映射。在 Python 中最自然的方式存儲映射是使用所謂的字典數據類型(在其他的編程語言又稱為關聯數組或哈希數組)。在本節中,我們來看看字典,看它如何能表示包括詞性在內的各種不同的語言信息。
## 3.1 索引列表 VS 字典
我們已經看到,文本在 Python 中被視為一個詞列表。鏈表的一個重要的屬性是我們可以通過給出其索引來“看”特定項目,例如`text1[100]`。請注意我們如何指定一個數字,然后取回一個詞。我們可以把鏈表看作一種簡單的表格,如[3.1](./ch05.html#fig-maps01)所示。

圖 3.1:列表查找:一個整數索引幫助我們訪問 Python 列表的內容。
對比這種情況與頻率分布([3](./ch01.html#sec-computing-with-language-simple-statistics)),在那里我們指定一個詞然后取回一個數字,如`fdist['monstrous']`,它告訴我們一個給定的詞在文本中出現的次數。用詞查詢對任何使用過字典的人都很熟悉。[3.2](./ch05.html#fig-maps02)展示一些更多的例子。

圖 3.2:字典查詢:我們使用一個關鍵字,如某人的名字、一個域名或一個英文單詞,訪問一個字典的條目;字典的其他名字有映射、哈希表、哈希和關聯數組。
在電話簿中,我們用名字查找一個條目得到一個數字。當我們在瀏覽器中輸入一個域名,計算機查找它得到一個 IP 地址。一個詞頻表允許我們查一個詞找出它在一個文本集合中的頻率。在所有這些情況中,我們都是從名稱映射到數字,而不是其他如列表那樣的方式。總之,我們希望能夠在任意類型的信息之間映射。[3.1](./ch05.html#tab-linguistic-objects)列出了各種語言學對象以及它們的映射。
表 3.1:
語言學對象從鍵到值的映射
```py
>>> pos = {}
>>> pos
{}
>>> pos['colorless'] = 'ADJ' ![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)
>>> pos
{'colorless': 'ADJ'}
>>> pos['ideas'] = 'N'
>>> pos['sleep'] = 'V'
>>> pos['furiously'] = 'ADV'
>>> pos ![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)
{'furiously': 'ADV', 'ideas': 'N', 'colorless': 'ADJ', 'sleep': 'V'}
```
所以,例如,[![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)](./ch05.html#pos-colorless)說的是 colorless 的詞性是形容詞,或者更具體地說:在字典`pos`中,鍵`'colorless'`被分配了值`'ADJ'`。當我們檢查`pos`的值時[![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)](./ch05.html#pos-inspect),我們看到一個鍵-值對的集合。一旦我們以這樣的方式填充了字典,就可以使用鍵來檢索值:
```py
>>> pos['ideas']
'N'
>>> pos['colorless']
'ADJ'
```
當然,我們可能會無意中使用一個尚未分配值的鍵。
```py
>>> pos['green']
Traceback (most recent call last):
File "<stdin>", line 1, in ?
KeyError: 'green'
```
這就提出了一個重要的問題。與列表和字符串不同,我們可以用`len()`算出哪些整數是合法索引,我們如何算出一個字典的合法鍵?如果字典不是太大,我們可以簡單地通過查看變量`pos`檢查它的內容。正如在前面([![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)](./ch05.html#pos-inspect)行)所看到,這為我們提供了鍵-值對。請注意它們的順序與最初放入它們的順序不同;這是因為字典不是序列而是映射(參見[3.2](./ch05.html#fig-maps02)),鍵沒有固定地排序。
換種方式,要找到鍵,我們可以將字典轉換成一個列表[![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)](./ch05.html#dict-to-list)——要么在期望列表的上下文中使用字典,如作為`sorted()`的參數[![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)](./ch05.html#dict-sorted),要么在`for` 循環中[![[3]](https://img.kancloud.cn/69/fc/69fcb1188781ff9f726d82da7988a139_15x15.jpg)](./ch05.html#dict-for-loop)。
```py
>>> list(pos) ![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)
['ideas', 'furiously', 'colorless', 'sleep']
>>> sorted(pos) ![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)
['colorless', 'furiously', 'ideas', 'sleep']
>>> [w for w in pos if w.endswith('s')] ![[3]](https://img.kancloud.cn/69/fc/69fcb1188781ff9f726d82da7988a139_15x15.jpg)
['colorless', 'ideas']
```
注意
當你輸入`list(pos)`時,你看到的可能會與這里顯示的順序不同。如果你想看到有序的鍵,只需要對它們進行排序。
與使用一個`for`循環遍歷字典中的所有鍵一樣,我們可以使用`for`循環輸出列表:
```py
>>> for word in sorted(pos):
... print(word + ":", pos[word])
...
colorless: ADJ
furiously: ADV
sleep: V
ideas: N
```
最后,字典的方法`keys()`、`values()`和`items()`允許我們以單獨的列表訪問鍵、值以及鍵-值對。我們甚至可以排序元組[![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)](./ch05.html#sort-tuples),按它們的第一個元素排序(如果第一個元素相同,就使用它們的第二個元素)。
```py
>>> list(pos.keys())
['colorless', 'furiously', 'sleep', 'ideas']
>>> list(pos.values())
['ADJ', 'ADV', 'V', 'N']
>>> list(pos.items())
[('colorless', 'ADJ'), ('furiously', 'ADV'), ('sleep', 'V'), ('ideas', 'N')]
>>> for key, val in sorted(pos.items()): ![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)
... print(key + ":", val)
...
colorless: ADJ
furiously: ADV
ideas: N
sleep: V
```
我們要確保當我們在字典中查找某詞時,一個鍵只得到一個值。現在假設我們試圖用字典來存儲可同時作為動詞和名詞的詞 sleep:
```py
>>> pos['sleep'] = 'V'
>>> pos['sleep']
'V'
>>> pos['sleep'] = 'N'
>>> pos['sleep']
'N'
```
最初,`pos['sleep']`給的值是`'V'`。但是,它立即被一個新值`'N'`覆蓋。換句話說,字典中只能有`'sleep'`的一個條目。然而,有一個方法可以在該項目中存儲多個值:我們使用一個列表值,例如`pos['sleep'] = ['N', 'V']`。事實上,這就是我們在[4](./ch02.html#sec-lexical-resources)中看到的 CMU 發音字典,它為一個詞存儲多個發音。
## 3.3 定義字典
我們可以使用鍵-值對格式創建字典。有兩種方式做這個,我們通常會使用第一個:
```py
>>> pos = {'colorless': 'ADJ', 'ideas': 'N', 'sleep': 'V', 'furiously': 'ADV'}
>>> pos = dict(colorless='ADJ', ideas='N', sleep='V', furiously='ADV')
```
請注意,字典的鍵必須是不可改變的類型,如字符串和元組。如果我們嘗試使用可變鍵定義字典會得到一個`TypeError`:
```py
>>> pos = {['ideas', 'blogs', 'adventures']: 'N'}
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: list objects are unhashable
```
## 3.4 默認字典
如果我們試圖訪問一個不在字典中的鍵,會得到一個錯誤。然而,如果一個字典能為這個新鍵自動創建一個條目并給它一個默認值,如 0 或者一個空鏈表,將是有用的。由于這個原因,可以使用一種特殊的稱為`defaultdict`的字典。為了使用它,我們必須提供一個參數,用來創建默認值,如`int`, `float`, `str`, `list`, `dict`, `tuple`。
```py
>>> from collections import defaultdict
>>> frequency = defaultdict(int)
>>> frequency['colorless'] = 4
>>> frequency['ideas']
0
>>> pos = defaultdict(list)
>>> pos['sleep'] = ['NOUN', 'VERB']
>>> pos['ideas']
[]
```
注意
這些默認值實際上是將其他對象轉換為指定類型的函數(例如`int("2")`, `list("2")`)。當它們不帶參數被調用時——`int()`, `list()`——它們分別返回`0`和`[]` 。
前面的例子中指定字典項的默認值為一個特定的數據類型的默認值。然而,也可以指定任何我們喜歡的默認值,只要提供可以無參數的被調用產生所需值的函數的名子。讓我們回到我們的詞性的例子,創建一個任一條目的默認值是`'N'`的字典[![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)](./ch05.html#default-noun)。當我們訪問一個不存在的條目時[![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)](./ch05.html#non-existent),它會自動添加到字典[![[3]](https://img.kancloud.cn/69/fc/69fcb1188781ff9f726d82da7988a139_15x15.jpg)](./ch05.html#automatically-added)。
```py
>>> pos = defaultdict(lambda: 'NOUN') ![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)
>>> pos['colorless'] = 'ADJ'
>>> pos['blog'] ![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)
'NOUN'
>>> list(pos.items())
[('blog', 'NOUN'), ('colorless', 'ADJ')] # [_automatically-added]
```
注意
上面的例子使用一個 lambda 表達式,在[4.4](./ch04.html#sec-functions)介紹過。這個 lambda 表達式沒有指定參數,所以我們用不帶參數的括號調用它。因此,下面的`f`和`g`的定義是等價的:
```py
>>> f = lambda: 'NOUN'
>>> f()
'NOUN'
>>> def g():
... return 'NOUN'
>>> g()
'NOUN'
```
讓我們來看看默認字典如何被應用在較大規模的語言處理任務中。許多語言處理任務——包括標注——費很大力氣來正確處理文本中只出現過一次的詞。如果有一個固定的詞匯和沒有新詞會出現的保證,它們會有更好的表現。在一個默認字典的幫助下,我們可以預處理一個文本,替換低頻詞匯為一個特殊的“超出詞匯表”詞符`UNK`。(你能不看下面的想出如何做嗎?)
我們需要創建一個默認字典,映射每個詞為它們的替換詞。最頻繁的 n 個詞將被映射到它們自己。其他的被映射到`UNK`。
```py
>>> alice = nltk.corpus.gutenberg.words('carroll-alice.txt')
>>> vocab = nltk.FreqDist(alice)
>>> v1000 = [word for (word, _) in vocab.most_common(1000)]
>>> mapping = defaultdict(lambda: 'UNK')
>>> for v in v1000:
... mapping[v] = v
...
>>> alice2 = [mapping[v] for v in alice]
>>> alice2[:100]
['UNK', 'Alice', "'", 's', 'UNK', 'in', 'UNK', 'by', 'UNK', 'UNK', 'UNK',
'UNK', 'CHAPTER', 'I', '.', 'UNK', 'the', 'Rabbit', '-', 'UNK', 'Alice',
'was', 'beginning', 'to', 'get', 'very', 'tired', 'of', 'sitting', 'by',
'her', 'sister', 'on', 'the', 'UNK', ',', 'and', 'of', 'having', 'nothing',
'to', 'do', ':', 'once', 'or', 'twice', 'she', 'had', 'UNK', 'into', 'the',
'book', 'her', 'sister', 'was', 'UNK', ',', 'but', 'it', 'had', 'no',
'pictures', 'or', 'UNK', 'in', 'it', ',', "'", 'and', 'what', 'is', 'the',
'use', 'of', 'a', 'book', ",'", 'thought', 'Alice', "'", 'without',
'pictures', 'or', 'conversation', "?'" ...]
>>> len(set(alice2))
1001
```
## 3.5 遞增地更新字典
我們可以使用字典計數出現的次數,模擬[fig-tally](./ch01.html#fig-tally)所示的計數詞匯的方法。首先初始化一個空的`defaultdict`,然后處理文本中每個詞性標記。如果標記以前沒有見過,就默認計數為零。每次我們遇到一個標記,就使用`+=`運算符遞增它的計數。
```py
>>> from collections import defaultdict
>>> counts = defaultdict(int)
>>> from nltk.corpus import brown
>>> for (word, tag) in brown.tagged_words(categories='news', tagset='universal'):
... counts[tag] += 1
...
>>> counts['NOUN']
30640
>>> sorted(counts)
['ADJ', 'PRT', 'ADV', 'X', 'CONJ', 'PRON', 'VERB', '.', 'NUM', 'NOUN', 'ADP', 'DET']
>>> from operator import itemgetter
>>> sorted(counts.items(), key=itemgetter(1), reverse=True)
[('NOUN', 30640), ('VERB', 14399), ('ADP', 12355), ('.', 11928), ...]
>>> [t for t, c in sorted(counts.items(), key=itemgetter(1), reverse=True)]
['NOUN', 'VERB', 'ADP', '.', 'DET', 'ADJ', 'ADV', 'CONJ', 'PRON', 'PRT', 'NUM', 'X']
```
[3.3](./ch05.html#code-dictionary)中的列表演示了一個重要的按值排序一個字典的習慣用法,來按頻率遞減順序顯示詞匯。`sorted()`的第一個參數是要排序的項目,它是由一個詞性標記和一個頻率組成的元組的列表。第二個參數使用函數`itemgetter()`指定排序的鍵。在一般情況下,`itemgetter(n)`返回一個函數,這個函數可以在一些其他序列對象上被調用獲得這個序列的第 n 個元素,例如:
```py
>>> pair = ('NP', 8336)
>>> pair[1]
8336
>>> itemgetter(1)(pair)
8336
```
`sorted()`的最后一個參數指定項目是否應被按相反的順序返回,即頻率值遞減。
在[3.3](./ch05.html#code-dictionary)的開頭還有第二個有用的習慣用法,那里我們初始化一個`defaultdict`,然后使用`for`循環來更新其值。下面是一個示意版本:
`>>> my_dictionary = defaultdict(`_function to create default value_`)``>>> for` _item_ `in` _sequence_`:``... my_dictionary[`_item_key_`]` _is updated with information about item_
下面是這種模式的另一個示例,我們按它們最后兩個字母索引詞匯:
```py
>>> last_letters = defaultdict(list)
>>> words = nltk.corpus.words.words('en')
>>> for word in words:
... key = word[-2:]
... last_letters[key].append(word)
...
>>> last_letters['ly']
['abactinally', 'abandonedly', 'abasedly', 'abashedly', 'abashlessly', 'abbreviately',
'abdominally', 'abhorrently', 'abidingly', 'abiogenetically', 'abiologically', ...]
>>> last_letters['zy']
['blazy', 'bleezy', 'blowzy', 'boozy', 'breezy', 'bronzy', 'buzzy', 'Chazy', ...]
```
下面的例子使用相同的模式創建一個顛倒順序的詞字典。(你可能會試驗第 3 行來弄清楚為什么這個程序能運行。)
```py
>>> anagrams = defaultdict(list)
>>> for word in words:
... key = ''.join(sorted(word))
... anagrams[key].append(word)
...
>>> anagrams['aeilnrt']
['entrail', 'latrine', 'ratline', 'reliant', 'retinal', 'trenail']
```
由于積累這樣的詞是如此常用的任務,NLTK 提供一個創建`defaultdict(list)`更方便的方式,形式為`nltk.Index()`。
```py
>>> anagrams = nltk.Index((''.join(sorted(w)), w) for w in words)
>>> anagrams['aeilnrt']
['entrail', 'latrine', 'ratline', 'reliant', 'retinal', 'trenail']
```
注意
`nltk.Index`是一個支持額外初始化的`defaultdict(list)`。類似地,`nltk.FreqDist`本質上是一個額外支持初始化的`defaultdict(int)`(附帶排序和繪圖方法)。
## 3.6 復雜的鍵和值
我們可以使用具有復雜的鍵和值的默認字典。讓我們研究一個詞可能的標記的范圍,給定詞本身和它前一個詞的標記。我們將看到這些信息如何被一個詞性標注器使用。
```py
>>> pos = defaultdict(lambda: defaultdict(int))
>>> brown_news_tagged = brown.tagged_words(categories='news', tagset='universal')
>>> for ((w1, t1), (w2, t2)) in nltk.bigrams(brown_news_tagged): ![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)
... pos[(t1, w2)][t2] += 1 ![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)
...
>>> pos[('DET', 'right')] ![[3]](https://img.kancloud.cn/69/fc/69fcb1188781ff9f726d82da7988a139_15x15.jpg)
defaultdict(<class 'int'>, {'ADJ': 11, 'NOUN': 5})
```
這個例子使用一個字典,它的條目的默認值也是一個字典(其默認值是`int()`,即 0)。請注意我們如何遍歷已標注語料庫的雙連詞,每次遍歷處理一個詞-標記對[![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)](./ch05.html#processing-pairs)。每次通過循環時,我們更新字典`pos`中的條目`(t1, w2)`,一個標記和它 _ 后面 _ 的詞[![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)](./ch05.html#tag-word-update)。當我們在`pos`中查找一個項目時,我們必須指定一個復合鍵[![[3]](https://img.kancloud.cn/69/fc/69fcb1188781ff9f726d82da7988a139_15x15.jpg)](./ch05.html#compound-key),然后得到一個字典對象。一個詞性標注器可以使用這些信息來決定詞 right,前面是一個限定詞時,應標注為`ADJ`。
## 3.7 反轉字典
字典支持高效查找,只要你想獲得任意鍵的值。如果`d`是一個字典,`k`是一個鍵,輸入`d[k]`,就立即獲得值。給定一個值查找對應的鍵要慢一些和麻煩一些:
```py
>>> counts = defaultdict(int)
>>> for word in nltk.corpus.gutenberg.words('milton-paradise.txt'):
... counts[word] += 1
...
>>> [key for (key, value) in counts.items() if value == 32]
['brought', 'Him', 'virtue', 'Against', 'There', 'thine', 'King', 'mortal',
'every', 'been']
```
如果我們希望經常做這樣的一種“反向查找”,建立一個映射值到鍵的字典是有用的。在沒有兩個鍵具有相同的值情況,這是一個容易的事。只要得到字典中的所有鍵-值對,并創建一個新的值-鍵對字典。下一個例子演示了用鍵-值對初始化字典`pos`的另一種方式。
```py
>>> pos = {'colorless': 'ADJ', 'ideas': 'N', 'sleep': 'V', 'furiously': 'ADV'}
>>> pos2 = dict((value, key) for (key, value) in pos.items())
>>> pos2['N']
'ideas'
```
首先讓我們將我們的詞性字典做的更實用些,使用字典的`update()`方法加入再一些詞到`pos`中,創建多個鍵具有相同的值的情況。這樣一來,剛才看到的反向查找技術就將不再起作用(為什么不?)作為替代,我們不得不使用`append()`積累詞和每個詞性,如下所示:
```py
>>> pos.update({'cats': 'N', 'scratch': 'V', 'peacefully': 'ADV', 'old': 'ADJ'})
>>> pos2 = defaultdict(list)
>>> for key, value in pos.items():
... pos2[value].append(key)
...
>>> pos2['ADV']
['peacefully', 'furiously']
```
現在,我們已經反轉字典`pos`,可以查任意詞性找到所有具有此詞性的詞。可以使用 NLTK 中的索引支持更容易的做同樣的事,如下所示:
```py
>>> pos2 = nltk.Index((value, key) for (key, value) in pos.items())
>>> pos2['ADV']
['peacefully', 'furiously']
```
[3.2](./ch05.html#tab-dict)給出 Python 字典方法的總結。
表 3.2:
Python 字典方法:常用的方法與字典相關習慣用法的總結。
```py
>>> from nltk.corpus import brown
>>> brown_tagged_sents = brown.tagged_sents(categories='news')
>>> brown_sents = brown.sents(categories='news')
```
## 4.1 默認標注器
最簡單的標注器是為每個詞符分配同樣的標記。這似乎是一個相當平庸的一步,但它建立了標注器性能的一個重要的底線。為了得到最好的效果,我們用最有可能的標記標注每個詞。讓我們找出哪個標記是最有可能的(現在使用未簡化標記集):
```py
>>> tags = [tag for (word, tag) in brown.tagged_words(categories='news')]
>>> nltk.FreqDist(tags).max()
'NN'
```
現在我們可以創建一個將所有詞都標注成`NN`的標注器。
```py
>>> raw = 'I do not like green eggs and ham, I do not like them Sam I am!'
>>> tokens = 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')]
```
不出所料,這種方法的表現相當不好。在一個典型的語料庫中,它只標注正確了八分之一的標識符,正如我們在這里看到的:
```py
>>> default_tagger.evaluate(brown_tagged_sents)
0.13089484257215028
```
默認的標注器給每一個單獨的詞分配標記,即使是之前從未遇到過的詞。碰巧的是,一旦我們處理了幾千詞的英文文本之后,大多數新詞都將是名詞。正如我們將看到的,這意味著,默認標注器可以幫助我們提高語言處理系統的穩定性。我們將很快回來講述這個。
## 4.2 正則表達式標注器
正則表達式標注器基于匹配模式分配標記給詞符。例如,我們可能會猜測任一以 ed 結尾的詞都是動詞過去分詞,任一以's 結尾的詞都是名詞所有格。可以用一個正則表達式的列表表示這些:
```py
>>> 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)
... ]
```
請注意,這些是順序處理的,第一個匹配上的會被使用。現在我們可以建立一個標注器,并用它來標記一個句子。做完這一步會有約五分之一是正確的。
```py
>>> 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
```
最終的正則表達式?`.*`?是一個全面捕捉的,標注所有詞為名詞。這與默認標注器是等效的(只是效率低得多)。除了作為正則表達式標注器的一部分重新指定這個,有沒有辦法結合這個標注器和默認標注器呢?我們將很快看到如何做到這一點。
注意
**輪到你來:**看看你能不能想出一些模式,提高上面所示的正則表達式標注器的性能。(請注意[1](./ch06.html#sec-supervised-classification)描述部分自動化這類工作的方法。)
## 4.3 查詢標注器
很多高頻詞沒有`NN`標記。讓我們找出 100 個最頻繁的詞,存儲它們最有可能的標記。然后我們可以使用這個信息作為“查找標注器”(NLTK `UnigramTagger`)的模型:
```py
>>> fd = nltk.FreqDist(brown.words(categories='news'))
>>> cfd = nltk.ConditionalFreqDist(brown.tagged_words(categories='news'))
>>> most_freq_words = fd.most_common(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 個最頻繁的詞的標記就使我們能正確標注很大一部分詞符(近一半,事實上)。讓我們來看看它在一些未標注的輸入文本上做的如何:
```py
>>> 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`。換句話說,我們要先使用查找表,如果它不能指定一個標記就使用默認標注器,這個過程叫做回退([5](./ch05.html#sec-n-gram-tagging))。我們可以做到這個,通過指定一個標注器作為另一個標注器的參數,如下所示。現在查找標注器將只存儲名詞以外的詞的詞-標記對,只要它不能給一個詞分配標記,它將會調用默認標注器。
```py
>>> baseline_tagger = nltk.UnigramTagger(model=likely_tags,
... backoff=nltk.DefaultTagger('NN'))
```
讓我們把所有這些放在一起,寫一個程序來創建和評估具有一定范圍的查找標注器 ,[4.1](./ch05.html#code-baseline-tagger)。
```py
def performance(cfd, wordlist):
lt = dict((word, cfd[word].max()) for word in wordlist)
baseline_tagger = nltk.UnigramTagger(model=lt, backoff=nltk.DefaultTagger('NN'))
return baseline_tagger.evaluate(brown.tagged_sents(categories='news'))
def display():
import pylab
word_freqs = nltk.FreqDist(brown.words(categories='news')).most_common()
words_by_freq = [w for (w, _) in word_freqs]
cfd = nltk.ConditionalFreqDist(brown.tagged_words(categories='news'))
sizes = 2 ** pylab.arange(15)
perfs = [performance(cfd, words_by_freq[:size]) for size in sizes]
pylab.plot(sizes, perfs, '-bo')
pylab.title('Lookup Tagger Performance with Varying Model Size')
pylab.xlabel('Model Size')
pylab.ylabel('Performance')
pylab.show()
```

圖 4.2:查找標注器
可以觀察到,隨著模型規模的增長,最初的性能增加迅速,最終達到一個穩定水平,這時模型的規模大量增加性能的提高很小。(這個例子使用`pylab`繪圖軟件包,在[4.8](./ch04.html#sec-libraries)討論過)。
## 4.4 評估
在前面的例子中,你會注意到對準確性得分的強調。事實上,評估這些工具的表現是 NLP 的一個中心主題。回想[fig-sds](./ch01.html#fig-sds)中的處理流程;一個模塊輸出中的任何錯誤都在下游模塊大大的放大。
我們對比人類專家分配的標記來評估一個標注器的表現。由于我們通常很難獲得專業和公正的人的判斷,所以使用黃金標準測試數據來代替。這是一個已經手動標注并作為自動系統評估標準而被接受的語料庫。當標注器對給定詞猜測的標記與黃金標準標記相同,標注器被視為是正確的。
當然,設計和實施原始的黃金標準標注的也是人。更深入的分析可能會顯示黃金標準中的錯誤,或者可能最終會導致一個修正的標記集和更復雜的指導方針。然而,黃金標準就目前有關的自動標注器的評估而言被定義成“正確的”。
注意
開發一個已標注語料庫是一個重大的任務。除了數據,它會產生復雜的工具、文檔和實踐,為確保高品質的標注。標記集和其他編碼方案不可避免地依賴于一些理論主張,不是所有的理論主張都被共享,然而,語料庫的創作者往往竭盡全力使他們的工作盡可能理論中立,以最大限度地提高其工作的有效性。我們將在[11.](./ch11.html#chap-data)討論創建一個語料庫的挑戰。
## 5 N-gram 標注
## 5.1 一元標注
一元標注器基于一個簡單的統計算法:對每個標識符分配這個獨特的標識符最有可能的標記。例如,它將分配標記`JJ`給詞 frequent 的所有出現,因為 frequent 用作一個形容詞(例如 a frequent word)比用作一個動詞(例如 I frequent this cafe)更常見。一個一元標注器的行為就像一個查找標注器([4](./ch05.html#sec-automatic-tagging)),除了有一個更方便的建立它的技術,稱為訓練。在下面的代碼例子中,我們訓練一個一元標注器,用它來標注一個句子,然后評估:
```py
>>> 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
```
我們訓練一個`UnigramTagger`,通過在我們初始化標注器時指定已標注的句子數據作為參數。訓練過程中涉及檢查每個詞的標記,將所有詞的最可能的標記存儲在一個字典里面,這個字典存儲在標注器內部。
## 5.2 分離訓練和測試數據
現在,我們正在一些數據上訓練一個標注器,必須小心不要在相同的數據上測試,如我們在前面的例子中的那樣。一個只是記憶它的訓練數據,而不試圖建立一個一般的模型的標注器會得到一個完美的得分,但在標注新的文本時將是無用的。相反,我們應該分割數據,90%為測試數據,其余 10%為測試數據:
```py
>>> 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...
```
雖然得分更糟糕了,但是現在我們對這種標注器的用處有了更好的了解,如它在之前沒有遇見的文本上的表現。
## 5.3 一般的 N-gram 標注
在基于一元處理一個語言處理任務時,我們使用上下文中的一個項目。標注的時候,我們只考慮當前的詞符,與更大的上下文隔離。給定一個模型,我們能做的最好的是為每個詞標注其 _ 先驗的 _ 最可能的標記。這意味著我們將使用相同的標記標注一個詞,如 wind,不論它出現的上下文是 the wind 還是 to wind。
一個 n-gram tagger 標注器是一個一元標注器的一般化,它的上下文是當前詞和它前面 _n_-1 個標識符的詞性標記,如圖[5.1](./ch05.html#fig-tag-context)所示。要選擇的標記是圓圈里的 _t_<sub>n</sub>,灰色陰影的是上下文。在[5.1](./ch05.html#fig-tag-context)所示的 n-gram 標注器的例子中,我們讓 _n_=3;也就是說,我們考慮當前詞的前兩個詞的標記。一個 n-gram 標注器挑選在給定的上下文中最有可能的標記。

圖 5.1:標注器上下文
注意
1-gram 標注器是一元標注器另一個名稱:即用于標注一個詞符的上下文的只是詞符本身。2-gram 標注器也稱為 _ 二元標注器 _,3-gram 標注器也稱為 _ 三元標注器 _。
`NgramTagger`類使用一個已標注的訓練語料庫來確定對每個上下文哪個詞性標記最有可能。這里我們看 n-gram 標注器的一個特殊情況,二元標注器。首先,我們訓練它,然后用它來標注未標注的句子:
```py
>>> 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)]
```
請注意,二元標注器能夠標注訓練中它看到過的句子中的所有詞,但對一個沒見過的句子表現很差。只要遇到一個新詞(如 13.5),就無法給它分配標記。它不能標注下面的詞(如 million),即使是在訓練過程中看到過的,只是因為在訓練過程中從來沒有見過它前面有一個`None`標記的詞。因此,標注器標注句子的其余部分也失敗了。它的整體準確度得分非常低:
```py
>>> bigram_tagger.evaluate(test_sents)
0.102063...
```
當 _n_ 越大,上下文的特異性就會增加,我們要標注的數據中包含訓練數據中不存在的上下文的幾率也增大。這被稱為 _ 數據稀疏 _ 問題,在 NLP 中是相當普遍的。因此,我們的研究結果的精度和覆蓋范圍之間需要有一個權衡(這與信息檢索中的精度/召回權衡有關)。
小心!
N-gram 標注器不應考慮跨越句子邊界的上下文。因此,NLTK 的標注器被設計用于句子列表,其中一個句子是一個詞列表。在一個句子的開始,_t_<sub>n-1</sub>和前面的標記被設置為`None`。
## 5.4 組合標注器
解決精度和覆蓋范圍之間的權衡的一個辦法是盡可能的使用更精確的算法,但卻在很多時候落后于具有更廣覆蓋范圍的算法。例如,我們可以按如下方式組合二元標注器、一元注器和一個默認標注器,如下:
1. 嘗試使用二元標注器標注標識符。
2. 如果二元標注器無法找到一個標記,嘗試一元標注器。
3. 如果一元標注器也無法找到一個標記,使用默認標注器。
大多數 NLTK 標注器允許指定一個回退標注器。回退標注器自身可能也有一個回退標注器:
```py
>>> t0 = nltk.DefaultTagger('NN')
>>> t1 = nltk.UnigramTagger(train_sents, backoff=t0)
>>> t2 = nltk.BigramTagger(train_sents, backoff=t1)
>>> t2.evaluate(test_sents)
0.844513...
```
注意
**輪到你來:** 通過定義一個名為`t3`的`TrigramTagger`,擴展前面的例子,它是`t2`的回退標注器。
請注意,我們在標注器初始化時指定回退標注器,從而使訓練能利用回退標注器。于是,在一個特定的上下文中,如果二元標注器將分配與它的一元回退標注器一樣的標記,那么二元標注器丟棄訓練的實例。這樣保持盡可能小的二元標注器模型。我們可以進一步指定一個標注器需要看到一個上下文的多個實例才能保留它,例如`nltk.BigramTagger(sents, cutoff=2, backoff=t1)`將會丟棄那些只看到一次或兩次的上下文。
## 5.5 標注生詞
我們標注生詞的方法仍然是回退到一個正則表達式標注器或一個默認標注器。這些都無法利用上下文。因此,如果我們的標注器遇到詞 blog,訓練過程中沒有看到過,它會分配相同的標記,不論這個詞出現的上下文是 the blog 還是 to blog。我們怎樣才能更好地處理這些生詞,或詞匯表以外的項目?
一個有用的基于上下文標注生詞的方法是限制一個標注器的詞匯表為最頻繁的 n 個詞,使用[3](./ch05.html#sec-dictionaries)中的方法替代每個其他的詞為一個特殊的詞 UNK。訓練時,一個一元標注器可能會學到 UNK 通常是一個名詞。然而,n-gram 標注器會檢測它的一些其他標記中的上下文。例如,如果前面的詞是 to(標注為`TO`),那么 UNK 可能會被標注為一個動詞。
## 5.6 存儲標注器
在大語料庫上訓練一個標注器可能需要大量的時間。沒有必要在每次我們需要的時候訓練一個標注器,很容易將一個訓練好的標注器保存到一個文件以后重復使用。讓我們保存我們的標注器`t2`到文件`t2.pkl`。
```py
>>> from pickle import dump
>>> output = open('t2.pkl', 'wb')
>>> dump(t2, output, -1)
>>> output.close()
```
現在,我們可以在一個單獨的 Python 進程中,我們可以載入保存的標注器。
```py
>>> from pickle import load
>>> input = open('t2.pkl', 'rb')
>>> tagger = load(input)
>>> input.close()
```
現在讓我們檢查它是否可以用來標注。
```py
>>> text = """The board's action shows what free enterprise
... is up against in our complex maze of regulatory laws ."""
>>> tokens = text.split()
>>> tagger.tag(tokens)
[('The', 'AT'), ("board's", 'NN$'), ('action', 'NN'), ('shows', 'NNS'),
('what', 'WDT'), ('free', 'JJ'), ('enterprise', 'NN'), ('is', 'BEZ'),
('up', 'RP'), ('against', 'IN'), ('in', 'IN'), ('our', 'PP$'), ('complex', 'JJ'),
('maze', 'NN'), ('of', 'IN'), ('regulatory', 'NN'), ('laws', 'NNS'), ('.', '.')]
```
## 5.7 準確性的極限
一個 n-gram 標注器準確性的上限是什么?考慮一個三元標注器的情況。它遇到多少詞性歧義的情況?我們可以根據經驗決定這個問題的答案:
```py
>>> cfd = nltk.ConditionalFreqDist(
... ((x[1], y[1], z[0]), z[1])
... for sent in brown_tagged_sents
... for x, y, z in nltk.trigrams(sent))
>>> ambiguous_contexts = [c for c in cfd.conditions() if len(cfd[c]) > 1]
>>> sum(cfd[c].N() for c in ambiguous_contexts) / cfd.N()
0.049297702068029296
```
因此,1/20 的三元是有歧義的[示例]。給定當前單詞及其前兩個標記,根據訓練數據,在 5%的情況中,有一個以上的標記可能合理地分配給當前詞。假設我們總是挑選在這種含糊不清的上下文中最有可能的標記,可以得出三元標注器準確性的一個下界。
調查標注器準確性的另一種方法是研究它的錯誤。有些標記可能會比別的更難分配,可能需要專門對這些數據進行預處理或后處理。一個方便的方式查看標注錯誤是混淆矩陣。它用圖表表示期望的標記(黃金標準)與實際由標注器產生的標記:
```py
>>> test_tags = [tag for sent in brown.sents(categories='editorial')
... for (word, tag) in t2.tag(sent)]
>>> gold_tags = [tag for (word, tag) in brown.tagged_words(categories='editorial')]
>>> print(nltk.ConfusionMatrix(gold_tags, test_tags))
```
基于這樣的分析,我們可能會決定修改標記集。或許標記之間很難做出的區分可以被丟棄,因為它在一些較大的處理任務的上下文中并不重要。
分析標注器準確性界限的另一種方式來自人類標注者之間并非 100%的意見一致。[更多]
一般情況下,標注過程會損壞區別:例如當所有的人稱代詞被標注為`PRP`時,詞的特性通常會失去。與此同時,標注過程引入了新的區別從而去除了含糊之處:例如 deal 標注為`VB`或`NN`。這種消除某些區別并引入新的區別的特點是標注的一個重要的特征,有利于分類和預測。當我們引入一個標記集的更細的劃分時,在 n-gram 標注器決定什么樣的標記分配給一個特定的詞時,可以獲得關于左側上下文的更詳細的信息。然而,標注器同時也將需要做更多的工作來劃分當前的詞符,只是因為有更多可供選擇的標記。相反,使用較少的區別(如簡化的標記集),標注器有關上下文的信息會減少,為當前詞符分類的選擇范圍也較小。
我們已經看到,訓練數據中的歧義導致標注器準確性的上限。有時更多的上下文能解決這些歧義。然而,在其他情況下,如[(Church, Young, & Bloothooft, 1996)](./bibliography.html#abney1996pst)中指出的,只有參考語法或現實世界的知識,才能解決歧義。盡管有這些缺陷,詞性標注在用統計方法進行自然語言處理的興起過程中起到了核心作用。1990 年代初,統計標注器令人驚訝的精度是一個驚人的示范,可以不用更深的語言學知識解決一小部分語言理解問題,即詞性消歧。這個想法能再推進嗎?第[7.](./ch07.html#chap-chunk)中,我們將看到,它可以。
## 6 基于轉換的標注
n-gram 標注器的一個潛在的問題是它們的 n-gram 表(或語言模型)的大小。如果使用各種語言技術的標注器部署在移動計算設備上,在模型大小和標注器準確性之間取得平衡是很重要的。使用回退標注器的 n-gram 標注器可能存儲 trigram 和 bigram 表,這是很大的稀疏陣列,可能有數億條條目。
第二個問題是關于上下文。n-gram 標注器從前面的上下文中獲得的唯一的信息是標記,雖然詞本身可能是一個有用的信息源。n-gram 模型使用上下文中的詞的其他特征為條件是不切實際的。在本節中,我們考察 Brill 標注,一種歸納標注方法,它的性能很好,使用的模型只有 n-gram 標注器的很小一部分。
Brill 標注是一種 _ 基于轉換的學習 _,以它的發明者命名。一般的想法很簡單:猜每個詞的標記,然后返回和修復錯誤。在這種方式中,Brill 標注器陸續將一個不良標注的文本轉換成一個更好的。與 n-gram 標注一樣,這是有 _ 監督的學習 _ 方法,因為我們需要已標注的訓練數據來評估標注器的猜測是否是一個錯誤。然而,不像 n-gram 標注,它不計數觀察結果,只編制一個轉換修正規則列表。
Brill 標注的的過程通常是與繪畫類比來解釋的。假設我們要畫一棵樹,包括大樹枝、樹枝、小枝、葉子和一個統一的天藍色背景的所有細節。不是先畫樹然后嘗試在空白處畫藍色,而是簡單的將整個畫布畫成藍色,然后通過在藍色背景上上色“修正”樹的部分。以同樣的方式,我們可能會畫一個統一的褐色的樹干再回過頭來用更精細的刷子畫進一步的細節。Brill 標注使用了同樣的想法:以大筆畫開始,然后修復細節,一點點的細致的改變。讓我們看看下面的例子:
```py
>>> nltk.tag.brill.demo()
Training Brill tagger on 80 sentences...
Finding initial useful rules...
Found 6555 useful rules.
B |
S F r O | Score = Fixed - Broken
c i o t | R Fixed = num tags changed incorrect -> correct
o x k h | u Broken = num tags changed correct -> incorrect
r e e e | l Other = num tags changed incorrect -> incorrect
e d n r | e
------------------+-------------------------------------------------------
12 13 1 4 | NN -> VB if the tag of the preceding word is 'TO'
8 9 1 23 | NN -> VBD if the tag of the following word is 'DT'
8 8 0 9 | NN -> VBD if the tag of the preceding word is 'NNS'
6 9 3 16 | NN -> NNP if the tag of words i-2...i-1 is '-NONE-'
5 8 3 6 | NN -> NNP if the tag of the following word is 'NNP'
5 6 1 0 | NN -> NNP if the text of words i-2...i-1 is 'like'
5 5 0 3 | NN -> VBN if the text of the following word is '*-1'
...
>>> print(open("errors.out").read())
left context | word/test->gold | right context
--------------------------+------------------------+--------------------------
| Then/NN->RB | ,/, in/IN the/DT guests/N
, in/IN the/DT guests/NNS | '/VBD->POS | honor/NN ,/, the/DT speed
'/POS honor/NN ,/, the/DT | speedway/JJ->NN | hauled/VBD out/RP four/CD
NN ,/, the/DT speedway/NN | hauled/NN->VBD | out/RP four/CD drivers/NN
DT speedway/NN hauled/VBD | out/NNP->RP | four/CD drivers/NNS ,/, c
dway/NN hauled/VBD out/RP | four/NNP->CD | drivers/NNS ,/, crews/NNS
hauled/VBD out/RP four/CD | drivers/NNP->NNS | ,/, crews/NNS and/CC even
P four/CD drivers/NNS ,/, | crews/NN->NNS | and/CC even/RB the/DT off
NNS and/CC even/RB the/DT | official/NNP->JJ | Indianapolis/NNP 500/CD a
| After/VBD->IN | the/DT race/NN ,/, Fortun
ter/IN the/DT race/NN ,/, | Fortune/IN->NNP | 500/CD executives/NNS dro
s/NNS drooled/VBD like/IN | schoolboys/NNP->NNS | over/IN the/DT cars/NNS a
olboys/NNS over/IN the/DT | cars/NN->NNS | and/CC drivers/NNS ./.
```
## 7 如何確定一個詞的分類
我們已經詳細研究了詞類,現在轉向一個更基本的問題:我們如何首先決定一個詞屬于哪一類?在一般情況下,語言學家使用形態學、句法和語義線索確定一個詞的類別。
## 7.1 形態學線索
一個詞的內部結構可能為這個詞分類提供有用的線索。舉例來說:-ness 是一個后綴,與形容詞結合產生一個名詞,如 happy → happiness, ill → illness。如果我們遇到的一個以-ness 結尾的詞,很可能是一個名詞。同樣的,-ment 是與一些動詞結合產生一個名詞的后綴,如 govern → government 和 establish → establishment。
英語動詞也可以是形態復雜的。例如,一個動詞的現在分詞以-ing 結尾,表示正在進行的還沒有結束的行動(如 falling, eating)。-ing 后綴也出現在從動詞派生的名詞中,如 the falling of the leaves(這被稱為動名詞)。
## 7.2 句法線索
另一個信息來源是一個詞可能出現的典型的上下文語境。例如,假設我們已經確定了名詞類。那么我們可以說,英語形容詞的句法標準是它可以立即出現在一個名詞前,或緊跟在詞 be 或 very 后。根據這些測試,near 應該被歸類為形容詞:
```py
Statement User117 Dude..., I wanted some of that
ynQuestion User120 m I missing something?
Bye User117 I'm gonna go fix food, I'll be back later.
System User122 JOIN
System User2 slaps User122 around a bit with a large trout.
Statement User121 18/m pm me if u tryin to chat
```
## 10 練習
1. ? 網上搜索“spoof newspaper headlines”,找到這種寶貝:British Left Waffles on Falkland Islands 和 Juvenile Court to Try Shooting Defendant。手工標注這些頭條,看看詞性標記的知識是否可以消除歧義。
2. ? 和別人一起,輪流挑選一個既可以是名詞也可以是動詞的詞(如 contest);讓對方預測哪一個可能是布朗語料庫中頻率最高的;檢查對方的預測,為幾個回合打分。
3. ? 分詞和標注下面的句子:They wind back the clock, while we chase after the wind。涉及哪些不同的發音和詞類?
4. ? 回顧[3.1](./ch05.html#tab-linguistic-objects)中的映射。討論你能想到的映射的其他的例子。它們從什么類型的信息映射到什么類型的信息?
5. ? 在交互模式下使用 Python 解釋器,實驗本章中字典的例子。創建一個字典`d`,添加一些條目。如果你嘗試訪問一個不存在的條目會發生什么,如`d['xyz']`?
6. ? 嘗試從字典`d`刪除一個元素,使用語法`del d['abc']`。檢查被刪除的項目。
7. ? 創建兩個字典,`d1`和`d2`,為每個添加一些條目。現在發出命令`d1.update(d2)`。這做了什么?它可能是有什么用?
8. ? 創建一個字典`e`,表示你選擇的一些詞的一個單獨的詞匯條目。定義鍵如`headword`、`part-of-speech`、`sense`和`example`,分配給它們適當的值。
9. ? 自己驗證 go 和 went 在分布上的限制,也就是說,它們不能自由地在[7](./ch05.html#sec-how-to-determine-the-category-of-a-word)中的[(3d)](./ch05.html#ex-go)演示的那種上下文中互換。
10. ? 訓練一個一元標注器,在一些新的文本上運行。觀察有些詞沒有分配到標記。為什么沒有?
11. ? 了解詞綴標注器(輸入`help(nltk.AffixTagger)`)。訓練一個詞綴標注器,在一些新的文本上運行。設置不同的詞綴長度和最小詞長做實驗。討論你的發現。
12. ? 訓練一個沒有回退標注器的二元標注器,在一些訓練數據上運行。下一步,在一些新的數據運行它。標注器的準確性會發生什么?為什么呢?
13. ? 我們可以使用字典指定由一個格式化字符串替換的值。閱讀關于格式化字符串的 Python 庫文檔`http://docs.python.org/lib/typesseq-strings.html`,使用這種方法以兩種不同的格式顯示今天的日期。
14. ? 使用`sorted()`和`set()`獲得布朗語料庫使用的標記的排序的列表,刪除重復。
15. ? 寫程序處理布朗語料庫,找到以下問題的答案:
1. 哪些名詞常以它們復數形式而不是它們的單數形式出現?(只考慮常規的復數形式,-s 后綴形式的)。
2. 哪個詞的不同標記數目最多。它們是什么,它們代表什么?
3. 按頻率遞減的順序列出標記。前 20 個最頻繁的標記代表什么?
4. 名詞后面最常見的是哪些標記?這些標記代表什么?
16. ? 探索有關查找標注器的以下問題:
1. 回退標注器被省略時,模型大小變化,標注器的準確性會發生什么?
2. 思考[4.2](./ch05.html#fig-tag-lookup)的曲線;為查找標注器推薦一個平衡內存和準確性的好的規模。你能想出在什么情況下應該盡量減少內存使用,什么情況下性能最大化而不必考慮內存使用?
17. ? 查找標注器的準確性上限是什么,假設其表的大小沒有限制?(提示:寫一個程序算出被分配了最有可能的標記的詞的詞符的平均百分比。)
18. ? 生成已標注數據的一些統計數據,回答下列問題:
1. 總是被分配相同詞性的詞類的比例是多少?
2. 多少詞是有歧義的,從某種意義上說,它們至少和兩個標記一起出現?
3. 布朗語料庫中這些有歧義的詞的 _ 詞符 _ 的百分比是多少?
19. ? `evaluate()`方法算出一個文本上運行的標注器的精度。例如,如果提供的已標注文本是`[('the', 'DT'), ('dog', 'NN')]`,標注器產生的輸出是`[('the', 'NN'), ('dog', 'NN')]`,那么得分為`0.5`。讓我們嘗試找出評價方法是如何工作的:
1. 一個標注器`t`將一個詞匯列表作為輸入,產生一個已標注詞列表作為輸出。然而,`t.evaluate()`只以一個正確標注的文本作為唯一的參數。執行標注之前必須對輸入做些什么?
2. 一旦標注器創建了新標注的文本,`evaluate()` 方法可能如何比較它與原來標注的文本,計算準確性得分?
3. 現在,檢查源代碼來看看這個方法是如何實現的。檢查`nltk.tag.api.__file__`找到源代碼的位置,使用編輯器打開這個文件(一定要使用文件`api.py`,而不是編譯過的二進制文件`api.pyc`)。
20. ? 編寫代碼,搜索布朗語料庫,根據標記查找特定的詞和短語,回答下列問題:
1. 產生一個標注為`MD`的不同的詞的按字母順序排序的列表。
2. 識別可能是復數名詞或第三人稱單數動詞的詞(如 deals, flies)。
3. 識別三個詞的介詞短語形式 IN + DET + NN(如 in the lab)。
4. 男性與女性代詞的比例是多少?
21. ? 在[3.1](./ch03.html#tab-absolutely)中我們看到動詞 adore, love, like, prefer 及前面的限定符 absolutely 和 definitely 的頻率計數的表格。探討這四個動詞前出現的所有限定符。
22. ? 我們定義可以用來做生詞的回退標注器的`regexp_tagger`。這個標注器只檢查基數詞。通過特定的前綴或后綴字符串進行測試,它應該能夠猜測其他標記。例如,我們可以標注所有-s 結尾的詞為復數名詞。定義一個正則表達式標注器(使用`RegexpTagger()`),測試至少 5 個單詞拼寫的其他模式。(使用內聯文檔解釋規則。)
23. ? 考慮上一練習中開發的正則表達式標注器。使用它的`accuracy()`方法評估標注器,嘗試想辦法提高其性能。討論你的發現。客觀的評估如何幫助開發過程?
24. ? 數據稀疏問題有多嚴重?調查 n-gram 標注器當 n 從 1 增加到 6 時的準確性。為準確性得分制表。估計這些標注器需要的訓練數據,假設詞匯量大小為 10<sup>5</sup>而標記集的大小為 10<sup>2</sup>。
25. ? 獲取另一種語言的一些已標注數據,在其上測試和評估各種標注器。如果這種語言是形態復雜的,或者有詞類的任何字形線索(如),可以考慮為它開發一個正則表達式標注器(排在一元標注器之后,默認標注器之前)。對比同樣的運行在英文數據上的標注器,你的標注器的準確性如何?討論你在運用這些方法到這種語言時遇到的問題。
26. ? [4.1](./ch05.html#code-baseline-tagger)繪制曲線顯示查找標注器的性能隨模型的大小增加的變化。繪制當訓練數據量變化時一元標注器的性能曲線。
27. ? 檢查[5](./ch05.html#sec-n-gram-tagging)中定義的二元標注器`t2`的混淆矩陣,確定簡化的一套或多套標記。定義字典做映射,在簡化的數據上評估標注器。
28. ? 使用簡化的標記集測試標注器(或制作一個你自己的,通過丟棄每個標記名中除第一個字母外所有的字母)。這種標注器需要做的區分更少,但由它獲得的信息也更少。討論你的發現。
29. ? 回顧一個二元標注器訓練過程中遇到生詞,標注句子的其余部分為`None`的例子。一個二元標注器可能只處理了句子的一部分就失敗了,即使句子中沒有包含生詞(即使句子在訓練過程中使用過)。在什么情況下會出現這種情況呢?你可以寫一個程序,找到一些這方面的例子嗎?
30. ? 預處理布朗新聞數據,替換低頻詞為 UNK,但留下標記不變。在這些數據上訓練和評估一個二元標注器。這樣有多少幫助?一元標注器和默認標注器的貢獻是什么?
31. ? 修改[4.1](./ch05.html#code-baseline-tagger)中的程序,通過將`pylab.plot()`替換為`pylab.semilogx()`,在 _x_ 軸上使用對數刻度。關于結果圖形的形狀,你注意到了什么?梯度告訴你什么呢?
32. ? 使用`help(nltk.tag.brill.demo)`閱讀 Brill 標注器演示函數的文檔。通過設置不同的參數值試驗這個標注器。是否有任何訓練時間(語料庫大小)和性能之間的權衡?
33. ? 寫代碼構建一個集合的字典的字典。用它來存儲一套可以跟在具有給定詞性標記的給定詞后面的詞性標記,例如 word<sub>i</sub> → tag<sub>i</sub> → tag<sub>i+1</sub>。
34. ★ 布朗語料庫中有 264 個不同的詞有 3 種可能的標簽。
1. 打印一個表格,一列中是整數 1..10,另一列是語料庫中有 1..10 個不同標記的不同詞的數目。
2. 對有不同的標記數量最多的詞,輸出語料庫中包含這個詞的句子,每個可能的標記一個。
35. ★ 寫一個程序,按照詞 must 后面的詞的標記為它的上下文分類。這樣可以區分 must 的“必須”和“應該”兩種詞意上的用法嗎?
36. ★ 創建一個正則表達式標注器和各種一元以及 n-gram 標注器,包括回退,在布朗語料庫上訓練它們。
1. 創建這些標注器的 3 種不同組合。測試每個組合標注器的準確性。哪種組合效果最好?
2. 嘗試改變訓練語料的規模。它是如何影響你的結果的?
37. ★ 我們標注生詞的方法一直要考慮這個詞的字母(使用`RegexpTagger()`),或完全忽略這個詞,將它標注為一個名詞(使用`nltk.DefaultTagger()`)。這些方法對于有新詞卻不是名詞的文本不會很好。思考句子 I like to blog on Kim's blog。如果 blog 是一個新詞,那么查看前面的標記(`TO`和`NP 即我們需要一個對前面的標記敏感的默認標注器。
1. 創建一種新的一元標注器,查看前一個詞的標記,而忽略當前詞。(做到這一點的最好辦法是修改`UnigramTagger()`的源代碼,需要 Python 中的面向對象編程的知識。
2. 將這個標注器加入到回退標注器序列(包括普通的三元和二元標注器),放在常用默認標注器的前面。
3. 評價這個新的一元標注器的貢獻。
38. ★ 思考[5](./ch05.html#sec-n-gram-tagging)中的代碼,它確定一個三元標注器的準確性上限。回顧 Abney 的關于精確標注的不可能性的討論[(Church, Young, & Bloothooft, 1996)](./bibliography.html#abney1996pst)。解釋為什么正確標注這些例子需要獲取詞和標記以外的其他種類的信息。你如何估計這個問題的規模?
39. ★ 使用`nltk.probability`中的一些估計技術,例如 _Lidstone_ 或 _Laplace_ 估計,開發一種統計標注器,它在訓練中沒有遇到而測試中遇到的上下文中表現優于 n-gram 回退標注器。
40. ★ 檢查 Brill 標注器創建的診斷文件`rules.out`和`errors.out`。通過訪問源代碼(`http://www.nltk.org/code`)獲得演示代碼,創建你自己版本的 Brill 標注器。并根據你從檢查`rules.out`了解到的,刪除一些規則模板。增加一些新的規則模板,這些模板使用那些可能有助于糾正你在`errors.out`看到的錯誤的上下文。
41. ★ 開發一個 n-gram 回退標注器,允許在標注器初始化時指定“anti-n-grams”,如`["the", "the"]`。一個 anti-n-grams 被分配一個數字 0,被用來防止這個 n-gram 回退(如避免估計 P(the | the)而只做 P(the))。
42. ★ 使用布朗語料庫開發標注器時,調查三種不同的方式來定義訓練和測試數據之間的分割:genre (`category`)、source (`fileid`)和句子。比較它們的相對性能,并討論哪種方法最合理。(你可能要使用 n-交叉驗證,在[3](./ch06.html#sec-evaluation)中討論的,以提高評估的準確性。)
43. ★ 開發你自己的`NgramTagger`,從 NLTK 中的類繼承,封裝本章中所述的已標注的訓練和測試數據的詞匯表縮減方法。確保一元和默認回退標注器有機會獲得全部詞匯。
關于本文檔...
UPDATED FOR NLTK 3.0\. 本章來自于 _Natural Language Processing with Python_,[Steven Bird](http://estive.net/), [Ewan Klein](http://homepages.inf.ed.ac.uk/ewan/) 和[Edward Loper](http://ed.loper.org/),Copyright ? 2014 作者所有。本章依據 _Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 United States License_ [[http://creativecommons.org/licenses/by-nc-nd/3.0/us/](http://creativecommons.org/licenses/by-nc-nd/3.0/us/)] 條款,與 _ 自然語言工具包 _ [`http://nltk.org/`] 3.0 版一起發行。
本文檔構建于星期三 2015 年 7 月 1 日 12:30:05 AEST