理论课:C2W2.Part-of-Speech (POS) Tagging and Hidden Markov Models
文章目录
理论课: C2W2.Part-of-Speech (POS) Tagging and Hidden Markov Models
Working with text files, Creating a Vocabulary and Handling Unknown Words
先导入包
import string
from collections import defaultdict
Read Text Data
文件WSJ_02-21.pos
中提供了《Wall Street Journal(WSJ)》的标记数据集,常用于词性标注任务。。
要读取该文件,可以使用 Python 的上下文管理器,使用 with
关键字并指定要读取的文件名。要将文件内容保存到内存中,您需要使用 readlines()
方法,并将其返回值保存到变量中。Python 的上下文管理器不需要显式地关闭与文件的连接:
# Read lines from 'WSJ_02-21.pos' file and save them into the 'lines' variable
with open("./data/WSJ_02-21.pos", 'r') as f:
lines = f.readlines()
打印部分内容
# Print columns for reference
print("\t\tWord", "\tTag\n")
# Print first five lines of the dataset
for i in range(5):
print(f'line number {i+1}: {lines[i]}')
结果:
数据集中的每一行都有一个单词和相应的标签。不过,由于打印时使用的是格式化字符串,因此可以推断出单词和标签之间用制表符(或空格)分隔,每行末尾有一个换行符(注意每行之间有一个空格)。
词性标记可以点这里
为了更好地理解数据集中的信息结构,可打印未格式化的版本:
# Print first line (unformatted)
lines[0]
结果:
‘In\tIN\n’
可以看到单词和标签之间是tab分隔符,每行最后是换行符。
Creating a vocabulary
词汇表由数据集中至少出现过 2 次的每个单词组成。大概步骤如下:
- 仅从数据集中获取单词
- 使用默认字典计算每个词出现的次数
- 过滤字典,只包含出现至少 2 次的单词
- 用过滤后的 dict 创建一个列表
- 对列表进行排序
在步骤 1 中,可以利用每个单词和标签都用制表符分隔以及单词总是排在前面的数据排列。利用列表推导式创建单词列表:
# Get the words from each line in the dataset
words = [line.split('\t')[0] for line in lines]
利用 defaultdict 可以轻松完成第 2 步。defaultdict 是一种特殊的字典,如果试图访问一个不存在的键,它会返回该类型的 “零 ”值。为获取单词的频率,可用 int 类型定义 defaultdict。这样字典中不存在单词的情况会返回一个零。
# Define defaultdict of type 'int'
freq = defaultdict(int)
# Count frequency of ocurrence for each word in the dataset
for word in words:
freq[word] += 1
再次使用列表推导式过滤freq
字典得到词表,还要注意去掉换行符
# Create the vocabulary by filtering the 'freq' dictionary
vocab = [k for k, v in freq.items() if (v > 1 and k != '\n')]
最后对词表进行排序,这里不需要重新赋值。
# Sort the vocabulary
vocab.sort()
# Print some random values of the vocabulary
for i in range(4000, 4005):
print(vocab[i])
结果:
Early
Earnings
Earth
Earthquake
East
做到这里可以将处理好的词表写入到一个文件中,这里就不扩展了。
Processing new text sources
Dealing with unknown words
有了词表后,就可以用其来处理其他文本。当然其他文本中通常会出现当前词表中未曾收录的词汇。要解决这个问题,可以简单粗暴地将每个新词归类为未知词,但也可以创建一个函数,尝试对每个未知词的类型进行分类,并为其分配一个相应的未知标记。
思考:简单粗暴地将每个新词归类为未知词有什么优缺点?从理论课笔记中可以找到答案。
- 检查未知单词是否包含任何数字字符
- 返回--unk_digit--
- 检查未知单词是否包含任何标点符号
- 返回--unk_punct--
- 检查未知单词是否包含任何大写字符
- 返回--unk_upper--
- 检查未知单词是否以后缀结尾,后缀可能表示它是名词、动词、形容词或副词
- 分别返回--unk_noun--
、--unk_verb--
、--unk_adj--
、--unk_adv--
。
如果一个词不满足以上任何条件,那么可标记为一个普通的 --unk--
。条件的评估顺序与此处列出的顺序相同。因此,如果一个单词包含一个标点符号但不包含数字,它将属于第二个条件。为了实现这一功能,可以使用一些 if/elif 语句和提前返回语句。
def assign_unk(word):
"""
Assign tokens to unknown words
"""
# Punctuation characters
# Try printing them out in a new cell!
punct = set(string.punctuation)
# Suffixes
noun_suffix = ["action", "age", "ance", "cy", "dom", "ee", "ence", "er", "hood", "ion", "ism", "ist", "ity", "ling", "ment", "ness", "or", "ry", "scape", "ship", "ty"]
verb_suffix = ["ate", "ify", "ise", "ize"]
adj_suffix = ["able", "ese", "ful", "i", "ian", "ible", "ic", "ish", "ive", "less", "ly", "ous"]
adv_suffix = ["ward", "wards", "wise"]
# Loop the characters in the word, check if any is a digit
if any(char.isdigit() for char in word):
return "--unk_digit--"
# Loop the characters in the word, check if any is a punctuation character
elif any(char in punct for char in word):
return "--unk_punct--"
# Loop the characters in the word, check if any is an upper case character
elif any(char.isupper() for char in word):
return "--unk_upper--"
# Check if word ends with any noun suffix
elif any(word.endswith(suffix) for suffix in noun_suffix):
return "--unk_noun--"
# Check if word ends with any verb suffix
elif any(word.endswith(suffix) for suffix in verb_suffix):
return "--unk_verb--"
# Check if word ends with any adjective suffix
elif any(word.endswith(suffix) for suffix in adj_suffix):
return "--unk_adj--"
# Check if word ends with any adverb suffix
elif any(word.endswith(suffix) for suffix in adv_suffix):
return "--unk_adv--"
# If none of the previous criteria is met, return plain unknown
return "--unk--"
在Python中,any() 函数用于检查可迭代对象(如列表、元组、集合等)中是否至少有一个元素为 True。如果至少有一个元素为 True,则 any() 函数返回 True;否则返回 False。例如:
any(char.isdigit() for char in word)
:检查单词中的字符是否至少有一个是数字。
通过使用 any() 函数,代码变得更加简洁和高效,同时减少了手动编写循环的需要。
Getting the correct tag for a word
接下来实现get_word_tag
函数,为特定单词获取正确的标签,并对未知单词进行特殊处理。由于数据集提供了同一行中的每个单词和标签,而单词是否为已知取决于所使用的词汇,因此这两个元素应成为该函数的参数。
该函数应检查一行是否为空,如果是,则应返回一个占位词和标签,分别为–n–和–s–。
如果不是,则应处理该行以返回正确的单词和标记对,如果单词未知,则应使用函数 assign_unk()标记未知单词具体未知类型。
split() 方法用于将字符串分割成子字符串,并根据指定的分隔符将字符串分割成多个部分。这里没有指定分隔符,因此会使用空白字符(包括空格、制表符、换行符等)作为分隔符。
def get_word_tag(line, vocab):
# If line is empty return placeholders for word and tag
if not line.split():
word = "--n--"
tag = "--s--"
else:
# Split line to separate word and tag
word, tag = line.split()
# Check if word is not in vocabulary
if word not in vocab:
# Handle unknown word
word = assign_unk(word)
return word, tag
下面给出不同条件的测试结果:
get_word_tag('\n', vocab)
结果:(‘–n–’, ‘–s–’)
get_word_tag('In\tIN\n', vocab)
结果:(‘In’, ‘IN’)
get_word_tag('tardigrade\tNN\n', vocab)
结果:(‘–unk–’, ‘NN’)
get_word_tag('scrutinize\tVB\n', vocab)
结果:(‘–unk_verb–’, ‘VB’)
Working with tags and Numpy
下面是LAB第二部分:
先导入包
import numpy as np
import pandas as pd
Some information on tags
这次实验我们将创建一个toy实例,因此只使用三个词性标记。
# Define tags for Adverb, Noun and To (the preposition) , respectively
tags = ['RB', 'NN', 'TO']
接下来构建两个字典,其中一个字典是 transition_counts
,它统计特定标记与另一标记相邻出现的次数。这个字典的键的形式是(previous_tag, tag)
,值是出现的频率。另一个字典是 emission_counts
字典,用于统计特定标签对(dtag, word)
在训练数据集中出现的次数。
一般来说,词性标签到词性标签属于 “transition”,词性标签到词语属于 “emission”。这里先关注前者:
# Define 'transition_counts' dictionary
# Note: values are the same as the ones in the assignment
transition_counts = {
('NN', 'NN'): 16241,
('RB', 'RB'): 2263,
('TO', 'TO'): 2,
('NN', 'TO'): 5256,
('RB', 'TO'): 855,
('TO', 'NN'): 734,
('NN', 'RB'): 2431,
('RB', 'NN'): 358,
('TO', 'RB'): 200
}
3 个词性标签有 9 种组合,每个标签都可以出现在同一个标签之后。
Using Numpy for matrix creation
词性标签之间的transition矩阵可以用以下代码创建:
# Store the number of tags in the 'num_tags' variable
num_tags = len(tags)
# Initialize a 3X3 numpy array with zeros
transition_matrix = np.zeros((num_tags, num_tags))
# Print matrix
transition_matrix
结果:
array([[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.]])
矩阵大小也和预计的一样:
# Print shape of the matrix
transition_matrix.shape
结果:
(3, 3)
在使用transition_counts
填充矩阵之前,先对标签进行排序
# Create sorted version of the tag's list
sorted_tags = sorted(tags)
# Print sorted list
sorted_tags
结果:
[‘NN’, ‘RB’, ‘TO’]
这里使用嵌套循环来完成填充,高手可以直接用itertools.product写一行
# Loop rows
for i in range(num_tags):
# Loop columns
for j in range(num_tags):
# Define tag pair
tag_tuple = (sorted_tags[i], sorted_tags[j])
# Get frequency from transition_counts dict and assign to (i, j) position in the matrix
transition_matrix[i, j] = transition_counts.get(tag_tuple)
# Print matrix
transition_matrix
结果:
array([[1.6241e+04, 2.4310e+03, 5.2560e+03],
[3.5800e+02, 2.2630e+03, 8.5500e+02],
[7.3400e+02, 2.0000e+02, 2.0000e+00]])
虽然能行,但是由于 Numpy 更注重效率,因此矩阵显示不太直观,换成df就好多了:
# Define 'print_matrix' function
def print_matrix(matrix):
print(pd.DataFrame(matrix, index=sorted_tags, columns=sorted_tags))
# Print the 'transition_matrix' by calling the 'print_matrix' function
print_matrix(transition_matrix)
结果:
注意这个矩阵是非对称的。
Working with Numpy for matrix manipulation
接下来看transition矩阵的操作。Python的Numpy对矩阵提供了很多便利的操作,可以让我们不使用循环就能完成矩阵的缩放、求和等操作:
# Scale transition matrix
transition_matrix = transition_matrix/10
# Print scaled matrix
print_matrix(transition_matrix)
结果:
当然,也可以使用
v
a
l
u
e
s
u
m
o
f
r
o
w
\frac{value}{sum \,of \,row}
sumofrowvalue来归一化矩阵的值。
# Compute sum of row for each row
rows_sum = transition_matrix.sum(axis=1, keepdims=True)
# Print sum of rows
rows_sum
结果:
array([[2392.8],
[ 347.6],
[ 93.6]])
axis:
axis
这个参数指定了沿哪个轴进行求和。在多维数组中,axis=0 表示沿着第一个轴(通常是行)进行求和,axis=1 表示沿着第二个轴(通常是列)进行求和。
keepdims
这个参数控制求和操作后结果的形状。如果设置为 True,则在执行求和操作后,结果数组的维度与原始数组保持一致,只是被求和的轴的维度被减少到1。如果将 keepdims 设置为 False,结果将是一个一维数组:[ 2392.8 347.6 93.6]
# Normalize transition matrix
transition_matrix = transition_matrix / rows_sum
# Print normalized matrix
print_matrix(transition_matrix)
以上是归一化操作,结果为:
检查一下:
transition_matrix.sum(axis=1, keepdims=True)
结果:
array([[1.],
[1.],
[1.]])
最后修改矩阵对角线的每个值,使它们等于当前行加当前值之和的对数。
import math
# Copy transition matrix for for-loop example
t_matrix_for = np.copy(transition_matrix)
# Copy transition matrix for numpy functions example
t_matrix_np = np.copy(transition_matrix)
# Loop values in the diagonal
#for i in range(num_tags):
# t_matrix_for[i, i] = t_matrix_for[i, i] + math.log(rows_sum[i])
for i in range(num_tags):
# 确保 rows_sum[i] 是一个标量
row_sum_scalar = rows_sum[i] # 假设 rows_sum[i] 已经是一个标量
# 如果 rows_sum[i] 是一个数组,你需要先将其转换为标量
# 例如,如果 rows_sum[i] 是一个一维数组,你可以使用 rows_sum[i][0] 来获取第一个元素
# 然后对它进行对数运算
t_matrix_for[i, i] += math.log(row_sum_scalar)
# Print matrix
print_matrix(t_matrix_for)
结果:
下面用另外一种方法实现相同功能:
# Save diagonal in a numpy array
d = np.diag(t_matrix_np)
# Print shape of diagonal
d.shape
结果:(3,)
为了要和rows_sum
矩阵进行相加,要进行reshape
# Reshape diagonal numpy array
d = np.reshape(d, (3,1))
# Print shape of diagonal
d.shape
使用 Numpy 的 vectorize() 函数对数组的每个元素应用函数。该函数返回一个接受 numpy 数组作为参数的矢量化函数。
要更新原始矩阵,可以使用 Numpy 的 fill_diagonal() 函数。
# Perform the vectorized operation
d = d + np.vectorize(math.log)(rows_sum)
# Use numpy's 'fill_diagonal' function to update the diagonal
np.fill_diagonal(t_matrix_np, d)
# Print the matrix
print_matrix(t_matrix_np)
结果:
可以看到,两种方法得到的结果是一样的。
# Check for equality
t_matrix_for == t_matrix_np