循环神经网络 — 从0开始

前面的教程里我们使用的网络都属于前馈神经网络。之所以叫前馈,是因为整个网络是一条链(回想下gluon.nn.Sequential),每一层的结果都是反馈给下一层。这一节我们介绍循环神经网络,这里每一层不仅输出给下一层,同时还输出一个隐含状态,给当前层在处理下一个样本时使用。下图展示这两种网络的区别。

循环神经网络的这种结构使得它适合处理前后有依赖关系数据样本。我们拿语言模型举个例子来解释这个是怎么工作的。语言模型的任务是给定句子的前t个字符,然后预测第t+1个字符。假设我们的句子是“你好世界”,使用前馈神经网络来预测的一个做法是,在时间1输入“你”,预测”好“,时间2向同一个网络输入“好”预测“世”。下图左边展示了这个过程。

注意到一个问题是,当我们预测“世”的时候只给了“好”这个输入,而完全忽略了“你”。直觉上“你”这个词应该对这次的预测比较重要。虽然这个问题通常可以通过n-gram来缓解,就是说预测第t+1个字符的时候,我们输入前n个字符。如果n=1,那就是我们这里用的。我们可以增大n来使得输入含有更多信息。但我们不能任意增大n,因为这样通常带来模型复杂度的增加从而导致需要大量数据和计算来训练模型。

循环神经网络使用一个隐含状态来记录前面看到的数据来帮助当前预测。上图右边展示了这个过程。在预测“好”的时候,我们输出一个隐含状态。我们用这个状态和新的输入“好”来一起预测“世”,然后同时输出一个更新过的隐含状态。我们希望前面的信息能够保存在这个隐含状态里,从而提升预测效果。

循环神经网络

在对输入输出数据有了解后,我们来正式介绍循环神经网络。

首先回忆一下单隐含层的前馈神经网络的定义,例如多层感知机。假设隐含层的激活函数是\(\phi\),对于一个样本数为\(n\)特征向量维度为\(x\)的批量数据\(\mathbf{X} \in \mathbb{R}^{n \times x}\)\(\mathbf{X}\)是一个\(n\)\(x\)列的实数矩阵)来说,那么这个隐含层的输出就是

\[\mathbf{H} = \phi(\mathbf{X} \mathbf{W}_{xh} + \mathbf{b}_h)\]

假定隐含层长度为\(h\),其中的\(\mathbf{W}_{xh} \in \mathbb{R}^{x \times h}\)是权重参数。偏移参数 \(\mathbf{b}_h \in \mathbb{R}^{1 \times h}\)在与前一项\(\mathbf{X} \mathbf{W}_{xh} \in \mathbb{R}^{n \times h}\) 相加时使用了广播。这个隐含层的输出的尺寸为\(\mathbf{H} \in \mathbb{R}^{n \times h}\)

把隐含层的输出\(\mathbf{H}\)作为输出层的输入,最终的输出

\[\hat{\mathbf{Y}} = \text{softmax}(\mathbf{H} \mathbf{W}_{hy} + \mathbf{b}_y)\]

假定每个样本对应的输出向量维度为\(y\),其中 \(\hat{\mathbf{Y}} \in \mathbb{R}^{n \times y}, \mathbf{W}_{hy} \in \mathbb{R}^{h \times y}, \mathbf{b}_y \in \mathbb{R}^{1 \times y}\)且两项相加使用了广播

将上面网络改成循环神经网络,我们首先对输入输出加上时间戳\(t\)。假设\(\mathbf{X}_t \in \mathbb{R}^{n \times x}\)是序列中的第\(t\)个批量输入(样本数为\(n\),每个样本的特征向量维度为\(x\)),对应的隐含层输出是隐含状态\(\mathbf{H}_t \in \mathbb{R}^{n \times h}\)(隐含层长度为\(h\)),而对应的最终输出是\(\hat{\mathbf{Y}}_t \in \mathbb{R}^{n \times y}\)(每个样本对应的输出向量维度为\(y\))。在计算隐含层的输出的时候,循环神经网络只需要在前馈神经网络基础上加上跟前一时间\(t-1\)输入隐含层\(\mathbf{H}_{t-1} \in \mathbb{R}^{n \times h}\)的加权和。为此,我们引入一个新的可学习的权重\(\mathbf{W}_{hh} \in \mathbb{R}^{h \times h}\)

\[\mathbf{H}_t = \phi(\mathbf{X}_t \mathbf{W}_{xh} + \mathbf{H}_{t-1} \mathbf{W}_{hh} + \mathbf{b}_h)\]

输出的计算跟前面一致:

\[\hat{\mathbf{Y}}_t = \text{softmax}(\mathbf{H}_t \mathbf{W}_{hy} + \mathbf{b}_y)\]

一开始我们提到过,隐含状态可以认为是这个网络的记忆。该网络中,时刻\(t\)的隐含状态就是该时刻的隐含层变量\(\mathbf{H}_t\)。它存储前面时间里面的信息。我们的输出是只基于这个状态。最开始的隐含状态里的元素通常会被初始化为0。

周杰伦歌词数据集

为了实现并展示循环神经网络,我们使用周杰伦歌词数据集来训练模型作词。该数据集里包含了著名创作型歌手周杰伦从第一张专辑《Jay》到第十张专辑《跨时代》所有歌曲的歌词。

下面我们读取这个数据并看看前面49个字符(char)是什么样的:

In [1]:
import zipfile
with zipfile.ZipFile('../data/jaychou_lyrics.txt.zip', 'r') as zin:
    zin.extractall('../data/')

with open('../data/jaychou_lyrics.txt') as f:
    corpus_chars = f.read()
print(corpus_chars[0:49])
想要有直升机
想要和你飞到宇宙去
想要和你融化在一起
融化在宇宙里
我每天每天每天在想想想想著你

我们看一下数据集里的字符数。

In [2]:
len(corpus_chars)
Out[2]:
64925

接着我们稍微处理下数据集。为了打印方便,我们把换行符替换成空格,然后截去后面一段使得接下来的训练会快一点。

In [3]:
corpus_chars = corpus_chars.replace('\n', ' ').replace('\r', ' ')
corpus_chars = corpus_chars[0:20000]

字符的数值表示

先把数据里面所有不同的字符拿出来做成一个字典:

In [4]:
idx_to_char = list(set(corpus_chars))
char_to_idx = dict([(char, i) for i, char in enumerate(idx_to_char)])

vocab_size = len(char_to_idx)

print('vocab size:', vocab_size)
vocab size: 1465

然后可以把每个字符转成从0开始的索引(index)来方便之后的使用。

In [5]:
corpus_indices = [char_to_idx[char] for char in corpus_chars]

sample = corpus_indices[:40]

print('chars: \n', ''.join([idx_to_char[idx] for idx in sample]))
print('\nindices: \n', sample)
chars:
 想要有直升机 想要和你飞到宇宙去 想要和你融化在一起 融化在宇宙里 我每天每天每

indices:
 [1447, 1280, 221, 272, 1103, 1118, 913, 1447, 1280, 753, 350, 240, 179, 716, 1302, 512, 913, 1447, 1280, 753, 350, 427, 1430, 1395, 1185, 933, 913, 427, 1430, 1395, 716, 1302, 177, 913, 670, 734, 50, 734, 50, 734]

时序数据的批量采样

同之前一样我们需要每次随机读取一些(batch_size个)样本和其对用的标号。这里的样本跟前面有点不一样,这里一个样本通常包含一系列连续的字符(前馈神经网络里可能每个字符作为一个样本)。

如果我们把序列长度(num_steps)设成5,那么一个可能的样本是“想要有直升”。其对应的标号仍然是长为5的序列,每个字符是对应的样本里字符的后面那个。例如前面样本的标号就是“要有直升机”。

随机批量采样

下面代码每次从数据里随机采样一个批量。

In [6]:
import random
from mxnet import nd

def data_iter_random(corpus_indices, batch_size, num_steps, ctx=None):
    # 减一是因为label的索引是相应data的索引加一
    num_examples = (len(corpus_indices) - 1) // num_steps
    epoch_size = num_examples // batch_size
    # 随机化样本
    example_indices = list(range(num_examples))
    random.shuffle(example_indices)

    # 返回num_steps个数据
    def _data(pos):
        return corpus_indices[pos: pos + num_steps]

    for i in range(epoch_size):
        # 每次读取batch_size个随机样本
        i = i * batch_size
        batch_indices = example_indices[i: i + batch_size]
        data = nd.array(
            [_data(j * num_steps) for j in batch_indices], ctx=ctx)
        label = nd.array(
            [_data(j * num_steps + 1) for j in batch_indices], ctx=ctx)
        yield data, label

为了便于理解时序数据上的随机批量采样,让我们输入一个从0到29的人工序列,看下读出来长什么样:

In [7]:
my_seq = list(range(30))

for data, label in data_iter_random(my_seq, batch_size=2, num_steps=3):
    print('data: ', data, '\nlabel:', label, '\n')
data:
[[ 3.  4.  5.]
 [ 6.  7.  8.]]
<NDArray 2x3 @cpu(0)>
label:
[[ 4.  5.  6.]
 [ 7.  8.  9.]]
<NDArray 2x3 @cpu(0)>

data:
[[ 21.  22.  23.]
 [  0.   1.   2.]]
<NDArray 2x3 @cpu(0)>
label:
[[ 22.  23.  24.]
 [  1.   2.   3.]]
<NDArray 2x3 @cpu(0)>

data:
[[  9.  10.  11.]
 [ 24.  25.  26.]]
<NDArray 2x3 @cpu(0)>
label:
[[ 10.  11.  12.]
 [ 25.  26.  27.]]
<NDArray 2x3 @cpu(0)>

data:
[[ 18.  19.  20.]
 [ 12.  13.  14.]]
<NDArray 2x3 @cpu(0)>
label:
[[ 19.  20.  21.]
 [ 13.  14.  15.]]
<NDArray 2x3 @cpu(0)>

由于各个采样在原始序列上的位置是随机的时序长度为num_steps的连续数据点,相邻的两个随机批量在原始序列上的位置不一定相毗邻。因此,在训练模型时,读取每个随机时序批量前需要重新初始化隐含状态。

相邻批量采样

除了对原序列做随机批量采样之外,我们还可以使相邻的两个随机批量在原始序列上的位置相毗邻。

In [8]:
def data_iter_consecutive(corpus_indices, batch_size, num_steps, ctx=None):
    corpus_indices = nd.array(corpus_indices, ctx=ctx)
    data_len = len(corpus_indices)
    batch_len = data_len // batch_size

    indices = corpus_indices[0: batch_size * batch_len].reshape((
        batch_size, batch_len))
    # 减一是因为label的索引是相应data的索引加一
    epoch_size = (batch_len - 1) // num_steps

    for i in range(epoch_size):
        i = i * num_steps
        data = indices[:, i: i + num_steps]
        label = indices[:, i + 1: i + num_steps + 1]
        yield data, label

相同地,为了便于理解时序数据上的相邻批量采样,让我们输入一个从0到29的人工序列,看下读出来长什么样:

In [9]:
my_seq = list(range(30))

for data, label in data_iter_consecutive(my_seq, batch_size=2, num_steps=3):
    print('data: ', data, '\nlabel:', label, '\n')
data:
[[  0.   1.   2.]
 [ 15.  16.  17.]]
<NDArray 2x3 @cpu(0)>
label:
[[  1.   2.   3.]
 [ 16.  17.  18.]]
<NDArray 2x3 @cpu(0)>

data:
[[  3.   4.   5.]
 [ 18.  19.  20.]]
<NDArray 2x3 @cpu(0)>
label:
[[  4.   5.   6.]
 [ 19.  20.  21.]]
<NDArray 2x3 @cpu(0)>

data:
[[  6.   7.   8.]
 [ 21.  22.  23.]]
<NDArray 2x3 @cpu(0)>
label:
[[  7.   8.   9.]
 [ 22.  23.  24.]]
<NDArray 2x3 @cpu(0)>

data:
[[  9.  10.  11.]
 [ 24.  25.  26.]]
<NDArray 2x3 @cpu(0)>
label:
[[ 10.  11.  12.]
 [ 25.  26.  27.]]
<NDArray 2x3 @cpu(0)>

由于各个采样在原始序列上的位置是毗邻的时序长度为num_steps的连续数据点,因此,使用相邻批量采样训练模型时,读取每个时序批量前,我们需要将该批量最开始的隐含状态设为上个批量最终输出的隐含状态。在同一个epoch中,隐含状态只需要在该epoch开始的时候初始化。

One-hot向量

注意到每个字符现在是用一个整数来表示,而输入进网络我们需要一个定长的向量。一个常用的办法是使用one-hot来将其表示成向量。也就是说,如果一个字符的整数值是\(i\), 那么我们创建一个全0的长为vocab_size的向量,并将其第\(i\)位设成1。该向量就是对原字符的one-hot向量。

In [10]:
nd.one_hot(nd.array([0, 2]), vocab_size)
Out[10]:

[[ 1.  0.  0. ...,  0.  0.  0.]
 [ 0.  0.  1. ...,  0.  0.  0.]]
<NDArray 2x1465 @cpu(0)>

记得前面我们每次得到的数据是一个batch_size * num_steps的批量。下面这个函数将其转换成num_steps个可以输入进网络的batch_size * vocab_size的矩阵。对于一个长度为num_steps的序列,每个批量输入\(\mathbf{X} \in \mathbb{R}^{n \times x}\),其中\(n=\) batch_size,而\(x=\)vocab_size(onehot编码向量维度)。

In [11]:
def get_inputs(data):
    return [nd.one_hot(X, vocab_size) for X in data.T]

inputs = get_inputs(data)

print('input length: ', len(inputs))
print('input[0] shape: ', inputs[0].shape)
input length:  3
input[0] shape:  (2, 1465)

初始化模型参数

对于序列中任意一个时间戳,一个字符的输入是维度为vocab_size的one-hot向量,对应输出是预测下一个时间戳为词典中任意字符的概率,因而该输出是维度为vocab_size的向量。

当序列中某一个时间戳的输入为一个样本数为batch_size(对应模型定义中的\(n\))的批量,每个时间戳上的输入和输出皆为尺寸batch_size * vocab_size(对应模型定义中的\(n \times x\))的矩阵。假设每个样本对应的隐含状态的长度为hidden_dim(对应模型定义中隐含层长度\(h\)),根据矩阵乘法定义,我们可以推断出模型隐含层和输出层中各个参数的尺寸。

In [12]:
import mxnet as mx

# 尝试使用GPU
import sys
sys.path.append('..')
import utils
ctx = utils.try_gpu()
print('Will use', ctx)

input_dim = vocab_size
# 隐含状态长度
hidden_dim = 256
output_dim = vocab_size
std = .01

def get_params():
    # 隐含层
    W_xh = nd.random_normal(scale=std, shape=(input_dim, hidden_dim), ctx=ctx)
    W_hh = nd.random_normal(scale=std, shape=(hidden_dim, hidden_dim), ctx=ctx)
    b_h = nd.zeros(hidden_dim, ctx=ctx)

    # 输出层
    W_hy = nd.random_normal(scale=std, shape=(hidden_dim, output_dim), ctx=ctx)
    b_y = nd.zeros(output_dim, ctx=ctx)

    params = [W_xh, W_hh, b_h, W_hy, b_y]
    for param in params:
        param.attach_grad()
    return params
Will use gpu(0)

定义模型

当序列中某一个时间戳的输入为一个样本数为batch_size的批量,而整个序列长度为num_steps时,以下rnn函数的inputsoutputs皆为num_steps 个尺寸为batch_size * vocab_size的矩阵,隐含变量\(\mathbf{H}\)是一个尺寸为batch_size * hidden_dim的矩阵。该隐含变量\(\mathbf{H}\)也是循环神经网络的隐含状态state

我们将前面的模型公式翻译成代码。这里的激活函数使用了按元素操作的双曲正切函数

\[\text{tanh}(x) = \frac{1 - e^{-2x}}{1 + e^{-2x}}\]

需要注意的是,双曲正切函数的值域是\([-1, 1]\)。如果自变量均匀分布在整个实域,该激活函数输出的均值为0。

In [13]:
def rnn(inputs, state, *params):
    # inputs: num_steps 个尺寸为 batch_size * vocab_size 矩阵。
    # H: 尺寸为 batch_size * hidden_dim 矩阵。
    # outputs: num_steps 个尺寸为 batch_size * vocab_size 矩阵。
    H = state
    W_xh, W_hh, b_h, W_hy, b_y = params
    outputs = []
    for X in inputs:
        H = nd.tanh(nd.dot(X, W_xh) + nd.dot(H, W_hh) + b_h)
        Y = nd.dot(H, W_hy) + b_y
        outputs.append(Y)
    return (outputs, H)

做个简单的测试:

In [14]:
state = nd.zeros(shape=(data.shape[0], hidden_dim), ctx=ctx)

params = get_params()
outputs, state_new = rnn(get_inputs(data.as_in_context(ctx)), state, *params)

print('output length: ',len(outputs))
print('output[0] shape: ', outputs[0].shape)
print('state shape: ', state_new.shape)
output length:  3
output[0] shape:  (2, 1465)
state shape:  (2, 256)

预测序列

在做预测时我们只需要给定时间0的输入和起始隐含变量。然后我们每次将上一个时间的输出作为下一个时间的输入。

In [15]:
def predict_rnn(rnn, prefix, num_chars, params, hidden_dim, ctx, idx_to_char,
                char_to_idx, get_inputs, is_lstm=False):
    # 预测以 prefix 开始的接下来的 num_chars 个字符。
    prefix = prefix.lower()
    state_h = nd.zeros(shape=(1, hidden_dim), ctx=ctx)
    if is_lstm:
        # 当RNN使用LSTM时才会用到,这里可以忽略。
        state_c = nd.zeros(shape=(1, hidden_dim), ctx=ctx)
    output = [char_to_idx[prefix[0]]]
    for i in range(num_chars + len(prefix)):
        X = nd.array([output[-1]], ctx=ctx)
        # 在序列中循环迭代隐含变量。
        if is_lstm:
            # 当RNN使用LSTM时才会用到,这里可以忽略。
            Y, state_h, state_c = rnn(get_inputs(X), state_h, state_c, *params)
        else:
            Y, state_h = rnn(get_inputs(X), state_h, *params)
        if i < len(prefix)-1:
            next_input = char_to_idx[prefix[i+1]]
        else:
            next_input = int(Y[0].argmax(axis=1).asscalar())
        output.append(next_input)
    return ''.join([idx_to_char[i] for i in output])

梯度剪裁

我们在正向传播和反向传播中提到, 训练神经网络往往需要依赖梯度计算的优化算法,例如我们之前介绍的随机梯度下降。 而在循环神经网络的训练中,当每个时序训练数据样本的时序长度num_steps较大或者时刻\(t\)较小,目标函数有关\(t\)时刻的隐含层变量梯度较容易出现衰减(valishing)或爆炸(explosion)。我们会在下一节详细介绍出现该现象的原因。

为了应对梯度爆炸,一个常用的做法是如果梯度特别大,那么就投影到一个比较小的尺度上。假设我们把所有梯度接成一个向量 \(\boldsymbol{g}\),假设剪裁的阈值是\(\theta\),那么我们这样剪裁使得\(\|\boldsymbol{g}\|\)不会超过\(\theta\)

\[\boldsymbol{g} = \min\left(\frac{\theta}{\|\boldsymbol{g}\|}, 1\right)\boldsymbol{g}\]
In [16]:
def grad_clipping(params, theta, ctx):
    if theta is not None:
        norm = nd.array([0.0], ctx)
        for p in params:
            norm += nd.sum(p.grad ** 2)
        norm = nd.sqrt(norm).asscalar()
        if norm > theta:
            for p in params:
                p.grad[:] *= theta / norm

训练模型

下面我们可以还是训练模型。跟前面前置网络的教程比,这里有以下几个不同。

  1. 通常我们使用困惑度(Perplexity)这个指标。
  2. 在更新前我们对梯度做剪裁。
  3. 在训练模型时,对时序数据采用不同批量采样方法将导致隐含变量初始化的不同。

困惑度(Perplexity)

回忆以下我们之前介绍的交叉熵损失函数。在语言模型中,该损失函数即被预测字符的对数似然平均值的相反数:

\[\text{loss} = -\frac{1}{N} \sum_{i=1}^N \log p_{\text{target}_i}\]

其中\(N\)是预测的字符总数,\(p_{\text{target}_i}\)是在第\(i\)个预测中真实的下个字符被预测的概率。

而这里的困惑度可以简单的认为就是对交叉熵做exp运算使得数值更好读。

为了解释困惑度的意义,我们先考虑一个完美结果:模型总是把真实的下个字符的概率预测为1。也就是说,对任意的\(i\)来说,\(p_{\text{target}_i} = 1\)。这种完美情况下,困惑度值为1。

我们再考虑一个基线结果:给定不重复的字符集合\(W\)及其字符总数\(|W|\),模型总是预测下个字符为集合\(W\)中任一字符的概率都相同。也就是说,对任意的\(i\)来说,\(p_{\text{target}_i} = 1/|W|\)。这种基线情况下,困惑度值为\(|W|\)

最后,我们可以考虑一个最坏结果:模型总是把真实的下个字符的概率预测为0。也就是说,对任意的\(i\)来说,\(p_{\text{target}_i} = 0\)。这种最坏情况下,困惑度值为正无穷。

任何一个有效模型的困惑度值必须小于预测集中元素的数量。在本例中,困惑度必须小于字典中的字符数\(|W|\)。如果一个模型可以取得较低的困惑度的值(更靠近1),通常情况下,该模型预测更加准确。

In [17]:
from mxnet import autograd
from mxnet import gluon
from math import exp

def train_and_predict_rnn(rnn, is_random_iter, epochs, num_steps, hidden_dim,
                          learning_rate, clipping_theta, batch_size,
                          pred_period, pred_len, seqs, get_params, get_inputs,
                          ctx, corpus_indices, idx_to_char, char_to_idx,
                          is_lstm=False):
    if is_random_iter:
        data_iter = data_iter_random
    else:
        data_iter = data_iter_consecutive
    params = get_params()

    softmax_cross_entropy = gluon.loss.SoftmaxCrossEntropyLoss()

    for e in range(1, epochs + 1):
        # 如使用相邻批量采样,在同一个epoch中,隐含变量只需要在该epoch开始的时候初始化。
        if not is_random_iter:
            state_h = nd.zeros(shape=(batch_size, hidden_dim), ctx=ctx)
            if is_lstm:
                # 当RNN使用LSTM时才会用到,这里可以忽略。
                state_c = nd.zeros(shape=(batch_size, hidden_dim), ctx=ctx)
        train_loss, num_examples = 0, 0
        for data, label in data_iter(corpus_indices, batch_size, num_steps,
                                     ctx):
            # 如使用随机批量采样,处理每个随机小批量前都需要初始化隐含变量。
            if is_random_iter:
                state_h = nd.zeros(shape=(batch_size, hidden_dim), ctx=ctx)
                if is_lstm:
                    # 当RNN使用LSTM时才会用到,这里可以忽略。
                    state_c = nd.zeros(shape=(batch_size, hidden_dim), ctx=ctx)
            with autograd.record():
                # outputs 尺寸:(batch_size, vocab_size)
                if is_lstm:
                    # 当RNN使用LSTM时才会用到,这里可以忽略。
                    outputs, state_h, state_c = rnn(get_inputs(data), state_h,
                                                    state_c, *params)
                else:
                    outputs, state_h = rnn(get_inputs(data), state_h, *params)
                # 设t_ib_j为i时间批量中的j元素:
                # label 尺寸:(batch_size * num_steps)
                # label = [t_0b_0, t_0b_1, ..., t_1b_0, t_1b_1, ..., ]
                label = label.T.reshape((-1,))
                # 拼接outputs,尺寸:(batch_size * num_steps, vocab_size)。
                outputs = nd.concat(*outputs, dim=0)
                # 经上述操作,outputs和label已对齐。
                loss = softmax_cross_entropy(outputs, label)
            loss.backward()

            grad_clipping(params, clipping_theta, ctx)
            utils.SGD(params, learning_rate)

            train_loss += nd.sum(loss).asscalar()
            num_examples += loss.size

        if e % pred_period == 0:
            print("Epoch %d. Perplexity %f" % (e,
                                               exp(train_loss/num_examples)))
            for seq in seqs:
                print(' - ', predict_rnn(rnn, seq, pred_len, params,
                      hidden_dim, ctx, idx_to_char, char_to_idx, get_inputs,
                      is_lstm))
            print()

以下定义模型参数和预测序列前缀。

In [18]:
epochs = 200
num_steps = 35
learning_rate = 0.1
batch_size = 32

softmax_cross_entropy = gluon.loss.SoftmaxCrossEntropyLoss()

seq1 = '分开'
seq2 = '不分开'
seq3 = '战争中部队'
seqs = [seq1, seq2, seq3]

我们先采用随机批量采样实验循环神经网络谱写歌词。我们假定谱写歌词的前缀分别为“分开”、“不分开”和“战争中部队”。

In [19]:
train_and_predict_rnn(rnn=rnn, is_random_iter=True, epochs=200, num_steps=35,
                      hidden_dim=hidden_dim, learning_rate=0.2,
                      clipping_theta=5, batch_size=32, pred_period=20,
                      pred_len=100, seqs=seqs, get_params=get_params,
                      get_inputs=get_inputs, ctx=ctx,
                      corpus_indices=corpus_indices, idx_to_char=idx_to_char,
                      char_to_idx=char_to_idx)
Epoch 20. Perplexity 217.379127
 -  分开的可爱 我不的你的可 我想的让我的我的可 我想的让我的我的可 我想的让我的我的可 我想的让我的我的可 我想的让我的我的可 我想的让我的我的可 我想的让我的我的可 我想的让我的我的可 我想的让我的我的可
 -  不分开的可我的可 我不的让我的我的可 我想的让我的我的可 我想的让我的我的可 我想的让我的我的可 我想的让我的我的可 我想的让我的我的可 我想的让我的我的可 我想的让我的我的可 我想的让我的我的可 我想的让我
 -  战争中部队 我不的你的可我的可 我不的让我的我的可 我想的让我的我的可 我想的让我的我的可 我想的让我的我的可 我想的让我的我的可 我想的让我的我的可 我想的让我的我的可 我想的让我的我的可 我想的让我的我的可

Epoch 40. Perplexity 86.985013
 -  分开 我想要再想 我不要 想不 我想再 想子 我想再 想子 我想再 想子 我想再 想子 我想再 想子 我想再 想子 我想再 想子 我想再 想子 我想再 想子 我想再 想子 我想再 想子 我想再 想子 我想再
 -  不分开 一直两 一颗四 一颗四 一颗四 一颗四 一颗四 一颗四 一颗四 一颗四 一颗四 一颗四 一颗四 一颗四 一颗四 一颗四 一颗四 一颗四 一颗四 一颗四 一颗四 一颗四 一颗四 一颗四 一颗四 一颗四
 -  战争中部队 我想能 想子我 别子的公式我 不知 我想再 爱你 停子 我想要 爱情 我想再 爱情 我想再 想子 我想再 想子 我想再 想子 我想再 想子 我想再 想子 我想再 想子 我想再 想子 我想再 想子 我想

Epoch 60. Perplexity 31.125739
 -  分开 我想想你的可面 让我们 半兽人 的灵魂 翻滚 停起 xi xー xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi
 -  不分开 我的世界 你只是 的表情 剩滚 我们全的灵写 我想你 你爱我 别你的人式我 不知 我想很久了我 说散 你想很久了吧 这时 我想很 为我 恨散 你只很 爱情 我想 你的表 有情 是我 恨i xㄚ xi
 -  战争中部队 等在泡间的风度 我说你的爱 有一种味道叫做家 他在泡的茶 有一种味道叫做家 他在泡的茶 有一种味道叫做家 他在泡的茶 有一种味道叫做家 他在泡的茶 有一种味道叫做家 他在泡的茶 有一种味道叫做家 他在

Epoch 80. Perplexity 13.781672
 -  分开 我想想你的微笑 让我们 半兽人 的灵魂 翻滚 停止古存 永指止 的表情 有一些人 我的是笑条 这什么( 我们是那条龙 后知后觉 我该好好生活 我知拳打你 我不要再想 我不 我不 我不要再想你 不知不觉
 -  不分开 我太 全是 但不知 想不到 一颗两颗三颗四颗 连成线一张一张ㄟ撕 将过去一张一张ㄟ撕 将日历一张一张ㄟ撕 这日历一张一张ㄟ撕 这日历一张一张ㄟ撕 这日历一张一张ㄟ撕 这日历一张一张ㄟ撕 这日历一张一张
 -  战争中部队 你也的黑 在小村的橱 有一种味道叫做家 爷爷泡的茶 有一种味道叫做家 他 泡着我的肩膀 你 古和你的棒膀 你 古和我 单膀 一起抢血 戒指止 的表情 有一些人 我的是笑条 这什么( 我们是那条龙 后知

Epoch 100. Perplexity 7.589913
 -  分开 我想会打着走 不知不觉 你已经离开我 不知不觉 我跟了这节奏 后知后觉 我该好好生活 我右拳打开了天 化身为龙 把山地心脏汹涌 不安跳动 全世界 的表情只剩下一种 回牵到荒去 我的世界将被摧毁 也许颓
 -  不分开 已经 失去再义 永指止尽的战争 让我们 半兽人 的灵魂 翻滚 对起古存 永指 我的表负 我不要说的我知道 别些的那一天 周地的假旧的 我想你已你离的天 别时准着风袭 我看着起国 我不着再想 我不要再想
 -  战争中部队的模戏酱样) 姥难道 睁覆盖 蜕变的公式我学不来 (难道这不是我要的天堂景象 沉沦假象 你只会感到更加沮丧) (难道这不是我要的天堂景象 沉沦假象 你只会感到更加沮丧) (难道这不是我要的天堂景象 沉沦

Epoch 120. Perplexity 4.942369
 -  分开不住日 不要说这样打对历不了 爷不到 龙不开 别让我的地球变暗 温街角的消防栓像的红色 我 泪不是 语沉默 娘子的是式我 不安 你的那久 你也是我会 我难不再想  你在那里里  失么我很离友咆 一只一步
 -  不分开 我在不知回忆 又你的客这天 周杰的假旧的 我想你陪你ㄟ的非 你怎么备风都 我 想和你看棒球 想这样没担忧 唱着歌 一直走 我想就这样牵着你的手不放开 爱可不能够永简单纯没有悲害 我 靠着我的肩膀 你
 -  战争中部队纵空落 爷爷泡的茶 有一种味道叫做家 爷法泡的茶 有一种味道叫做家 他往泡的茶 有一种味道叫做家 爷羽泡的茶 有一种味道叫做家 他满泡白发 喝感时觉还不差 陆羽泡的茶 听说名和利都不拿 爷爷泡的茶 有一

Epoch 140. Perplexity 3.726908
 -  分开不觉 你面怕这 过自出中 怎么己真 没人了空 没人下空 没人下痛 没人下痛 没人下痛 没人下痛 没人下痛 没人下痛 没人下痛 没人下痛 没人下痛 没人下痛 没人下痛 没人下痛 没人下痛 没人下痛 没人下
 -  不分开 现在已经看不到 铁盒的钥匙孔 透了光 看见它的口快 想抹记 瞎透开 的灵魂 单纯 对起残存在的神 用谦卑的身份 爬廊灯回 在谁螂外的过边 默默等待 娘子 一壶好酒 再平忆 的路上 时间变空 回始地人
 -  战争中部队下 放时的叹 有着配的 不要我 不来我有 思静的梦 就多裂纵 不要我碰 你想著一场 他没事雄 我有是那条龙 哼者过去 快使用双截棍 哼哼哈兮 快使用双截棍 哼哼哈兮 快使用双截棍 哼哼哈兮 快使用双截棍

Epoch 160. Perplexity 2.984804
 -  分开不会日 他 爱是简外我听的处 有你爱 一想就 别静的那味我学了来多太多 你前 我们再来ㄟ鱼 你去ㄟ逗 像谁边过的溪争 让默还待 娘子出心满静 一身正气 快使我很轻功 飞檐走壁 为人耿直不屈再一个个柳 这
 -  不分开 已经 失去意义 戒指在哭泣 静静躺在抽屉 它所拥有的只剩下直  沦灵儿有简都会扰安 你不到我去 有不懂我 说你是 分数开 娘子的公旧我学不来 我难不能好烂的香  说谢我很不要 我的你说我开不见 当我
 -  战争中部队水 它时上 想最依的完快就像龙卷风 离不开暴风圈来不及逃 那不安化所人去 还满在心武过我的愿 将不着你 我没好好想  我该不这日质 没就这 快 我的脚界 你不是我只得你可要我 不是你那手 你以不起道 没

Epoch 180. Perplexity 2.657006
 -  分开不觉 难的风 连给我抬起头 我留想起你都我 化散棍迷陪要不曾 放 没有了过的我面怕 我不到 龙不来 别满了 x纯 dou xi x拳 xi xi xi xi 是ー ㄇー xー li li xi 是i ㄇ
 -  不分开 已在已经看不到 我 你和开骑去给 不 再着来梦强子 我看着没心回开 化什么(客我用微笑来带过 我没有这种天份 包容你也接受他 不是担河一起白上满 想要 故边飞血着永恒 只对暴力忠 爷爷三 旧皮空 是属
 -  战争中部队纵 我该着你心碎没人帮你 眼难瓜这 是你在外演 白辛蜡烛 全家怕过去 白慢蜡习 温暖了空 恨自己在空URE 迷往待我看 半着我的摩托车 载你缓缓 连世界 的表情调剩下一空 火待到缝边物的母斑 嗜兰内林醒

Epoch 200. Perplexity 2.365872
 -  分开不会日 不放中手不是说你的泪不 我想以的讯写在西元前里深到 拜怕再大不能不到意时间 我想你这样的注争 说底里梦琴得 所果按逗对走 进攻着钢 像面跳动 O红有一种 等记英雄 我就是那条龙 我右拳打开了天
 -  不分开 我知 这不 我都 你的人界每天 想才声的消给 我不红 你才开 沉不开 别果 dou xi xi xi xi xi xー xo ㄇー la ㄇo 是谁 ㄇー xi ㄇo 是谁 ㄇー xi ㄇo 是谁 ㄇ
 -  战争中部队家 我们的天北人 那杰的假一天 我怎么看不见 消失的在一天 我怎么看不见 消失的下雨天 我好想再淋一遍 没想到龙 那大地心脏的坟墓 说著一口吴侬 不上半着我嘲出  小时你梦想堡咆了 那可太手边写 累就会

我们再采用相邻批量采样实验循环神经网络谱写歌词。

In [20]:
train_and_predict_rnn(rnn=rnn, is_random_iter=False, epochs=200, num_steps=35,
                      hidden_dim=hidden_dim, learning_rate=0.2,
                      clipping_theta=5, batch_size=32, pred_period=20,
                      pred_len=100, seqs=seqs, get_params=get_params,
                      get_inputs=get_inputs, ctx=ctx,
                      corpus_indices=corpus_indices, idx_to_char=idx_to_char,
                      char_to_idx=char_to_idx)
Epoch 20. Perplexity 216.472476
 -  分开 我不的我的 我不的你的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我
 -  不分开 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我
 -  战争中部队 我不的你的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我的 我不的我

Epoch 40. Perplexity 68.166988
 -  分开 你不要这样 我不要我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我
 -  不分开 我要你这样 我不要我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我
 -  战争中部队 我要你这想 我不要我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我想 我不能我

Epoch 60. Perplexity 21.669895
 -  分开 这不是我不要 说样走 一直我 说你 你很人 在抹 一直 xi ㄈ不 我不要我想能 这样走 一直我 别你 你很着 马抹 一直 xi ㄈ不 我不要我想能 这样走 一直我 的灵魂 单纯 对止忿恨在永 这是
 -  不分开你的手 我说你的爱写在西元前 深埋在美 不颗村外 在不了  不是那剩 我想要你已经 你是 你想再这气的 这是 你想是我的世 有样 我想再水ㄟ鱼 这是 你想离久气鱼 这是 你想再久气鱼 这是 你想离久气鱼
 -  战争中部队 我想想再你 我不要再想 我不要我想你 想什么的客快 然何 一阵ㄟ考 却水的钥 我不了这 我不要一个 我的想界 你我想始 你已了这 我不没这 我不着你 你不了 别不知 说想 你很忿 在抹 一直 xi ㄈ

Epoch 80. Perplexity 9.007901
 -  分开 这经在的爱情好像顶不住那时间 只是我然不起 不着我不见你是一场悲剧 我想一步是你就一个悲剧 我的你里不能 爱你我 见你的一场 单何的侧旧 三在什么走 不色横的爱情 让我想要睡离开 这样了顽固 这不是逃
 -  不分开就走 我不到你想开没人 别静的让我面娇 可你 没有你的我 那一种实现 你我的手爱 泡在什么走有到我 也你开笑爱你 我们多打分你的美桌  想上风木里很 但家为你太爱你 是因为我太爱你 是因为我太爱你 是因
 -  战争中部队着 我想眼你已碎没人帮你擦眼泪 别离开人边拥有你我的世界才能完美 我们你陪辈子的世 有一种味的泪度 请默谅我 全你的外婆 银一枝杨走 三色在的爱 好一种味道叫做家 他爷泡的茶 有一种味道叫做家 他满泡的

Epoch 100. Perplexity 5.064061
 -  分开 这里的没里 这在水依太 是底事什么 是你就不球 三乡中的爱情好像顶不住那时间 为什么这样子 你看着我说你已经看逃着你 想不到过去 试着让不感 是人事怕你走才快乐 没人帮着你走才快乐 我亲拳 为小来的太
 -  不分开就走  是你的眼爸 我有想很你走离我的想难你 全 那不简骑单!单! 爱~~~~~~~~~~~~好远 去不知美不来不要你一句 但小的天息  后悔着对不起 一定铜(客 一阵不剧 在回了外屋 白色蜡烛 温暖了
 -  战争中部队着的 想小一点的风面的天都 想著说有你 那学的距事 是你的侧脸倒在我的怀里 你慢慢睡去 我摇不再你 是我在战乡对决命堤 在你在一只是说法记 为啥一场默剧 让我想着你 漂亮的让我面红的可爱女人 温柔的让我

Epoch 120. Perplexity 3.469825
 -  分开 想要再这样打你 不多你 一起两 我想就这样牵着你 你要开 我怎一句 我 想和你看棒我 说这样的爱活 让何时 睡 我不的泪我 是有一起热人 他望着我说不睡 我想是你的微知每天都能看到  我知道这里很美但
 -  不分开就走  一个伊最坦 有一定人太得做 陆羽的我后么 周了么 我连开打在你 你这着很经都 你着你怎是难过我不想信 不着你 一颗两步三步四颗望著天 看星星 一颗两颗三颗四颗 连成线背著背默默许下心愿 看远方的
 -  战争中部队家 去什么一人一张日撕 为啥去一张一张ㄟ撕 将过去一张一张ㄟ撕 这过去 马撕到的太快 马蹄在 让人睡不安稳 我在等 灵魂序曲完成 带领族人写下祈开文 让我们在半是我 甩开为没有路事 我也能慢让你看 为什

Epoch 140. Perplexity 2.721817
 -  分开 这在在人后不水 陆领的父娘孔破晓我 没有了暗 铁盒的钥匙 有制茶在 装蟑螂蜘蛛在空了 (他下也三在许实 像 无底我弃么只单 我有拳 有给开 蜕满 是小记 都杰记 过是却存都难你 是你去 一场两颗三颗四
 -  不分开就走  是你的眼爸 我想要你不见 干留许的街叫 如烁在回忆默 它在着我会也睡着我 想不去 语沉默 娘变 是非u忍在永恒 只对枝力忠诚 在我们 半兽人 的灵魂 翻滚 停止忿恨 永无止尽的战争 让我们 半兽
 -  战争中部队着了 想著你大的风 从地无觉不到 已静着我 说你说 分数怎么停留 一直在停留 谁让过人留的 为什么我女朋你气铁好想望 我真的没有天份 安静的没这么快 我会学着放弃你 是因为我太爱你 是因为我太爱你 是因

Epoch 160. Perplexity 2.323887
 -  分开 这里的没后么这在堤 等小 一阵筐 木炭 你是 xi xi ㄇー xa xi xi la sa xa sa sa xa sa sa sa sa sa sa sa sa sa sa sa sa sa sa
 -  不分开就走 把手着你不再我 不来你在接汉多 不追担留味 我的世界将被摧毁 也许颓废也是 一不情气 快使用双截棍 哼哼哈兮 习武用双切棍 哼哼哈兮 如使用双截棍 哼哼哈兮 快使用双截棍 哼哼哈兮 快使用双截棍
 -  战争中部队生 放开方在念 在一种 如皮到间就快望 回想刚买的书 一本名叫半岛铁盒 放在床 边堆好多 第一页第六页第七页序 我说远 爱想不到陪我看这书的你会要走 不再是不再有 现在已经看不到 铁盒的钥匙孔 透了光

Epoch 180. Perplexity 2.125749
 -  分开了口离 闭着的父我已坠的可爱女人 温柔的让我心狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯
 -  不分开就走 把着你慢身子 心短透的 快著风种落棍 让生无斤的牛肉 我说店小二 三两银够不够 景色入秋 漫天黄沙凉过 塞北的客栈人 帅呆了我 全场盯人防守 篮下禁区游走 快攻抢篮板球 得分都靠我 你拿着球不投
 -  战争中部队着~ 被著 没想 ㄙ着 ㄈㄚ ㄇー 拿a笔写歌思唱我  为是笑吹是单的ㄟ指 嗜何到 什梭时间的画面的钟 从反方向开始移动 回到当初爱你的时空 停格内容不忠 所有回忆对着我进攻 我的伤口被你拆封 誓言太沉

Epoch 200. Perplexity 2.083253
 -  分开了天离着牵 爱他当最离开都一意个夏天的年少 我放下枪回忆去年一起毕业的学校 而眼泪一直都忘记一定毕业 全远方外的溪边 时后你心的画面 然后还原的人面 然安着原的人多 我也耍慢远离开 为什么还要我用微笑来
 -  不分开就走天 一定为人三的横 我说你那的模笑每天都天 你 你!简的快  你悔跟对不是你一天人生 我想在这样了没开始水的味戏 和那说情告是我 却一你 是你开一直悲一种 等待英雄 我就是那条龙 我右拳打开了天 化
 -  战争中部队着闭 想多下来的溪会河天 还再灵 融撕愿的太快 马蹄在 让人睡 我想就这样球着的味 就等到底哪里有野语 为啥咪铁支路直直 火车叨位去 为啥咪铁支路直直 火车叨位去 为啥咪铁支路直直 火车叨位去 为啥咪铁

可以看到一开始学到简单的字符,然后简单的词,接着是复杂点的词,然后看上去似乎像个句子了。

结论

  • 通过隐含状态,循环神经网络适合处理前后有依赖关系时序数据样本。
  • 对前后有依赖关系时序数据样本批量采样时,我们可以使用随机批量采样和相邻批量采样。
  • 循环神经网络较容易出现梯度衰减和爆炸。

练习

  • 调调参数(例如数据集大小、序列长度、隐含状态长度和学习率),看看对运行时间、perplexity和预测的结果造成的影响。
  • 在随机批量采样中,如果在同一个epoch中只把隐含变量在该epoch开始的时候初始化会怎么样?

吐槽和讨论欢迎点这里