任务描述
本关任务:根据所学有关统计分词的知识,完成基于 HMM 的中文分词算法程序的编写并通过所有测试用例。
相关知识
为了完成本关任务,你需要掌握:
-
统计分词的思想;
-
基于 HMM 的分词方法。
统计分词简介
1、主要思想
把每个词看做是由词的最小单位的各个字组成的,如果相连的字在不同的文本中出现的次数越多,就证明这相连的字很可能就是一个词。
因此我们就可以利用字与字相邻出现的频率来反应成词的可靠度,统计语料中相邻共现的各个字的组合的频度,当组合频度高于某一个临界值时,我们便可认为此字组可能会构成一个词语。
2、方法步骤
基于统计的分词,一般要做如下两步操作:
-
建立统计语言模型;
-
对句子进行单词划分,然后对划分结果进行概率计算,获得概率最大的分词。这里就用到了统计学习算法,如隐含马尔可夫( HMM )、条件随机场( CRF )等。
什么是语言模型
语言模型在信息检索、机器翻译、语音识别中承担着重要的任务。这种模型结构简单,直接,但同时也因为数据缺乏而必须采取平滑算法。这里主要介绍 n 元语言模型( n-gram )。
假设 S 表示长度为 i 由(W1,W2,....,Wi)字序列组成的句子,则 S 的概率为:
P(S)=P(W1,W2,...,Wi)=P(W1)∗P(W2∣W1)∗P(W3∣W2,W1)....P(Wi∣W1,W2,...,Wi−1)
即每个字的出现都与它之前出现过的字有关,最后整个句子 S 的概率为这些字概率的乘积。但是这个计算量很大,所以在这里我们可以利用马尔科夫假设,即当前词只与最多前n-1
个有限的词相关:
当 n=1 时,即出现在第i位上的词Wi独立于历史词时,一元文法被记作 uni-gram ,一元语言模型可以记作:
P(W1,W2,...,Wm)=∏i=1mP(Wi)
当 n=2 时,即出现在第i位上的词Wi仅与它前面的一个历史词Wi−1有关,二元文法模型被称为一阶马尔可夫链( Markov chain ),记作 bi-gram ,二元语言模型可以记作:
P(W1,W2,...,Wm)=∏i=1mP(Wi∣Wi−1)
当 n=3 时,即出现在第i位置上的词Wi仅与它前面的两个历史词Wi−2和Wi−1有关,三元文法模型被称为二阶马尔可夫链,记作 tri-gram ,三元语言模型可以记作:
P(W1,W2,...,Wm)=∏i=1mP(Wi∣Wi−2∗Wi−1)
在实际应用中,一般使用频率计数的比例来计算 n 元条件概率。
基于 HMM 的分词法
在统计分词中,隐含马尔可夫模型( HMM )是我们常用的模型,它主要通过将分词作为字在字串中的序列标注任务来实现,它的基本思路是:每个字在构造一个特定的词语时都占据着一个确定的构词位置(即词位),规定每个字最多有四个构词位置: B (词首)、 M (词中)、 E (词尾)、 S (单独成词)。
如:
中文/分词/是/文本处理/不可或缺/的/一步/!
标注后的形式为:
中/B 文/E 分/B 词/E 是/S 文/B 本/M 处/M 理/E 不/B 可/M 或/M 缺/E 的/S 一/B 步/E !/S
其中,词位序列代表着 HMM 中不可见的隐藏状态序列,而训练集中的文本则为可见的观测序列。这样就变成了已知观测序列,求未知的隐藏序列的 HMM 问题。
基于 HMM 进行分词共包含以下步骤:
-
使用已经分好词的训练集去训练 HMM 模型,计算频数得到 HMM 的三要素(初始状态概率,状态转移概率和发射概率);
-
使用 Viterbi 算法以及训练好的三个概率矩阵,将待分词的句子转换为 BMES 类型的状态序列;
-
根据已经求出的状态序列,划分句子进行分词;
-
最后输出结果。
编程要求
在右侧编辑器中的 Begin-End 之间补充 Python 代码,完成基于 HMM 的统计分词算法,对所输入的文本进行统计分词,并输出分词结果。其中文本内容通过 input 从后台获取。
测试说明
平台将使用测试集运行你编写的程序代码,若全部的运行结果正确,则通关。
测试输入:
研究生命的起源
预期输出:
model train done,parameters save to hmm_model.pkl # 接口附加信息,表明模型训练完成
model parameters load done!
研究/生命/的/起源
参考资料
【1】基于统计的分词方法
参考代码:
class HMM(object):
def __init__(self):
self.state_list = ['B','M','E','S']
self.start_p = {}
self.trans_p = {}
self.emit_p = {}
self.model_file = 'hmm_model.pkl'
self.trained = False
def train(self,datas,model_path=None):
if model_path == None:
model_path = self.model_file
#统计状态频数
state_dict = {}
def init_parameters():
for state in self.state_list:
self.start_p[state] = 0.0
self.trans_p[state] = {s:0.0 for s in self.state_list}
self.emit_p[state] = {}
state_dict[state] = 0
def make_label(text):
out_text = []
if len(text) == 1:
out_text = ['S']
else :
out_text += ['B']+['M']*(len(text)-2)+['E']
return out_text
init_parameters()
line_nb = 0
#监督学习方法求解参数
for line in datas:
line = line.strip()
if not line:
continue
line_nb += 1
word_list = [w for w in line if w != ' ']
line_list = line.split()
line_state = []
for w in line_list:
line_state.extend(make_label(w))
assert len(line_state) == len(word_list)
for i,v in enumerate(line_state):
state_dict[v] += 1
if i == 0:
self.start_p[v] += 1
else :
self.trans_p[line_state[i-1]][v] += 1
self.emit_p[line_state[i]][word_list[i]] = self.emit_p[line_state[i]].get(word_list[i],0)+1.0
self.start_p = {k: v*1.0/line_nb for k,v in self.start_p.items()}
self.trans_p = {k:{k1: v1/state_dict[k1] for k1,v1 in v0.items()} for k,v0 in self.trans_p.items()}
self.emit_p = {k:{k1: (v1+1)/state_dict.get(k1,1.0) for k1,v1 in v0.items()} for k,v0 in self.emit_p.items()}
with open(model_path,'wb') as f:
import pickle
pickle.dump(self.start_p,f)
pickle.dump(self.trans_p,f)
pickle.dump(self.emit_p,f)
self.trained = True
print('model train done,parameters save to ',model_path)
#读取参数模型
def load_model(self,path):
import pickle
with open(path,'rb') as f:
self.start_p = pickle.load(f)
self.trans_p = pickle.load(f)
self.emit_p = pickle.load(f)
self.trained = True
print('model parameters load done!')
#维特比算法求解最优路径
def __viterbi(self,text,states,start_p,trans_p,emit_p):
V = [{}]
path = {}
for y in states:
V[0][y] = start_p[y]*emit_p[y].get(text[0],1.0)
path[y] = [y]
for t in range(1,len(text)):
V.append({})
new_path = {}
for y in states:
emitp = emit_p[y].get(text[t],1.0)
(prob , state) = max([(V[t - 1][y0] * trans_p[y0].get(y, 0) * emitp, y0) \
for y0 in states if V[t - 1][y0] > 0])
V[t][y] = prob
new_path[y] = path[state]+[y]
path = new_path
if emit_p['M'].get(text[-1],0) > emit_p['S'].get(text[-1],0):
(prob,state) = max([(V[len(text)-1][y],y) for y in ('E',"M")])
else :
(prob,state) = max([(V[len(text)-1][y],y) for y in states])
return (prob,path[state])
def cut(self,text):
if not self.trained:
print('Error:please pre train or load model parameters')
return
prob,pos_list = self.__viterbi(text,self.state_list,self.start_p,self.trans_p,self.emit_p)
begin_,next_ = 0,0
#任务:完成 HMM 中文分词算法
# ********* Begin *********#
for i, char in enumerate(text):
pos = pos_list[i]
if pos == 'B':
begin_ = i
elif pos == 'E':
yield text[begin_:i+1]
next_ = i+1
elif pos == 'S':
yield char
next_ = i+1
if next_ < len(text):
yield text[next_:]
# ********* Begin *********#
if __name__ == '__main__':
text = input()
train_data = 'pku_training.utf8'
model_file = 'hmm_model.pkl'
hmm = HMM()
hmm.train(open(train_data, 'r', encoding='utf-8'), model_file)
hmm.load_model(model_file)
print('/'.join(hmm.cut(text)))