本文探讨以不同的方式给文本自动添加词性标记。
首先加载要使用的数据。
import nltk
from nltk.corpus import brown
brown_tagged_sents = brown.tagged_sents(categories='news')
brown_sents = brown.sents(categories='news')
1、默认标注器
最简单的标注器是为每个标识符分配同样的标记。这似乎是一个相对普通的方法,但为标注器的性能建立了一个重要的标准。为了得到最好的效果,我们用最有可能的标记标注每个词。通过下例找出哪个标记是最有可能的。
tags = [tag for (word, tag) in brown.tagged_words(categories='news')]
tag = nltk.FreqDist(tags).max
>>>'NN'
我们现在可以创建一个将所有词都标注为NN的标注器。
raw = 'I do not like green egg and ham, I do not like them Sam I am!'
tokens = nltk.word_tokenize(raw)
default_tagger = nltk.DefaultTagger('NN')
default_tagger.tag(tokens)
不出所料,这种方法的表现相当不好。在一个典型的语料库中,它只正确标注了八分之一的标识符,如下列所示:
default_tagger.evaluate(brown_tagged_sents)
# 0.13089484257215028
默认的标注器给每个单独的词分配标记,即使是之前从未遇到过的词。碰巧的是,在处理几千词的英文文本时发现,大多数新名词都是名词。这意味着,默认标注器可以帮助我们提高语言处理系统的稳定性。
2、正则表达式标注器
正则表达式标注器基于匹配模式分配标记给标识符。例如,一般情况下认为任一以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 (deafult)
]
这些是按顺序处理的,第一个匹配上的会被使用。现在建立一个标注器,并用它来标注句子。
regexp_tagger = nltk.RegexpTagger(patterns)
regexp_tagger.tag(brown_sents[3])
regexp_tagger.evaluate(brown_tagged_sents)
# 0.20326391789486245
我们发现有大约五分之一是正确的。
3、查询标注器
很多高频词没有NN标记,我们找出100个最频繁的词,存储它们最有可能的标记,然后我们可以使用这个信息作为“查询标注器(NLTKUnigramTagger)”的模型,如下例:
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.005171350717027666 ?? in book it should be 0.45
此处结果与书中不同,书中结果为0.45左右,即仅仅知道100个最频繁的词的标记就能正确标注很大一部分标识符。
来看看它在未标注的输入文本是运行得怎么样:
sent = brown.sents(categories='news')[3]
baseline_tagger.tag(sent) # many words are tagged 'None',because they do not belong to 100th most used words
下面是结果:
[(u'``', None),
(u'Only', None),
(u'a', None),
(u'relative', None),
(u'handful', None),
(u'of', None),
(u'such', None),
(u'reports', u'NNS'),
(u'was', None),
(u'received', None),
(u"''", None),
(u',', None),
(u'the', None),
(u'jury', None),
(u'said', None),
(u',', None),
(u'``', None),
(u'considering', None),
(u'the', None),
(u'widespread', None),
(u'interest', None),
(u'in', None),
(u'the', None),
(u'election', None),
(u',', None),
(u'the', None),
(u'number', None),
(u'of', None),
(u'voters', None),
(u'and', None),
(u'the', None),
(u'size', None),
(u'of', None),
(u'this', None),
(u'city', None),
(u"''", None),
(u'.', None)]
可以看到很多词都被分配了'None'标签,因为它们不在100个最频繁的词中。这种情况我们想分配默认标记NN。也就是说,我们应先使用查找表,如果不能指定就使用默认标注器,这个过程叫“回退”。
baseline_tagger = nltk.UnigramTagger(model=likely_tags,backoff=nltk.DefaultTagger('NN'))
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
words_by_freq = list(nltk.FreqDist(brown.words(categories='news')))
cfd = nltk.ConditionalFreqDist(brown.tagged_words(categories='news'))
sizes = 2 ** pylab.arange(16)
prefs = [performance(cfd,words_by_freq[:size]) for size in sizes]
pylab.plot(sizes,prefs,'-bo')
pylab.title('Lookup Tagger Performance with Varying Model Size')
pylab.xlabel('Model Size')
pylab.ylabel('Performace')
pylab.show()
display()
可以看到随着模型规模的增长,最初性能增加较快,最终达到稳定水平,这时哪怕模型规模再增加,性能提升幅度也很小。