您的位置:首页 > Web前端

Simple and Accurate Dependency Parsing Using Bidirectional LSTM Feature Representations

2017-01-08 14:08 381 查看
对于句子s,将其分词为n个词wi,每个词wi对应取wi的词性为ti,取向量e(wi),和词性向量e(ti),将词向量和词性向量串联得到新的向量xi=concat(e(wi),e(ti))。

BILSTM 向量vi=BILSTM(x_1:n,i)=concat(RNN_F(x_1:i),RNN_R(x_n:i))

将一小部分的BILSTM 向量vi串联,从而得到特征向量F,实际的特征向量F依赖于句法分析,我们将在讨论句法分析的时候分析特征向量。得到特征向量后,我们就可以将特征向量输入常规的句法分析网络,例如可以通过一个非线性函数,例如一个MLP网络,得到每个句法标记的得分:


 



除了使用基于BILSTM的特征向量外,我们利用标准的句法分析技术,联合训练BILSTM模型和句法分析模型,这使得所学习到的特征适合于句法分析任务。

考虑串联两个BILSTM 向量vi,vj,x=concat(vi,vj),将x输入MLP得到输出得分y。

Transition-based parser:

本文所使用的句法分析网络为transition-based parser,算法如下:

 


给定一个句子s,初始化句法分析网络的配字c(步骤2),之后,一个特征函数fi(c),将配置c表示为特征向量v_c,将特征向量输入到socoring 函数,将scores,c,trainsition组合成对,socre函数得到可能的转移t,旋转得分最高的转移t’,将转移t’应于语配置网络,再有配置网络输出得到句法分析树(步骤6).

转移系统t与配置系统不同,转移系统可以决定句法分析,特征函数和得分函数。

本分采用的转移系统为Arc-Hybrid Transition System:

在Arc-Hybrid Transition System系统中,配置c=(sigma,belta,T),有stack sigma,buffer belta, 集合T组成dependency arcs。Stack 和buffer保存有句子元素的索引,给定句子s,句子分词为wi,词性标注为ti,系统初始化为空的stack,空的arc set,且belta=1,...,n,ROOT,这里ROOT为root的索引。任何配置c包含有一个空的stack和一个buffer,这个buffer只包含有ROOT,则这个配置c为terminal,并且此时可以得到parse tree为arc set Tc。Arc-Hybrid Transition System允许3个可能的转移,分别为shift,left_l,right_l,定义如下:

 


Shift移动buffer(b0)的第一个元素到stack。Left_l移除stack的最顶端的元素,并用bo 和标记l代替连接它,right_l从stack移除s0,并将so连接stack(s1)的下一个元素,添加arc(s1,s0,l)。

Scoring function:


 
Simple Feature Function:

我们的特征函数为串联的stack的顶端的3个BILSTM向量和buffer的第一个元素。例如对于配置c=(...|s2|s1|s0, b0|...,T),提取配置c的特征向量:


 
该特征函数考虑了s0,s1,s2,b0的BLSTM表示,其可以受转移系统的影响得分。

Details of the Training Algorithm:

句法分析配置c的损失函数定义为:


 

A为可能的转移集合,G为正确的转移集合.函数的目的是使得正确分类的得分与错误分类的得分的差距最大,网络结构可见下如:



代码解析:

网络结构为,embedding+bilstm+MLP

Emdeding层代码如下:

ef getWordEmbeddings(self, sentence, train):
for root in sentence:
c = float(self.wordsCount.get(root.norm, 0))#get the word index,root.norm is the word
dropFlag =  not train or (random.random() < (c/(0.25+c)))
root.wordvec = lookup(self.model["word-lookup"], int(self.vocab.get(root.norm, 0)) if dropFlag else 0)#得到字的向量表示
root.posvec = lookup(self.model["pos-lookup"], int(self.pos[root.pos])) if self.pdims > 0 else None#得到词性的向量表示
if self.external_embedding is not None:#是否添加附加向量
if not dropFlag and random.random() < 0.5:
root.evec = lookup(self.model["extrn-lookup"], 0)
elif root.form in self.external_embedding:
root.evec = lookup(self.model["extrn-lookup"], self.extrnd[root.form], update = True)
elif root.norm in self.external_embedding:
root.evec = lookup(self.model["extrn-lookup"], self.extrnd[root.norm], update = True)
else:
root.evec = lookup(self.model["extrn-lookup"], 0)
else:
root.evec = None
root.ivec = concatenate(filter(None, [root.wordvec, root.posvec, root.evec]))#串串联字向量、词性向量作为输入向量

if self.blstmFlag:
forward  = self.surfaceBuilders[0].initial_state()#前向lstm
backward = self.surfaceBuilders[1].initial_state()#逆向lstm

for froot, rroot in zip(sentence, reversed(sentence)):
forward = forward.add_input( froot.ivec )
backward = backward.add_input( rroot.ivec )
froot.fvec = forward.output()#前向lstm输出
rroot.bvec = backward.output()#逆向lstm输出
for root in sentence:
root.vec = concatenate( [root.fvec, root.bvec] )#bilstm输出

if self.bibiFlag:
bforward  = self.bsurfaceBuilders[0].initial_state()
bbackward = self.bsurfaceBuilders[1].initial_state()

for froot, rroot in zip(sentence, reversed(sentence)):
bforward = bforward.add_input( froot.vec )
bbackward = bbackward.add_input( rroot.vec )
froot.bfvec = bforward.output()
rroot.bbvec = bbackward.output()
for root in sentence:
root.vec = concatenate( [root.bfvec, root.bbvec] )

else:
for root in sentence:
root.ivec = (self.word2lstm * root.ivec) + self.word2lstmbias
root.vec = tanh( root.ivec )

mbedding层就是对于输入已经分词、词性标注的句子s,对于s的每个词wi,对于词性pi,查找wi,pi对应的向量表示,并将其串联为输入向量input_bilstm,将vi输入bilstm得到输出向量ivec。

训练阶段:

我们分配两个数组,stack,buf,首先初始化stack为空,并将句子s中所有的词放入buf,

stack = ParseForest([])
buf = ParseForest(sentence)

将取b0=buf[0],s0=stack[-1],s1=stack[-2],s2=stack[-3],将其对应的bilstm向量串联得到MLP的输入向量input_mlp,将输入向量输入mlp1,mlp2网络得到routput,output,mlp1网络输出维度为2*size(relation)+1,即用于预测relation,mlp2输出维度为3,用于只是stack,buf中元素的输入或移除(具体原理不明白??),此部分代码为:

def __evaluate(self, stack, buf, train):
topStack = [ stack.roots[-i-1].lstms if len(stack) > i else [self.empty] for i in xrange(self.k) ]#去s0,s1,s2对应的bilstm输出特征向量
topBuffer = [ buf.roots[i].lstms if len(buf) > i else [self.empty] for i in xrange(1) ]#取bo对应的bilstm输出特征向量

input = concatenate(list(chain(*(topStack + topBuffer))))#特征向量串联
#将特征向量输入MLP
if self.hidden2_units > 0:
routput = (self.routLayer * self.activation(self.rhid2Bias + self.rhid2Layer * self.activation(self.rhidLayer * input + self.rhidBias)) + self.routBias)
else:
routput = (self.routLayer * self.activation(self.rhidLayer * input + self.rhidBias) + self.routBias)

if self.hidden2_units > 0:
output = (self.outLayer * self.activation(self.hid2Bias + self.hid2Layer * self.activation(self.hidLayer * input + self.hidBias)) + self.outBias)
else:
output = (self.outLayer * self.activation(self.hidLayer * input + self.hidBias) + self.outBias)

scrs, uscrs = routput.value(), output.value()#.value为relation(141),r(3)的概率,routput,output为对应的交叉损失熵

uscrs0 = uscrs[0]
uscrs1 = uscrs[1]
uscrs2 = uscrs[2]
if train:#训练阶段
output0 = output[0]
output1 = output[1]
output2 = output[2]
#ret[0]=[rel(依存关系),0,奇数下标依存关系对应的概率+uscrs[0],依存个关系对于的估计网络输出+output[1]]
ret = [ [ (rel, 0, scrs[1 + j * 2] + uscrs1, routput[1 + j * 2 ] + output1) for j, rel in enumerate(self.irels) ] if len(stack) > 0 and len(buf) > 0 else [],
[ (rel, 1, scrs[2 + j * 2] + uscrs2, routput[2 + j * 2 ] + output2) for j, rel in enumerate(self.irels) ] if len(stack) > 1 else [],
[ (None, 2, scrs[0] + uscrs0, routput[0] + output0) ] if len(buf) > 0 else [] ]
else:#预测阶段
s1,r1 = max(zip(scrs[1::2],self.irels))#取奇数下标索引,求其最大值s1对应的索引值为当前依存关系r1
s2,r2 = max(zip(scrs[2::2],self.irels))#去偶数下标索引,求其最大值s2对应的索引值为当前依存关系r2
s1 += uscrs1
s2 += uscrs2
ret = [ [ (r1, 0, s1) ] if len(stack) > 0 and len(buf) > 0 else [],
[ (r2, 1, s2) ] if len(stack) > 1 else [],
[ (None, 2, scrs[0] + uscrs0) ] if len(buf) > 0 else [] ]
return ret

routput为预测relation的交叉损失熵,routput.value为预测relation的概率。

得到预测值之后 ,我们便可以求解总的损失函数:

for iSentence, sentence in enumerate(shuffledData):
if iSentence % 100 == 0 and iSentence != 0:
print 'Processing sentence number:', iSentence, 'Loss:', eloss / etotal, 'Errors:', (float(eerrors)) / etotal, 'Labeled Errors:', (float(lerrors) / etotal) , 'Time', time.time()-start
start = time.time()
eerrors = 0
eloss = 0.0
etotal = 0
lerrors = 0
ltotal = 0

sentence = sentence[1:] + [sentence[0]]#将句子中最后一个词放到数组最后
self.getWordEmbeddings(sentence, True)#得到每个词的输入向量表示
stack = ParseForest([])#初始化stack为空
buf = ParseForest(sentence)#将sentence放入buf

for root in sentence:
root.lstms = [root.vec for _ in xrange(self.nnvecs)]#词的lstm输入为self.nnvecs个输入向量串联

hoffset = 1 if self.headFlag else 0

while len(buf) > 0 or len(stack) > 1 :
scores = self.__evaluate(stack, buf, True)#MLP对数如特征估计得分
scores.append([(None, 3, ninf, None)])

alpha = stack.roots[:-2] if len(stack) > 2 else []
s1 = [stack.roots[-2]] if len(stack) > 1 else []
s0 = [stack.roots[-1]] if len(stack) > 0 else []
b = [buf.roots[0]] if len(buf) > 0 else []
beta = buf.roots[1:] if len(buf) > 1 else []

left_cost  = ( len([h for h in s1 + beta if h.id == s0[0].parent_id]) +
len([d for d in b + beta if d.parent_id == s0[0].id]) )  if len(scores[0]) > 0 else 1
right_cost = ( len([h for h in b + beta if h.id == s0[0].parent_id]) +
len([d for d in b + beta if d.parent_id == s0[0].id]) )  if len(scores[1]) > 0 else 1
shift_cost = ( len([h for h in s1 + alpha if h.id == b[0].parent_id]) +
len([d for d in s0 + s1 + alpha if d.parent_id == b[0].id]) )  if len(scores[2]) > 0 else 1
costs = (left_cost, right_cost, shift_cost, 1)

bestValid = max(( s for s in chain(*scores) if costs[s[1]] == 0 and ( s[1] == 2 or s[0] == stack.roots[-1].relation ) ), key=itemgetter(2))
bestWrong = max(( s for s in chain(*scores) if costs[s[1]] != 0 or ( s[1] != 2 and s[0] != stack.roots[-1].relation ) ), key=itemgetter(2))
best = bestValid if ( (not self.oracle) or (bestValid[2] - bestWrong[2] > 1.0) or (bestValid[2] > bestWrong[2] and random.random() > 0.1) ) else bestWrong

if best[1] == 2:#未得到relation,将b0移入stack
stack.roots.append(buf.roots[0])
del buf.roots[0]

elif best[1] == 0:
child = stack.roots.pop()
parent = buf.roots[0]#父结点为b0

child.pred_parent_id = parent.id
child.pred_relation = best[0]

bestOp = 0
if self.rlMostFlag:
parent.lstms[bestOp + hoffset] = child.lstms[bestOp + hoffset]
if self.rlFlag:
parent.lstms[bestOp + hoffset] = child.vec

elif best[1] == 1:
child = stack.roots.pop()
parent = stack.roots[1]#父结点为s0

child.pred_parent_id = parent.id
child.pred_relation = best[0]
bestOp = 1
if self.rlMostFlag:
parent.lstms[bestOp + hoffset] = child.lstms[bestOp + hoffset]
if self.rlFlag:
parent.lstms[bestOp + hoffset] = child.vec
if bestValid[2] < bestWrong[2] + 1.0:
loss = bestWrong[3] - bestValid[3]#损失函数
mloss += 1.0 + bestWrong[2] - bestValid[2]
eloss += 1.0 + bestWrong[2] - bestValid[2]
errs.append(loss)

if best[1] != 2 and (child.pred_parent_id != child.parent_id or child.pred_relation != child.relation):
lerrors += 1#id或者relation估计不准确,则错误加1
if child.pred_parent_id != child.parent_id:#如果估计id不准确,则错误加1
errors += 1
eerrors += 1
etotal += 1
if len(errs) > 50: # or True:
#eerrs = ((esum(errs)) * (1.0/(float(len(errs)))))
eerrs = esum(errs)
scalar_loss = eerrs.scalar_value()
eerrs.backward()
self.trainer.update()
errs = []
lerrs = []
renew_cg()
self.Init()
if len(errs) > 0:
eerrs = (esum(errs)) # * (1.0/(float(len(errs))))
eerrs.scalar_value()
eerrs.backward()#根据损失函数求梯度
self.trainer.update()#参数更新

errs = []
lerrs = []

renew_cg()

self.trainer.update_epoch()
print "Loss: ", mloss/iSentence
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: