PCFG parser及实现

 

源代码点击下载

背景介绍:

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

  • 6
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值