5 分类和标注词汇
- Categorizing and Tagging Words(分类和标注词汇)
英文文档 http://www.nltk.org/book/
中文文档 https://www.bookstack.cn/read/nlp-py-2e-zh/0.md
以下编号按个人习惯
Categorizing and Tagging Words(分类和标注词汇)
1 Using a Tagger(使用标注器)
词性标注器处理一个单词序列,为每个词附加一个词性标记。
nltk中提供了标注器pos_tag(),函数参数为词汇列表。
text = nltk.word_tokenize("And now for something completely different")
tag_result = nltk.pos_tag(text)
# 词性情况。cc-并列连词,RB-副词,IN-介词,NN-名词,JJ-形容词
print(tag_result)
2 Tagged Corpora(已经被标记的语料库)
首先使用元组,例如(词符,标记),来表示一个已标注的词符。str2tuple()函数将一个已标注的词符的字符串,例如 词汇/标记,转换成元组。示例如下:
tagged_token = nltk.tag.str2tuple('fly/NN')
print(tagged_token) # ('fly', 'NN')
print(tagged_token[0]) # fly
print(tagged_token[1]) # NN
除了可以从一个短字符串构造出元组,还可以将一个长字符串构造成已标注的词符的列表。需要遍历长字符串。代码如下:
# 三引号可以换行写代码
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 ''/'' ./.
'''
# split()不带参数,会把所有的空格(空格符,制表符,换行符)当做分隔符
tagged_token_1 = [nltk.tag.str2tuple(string) for string in sent.split()]
print(tagged_token_1)
3 Reading Tagged Corpora(读取已标注的语料库)
只要语料库中包含了已标注的文本,可以使用nltk语料库提供的tagged_words()方法得到。
# 读取已标注的语料库
tagged_words_1 = nltk.corpus.brown.tagged_words()
print(tagged_words_1)
# 由于并非所有的语料库都采用同一组标记,因此可以指定tagset参数为universal来获取以通用词性标记的词汇列表
tagged_words_2 = nltk.corpus.brown.tagged_words(tagset='universal')
print(tagged_words_2)
4 Unsimplified Tags(未简化的标记)
dict(字典):dict是一个可变的数据类型,格式为{key:value,key:value},dict的key必须是不可变的数据类型,且value的数据类型任意。注意键值对若是字符串用单引号。
5 Mapping Words to Properties Using Python Dictionaries(使用python字典映射单词到属性)
此章节主要是使用字典。字典中的key不能重复,且key是不可变的类型。
首先基本的建立字典,其中有两种方法来定义一个字典(第一个常用)。
# 使用键值对格式来创建一个字典
pos = {'colorless': 'ADJ', 'ideas': 'N', 'sleep': 'V', 'furiously': 'ADV'}
pos = dict(colorless='ADJ', ideas="N", sleep="V", furiously="ADV")
向其中填充键值对
# 填充键值对
pos = {}
print(pos) # {}
pos['colorless'] = 'ADJ'
print(pos) # {'colorless': 'ADJ'}
pos['ideas'] = 'N'
pos['sleep'] = 'V'
print(pos) # {'colorless': 'ADJ', 'ideas': 'N', 'sleep': 'V'}
字典中检索是通过键来检索的
# 使用键来检索字典
value_1 = pos['ideas']
print(value_1) # N
# 当我们检索一个没有分配值的键,就会抛红
print(pos['green']) # KeyError: 'green'
上述代码中我们试图访问一个不在字典中的键,会得到一个错误。然而,如果希望一个字典能为这个新键自动创建一个条目并给它一个默认值,如 0 或者一个空链表或者我们自定义的值,这也是可以实现的。
# 定义一个默认字典,且默认值的类型是int
frequency = defaultdict(int)
frequency['colorless'] = 9
print(frequency['ideas']) # 0
# 定义一个默认字典,value类型为list
pos = defaultdict(list)
pos['sleep'] = ['NOUN', 'VERB']
# 自动创建的新键对应的value默认为[]
print(pos['ideas']) # []
print(pos['sleep']) # ['NOUN', 'VERB']
# 当我们想让默认值是我们自定义的值时,需要提供一个无参有返回值的函数。
pos1 = defaultdict(lambda: 'DEFAULT_VALUE')
print(pos1['blog']) # DEFAULT_VALUE
找到字典中的键,可以通过将字典转换为列表。有三种方法如下:
# 将一个字典转换为list,得到的list是字典中key的列表
pos_key_list = list(pos)
# 对一个字典进行排序,得到的结果是个列表,并且列表中的内容是key的有序列表
pos_key_sorted_list = sorted(pos)
# 针对字典的for循环,都是对key的遍历。字典for循环的结果是个列表
pos_loop_list = [w for w in pos if w.endswith('s')]
学习下字典中的方法keys(),values(),items()。另外可以使用sorted(pos.items())对其进行排序,按它们的第一个元素排序(如果第一个元素相同,就使用它们的第二个元素)
# pos.keys()获得单独的键列表
print(pos.keys()) # dict_keys(['colorless', 'ideas', 'sleep'])
print(list(pos.keys())) # ['colorless', 'ideas', 'sleep']
# 获得单独的值列表
print(pos.values()) # dict_values(['ADJ', 'N', 'V'])
# 获得键值对列表
print(pos.items()) # dict_items([('colorless', 'ADJ'), ('ideas', 'N'), ('sleep', 'V')])
由于在nlp中,将相同字母组成的单词积累到一起,是很常用的任务。因此nltk提供了更方便的实现方式,如下:
# 相同字母组成的单词积累到一起。使用字典
def anagram_dictionary():
anagrams = defaultdict(list)
words = nltk.corpus.words.words('en')
for word in words:
# key为单词中字母的有序排列,增序
key = ''.join(sorted(word))
anagrams[key].append(word)
# ------------------------------------------------
# 上述几行代码等价于下一行代码。nltk提供了一个更方便的方式
anagrams_1 = nltk.Index((''.join(sorted(word)), word) for word in words)
return anagrams
反转字典的三个方式,及其反转结果。
tagged_words_1 = nltk.corpus.brown.tagged_words(categories='news', tagset='universal')
# 这种反转的方式,会将value值相同的项覆盖,只留下一个
tagged_words_2 = dict((value, key) for (key, value) in tagged_words_1)
# 以下方式会将相同value的key连成list
tagged_words_3 = defaultdict(list)
for key, value in tagged_words_1:
tagged_words_3[value].append(key)
# nltk的索引,可以实现反转。其实就是将相同的key的值,积累到一起
tagged_words_4 = nltk.Index((value, key) for (key, value) in tagged_words_1)
print(tagged_words_2['NOUN'])
print(tagged_words_3['NOUN'])
print(tagged_words_4['NOUN'])
6 Automatic Tagging(自动标注)
6.1 The Default Tagger(默认标注器)
默认标注器可以帮助我们提高语言处理系统的稳定性
# 创建一个将所有词都标注成tag的标注器
def default_tagger_TAG(tag):
raw = 'I do not like green eggs and ham, I do not like them Sam I am!'
tokens = nltk.word_tokenize(raw)
# 定义一个默认的标注器。标注内容为tag
default_tagger = nltk.DefaultTagger(tag)
# 标注一个词汇列表
tokens_tagged = default_tagger.tag(tokens)
print(tokens_tagged)
return default_tagger
若想知道定义的默认标注器正确标注了多少词汇,可以使用evaluate()方法,如下:
get_default_tagger = default_tagger_TAG(max_tag)
# 将一句话中的词按上下文的内容意思来进行标注
brown_tagged_sents = nltk.corpus.brown.tagged_sents(categories='news')
right = get_default_tagger.evaluate(brown_tagged_sents)
print(right) # 0.13089484257215028
6.2 The Regular Expression Tagger(正则表达式标注器)
正则表达式标注器基于匹配模式分配标记给词。例如,我们可能会猜测任一以 ed 结尾的词都是动词过去分词,任一以’s 结尾的词都是名词所有格。
# 正则表达式标注器
def regular_expression_tagger():
patterns = [(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')]
brown_sents = nltk.corpus.brown.sents(categories='news')
brown_tagged_sents = nltk.corpus.brown.tagged_sents(categories='news')
# 创建一个正则表达式标注器。匹配规则按patterns
regexp_tagger = nltk.RegexpTagger(patterns)
# 标注
brown_sents_tag = regexp_tagger.tag(brown_sents[3])
print(brown_sents_tag)
# 计算标注正确情况
right = regexp_tagger.evaluate(brown_tagged_sents)
print(right) # 0.20186168625812995
6.3 The Lookup Tagger(查询标注器)
# 查询标注器
def lookup_tagger():
brown_words = nltk.corpus.brown.words(categories='news')
brown_sents = nltk.corpus.brown.sents(categories='news')
brown_tagged_words = nltk.corpus.brown.tagged_words(categories='news')
brown_tagged_sents = nltk.corpus.brown.tagged_sents(categories='news')
fd = nltk.FreqDist(brown_words)
cfd = nltk.ConditionalFreqDist(brown_tagged_words)
most_freq_words = fd.most_common(100)
# 最可能的标记,即最频繁的100个词的标记
likely_tags = dict((word, cfd[word].max()) for (word, _) in most_freq_words)
# backoff回退,用来指定默认标注器。当使用model指定的标记不能给一个词分配标记时,就使用默认标注器
baseline_tagger = nltk.UnigramTagger(model=likely_tags,backoff=nltk.DefaultTagger('NN'))
right = baseline_tagger.evaluate(brown_tagged_sents)
print(right) # 0.5817769556656125
print(baseline_tagger.tag(brown_sents[3]))
查询标注器随着模型规模的增长,最初的性能增加迅速,最终达到一个稳定水平,此时模型的规模大量增加时,性能提高很小。
7 N-Gram Tagging(n-gram标注)
7.1 Unigram Tagging(一元标注器)
一元标注器需要通过训练,然后才能用来标注。一元标注器依据指定的已标注的句子,检查每个词的标记,统计出每个词最可能的标记,并将这些存储在标注器内部的一个字典中。一元标注器的行为类似查找标注器。
# 一元标注器。行为类似查找标注器
def unigram_tagging():
# 训练一个一元标注器,通过指定的已标注的句子数据作为参数。
# 在这个训练过程中,统计了每个词的标记,将所有词的最可能的标记存储在一个字典中,这个过程由nltk提供的初始化函数完成
unigram_tagger = nltk.UnigramTagger(brown_tagged_sents)
# 依据标注器内部训练好的字典,进行标注
tagged_sent = unigram_tagger.tag(brown_sents[2007])
print(tagged_sent)
right = unigram_tagger.evaluate(brown_tagged_sents)
print(right)
为保证标注器的质量,最好将数据集分为训练集和测试集,在初始化标注器使用训练集,评估标注器使用测试集。
# 90%训练,10%测试
size = int(len(brown_tagged_sents) * 0.9)
train_sents = brown_tagged_sents[:size]
test_sents = brown_tagged_sents[size:]
7.2 General N-Gram Tagging(一般的n-gram标注)
n-gram标注器考虑第n个词和前n-1个词的标记,从而给出第n个词最可能的标记。如下,即一直wn,t1,t2,t3…tn-1,求tn
当n越大,上下文的特异性就增加,要进行标注的数据中,不在训练数据中的几率增大,即数据稀疏问题。
以二元标注器为例,代码如下:
# 一般的n-gram标注
def general_nGram_tagging():
# 以下测试二元标注器。此标注器能够标注训练中看到过的句子中的所有词,但对没见过的句子表现差。即遇到新词无法分配标记
# 因为n-gram标注器,根据第n个词和前n-1个词的标记,来求得第n个词的标记。必须要条件匹配,才能确定第n个词的标记
bigram_tagger = nltk.BigramTagger(train_sents)
tag_sents = bigram_tagger.tag(brown_sents[2007])
print(tag_sents)
# 测试不在训练数据中的数据的标注情况
unseen_sent = brown_sents[4203]
tag_sent_2 = bigram_tagger.tag(unseen_sent)
print(tag_sent_2)
right = bigram_tagger.evaluate(test_sents)
print(right) # 0.10206319146815508
7.3 Combining Taggers(组合标注器)
解决精度和覆盖范围之间的权衡的办法就是尽可能的使用更精确的算法。可以组合二元标注器、一元注器和一个默认标注器。代码如下:
# 组合标注器
def combining_taggers():
# 以下组合了二元标注器、一元标注器、默认标注器
# 首先尝试使用二元标注器标注标识符。
# 如果二元标注器无法找到一个标记,尝试一元标注器。
# 如果一元标注器也无法找到一个标记,使用默认标注器。
tag_0 = nltk.DefaultTagger('NN')
tag_1 = nltk.UnigramTagger(train_sents, backoff=tag_0)
# 为保持二元标注器尽可能的小,指定了cutoff的值,那么标注器会丢弃那些出现次数小于等于cutoff的实例。
tag_2 = nltk.BigramTagger(train_sents, cutoff=2, backoff=tag_1)
right = tag_2.evaluate(test_sents)
print(right) # 0.8424200139539519
7.4 Storing Taggers(存储标注器)
将一个训练好的标注器保存到一个文件中,以便以后能重复使用
保存标注器代码如下:
# 存储标注器。将一个训练好的标注器保存到一个文件中,以便以后能重复使用
def store_tagger():
tag_0 = nltk.DefaultTagger('NN')
tag_1 = nltk.UnigramTagger(train_sents, backoff=tag_0)
tag_2 = nltk.BigramTagger(train_sents, backoff=tag_1)
from pickle import dump
output = open('tag_2.pkl', 'wb')
dump(tag_2, output, -1)
output.close()
载入标注器代码如下:
# 载入标注器
def get_tagger():
from pickle import load
input = open('tag_2.pkl', 'rb')
tagger = load(input)
input.close()
return tagger
8 Transformation-Based Tagging(基于转换的标注)
Brill标注是一种归纳标注方法,基于转换的学习。不计数观察结果,只编制一个转换修正规则列表。Brill想法是以大笔画开始,然后修复细节,一点点的细致的改变。
规则类似于:Replace NN with VB when the previous word is TO;