基于条件随机场模型的中文分词实现(项目+源码)

1 篇文章 0 订阅
1 篇文章 0 订阅

摘 要

基于条件随机场模型是中文分词的常用方法,是一种利用语料库进行统计学习的方法。其一般会提供一个已经人为分好词的语料库,对输入的字符串进行字字匹配,统计字符串的每一个字在语料库中出现的次数,每一个字分别为词头、词中、词尾和单字成词的概率,字与字之间的转移概率,相邻字在对应状态下同时出现的概率,基于这些概率求解字符中串的字与状态的矩阵映射关系表,然后对各个字进行标记,回溯出路径然后标记字的状态。
本实验按照求解的步骤利用Eclipse平台进行写程序,一步一步分模块进行,分别实现不同功能,对中文分词所需统计的参数一一解出,包括字符串的每一个字在语料库中出现的次数,每一个字分别为词头、词中、词尾和单字成词的概率,字与字之间的转移概率,当前字出现时上一个字出现且上一个字在对应状态下同时出现的个数和概率,当前字出现时下一个字出现且下一个字在对应状态下同时出现的个数和概率,基于这些概率求解字符中串的字与状态的矩阵映射关系表,利用矩阵模型存储相关模型数据,利用公式求解字与状态的矩阵映射关系表,进而回溯路径完成分词。

关键词:中文分词;条件随机场;转移概率;回溯路径;映射关系表

1. 引言
条件随机场模型对应于一个无向图,用于在给定需要标记的观察序列的条件下,计算整个标记序列的联合概率分布。从中文分词的角度来分析,每个词语中的每一个字都存在4种可能的状态,分别是词头(Begin)、词中(Middle)、词尾(End)和单字成词(Single),简称B、M、E、S。给定句子中的每个词则被视为条件随机场模型中的观察序列,条件随机场模型求解的就是句子中字的标记序列的联合概率分布,然后对标记序列进行回溯路径。
求解标记序列的联合概率分布的整体思路是:准备语料库;语料库特征初步学习;词语特征学习;开始分词。
标记序列的联合概率分布的求解公式为:
S[字][当前状态] = MAX(P[上一个字的状态][当前状态] x S[上一个字][任何一种状态] ) + W前(后)[前(后)一个字的状态][当前的字] + R[当前状态概率]

2.相关工作及实现原理

2.1 准备语料库
随机场模型是基于统计学习的方法,在采用条件随机场模型进行分词以前,需要准备一个语料库。语料库是由大量的句子组成,并且尽可能包含各种句式,语料库的质量直接影响联合概率分布和分词的效果,词语之间采用空格符分隔。例如下面的句子:
尽管 印尼 中央 和 地方政府 已 派出 上千人 的 灭火队,但 由于 该 地区 长期 干旱 少雨,所以 火势 至今 未 得到 有效 控制。

2.2 语料库特征学习
根据第一步分好词的语料库,我们可以看出一个字的状态可能有四种可能,分别是词头(Begin)、词中(Middle)、词尾(End)和单字成词(Single),简称B、M、E、S;当然,标点符号也是需要标记的,但一般只有一种状态,就是单字成词。我们这一步需要做的就是把语料库中的词进行标记,根据第一步的分词结果进行标记,标记后的结果如下所示:
尽\B管\E印\B尼\E中\B央\E和\S地\B方\M政\M府\E已\S派\B出\E上\B千\M人\E的\S灭\B火\M队\E,\S但\S由\B于\E该\S地\B区\E长\B期\E干\B旱\E少\B雨\E,\S所\B以\E火\B势\E至\B今\E未\S得\B到\E有\B效\E控\B制\E。\S
由上面的标注的可以看出,只要将一个句子的每一个字的状态标出,即相当于已经分词;因此,可将中文分词转换成序列标注问题,对一个未分词的序列进行标注(标注状态为B,M,E,S)可知道分词结果。例如将句子“我们爱中国”分词为“我\B们\E爱\S中\B国\E”,表示将句子分词为“我们 爱 中国”。

2.3 词语特征学习

  1. 基于用户输入的字符串,针对字符串中的每一个字,统计它在语料库中出现的次数,例如‘希’字在语料库中出现了1352次。
  2. 针对具体的某个字,计算它的状态分别为词头(Begin)、词中(Middle)、词尾(End)和单字成词(Single)的概率;例如:出现1352次的“希”字,其中有1208次希的状态是B,则“希”字的状态为B的概率为1208/1352=0.8935
  3. 针对某一个字,计算它的状态分别为词头(Begin)、词中(Middle)、词尾(End)和单字成词(Single)时,每一个字都有属于自己的状态,后面这一个字也有自己的状态,计算下一个字分别为词头(Begin)、词中(Middle)、词尾(End)和单字成词(Single)的概率,这里需要注意,只考虑下一个字的状态而不管下一个字是什么字。此过程计算的就是状态之间的相互转移的矩阵,针对每一个字的4种状态,转移到4种状态,就构成了一个4*4的矩阵,矩阵中的值就是他们相互之间转移概率。
  4. 当某一个字出现的时候,计算下一个字出现的内容,并计算两个字同时出现的概率。例如:状态为B的希字,下一个字的状态为E的腊字的概率是0.04216;还要计算当某一个字出现的时候,计算上一个字出现的内容,并计算两个字同时出现的概率。例如:状态为B的腊字,上一个字的状态为B的希字的概率是0.7024。还要考虑一个问题,就是第一个字没有前一个字,最后一个字没有后一个。

2.4 开始分词

  1. 求解每个字对应的特征,根据状态信息绘制字与状态的初始矩阵映射关系表,在未分词前,这个矩阵的初始值都为0。
  2. 依据公式求解每一个字的每一种状态的值。对于一个字符串,只要先求出第一个字的四种状态下的值,后面的类似。但不同的是,计算第一个字时,没有max部分。
  3. 第二步求映射矩阵时,总是从四个值中选一个最大值,对应的下表为0,1,2,3,然后在映射表中标记选中最大值得下标。
  4. 完成了映射矩阵和标记了下标之后,就可以进行回溯路径了。将映射矩阵的最后一个字的最大值取出,从后往前回溯,第一个数字就是最大值的下标,然后看它标记的数字,这个数字就来源于上一个字的位置,再记录下标……,
  5. 写出回溯路径后,就可以对字符串进行序列标记,0对应B,1对应M,2对应E,3对应S,标记完成即相当于分词完成。

3. 基于条件随机场模型的中文分词实现步骤

3.1 词语特征学习
由于语料库msr_training.utf8.ic已经好了序列,帮我们省略了前两步的工作,为了方便,我把语料库另存为msr_training.utf8.txt文本文件,接下来的操作都是以这个文本文件进行的。
根据我的思路,首先第一步就是把语料库的内容当成一个字符串进行处理。面对四百多万行的文本文件,进行普通的字符流或字节流读取操作是很费时间的,因此我想到了利用字符流+缓冲区进行操作,使读取速度大大加快。把文件的读取内容赋值给可变的StringBuffer类型的变量,待完全读取后,再赋给String类型变量,因为之后语料库的内容是不变的。

  1. 统计输入的字符串中的每一个字在语料库中出现的次数。例如输入了“希腊的经济结构较特殊”,定义了一个数组,分别记录这十个字在语料库中的次数,每一个字分别遍历语料库进行统计,统计的结果如下:在这里插入图片描述
  2. 统计字符串中每个字分别为B,M,E,S的个数。这一模块与第一个类似,就是多加了一个条件,就是还要满足该字的状态。则对于一个字来讲,是一个 的矩阵,;对于一个句子,是一个行为字符串长度,列为4的矩阵,统计结果如下:
    在这里插入图片描述
  3. 计算字符串中每个字分别为B,M,E,S的概率。这一模块与第二个类似,就是用上一步统计的结果除以对应字的个数。对于一个字来讲,是一个 的矩阵,加起来和为1;对于一个句子,是一个行数为字符串的长度,列数为4的矩阵,计算结果如下:
    在这里插入图片描述
  4. 统计当某一个字分别为B,M,E,S的状态出现时,下一个字的状态为B,M,E,S时的个数。要注意的是,这里只需要下一个字的状态而不需要考虑下一个字是什么字。对于每一个字,就是一个 的矩阵,对于矩阵的每一行,代表为B,M,E,S的一种时,下一个字的状态是B,M,E,S的个数,的结果是n个 的矩阵,其中n是输入的字符串的长度,代码中用三维矩阵表示。重要代码如下:
    在这里插入图片描述
  5. 统计当某一个字分别为B,M,E,S的状态出现时,下一个字的状态为B,M,E,S时的条件概率。对于每一个字,就是一个 的矩阵,对于矩阵的每一行,代表为B,M,E,S的一种时,下一个字的状态是B,M,E,S的概率,就是用上一部的结果除以对应字对应状态的个数,因此矩阵中的每一行的和为1。
  6. 统计当字符串中的某一个字出现在语料库时,下一个字的状态分别为为B,M,E,S同时出现的个数 。这里需要注意与第四步不同的是,我们需要知道这一个字是什么字,而不必考虑这一个字的状态,但下一个字不仅需要考虑是什么字,还要考虑下一个字的状态;例如:我们输入了“希腊的经济结构较特殊”,‘希’字在语料库出现了1352次,在语料库中,紧接“希”字下一个字为 “腊”且“腊”字的状态为E的情况出现了57次。在这里,我们还需要考虑一个问题,就是最后一个字没有下一个字的问题,这里可以设为0。其中定义的二维矩阵第i行第j列的含义是:字符串中的第i个字出现在语料库时,下一个为字符串中的第i+1个字,且状态分别为为B,M,E,S的个数;基于此,统计出的结果为:
    在这里插入图片描述
  7. 统计当字符串中的某一个字出现在语料库时,下一个字的状态分别为为B,M,E,S同时出现的概率 。这里和第六步类似,只是计算了概率;例如:第六步中 ‘希’字在语料库出现了1352次,在语料库中,紧接“希”字下一个字为 “腊”且了字的状态为E的情况出现了57次,则该值为57/1352=0.04216。在这里,我们还需要考虑一个问题,就是最后一个字没有下一个字的问题,这里可以设为0。基于此,统计出的结果为:
    在这里插入图片描述
  8. 统计当字符串中的某一个字出现在语料库时,上一个字的状态分别为为B,M,E,S同时出现的个数 。这里和第六步类似,我们需要知道这一个字是什么字,而不必考虑这一个字的状态,但上一个字不仅需要考虑是什么字,还要考虑上一个字的状态;例如:我们输入了“希腊的经济结构较特殊”,‘腊’字在语料库出现了84次,在语料库中,紧接“腊”字上一个字为 “希”且“希”字的状态为B的情况出现了59次。在这里,我们还需要考虑一个问题,就是第一个字没有下一个字的问题,这里可以设为0。其中定义的二维矩阵第i行第j列的含义是:字符串中的第i个字出现在语料库时,上一个为字符串中的第i+1个字,且状态分别为为B,M,E,S的个数;基于此,统计出的结果为:
    在这里插入图片描述
  9. 统计当字符串中的某一个字出现在语料库时,上一个字的状态分别为为B,M,E,S同时出现的概率 。这里和第八步类似,只是计算了概率;例如:第八步中‘腊’字在语料库出现了84次,在语料库中,紧接“腊”字上一个字为 “希”且“希”字的状态为B的情况出现了59次,则该值为59/84=0.7024。在这里,我们还需要考虑一个问题,就是第一个字没有下一个字的问题,这里可以设为0。基于此,统计出的结果为:
    在这里插入图片描述
    3.2 映射矩阵
    求解出了公式中需要用到的参数,接下来就可以求解字与状态的矩阵映射关系表了。因为字符串的第一个字没有上一个字,只需要求上半部分,例如,希字的状态分别为B,M,E,S的值计算公式为:

S[希][B]=W(前)[_B][希]+W[腊_B][希]+R[B]
S[希][M]=W(前)[_M][希]+W[腊_M][希]+R[M]
S[希][E]=W(前)[_E][希]+W[腊_E][希]+R[E].
S[希][S]=W(前)[_S][希]+W[腊_S][希]+R[S]

同理,我们算出下面的每一个字与状态的矩阵里的值,需要注意的是,从第二个字开始,就多了一步,还要加上下面这个式子:

MAX(P[上一个字的状态][当前状态] x S[上一个字][任何一种状态])

我们需要根据上一个字的四种状态下的值,计算出四个结果,选取最大的值作为该字与状态的值填在映射关系表里,对于每个字还需要计算四种状态下的值。 以一个字的一种状态为例,主要代码如下:
在这里插入图片描述
3.3 回溯路径
求出了上一步的字与状态的映射关系的值,其实也就解决了每一个字标记0,1,3的问题,这一步可以与上一步同时做。在上一步中,我们从第二个字开始,针对每一个字的其中一种状态,都是利用到了上一个字的四种状态的值,在求出四个值之后选出一个最大的。那么,这个选出的最大值所对应位置(0,1,2,3)就是当前字的一种状态的所做的标记,也就是上图中的path矩阵。除了第一个字的四种状态默认0,其他每一个字都要计算四种状态下的表号。例如,我们输入“希腊的经济结构较特殊”标号如下:
在这里插入图片描述
完成了字与状态的映射关系表,接下来可以说是分词的最后一步了,找出回溯路径。回溯路径的回溯方法是:先在映射表中找出最后一个字的四种状态的最大值,该字的此位置代表着字符串的最后一个字就是该状态;然后看它的标号,它的标号记录着上一个字的对应位置,从而可知状态……
虽然思路简单,但是实现回溯起来输出这个回溯路径是个比较复杂的问题。从回溯路径的方法我们可以看出一些规律,首先看最后一个字的最大值,先不看这个字的标号,而是先记录这个字的位置index(0,1,2,3);然后这一个字的标号代表上一个字的下表进行回溯,即以下思想。
上一个字的index = path[当前字][index]

主要代码如下:
在这里插入图片描述
回溯路径可以转换为状态标记,0代表B,1代表M,2代表E,3代表S,回溯的结果如下:
在这里插入图片描述

4. 基于条件随机场模型的中文分词改进方法
4.1 准确率及稳定性的改进

在准确率方面做了两个改进,就是在语料库特征学习的过程中,求当一个字出现时,上一个字和下一个字的同时出现,且上一个字和下一个字的状态分别为B,M,E,S的个数和概率,但是第一个字没有上一个字,最后一个字没有下一个字。
在实验2中,针对第一个字,上一个字是任意字都可以;针对最后一个字,下一个字是任意字都可以,这样的结果导致“较特”分在了一起。
而在本实验中,针对第一个字,上一个字出现的个数和概率都设为0;针对最后一个字,下一个字出现的个数和概率都设为0,实验结果证明效果良好,看示例图:
改进前:
在这里插入图片描述
改进后:
在这里插入图片描述
4.2 速度的改进
本程序在读取语料库时是赋值给可变的StringBuffer类型的变量,待读取完毕后在赋值给不可变的String类型变量,而且读取语料库采用字符流+缓冲区的的方法;遍历语料库时,偏移量为4而不是1,这样极大程度上节省了时间。以输入的字符串为“希腊的经济结构较特殊”为例,读取语料库到统计,到分词结束平均用时1.6秒。
4.3 可扩展性的改进
本程序改变了实验2的实现机制,实验2采取的是面向过程的编程,这次实验实现了面向对象编程,定义了一个接口CalcDao.java,接口里面声明了很多抽象方法,实现这个接口的类都具有该接口的方法;CalcDaoment.java实现了该接口,除此之外,也有自己的print系列方法;Test是测试类。这样就使整个程序增强了扩展性,需要添加什么功能只需要再编写一个函数即可,不必大幅度的改动,也便于程序的维护。
5. 实验结果
以输入的字符串为“希腊的经济结构较特殊”。参数的统计或计算结果大部分在上述实现步骤中已经列出,下面列出实验的分词结果。
分词结果:希腊 的 经济 结构 较 特殊
耗时:1602毫秒
回溯路径:0 2 3 0 2 0 2 3 0 2

在这里插入图片描述
再举一个例子,“中国像一条巨龙屹立在世界的东方”
在这里插入图片描述
在这里插入图片描述
结果:中国 像 一条 巨龙 屹立 在 世界 的 东方
分词结束后,我们还可以选择查看分词过程中计算产生的参数,如下,参数结果上述已列出:
在这里插入图片描述
6. 结论及尚存在的问题
6.1 结论

该算法实现的中文分词效果很好,而且在回溯路径这一块用到了比较巧妙的方法;尤其是在时间这一块取得了不错的效果。总体来说,遇到了不少问题也都解决了。
在读文件这一块考虑了很久,先是考虑要不要把语料库中的每一个字都统计出来后,再用字符串查找,这样就不必每一个字都要遍历语料库了;但是这样的话,首先统计语料库也是一个很费时间的工作,而且也只是求解第一步用到,所以就直接读取语料库遍历,效果证明直接遍历速度还要更快。
本实验的数据模型很便于理解,因为定义了很多抽象方法,每一个方法都实现了不同的功能,用二维,三维矩阵去存储数据模型,矩阵也便于做循环计算。
本实验还对实验2做了一些改进,使实验的准确性提高了,在第一个字和最后一个字的参数做了调整;而且在回溯路径这一块,发现了巧妙的规律后难题迎刃而解。
6.2 尚存在的问题
对长句的分词效果一般。对于长句子,前大半部分的分词效果良好,后面部分分不开;但是,好在后面部分是没有分的,不是误分,这样只需要把长句子分成两个或多个短句子就可以解决了。
附 录(项目+源码)
链接:https://pan.baidu.com/s/1Y_xR8eQU7XWT0rM_UeoM8Q
提取码:mvxl

  • 6
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
条件随机场(Conditional Random Fields, CRF)是一种用于序列标注问题的统计模型。下面是一个使用Python实现条件随机场的示例代码: ```pythonimport numpy as np# 定义条件随机场类class CRF: def __init__(self, num_states, num_features): self.num_states = num_states # 状态数 self.num_features = num_features # 特征数 self.transition_matrix = np.zeros((num_states, num_states)) # 转移矩阵 self.feature_matrix = np.zeros((num_states, num_features)) # 特征矩阵 # 计算特征函数关于当前状态和观测的概率 def compute_feature_prob(self, state, observation): return np.exp(np.dot(self.feature_matrix[state], observation.T)) # 计算一条序列对应的得分 def compute_score(self, sequence): score =0.0 for i in range(len(sequence)-1): score += self.transition_matrix[sequence[i], sequence[i+1]] return score # 计算一条序列对应的特征函数的期望 def compute_feature_expectation(self, sequence): feature_expectation = np.zeros((self.num_states, self.num_features)) for i in range(len(sequence)): observation = sequence[i][0] state = sequence[i][1] feature_expectation[state] += self.compute_feature_prob(state, observation) return feature_expectation # 更新参数 def update_parameters(self, sequence, learning_rate): observed_feature_expectation = self.compute_feature_expectation(sequence) model_feature_expectation = np.zeros((self.num_states, self.num_features)) Z =0.0 for state in range(self.num_states): Z += np.exp(np.dot(self.feature_matrix[state], observed_feature_expectation.T)) for state in range(self.num_states): model_feature_expectation[state] = np.exp(np.dot(self.feature_matrix[state], observed_feature_expectation.T)) / Z self.feature_matrix[state] += learning_rate * (observed_feature_expectation[state] - model_feature_expectation[state]) # 训练模型 def train(self, train_data, num_epochs, learning_rate): for epoch in range(num_epochs): for sequence in train_data: self.update_parameters(sequence, learning_rate) # 示例用法if __name__ == '__main__': # 构造训练数据 train_data = [([1,2,3],0), ([4,5,6],1), ([7,8,9],0)] # 创建条件随机场对象 crf = CRF(2,3) # 训练模型 crf.train(train_data, num_epochs=10, learning_rate=0.1) ``` 以上代码是一个简单的条件随机场实现,其中定义了CRF类,包含了条件随机场的初始化、特征概率计算、得分计算、特征期望计算、参数更新等方法。在示例用法中,我们构造了一个简单的训练数据集,然后创建了一个包含2个状态和3个特征的条件随机场对象,调用train方法进行训练。请注意,这只是一个简单的示例,实际应用中可能需要更复杂的特征函数和更大规模的数据集进行训练。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值