源代码点击下载
背景介绍:
PCFG是ProbabilisticContext Free Grammar的简写,是Chomsky范式中的2型文法。句法分析就是解析出句子的词之间的结构关系,对于什么是句法分析,我们并不陌生,因为我们从上小学开始就经过了句法分析的“严酷”训练,回想一下,语文老师教我们怎么解析一个句子的主谓宾定状补。
为了便于科学描述和交流,把一个句法表示成四元组:
G= (N,T,N1, R)
而PCFG是多了概率的句法:
PCFG= (N,T,N1,R,P)
其中,
N代表非终结符集合
T代表终结符集合
N1代表初始非终结符
R代表产生规则集
P 代表每个产生规则的统计概率
如下为一个PCFG规则集以及概率
S -> NP VP 0.7;
S -> VP 0.2;
S -> NP 0.1;
S -> VC 0.1;
NP -> noun 0.3;
NP -> adj noun 0.2;
NP -> DJ 0.2;
NP -> DJ NP 0.3;
DJ -> VP de 0.4;
DJ -> NP de 0.6;
VP -> VC NP 1.0;
VC -> vt adj 0.3;
VC -> VC utl 0.5;
VC -> vt 0.2;
Grammar 1
对于非科班出身的同学,理解这些可能有些难度。举个例子来说:咬死了猎人的狗(这是一个歧义句,分词结果:咬/vt 死/adj 了/utl 猎人/noun 的/de 狗/noun)它的句法结构树如下:
如上图所示,“咬死了猎人的狗”每个词可以理解为T的一个子集。其他的英文字符理解为N的一个子集。S即是N1。
从句法树中可以看出,句法树1是一个NP结构,着重强调名词短语-狗,可理解为有条狗咬死了猎人,这句话描述的就这个凶手---狗。句法树2是一个 VP结构,猎人的狗作为一个名词短语,说明猎人的狗被咬死了。
PCFG的三个问题:
(1)给定一个句子,估计产生句子的概率;
(2)在语句句法结构有歧义的情况下,如何快速选择最佳的句法分析;
(3)如何从语料库中训练文法的参数。
问题1解答:
问题1是计算给定的句子,按照句法规则计算所有可能句法树,并求出其概率之和。用w(p,q)表示句子中从下标为p到下标为q(包括q)的词。句子分词后,第一个词标记为1,第二个标记成2,以此类推。那么”咬死猎人的狗”为例,对应的分词和下标是:
咬 死 了 猎人 的 狗
1 2 3 4 5 6
那么w(1,3) = 咬死了;w(2,4) = 死了猎人;w(1,6)=咬死了猎人的狗。所以求解问题1,对于这句话来说,相当于求解w(1,6)所有句法树的概率之和,也就是图1中句法树1和句法树2的概率之和。
用P(NJpq)表示从p开始到q的所有符合Nj的产生式的概率。那么产生” 咬死了猎人的狗”的概率就是P(N11,6)。
计算这个概率的方法有两种,一种是从内部计算,另外一种是外部计算。
内部算法
要计算P(NJpq),我们可以从内部把pq字符串一分为2(这里假设所有产生式都是规范的Chomsky产生式,Nf- > Nr Ns) , 分割点为e,那么只要看看w(p,e) 是否符合Nr,且w(e+1, q)符合Ns,然后把所有符合规则的概率相加,就计算出来了P(NJpq)。内部算法公式如下:
举例来说吧,我们来计算”咬死了”是VC的概率。对于我们的语法Grammar 1, 由VC发出的规则有三条:
VC -> vtadj 0.3;
VC -> VCutl 0.5;
VC -> vt 0.2;
咬/vt 死/adj 了/utl。这里假设P(咬|vt) = 1.0, P(死|adj) = 1.0, P(了|utl) = 1.0。
对于第一条规则,VC -> vtadj 0.3; 我们分别要计算P(咬|vt)*P(死了|adj) + P(咬死|vt)*P(了|adj)的概率,发现都不符合,所以这条概率为0。
对于第二条规则,VC -> VCutl 0.5;我们分别要计算 P(咬|VC)*P(死了|utl) + P(咬死|VC)*P(了|utl)的概率。P(死了|utl) = 0, P(咬死|VC) = 0.3(只符合第一条规则),P(了|utl) = 1.0。故此条的概率为 (0 +0.3) * 0.5 = 0.15
对于第三条规则,VC ->vt 0.2;我们要计算P(咬死了|vt)的概率,为0。
那么“咬死了”为VC的概率 = 0 + 0.15 + 0 = 0.15;
由公式我们可知这是一个递归过程,我们简单的交给递归函数去实现就可以了。我们定义GetBeta(double *** beta, p, q, j)为计算Nj->W(p,q)的概率,那么其核心算法就可以表示成如下的递归式调用:
GetBeta(……){
//……..
for(intd = p; d< q ; d ++){
prob+= GetBeta(beta, p, d, rule_id_table[it->r1]) * GetBeta(beta, d+1, q, rule_id_table[it->r2]);
}
prob*= it->prob;
//……
}
//详细代码在下载包内。
最后,如果要计算整条词串的概率,只需要计算N1 –> w(1,n)的概率即可。
外部算法
外部算法的思想是对于计算W(p,q)为Nj的概率,假设已知其父串Nf的概率(Nf -> Nj Ng 或 Nf->Ng Nj),只要计算除去串中w(p,q)部分,其他部分为Ng的概率。举例来说,假设我们已知了”咬死了”为VC的概率,对于产生式VC->VC utl, 我们要计算”咬死”为VC的概率,等同于我们计算”了”为utl的概率。”了”为utl的概率我们可以用内部算法计算获得。外部算法的公式如下:
核心算法:
GetAlpha(.....){
//.......
for(int e = q + 1; e <= m; e++ ){
al= GetAlpha(alpha, p, e, rule_id_table[rule->first], m, beta);
be= GetBeta(beta, q+1,e,rule_id_table[probrule->r2]);
prob+= al * be;
//cout<<"alpha["<<rule->first<<"]["<<p<<","<<e<<"]="<<al<<"beta["
//<<probrule->r2<<"]"<<"["<<q+1<<","<<e<<"]="<<be<<"prob="<<prob<<endl;
}
prob *= probrule->prob;
//......
}
问题2解答
问题2的解答使用经典的Viterbi算法。即每次都找最优的,通过回溯的方法,找到最佳解。其核心部分也是一个递归调用的方法。这里的Viterbi的实现和HMM的Viterbi实现方式上有所不同(HMM的代码将来会贴出来),HMM中使用循环迭代方式。公式如下:
GetDeta(....){
// .......
for(inte = p; e < q; e ++){
prob= it->prob * GetDeta(deta, p, e, rule_id_table[it->r1], bestpath)
*GetDeta(deta, e+1, q, rule_id_table[it->r2], bestpath);
if(prob> maxprob){
maxprob= prob;
best.e= e;
best.r1= rule_id_table[it->r1];
best.r2= rule_id_table[it->r2];
}
}
//.........
}
问题3解答
对于有标注好的训练语料,解决问题3就比较简单了,可以使用最大似然概率法来统计各条产生规则的概率,最后保证每条sum{Nf-> Nj Ng} = 1.0即可。
对于从生语料中学习,可以使用概率期望算法,分别计算出整条句子的期望概率,然后计算每个产生式的期望值,做商。最后迭代到误差小于某一值。
代码就不再写了,因为所有需要的元素都已经实现了,只要调用下就能求得。
小结
在代码编写过程中,遇到两个bug,总共浪费了10多个小时找出来。
第一个bug是在写内部算法时,把除了W(k,k)除了词性以外的其他非终结符概率都设置成了0,导致对于单产生式的语法结果都变成了0.举例来说,如咬/vt,我初始化的时候,把beta[0][0][vt] = 1.0, beta[0][0][i] = 0, i!= vt; 那么在获取产生式VC->vt的概率时,就查到概率为0,直接返回,最后结果当然就不对了。对于这种bug,目前还真没有什么好办法避免,除非花大量时间去弄明白每条公式的意义,以及每个细节的详细流程。不过这又不太现实,人总是会被某些思维定式给影响住。这种只好遇到bug,进行打log分析吧。
第二个bug花的时间比较长,写完代码是正好周五下午,搞到很晚没解决掉,周一上午才发现问题。这个bug是在写外部算法时两次初始化beta时产生的。还记得写这段代码时我从内部算法中拷贝了代码过来,发现初始化beta有两处代码调用,于是把这段代码重构成了一个函数,等函数抽取完,竟然忘了把copy的代码beta改成alpha,这样在初始化alpha的地方又初始化了一遍beta...就是这一处bug,弄的我百思不得骑姐!将来如何避免这种bug呢,一是写伪代码,二copy代码时要彻底,甚至不copy,三不要同时做两件事情!
参考
[1] 统计自然语言处理基础,Christopher.D.Manning,Hinrich Schutze.
[2] 一个简单的PCFG分析器 http://www.pudn.com/downloads156/sourcecode/compiler/detail694248.html