自制基于HMM的中文分词器
来源:互联网 发布:caxa数控车编程软件 编辑:程序博客网 时间:2024/05/14 22:48
不像英文那样单词之间有空格作为天然的分界线, 中文词语之间没有明显界限。必须采用一些方法将中文语句划分为单词序列才能进一步处理, 这一划分步骤即是所谓的中文分词。
主流中文分词方法包括基于规则的分词,基于大规模语料库的统计学习方法以及在实践中应用最多的规则与统计综合方法。
隐马尔科夫模型(HMM)是中文分词中一类常用的统计模型, 本文将使用该模型构造分词器。关于HMM模型的介绍可以参见隐式马尔科夫模型.
方法介绍
中文分词问题可以表示为一个序列标注问题,定义两个类别:
E代表词语中最后一个字
B代表词的首个字
M代表词中间的字
S代表单字成词
对于分词结果:"我/只是/做了/一些/微小/的/工作",可以标注为"我E只B是E做B了E一B些E微B小E的S工B作E".
将标记序列"EBEBEBEBESBE"作为状态序列, 原始文本"我只是做了一些微小的工作"为观测序列. 分词过程即变成了求使给定观测序列出现概率最大的状态序列, 即解码问题。
这里需要说明一下,所谓出现概率最大是指在自然语言中出现概率最大。
根据语料库是否标注, HMM可以进行监督学习和无监督学习。若语料库未标注则需要使用EM算法进行无监督学习, 若语料库已经标注那么可以使用频率估计概率即监督学习方法。
程序实现
定义数据结构
上文中已定义用于标注序列的标签, 也即HMM中的状态:
STATES = {'B', 'M', 'E', 'S'}
定义HMMSegger类来维护数据:
class HMMSegger: def __init__(self): self.trans_mat = {} self.emit_mat = {} self.init_vec = {} self.state_count = {} self.word_set = set() self.line_num = 0
其中:
trans_mat
: 状态转移矩阵,trans_mat[state1][state2]
表示训练集中由state1转移到state2的次数。emit_mat
: 观测矩阵,emit_mat[state][char]
表示训练集中单字char被标注为state的次数init_vec
: 初始状态分布向量,init_vec[state]
表示状态state在训练集中出现的次数state_count
: 状态统计向量,state_count[state]
word_set
: 词集合, 包含所有单词line_num
: 训练集中的行数
初始化上述数据结构:
def setup(self): for state in STATES: # build trans_mat self.trans_mat[state] = {} for target in STATES: self.trans_mat[state][target] = 0.0 # build emit_mat self.emit_mat[state] = {} # build init_vec self.init_vec[state] = 0 # build state_count self.state_count[state] = 0 self.word_set = set() self.line_num = 0
加载数据:
def load_data(self, filename): self.data = file(data_path(filename)) self.setup()
训练数据集用空格分割单词:
如果 出现 这种 情况 , 则 增加 的 居民 储蓄 存款 的 全部 或 一 部 只能 抵 作 积压 产品 占用 的 资金 而 不能 补充 流动资金 正常 需要量 的 增加
训练模型
定义一个工具函数,输入一个单词输出其中每个字的标签:
def get_tags(src): tags = [] if len(src) == 1: tags = ['S'] elif len(src) == 2: tags = ['B', 'E'] else: m_num = len(src) - 2 tags.append('B') tags.extend(['M'] * m_num) tags.append('S') return tags
因为使用标注数据集, 可以使用更简单的监督学习算法:
def train(self): for line in self.data: # pre processing line = line.strip() if not line: continue line = line.decode("utf-8", "ignore") self.line_num += 1 # update word_set word_list = [] for i in range(len(line)): if line[i] == " ": continue word_list.append(line[i]) self.word_set = self.word_set | set(word_list) # get tags words = line.split(" ") # spilt word by whitespace line_tags = [] for word in words: if word in stop_words: # pass stop words continue line_tags.extend(get_tags(word)) # update model params for i in range(len(line_tags)): if i == 0: self.init_vec[line_tags[0]] += 1 self.state_count[line_tags[0]] += 1 else: self.trans_mat[line_tags[i - 1]][line_tags[i]] += 1 self.state_count[line_tags[i]] += 1 if word_list[i] not in self.emit_mat[line_tags[i]]: self.emit_mat[line_tags[i]][word_list[i]] = 0.0 else: self.emit_mat[line_tags[i]][word_list[i]] += 1
依次进行读取训练数据-更新词汇表-添加标注- 更新统计量。
类中维护的模型参数均为频数而非频率, 这样的设计使得模型可以进行在线训练。所谓的在线训练是指, 模型随时都可以接受新的训练数据继续训练,不会丢失前次训练的结果。
应用模型
在进行预测前需要定义get_prob
方法将频数转换为频率:
def get_prob(self): init_vec = {} trans_mat = {} emit_mat = {} # convert init_vec to prob for key in self.init_vec: init_vec[key] = float(self.init_vec[key]) / self.state_count[key] # convert trans_mat to prob for key1 in self.trans_mat: trans_mat[key1] = {} for key2 in self.trans_mat[key1]: trans_mat[key1][key2] = float(self.trans_mat[key1][key2]) / self.state_count[key1] # convert emit_mat to prob for key1 in self.emit_mat: emit_mat[key1] = {} for key2 in self.emit_mat[key1]: emit_mat[key1][key2] = float(self.emit_mat[key1][key2]) / self.state_count[key1] return init_vec, trans_mat, emit_mat
预测采用Viterbi算法求得最优路径:
def predict(self, sentence): tab = [{}] path = {} init_vec, trans_mat, emit_mat = self.get_prob() # init for state in STATES: tab[0][state] = init_vec[state] * emit_mat[state].get(sentence[0], 0) path[state] = [state] # build dynamic search table for t in range(1, len(sentence)): tab.append({}) new_path = {} for state1 in STATES: items = [] for state2 in STATES: if tab[t - 1][state2] > 0: prob = tab[t - 1][state2] * trans_mat[state2].get(state1, 0) * emit_mat[state1].get(sentence[t], 0) items.append((prob, state2)) (prob, state) = max(items) tab[t][state1] = prob new_path[state1] = path[state] + [state1] path = new_path # search best path prob, state = max([(tab[len(sentence) - 1][state], state) for state in STATES]) return path[state]
tab是动态规划表, tab[t][state]
表示 时刻t 到达state状态 的所有路径中,概率最大路径的 概率值。
初值tab[0][state]
为emit_mat[state][char0] * init_vec[state]
, 其中char0代表输入序列的第一个单字, 若emit_mat中不存在char0的记录则默认为0.
若输入序列的长度为T, 则执行T-1次迭代, 每次迭代在tab中添加一行. 计算t-1时刻各状态转移到state状态的概率, 选择其中最大者作为tab[t][state]
的值, 并将选中的状态转移写入path
中。
在tab的最后一行中寻找概率最大的状态作为终态,从path
中取出状态转移路径作为标注序列。
最后定义一个工具函数, 根据标注序列将输入的句子分割为词语列表:
def cut_sent(src, tags): word_list = [] start = -1 started = False if len(tags) != len(src): return None for i in range(len(tags)): if tags[i] == 'S': if started: started = False word_list.append(src[start:i]) # for tags: r"BM*S" word_list.append(src[i]) elif tags[i] == 'B': if started: word_list.append(src[start:i]) # for tags: r"BM*B" start = i started = True elif tags[i] == 'E': started = False word = src[start:i+1] word_list.append(word) elif tags[i] == 'M': continue return word_list
需要说明的是, 因为概率模型无法保证每个B标记后都有对应的E标记, 因此认为B标记后的S和B标记都会像E标记一样结束一个单词。
完整的代码可以参见hmm.py。
因依赖关系,若要运行代码请clone整个项目。因训练数据集过于庞大故未上传,可自行制备训练数据集。 项目中包含训练过的模型data/hmm.json
可以自由体验。
- 自制基于HMM的中文分词器
- 自制基于HMM的中文分词器
- 自制基于HMM的中文分词器
- 基于HMM的中文分词
- 基于HMM的中文分词模型实现
- 中文分词的python实现-基于HMM算法
- 中文分词的python实现-基于HMM算法
- 从头开始编写基于隐含马尔可夫模型HMM的中文分词器之一 - 资源篇
- 隐含马尔可夫模型HMM的中文分词器 入门-1
- 从头开始编写基于隐含马尔可夫模型HMM的中文分词器之二 - 模型训练与使用
- 中文分词的python实现----HMM、FMM
- 基于感知器的中文分词算法
- 高阶HMM中文分词
- 基于一阶 HMM 标注序列算法的分词算法解析
- 基于CRF的中文分词
- 基于CRF的中文分词
- 基于CRF的中文分词
- 基于CRF的中文分词
- 10年地理信息行业之我见
- 2016个人小结
- android 模拟器无法启动问题解决
- 51nod 1358 浮波那契【思维+矩阵快速幂】好题!
- 阿里巴巴2016前端工程师面试题
- 自制基于HMM的中文分词器
- 字符串面试题(二)— 间隔字符串逆序
- 【Struts】——宏观了解与原理
- 全排列递归算法
- 【Struts】——工作流程
- 11.3.1
- 【Struts】——登录示例
- 2016年终总结
- STM32高级开发(5)-gcc-arm-none-eabi