使用 CRF 做中文分词
概要
- 简述
CRF
- 问题描述(中文分词任务)
- 构建特征函数
- CRF 学习算法(改进的迭代尺度法)
- CRF 预测算法(维特比算法)
注:以上实现只针对中文分词任务。
1. 简述 CRF
注,以下内容需要一定的学习成本,如有不适请跳至下一节(实战中学习)。但,建议先大概学一下理论!
学习 CRF 的路线:
- 大概了解
概率图模型
(将概率用图的方式表示出来,节点表事件,边代表事件间的联系,这样便于计算联合概率);- 概率有向图模型 -->
贝叶斯网络
- 概率无向图模型 -->
马尔科夫随机场
- 概率无向图模型的因子分解(考虑如何表示联合概率),不求证明但求理解
- 资料
- 概率有向图模型 -->
- 大概学习
逻辑斯蒂回归
&最大熵模型
逻辑斯蒂回归
非常简单,它能让我们了解什么是 对数线性模型(CRF也是哦)最大熵模型
- 本质,模型的学习过程就是求解最大熵模型的过程
- 也就是,在满足所有约束条件下,模型最后学到的是熵最大的,最公平的
- 最后,学习使用 改进的迭代尺度法 优化ME(后面 CRF 的学习也使用这个方法)
- 资料
- 《统计学习方法》第六章
- 了解
HMM
&MEMM
- 从三个点学习 HMM (其实 CRF MEM 类似)–> 学习过程中一定要牢记这三点
- 概率问题(数学问题)
- 学习问题(学习模型)
- 预测问题(利用模型)–> 维特比算法
- HMM 的两个假设
- 马尔科夫性假设
- 观测独立性假设:任意时刻的观测只依赖该时刻的状态,与其他观测以及状态无关(☆,注意与 CRF MEMM 的不同)
- MEMM 的问题 --> 标注偏执(CRF,解决了此问题)
- 资料
- 《统计学习方法》第十章
- 【中文分词】最大熵马尔可夫模型MEMM
- 从三个点学习 HMM (其实 CRF MEM 类似)–> 学习过程中一定要牢记这三点
- 再回看
概率图模型
- 重点回顾
马尔科夫随机场
- 资料
- 重点回顾
- 最后便可很轻松地学习
CRF
(下面 CRF ,特指线性链CRF)- CRF 通俗的说,就是条件概率分布构成一个马尔科夫随机场;
- CRF 参数化形式,由它的定义结合概率无向图的因子分解可得;
- 可能会发现其与 ME 的形式,有几分相似,但它们的思想不一样(CRF 做序列标注,ME 做分类);
- 接下来从三个点学习 CRF
- 概率问题(重点关注求期望,在学习算法要用到)
- 学习问题(已学,改进的迭代尺度法)
- 解码问题(已学,维特比算法)
- 资料
- 《统计学习方法》第十一章
- 【中文分词】条件随机场CRF
学习过程中可能对一些符合表示产生歧义,后面的中文分词应用会有解释。
学完后,可能还有点迷糊,不要紧,通过下面做中文分词的应用便能完全懂了!
2. 问题描述
我们的目标是:中文分词!OK,接下来分析此任务。
白 B // 第一列,等待分词的输入序列;第二列,标记序列(即分词结果)
菜 E
清 B // B,表示词的开始
炖 E // M,表示词的中间
白 B // E,表示词的结束
萝 M // S, 表示单字成词
卜 E
是 S
什 B
么 E
味 B
道 E
? S
这里,把中文分词视为序列标注问题。即,给每个字打上标签,由这些标签描述了分词结果。上面是“白菜清炖白萝卜是什么味道?”的分词结果。从注释中可以看到,一共有 4 个标签。
通常,我们称等待分词的语句输入序列(或称为观测序列);而称分词结果为输出序列(或称为标记序列、状态序列)。所以,我们的目标(中文分词)可以描述为:在给定观测序列 X X X 下,找到概率最大的标记序列 Y Y Y。
怎么解决呢?CRF的思想是:首先,假设条件分布 P ( Y ∣ X ) P(Y|X) P(Y∣X) 可构成马尔科夫随机场(即每个状态只受相邻状态或输入序列的影响);然后,我们通过训练(统计)可以确定此分布;最后,序列标注问题(如中文分词)是在给定条件随机场 P ( Y ∣ X ) P(Y|X) P(Y∣X) 和输入序列 X X X 求条件概率最大的标记序列 Y ∗ Y^{*} Y∗。可以看到,最后变为一个动态规划问题(DP)。
在我们继续进行下去之前,需要线性链CRF的参数化形式(想要了解如何得到的该形式,请参考概率无向图的因子分解)
P ( Y ∣ X ) = 1 Z ( X ) e x p ( ∑ i , k λ k t k ( y i − 1 , y i , X , i ) + ∑ i , l μ l s l ( y i , x , i ) ) P(Y|X)=\frac{1}{Z(X)}exp(\sum_{i,k}\lambda_kt_k(y_{i-1},y_i,X,i)+\sum_{i,l}\mu_ls_l(y_i,x,i)) P(Y∣X)=Z(X)1exp(i,k∑λktk(yi−1,yi,X,i)+i,l∑μlsl(yi,x,i))
其中, t k t_k tk s l s_l sl 分别为 转移特征、状态特征,而 λ k \lambda_k λk μ l \mu_l μl 为它们对应的权重(学习问题就是去学习这些权重)。而 Z ( X ) Z(X) Z(X) 是规范化因子,求和是在所有可能的输出序列上进行(如何理解这句话,其实就是遍历 Y Y Y 的全排列,比如这里一共有 4 句 子 长 度 4^{句子长度} 4句子长度 个可能)
Z ( X ) = ∑ Y e x p ( ∑ i , k λ k t k ( y i − 1 , y i , X , i ) + ∑ i , l μ l s l ( y i , x , i ) ) Z(X) = \sum_Yexp(\sum_{i,k}\lambda_kt_k(y_{i-1},y_i,X,i)+\sum_{i,l}\mu_ls_l(y_i,x,i)) Z(X)=Y∑exp(i,k∑λktk(yi−1,yi,X,i)+i,l∑μlsl(yi,x,i))
可以看到, Z ( X ) Z(X) Z(X) 起到的是全局归一化,而在 MEMM 中只使用了局部归一化,所以出现局部偏执问题。
在下一节,我们将关注 状态特征 与 转移特征。
3. 构建特征函数
在线性链CRF参数化形式中,未知的只有特征与对应参数(或称为权重),而参数是在学习问题中关注的,所以只剩下特征了。怎样定义或构建特征呢?
我们的目标是:中文分词!所以,针对中文分词任务我们有独特的特征构造方式(借鉴于CRF++
)。首先,明确特征函数的含义:它描述是否满足指定特征,满足返回1否则返回0;再来看特征模板
# Unigram --> 构造状态特征的模板
U00:%x[-2,0]
U01:%x[-1,0]
U02:%x[0,0]
U03:%x[1,0]
U04:%x[2,0]
U05:%x[-2,0]/%x[-1,0]/%x[0,0]
U06:%x[-1,0]/%x[0,0]/%x[1,0]
U07:%x[0,0]/%x[1,0]/%x[2,0]
U08:%x[-1,0]/%x[0,0]
U09:%x[0,0]/%x[1,0]
# Bigram --> 构造转移特征的模板
B
我们的特征有两种形式:状态特征 & 转移特征。下面结合特征模板分别介绍这两种特征。
状态特征函数
s
l
(
y
i
,
X
,
i
)
)
s_l(y_i,X,i))
sl(yi,X,i)) ,它的输入为当前状态、整个观测序列以及当前位置。上面特征模板中 U01~U09
用于生成状态特征。以 U00:%x[-2,0]
为例,-2 代表取观测序列中相对当前位置的倒数第2个,0 在此处无意义。所以,用 U00~U09
做模板在 我爱我的祖国。 第3个位置下可构建的状态特征为
我
爱
我 <-- 当前处于此位置
的
祖
国
# 下面为生成的状态特征
U00:我
U01:爱
U02:我
U03:的
U04:祖
U05:我/爱/我
U06:爱/我/的
U07:我/的/祖
U08:爱/我
U09:我/的
注:每行代表 4 个状态特征,因为这里有 4 标签(BMES)。所以,这里一共产生 40 个状态特征。
注:请注意,这只是在第 3 个位置下产生的。
转移特征
t
k
(
y
i
−
1
,
y
i
,
X
,
i
)
t_k(y_{i-1},y_i,X,i)
tk(yi−1,yi,X,i),它的输入为上一状态、当前状态、整个观测序列以及当前位置。上面特征模板中,B
表示生成所有的转移特征。因而,一共可产生 16 种转移特征(无视观测序列)。它们为
BB --> 表示从上一个状态 B 转移到当前状态 B
BM
BE
BS
MB
MM
ME
MS
EB
EM
EE
ES
SB
SM
SE
SS
OK,了解了这两种特征的构造过程,我们便可以通过扫描训练集来构建大量的特征(注意,转移特征固定为16个)。我的 25M 训练集下,大约可以生成 560w+ 个特征函数(只取频率大于等于3的)。
下面看程序中如何定义特征,首先看 Feature
类
import java.io.Serializable;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author iwant
* @date 19-6-13 09:31
* @desc 特征函数
*/
public class Feature implements Serializable, Cloneable {
// 特征函数数字标识
private int id;
// 特征函数标识(含特征函数的内容) --> 比如,featureId="U00:我"
private String featureId;
// 出现频率 --> 数组大小为 4 ,从这里可以看出一个该类对象对应 4 个状态特征
private AtomicInteger[] freqs = new AtomicInteger[4];
// 权重 --> 数组大小为 4 ,从这里可以看出一个该类对象对应 4 个状态特征
// 注:对应四个标记(BMES)
private double[] weights = new double[4];
public Feature(int id, String featureId) {
this.id = id;
this.featureId = featureId;
for (int i = 0; i < weights.length; i++)
this.freqs[i] = new AtomicInteger();
}
public Feature(String featureId) {
this(-1, featureId);
}
}
从注释中我们可以看到,一个Feature
对象包含 4 个状态特征。
我们把所有的特征放在一个 Map<String,Feature>
集合中,其中 key 即 featureId
便于后续查找某一特征,而 value 自然就是对应的 Feature
对象了。
注:因这里的转移特征无视观测序列,所以在用 Feature
对象描述转移特征时,一个对象对应一个转移特征。因而此时规定对象中的数组只有下标 0 处有意义。
这里就不列出扫描训练集构造所有特征函数的代码,具体请在源码中 Utils.java
中查找。
有了特征函数我们便可关注参数了,也就是下一节中学习算法做的事。
4. CRF 学习算法
首先,我们需要明确学习的是什么。在线性链CRF的参数化形式中原有两个未知,一个是特征函数另一个是参数(亦称为权重)。在上一节,我们已经构造了所有的特征函数。所以现在只剩下它们对应的参数就可以确定模型了。自然而然,CRF 学习的目标就是更新这些参数!
至于如何更新这些参数,是这一小节的重点。前面已经提到,我们将会使用改进的迭代尺度法来优化CRF。
可能你会问为什么这么学习。首先要明确我们已知训练数据集,由此可知经验概率分布 P ~ ( X , Y ) \tilde{P}(X,Y) P~(X,Y) (统计学习的最基本假设)。现在求条件分布,便可以通过 极大化 训练数据的 对数似然函数 来求模型参数。改进的迭代尺度法通过迭代的方法不断优化对数似然函数改变量的下界,达到极大化对数似然函数的目的。通俗地讲,为了求条件分布,数学家给我们提供了一个NB的工具 – 极大化对数似然函数,这样就变成了优化问题。针对该优化问题呢,数学家经过对原函数不断放缩、求导给我们推导出了许多优化算法,改进的迭代尺度法(IIS)便是其中一个,其他的还有 拟牛顿法 梯度下降法 等等。(注:这里不再详述推导过程。感兴趣的,可以参考《统计学习方法》第六章)
说个题外话,再来谈谈为什么CRF学习算法学习的是参数。我们还可以这么理解,由不同的参数可以定义不同的模型。所有的参数可能构成了 模型空间(或称为 模型集合)。学习算法的目的就是在模型空间中找寻最能拟合训练数据的模型。
迭代尺度法的基本思想:我们有种方法能够使得对数似然函数每次增加一点点,所以我们可以不断地使用(迭代)该方法慢慢的增大对数似然函数。总有那么一个时刻,它会收敛,于是达到了极大化对数似然函数的目的。
OK,是时候看算法描述
下面将结合中文分词任务解释上面的算法描述。
- 可以看到算法的输入有:转移特征、状态特征以及经验分布(就是训练样本)。这里需要说明的是每次训练选取多少个样本(机器学习中的 batch size 概念)。在中文分词任务中,也就是考虑每次训练使用多少个句子。实现时,我每次训练使用 500 个句子。
- 再来看其中的步骤(a),这一步便是上述迭代尺度法的基本思想中提到的每次能够使对数似然函数增加一点点的方法。只要我们求解出 δ k \delta_k δk 即可。可以看到公式 (11.41) (11.44) 给出了求解方法。后面再详诉它们。
- 再来看其中的步骤(3),这显然就是那个迭代过程。但我们实现时需要考虑如何定义 收敛(即迭代介绍的条件)。比如,在验证集上的准确率不在提高。
不难发现整个算法中最难的就是步骤(a)中的方程求解。怎么求,公式11.41、11.44已经说明了。下面以公式11.41为例来说明。
- 先要明确它是针对转移特征的,且 δ k \delta_k δk 只针对下标为 k k k 这一转移特征;
- 再来看 S S S,它是一个常数,是一个足够大的特征总数。这里,我们每次训练都要选一批句子,而每个句子都有个特征总数(即该句子满足的状态特征与转移特征总数),足够大的特征总数可以为这些特征总数中最大的那个。
- 再来看 E p ~ [ t k ] E_{\tilde{p}}[t_k] Ep~[tk] ,表示在经验概率分布下 P ~ ( X , Y ) \tilde{P}(X,Y) P~(X,Y),转移特征函数 t k t_k tk 的期望值。听起来很高大上,其实就是统计该特征函数在当前批量句子中出现的次数;
- 最后有
E
p
[
t
k
]
E_{p}[t_k]
Ep[tk] ,表示特征函数在经验分布为
P
~
(
X
)
\tilde{P}(X)
P~(X) 下关于联合分布
P
(
X
,
Y
)
P(X,Y)
P(X,Y) 的数学期望。公式 (11.42) 说明了怎么求。但是相当繁琐!我们需要知道
a
l
p
h
a
alpha
alpha
M
M
M
β
\beta
β。
- 先来解释公式11.42,最外面的求和是针对所有的句子(比如,我实现时每次训练要用 500 个句子);从 1~n+1 的求和是遍历一个句子(注意一个句子的长度为 n);最里面的求和是针对所有的“前后状态”(在中文分词任务中,有4中标签,所以一共有16个前后状态);
- M M M (参考《统计学习方法》公式11.23)
M i ( X ) = [ e x p ( ∑ k = 1 K ( ∑ i , k λ k t k ( y i − 1 , y i , X , i ) + ∑ i , l μ l s l ( y i , x , i ) ) ) ] M_i(X)=[exp(\sum_{k=1}^K(\sum_{i,k}\lambda_kt_k(y_{i-1},y_i,X,i)+\sum_{i,l}\mu_ls_l(y_i,x,i)))] Mi(X)=[exp(k=1∑K(i,k∑λktk(yi−1,yi,X,i)+i,l∑μlsl(yi,x,i)))]
- a l p h a alpha alpha (参考《统计学习方法》公式11.28)
α i T ( X ) = α i − 1 T ( X ) M i ( X ) \alpha_i^T(X)=\alpha_{i-1}^T(X)M_i(X) αiT(X)=αi−1T(X)Mi(X)
- b e t a beta beta (参考《统计学习方法》公式11.31)
β i ( X ) = M i + 1 ( X ) β i + 1 ( X ) \beta_i(X)=M_{i+1}(X)\beta_{i+1}(X) βi(X)=Mi+1(X)βi+1(X)
- 注意,在计算它们是要注意下标!每个句子都要计算 a l p h a alpha alpha M M M β \beta β 一次。
上面只是理解计算过程,而在具体的实现中需要优秀的设计来提高性能。比如,我们的特征有几百万个,每次训练要遍历这么多特征?显然,这样行不通。我们能不能率先找出该批量句子中所含有的特征?再比如,考虑这一系列的计算过程那些能用多线程?
这些设计很吃个人的经验,建议在 先弄懂算法流程后,再设计,最后动手。大家先自己想想怎么设计,看看有没有奇淫技巧。如果没想到,可以看看我的代码(篇幅有限,就不再描述我是如何实现的了)。我的肯定不是最好的,但还能凑合。
5. CRF 预测算法
最后的预测算法,用于解决解码问题,说白了就是能够对输入的句子进行分词。
现在已经有条件随机场 P ( Y ∣ X ) P(Y|X) P(Y∣X)(由上面得到的)以及输入序列(句子),要求条件概率最大的输出序列(标记序列) Y ∗ Y^* Y∗。最后会发现这就是一个最优路径问题,要用到 动态规划。此处便用的是著名的 维特比算法。
具体过程不打算写了。请参考《统计学习方法》中的 206 页。很简单的!书中已经给出递归公式,实现时按照公式来即可。
6. 结果
我是在 人民日报语料(2014版) 语料上做的实验。模型大约训练了一个半小时,最后的准确率为 94.20%。还算可以。
[INFO] 开始初始化!
[INFO] 开始解析特征模板...
[INFO] 特征模板解析完毕!
[INFO] 开始加载模型...
[INFO] 一共加载了 5656572 个特征!
[INFO] 初始化完毕!
[INFO] 评测使用的文件:data/save/test.data !
[INFO] 结果将会保存在:data/save/result.data !
[INFO] 分词已结束,正在评估准确率...
[INFO] 评测结果如下:
wTotal: 2042582
sTotal: 47884
wError: 118489, 5.80% --> 94.20%
sError: 29377, 61.35%