1. 什么是词汇分类,在自然语言处理中它们是如何使用?
2. 一个好的存储词汇和它们的分类的 Python 数据结构是什么?
3. 我们如何自动标注文本中词汇的词类?
将词汇按它们的词性(parts-of-speech,POS)分类以及相应的标注它们的过程被称为词 性标注(part-of-speech tagging, POS tagging)或干脆简称标注。
词性也称为词类或词汇范畴。用于特定任务的标记的集合被称为一个标记集。
5.1 使用词性标注器
#一个词性标注器(part-of-speech tagger 或 POS tagger)处理一个词序列,为每个词附 加一个词性标记
import nltk
#and 是 CC,并列连词;now 和 completely 是 RB,副词;for 是 IN,介 词;something 是 NN,名词;different 是 JJ,形容词
text = nltk.word_tokenize("And now for something completely different")
print(nltk.pos_tag(text))
#中文不可分
text2 = nltk.word_tokenize("小白是小狗")
print(nltk.pos_tag(text2))
#同形同音异义词
#refuse 和 permit 都以一般现在时动词(VBP)和名词(NN)形式出现, 我们需要知道正在使用哪一个词以便能正确读课文
text = nltk.word_tokenize("They refuse to permit us to obtain the refuse permit")
print(nltk.pos_tag(text))
#为什么要引进词汇类别和词性标注
#这些类别中很多都源于对文本中词的分布的浅层的分析
#text.similar()方法为一个词 w 找出所有上下文 w1ww2,然 后找出所有出现在相同上下文中的词 w',即w1w'w2。
text = nltk.Text(word.lower() for word in nltk.corpus.brown.words())
print('woman' + '=' * 50)
print(text.similar('woman'))
print('bought' + '=' * 50)
print(text.similar('bought'))
print('over' + '=' * 50)
print(text.similar('over'))
print('the' + '=' * 50)
print(text.similar('the'))
#搜索 woman 找到名词;搜索 bought 找到的大部分是动词;搜索 over 一般会找到介词;搜索 the 找到几个限定词。
#一个标注器能够正确识别一个句子的上下文中的这些词的标记。
5.2 标注语料库
表示已标注的标识符
#按照 NLTK 的约定,一个已标注的标识符使用一个由标识符和标记组成的元组来表示。
#我们可以使用函数 str2tuple()从表示一个已标注的标识符的标准字符串创建一个这样的特殊元组:
tagged_token = nltk.tag.str2tuple('fly/NN')
print(tagged_token)
print(tagged_token[0])
print(tagged_token[1])
#直接从一个字符串构造一个已标注的标识符的链表。
#第一步是对字符串分词以 便能访问单独的词/标记字符串,然后将每一个转换成一个元组(使用 str2tuple())
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/PP said/VBD ``/``
ARE/BER well/QL operated/VBN and/CC follow/VB generally/R accepted/VBN practices/NNS which/WDT inure/VB to/IN the/AT best/JJT
interest/NN of/IN both/ABX governments/NNS ''/'' ./.'''
print(sent.split()[:5])
print([nltk.tag.str2tuple(t) for t in sent.split()][:5])
读取已标注的语料库
#NLTK 中包括的若干语料库已标注了词性
#NLTK 中的语料库阅读器提供了一个统一的接口,使你不必理会这些不同的文件格式
#注意:部分词性标记已转换为大写的;自从布朗语料库发布以来,这已成为标准的做法。
print(nltk.corpus.brown.tagged_words())
print(nltk.corpus.brown.tagged_words(tagset = 'universal')) #与原书不一样
#只要语料库包含已标注的文本,NLTK的语料库接口都将有一个 tagged_words()方法
print(nltk.corpus.nps_chat.tagged_words())
print(nltk.corpus.conll2000.tagged_words())
print(nltk.corpus.treebank.tagged_words())
#NLTK 中还有其他几种语言的已标注语料库,包括中文,印地语,葡萄牙语,西班牙语, 荷兰语和加泰罗尼亚语。
print(nltk.corpus.sinica_treebank.tagged_words())
print(nltk.corpus.indian.tagged_words())
print(nltk.corpus.mac_morpho.tagged_words())
print(nltk.corpus.conll2002.tagged_words())
print(nltk.corpus.cess_cat.tagged_words())
简化的词性标记集
已标注的语料库使用许多不同的标记集约定来标注词汇。为了帮助我们开始,我们将看 一看一个简化的标记集
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')
fdist = nltk.FreqDist(tag for (word, tag) in brown_news_tagged)
print(fdist.items())
print(fdist.most_common())
print(list(fdist))
tag = [tag for (tag, freq) in fdist.most_common()]
print(tag)
freq = [freq for (tag, freq) in fdist.most_common()]
print(freq)
#使用 tag_fd.plot(cumulative=True)为上面显示的频率分布绘 图
fdist.plot(cumulative=True)
名词
#名词一般指的是人、地点、事情或概念
#名词可能 出现在限定词和形容词之后,可以是动词的主语或宾语,简化的名词标记对普通名词是 N,如:书,对专有名词是 NP,如苏格兰。
#让我们检查一些已标注的文本,看看哪些词类出现在一个名词前,频率最高的在最前面。
#首先,我们构建一个双连词链表,它的成员是它们自己的词-标记对,
#例如:(('The', 'DET '),('Fulton', 'NOUN'))和(('Fulton', 'NOUN'),('County', 'NOUN'))。
#然后,我们构建了一个双连词的标记部分的 FreqDist。
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)
print([tag for (tag, _) in fdist.most_common()])
动词
#动词是用来描述事件和行动的词,在一个句子中,动词通常表示涉及一个或多个名词短语所指示物的关系。
#新闻文本中最常见的动词是什么?让我们按频率排序所有动词
wsj = nltk.corpus.treebank.tagged_words(tagset='universal')
word_tag_fd = nltk.FreqDist(wsj)
print([(word,tag) for (word, tag) in word_tag_fd.most_common()][:5])
print([wt[0] + '/' + wt[1] for (wt, _) in word_tag_fd.most_common() if wt[1] == 'VERB'][:5])
#频率分布中计算的项目是词-标记对。
#由于词汇和标记是成对的,我们可以把词作作为条件,标记作为事件,使用条件-事件对的链表初始化一个条件频率分布。
#这让我们看到了一个给定的词的标记的频率顺序列表。
cfd1 = nltk.ConditionalFreqDist(wsj)
print(cfd1['yield'].most_common())
print(cfd1['cut'].most_common())
#颠倒配对的顺序,这样标记作为条件,词汇作为事件。现在我们可以看到对于一个给定的标记可能的词。
cfd2 = nltk.ConditionalFreqDist((tag, word) for (word, tag) in wsj)
print(list(cfd2['VERB'])[:5])
形容词和副词
#另外两个重要的词类是形容词和副词。形容词修饰名词,副词修饰动词,副词也可以修饰的形容词.
#英语中还有几个封闭的词类,如介词,冠词(也常称为限定词)(如:the,a),情态动 词(如:shoul d,may),人称代词 (如:she,they)。
#每个词典和语法对这些词的分类都不同。
未简化的标记
#找出最频繁的名词标记的程序
def findtags(tag_prefix, tagged_text):
cfd = nltk.ConditionalFreqDist((tag, word) for (word, tag) in tagged_text
if tag.startswith(tag_prefix))
print(cfd.conditions())
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])
break
探索已标注的语料库
#假设我们正在研究词 often,想看看它是如何在文本中使用的。我们可以试着看看跟在 often 后面的词汇
brown_learned_text = brown.words(categories='learned')
print(brown_learned_text)
print(sorted(set(b for (a, b) in nltk.bigrams(brown_learned_text) if a == 'often'))[:5])
#使用 tagged_words()方法查看跟随词的词性标记可能更有指导性
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()
#使用POS标记寻找三词短语。
from nltk.corpus import brown
def process(sentence):
for (w1,t1), (w2,t2), (w3,t3) in nltk.trigrams(sentence):
if (t1.startswith('V') and t2 == 'TO' and t3.startswith('V')):
print(w1, w2, w3)
for tagged_sent in brown.tagged_sents():
print(process(tagged_sent))
break
#标记关系高度模糊不清的词
#了解为什么要标注这样的词是因为它们各自的上下文可以帮助我们弄清楚标记之间的区别。
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 data.conditions():
if len(data[word]) > 3:
tags = [tag for (tag, _) in data[word].most_common()]
print(word, ' '.join(tags))
5.3 使用 Python 字典映射词及其属性
索引链表 VS 字典
Python 字典
#Python 提供了一个字典数据类型,可用来做任意类型之间的映射。它更像是一个传统 的字典,给你一种高效的方式来查找事物。
#定义 pos为一个空字典,然后给它添加四个项目,指定一些词的词性
pos = {}
print(pos)
pos['colorless'] = 'ADJ'
print(pos)
pos['ideas'] = 'N'
pos['sleep'] = 'V'
pos['furiously'] = 'ADV'
print(pos)
print(pos['ideas'])
#字典不是序列而是映射,它的键并不按固有的顺序。
#要找到键,我们可以将字典转换成一个链表或在需要使用链表的地方使用字典,
#如作为 sorted()的参数或用在 for 循环中。
print(list(pos))
print(sorted(pos))
print([w for w in pos if w.endswith('s')])
#与使用一个 for 循环遍历字典中的所有键一样,我们可以使用 for 循环输出字典的内容
print("使用 for 循环输出字典的内容:")
for word in sorted(pos):
print(word + ":",pos[word])
#字典的方法 keys()、values()和 items()允许我们访问作为单独的链表的键、值以及键-值对
#我们甚至可以按它们的第一个元素排序元组(如果第一个元素相同,就 使用它们的第二个元素)。
print(pos.keys())
print(pos.values())
print(pos.items())
for key, val in sorted(pos.items()):
print(key + ":", val)# “+”无空格,“,”有空格
#字典中只能有'sleep'的一个条目。然而,有一个方法可以在该项目中存储多个值:我们使 用一个链表值,例如:pos['sleep'] = ['N', 'V']。
pos = {'colorless': 'ADJ', 'ideas': 'N', 'sleep': ['V', 'N'], 'furiously': 'ADV'}
print(pos['sleep'])
定义字典
#我们可以使用键-值对格式创建字典。有两种方式做这个,我们通常会使用第一个:
pos = {'colorless': 'ADJ', 'ideas': 'N', 'sleep': 'V', 'furiously': 'ADV'}
pos = dict(colorless='ADJ', ideas='N', sleep='V', furiously='ADV')
#请注意:字典的键必须是不可改变的类型,如字符串和元组。如果我们尝试使用可变键定义字典会得到一个TypeError:
pos = {['ideas', 'blogs', 'adventures']: 'N'}
默认字典
#如果我们试图访问一个不在字典中的键,会得到一个错误。
#然而,如果一个字典能为这个新键自动创建一个条目并给它一个默认值,如 0 或者一个空链表,将是有用的
#一种特殊的称为 defaultdict 的字典已经出现
#为了使用它,我们必须提供一个参数,用来创建默 认值,如:int、float、str 、list 、dict、tuple。
frequency = nltk.defaultdict(int)
frequency['colorless'] = 4
print(frequency['ideas'])
pos = nltk.defaultdict(list)
pos['sleep'] = ['N', 'V']
print(pos['ideas'])
#可以指定 任何我们喜欢的默认值,只要提供可以无参数的被调用产生所需值的函数的名子
pos = nltk.defaultdict(lambda: 'NOUN')
pos['colorful'] = 'ADJ'
print(pos['blog'])
print(list(pos.items()))
#许多语言处理任务— —包括标注——费很大力气来正确处理文本中只出现过一次的词。
#如果有一个固定的词汇和 没有新词会出现的保证,它们会有更好的表现
#我们可以预处理一个文本,在一个默认字典 的帮助下,替换低频词汇为一个特殊的“超出词汇表”标识符,UNK(out of vocabulary)。
#我们需要创建一个默认字典,映射每个词为它们的替换词。最频繁的n个词将被映射到它们自己。其他的被映射到 UNK。
alice = nltk.corpus.gutenberg.words('carroll-alice.txt')
vocab = nltk.FreqDist(alice)
v1000 = [word for [word, _] in vocab.most_common(1000)]
print(v1000[:5])
mapping = nltk.defaultdict(lambda: 'UNK')
for v in v1000:
mapping[v] = v
alice2 = [mapping[v] for v in alice]
print(alice2[:5])
print(len(set(alice2)))
递增地更新字典
#我们可以使用字典计数出现的次数
#首先初始化一 个空的 defaultdict,然后处理文本中每个词性标记。
#如果标记以前没有见过,就默认计数为零。每次我们遇到一个标记,就使用+=运算符递增它的计数
#递增地更新字典,按值排序。
counts = nltk.defaultdict(int)
from nltk.corpus import brown
for (word, tag) in brown.tagged_words(categories='news', tagset='universal'):
counts[tag] += 1
print(counts['NOUN'])
print(sorted(counts))
#按频率递减顺序显示词汇
from operator import itemgetter
#sorted()的第一个参数是要排序的项目,它是由一个 POS 标记和一个频率组成的元组的链表。
#第二个参数使用函数 itemgetter()指定排序键。
#sorted()的最后一个参数指定项目是否应被按相反的顺序返回,即频率值递减。
print(sorted(counts.items(), key=itemgetter(1), reverse=True)[:5])
print([t for t, c in sorted(counts.items(), key=itemgetter(1), reverse=True)])
#一般情况下,itemgetter(n) 返回一个函数,这个函数可以在一些其他序列对象上被调用获得这个序列的第 n 个元素的
pair = ('NP', 8336)
print(pair[1])
print(itemgetter(1)(pair))
复杂的键和值
#研究一个词可能的标记的范围,给 定词本身和它前一个词的标记。
#我们将看到这些信息如何被一个 POS 标注器使用。
from nltk import defaultdict
pos = defaultdict(lambda:defaultdict(int))
brown_news_tagged = brown.tagged_words(categories='news', tagset='universal')
print(brown_news_tagged)
#每次遍历处理一个词-标记对
for ((w1, t1), (w2, t2)) in nltk.bigrams(brown_news_tagged):
#每次通过循 环时,我们更新字典 pos 中的条目 (t1, w2),一个标记和它后面的词
pos[(t1, w2)][t2] += 1
#在 pos 中查找一个项目时,我们必须指定一个复合键,然后得到一个字典对象
print(pos[('DET', 'right')])
#一个 POS 标注 器可以使用这些信息来决定词 right,前面是一个限定词时,应标注为 ADJ。
颠倒字典
#典支持高效查找,只要你想获得任意键的值。如果 d 是一个字典,k 是一个键,输入 d[K],就立即获得值。
#给定一个值查找对应的键要慢一些和麻烦一些
counts = nltk.defaultdict(int)
for word in nltk.corpus.gutenberg.words('milton-paradise.txt'):
counts[word] += 1
print([key for (key, value) in counts.items() if value == 32])
#“反向查找”,建立一个映射值到键的字典
#用键-值对初始化字典 pos
pos = {'colorless': 'ADJ', 'ideas': 'N', 'sleep': 'V', 'furiously': 'ADV'}
pos2 = dict((value, key) for (key, value) in pos.items())
print(pos2['N'])
#首先让我们将我们的词性字典做的更实用些,使用字典的 update()方法加入再一些词 到 pos中,创建多个键具有相同的值的情况。
#这样一来,刚才看到的反向查找技术就将不再起作用
#作为替代,我们不得不使用 append()积累词和每个词性
pos.update({'cats': 'N', 'scratch': 'V', 'peacefully': 'ADV', 'old': 'ADJ'})
pos2 = nltk.defaultdict(list)
for key, value in pos.items():
pos2[value].append(key)
print(pos2['ADV'])
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 的字典
5.4 自动标注
#加载将要使用的数据
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')]
fdist = nltk.FreqDist(tags)
print([fdist])
print(fdist.max())
#创建一个将所有词都标注成 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')
print(default_tagger.tag(tokens))
#不出所料,这种方法的表现相当不好
#在一个典型的语料库中,它只标注正确了八分之一的标识符
print(default_tagger.evaluate(brown_tagged_sents))
正则表达式标注器
#正则表达式标注器基于匹配模式分配标记给标识符
#正则表达式标注器基于匹配模式分配标记给标识符。
#例如:我们可能会猜测任一以 ed 结尾的词都是动词过去分词,任一以's 结尾的词都是名词所有格。可以用一个正则表达式的 列表表示这些:
pattenrs = [(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')]#«.*»是一个全面捕捉的,标注所有词为名词
#这些是顺序处理的,第一个匹配上的会被使用
#建立一个标注器,并用它来标记一个句子
regexp_tagger = nltk.RegexpTagger(pattenrs)
print(regexp_tagger.tag(brown_sents[3]))
print(regexp_tagger.evaluate(brown_tagged_sents))
查询标注器
#很多高频词没有 NN 标记。让我们找出 100 个最频繁的词,存储它们最有可能的标记。
#然后我们可以使用这个信息作为“查找标注器”(NLTK UnigramTagger)的模型:
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)
print(baseline_tagger.evaluate(brown_tagged_sents))
#在一些未标注的输入文本
sent = brown.sents(categories='news')[3]
print(baseline_tagger.tag(sent)[:5])
#许多词都被分配了一个 None 标签,因为它们不在 100 个最频繁的词之中
#我们要先使用查找表,如果它不能指定一个标 记就使用默认标注器,这个过程叫做回退
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
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()
print(display())
#随着模型规模的增长,最初的性能增加迅速,最终达到一个稳定水平,这时模型的规模大量增加性能的提高很小。
评估
#我们对比专家分配的标记来评估一个标注器的性能。
#由于我们通常很难获得专业和公正 的人的判断,所以使用黄金标准测试数据来代替。这是一个已经手动标注并作为自动系统评 估标准而被接受的语料库。
#当标注器对给定词猜测的标记与黄金标准标记相同,标注器被视 为是正确的,。
5.5 N-gram 标注
一元标注( Unigram Tagging)
#一元标注器基于一个简单的统计算法:对每个标识符分配这个独特的标识符最有可能的 标记
#一个一元标 注器的行为就像一个查找标注器(5.4 节),除了有一个更方便的建立它的技术,称为训练。
#在下面的代码例子中,我们训练一个一元标注器,用它来标注一个句子,然后评估
from nltk.corpus import brown
brown_tagged_sents = brown.tagged_sents(categories='news')
print(brown_tagged_sents[0][:5])
brown_sents = brown.sents(categories='news')
print(brown_sents[0][:5])
#训练一个 UnigramTagger,通过在我们初始化标注器时指定已标注的句子数据作为参数
#训练过程中涉及检查每个词的标记,将所有词的最可能的标记存储在一个字典里面,这个字典存储在标注器内部。
unigram_tagger = nltk.UnigramTagger(brown_tagged_sents)
print(unigram_tagger.tag(brown_sents[2007]))
#评估
print(unigram_tagger.evaluate(brown_tagged_sents))
分离训练和测试数据
#分割数据,90%为测试数据,其余 10%为测试数据
size = int(len(brown_tagged_sents) * 0.9)
print(size)
train_sents = brown_tagged_sents[:size]
test_sents = brown_tagged_sents[size:]
unigram_tagger = nltk.UnigramTagger(train_sents)
print(unigram_tagger.evaluate(test_sents))
一般的 N-gram 的标注
#一个 n-gram 标注器是一个 unigram 标注器的一般化,它的上下文是当前词和它前面 n- 1个标识符的词性标记
# 1-gram 标注器是一元标注器(unigram tagger)另一个名称:即用于标注 一个标识符的上下文的只是标识符本身。
#2-gram 标注器也称为二元标注器 (bigram taggers),3-gram 标注器也称为三元标注器(trigram taggers)。
#NgramTagger类使用一个已标注的训练语料库来确定对每个上下文哪个词性标记最 有可能
bigram_tagger = nltk.BigramTagger(train_sents)
print(bigram_tagger.tag(brown_sents[2007])[:5])
print(bigram_tagger.evaluate(test_sents))
#当 n 越大,上下文的特异性就会增加,我们要标注的数据中包含训练数据中不存在的上下文的几率也增大。
#这被称为数据稀疏问题,在 NLP 中是相当普遍的。
#因此,我们的研究结果的精度和覆盖范围之间需要有一个权衡(这与信息检索中的精度/召回权衡有关)。
#N-gram 标注器不应考虑跨越句子边界的上下文。
#因此, NLTK的标注器被 设计用于句子链表,一个句子是一个词链表。
#在一个句子的开始,tn-1 和前 面的标记被设置为 None。
组合标注器
#解决精度和覆盖范围之间的权衡的一个办法是尽可能的使用更精确的算法,但却在很多 时候落后于具有更广覆盖范围的算法。
#例如:我们可以按如下方式组合 bigram 标注器、uni gram 标注器和一个默认标注器:
# 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)
print(t2.evaluate(test_sents))
#定义一个名为 t3 的 TrigramTagger,它是 t2 的回退标注器。
t0 = nltk.DefaultTagger('NN')
t1 = nltk.UnigramTagger(train_sents, backoff=t0)
t2 = nltk.BigramTagger(train_sents, backoff=t1)
t3 = nltk.TrigramTagger(train_sents, backoff=t2)
print(t3.evaluate(test_sents))
#进一步指定一个标注器需要看到一个上下文的多个实例才能保留它
#例如:nl tk.Bigram Tagger(sents, cutoff=2, backoff=t1)将会丢弃那些只看到一次或两次的上下文。
t0 = nltk.DefaultTagger('NN')
t1 = nltk.UnigramTagger(train_sents, backoff=t0)
t2 = nltk.BigramTagger(train_sents, cutoff=2, backoff=t1)
print(t2.evaluate(test_sents))
标注生词
#标注生词的方法仍然是回退到一个正则表达式标注器或一个默认标注器,这些都无 法利用上下文。
#因此,如果我们的标注器遇到词 blog,训练过程中没有看到过,它会分配相同的标记,不论这个词出现的上下文是the blog还是to blog。
#我们怎样才能更好地处理这 些生词,或词汇表以外的项目?
#一个有用的基于上下文标注生词的方法是限制一个标注器的词汇表为最频繁的 n 个词,
存储标注器
#在大语料库上训练一个标注器可能需要大量的时间。
#没有必要在每次我们需要的时候训 练一个标注器,很容易将一个训练好的标注器保存到一个文件以后重复使用
#让我们保存我 们的标注器 t2 到文件 t2.pkl:
from pickle import dump
output = open('t2.pkl', 'wb')
dump(t2, output, -1)
output.close()
#一个单独的 Python 进程中载入我们保存的标注器:
from pickle import load
input = open('t2.pkl', 'rb')
tagger = load(input)
input.close()
#现在,让我们的检查它是否可以用来标注
text = """The board's action shows what free enterprise is up against in our complex maze of regulatory laws ."""
tokens = text.split()
print(tagger.tag(tokens))
性能限制
#一个 n-gram 标注器的性能上限是什么?
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]
print(sum(cfd[c].N() for c in ambiguous_contexts) / cfd.N())
#1/20 的 trigrams 是有歧义的。
#给定当前单词及其前两个标记,根据训练数据,在5%的情况中,有一个以上的标记可能合理地分配给当前词。
#假设我们总是挑选在这种含糊 不清的上下文中最有可能的标记,可以得出 trigram 标注器性能的一个下界。
#调查标注器性能的另一种方法是研究它的错误。
#有些标记可能会比别的更难分配,可能 需要专门对这些数据进行预处理或后处理。
#一个方便的方式查看标注错误是混淆矩阵。它用 图表表示期望的标记(黄金标准)与实际由标注器产生的标记
跨句子边界标注
#句子层面的N-gram标注
brown_tagged_sents = brown.tagged_sents(categories='news')
brown_sents = brown.sents(categories='news')
size = int(len(brown_tagged_sents) * 0.9)
train_sents = brown_tagged_sents[:size]
test_sents = brown_tagged_sents[size:]
t0 = nltk.DefaultTagger('NN')
t1 = nltk.UnigramTagger(train_sents, backoff=t0)
t2 = nltk.BigramTagger(train_sents, backoff=t1)
print(t2.evaluate(test_sents))
5.6 基于转换的标注
#n-gram 标注器的一个潜在的问题是它们的 n-gram 表的大小
#第二个问题是关于上下文的。n-gram 标注器从前面的上下文中获得的唯一的信息是标 记,虽然词本身可能是一个有用的信息源。
#Brill 标注是一种基于转换的学习,以它的发明者命名。一般的想法很简单:猜每个词 的标记,然后返回和修复错误的。
5.7 如何确定一个词的分类
形态学线索
#一个词的内部结构可能为这个词分类提供有用的线索
#英语动词也可以是形态复杂的
句法线索
#一个词可能出现的典型的上下文语境
语义线索
#一个词的意思对其词汇范畴是一个有用的线索
#语义标准巩固了我们对许多词类的直觉,使我们能够在 不熟悉的语言中很好的猜测词的分类。
新词
#所有的语言都学习新的词汇。
#所有这些新词都是名词,这反映在名词被称为开放类。
#相反,介词被认为是一个封闭类。 也就是说,只有有限的词属于这个类别
词性标记集中的形态学
#没有一个“正确的方式”来分配标记,只有根据目标不同或多或 少有用的方法。
5.8 小结
词可以组成类,如名词、动词、形容词以及副词。这些类被称为词汇范畴或者词性。词 性被分配短标签或者标记,如 NN 和 VB。
给文本中的词自动分配词性的过程称为词性标注、POS 标注或只是标注。
自动标注是 NLP 流程中重要的一步,在各种情况下都十分有用,包括预测先前未见过的词的行为、分析语料库中词的使用以及文本到语音转换系统。
一些语言学语料库,如布朗语料库,已经做了词性标注。
有多种标注方法,如默认标注器、正则表达式标注器、unigram 标注器、n-gram 标注器。这些都可以结合一种叫做回退的技术一起使用。
标注器可以使用已标注语料库进行训练和评估。
回退是一个组合模型的方法:当一个较专业的模型(如 bigram 标注器)不能为给定内容分配标记时,我们回退到一个较一般的模型(如 unigram 标注器)
词性标注是 NLP 中一个重要的早期的序列分类任务利用局部上下文语境中的词和标记对序列中任意一点的分类决策。
字典用来映射任意类型之间的信息,如字符串和数字:freq['cat']=12我们使用大括号来创建字典:pos = {},pos = {'furiously': 'adv', 'ideas': 'n', 'colorless':'adj'}。
N-gram 标注器可以定义较大数值的 n,但是当 n 大于 3 时,我们常常会面临数据稀疏问题;即使使用大量的训练数据,我们看到的也只是可能的上下文的一小部分。
基于转换的标注学习一系列“改变标记 s 为标记 t 在上下文 c 中”形式的修复规则每个规则会修复错误,也可能引入(较小的)错误。