本章关注的问题:
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。
形容词和副词
形容词修饰名词,副词修饰动词。
我们可以使用键-值对格式创建字典。有两种方式做这个,我们通常会使用第一个:
>>> 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 "" , 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'))
一元标注器基于一个简单的统计算法:对每个标识符分配这个标识符最有可能的标记。我们训练一个一元标注器,用它来标注一个句子,然后评估:
>>> 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 标注器是一个 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。
另一个信息来源是一个词可能出现的典型的上下文语境。
最后,一个词的意思对其词汇范畴是一个有用的线索。