ON-LSTM:用有序神经元表达层次结构
By 苏剑林 | 2019-05-28 | 191572位读者 |今天介绍一个有意思的LSTM变种:ON-LSTM,其中“ON”的全称是“Ordered Neurons”,即有序神经元,换句话说这种LSTM内部的神经元是经过特定排序的,从而能够表达更丰富的信息。ON-LSTM来自文章《Ordered Neurons: Integrating Tree Structures into Recurrent Neural Networks》,顾名思义,将神经元经过特定排序是为了将层级结构(树结构)整合到LSTM中去,从而允许LSTM能自动学习到层级结构信息。这篇论文还有另一个身份:ICLR 2019的两篇最佳论文之一,这表明在神经网络中融合层级结构(而不是纯粹简单地全向链接)是很多学者共同感兴趣的课题。
笔者留意到ON-LSTM是因为机器之心的介绍,里边提到它除了提高了语言模型的效果之外,甚至还可以无监督地学习到句子的句法结构!正是这一点特性深深吸引了我,而它最近获得ICLR 2019最佳论文的认可,更是坚定了我要弄懂它的决心。认真研读、推导了差不多一星期之后,终于有点眉目了,遂写下此文。
在正式介绍ON-LSTM之后,我忍不住要先吐槽一下这篇文章实在是写得太差了,将一个明明很生动形象的设计,讲得异常晦涩难懂,其中的核心是$\tilde{f}_t$和$\tilde{i}_t$的定义,文中几乎没有任何铺垫就贴了出来,也没有多少诠释,开始的读了好几次仍然像天书一样...总之,文章写法实在不敢恭维~
背景内容 #
通常来说,在文章的前半部分,都是要先扯一些背景知识的~
回顾LSTM #
首先来回顾一下普通的LSTM。用常见的记号,普通的LSTM写为:
\begin{equation}\begin{aligned} f_{t} & = \sigma \left( W_{f} x_{t} + U_{f} h_{t - 1} + b_{f} \right) \\
i_{t} & = \sigma \left( W_{i} x_{t} + U_{i} h_{t - 1} + b_{i} \right) \\
o_{t} & = \sigma \left( W_{o} x_{t} + U_{o} h_{t - 1} + b_{o} \right) \\
\hat{c}_t & = \tanh \left( W_{c} x_{t} + U_{c} h_{t - 1} + b_{c} \right)\\
c_{t} & = f_{t} \circ c_{t - 1} + i_{t} \circ \hat{c}_t \\
h_{t} & = o_{t} \circ \tanh \left( c_{t} \right)\end{aligned}\label{eq:lstm}\end{equation}
如果熟悉了神经网络本身,其实这样的结构没有什么神秘的,$f_t,i_t,o_t$就是三个单层全连接模型,输入是历史信息$h_{t-1}$和当前信息$x_t$,用sigmoid激活,因为sigmoid的结果在0~1之间,所以它们的含义可以诠释为“门(gate)”,分别称为遗忘门、输入门、输出门。不过我个人觉着gate这个名字是不够贴切的,“valve(阀门)”也许更贴切些。
有了门之后,$x_t$被整合为$\hat{c}_t$,然后通过$\circ$运算(对应逐位相乘,有时候也记为$\otimes$)与前面的“门”结合起来,来对$c_{t-1}$和$\hat{c}_t$进行加权求和。
下面是自己画的一个LSTM的示意图:
语言和序信息 #
在常见的神经网络中,神经元通常都是无序的,比如遗忘门$f_t$是一个向量,向量的各个元素的位置没有什么规律。如果把LSTM运算过程中涉及到的所有向量的位置按照同一方式重新打乱,权重的顺序也相应地打乱,然后输出结果可以只是原来向量的重新排序(考虑多层的情况下,甚至可以完全不变),信息量不变,不影响后续网络对它的使用。
换言之,LSTM以及普通的神经网络都没有用到神经元的序信息,ON-LSTM则试图把这些神经元排个序,并且用这个序来表示一些特定的结构,从而把神经元的序信息利用起来。
ON-LSTM的思考对象是自然语言。一个自然句子通常能表示为一些层级结构,这些结构如果人为地抽象出来,就是我们所说的句法信息,而ON-LSTM希望能够模型在训练的过程中自然地学习到这种层级结构,并且训练完成后还能把它解析出来(可视化),这就利用到了前面说的神经元的序信息。(曾经做过的相关研究《最小熵原理(三):“飞象过河”之句模版和语言结构》)
为了达到这个目标,我们需要有一个层级的概念,层级越低代表语言中颗粒度越小的结构,而层级越高则代表颗粒度越粗的结构,比如在中文句子中,“字”可以认为是最低层级的结构,词次之,再上面是词组、短语等。层级越高,颗粒度越粗,那么它在句子中的跨度就越大。
用原文的图示就是:
ON-LSTM #
最后一句“层级越高,颗粒度越粗,那么它在句子中的跨度就越大”看起来是废话,但它对于ON-LSTM的设计有着指导作用。首先,这要求我们在设计ON-LSTM的编码时能区分高低层级的信息;其次,这也告诉我们,高层级的信息意味着它要在高层级对应的编码区间保留更久(不那么容易被遗忘门过滤掉),而低层级的信息则意味着它在对应的区间更容易被遗忘。
设计:分区间更新 #
有了这个指导之后,我们可以着手建立。假设ON-LSTM中的神经元都排好序后,向量$c_t$的index越小的元素,表示越低层级的信息,而index越大的元素,则表示越高层级的信息。然后,ON-LSTM的门结构和输出结构依然和普通的LSTM一样:
\begin{equation}\begin{aligned} f_{t} & = \sigma \left( W_{f} x_{t} + U_{f} h_{t - 1} + b_{f} \right) \\
i_{t} & = \sigma \left( W_{i} x_{t} + U_{i} h_{t - 1} + b_{i} \right) \\
o_{t} & = \sigma \left( W_{o} x_{t} + U_{o} h_{t - 1} + b_{o} \right) \\
\hat{c}_t & = \tanh \left( W_{c} x_{t} + U_{c} h_{t - 1} + b_{c} \right)\\
h_{t} & = o_{t} \circ \tanh \left( c_{t} \right) \end{aligned}\label{eq:update-o0}\end{equation}
不同的是从$\hat{c}_t$到$c_t$的更新机制不一样。
接下来,初始化一个全零的$c_t$,即没有任何记忆,或者想象为一个空的U盘。然后,我们将历史信息和当前输入按一定规律存入到$c_t$中(即更新$c_t$)。每次在更新$c_t$之前,首先预测两个整数$d_f$和$d_i$,分别表示历史信息$h_{t-1}$和当前输入$x_t$的层级:
\begin{equation}\begin{aligned} d_f = F_1\left(x_t, h_{t-1}\right) \\ d_i = F_2\left(x_t, h_{t-1}\right) \end{aligned}\end{equation}
至于$F_1, F_2$的具体结构,我们后面再补充,先把核心思路讲清楚。这便是我不满原论文写作的原因,一上来就定义$\text{cumax}$,事前事后都没能把思想讲清楚。
有了$d_f,d_i$之后,那么有两种可能:
1、$d_f \leq d_i$,这意味着当前输入$x_t$的层级要高于历史记录$h_{t-1}$的层级,那就是说,两者之间的信息流有交汇,当前输入信息要整合到高于等于$d_f$的层级中,方法是:
\begin{equation}\begin{aligned} c_t = \begin{pmatrix}\hat{c}_{t,< d_f} \\
f_{t,[d_f:d_i]}\circ c_{t-1,[d_f:d_i]} + i_{t,[d_f:d_i]}\circ \hat{c}_{t,[d_f:d_i]} \\
c_{t-1,> d_i}
\end{pmatrix}\end{aligned}\label{eq:update-o1}\end{equation}
这个公式是说,由于当前输入层级更高,它影响到了交集$[d_f, d_i]$的部分,这部分由普通的LSTM更新公式来更新,小于$d_f$的部分,直接覆盖为当前输入$\hat{c}_t$对应的部分,大于$d_i$的部分,保持历史记录$c_{t-1}$对应的部分不变。
这个更新公式是符合直觉的,因为我们已经将神经元排好序了,位置越前的神经元储存越低层结构信息。而对于当前输入来说,显然更容易影响低层信息,所以当前输入的“波及”范围是$[0, d_i]$(自下而上),也可以理解为当前输入所需要的储蓄空间就是$[0, d_i]$;而对于历史记录来说,它保留的是高层信息,所以“波及”范围是$[d_f, d_{\max}]$(自上而下,$d_{\max}$是最高层级),或许说历史信息所需要的储蓄空间是$[d_f, d_{\max}]$。在不相交部分,它们“各自为政”,各自保留自己的信息;在相交部分,信息要进行融合,退化为普通的LSTM。
2、$d_f > d_i$,这意味着历史记录$h_{t-1}$和当前输入$x_t$互不相交,那么对于$(d_i, d_f)$的区间“无人问津”,所以只好保持初始状态(即全零,可以理解为没有东西写入);而剩下部分,当前输入直接把自己的信息写入到$[0, d_i]$区间,而历史信息直接写入到$[d_f, d_{\max}]$区间。这种情况下,当前输入和历史信息合并起来都不能占满整个储蓄空间,从而空下了一些剩余容易(中间那部分全零)。:
\begin{equation}\begin{aligned} c_t = \begin{pmatrix}\hat{c}_{t,\leq d_i} \\
0_{(d_i : d_f)} \\
c_{t-1,\geq d_f}
\end{pmatrix}\end{aligned}\label{eq:update-o2}\end{equation}
其中$(d_i : d_f)$表示大于$d_i$、小于$d_f$的区间;而前面的$[d_f : d_i]$表示大于等于$d_f$、小于等于$d_i$的区间。
至此,我们能够理解ON-LSTM的基本原理了,它将神经元排序之后,通过位置的前后来表示信息层级的高低,然后在更新神经元时,先分别预测历史的层级$d_f$和输入的层级$d_i$,通过这两个层级来对神经元实行分区间更新。
这样一来,高层信息就可能保留相当长的距离(因为高层直接复制历史信息,导致历史信息可能不断被复制而不改变),而低层信息在每一步输入时都可能被更新(因为低层直接复制输入,而输入是不断改变的),所以就通过信息分级来嵌入了层级结构。更通俗地说就是分组更新,更高的组信息传得更远(跨度更大),更低的组跨度更小,这些不同的跨度就形成了输入序列的层级结构。
(请反复阅读这段话,必要时对照上图,直接完全理解为止,这段话称得上是ON-LSTM的设计总纲。)
成型:分段软化 #
现在要解决的问题就是,这两个层级怎么预测,即$F_1, F_2$怎么构建。用一个模型来输出一个整数不难,但是这样的模型通常都是不可导的,无法很好地整合到整个模型进行反向传播,所以,更好的方案是进行“软化”,即寻求一些光滑近似。
为了进行软化,我们先对$\eqref{eq:update-o1},\eqref{eq:update-o2}$进行改写。引入记号$1_k$,它表示第$k$位为1、其他都为0的$d_{\max}$维向量(即one hot向量),那么$\eqref{eq:update-o1},\eqref{eq:update-o2}$可以统一地写为
\begin{equation}\begin{aligned}\tilde{f}_t & = \stackrel{\rightarrow}{\text{cs}}\left(1_{d_f}\right), \quad \tilde{i}_t = \stackrel{\leftarrow}{\text{cs}}\left(1_{d_i}\right) \\
\omega_t & = \tilde{f}_t \circ \tilde{i}_t \quad (\text{用来表示交集})\\
c_t & = \underbrace{\omega_t \circ \left(f_{t} \circ c_{t - 1} + i_{t} \circ \hat{c}_t \right)}_{\text{交集部分}} + \underbrace{\left(\tilde{f}_t - \omega_t\right)\circ c_{t - 1}}_{\text{大于}\max\left(d_f, d_i\right)\text{的部分}} + \underbrace{\left(\tilde{i}_t - \omega_t\right)\circ \hat{c}_{t}}_{\text{小于}\min\left(d_f,d_i\right)\text{的部分}}
\end{aligned}\label{eq:update-o3}\end{equation}
其中$\stackrel{\rightarrow}{\text{cs}}$/$\stackrel{\leftarrow}{\text{cs}}$分别是右向/左向的cumsum操作:
\begin{equation}\begin{aligned}\stackrel{\rightarrow}{\text{cs}}([x_1,x_2,\dots,x_n]) & = [x_1, x_1+x_2, \dots,x_1+x_2+\dots+x_n]\\
\stackrel{\leftarrow}{\text{cs}}([x_1,x_2,\dots,x_n]) & = [x_1+x_2+\dots+x_n,\dots,x_n+x_{n-1},x_n]\end{aligned}\end{equation}
注意,这里指的是$\eqref{eq:update-o3}$所给出的结果,跟$\eqref{eq:update-o1},\eqref{eq:update-o2}$分情况给出的结果,是完全等价的。这只需要留意到$\tilde{f}_t$给出了一个从$d_f$位开始后面全是1、其他位全是0的$d_{\max}$维向量,而$\tilde{i}_t$给出了一个从0到$d_i$位全是1、其他位全是0的$d_{\max}$维向量,那么$\omega_t = \tilde{f}_t \circ \tilde{i}_t$正好给出了交集部分为1、其余全是0的向量(如果没有交集,那就是全0向量),所以$\omega_t \circ \left(f_{t} \circ c_{t - 1} + i_{t} \circ \hat{c}_t \right)$这部分就是在处理交集部分;而$\left(\tilde{f}_t - \omega_t\right)$得到一个从$\max\left(d_f,d_i\right)$位开始后面全是1、其他位全是0的$d_{\max}$维向量,正好标记了历史信息的范围$[d_f, d_{\max}]$去掉了交集之后的部分;而$\left(\tilde{i}_t - \omega_t\right)$得到一个从$0\sim \min\left(d_f,d_i\right)$位全是1、其他位全是0的$d_{\max}$维向量,正好标记了当前输入的范围$[0, d_i]$去掉了交集之后的部分。
现在,$c_t$的更新公式由式$\eqref{eq:update-o3}$来描述,两个one hot向量$1_{d_f},1_{d_i}$由两个整数$d_f,d_i$决定,而这两个整数本身是由模型$F_1, F_2$预测出来的,所以我们可以干脆直接用模型预测$1_{d_f},1_{d_i}$就是了。当然,就算预测出来两个one hot向量,也没有改变整个更新过程不可导的事实。但是,我们可以考虑将$1_{d_f},1_{d_i}$用一般的浮点数向量来代替,比如:
\begin{equation}\begin{aligned}1_{d_f}\approx& softmax\left( W_{\tilde{f}} x_{t} + U_{\tilde{f}} h_{t - 1} + b_{\tilde{f}} \right)\\
1_{d_i}\approx& softmax\left( W_{\tilde{i}} x_{t} + U_{\tilde{i}} h_{t - 1} + b_{\tilde{i}} \right)
\end{aligned}\end{equation}
这样一来,我们用一个$h_{t-1}$和$x_t$的全连接层,来预测两个向量并且做$softmax$,就可以作为$1_{d_f},1_{d_i}$的近似,并且它是完全可导的,从而我们将它们取代$1_{d_f},1_{d_i}$代入到$\eqref{eq:update-o3}$中,就得到ON-LSTM的$c_t$的更新公式:
\begin{equation}\begin{aligned}\tilde{f}_t & = \stackrel{\rightarrow}{\text{cs}}\left(softmax\left( W_{\tilde{f}} x_{t} + U_{\tilde{f}} h_{t - 1} + b_{\tilde{f}} \right)\right)\\
\tilde{i}_t & = \stackrel{\leftarrow}{\text{cs}}\left(softmax\left( W_{\tilde{i}} x_{t} + U_{\tilde{i}} h_{t - 1} + b_{\tilde{i}} \right)\right) \\
\omega_t & = \tilde{f}_t \circ \tilde{i}_t \quad (\text{用来表示交集})\\
c_t & = \underbrace{\omega_t \circ \left(f_{t} \circ c_{t - 1} + i_{t} \circ \hat{c}_t \right)}_{\text{交集部分}} + \underbrace{\left(\tilde{f}_t - \omega_t\right)\circ c_{t - 1}}_{\text{大于}\max\left(d_f, d_i\right)\text{的部分}} + \underbrace{\left(\tilde{i}_t - \omega_t\right)\circ \hat{c}_{t}}_{\text{小于}\min\left(d_f,d_i\right)\text{的部分}}
\end{aligned}\end{equation}
把剩余部分(即$\eqref{eq:update-o0}$)也写在一起,整个ON-LSTM的更新公式就是:
\begin{equation}\begin{aligned} f_{t} & = \sigma \left( W_{f} x_{t} + U_{f} h_{t - 1} + b_{f} \right) \\
i_{t} & = \sigma \left( W_{i} x_{t} + U_{i} h_{t - 1} + b_{i} \right) \\
o_{t} & = \sigma \left( W_{o} x_{t} + U_{o} h_{t - 1} + b_{o} \right) \\
\hat{c}_t & = \tanh \left( W_{c} x_{t} + U_{c} h_{t - 1} + b_{c} \right)\\
\tilde{f}_t & = \stackrel{\rightarrow}{\text{cs}}\left(softmax\left( W_{\tilde{f}} x_{t} + U_{\tilde{f}} h_{t - 1} + b_{\tilde{f}} \right)\right)\\
\tilde{i}_t & = \stackrel{\leftarrow}{\text{cs}}\left(softmax\left( W_{\tilde{i}} x_{t} + U_{\tilde{i}} h_{t - 1} + b_{\tilde{i}} \right)\right) \\
\omega_t & = \tilde{f}_t \circ \tilde{i}_t\\
c_t & = \omega_t \circ \left(f_{t} \circ c_{t - 1} + i_{t} \circ \hat{c}_t \right) + \left(\tilde{f}_t - \omega_t\right)\circ c_{t - 1} + \left(\tilde{i}_t - \omega_t\right)\circ \hat{c}_{t}\\
h_{t} & = o_{t} \circ \tanh \left( c_{t} \right)\end{aligned}\end{equation}
示意图如下。对比LSTM的$\eqref{eq:lstm}$,就可以发现主要改动在哪了。其中新引入的$\tilde{f}_t$和$\tilde{i}_t$被作者称为“主遗忘门(master forget gate)”和“主输入门(master input gate)”。
注:
1、论文中将$\stackrel{\rightarrow}{\text{cs}}(softmax(x))$简记为$\text{cumax}(x)$,这只是记号上的转换而已;
2、作为数列来看,$\hat{f}_t$是一个单调递增的数列,而$\hat{i}_t$是一个单调递减的数列;
3、对于$\tilde{i}_t$,论文定义为
\begin{equation}1-\text{cumax}\left( W_{\tilde{i}} x_{t} + U_{\tilde{i}} h_{t - 1} + b_{\tilde{i}} \right)\end{equation}
这个选择会产生类似的单调递减的向量,一般情况下没有什么差别,但从对称性的角度来看,我认为我的选择更合理一些。
实验与思考 #
下面简单汇总一下ON-LSTM的实验,其中包括原作者的实现(PyTroch)以及笔者自己的复现(Keras),最后谈及笔者对此ON-LSTM的一些思考。
作者实现:https://github.com/yikangshen/Ordered-Neurons
个人实现:https://github.com/bojone/on-lstm
(限于笔者水平,个人的理解、复现可能存在问题,如果读者发现,请不吝指出,谢谢。个人复现目前只保证Python 2.7 + Tensorflow 1.8 + Keras 2.24能跑通,其他环境不保证。)
分组的层级 #
代表层级的向量$\tilde{f}_t,\tilde{i}_t$要与$f_t$等做$\circ$运算,这意味着它们的维度大小(即神经元数目)要相等。而我们知道,根据不同的需求,LSTM的隐层神经元数可以达到几百甚至几千,这意味着$\tilde{f}_t,\tilde{i}_t$所描述的层级数也有几百甚至几千。而事实上,序列的层级结构(如果存在的话)的总层级数一般不会太大,也就是说两者之间存在一点矛盾之处。
ON-LSTM的作者想了个比较合理的解决方法,假设隐层神经元数目为$n$,它可以分解为$n=pq$,那么我们可以只构造一个$p$个神经元的$\tilde{f}_t,\tilde{i}_t$,然后将$\tilde{f}_t,\tilde{i}_t$的每个神经元依次重复$q$次,这样就得到一个$n$维的$\tilde{f}_t,\tilde{i}_t$,然后再与$f_t$等做$\circ$运算。例如$n=6=2\times 3$,那么先构造一个2维向量如$[0.1, 0.9]$,然后依次重复3次得到$[0.1, 0.1, 0.1, 0.9, 0.9, 0.9]$。
这样一来,我们既减少了层级的总数,同时还减少了模型的参数量,因为$p$通常可以取得比较小(比$n$小1~2个数量级),因此相比普通的LSTM,ON-LSTM并没有增加太多参数量。
语言模型 #
作者做了若干个实验,包括语言模型、句法评价、逻辑推理等,不少实验中均达到当前最优效果,普遍超越了普通的LSTM,这证明了ON-LSTM所引入的层级结构信息是有价值的。其中我比较熟悉的也就只有语言模型了,就放一个语言模型实验的截图好了:
无监督句法 #
如果仅仅是在常规的一些语言任务中超过普通LSTM,那么ON-LSTM也算不上什么突破,但ON-LSTM的一个令人兴奋的特性是它能够无监督地从训练好的模型(比如语言模型)中提取输入序列的层级树结构。提取的思路如下:
首先我们考虑:
\begin{equation}p_f = softmax\left( W_{\tilde{f}} x_{t} + U_{\tilde{f}} h_{t - 1} + b_{\tilde{f}} \right)\end{equation}
它是$\tilde{f}_t$在$\stackrel{\rightarrow}{\text{cs}}$之前的结果,根据我们前面的推导,它就是历史信息的层级$d_f$的一个软化版本,那么我们可以写出:
\begin{equation}d_f\approx\mathop{\text{argmax}}_{k} p_f(k)\end{equation}
这里的$p_f(k)$就是指向量$p_f$的第$k$个元素。但是,$p_f$中所包含的$softmax$本身就是一个“软化”后的算子,这种情况下我们可能考虑“软化”的$\text{argmax}$比较好(参考《函数光滑化杂谈:不可导函数的可导逼近》),即
\begin{equation}d_f\approx \sum_{k=1}^n k\times p_f(k)=n\left(1 - \frac{1}{n}\sum_{k=1}^n \tilde{f}_t(k)\right)+1\end{equation}
第二个等号是恒等变换,大家可以自行证明一下(原论文$(15)$式是有误的)。这样我们就得到了一个层级的计算公式了,当$n$固定时它直接取决于它$\left(1 - \frac{1}{n}\sum\limits_{k=1}^n \tilde{f}_t(k)\right)$
这样以来,我们就可以用序列
\begin{equation}\left\{d_{f,t}\right\}_{t=1}^{\text{seq_len}}=\left\{\left(1 - \frac{1}{n}\sum\limits_{k=1}^n \tilde{f}_t(k)\right)\right\}_{t=1}^{\text{seq_len}}\end{equation}
来表示输入序列的层级变化。有了这个层级序列后,按照下述贪心算法来析出层次结构:
给定输入序列$\left\{x_{t}\right\}$到预训练好的ON-LSTM,输出对应的层级序列$\left\{d_{f,t}\right\}$,然后找出层级序列中最大值所在的下标,比如$k$,那么就将输入序列分区为$[x_{t < k}, [x_k, x_{t > k}]]$。然后对子序列$x_{t < k}$和$x_{t > k}$重复上述步骤,直到每个子序列长度为1。
算法的大概意思是从最高层级处断开(这意味着当此处包含的历史信息最少,与前面所有内容的联系最为薄弱,最有可能是一个新的子结构的开始),然后递归处理,从而逐渐得到输入序列隐含的嵌套结构。作者是用三层的ON-LSTM训练了一个语言模型,然后用中间那层ON-LSTM的$\tilde{f}_t$来计算层级,然后跟标注的句法结构对比,发现准确率颇高。我自己也在中文语料下尝试了一下:https://github.com/bojone/on-lstm/blob/master/lm_model.py
至于效果,因为我没做过也不了解句法分析,我也不知道怎么评价,反正好像看着是那么一回事,但是又好像不大对一样,所以各位读者自己评价好了~近一两年,无监督句法分析其实还有不少研究工作,可能要都读一读才能更深刻地理解ON-LSTM。
输入:苹果的颜色是什么
输出:
[
[
[
'苹果',
'的'
],
[
'颜色',
'是'
]
],
'什么'
]输入:爱真的需要勇气
输出:
[
'爱',
[
'真的',
[
'需要',
'勇气'
]
]
]
思考与发散 #
文章最后,我们来一起思考几个问题。
RNN还有研究价值?
首先,有读者可能会困惑,都9102年了,居然还有人研究RNN类模型,还有研究价值吗?近年来,BERT、GPT等基于Attention和语言模型的预训练模型,在NLP的诸多任务上都提升了效果,甚至有文章直接说“RNN已死”之类的。事实上真的如此吗?我认为,RNN活得好好的,并且在将来的相当长时间内都不会死,原因至少包含下面几个:
第一,BERT之类的模型,以增加好几个数量级的算力为代价,在一些任务上提升了也就一两个百分点的效果,这样的性价比只有在学术研究和比赛刷榜才有价值,在工程上几乎没什么用(至少没法直接用);第二,RNN类的模型本身具有一些无可比拟的优势,比如它能轻松模拟一个计数函数,在很多序列分析的场景,RNN效果好得很;第三,几乎所有seq2seq模型(哪怕是BERT中)decoder都是一种RNN,因为它们基本都是递归解码的,RNN哪会消失?
单向ON-LSTM就够了?
然后,读者可能会有疑惑:你要析出层级结构,但是只用了单向的ON-LSTM,这意味着当前的层级分析还不依赖于将来的输入,这显然是不大符合事实的。这个笔者也有同样的困惑,但是作者的实验表明这样做效果已经够好了,可能自然语言的整体结构都倾向于是局部的、单向的(从左往右),所以对于自然语言来说单向也就够了。
如果一般情况下是否用双向比较好呢?双向的话是不是要像BERT那样用masked language model的方式来训练呢?双向的话又怎么计算层级序列呢?这一切都还没有完整的答案。至于无监督析出的结构是不是一定就符合人类自身理解的层级结构呢?这个也说不准,因为比较没有什么监督指引,神经网络就“按照自己的方式去理解”了,而幸运的是,神经网络的“自己的方式”,似乎跟人类自身的方式有不少重叠之处。
为什么析出层级考虑的是$d_f$而不是$d_i$?
读者可能会困惑,明明有两个master gate,为什么析出层级用$d_f$而不是$d_i$?要回答这个问题,我们要理解$d_f$的含义。我们说$d_f$是历史信息的层级,换言之,它告诉我们做出当前决策还要用多少历史信息。如果$d_f$很大,意味着当前决策几乎用不着历史信息了,这意味着从当前开始就是一个新层级的开始,与历史输入几乎割断了联系。也就是从这种割断和联系中析出了层级结构,所以只能用$d_f$。
能否用到CNN或者Attention?
最后,可能想到的一个困惑是,这种设计能不能用到CNN、Attention之中呢?换句话说能不能将CNN、Attention的神经元也排个序,融入层级结构信息呢?个人感觉是有可能的,但需要重新设计,因为层级结构被假设为连续嵌套的,RNN的递归性正好可以描述了这种连续性,而CNN、Attention的非递归性导致我们很难直接去表达这种连续嵌套结构。
不管怎样,我觉得这是个值得思考的主题,有进一步的思考结果我会和大家分享,当然也欢迎读者们和我分享你的思考。
文章总结 #
本文梳理了LSTM的一个新变种ON-LSTM的来龙去脉,主要突出了它在表达层级结构上的设计原理。个人感觉整体的设计还是比较巧妙和有趣的,值得细细思考一番。
最后,学习和研究都关键是有自己的判断能力,不要人云亦云,更不能轻信媒体的“标题党”。BERT的Transformer固然有它的优势,但是LSTM等RNN模型的魅力依然不可小觑。我甚至觉得,诸如LSTM之类的RNN模型,会在将来的某天,焕发出更强烈的光彩,transformer与之相比将会相当逊色。
让我们拭目以待好了。
转载到请包括本文地址:https://kexue.fm/archives/6621
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (May. 28, 2019). 《ON-LSTM:用有序神经元表达层次结构 》[Blog post]. Retrieved from https://kexue.fm/archives/6621
@online{kexuefm-6621,
title={ON-LSTM:用有序神经元表达层次结构},
author={苏剑林},
year={2019},
month={May},
url={\url{https://kexue.fm/archives/6621}},
}
June 14th, 2019
赞,多多少少看懂了,原论文看完一脸懵逼,感谢苏神。
June 17th, 2019
当我尝试在用这个模型时,遇到了一些错误。希望您能帮助我一下,谢谢您。我刚入门不知道怎么做。
我的邮箱号是267790066@qq.com
x = Embedding(max_features=50000, embed_size=200, weights=[embedding_matrix], trainable=True)(inp)
x = ONLSTM(200,4,return_sequences=True, dropconnect=0.25)(x)
报错:
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
in
23 # x = Bidirectional(LSTM(32, return_sequences=True, dropout=0.1, recurrent_dropout=0.1))(x)
24
---> 25 x = ONLSTM(200,16,return_sequences=True, dropconnect=0.25)(x)
26
27 # att = Attention(maxlen)(x)
~/work/on_lstm_keras.py in __init__(self, units, levels, return_sequences, dropconnect, **kwargs)
39 dropconnect=None,
40 **kwargs):
---> 41 assert units % levels == 0
42 self.units = units
43 self.levels = levels
AssertionError:
我也不知道为什么你这里会出现assert错误,理论上你写units=200, levels=4是合理的。
要不然你去掉assert units % levels == 0这一句看看吧。
x = ONLSTM(200,16,return_sequences=True, dropconnect=0.25)(x)这里明明写的是200和16,怎么会是200和4呢?16肯定不能整除200呀?
June 18th, 2019
公式(8)上方“也没有改变整个更新课程不可导的事实”这里的“课程”是不是“过程”的意思
是的,已经修正,感谢指出。
June 26th, 2019
哇超棒的博客,感谢!!
July 2nd, 2019
在lm_lstm.py中的loss怎么定义成K.sparse_categorical_crossentropy(x_in[:, 1:], x[:, :-1]),没看明白,希望解答一下
请思考以下几个问题:
1、x_in是什么?x_in[:, 1:]相比x_in少了什么?
2、x是什么?x[:, :-1]相比x少了什么?
x_in我理解的是句子分词后的编码,你这里是统计了词频进行排序,但是没看懂为什么+4,x是softmax输出,我理解的是你这里的语言模型应该预测下一个词,但是这个loss我确实没看懂,
那你为什么觉得K.sparse_categorical_crossentropy(x_in[:, 1:], x[:, :-1])不是在预测下一个词呢?我说它明明就是在预测下一个词呀。
我接触的nlp不多,我举个例子,不知道对不对,比方输入是[今天 天气 不错 呀],x_in[:,1:]是不是就变成了[天气 不错 呀],我感觉是不应该写成x_in[:,maxlen],但是你的数据处理又没有像传统的三元模型那样进行预处理,麻烦大神解下疑惑
如果句子是“今天 天气 不错 呀”,那么输入就是“ 今天 天气 不错 呀 ”,x[:, :-1]就是“ 今天 天气 不错 呀”的编码,x_in[:, 1:]就是“今天 天气 不错 呀 ”。
K.sparse_categorical_crossentropy(x_in[:, 1:], x[:, :-1])就是用“ 今天 天气 不错 呀”去预测“今天 天气 不错 呀 ”,这不就是语言模型了吗?
0,1,2,3,分别表示填充,未登录词,开始,结束符,所以要用len(word)+4.
July 10th, 2019
请问使用paddlepaddle实现这种lstm应该在哪里对已有的lstm进行修改呢?
我怎么知道呀,我又不教paddlepaddle...
August 9th, 2019
请问在您提供的代码里面,有question.txt和onlstm_config.json这两个文件,在github上没有,能否提供一下?谢谢
不能~难道训练个语言模型还要我来提供语料么?
提供个格式就可以了
onlstm_config.json自己会生成,至于训练语料,自己根据自己的数据格式修改text_generator和data_generator这两个函数(生成器)就行了,不需要遵循我的格式。
August 27th, 2019
苏老师,你好!尝试把你的代码跑通后,训练得到一个语言模型,请问如何使用该语言模型得到输入的编码输出,貌似您代码里面没有给出!
看样子你对NLP是有点基础的,因此只需要先学好Python,然后再学好Keras即可。
苏老师,输出输入序列的编码,是不是用你代码里面的lm_model.predict得到?此外,我用训练好的语言模型计算输入序列的困惑度ppl,是不是这样计算:先求loss,然后math.exp(loss),但我求得loss有16这么大,是不是属于异常情况,那得到的困惑度岂不是2.71的16次方?
计算输入序列loss的函数定义:lm_loss = K.function([x_in], [loss]),输出loss_v=lm_loss([sid])[0],其中sid为输入序列的词索引序列,请问这样对不对?
苏老师,可以留个联系方式,能够直接联系到您的吗?我有挺多不解,跑出来的语言模型,计算文本的loss,和训练时的loss差的太大了,不知道是哪里出了问题?训练得到的语言模型以h5形式保存整个模型,而不仅仅是权重,加载时使用load_model,并重写ONSTM层的get_config方法,设置custom_objects={'ONLSTM':ONLSTM},compile=False,才可以加载成功,否则使用默认的compile=True加载失败,报错:ValueError: When passing a list as loss, it should have one entry per model outputs. The model has 1 outputs, but you passed loss=[],请问是哪里出问题了呢?
博客就可以直接联系到我。
1、loss不知道哪里出了问题就逐一排除;
2、我没有教过你用model.save来保存模型,我所有keras开源代码中都是用model.save_weights来保存模型权重的,所以这是你自己的选择,所以只能你自己解决,我帮不了你。
苏老师,不管是保存权重还是整个模型,得到的loss和编码输出都是相同的,说明模型加载方式没问题,但是使用参与过训练的某个句子计算其loss,还是很大,训练停止的loss比较合理,在1以下,排查了n次了,也不知道是哪里出问题,麻烦您在你的代码上面跑一下loss看看和我这边的情况是不是一样的?
你通过什么方式计算单个样本的loss,给你的代码看看。
class keras_model(object):
def __init__(self, vocab_size, emb_size, num_levels,dropout):
self.x_in = Input(shape=(None,), dtype='int32') # 句子输入
self.x = self.x_in
self.x = Embedding(vocab_size,emb_size)(self.x)
self.x = Dropout(dropout)(self.x)
self.onlstms = []
for i in range(3):
onlstm = ONLSTM(emb_size, num_levels, return_sequences=True,\ dropconnect=dropout)
self.onlstms.append(onlstm)
self.x = onlstm(self.x)
self.x = Dense(vocab_size, activation='softmax')(self.x)
self.x_mask = K.cast(K.greater(self.x_in[:, :-1], 0), 'float32')
self.loss = K.sum(K.sparse_categorical_crossentropy(self.x_in[:,\ 1:], self.x[:, :-1]) * self.x_mask) / K.sum(self.x_mask)
kmodel = keras_model(len(words)+4, self.word_size,self.num_levels,0.25)
lm_model = Model(kmodel.x_in, kmodel.x)
lm_model.add_loss(kmodel.loss)
lm_model.compile(optimizer='adam')
class Evaluate(Callback):
def __init__(self):
self.lowest = 1e10
def on_epoch_end(self, epoch, logs=None):
if logs['loss']
接上没贴完的代码
class Evaluate(Callback):
def __init__(self):
self.lowest = 1e10
def on_epoch_end(self, epoch, logs=None):
# print("min loss:{}".format(self.lowest))
# 保存最优结果
if logs['loss']
func_encoder_loss_distance = K.function([kmodel.x_in], [kmodel.x, kmodel.loss, kmodel.onlstms[0].distance])
s=u'明天去北京出差参加会议'
s = self.tokenize(s)
sid = np.array([self.string2id(s)[:-1]])
encoder, loss, distance = func_encoder_loss_distance([sid])
@hflv|comment-11964
辛苦一下,直接把脚本文件发我邮箱吧(bojone@spaces.ac.cn),这样不好测,但目测好像也没啥问题。
如果你想即时交流,可以通过加机器人微信号spaces_ac_cn进入交流群,或者QQ群67729435。
好的
September 1st, 2019
请问层次有什么固定值会比较好么,或者不同长度文本也会不一样?
没有标准答案。
September 29th, 2019
最近一些论文研究表示,通过分析attn weight,认为BERT能够一定程度上学习到句子的语法层级结构。通过分析每一层输出向量在二位空间的位置映射,认为BERT能够把与label相关的concept和reference聚类在临近的空间。因此,BERT具有一定的语法树分析能力,并且利用这能力进行语义聚类。
后续也有论文通过学习syntatic prior probability,通过masking加强了transformer对树型结构的学习。那么你觉得,应该如何仿照ON-LSTM,让BERT动态判断语句的深度呢?因为预训练模型完成语法和语义分析都是在中低层,如果实际语句的树层数比较高,要动态利用更高的层数去分析语法是难点。毕竟BERT,或者说预训练模型是有限层数的。其实我更像解决的是,如何更像人类那样multi-hop的思考或者提取特定特征
其实我想过这个问题。准确来说,只需要想明白如何在双向的模型结构中引入层级评估,但是很遗憾,我还没有想到比较好的方案。