CRF
注:以上实现只针对中文分词任务。
注,以下内容需要一定的学习成本,如有不适请跳至下一节(实战中学习)。但,建议先大概学一下理论!
学习 CRF 的路线:
概率图模型
(将概率用图的方式表示出来,节点表事件,边代表事件间的联系,这样便于计算联合概率);
贝叶斯网络
马尔科夫随机场
逻辑斯蒂回归
& 最大熵模型
逻辑斯蒂回归
非常简单,它能让我们了解什么是 对数线性模型(CRF也是哦)最大熵模型
HMM
& MEMM
概率图模型
马尔科夫随机场
CRF
(下面 CRF ,特指线性链CRF)
学习过程中可能对一些符合表示产生歧义,后面的中文分词应用会有解释。
学完后,可能还有点迷糊,不要紧,通过下面做中文分词的应用便能完全懂了!
我们的目标是:中文分词!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 中只使用了局部归一化,所以出现局部偏执问题。
在下一节,我们将关注 状态特征 与 转移特征。
在线性链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
集合中,其中 key 即 featureId
便于后续查找某一特征,而 value 自然就是对应的 Feature
对象了。
注:因这里的转移特征无视观测序列,所以在用 Feature
对象描述转移特征时,一个对象对应一个转移特征。因而此时规定对象中的数组只有下标 0 处有意义。
这里就不列出扫描训练集构造所有特征函数的代码,具体请在源码中 Utils.java
中查找。
有了特征函数我们便可关注参数了,也就是下一节中学习算法做的事。
首先,我们需要明确学习的是什么。在线性链CRF的参数化形式中原有两个未知,一个是特征函数另一个是参数(亦称为权重)。在上一节,我们已经构造了所有的特征函数。所以现在只剩下它们对应的参数就可以确定模型了。自然而然,CRF 学习的目标就是更新这些参数!
至于如何更新这些参数,是这一小节的重点。前面已经提到,我们将会使用改进的迭代尺度法来优化CRF。
可能你会问为什么这么学习。首先要明确我们已知训练数据集,由此可知经验概率分布 P ~ ( X , Y ) \tilde{P}(X,Y) P~(X,Y) (统计学习的最基本假设)。现在求条件分布,便可以通过 极大化 训练数据的 对数似然函数 来求模型参数。改进的迭代尺度法通过迭代的方法不断优化对数似然函数改变量的下界,达到极大化对数似然函数的目的。通俗地讲,为了求条件分布,数学家给我们提供了一个NB的工具 – 极大化对数似然函数,这样就变成了优化问题。针对该优化问题呢,数学家经过对原函数不断放缩、求导给我们推导出了许多优化算法,改进的迭代尺度法(IIS)便是其中一个,其他的还有 拟牛顿法 梯度下降法 等等。(注:这里不再详述推导过程。感兴趣的,可以参考《统计学习方法》第六章)
说个题外话,再来谈谈为什么CRF学习算法学习的是参数。我们还可以这么理解,由不同的参数可以定义不同的模型。所有的参数可能构成了 模型空间(或称为 模型集合)。学习算法的目的就是在模型空间中找寻最能拟合训练数据的模型。
迭代尺度法的基本思想:我们有种方法能够使得对数似然函数每次增加一点点,所以我们可以不断地使用(迭代)该方法慢慢的增大对数似然函数。总有那么一个时刻,它会收敛,于是达到了极大化对数似然函数的目的。
OK,是时候看算法描述
下面将结合中文分词任务解释上面的算法描述。
不难发现整个算法中最难的就是步骤(a)中的方程求解。怎么求,公式11.41、11.44已经说明了。下面以公式11.41为例来说明。
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)))]
α 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)
β 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)
上面只是理解计算过程,而在具体的实现中需要优秀的设计来提高性能。比如,我们的特征有几百万个,每次训练要遍历这么多特征?显然,这样行不通。我们能不能率先找出该批量句子中所含有的特征?再比如,考虑这一系列的计算过程那些能用多线程?
这些设计很吃个人的经验,建议在 先弄懂算法流程后,再设计,最后动手。大家先自己想想怎么设计,看看有没有奇淫技巧。如果没想到,可以看看我的代码(篇幅有限,就不再描述我是如何实现的了)。我的肯定不是最好的,但还能凑合。
最后的预测算法,用于解决解码问题,说白了就是能够对输入的句子进行分词。
现在已经有条件随机场 P ( Y ∣ X ) P(Y|X) P(Y∣X)(由上面得到的)以及输入序列(句子),要求条件概率最大的输出序列(标记序列) Y ∗ Y^* Y∗。最后会发现这就是一个最优路径问题,要用到 动态规划。此处便用的是著名的 维特比算法。
具体过程不打算写了。请参考《统计学习方法》中的 206 页。很简单的!书中已经给出递归公式,实现时按照公式来即可。
我是在 人民日报语料(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%
代码(Github)