Auto Byte

专注未来出行及智能汽车科技

微信扫一扫获取更多资讯

Science AI

关注人工智能与其他前沿技术、基础学科的交叉研究与融合发展

微信扫一扫获取更多资讯

苏剑林作者

貌离神合的RNN与ODE:花式RNN简介

作者丨苏剑林

单位丨广州火焰信息科技有限公司

研究方向丨NLP,神经网络

个人主页丨kexue.fm

本来笔者已经决心不玩 RNN 了,但是在上个星期思考时忽然意识到 RNN 实际上对应了 ODE(常微分方程)的数值解法,这为我一直以来想做的事情——用深度学习来解决一些纯数学问题——提供了思路。事实上这是一个颇为有趣和有用的结果,遂介绍一翻。顺便地,本文也涉及到了自己动手编写 RNN 的内容,所以本文也可以作为编写自定义的 RNN 层的一个简单教程

注:本文并非前段时间的热点“神经 ODE [1]”的介绍(但有一定的联系)。

RNN基本

什么是RNN? 

众所周知,RNN 是“循环神经网络(Recurrent Neural Network)”,跟 CNN 不同,RNN 可以说是一类模型的总称,而并非单个模型。简单来讲,只要是输入向量序列 (x1,x2,…,xT),输出另外一个向量序列 (y1,y2,…,yT),并且满足如下递归关系的模型,都可以称为 RNN。

也正因为如此,原始的朴素 RNN,还有改进的如 GRU、LSTM、SRU 等模型,我们都称为 RNN,因为它们都可以作为上式的一个特例。还有一些看上去与 RNN 没关的内容,比如前不久介绍的 CRF 的分母的计算,实际上也是一个简单的 RNN。

说白了,RNN 其实就是递归计算

自己编写RNN

这里我们先介绍如何用 Keras 简单快捷地编写自定义的 RNN。 

事实上,不管在 Keras 还是纯 tensorflow 中,要自定义自己的 RNN 都不算复杂。在 Keras 中,只要写出每一步的递归函数;而在 tensorflow 中,则稍微复杂一点,需要将每一步的递归函数封装为一个 RNNCell 类。

下面介绍用 Keras 实现最基本的一个 RNN:

代码非常简单:

#! -*- coding: utf-8- -*-

from keras.layers import Layer
import keras.backend as K


class My_RNN(Layer):

    def __init__(self, output_dim, **kwargs):
        self.output_dim = output_dim # 输出维度
        super(My_RNN, self).__init__(**kwargs)

    def build(self, input_shape): # 定义可训练参数
        self.kernel1 = self.add_weight(name='kernel1',
                                      shape=(self.output_dim, self.output_dim),
                                      initializer='glorot_normal',
                                      trainable=True)
        self.kernel2 = self.add_weight(name='kernel2',
                                      shape=(input_shape[-1], self.output_dim),
                                      initializer='glorot_normal',
                                      trainable=True)
        self.bias = self.add_weight(name='kernel',
                                      shape=(self.output_dim,),
                                      initializer='glorot_normal',
                                      trainable=True)

    def step_do(self, step_in, states): # 定义每一步的迭代
        step_out = K.tanh(K.dot(states[0], self.kernel1) +
                          K.dot(step_in, self.kernel2) +
                          self.bias)
        return step_out, [step_out]

    def call(self, inputs): # 定义正式执行的函数
        init_states = [K.zeros((K.shape(inputs)[0],
                                self.output_dim)
                              )] # 定义初始态(全零)
        outputs = K.rnn(self.step_do, inputs, init_states) # 循环执行step_do函数
        return outputs[0] # outputs是一个tuple,outputs[0]为最后时刻的输出,
                          # outputs[1]为整个输出的时间序列,output[2]是一个list,
                          # 是中间的隐藏状态。

    def compute_output_shape(self, input_shape):
        return (input_shape[0], self.output_dim)


可以看到,虽然代码行数不少,但大部分都只是固定格式的语句,真正定义 RNN 的,是 step_do 这个函数,这个函数接受两个输入:step_in 和 states。其中 step_in 是一个 (batch_size, input_dim) 的张量,代表当前时刻的样本 xt,而 states 是一个 list,代表 yt−1 及一些中间变量。

特别要注意的是,states 是一个张量的 list,而不是单个张量,这是因为在递归过程中可能要同时传递多个中间变量,而不仅仅是 yt−1 一个,比如 LSTM 就需要有两个态张量。最后 step_do 要返回 yt 和新的 states,这是 step_do 这步的函数的编写规范。 

而 K.rnn 这个函数,接受三个基本参数(还有其他参数,请自行看官方文档),其中第一个参数就是刚才写好的 step_do 函数,第二个参数则是输入的时间序列,第三个是初始态,跟前面说的 states 一致,所以很自然 init_states 也是一个张量的 list,默认情况下我们会选择全零初始化。

ODE基本

什么是ODE?

ODE 就是“常微分方程(Ordinary Differential Equation)”,这里指的是一般的常微分方程组:

研究 ODE 的领域通常也直接称为“动力学”、“动力系统”,这是因为牛顿力学通常也就只是一组 ODE 而已。

ODE可以产生非常丰富的函数。比如 e^t 其实就是 x˙=x 的解,sint 和 cost 都是 x¨+x=0 的解(初始条件不同)。事实上,我记得确实有一些教程是直接通过微分方程 x˙=x 来定义 e^t 函数的。除了这些初等函数,很多我们能叫得上名字但不知道是什么鬼的特殊函数,都是通过 ODE 导出来的,比如超几何函数、勒让德函数、贝塞尔函数...

总之,ODE 能产生并且已经产生了各种各样千奇百怪的函数~

数值解ODE 

能精确求出解析解的 ODE 其实是非常少的,所以很多时候我们都需要数值解法。 

ODE 的数值解已经是一门非常成熟的学科了,这里我们也不多做介绍,仅引入最基本的由数学家欧拉提出来的迭代公式:

这里的 h 是步长。欧拉的解法来源很简单,就是用:

来近似导数项 x˙(t)。只要给定初始条件 x(0),我们就可以根据 (4) 一步步迭代算出每个时间点的结果。

ODE与RNN 

ODE也是RNN

大家仔细对比 (4) 和 (1),发现有什么联系了吗?

在 (1) 中,t 是一个整数变量,在 (4) 中,t 是一个浮点变量,除此之外,(4) 跟 (1) 貌似就没有什么明显的区别了。事实上,在 (4) 中我们可以以 h 为时间单位,记 t=nh,那么 (4) 变成了:

可以看到现在 (6) 中的时间变量 n 也是整数了。这样一来,我们就知道:ODE 的欧拉解法 (4) 实际上就是 RNN 的一个特例罢了。这里我们也许可以间接明白为什么 RNN 的拟合能力如此之强了(尤其是对于时间序列数据),我们看到 ODE 可以产生很多复杂的函数,而 ODE 只不过是 RNN 的一个特例罢了,所以 RNN 也就可以产生更为复杂的函数了。 

用RNN解ODE 

于是,我们就可以写一个 RNN 来解 ODE 了,比如《两生物种群竞争模型》[2] 中的例子:

我们可以写出:

#! -*- coding: utf-8- -*-

from keras.layers import Layer
import keras.backend as K


class ODE_RNN(Layer):

    def __init__(self, steps, h, **kwargs):
        self.steps = steps
        self.h = h
        super(ODE_RNN, self).__init__(**kwargs)

    def step_do(self, step_in, states): # 定义每一步的迭代
        x = states[0]
        r1,r2,a1,a2,iN1,iN2 = 0.1,0.3,0.0001,0.0002,0.002,0.003
        _1 = r1 * x[:,0] * (1 - iN1 * x[:,0]) - a1 * x[:,0] * x[:,1]
        _2 = r2 * x[:,1] * (1 - iN2 * x[:,1]) - a2 * x[:,0] * x[:,1]
        _1 = K.expand_dims(_1, 1)
        _2 = K.expand_dims(_2, 1)
        _ = K.concatenate([_1, _2], 1)
        step_out = x + self.h * _
        return step_out, [step_out]

    def call(self, inputs): # 这里的inputs就是初始条件
        init_states = [inputs]
        zeros = K.zeros((K.shape(inputs)[0],
                         self.steps,
                         K.shape(inputs)[1])) # 迭代过程用不着外部输入,所以
                                              # 指定一个全零输入,只为形式上的传入
        outputs = K.rnn(self.step_do, zeros, init_states) # 循环执行step_do函数
        return outputs[1] # 这次我们输出整个结果序列

    def compute_output_shape(self, input_shape):
        return (input_shape[0], self.steps, input_shape[1])


from keras.models import Sequential
import numpy as np
import matplotlib.pyplot as plt


steps,h = 1000,0.1

M = Sequential()
M.add(ODE_RNN(steps, h, input_shape=(2,)))
M.summary()

# 直接前向传播就输出解了
result = M.predict(np.array([[100, 150]]))[0] # 以[100, 150]为初始条件进行演算
times = np.arange(1, steps+1) * h

# 绘图
plt.plot(times, result[:,0])
plt.plot(times, result[:,1])
plt.savefig('test.png')

整个过程很容易理解,只不过有两点需要指出一下。首先,由于方程组 (7) 只有两维,而且不容易写成矩阵运算,因此我在 step_do 函数中是直接逐位操作的(代码中的 x[:,0],x[:,1]),如果方程本身维度较高,而且能写成矩阵运算,那么直接利用矩阵运算写会更加高效;然后,我们可以看到,写完整个模型之后,直接 predict 就输出结果了,不需要“训练”。

▲ RNN解两物种的竞争模型

反推ODE参数

前一节的介绍也就是说,RNN 的前向传播跟 ODE 的欧拉解法是对应的,那么反向传播又对应什么呢?

在实际问题中,有一类问题称为“模型推断”,它是在已知实验数据的基础上,猜测这批数据符合的模型(机理推断)。这类问题的做法大概分两步,第一步是猜测模型的形式,第二步是确定模型的参数。假定这批数据可以由一个 ODE 描述,并且这个 ODE 的形式已经知道了,那么就需要估计里边的参数

如果能够用公式完全解出这个 ODE,那么这就只是一个非常简单的回归问题罢了。但前面已经说过,多数 ODE 都没有公式解,所以数值方法就必须了。这其实就是 ODE 对应的 RNN 的反向传播所要做的事情:前向传播就是解 ODE(RNN 的预测过程),反向传播自然就是推断 ODE 的参数了(RNN 的训练过程)。这是一个非常有趣的事实:ODE 的参数推断是一个被研究得很充分的课题,然而在深度学习这里,只是 RNN 的一个最基本的应用罢了

我们把刚才的例子的微分方程的解数据保存下来,然后只取几个点,看看能不能反推原来的微分方程出来,解数据为:

假设就已知这有限的点数据,然后假定方程 (7) 的形式,求方程的各个参数。我们修改一下前面的代码:

#! -*- coding: utf-8- -*-

from keras.layers import Layer
import keras.backend as K


def my_init(shape, dtype=None): # 需要定义好初始化,这相当于需要实验估计参数的量级
    return K.variable([0.1, 0.1, 0.001, 0.001, 0.001, 0.001])


class ODE_RNN(Layer):

    def __init__(self, steps, h, **kwargs):
        self.steps = steps
        self.h = h
        super(ODE_RNN, self).__init__(**kwargs)

    def build(self, input_shape): # 将原来的参数设为可训练的参数
        self.kernel = self.add_weight(name='kernel', 
                                      shape=(6,),
                                      initializer=my_init,
                                      trainable=True)
    def step_do(self, step_in, states): # 定义每一步的迭代
        x = states[0]
        r1,r2,a1,a2,iN1,iN2 = (self.kernel[0], self.kernel[1],
                               self.kernel[2], self.kernel[3],
                               self.kernel[4], self.kernel[5])
        _1 = r1 * x[:,0] * (1 - iN1 * x[:,0]) - a1 * x[:,0] * x[:,1]
        _2 = r2 * x[:,1] * (1 - iN2 * x[:,1]) - a2 * x[:,0] * x[:,1]
        _1 = K.expand_dims(_1, 1)
        _2 = K.expand_dims(_2, 1)
        _ = K.concatenate([_1, _2], 1)
        step_out = x + self.h * K.clip(_, -1e5, 1e5) # 防止梯度爆炸
        return step_out, [step_out]

    def call(self, inputs): # 这里的inputs就是初始条件
        init_states = [inputs]
        zeros = K.zeros((K.shape(inputs)[0],
                         self.steps,
                         K.shape(inputs)[1])) # 迭代过程用不着外部输入,所以
                                              # 指定一个全零输入,只为形式上的传入
        outputs = K.rnn(self.step_do, zeros, init_states) # 循环执行step_do函数
        return outputs[1] # 这次我们输出整个结果序列

    def compute_output_shape(self, input_shape):
        return (input_shape[0], self.steps, input_shape[1])



from keras.models import Sequential
from keras.optimizers import Adam
import numpy as np
import matplotlib.pyplot as plt


steps,h = 50, 1 # 用大步长,减少步数,削弱长时依赖,也加快推断速度
series = {0: [100, 150],
          10: [165, 283],
          15: [197, 290],
          30: [280, 276],
          36: [305, 269],
          40: [318, 266],
          42: [324, 264]}

M = Sequential()
M.add(ODE_RNN(steps, h, input_shape=(2,)))
M.summary()

# 构建训练样本
# 其实就只有一个样本序列,X为初始条件,Y为后续时间序列
X = np.array([series[0]])
Y = np.zeros((1, steps, 2))

for i,j in series.items():
    if i != 0:
        Y[0, int(i/h)-1] += series[i]

# 自定义loss
# 在训练的时候,只考虑有数据的几个时刻,没有数据的时刻被忽略
def ode_loss(y_true, y_pred):
    T = K.sum(K.abs(y_true), 2, keepdims=True)
    T = K.cast(K.greater(T, 1e-3), 'float32')
    return K.sum(T * K.square(y_true - y_pred), [1, 2])

M.compile(loss=ode_loss,
          optimizer=Adam(1e-4))

M.fit(X, Y, epochs=10000) # 用低学习率训练足够多轮

# 用训练出来的模型重新预测,绘图,比较结果
result = M.predict(np.array([[100, 150]]))[0]
times = np.arange(1, steps+1) * h

plt.clf()
plt.plot(times, result[:,0], color='blue')
plt.plot(times, result[:,1], color='green')
plt.plot(series.keys(), [i[0] for i in series.values()], 'o', color='blue')
plt.plot(series.keys(), [i[1] for i in series.values()], 'o', color='green')
plt.savefig('test.png')

结果可以用一张图来看:

▲ RNN做ODE的参数估计效果

(散点:有限的实验数据,曲线:估计出来的模型)

显然结果是让人满意的。

又到总结

本文在一个一般的框架下介绍了 RNN 模型及其在 Keras 下的自定义写法,然后揭示了 ODE 与 RNN 的联系。在此基础上,介绍了用 RNN 直接求解 ODE 以及用 RNN 反推 ODE 参数的基本思路。

需要提醒读者的是,在 RNN 模型的反向传播中,要谨慎地做好初始化和截断处理处理,并且选择好学习率等,以防止梯度爆炸的出现(梯度消失只是优化得不够好,梯度爆炸则是直接崩溃了,解决梯度爆炸问题尤为重要)。

总之,梯度消失和梯度爆炸在 RNN 中是一个很经典的困难,事实上,LSTM、GRU 等模型的引入,根本原因就是为了解决 RNN 的梯度消失问题,而梯度爆炸则是通过使用 tanh 或 sigmoid 激活函数来解决的。

但是如果用 RNN 解决 ODE 的话,我们就没有选择激活函数的权利了(激活函数就是 ODE 的一部分),所以只能谨慎地做好初始化及其他处理。据说,只要谨慎做好初始化,普通 RNN 中用 relu 作为激活函数都是无妨的。

相关链接

[1]. Tian Qi C, Yulia R, Jesse B, David D. Neural Ordinary Differential Equations. arXiv preprint arXiv:1806.07366, 2018.

[2]. 两生物种群竞争模型

https://kexue.fm/archives/3120

PaperWeekly
PaperWeekly

推荐、解读、讨论和报道人工智能前沿论文成果的学术平台。

入门RNNODE
61
相关数据
深度学习技术

深度学习(deep learning)是机器学习的分支,是一种试图使用包含复杂结构或由多重非线性变换构成的多个处理层对数据进行高层抽象的算法。 深度学习是机器学习中一种基于对数据进行表征学习的算法,至今已有数种深度学习框架,如卷积神经网络和深度置信网络和递归神经网络等已被应用在计算机视觉、语音识别、自然语言处理、音频识别与生物信息学等领域并获取了极好的效果。

激活函数技术

在 计算网络中, 一个节点的激活函数定义了该节点在给定的输入或输入的集合下的输出。标准的计算机芯片电路可以看作是根据输入得到"开"(1)或"关"(0)输出的数字网络激活函数。这与神经网络中的线性感知机的行为类似。 一种函数(例如 ReLU 或 S 型函数),用于对上一层的所有输入求加权和,然后生成一个输出值(通常为非线性值),并将其传递给下一层。

参数技术

在数学和统计学裡,参数(英语:parameter)是使用通用变量来建立函数和变量之间关系(当这种关系很难用方程来阐述时)的一个数量。

学习率技术

在使用不同优化器(例如随机梯度下降,Adam)神经网络相关训练中,学习速率作为一个超参数控制了权重更新的幅度,以及训练的速度和精度。学习速率太大容易导致目标(代价)函数波动较大从而难以找到最优,而弱学习速率设置太小,则会导致收敛过慢耗时太长

张量技术

张量是一个可用来表示在一些矢量、标量和其他张量之间的线性关系的多线性函数,这些线性关系的基本例子有内积、外积、线性映射以及笛卡儿积。其坐标在 维空间内,有 个分量的一种量,其中每个分量都是坐标的函数,而在坐标变换时,这些分量也依照某些规则作线性变换。称为该张量的秩或阶(与矩阵的秩和阶均无关系)。 在数学里,张量是一种几何实体,或者说广义上的“数量”。张量概念包括标量、矢量和线性算子。张量可以用坐标系统来表达,记作标量的数组,但它是定义为“不依赖于参照系的选择的”。张量在物理和工程学中很重要。例如在扩散张量成像中,表达器官对于水的在各个方向的微分透性的张量可以用来产生大脑的扫描图。工程上最重要的例子可能就是应力张量和应变张量了,它们都是二阶张量,对于一般线性材料他们之间的关系由一个四阶弹性张量来决定。

神经网络技术

(人工)神经网络是一种起源于 20 世纪 50 年代的监督式机器学习模型,那时候研究者构想了「感知器(perceptron)」的想法。这一领域的研究者通常被称为「联结主义者(Connectionist)」,因为这种模型模拟了人脑的功能。神经网络模型通常是通过反向传播算法应用梯度下降训练的。目前神经网络有两大主要类型,它们都是前馈神经网络:卷积神经网络(CNN)和循环神经网络(RNN),其中 RNN 又包含长短期记忆(LSTM)、门控循环单元(GRU)等等。深度学习是一种主要应用于神经网络帮助其取得更好结果的技术。尽管神经网络主要用于监督学习,但也有一些为无监督学习设计的变体,比如自动编码器和生成对抗网络(GAN)。

梯度消失问题技术

梯度消失指的是随着网络深度增加,参数的梯度范数指数式减小的现象。梯度很小,意味着参数的变化很缓慢,从而使得学习过程停滞,直到梯度变得足够大,而这通常需要指数量级的时间。这种思想至少可以追溯到 Bengio 等人 1994 年的论文:「Learning long-term dependencies with gradient descent is difficult」,目前似乎仍然是人们对深度神经网络的训练困难的偏好解释。

楼主你好,很好的一篇文章,但是我有一个疑问。没太看懂“ODE 的欧拉解法 (4) 实际上就是 RNN 的一个特例罢了”这个结论是怎么来的,事实上在我看来正好相反,更像是RNN是当step size为1的时候对ODE的求解,似乎RNN是ODE的特例?谢谢~