循环神经网络(RNN)的原理及实现

495 阅读6分钟

在前馈神经网络中,信息的传递是单向的,这种限制虽然使得网络变得更容易学习,但在一定程度上也减弱了神经网络模型的能力。在生物神经网络中,神经元之间的连接关系要复杂的多。前馈神经网络可以看着是一个复杂的函数,每次输入都是独立的,即网络的输出只依赖于当前的输入。但是在很多现实任务中,网络的输入不仅和当前时刻的输入相关,也和其过去一段时间的输出相关。比如一个有限状态自动机,其下一个时刻的状态(输出)不仅仅和当前输入相关,也和当前状态(上一个时刻的输出)相关。此外,前馈网络难以处理时序数据,比如视频、语音、文本等。时序数据的长度一般是不固定的,而前馈神经网络要求输入和输出的维数都是固定的,不能任意改变。因此,当处理这一类和时序相关的问题时,就需要一种能力更强的模型。

循环神经网络(Recurrent Neural Network,RNN) 是一类具有短期记忆能力的神经网络。在循环神经网络中,神经元不但可以接受其它神经元的信息,也可以接受自身的信息,形成具有环路的网络结构。和前馈神经网络相比,循环神经网络更加符合生物神经网络的结构。循环神经网络已经被广泛应用在语音识别、语言模型以及自然语言生成等任务上。循环神经网络的参数学习可以通过 随时间反向传播算法[Werbos, 1990] 来学习。随时间反向传播算法即按照时间的逆序将错误信息一步步地往前传递。当输入序列比较长时,会存在梯度爆炸 和消失问题[Bengio et al., 1994, Hochreiter and Schmidhuber, 1997, Hochreiter et al., 2001],也称为长期依赖问题。为了解决这个问题,人们对循环神经网络进行了很多的改进,其中最有效的改进方式引入门控机制。

此外,循环神经网络可以很容易地扩展到两种更广义的记忆网络模型:递归神经网络图网络

给网络增加记忆能力

为了处理这些时序数据并利用其历史信息,我们需要让网络具有短期记忆能力。 而前馈网络是一个静态网络,不具备这种记忆能力。 一般来讲,我们可以通过以下三种方法来给网络增加短期记忆能力。

延时神经网络

一种简单的利用历史信息的方法是建立一个额外的延时单元,用来存储网 络的历史信息(可以包括输入、输出、隐状态等)。比较有代表性的模型是延时 神经网络(Time Delay Neural Network,TDNN)[Lang et al., 1990, Waibelet al., 1989]。

延时神经网络是在前馈网络中的非输出层都添加一个延时器,记录最近几 次神经元的输出。在第tt个时刻,第l+1l +1 层神经元和第ll层神经元的最近pp 次输出相关,即

ht(l+1)=f(ht(l),ht1(l),,htp+1(l)).\mathbf{h}_t^{(l+1)}=f(\mathbf{h}_t^{(l)},\mathbf{h}_{t-1}^{(l)},\cdots,\mathbf{h}_{t-p+1}^{(l)}).

通过延时器,前馈网络就具有了短期记忆的能力。

有外部输入的非线性自回归模型

自回归模型(Autoregressive Model,AR) 是统计学上常用的一类时间序列模型,用一个变量yty_t 的历史信息来预测自己。

yt=w0+i=1pwpyti+ϵt,\mathbf{y}_t=w_0+\sum\limits_{i=1}^p w_p\mathbf{y}_{t-i}+\epsilon_t,

其中pp为超参数,wpw_p 为参数,ϵtN(0,σ2)ϵ_t ∼ N(0, \sigma^{2}) 为第tt 个时刻的噪声,方差σ2\sigma^{2} 和时间无关。

有外部输入的非线性自回归模型(Nonlinear Autoregressive with Exogenous Inputs Model,NARX)[Leontaritis and Billings, 1985] 是自回归模型的 扩展,在每个时刻tt 都有一个外部输入xtx_t,产生一个输出yty_t。NARX通过一个延时器记录最近几次的外部输入和输出,第tt个时刻的输出yty_t

yt=f(xt,xt1,,xtp,yt1,yt2,,ytq)\mathbf{y}_t=f(\mathbf{x}_t,\mathbf{x}_{t-1},\cdots,\mathbf{x}_{t-p},\mathbf{y}_{t-1},\mathbf{y}_{t-2},\cdots,\mathbf{y}_{t-q})

其中f()f(·) 表示非线性函数,可以是一个前馈网络,ppqq 为超参数。

循环神经网络

循环神经网络通过使用带自反馈的神经元,能够处理任意长度的时序数据。

给定一个输入序列

x1:T=(x1,x2,,xt,,xT)\mathbf{x}_{1:T}=(\mathbf{x}_{1},\mathbf{x}_{2},\ldots,\mathbf{x}_{t},\ldots,\mathbf{x}_{T})

循环神经网络通过下面公式更新带反馈边的隐藏层的活性值hth_t

ht=f(ht1,xt)\mathbf{h}_t=f(\mathbf{h}_{t-1},\mathbf{x}_t)

其中h0=0h_0 = 0f()f(·) 为一个非线性函数,也可以是一个前馈网络。

如图给出了循环神经网路的示例。

循环神经网络

从数学上讲,上述公式可以看成一个动力系统。动力系统(Dynamical Sys-tem) 是一个数学上的概念,指系统状态按照一定的规律随时间变化的系统。

生活中很多现象都可以动力系统来描述,比如钟摆晃动、台球轨迹等。具体地讲,动力系统是使用一个函数来描述一个给定空间(如某个物理系统的状态空间)中所有点随时间的变化情况。因此,隐藏层的活性值hth_t 在很多文献上也称为状态(state)或隐状态(hidden states)。理论上,循环神经网络可以近似任意的非线性动力系统。

RNN的实现:

class rnn(TFBaseModel):

    def __init__(
        self,
        lstm_size,
        output_mixture_components,
        attention_mixture_components,
        **kwargs
    ):
        self.lstm_size = lstm_size
        self.output_mixture_components = output_mixture_components
        self.output_units = self.output_mixture_components*6 + 1
        self.attention_mixture_components = attention_mixture_components
        super(rnn, self).__init__(**kwargs)

    def parse_parameters(self, z, eps=1e-8, sigma_eps=1e-4):
        pis, sigmas, rhos, mus, es = tf.split(
            z,
            [
                1*self.output_mixture_components,
                2*self.output_mixture_components,
                1*self.output_mixture_components,
                2*self.output_mixture_components,
                1
            ],
            axis=-1
        )
        pis = tf.nn.softmax(pis, axis=-1)
        sigmas = tf.clip_by_value(tf.exp(sigmas), sigma_eps, np.inf)
        rhos = tf.clip_by_value(tf.tanh(rhos), eps - 1.0, 1.0 - eps)
        es = tf.clip_by_value(tf.nn.sigmoid(es), eps, 1.0 - eps)
        return pis, mus, sigmas, rhos, es

    def NLL(self, y, lengths, pis, mus, sigmas, rho, es, eps=1e-8):
        sigma_1, sigma_2 = tf.split(sigmas, 2, axis=2)
        y_1, y_2, y_3 = tf.split(y, 3, axis=2)
        mu_1, mu_2 = tf.split(mus, 2, axis=2)

        norm = 1.0 / (2*np.pi*sigma_1*sigma_2 * tf.sqrt(1 - tf.square(rho)))
        Z = tf.square((y_1 - mu_1) / (sigma_1)) + \
            tf.square((y_2 - mu_2) / (sigma_2)) - \
            2*rho*(y_1 - mu_1)*(y_2 - mu_2) / (sigma_1*sigma_2)

        exp = -1.0*Z / (2*(1 - tf.square(rho)))
        gaussian_likelihoods = tf.exp(exp) * norm
        gmm_likelihood = tf.reduce_sum(pis * gaussian_likelihoods, 2)
        gmm_likelihood = tf.clip_by_value(gmm_likelihood, eps, np.inf)

        bernoulli_likelihood = tf.squeeze(tf.where(tf.equal(tf.ones_like(y_3), y_3), es, 1 - es))

        nll = -(tf.log(gmm_likelihood) + tf.log(bernoulli_likelihood))
        sequence_mask = tf.logical_and(
            tf.sequence_mask(lengths, maxlen=tf.shape(y)[1]),
            tf.logical_not(tf.is_nan(nll)),
        )
        nll = tf.where(sequence_mask, nll, tf.zeros_like(nll))
        num_valid = tf.reduce_sum(tf.cast(sequence_mask, tf.float32), axis=1)

        sequence_loss = tf.reduce_sum(nll, axis=1) / tf.maximum(num_valid, 1.0)
        element_loss = tf.reduce_sum(nll) / tf.maximum(tf.reduce_sum(num_valid), 1.0)
        return sequence_loss, element_loss

    def sample(self, cell):
        initial_state = cell.zero_state(self.num_samples, dtype=tf.float32)
        initial_input = tf.concat([
            tf.zeros([self.num_samples, 2]),
            tf.ones([self.num_samples, 1]),
        ], axis=1)
        return rnn_free_run(
            cell=cell,
            sequence_length=self.sample_tsteps,
            initial_state=initial_state,
            initial_input=initial_input,
            scope='rnn'
        )[1]

    def primed_sample(self, cell):
        initial_state = cell.zero_state(self.num_samples, dtype=tf.float32)
        primed_state = tf.nn.dynamic_rnn(
            inputs=self.x_prime,
            cell=cell,
            sequence_length=self.x_prime_len,
            dtype=tf.float32,
            initial_state=initial_state,
            scope='rnn'
        )[1]
        return rnn_free_run(
            cell=cell,
            sequence_length=self.sample_tsteps,
            initial_state=primed_state,
            scope='rnn'
        )[1]

    def calculate_loss(self):
        self.x = tf.placeholder(tf.float32, [None, None, 3])
        self.y = tf.placeholder(tf.float32, [None, None, 3])
        self.x_len = tf.placeholder(tf.int32, [None])
        self.c = tf.placeholder(tf.int32, [None, None])
        self.c_len = tf.placeholder(tf.int32, [None])

        self.sample_tsteps = tf.placeholder(tf.int32, [])
        self.num_samples = tf.placeholder(tf.int32, [])
        self.prime = tf.placeholder(tf.bool, [])
        self.x_prime = tf.placeholder(tf.float32, [None, None, 3])
        self.x_prime_len = tf.placeholder(tf.int32, [None])
        self.bias = tf.placeholder_with_default(
            tf.zeros([self.num_samples], dtype=tf.float32), [None])

        cell = LSTMAttentionCell(
            lstm_size=self.lstm_size,
            num_attn_mixture_components=self.attention_mixture_components,
            attention_values=tf.one_hot(self.c, len(drawing.alphabet)),
            attention_values_lengths=self.c_len,
            num_output_mixture_components=self.output_mixture_components,
            bias=self.bias
        )
        self.initial_state = cell.zero_state(tf.shape(self.x)[0], dtype=tf.float32)
        outputs, self.final_state = tf.nn.dynamic_rnn(
            inputs=self.x,
            cell=cell,
            sequence_length=self.x_len,
            dtype=tf.float32,
            initial_state=self.initial_state,
            scope='rnn'
        )
        params = time_distributed_dense_layer(outputs, self.output_units, scope='rnn/gmm')
        pis, mus, sigmas, rhos, es = self.parse_parameters(params)
        sequence_loss, self.loss = self.NLL(self.y, self.x_len, pis, mus, sigmas, rhos, es)

        self.sampled_sequence = tf.cond(
            self.prime,
            lambda: self.primed_sample(cell),
            lambda: self.sample(cell)
        )
        return self.loss


if __name__ == '__main__':
    dr = DataReader(data_dir='data/processed/')

    nn = rnn(
        reader=dr,
        log_dir='logs',
        checkpoint_dir='checkpoints',
        prediction_dir='predictions',
        learning_rates=[.0001, .00005, .00002],
        batch_sizes=[32, 64, 64],
        patiences=[1500, 1000, 500],
        beta1_decays=[.9, .9, .9],
        validation_batch_size=32,
        optimizer='rms',
        num_training_steps=100000,
        warm_start_init_step=0,
        regularization_constant=0.0,
        keep_prob=1.0,
        enable_parameter_averaging=False,
        min_steps_to_checkpoint=2000,
        log_interval=20,
        grad_clip=10,
        lstm_size=400,
        output_mixture_components=20,
        attention_mixture_components=10
    )
    nn.fit()

“本文正在参加 人工智能创作者扶持计划 ”