BERT在MRC任务上已经达到了很高的效果,但是缺点在于BERT的输入最多只能512个单词。而对于MRC任务来说,有的数据集的文章特别长。因此想要用BERT处理这类数据集,就必须将文章切分开。每一篇文章与问题独立的构成一个example,也就是一个样本作为BERT的输入。
拿BERT的源码举例:
import collections
length_of_doc_tokens=700#文章的长度为700
max_seq_length=512
length_of_query_tokens=9#问题长度为9
max_tokens_for_doc=max_seq_length-length_of_query_tokens-3#减去问题长度,[CLS],[SEP],[SEP]后才是文章的最大长度
_DocSpan=collections.namedtuple("DocSpan",["start","length"])
doc_spans=[]
start_offset=0
doc_stride=128#doc_stride指的是在切分文章时的滑动步长
while start_offset<length_of_doc_tokens:
length=length_of_doc_tokens-start_offset
if length>max_tokens_for_doc:
#剩余的文章长度大于文章可以传进BERT的最大长度
length=max_tokens_for_doc
doc_spans.append(_DocSpan(start=start_offset,length=length))
if start_offset+length==length_of_doc_tokens:
break#当偏移位置加上切分的文章长度等于该文章的长度时就可以退出了
start_offset+=min(doc_stride,length)#从滑动步长和剩余文章长度中选出最小的
#通常来说剩余文章的长度都是要小于doc_stride的
print(max_tokens_for_doc)#500
我们来一步一步的计算切分过程:
- 首先start_offset=0,length=700>max_tokens_for_doc,所以第一次的切分就是[0-500],由于length=500>doc_stride=128,所以start_offset=128
- 第二次start_offset=128,length=700-128=572>max_tokens_for_doc,所以第二次的切分就是[128-628],由于length=500>doc_stride=128,所以start_offset这时等于256
- 第三次start_offset=256,length=700-256=444<max_tokens_for_doc,所以第三次的切分就是[256-700]
因此第一次切分的文章长度为500,第二次切分的文章长度为500,第三次剩下的文章长度为444
看来是没有错的。doc_stride设置为321,那么此时的切分结果如下:
显然第一次是从0–500,第二次是从321–700
切分后的每一篇segment与问题构成一个样本作为BERT的输入,比如在文章长度为700,doc_stride为128的情况下,会切分出来3个segment,于是也就和同一个问题组成了三个example,需要注意的是如果某个segment不包含有答案,那么这个segment是不用的。
之前的做法有两个弊端:
- 以固定长度分割文章,那么就可能导致某些分割后的文章所包含的答案的上下文不充分,也就是指答案在分割的文章中的位置比较靠近边缘。
论文的图1鲜明的指出当答案的中心词位置与文章的中心词位置之间的距离影响着模型预测答案位置的准确率。
如下图所示,假如红色的单词是答案,绿色的单词是,那么当前的文章就包含有充足的要预测答案的上下文
下图这种情况下答案的上下文就没有上一个的充足
- 第二个弊端就是当分割完成之后,每一篇分割后的文章独立的与问题构成独立的样本送进BERT,假如一篇文章被分割成两份,那么显然模型在阅读了第一个分割片段后的语义信息,有助于模型在第二个片段提取答案。而之前的做法忽略了这一点。
这篇论文的两个创新点正是基于上述两个弊端提出的。
模型:
首先BERT的输入形式如下:
我们重点关注CLS,BERT原文中对CLS的描述为:
The final hidden state corresponding to this token is used as the aggregate sequence representation for classification tasks.
也就是说CLS这个token的最终的hidden state聚集了整个sequence的表示,因此这篇论文的模型的循环机制的原理就是:
h t − 1 h_{t-1} ht−1,当前segment的CLS的表示作为LSTM的输入 x t x_t xt,然后当前的LSTM的输出$h_{t}作为下一时刻的segment的hidden state
将CLS的hidden state通过LSTM来达到循环的目的,具体的就是将前一个segment的LSTM的输出作为当前LSTM的hidden state看论文的源码:
论文的run_RCM_triva.py以及其它的两个py文件类似,区别在于数据处理的方式要依据数据集的差异制定。我们重点探讨模型,因此就以run_RCM_triva.py为例:
在train_model中有for t in range(args.max_read_times)
根据这行代码我们了解到模型是固定了分割次数的,也就是说模型会在一篇document上分割max_read_times次数。
在175行可以看到模型需要的参数其中就有prev_hidden_states,而整个调用过程就是prev_hidden_states=model(prev_hidden_states)
所以达到了循环机制的目的。
这里有一点需要注意,我们打开modeling_RCM.py,定位在158行
outputs其实是包含有两个tensor的,第一个是sequence_output,第二个是pooler_output,
sequence_output的第一个单词就是CLS的表示,pooler_output也是CLS的表示,不同的是pooler_output是CLS的最后一层的hidden state通过一个带有tanh激活函数的全连接层。具体的请详细阅读transformers的源码.
难点在于利用强化学习来解决分割document的问题
这里说一下基于策略的强化学习(policy-based RL)
RL的思想是:环境environment输入给智能体agent一个状态s,agent根据策略
π
θ
\pi_\theta
πθ做出一个行为a,然后得到环境给它的奖励r的同时环境转移到了下一个状态。
θ
\theta
θ为模型的参数。如下图所示
其中环境与agent交互,环境给agent的是状态和奖励,agent做出的行为会改变环境的状态。
在深度学习中应用强化学习时,策略policy就是神经网络NN。
好的接下来我们定义目标函数:
我们从agent与环境交互所产生的轨迹的角度来定义目标函数:
参照上图,分别解释每一行公式:
首先,给定一个初始状态
s
1
s_1
s1,agent会在
s
1
s_1
s1下与环境进行交互,产生一条轨迹
τ
\tau
τ
我们希望的是在这条轨迹
τ
\tau
τ上的所有奖励
r
t
τ
r_t^\tau
rtτ的累计值最大。
那么目标函数自然就是期望所有的轨迹上的累计奖励最大。
也就是说我们希望在最优的参数 θ ∗ \theta^* θ∗下,根据策略 π θ ∗ \pi_{\theta^{*}} πθ∗,我们任意得到的轨迹 τ \tau τ上的累计奖励值都是最大的
所以接下来的问题就是如何优化策略的参数 θ \theta θ。
对目标函数求导
首先我们重新写出来目标函数:
第一行我们已经知道,它的含义就是对策略
π
θ
\pi_\theta
πθ下的任意一条轨迹上的累计奖励值的期望。
第二行公式就是将期望用公式符号表达出来,也就是在策略 π θ \pi_\theta πθ下产生的轨迹 τ \tau τ的概率乘以这条轨迹 τ \tau τ的累计奖励值。
但是轨迹的个数是不确定的,因为随便拿一个初始状态,都会产生很多条轨迹。因此用采样近似的方式。取出来N条轨迹,我们的目标是这N条轨迹上的累计奖励值求和后的值最大。
而给定一个初始状态,在参数
θ
\theta
θ下产生一条轨迹的概率为:
第二行是对概率取log,第三行对log概率求导。
这里的第一行公式就是指一条轨迹的产生的概率为:
初始状态
s
1
s_1
s1的概率乘以在
s
1
s_1
s1状态下根据
π
θ
\pi_\theta
πθ采取行为
a
1
a_1
a1的概率乘以在
s
1
s_1
s1状态下采取了行为
a
1
a_1
a1后得到的奖励
r
1
r_1
r1以及环境转移为状态
s
2
s_2
s2的概率,以此类推。
注意到
P
(
s
1
τ
)
P(s_1^\tau)
P(s1τ)是初始状态的概率,
P
(
s
t
+
1
τ
,
r
t
τ
∣
a
t
τ
,
s
t
τ
)
P(s_{t+1}^\tau,r_t^\tau|a_t^\tau,s_t^\tau)
P(st+1τ,rtτ∣atτ,stτ)是由环境给出的,因此这两项与参数
θ
\theta
θ无关,所以对参数的导数为0
我们重新整理一下:
在已知上面的公式后,我们对目标函数求导:
第一行公式就是对目标函数求导数,奖励是环境给的,与策略无关,因此只需要计算 P ( τ ∣ θ ) P(\tau|\theta) P(τ∣θ)的导数。
第二行的目的是为了构造出对log概率求导,因为 d log f ( x ) = d f ( x ) f ( x ) d\log f(x)=\frac{df(x)}{f(x)} dlogf(x)=f(x)df(x),于是也就有了第三行公式
最后我们按照前面所说的,将
∑
τ
P
(
τ
∣
θ
)
\sum_{\tau}P(\tau|\theta)
∑τP(τ∣θ)采样近似,并且我们已经推导出:
∇
log
P
(
τ
∣
θ
)
=
∑
t
∇
log
P
(
a
t
τ
∣
s
t
τ
;
θ
)
\nabla\log P(\tau|\theta)=\sum_{t}\nabla\log P(a_t^\tau|s_t^\tau;\theta)
∇logP(τ∣θ)=∑t∇logP(atτ∣stτ;θ)
因此我们得到最后一行公式
最后一行公式有两个求和符号,他们的意义是,对N条轨迹的每一条轨迹上的累计奖励求和取平均,而我们知道轨迹上的奖励是在某个状态s下采取行为a获得的。因此上述公式也就等价于对数据集中的所有可能的(状态,行为)对,所获得的奖励求和取平均。
我们的的目标就是在参数 θ ∗ \theta^* θ∗下,数据集中所有可能的(状态,行为)对,所获得的奖励累计是最大的。
最终我们将
J
(
θ
)
J(\theta)
J(θ)改写为:
这也正是论文的公式(14)。上述的推导过程基于策略梯度理论。
就算没有懂上述的推导过程,也没关系,我们只要知道 P ( a ∣ s ; θ ) P(a|s;\theta) P(a∣s;θ)和R(s,a)是什么就可以训练模型了。
这里的 P ( a ∣ s ; θ ) P(a|s;\theta) P(a∣s;θ)就是神经网络的输出,这个输出是指在行为空间上的概率分布。
所以我们要清楚状态、行为、奖励是什么:
- 状态是模型的输入,在本文中当然就是切割后的某一段文本片段segment
- 行为是模型的输出,本文的目的是让模型学会自己分割文章,那么输出的自然是下一次切分的滑动步长,行为空间定义为{-16,16,32,64,128},显然输出层的维度是5。
- 奖励是要预先定义好的,见论文的公式11
我们对照着论文的公式11和公式12:
不需要看文字,只要看公式就行了:
q c q_c qc是模型预测当前segment包含答案的概率,它是由前一个segment的循环状态和当前segemnt的CLS的表示通过LSTM得到的。
r c r_c rc是模型从当前的segment提取出答案位置的概率, R ( s ′ , a ′ ) R(s',a') R(s′,a′)是未来的累计奖励,关于 R ( s ′ , a ′ ) R(s',a') R(s′,a′)我们后面参照论文源码来解释。
这里需要知道的是当当前的segment包含有答案时,如果模型预测的当前segment包含答案的概率较高,也就是 q c q_c qc值比较高,那么当前状态下采取行为a的奖励主要是由 q c ∗ r c q_c*r_c qc∗rc决定的。
而如果当前的segment没有包含答案,那么 r c r_c rc=0,而如果模型预测当前segment包含答案的概率较小,也就是 q c q_c qc值比较小,那么 1 − q c 1-q_c 1−qc比较大,所以当前状态采取行为a的奖励主要由未来的累计奖励决定。若模型预测当前segment包含答案的概率较大,那么 1 − q c 1-q_c 1−qc比较小,显然此时模型获得的奖励就比较少。
通俗的说就是,若当前的segment包含答案,模型预测当前的segment确实包含答案,而且还能正确的提取出答案位置,那么此时给模型的奖励主要是 q c ∗ r c q_c*r_c qc∗rc,希望模型在下次见到同样的segment时,增加 P ( a ∣ s ; θ ) P(a|s;\theta) P(a∣s;θ)的概率。其他的情况下是类似的。
训练模型
想要训练模型我们需要知道模型的loss
模型的loss有三个,第一个是答案提取器(Answer Extractor)的loss,它的值是模型预测的答案位置与真实位置的交叉熵:
其中有两个求和符号,第一个求和符号是对整个序列上每一个位置的单词的预测概率,第二个求和符号是对所有的分割次数求和。源码中定义了分割次数固定为3
第二个是 L C S L_{CS} LCS,它代表的是切分段落打分器(Chunking Scorer)的损失值,也就是模型在每一次切分的segment下,预测该segment包含有答案的概率与实际包含答案的交叉熵,仍然是对所有的切分次数求和。
前两个的损失函数都是交叉熵损失函数,属于监督学习范畴,有明确的标签,但是第三个损失值:
L
C
P
L_{CP}
LCP是强化学习的损失函数,没有标签,只有奖励。
不过根据前面基于策略梯度理论,我们已经推导出该损失函数的导数为:
所以现在我们只需要计算出 log P ( a ∣ s ; θ ) \log P(a|s;\theta) logP(a∣s;θ)和 R ( s , a ) R(s,a) R(s,a)就可以了
下面我们仿照论文的源码一步一步的看:
以rum_RCM_trivia.py为例:
chunk_stop_rewards=[]#用来记录每一次分割下得到的r_c
contarin_answer_probs=[]#用来记录每一次分割下的q_c
#我们并不能每一个segment下立即计算出奖励,由公式11可以看出来每一步的奖励是要依赖于后面时间步的
#因此我们要保存每一步的r_c和q_c,然后才能计算最终的奖励
#源码用chunk_stop_rewards来命名,含义就是说r_c这个值其实对应着模型能从中正确提取出答案的奖励
#rewards_for_stop[i] = chunk_start_probs[i][start_position] * chunk_end_probs[i][end_position]
#强制模型切分一篇document max_read_times次,我们假设max_read_times=4,
#######第一次切分 ############################################################chunk_stop_rewards=[]#用来记录每一次分割下得到的r_c
contarin_answer_probs=[]#用来记录每一次分割下的q_c
#我们并不能每一个segment下立即计算出奖励,由公式11可以看出来每一步的奖励是要依赖于后面时间步的
#因此我们要保存每一步的r_c和q_c,然后才能计算最终的奖励
#源码用chunk_stop_rewards来命名,含义就是说r_c这个值其实对应着模型能从中正确提取出答案的奖励
#rewards_for_stop[i] = chunk_start_probs[i][start_position] * chunk_end_probs[i][end_position]
stride_log_probs=[]#用来记录每一次的logP(a|s;\theta)
#强制模型切分一篇document max_read_times次,我们假设max_read_times=4,
#######第一次切分 ############################################################
cur_global_pointer=0#我们不考虑batch_size这个维度,仅仅只考虑一篇文章document
stop_flag=1#代表第一个segment是包含有答案的
prev_hidden_states=None#第一次切割下是没有之前的prev_hidden_states的
#把stop_flag输入到模型中用来计算Chunking Scorer这个loss,也就是模型预测第一篇document包含有答案的损失值
(sequence_output,pool_output)=bert(input_ids)#我们把第一篇segment的input_ids输入到bert_model中
cls_representation=sequence_output.narrow(1,0,1)#这就是cls的表示,也就是论文的v_c
start_logits,end_logits=unstack(Linear(in_features=768,out_features=2)(sequence_output),axis=2)
start_loss=cross_entropy(start_logits,start_positions)
end_loss=cross_entropy(end_logits,end_positions)#计算Answer Extractor的loss
start_probs,end_probs=softmax(start_logits),softmax(end_logits)
if stop_flag==1:#表明当前segment包含答案
r_c=start_probs*end_probs
else:
r_c=0#对应论文的公式12
v_c_pie=LSTM(cls_representation,prev_hidden_states)#第一次的prev_hidden_states为None,那么默认初始化为全0向量
stride_prob=Softmax(Linear(in_features=768,out_features=5)(cls_representation))#输出的张量为行为空间{-16,16,32,64,128}的概率分布
stride_index=sample(stride_prob)#从概率分布中随机采样,概率大的会优先采样出来,注意这不是argmax,也就是说
#某些概率不是最大的行为也会被采样出来,一种解释是:即使是同样的segment,问题不同的情况下,答案当然不同,因此同样的segment在面对
#不同的问题时,要采取不同的分割策略,而不是看到一篇segment,就以某种固定的分割方式切分
stride_log_prob=log(stride_prob)#这个值就是log(a|s,\theta),接下来我们计算奖励
q_c=sigmoid(Linear(in_features=768,out_features=2)v_c_pie)
contain_answer_probs.append(q_c)
chunk_stop_rewards.append(r_c)
经过第一次切分之后,假设有下列值存在
chunk_stop_rewards=[0.07]
contain_answer_probs=[0.75]
prev_hidden_states#此时不是空值了,而是LSTM的输出值
stride_log_probs=[-0.68]
stride_index=2#行为空间:{-16,16,32,64,128},也就代表这一次应该向右移动32个单词
经过max_read_times次数后我们会得到下列值:
- chunk_stop_rewards=[0.07,0,0.15,0.05],chunk_stop_rewards存储的是max_read_times次数下的每一次模型获得的
r
c
r_c
rc,
r c r_c rc也就是正确的答案可以从那一次的segment中提取出来的概率, r c r_c rc也可以认为是当前的segment,也就是在当前状态下,环境给模型预测答案的奖励,如果答案在当前的segment下,那么奖励就是 p c s t a r t ∗ p c e n d p_c^{start}*p_c^{end} pcstart∗pcend。否则奖励为0,也就是说不希望模型在这种状态下还要预测答案。 - contain_answer_probs=[0.75,0.15,0.9,0.6], contain_answer_probs存储的是max_read_times次数下的每一次模型预测该次的segment包含答案的概率,它是将 v ~ c \tilde{v}_c v~c通过一个激活函数为sigmoid的全连接层输出的概率,而 v ~ c \tilde{v}_c v~c是 v c v_c vc和 v ~ c − 1 \tilde{v}_{c-1} v~c−1通过LSTM得到的,其中 v c v_c vc是CLS的向量表示, v ~ c − 1 \tilde{v}_{c-1} v~c−1是上一次的segment的LSTM的输出
- stride_log_probs=[-2.12,-0.85,-0.56,-0.28], stride_log_probs存储的是max_read_times次数下的每一次的segment,模型在该segment下做出的行为a的log概率,对应着 log P ( a ∣ s ) \log P(a|s) logP(a∣s)。而 P ( a ∣ s ) P(a|s) P(a∣s)是由 v ~ c \tilde{v}_c v~c通过一个输出维度为action_space_nums的全连接层然后softmax得到的,其中action_space_nums指的是行为数目,本文中是5,比如{-16,16,32,64,128}
有了上面的数据后我们就可以计算模型中切分策略(Chunking Policy)的损失函数了,也就是 ∑ ( s , a ) ∈ D log P ( a ∣ s ) R ( s , a ) \sum_{(s,a)\in \mathcal{D}}\log P(a|s)R(s,a) ∑(s,a)∈DlogP(a∣s)R(s,a)
损失函数有两部分,第一个 log P ( a ∣ s ) \log P(a|s) logP(a∣s)我们已经获得了,主要来看奖励是如何计算的。
这里的 R ( s ′ , a ′ ) R(s',a') R(s′,a′)是未来的奖励
#到了这里我们就可以计算强化学习部分的loss了,我们自己构造下数据
#假设batch_size=2,max_read_times=5,
#stop_probs代表的是这2个document在这5次的切分当中,每一次模型预测segment包含answer的概率,
stop_probs=[[0.15,0.5],
[0.2,0.7],
[0.25,0.35],
[0.40,0.2],
[0.15,0.30]]#每一个值对应的是q_c
#stop_rewards代表的是这2个document在这5次的切分当中,每一次模型从segment中提取出answer的概率
stop_rewards=[[0.08,0.15],
[0.0,0.36],
[0.5,0.0],
[0.15,0.25],
[0.0,0.4]]#每一个值对应的是r_c,0.0代表这个segment不包含answer
stop_probs=np.transpose(stop_probs)
stop_rewards=np.transpose(stop_rewards)
print(stop_probs)
print(stop_rewards)
整个计算过程如下:
stop_probs=[0.15 0.2 0.25 0.4 0.15]
stop_rewards=[0.08 0. 0.5 0.15 0. ]
下面我们按照公式 R ( s , a ) = q c r c + ( 1 − q c ) R ( s ′ , a ′ ) R(s,a)=q_cr_c+(1-q_c)R(s',a') R(s,a)=qcrc+(1−qc)R(s′,a′)来手动计算第一个样本所应该获得的奖励
首先我们需要的是从后向前计算,max_read_times=5
- 第五次由于没有下一次了,所以第五次的奖励设置为第五次的 r c r_c rc,也就是0.
- 第四次的奖励为0.4*0.15+(1-0.4)*0.=0.06
- 第三次的奖励为0.25*0.5+(1-0.25)*0.06=0.17
- 第二次的奖励为0.2*0. +(1-0.2)*0.17=0.136
- 第一次的奖励为0.15*0.08+(1-0.15)*0.136=0.1276
所以对于第一个样本从第一次到倒数第二次的奖励应该为[0.1276,0.136,0.17,0.06]
但是按照论文的github代码运行出来的结果:
我觉得我的理解没有问题,感觉好像是论文的代码错了
所以我认为rl_reward.py中的代码应该是下面这样
q_vals = []
# calc from the end to the beginning time
next_q_vals = stop_rewards[:,-1] #np.zeros(len(stop_rewards))
for t in reversed(range(0, stop_rewards.shape[1]-1)):
t_rewards = stop_rewards[:, t]
t_probs = stop_probs[:, t]
cur_q_vals = np.multiply(t_rewards, t_probs) + np.multiply(next_q_vals, 1-t_probs)
q_vals.append(list(cur_q_vals)[:])
next_q_vals = cur_q_vals
# q_vals: (bsz, max_read_times-1)
q_vals=list(reversed(q_vals))
q_vals = np.transpose(q_vals)
运行结果:
假如我的理解是对的,那么我们已经获得了每一个时刻的奖励 R ( s , a ) R(s,a) R(s,a),而且之前也已经把各个时刻的stride_log_probs记录下来,
那么现在 log p a c t ( a ∣ s ) \log p^{act}(a|s) logpact(a∣s)和 R ( s , a ) R(s,a) R(s,a)都已经得到了,
接下来自然就是:
reinforce_loss=torch.mean(torch.sum(-q_vals*stride_log_probs,dim=1))
最终的loss形式为:
loss = (stop_loss + answer_loss) / args.max_read_times + reinforce_loss
需要注意的是,因为最后一个时间步的行为不与环境进行交互,因此对最后一个时间步没有奖赏。
模型的loss=(stop_loss+answer_loss)/max_read_times+reinforce_loss
根据这个式子我们可以看出,每一个时间步我们都会计算Answer Extractor部分提取答案的loss,以及Chunking Scorer部分预测segment包含答案的loss,并且每一个时间步的值是累加的。
而reinforce_loss是整个轨迹执行完之后,再计算loss
测试阶段
测试阶段不需要那么复杂,因为不必计算loss了,我们只需要根据模型在当前state下
采取的action a 得到的概率值
P
(
a
∣
s
)
P(a|s)
P(a∣s),这是在行为空间上的概率分布,在训练阶段我们是采样的方式,也就是每一个行为都有可能被采样出来,只是概率大的更多次的会被采样出来。而测试阶段采取贪心策略,只选取概率最大的那个行为作为输出
定义:
作为当前的segment下预测答案位置的分数
然后根据当前状态下所作出的行为 a ∗ a^* a∗,我们会得到一个新的状态 s t + 1 s_{t+1} st+1,比如之前的 a ∗ = 32 a^*=32 a∗=32,那么当前的状态就是上一次的segment向右移动32个单词长度得到的。
然后这个新的状态下,仍然得到:
总计会有max_read_times个,我们取出来其中值最大的作为模型在切分了一篇文章max_read_times后所作出的综合预测。
实验部分
根据此表我们可以看出来,当最大长度max_seq_len限制为512的时候,其实这种循环切分的机制并没有比BERT-large有什么明显的效果,主要原因是,CoQA和QuAC数据集的文章大部分不是特别的长
根据上图我们了解到,CoQA和QuAC的训练集合的文章长度平均为352和516个单词,显然很多文章并不需要切分。因此Recurrent chunking机制发挥不了什么作用,但是随着最大长度的减小,比如,当最大长度限制为192的时候,也就是说当一篇文章会被切分成很多长度不超过192的segment的时候,循环切分的方式明显好于baseline,因为baseline的做法是将切分的各个segment独立的预测答案,而且也不考虑answer在segment中的位置,前面已经分析了,那些answer位置在segment位置中心的答案容易预测出来,而处于segment边缘的答案由于缺少足够的上下文,是不容易准确的预测出来的
下表是在TriviaQA数据集上的实验结果,
因为TriviaQA数据集比较长,max_seq_len固定为512的情况下,显然循环切分机制效果要好于baseline的,说明循环切分机制适用于处理长文本。
上图指的是BERT-large这个基线模型和RCM这个循环切分机制在切分文本时,切分的文本包含完整答案在切分的所有文本中的比例,也叫Hit rate,命中率。显然循环切分机制切分的文章中有大部分都是包含完整答案的。而baseline这种按照固定滑动步数切分文章的做法,有相当一部分的文章是不包含有答案的。
这里需要注意的是,在baseline中,也就是BERT模型。它是按照固定的doc_stride切分一篇文章,如果切分的某个segment不包含答案,那么这个segment是不算作样本训练模型的,因为没有答案需要预测。具体的可以查看BERT源码:
从代码中我们可以看到,doc_start指的就是当前的segment的第一个单词在原始的document中的位置,显然若是该document的答案的起始位置和终止位置小于segment的起始位置或者都大于segment的末尾位置,表明当前所分割的segment成功的避开了答案,那么就continue,也就是不要这个segment了
而在循环切分机制当中,对于不包含有答案的segment,仍然会保留下来作为样本参与训练,因为整个切分过程是在一篇document上连续切分的,中间断了显然是不行的,而且我们需要让模型学会避免切分出这种不包含answer的segment.具体的做法是,对于不包含答案的segment,和question构成一个example输入到BERT当中,BERT的CLS的表示和上一时刻的segment的LSTM的输出一起输入到LSTM当中,得到 v ~ c \tilde{v}_c v~c,然后 v ~ c \tilde{v}_c v~c输入到激活函数为sigmiod,输出维度是2的全连接层得到 q c q_c qc,这个 q c q_c qc代表的就是模型预测当前segment包含答案的概率,然后在这个不包含answer的segment下,给模型的奖励是 R ( s , a ) = ( 1 − q c ) ∗ R ( s ′ , a ′ ) R(s,a)=(1-q_c)*R(s',a') R(s,a)=(1−qc)∗R(s′,a′),显然模型预测的 q c q_c qc越小,获得的奖励越多
上图是论文给出的一个例子,红色字体代表的是answer,第一次切分时,固定到max_seq_length为止,也就是图片中的黄色部分,第二次切分时,向右滑动128个单词,也就是蓝色部分chunk 2,第三次切分下模型向左移动了16个单词,目的是使得第三个chunk包含答案更多的上下文,也就是图片中的橘色部分,显然这种切分方式比固定切分要好,而且固定的切分方式是不会向左移动的。