本篇文章主要讲解本人在今年EMNLP2021发表的一篇关于应用题自动解题的论文。
论文题目为:Recall and Learn: A Memory-augmented Solver for Math Word Problems
代码已开源,欢迎star:REAL4MWP
另外,本人还搜集了近五年来所有关于应用题解题的论文,供研究该方向的学者参考:Math-Word-Problems-PaperList
1. 背景介绍
数学应用题自动解题任务是指通过给定的应用题题目,设定特定的技术方案得到对应的答案。直接通过题目得到答案忽略了中间的推理过程,因此更通用的做法是通过题目信息得到对应的表达式,再对表达式进行逻辑运算得到答案。受限于数据集的规模,以前的方法大多使用统计机器学习以及模板匹配的方法,但因为无法遍历所有应用题模板,已有方法只在限定的题目模板下效果较好,其他题目的表达式模板若不在语料中出现,则效果较差。自从大规模应用题解题数据集math23k在2017年被公开了之后,基于深度学习的方法开始广泛出现。
已有方法大多使用基于循环神经网络的序列到序列模型,为了统一模板,首先将题目及表达式中的数字使用统一字符进行表示,其次将题目文本进行编码得到编码向量,再通过解码层解码得到表达式。通过表达式模板进行解题的方法跟人类进行应用题解题的模式差别较大,当人类阅读题目内容时,会尽量弄清楚题目的描述以及题目数字之间的关系,并根据以前掌握的知识进行举一反三。而现有的技术只能学到一种固定模板,不能通过已有的相似题目进行类比学习。
2. 数据集介绍
数据集格式如上图所示,我们的任务为根据题目信息,预测出对应的表达式。
首先,我们将任务转换成生成任务,基于seq2seq框架来完成表达式的生成。
3. 动机
人类的学习方式通常依赖于类比学习方法,而不是通过单个训练示例进行学习。因此,如何将类比学习方法整合到一个统一的框架中是本文主要探索的内容。
如上图所示,我们设计了一个名为REAL的新的记忆增强模型来回忆在解决新问题时已经解决的一些熟悉问题,并学习以类比的方式生成类似的解决方案。
4. 提出的方法
本文提出的方法包括四个部分:1)Memory Module(记忆模块); 2) Representation Module(表示模块); 3) Analogy Module(类比模块); 4) Reasoning Module(推理模块)。
4.1 Memory Module(记忆模块)
记忆模块为框架图的左边部分,首先,使用word2vec训练语料库的词向量,并使用Mean pooling的方法构造句子向量,以此获取每个题目的题目向量。然后,使用余弦相似度/向量内积对每个题目与语料库所有题目计算相似度,取TopK个题目作为相似题,如下图所示:
首先,将语料库中的每道题目向量化得到对应的题目向量,向量化方法可采用word2vec或glove算法训练语料库得到每个词的向量表示,然后将每道题目的词向量进行均值池化可得到题目向量;同时将待求解题目向量化得到待求解题目向量;将待求解题目向量与语料库中的每道题目向量进行余弦相似度计算,并取相似度最高的n道题目作为下一步的候选相似习题。
4.2 Representation Module(表示模块)
表示模块目的是获取待求解题目和检索题目各自的向量表征(token级别)。熟悉生成模型的同学应该知道,Unilm使用了特殊的掩码矩阵来实现基于Bert的文本生成任务。在表示模块中,我们使用了6层的transformer encoder结构,同时借鉴Unilm模型使用掩码矩阵的方法,在此阶段我们也设计了一个相似的掩码矩阵(seq2seq部分的掩码矩阵),即对于一个包含题干和表达式的题目(不管是待求解题目还是检索题目都使用相同的掩码方法),我们让题干各token之间都可以相互看见,而表达式的第k个token只能看到题干的所有token以及表达式的前k-1个token,因此该部分可实现表达式序列的生成同时也不会造成数据的泄露。
具体掩码矩阵为框架图的中间部分,其中Q表示题干,E表示表达式:
掩码矩阵中,白色方块使用0进行填充,黑色方块使用负无穷进行填充(负无穷经过softmax之后为0)。最后,将掩码矩阵与Attention矩阵进行叠加,可得到经过掩码处理后的结果。
4.3 Analogy Module(类比模块)
为了达到类比学习的效果,模型需要聚合待求解题目(题干)及检索题目(包括题干和表达式)的信息。类比模块同样使用6层的transformer encoder结构,并设计了一个聚合多方面信息的掩码矩阵。
具体掩码矩阵为框架图的右边部分:
其中输入为
{
Z
q
,
Z
e
,
X
q
,
X
e
}
\{Z_q, Z_e, X_q, X_e\}
{Zq,Ze,Xq,Xe},分别表示检索到的题目的题干及表达式,待求解题目的题干及表达式,为了防止数据泄露,我们设计了上图的掩码矩阵(同表示模块,白色方块使用0进行填充,黑色方块使用负无穷进行填充),黑色部分就是我们掩码的部分,具体分析如下:检索题目的题干信息可以互相看到,检索题目的表达式信息可以看到检索题目的题干信息及表达式当前时刻之前的信息;待求解题目的题干可以看到检索题目的题干和表达式信息以及待求解题目的题干信息;待求解题目的表达式可以看到检索题目的题干和表达式信息以及待求解题目的题干信息和表达式当前时刻之前的信息。如此设计的动机是:待求解题目可以参考检索题目的所有信息,学习类比功能;同时确保检索题目本身具备生成能力。为了从检索题目中提取出相应知识,我们针对检索题目添加了一个inductive loss参与训练:
L
i
n
d
u
c
t
i
v
e
=
−
∑
t
N
l
o
g
p
θ
a
(
z
e
t
∣
Z
q
,
z
e
1
:
t
−
1
)
L_{inductive}=-\sum_t^Nlogp_{\theta_a}(z_e^t|Z_q,z_e^{1:t-1})
Linductive=−t∑Nlogpθa(zet∣Zq,ze1:t−1)其中t表示表达式
Z
e
Z_e
Ze的第t个token,
θ
a
\theta_a
θa为待学习参数。
4.4 Reasoning Module(推理模块)
推理模块借助上一步的类比模块进行有效信息的提取,同时,考虑到在待求解题目的表达式生成过程中,操作数更多地来自于待求解题目的题干中,因此,我们在类比模块的最后一层中引入了copy机制(pointer generation network(PGN),指针网络原理请参考我的另一篇博文:文本生成系列(二)Pointer Generator Network),具体公式推理请参考paper详细内容。
PGN指针网络基于RNN框架实现,本论文是基于transformer框架,具体实现逻辑如下图所示:
这里贴出具体的实现源码:
class CopyNet2(nn.Module):
def __init__(self, config, bert_model_embedding_weights):
super(CopyNet2, self).__init__()
self.decoder = nn.Linear(bert_model_embedding_weights.size(1),
bert_model_embedding_weights.size(0),
bias=False)
self.decoder.weight = bert_model_embedding_weights
self.bias = nn.Parameter(torch.zeros(bert_model_embedding_weights.size(0)))
self.vocab_size = config.vocab_size
self.p_gen_linear = nn.Linear(config.hidden_size*2, 1, bias=False)
def forward(self, x_e, dec_input, attn_dist, enc_batch_extend_vocab, extra_zeros):
'''
x_e: shape [batch_size, top_k, pred_len, output_dim]
dec_input: shape [batch_size, top_k, pred_len, output_dim]
attn_dist: shape [batch_size, top_k, ques_seq_len, output_dim]
enc_batch_extend_vocab: shape [batch_size, top_k, ques_seq_len]
extra_zeros: shape [batch_size, top_k, equ_seq_len, vocab_size]
'''
p_gen_input = torch.cat((x_e, dec_input), 3) # B x (2*hidden_dim)
p_gen = self.p_gen_linear(p_gen_input) # [batch_size, top_k, pred_len, 1]
p_gen = torch.sigmoid(p_gen)
# gen_scores = self.phi_g(x_e) # batch_size, top_k, pred_len, vocab_size
gen_logit = self.decoder(x_e) + self.bias # batch_size, top_k, pred_len, vocab_size
vocab_dist = torch.softmax(gen_logit, dim=3)
# Multiply vocab dists by p_gen and attention dists by (1-p_gen)
vocab_dist = p_gen * vocab_dist
attn_dist = (1-p_gen) * attn_dist
if extra_zeros is not None:
vocab_dist = torch.cat([vocab_dist, extra_zeros.float()], 3)
final_dist = vocab_dist.scatter_add(3, enc_batch_extend_vocab, attn_dist)
probs = final_dist.mean(1) # batch_size, pred_len, vocab_size
return torch.log(probs+1e-8)
其中attn_dist为最后一层transformer的self attention中,Q、K构造的attention输出概率,多头注意力需要进行叠加:
# Q、K构造attention输出概率,多头注意力进行叠加(modeling_mwp.py中的第373行)
attn_dist = nn.Softmax(dim=-1)(torch.sum(attention_scores, dim=1))
4.5 损失函数
推理模块损失函数为:
L
a
n
a
l
o
g
i
c
a
l
=
−
l
o
g
(
p
θ
(
y
∣
X
q
)
)
,
L_{analogical}=-log(p_\theta(y|X_q)),
Lanalogical=−log(pθ(y∣Xq)),
p
θ
(
y
∣
X
q
)
=
∏
t
N
E
Z
∈
t
o
p
−
K
(
p
(
Z
q
∣
X
q
)
p
θ
(
y
t
∣
X
q
,
Z
,
y
1
:
t
−
1
)
p_\theta(y|X_q)=\prod \limits_t^N \underset{{Z\in top-K(p(Z_q|X_q)}} {\mathbb{E}} p_\theta(y_t|X_q,Z,y_{1:t-1})
pθ(y∣Xq)=t∏NZ∈top−K(p(Zq∣Xq)Epθ(yt∣Xq,Z,y1:t−1) 其中
Z
∈
t
o
p
−
K
Z\in top-K
Z∈top−K表示取top-K个最相似的检索题目计算数学期望。
最终,模型损失函数结合inductive loss和analogical loss:
L
=
L
a
n
a
l
o
g
i
c
a
l
+
λ
L
i
n
d
u
c
t
i
v
e
L=L_{analogical}+\lambda L_{inductive}
L=Lanalogical+λLinductive在本论文中的所有实验中,设置
λ
=
1
\lambda =1
λ=1即可得到不错的效果。
5. 实验
数据集使用的是Math23K(大约2.3w条样本) 和 Ape210K(大约21w条样本) .
5.1 模型效果对比
与其他SOTA模型的对比效果如下图:
可以看到,我们的REAL模型相对当前SOTA模型在Math23K上有3-5个点的提升,在Ape210K上有7个点的提升。
5.2 消融实验:
其中EN表示等式归一化,为其它SOTA模型常用方法,我自己实现了一个简单版本的,具体请参考源码。
PS: 这篇论文在IDEA方面比较受审稿人认可,只中了EMNLP的Findings的原因是当时实验做的不是很充分,memory module作为主要创新点提升不够明显,这个问题我们在后续的一篇NIPS workshop中做了优化和补充,使得memory module模块效果大幅提升,具体请期待我的下一篇分享。
5.3 top-K实验
以上实验结果都基于top-K=1的情况下,实际上,当top-K增加时,REAL模型的性能还可以进一步地提升。
最终,Math23K最优效果为81.6%,Ape210K最优效果为77.6%.