keras 复现 memory networks 系列
概述
参考论文:《End-To-End Memory Networks》
数据集:bAbI-tasks
最终源码:keras-MemN2N(GitHub)
前期工作:
论文相关
数据相关
- 数据集里每个任务有 I(I≤320)个句子作为 input {xi}
- 词典大小 V=177
维度计算
架构图
梳理一下维度关系
以 bAbI-task-qa1 为例进行计算
vocab_size = 20
- 字典大小为 20
story.shape = (None, 10, 6)
- 一个 story 最多 10 个句子
- 每个句子最多 6 个单词
query.shape = (None, 6)
- query 的单词数和 story 里的单词数要统一,这里就定为 6(实际上 query 都是 3 个词)
m_emb.shape = (None, 10, d)
- story 通过了一个
(embedding_size, vocab_size)
的 embedding 矩阵 A 得到的输出,d
为embedding_size
- 架构图里的 Input(mi 的 shape 为
(None, d)
) - 相当于每个句子 embedding 成了一个
d
维向量 - 参考公式:mi=∑jAxij,其中 xij 为第 i 个句子的第 j 个词(BoW表示句子,最简单的一个公式)
- story 通过了一个
c_emb.shape = (None, 10, d)
- 基本同
m_emb
,embedding 矩阵为 C,ci=∑jCxij - 架构图里的 Output(ci 的 shape 为
(None, d)
)
- 基本同
u_emb.shape = (None, d)
- query 通过了一个
(embedding_size, vocab_size)
的 embedding 矩阵 B 得到的输出 - 架构图里的 u
- 参考公式:u=∑jBqj
- query 通过了一个
probs.shape = (None, 10)
- 参考公式:pi=Softmax(uTmi)
o_weight.shape = (None, d)
- 参考公式:o=∑ipici
answer.shape = (None, 20)
- 参考公式:ˆa=Softmax(W(o+u))
- 最后的输出应该是字典里的一个词,所以维度是词典的大小
细节设计
默认使用 K=3 hops 模型,使用 Adjacent 权重取值方式。
几种设计选择:
- Sentence Representation:
- BoW
- Position Encoding(PE)
- mi=∑jlj⋅Axij(⋅ 是 element-wise multiplication)
- lkj=(1−j/J)−(k/d)(1−2j/J)(J 是句子里单词的数量)
- Training on all 20 tasks:
- Independently
- Jointly
- Phase Training:
- Linear start(LS)
- Softmax
- Hops: 1~3
论文最终数据
错误率表
实现
Keras 的 example 里有一个 babi_memnn.py,说是照着论文复现的,可惜连数据预处理都没对上(代码里直接把一个 tasks 所有句子拼接成一句了),而且也没有实现多 hop,也没有实现论文里 PE、LS 之类的改进,甚至还把模型里最后一层强行改成了 LSTM。完全不能算复现,参考价值也不大。数据预处理部分参考一个 MemN2N 的 tensorflow 实现。
基本实现
从以最简单的 BoW + Adjacent 为例子
用现有层搭建网络
输入部分
1 | story = Input(shape=(max_story_size, sentence_size,)) |
初始化嵌入层,因为使用 Adjacent 类的权重(Ak+1=Ck,WT=CK,B=A1),所以 k hops 的模型只需要训练 k+1 个嵌入矩阵
1 | Embedding_layer = [] |
计算第一层的 m 、c 和 u,注意 m 和 u 使用的是嵌入矩阵 A1,c 使用的是 A2=C1。
以 m 为例,因为经过嵌入层之后的 shape=(None, 10, 6, d)
,而我们需要第二维上的 6 个数字之和并把这一维直接消掉才算能得到目标 shape=(None, 10, d)
,所以这里用了一个Lambda
层来进行求和操作,u、c 同理,就是要注意 u 是在第一维上求和。
1 | m_emb = Embedding_layer[0](story) |
接下来计算权重 pi=Softmax(uTmi),为了进行 uTmi这一步,我先把uT复制到和mi维度一样,然后进行 element-wise multiplication,然后再和上面一样进行求和降维(总觉得应该还有更优雅点的实现……
1 | u_temp = Lambda(lambda x: K.expand_dims(x, 1))(u_emb) |
然后计算加权和 o=∑ipici,也是上面那种复制填充维度,element-wise multiplication,再求和降维
1 | probs_temp = Lambda(lambda x: K.expand_dims(x, 2))(probs) |
最后求输出 ˆa=Softmax(W(o+u))
1 | answer = Dense(vocab_size, kernel_initializer='random_normal')(u_emb[-1]) |
这里有一个问题,就是在 Adjacent 权重取值下,最后的 W 无法取到 WT=CK,于是最后还是决定直接封装一个层出来。
自己封装一个 MemN2N 层
1 | class MemN2N(Layer): |
思路和用现有层差不多,只是能够取到 WT=CK 了。
效果很差,无论是现有层还是自定义的层,task-1 只能到 0.66+,但是 task-20 能到 1(数据集 1k 还是 10k 都差不多,hop 取 3 和取 1 也差不多)
待续
v1.5.2