基于隐马尔可夫的中文分词方法
序列标注
基于字典的方法实现简单,而且部署方便。但是其有一个缺点那就是不能识别 [未登录词],也就是不能准确切分字典中不包含的词。这也使其在面对新词出现的词时束手无策。
为解决未登录词问题,有研究学者提出了基于统计的分词方法。在基于统计的方法中,将中文分词问题转化为一个序列标注问题。定义一个词开始的字被标记为 B ;词中间的字标记为 M ;词结尾的字标记为 E ;如果一个字为一个词,则标记该字为 S 。
为了解释这个概念,我们来看一个下图所示的例子。将 【自然语言处理】 标记为 【BMMMME】 ,将 【是】 标记为 【S】,以此类推。
image.png
通过这种转换,我们将分词问题转换为标注问题。也就是说,给定一个句子,只需要标注出该句子中的每一个字属于哪一个标签即可。然而标注过程可以借助许多机器学习方法或深度学习方法来完成。一般标注的流程如下:
image.png
这种方法巧妙的避开了未登录词的问题。因为其标注的对象为字,不存在未登录词的问题。接下来,介绍常用的统计方法。
基于统计的切分方法
在自然语言处理的发展历史上,总与机器学习学科的发展息息相关。而机器学习学科近年来发展迅猛,尤其是深度学习方向,似乎有 “大一统” 的趋势。
因此,这里将基于统计的切分方法主要划分为两大类,分别是常规机器学习方法和“新起之秀”的深度学习方法。常规机器学习方法也是目前使用的得最多的中文分词方法,分词速度相对较快,准确率也很高。
而深度学习方法则是近几年学术界较为流行的算法。这主要得益于计算机计算速度的提升,使得使用大规模语料训练成为可能。
image.png
如果你之前没有学习过机器学习或深度学习,可能会对这些术语比较陌生。这里也没办法对每一种方法进行深入的讲解。机器学习方法可以去看李航老师写的 [《统计学习方法》]和周志华老师编写的 [《机器学习》]。而深度学习方法可以去看深度学习学界的三位资深研究者联合编写的 [《深度学习》]。
基于隐马尔可夫模型的切分方法
你可能还记得,在上一个实验中使用到了 jieba 工具。而 jieba.cut
方法包含三个输入参数。
字符串参数: 需要分词的字符串
cut_all 参数: 用来控制是否采用全模式
HMM 参数: 用来控制是否使用 [HMM]模型
隐马尔可夫模型原理
隐马尔可夫模型是比较经典的机器学习模型,它在语言识别,自然语言处理,模式识别等领域都得到广泛的应用。其结构图如下所示:
image.png
image.png
为了便于理解,这里举一个例子来说明。
假设我们有一个大箱子。里面装着两个颜色不同的小箱子。而每个小箱子里又都包含三种水果,分别是:苹果、橘子和香蕉。如下图所示。
image.png
要是我们每次从大箱子中有放回的拿取小箱子则会得到一个状态序列。例如,下面这个序列。
image.png
image.png
image.png
在上面假设的状态转移矩阵中,可以看到红色转移到红色的概率为 P( 红色 | 红色 )=0.4;绿色转移到红色的概率 P( 红色 | 绿色 )=0.6。
而第一个时刻的状态因为没有前一个时刻的状态来转移,所以需要定义一个初始状态矩阵π 来表示。例如下图所示。
image.png
在上面假设的初始状态矩阵中,可以看到第一个状态Y1为红色的概率 P( 红色 )=0.4,为绿色的概率 P( 绿色 )=0.6。要是每次直接从大箱子中拿取水果,则会得到下面这样一个序列。
image.png
image.png
image.png
在上面的假设中,隐藏状态为红色时生成观测状态为苹果的概率为 P( 苹果 | 红色 )=0.4。而隐藏状态为绿色时生成观测状态为苹果的概率为 P( 苹果 | 红色 )=0.6。
总的来说,隐马尔可夫模型主要由三个矩阵来确定,分别是初始状态矩阵π 和转移状态矩阵 A ,以及发射矩阵B 。用公式来表达如下:
image.png
也就是说,这三个矩阵定义了一个隐马尔可夫模型。隐马尔可夫模型的训练过程就是求出这三个矩阵。
隐马尔可夫模型的学习
上面主要使用一个小例子来说明隐马尔可夫的模型的原理,现在来看看隐马尔可夫是如何使用在中文分词中。
前面我们有讲到,中文分词可以转化为序列标注问题。也就是定义一个规则:一个词的第一个字标记为 B ,中间字标记为 M ,结尾字标记为 E ,单字成词标记为 S。通过这个规则来将句子转化为相应的标注。
在隐马尔可夫模型中,将句子标注的序列看作是隐藏状态序列,将原始的句子看成是观测序列,如下:
image.png
因为隐藏状态序列只能取 B、M、E、S 这四个字母。因此,隐藏状态空间 Q={ B,M,E,S }。 而观测序列为由每个汉字组成的句子,因此,观测状态空间 V 主要为训练集中出现的汉字总数。
前面我们提到隐藏状态序列是未知的,也正是我们所要求的结果。但是在训练数据中,状态序列已经由人工进行标记出。训练数据中每一个句子都有一个标注序列。因此,通过对训练数据进行统计就可以得出隐马尔可夫模型的三个概率矩阵。
现在通过一个实验来进行说明。实验数据选用的是北京大学标注的人民日报上的数据。先执行下面代码下载数据。
!wget -nc https://labfile.oss-cn-hangzhou.aliyuncs.com/courses/1329/pku_training.txt
读取数据。
# 创建一个空列表来存放数据
sentence_list = []
with open("pku_training.txt", 'rb') as f:
# 每次读取一行数据
line = f.readline()
while line:
line = line.decode("gb18030", "ignore")
# 每一行为一个段落,按句号将段落切分成句子
sentence = line.split('。')
sentence_list.extend(sentence)
line = f.readline()
# 打印出数据的前 5 句
for i in range(5):
print(sentence_list[i])
print('-'*100)
从上面的输出结果可看出,该数据集的标记方式是在词与词之间加一个空格键。而隐马尔可夫模型需要的是句子本身和句子所对应的标注。所以需要将数据转化成为一个句子对应一个标注的形式。先定义一个将词转化为标注的函数。
def get_tag(word):
tags = [] # 创建一个空列表用来存放标注数据
word_len = len(word)
if word_len == 1: # 如果是单字成词,标记为 S
tags = ['S']
elif word_len == 2: # 如果该词仅有两个字,则标记为 B 和 E
tags = ['B', 'E']
else:
tags.append('B') # 第一个字标记为 B
tags.extend(['M']*(len(word)-2)) # 中间标记为 M ,
tags.append('E') # 最后一个标记为 E
return tags
# 测试标注函数
get_tag('蓝桥云课')
定义完标注函数之后,接着定义一个数据预处理函数用于一次性标注完所有的数据。
def pre_data(data):
X = [] # 创建一个空列表来存放每个中文句子
y = [] # 创建一个空列表来存放每个句子标注结果
word_dict = [] # 创建一个空列表来存放每个句子的正确分词结果
for sentence in data:
sentence = sentence.strip()
if not sentence:
continue
# 将句子按空格进行切分,得到词
words = sentence.split(" ")
word_dict.append(words)
sent = [] # 用于临时存放一个中文句子
tags = [] # 用于临时存放一个句子对应的标注
for word in words:
sent.extend(list(word))
tags.extend(get_tag(word)) # 获得标注结果
X.append(sent)
y.append(tags)
return X, y, word_dict
和上一个实验一样,将数据集划分为训练集和测试集。使用最后 60 份数据用于测试。
train_data = sentence_list[:-60]
test_data = sentence_list[-60:]
完成数据预处理,并打印出处理结果。
train_X, train_y, train_word_dict = pre_data(train_data)
test_X, test_y, test_word_dict = pre_data(test_data)
print(train_X[0])
print('-'*100)
print(train_y[0])
print('-'*100)
print(train_word_dict[0])
print('='*100)
print(test_X[0])
print('-'*100)
print(test_y[0])
print('-'*100)
print(test_word_dict[0])
上面主要完成了数据的预处理,接下来用训练数据集来训练隐马尔可夫模型。前面我们已经讲到,隐马尔可夫的训练过程就是统计出其初始状态矩阵π 和转移状态矩阵A ,以及发射矩阵B 。现在我们先定义一个函数来统一初始化这三个概率矩阵。
states = {'B', 'M', 'E', 'S'}
def para_init():
init_mat = {} # 初始状态矩阵
emit_mat = {} # 发射矩阵
tran_mat = {} # 转移状态矩阵
state_count = {} # 用于统计每个隐藏状态(即 B,M,E,S)出现的次数
for state in states:
tran_mat[state] = {}
for state1 in states:
tran_mat[state][state1] = 0.0 # 初始化转移状态矩阵
emit_mat[state] = {} # 初始化发射矩阵
init_mat[state] = 0.0 # 初始化初始状态矩阵
state_count[state] = 0.0 # 初始化状态计数变量
return init_mat, emit_mat, tran_mat, state_count
查看一下三个矩阵初始化的结果。
import pandas as pd
init_mat, emit_mat, tran_mat, state_count = para_init()
print(pd.DataFrame(init_mat, index=['init']))
print('-'*100)
print(pd.DataFrame(tran_mat).T)
print('-'*100)
print((pd.DataFrame(emit_mat)).T)
从上面的结果可知,已经成功对三个矩阵进行初始化,发射矩阵为空的原因是,其需要观测数据才能统计。接下来对三个矩阵进行统计,这里通过定义一个统计函数来完成。
def count(train_X, train_y):
"""
train_X: 中文句子
train_Y: 句子对应的标注
"""
# 初始化三个矩阵
init_mat, emit_mat, tran_mat, state_count = para_init()
sent_count = 0
for j in range(len(train_X)):
# 每次取一个句子进行统计
sentence = train_X[j]
sent_state = train_y[j]
for i in range(len(sent_state)):
if i == 0:
# 统计每个状态(即 B,M,E,S)在每个句子对应的标注序列中第一个位置的次数
init_mat[sent_state[i]] += 1
# 统计每个隐藏状态(即 B,M,E,S)在整个训练样本中出现的次数
state_count[sent_state[i]] += 1
# 统计有多少个句子。
sent_count += 1
else:
# 统计两个相邻时刻的不同状态组合同时出现的次数
tran_mat[sent_state[i-1]][sent_state[i]] += 1
state_count[sent_state[i]] += 1
# 统计每个状态对应于每个文字的次数
if sentence[i] not in emit_mat[sent_state[i]]:
emit_mat[sent_state[i]][sentence[i]] = 1
else:
emit_mat[sent_state[i]][sentence[i]] += 1
return init_mat, emit_mat, tran_mat, state_count, sent_count
下面使用前面所定义的统计函数对训练数据进行统计。并打印出统计结果。
init_mat, emit_mat, tran_mat, state_count, sent_count = count(train_X, train_y)
print(pd.DataFrame(init_mat, index=['init']))
print('-'*100)
print(pd.DataFrame(tran_mat).T)
print('-'*100)
# 随机取 6 个观测字
print((pd.DataFrame(emit_mat)).iloc[94:100, :].T)
从上面的统计结果可以看到,在初始状态矩阵π 中,M 和 E 的统计结果为 0 ,这意味着训练语料中,没有一个句子的第一个字被标记为 M 或 E。想一想这也是符合事实的,因为按照我们的标注规则,每个句子的第一个字只能被标记为 B 或 S。
同理,在状态转移矩阵A 中,从 M 转移到 B 和 S 的统计结果为 0。这也是符合事实的,因为按照我们的标注规则,M 后面只能是 M 或 E。其他统计结果为 0 的情况也类似。
在发射矩阵B 中,有些统计结果为 NaN ,这是因为这里为了方便观察,将三个矩阵的输出结果转换为 [Pandas] 的 [DataFrame]格式数据。因为有些情况没有统计结果,例如训练语料中 【乖】字没有被标记为 M 或 S。因此 Pandas 默认将这些没有统计出来的设置 NaN。在这里也就是 0 的意思。
上面所完成的只是统计出了三个矩阵,而我们所要求的是三个概率矩阵,因此定义一个函数将其转换为概率矩阵。
def get_prob(init_mat, emit_mat, tran_mat, state_count, sent_count):
tran_prob_mat = {} # 状态转移矩阵
emit_prob_mat = {} # 发射矩阵
init_prob_mat = {} # 初始状态矩阵
# 计算初始状态矩阵
for state in init_mat:
init_prob_mat[state] = float(
init_mat[state]/sent_count)
# 计算状态转移矩阵
for state in tran_mat:
tran_prob_mat[state] = {}
for state1 in tran_mat[state]:
tran_prob_mat[state][state1] = float(
tran_mat[state][state1]/state_count[state])
# 计算发射矩阵
for state in emit_mat:
emit_prob_mat[state] = {}
for word in emit_mat[state]:
emit_prob_mat[state][word] = float(
emit_mat[state][word]/state_count[state])
return tran_prob_mat, emit_prob_mat, init_prob_mat
使用定义的函数将三个计数矩阵转换成为概率矩阵,并打印出结果。
tran_prob_mat, emit_prob_mat, init_prob_mat = get_prob(
init_mat, emit_mat, tran_mat, state_count, sent_count)
print(pd.DataFrame(init_prob_mat, index=['init']))
print('-'*100)
print(pd.DataFrame(tran_prob_mat).T)
print('-'*100)
print((pd.DataFrame(emit_prob_mat)).iloc[94:100, :].T)
上面我们通过对训练语料进行统计,得到了隐马尔可夫模型的初始状态矩阵π 和转移状态矩阵A ,以及发射矩阵B 。到此为止,我们已经成功构建出了隐马尔可夫模型。
维特比算法
你可能会有疑问,如何使用构建好的隐马尔可夫模型进行分词标注呢?在前面中也讲解到,使用隐马尔可夫模型进行分词,就是给定一个句子,然后通过隐马尔可夫模型求出该句子对应的标注,然后使用标注进行分词。
image.png
为了更加形象的解释这个问题,使用一个小例子来进行说明。假设要预测的句子为 【明天要下雨】。则寻找最佳序列标注的过程如下图所示。
image.png
image.png
print(pd.DataFrame(init_prob_mat, index=['init']))
print('-'*100)
print(pd.DataFrame(tran_prob_mat).T)
print('-'*100)
print((pd.DataFrame(emit_prob_mat)).loc[['明', '天', '要', '下', '雨'], :].T)
image.png
现在我们将上面所计算得到的结果用图形来表示则得到下图:
image.png
由上图可知,我们所要寻找的最佳序列标注就是寻找一条最佳路径的问题,在这个网络中,每个箭头都代表一个概率。而寻找最佳路径就是找到概率最大的路径。我们接着来考虑最初的问题,请看下图,这里为了讲解方便,下图中箭头上的概率值是人为假定的。而不是使用上面两个公式计算得出。
image.png
上图所示的红色箭头所连接的五个状态值,即为我们所要的状态序列。我们可以搜索所有的路径,然后选择一个概率最大的状态序列进行输出。但这种方法有个明显的缺点,就是其需要巨大的计算开销。在本例中需要计算4^5 次。但如果一个句子有 50 个字,则需要计算4^50 次才能得到最优的路径。这显然是不现实的。
为解决这一问题,有学者提出使用 [维特比] 算法来求解。而维特比算法主要是利用动态规划的思想来解决隐马尔可夫模型的预测问题。
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
维特比算法虽然原理简单,但还是有点不容易理解。如果想了解更多,可以去看吴军老师编写的 [《数学之美》]。
下面我们使用维特比算法来定义一个预测函数。
def predict(sentence, tran_prob_mat, emit_prob_mat, init_prob_mat):
tab = [{}] # 用于存放对应节点的 𝛿 值
path = {} # 用于存放对应节点所经过的最优路径
# 求出 T0 时刻的 𝛿 值
for state in states:
tab[0][state] = init_prob_mat.get(
state) * emit_prob_mat[state].get(sentence[0], 0.000000001)
path[state] = [state]
for t in range(1, len(sentence)):
tab.append({}) # 创建一个元组来存放 𝛿 值和对应的节点
new_path = {}
for state1 in states:
# state1 为后一个时刻的状态
items = []
for state2 in states:
# state2 为前一个时刻的状态
if tab[t - 1][state2] == 0:
continue
# 计算上一个时刻状态 state2 到当前时刻的状态 state1 的转移概率值
tr_prob = tran_prob_mat[state2].get(
state1, 0.000000001) * emit_prob_mat[state1].get(sentence[t], 0.0000001)
# 计算当前的状态为 state1 时经过上一个时刻状态 state2 的概率值
prob = tab[t - 1][state2] * tr_prob
items.append((prob, state2))
if not items:
items.append((0.000000001, 'S'))
# 求出某个时刻的每个状态节点对应的最优路径
best = max(items) # best: (prob, state)
tab[t][state1] = best[0]
new_path[state1] = path[best[1]] + [state1]
path = new_path
# 寻找最后一个时刻最大的 𝛿 值以及所对应的节点(即状态)。
prob, state = max([(tab[len(sentence) - 1][state], state)
for state in states])
return path[state] # 返回最大 𝛿 值节点所对应的路径
使用定义的函数来进行预测刚才所举的例子。
sentence = "明天要下雨"
tag = predict(sentence, tran_prob_mat, emit_prob_mat, init_prob_mat)
' '.join(tag)
显然,标注的结果为 【B E S B E】。但这并不算我们想要的最终结果,我们想要的结果是将句子切分成词。所以要将标注转化为分词的形式。下面定义一个转化函数。
def cut_sent(sentence, tags):
"""
sentence:句子
tags:标注
"""
word_list = [] # 存放切分结果
start = -1
started = False
if len(tags) != len(sentence):
return None
if tags[-1] not in {'S', 'E'}:
if tags[-2] in {'S', 'E'}: # 如果最后一个没有标记为 'S', 'E',并且倒数
tags[-1] = 'S' # 第二个标记为 'S','E'则将最后一个标记为 'S'
else: # 如果最后一个没有标记为 'S', 'E',并且倒数
tags[-1] = 'E' # 第二个标记为 'B','M'则将最后一个标记为 'E'
for i in range(len(tags)):
if tags[i] == 'S':
if started:
started = False
word_list.append(''.join(sentence[start:i]))
word_list.append(sentence[i])
elif tags[i] == 'B':
if started:
word_list.append(''.join(sentence[start:i]))
start = i
started = True
elif tags[i] == 'E':
started = False
word = sentence[start:i + 1]
word_list.append(''.join(word))
elif tags[i] == 'M':
continue
return word_list
使用定义的转换函数来切分句子。
result = cut_sent(sentence, tag)
' | '.join(result)
先定义一个函数来将预测和转化一次完成,以便于后续的测试。
def word_seg(sentence, tran_prob_mat, emit_prob_mat, init_prob_mat):
tags = predict(sentence, tran_prob_mat, emit_prob_mat, init_prob_mat)
result = cut_sent(sentence, tags)
return result
# 测试定义的函数
result = word_seg(sentence, tran_prob_mat, emit_prob_mat, init_prob_mat)
' | '.join(result)
定义一个准确率计算函数来计算隐马尔可夫模型分词的准确率。评价标准与上一个实验一致。
def accurency(y_pre, y):
"""
分词准确率计算函数
y_pre:预测结果
y: 正确结果
"""
count = 0
n = len(y_pre)
for i in range(len(y_pre)):
# 统计每个句子切分出来的词数
n += len(y_pre[i])
for word in y_pre[i]:
# 统计每个句子切词正确的词数
if word in y[i]:
count += 1
return count/n
使用测试集对所构建的隐马尔可夫模型进行测试。
from tqdm.notebook import tqdm
word_cut_result = [] # 创建一个空列表来存放分词结果
# 每次切一个句子
for sent in tqdm(test_X):
# 使用前面所构建的分词器进行切词
temp = word_seg(sent, tran_prob_mat, emit_prob_mat, init_prob_mat)
# 存放分词后的数据
word_cut_result.append(temp)
# 计算准确率
acc = accurency(word_cut_result, test_word_dict)
acc
从实验结果可以看到,该模型的切分准确率为 75 %。这个结果并不算理想。但我们在实现隐马尔可夫模型时,很多细节没有考虑,例如计算溢出问题。
此外,从实验结果还可以知道,隐马尔可夫模型的分词速度要比基于字典的速度要快很多。上一节实验中,基于字典的分词方法切分同样的测试集要花费差不多 5 分钟,而本节实现的隐马尔可夫模型则需要不到 5 秒钟。
下面打印出测试集的切分结果并与标准切分进行对比。
print(' | '.join(word_cut_result[0]))
print('-'*100)
print(' | '.join(test_word_dict[0]))
我们现在来看看未登录词问题,上一节实验中,基于字典的方法并不能准确切分 【甲状腺激素是一种激素】 这句话。现在使用隐马尔可夫模型来试一试。
sentence = '甲状腺激素是一种激素'
result = word_seg(sentence, tran_prob_mat, emit_prob_mat, init_prob_mat)
' | '.join(result)
从切分结果看来,隐马尔可夫模型仅把 【甲状腺】 这个词切错。其他词都切分正确。这也说明了隐马尔可夫模型确实能够解决未登录词问题。
实现维特比算法
挑战介绍
在上一个实验中,我们讲解了基于隐马尔可夫的中文分词方法。在讲解到隐马尔可夫模型的预测问题时,提到了维特比算法。并对其进行了详细的阐述。维特比算法是信息学科中的一个基本算法,它诞生于上个世纪 60 年代。至今任然在通信领域发挥着巨大的作用。而本次挑战的主角就是维特比算法。
维特比算法在上一个实验中已经进行了详细的说明。其基本思想是假设最优路径通过某个节点,那么从开始到该节点的路径一定是最优的。
假设我们有如下图的一个篱笆网络。网络上的边表示节点之间的距离。
image.png
而本次的挑战就是要求出上图中 A 到 E 的最优路径,即红色箭头路径。这里需要注意的是最优路径指的是最短的路径,这与上一节所讲的概率最大路径正好相反。我们现在再来简单回顾一下维特比算法,在上图中,我们可以定义两个变量来存放最优路径值以及最优路径,分别为 tab=[{}] 和 path={}。维特比实现如下:
首先来求第一个时刻 T1 的节点:
tab[T1]['A1']=2 ; path=['A1']=['A1']
tab[T1]['A2']=4 ; path=['A2']=['A2']
tab[T1]['A3']=1 ; path=['A3']=['A3']
然后用维特比算法来求出走到第二个时刻 T2 的最优路径。先来计算走到 T2 时刻的 B1 节点所经过 T1 时刻的最优节点。
tab[T2]['B1']=tab[T1]['A1']+6=2+6=8
tab[T2]['B1']=tab[T1]['A2']+7=4+7=11
tab[T2]['B1']=tab[T1]['A3']+2=1+2=3
从上面的计算可以看出,经过节点 A3 路径最短。所以存放该路径 path[‘B1’]=path[‘A3’]+‘B1’=[‘A3’,‘B1’] ,同理求出 T2 时刻的其他节点。
tab[T2]['B1']=tab[T1]['A3']+2=1+2=3 ; path['B1']=path['A3']+'B1'=['A3','B1']
tab[T2]['B2']=tab[T1]['A3']+6=1+6=7 ; path['B2']=path['A3']+'B1'=['A3','B2']
tab[T2]['B3']=tab[T1]['A1']+4=2+4=6 ; path['B3']=path['A1']+'B1'=['A1','B3']
现在来求到 T3 时刻 C1 节点的最优路径。即计算走到 C1 要经过 T2 时刻的哪一个节点最好。
tab[T3]['C1']=tab[T2]['B1']+3=3+3=6
tab[T3]['C1']=tab[T2]['B2']+6=7+6=13
tab[T3]['C1']=tab[T2]['B3']+6=6+6=12
从上面的计算可以看出,经过节点 B1 路径最短。所以存放该路径 path[‘C1’]=path[‘B1’]+‘C1’=[‘A3’,‘B1’]+‘C1’=[‘A3’,‘B1’,‘C1’] ,同理求出 T3 时刻的其他节点。
tab[T3]['C1']=tab[T2]['B1']+3=3+3=6 ; path['C1']=path['B1']+'C1'=['A3','B1']+'C1'=['A3','B1','C1']
tab[T3]['C2']=tab[T2]['B3']+1=6+1=7 ; path['C2']=path['B3']+'C2'=['A1','B3']+'C2'=['A1','B3','C2']
tab[T3]['C3']=tab[T2]['B1']+2=3+2=5 ; path['C3']=path['B1']+'C3'=['A3','B1']+'C3'=['A3','B1','C3']
然后重复上面的过程,最后可以得到 tab[T5][E1],tab[T5][E2],tab[T5][E3]
的值以及对应路径 path['E1'],path['E2'],path['E3']
。然后同样的方法,选择最优的节点,即选择最小的 tab[T5][Ei]
。然后得到对应的节点 Ei
。再通过 path['Ei']
得到最优的路径,也即是我们所要的最终结果。
如果你对维特比算法,还没有理解。可以回去看上一个实验的内容。或是去看李航老师的 [《统计学习方法》] 里的隐马尔可夫章节或吴军老师写的[《数学之美》]。
挑战内容
在本次挑战中,你需要在 ~/Code/decode.py 文件中编写一个函数 viterbi,viterbi 函数接受一个参数,就是存放节点以及节点之间距离的一个包含字典的列表。该函数返回一个最优路径结果,返回结果用 list 的形式存放。
挑战要求
代码必须写入 ~/Code/decode.py 文件中。
函数名必须是 viterbi 。
测试时请使用 /home/shiyanlou/anaconda3/bin/python 运行 decode.py ,避免出现无相应模块的情况。