主要内容
- 问题描述
- 模型训练
- 样本格式
- 模板文件
- 训练参数
- 总结
之前我们简单介绍了CRF的背景知识、基本原理、应用场景。接下来我们主要介绍通过CRF来解决实际问题的工具CRF++。CRF++是工业应用比较广泛的条件随机场的开源工具。安装包下载地址:CRF++安装包,官方使用教程:CRF++教程。本文主要通过序列标注任务中的实体识别(Named Entity Recognition,NER,命名实体识别),进行介绍。
一、问题描述
原始query:播放周杰伦的告白气球
序列标注结果:播/O 放/O 周/B-artist 杰/I-artist 伦/E-artist 的/O 告/B-song 白/I-song 气/I-song 球/E-song
其中,artist表示歌手,song表示歌曲,是我们需要从句子中识别的实体。O表示没有实体意义的字,B表示实体的第一个字,E表示实体的最后一个字,I表示实体的中间部分。这里是用基于BIEO的方式进行标注,其中,两个字构成的实体只需要BE进行标注,单个字构成的实体只需要B进行标注。也可以用基于BIESO的方式进行标注,其中,单个字构成的实体通过S标注。
原始query,输入至模型中,输出的序列标注结果,从而得到最终的实体 {"artist":"周杰伦", "song":"告白气球"}。
二、模型训练
1、样本格式
采用CRF进行序列标注,准备训练数据就是准备训练序列对(X,Y),准备测试数据就是准备测试序列对(X,Y),其中表示有D个特征。训练样本的格式如下图。第一列特征,表示原始query的每个字,也可以是其他形式的token,比如词等等。第二列特征,这里的例子采用的是query的每个字(token)可能是实体的概率,假设这里的概率是根据实际样本分布得到的(具有一定的置信度),将概率hash成枚举型[1,2,3,4,5](例如概率大于0.8取值5,概率小于0.2取值1),这样做的好处是该特征具有更好的容错性以及泛化性;当然也这里可以选取其他的特征,具体根据业务需求设定。第三列表示query的序列标注结果。这里的例子只用了两个特征(第一二列),当然也可以用更多的特征,每个特征一列,还是那句话,具体根据业务需求设定。所有列之间通过制表符"\t"(Tab键)分隔,两条不同query之间间隔一个空行。测试样本的格式和训练样本严格一致。
来 1 O
一 1 O
首 1 O
王 5 B-artist
力 5 I-artist
宏 5 E-artist
的 1 O
歌 1 O
曲 1 O
心 5 B-song
跳 5 E-song
播 1 O
放 1 O
七 5 B-song
里 5 I-song
香 5 E-song
2、模板文件
有了训练样本和测试样本之后,接下来准备模板文件,如下图:
# Unigram
U00:%x[-2,0]
U01:%x[-1,0]
U02:%x[0,0]
U03:%x[1,0]
U04:%x[2,0]
U05:%x[-1,0]/%x[0,0]
U06:%x[0,0]/%x[1,0]
U07:%x[-2,0]/%x[-1,0]/%x[0,0]
U08:%x[-1,0]/%x[0,0]/%x[1,0]
U09:%x[0,0]/%x[1,0]/%x[2,0]
U10:%x[0,0]/%x[0,1]
U11:%x[-1,1]/%x[0,1]
U12:%x[0,1]/%x[1,1]
# Bigram
B
其中,"%x[row,col]",row表示相对于当前token的行的位置,col表示列的位置(表示特征)。例如上文中的query:来一首王力宏的歌曲心跳,假如"王"作为当前token,模板"U00:%x[-2,0]"中"0"表示采用第1列特征,"-2"表示当前token的前2行,那么模板"U00:%x[-2,0]"对应的是"一"。模板"U09:%x[0,0]/%x[1,0]/%x[2,0]"对应的是"王/力/宏"。模板"U10:%x[0,0]/%x[0,1]"对应的是"王/5"。模板"U11:%x[-1,1]/%x[0,1]"对应的是"1/5"。
模板文件中一般包含两种模板。
第一种是Unigram template:第一个字符是U,这是用于描述unigram feature的模板。每个"%x[row,col]"生成一个CRF中的点函数(State Function): ,其中
为
时刻的标签(预测标记),
为
时刻的上下文。还是以"王"作为当前字,根据第一个模板"U00:%x[-2,0]"能得到特征函数如下:
func1=if(output=B-artist and feature='U00:一' ) return 1 else return 0
func1=if(output=I-artist and feature='U00:一' ) return 1 else return 0
func1=if(output=E-artist and feature='U00:一' ) return 1 else return 0
func1=if(output=B-song and feature='U00:一' ) return 1 else return 0
func1=if(output=I-song and feature='U00:一' ) return 1 else return 0
func1=if(output=E-song and feature='U00:一' ) return 1 else return 0
func1=if(output=O and feature='U00:一' ) return 1 else return 0
其中"output=B-artist"指的是当前token的预测标记,也就是"王"的预测标记,每个模板会把所有可能的预测标记(BIEO)都计算一遍,然后通过训练确定每种标记的权重,合理的标记在训练样本中出现的次数多,对应的权重就高,不合理的标记在训练样本中出现的少,对应的权重就少。利用模板生成特征函数就是把所有可能的特征函数都列出来,由模型通过训练决定每个特征函数的重要程度。 Unigram template中,每个query根据模板生成的特征函数共有,其中K表示预测标记的类别个数(上例中有7个,"B-song、B-artist、O、..."),M表示特征模板的个数。
第二种是Bigram template:第一个字符是B,这是用于描述Bigram feature的模板。每个"%x[row,col]"生成一个CRF中的边函数(Edge Function):,其中
为
时刻的标签(预测标记),也就是说,Bigram类型与Unigram类型大致相同,只是还要考虑到
时刻的预测标记。Bigram template中,每个query根据模板生成的特征函数共有
,其中K表示预测标记的类别个数(上例中有7个,"B-song、B-artist、O、..."),M表示特征模板的个数。由于Bigram template生成的特征模板个数过于庞大,在训练和预测阶段会带来比较大的内存和计算开销,同时Unigram template可以模拟Bigram template的二元特征,例如"U05:%x[-1,0]/%x[0,0]",所以通常只用Unigram template。
说完两种特征模板,我们回过头来看一下CRF的条件概率函数,里边的特征函数包含两种:转移特征函数、状态特征函数。个人理解:Unigram template每个"%x[row,col]"代表状态特征函数,Bigram template每个"%x[row,col]"代表转移特征函数。但是一般情况下我们只用Unigram template,那么转移特征函数如何表示呢?就是我们通过Unigram template中类似"U05:%x[-1,0]/%x[0,0]"的这种组合来实现。
3、训练参数
如果我们已经准备完成训练样本以及模板文件,那么我们就可以开始训练了,模型结果保存至模型文件中,如下:
crf_learn = "./CRF++-0.58/crf_learn" #训练函数
template_file = "./crf_template_file" #模板文件
train_file = "./crf_train_data" #训练样本
model_file = "./model/crf" #模型文件
${crf_learn} ${template_file} ${train_file} ${model_file}
这里有四个主要的参数,来调整模型的训练:
-a, –algorithm=(CRF-L1|CRF-L2|MIAR) #训练算法,默认CRF-L2
-c, –cost=FLOAT #代价参数,默认1.0
-f, –freq=INT #特征至少出现的频次,默认1
-p, –thread=INT #CPU线程数,默认1
-a 训练算法选择上,通常基于L2正则化的准确率略高于基于L1正则化的准确率,但是基于L2正则化会产生大量非零参数,导致模型过大,所以根据实际业务需求选择CRF-L1还是CRF-L2。
-c 代价参数的选择上,越大则模型越拟合训练样本,用于调整模型对训练样本的拟合程度,或者说用于平衡过拟合与欠拟合。
-f 频次控制的选择上,用于控制训练样本中特征出现的频次至少满足N个,当使用CRF++进行大规模数据训练时,只出现一次的特征可能会有成百上千万个,通过这个参数能够有效控制特征规模。
-p CPU线程数的选择上,越多训练速度越快。
其他的可选参数:
-m, –maxiter=INT #设置LBFGS的最大迭代次数,默认10k
-n, #输出n-best结果,INT
-e, –eta=FLOAT #设置终止标准,默认0.0001
-C, –convert #将文本模式转为二进制模式
-t, –textmodel #为调试建立文本模型文件
-H, –shrinking-size=INT #设置迭代变量次数,默认20
-v, –version #显示版本号并退出
-h, –help #显示帮助并退出
以下举个例子:
crf_learn = "./CRF++-0.58/crf_learn" #训练函数
template_file = "./crf_template_file" #模板文件
train_file = "./crf_train_data" #训练样本
model_file = "./model/crf" #模型文件
train_log = "./train.log" #训练日志
${crf_learn} -a CRF-L1 -f 2 -p 8 -m 8000 ${template_file} ${train_file} -t ${model_file} > ${train_log}
最终模型结果如下所示(数据是楼主随意造的,明白意思即可):每一行表示对应模板的结果,每一列表示当前token可能属于各个类别的概率。预测过程,就是加载这个文件,然后类似查表操作得到对应的概率,最后用维特比算法求得概率最大的路径。
[u_features]:200000
U_feature/label B-artist I-artist E-artist B-song I-song E-song O
0 U00:_B-2 -0.12345 0.000000 0.000000 -0.123 0.000000 0.0000 0.9
5 U01:_B-1 -0.12345 0.000000 0.000000 -0.123 0.000000 0.0000 0.9
10 U02:我 -0.12345 0.000000 0.000000 -0.123 0.000000 0.0000 0.9
15 U03:想 -0.12345 0.000000 0.000000 -0.123 0.000000 0.0000 0.9
20 U04:听 -0.12345 0.000000 0.000000 -0.123 0.000000 0.0000 0.9
25 U05:_B-1/我 -0.12345 0.000000 0.000000 -0.123 0.000000 0.0000 0.9
30 U06:我/想 -0.12345 0.000000 0.000000 -0.123 0.000000 0.0000 0.9
35 U07:_B-2/_B-1/我 -0.12345 0.000000 0.000000 -0.123 0.000000 0.0000 0.9
40 U08:_B-1/我/想 -0.12345 0.000000 0.000000 -0.123 0.000000 0.0000 0.9
45 U09:我/想/听 -0.12345 0.000000 0.000000 -0.123 0.000000 0.0000 0.9
三、总结
1、token的粒度:可以是字粒度,也可以是词粒度,也可以其它形式。英文建议至少词粒度,而不是character粒度。例如,如果解决pattern明确的NER问题,可以用字粒度、词粒度,可以用于解决槽位的欠召、也可以用于解决槽位的过招(需要一定的技巧)。
2、特征的选择:需要根据具体的业务进行特征的选择。如果有个多个特征,可以增加"U10:%x[0,0]/%x[0,1]"这种类型的模板,能够把特征之间的关联对最终的分类结果的影响找出来。也可以尝试不同特征的上下文关系。