循环神经网络 — 从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:
 [1011, 435, 573, 350, 239, 1458, 407, 1011, 435, 561, 466, 292, 216, 732, 496, 615, 407, 1011, 435, 561, 466, 430, 1390, 1262, 1198, 1290, 407, 430, 1390, 1262, 732, 496, 512, 407, 115, 689, 74, 689, 74, 689]

时序数据的批量采样

同之前一样我们需要每次随机读取一些(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:
[[  6.   7.   8.]
 [ 15.  16.  17.]]
<NDArray 2x3 @cpu(0)>
label:
[[  7.   8.   9.]
 [ 16.  17.  18.]]
<NDArray 2x3 @cpu(0)>

data:
[[ 12.  13.  14.]
 [  9.  10.  11.]]
<NDArray 2x3 @cpu(0)>
label:
[[ 13.  14.  15.]
 [ 10.  11.  12.]]
<NDArray 2x3 @cpu(0)>

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

data:
[[ 18.  19.  20.]
 [  3.   4.   5.]]
<NDArray 2x3 @cpu(0)>
label:
[[ 19.  20.  21.]
 [  4.   5.   6.]]
<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\)时刻的隐含层变量梯度较容易出现衰减(vanishing)或爆炸(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 228.879200
 -  分开 我不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让
 -  不分开 我不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让
 -  战争中部队 我不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让我的可 你不的让

Epoch 40. Perplexity 88.198696
 -  分开 我想要你的爱 有一种 你在那 一步四步三颗四 我想 你不 我不要 你人 我想 我不要 你人 我想 我不要 你人 我想 我不要 你人 我想 我不要 你人 我想 我不要 你人 我想 我不要 你人 我想 我
 -  不分开 我想想你 我已不知 我想了你 我已不知 我想了你 我已不知 我想了你 我已不知 我想了你 我已不知 我想了你 我已不知 我想了你 我已不知 我想了你 我已不知 我想了你 我已不知 我想了你 我已不知
 -  战争中部队 我想要你的爱度 我想要你的爱度 我想要你的爱 有一种 你在那 一步四步三颗四 我想 你不 我不要 你人 我想 我不要 你人 我想 我不要 你人 我想 我不要 你人 我想 我不要 你人 我想 我不要 你

Epoch 60. Perplexity 31.977544
 -  分开 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再
 -  不分开 我给不这样 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再
 -  战争中部队 在有你在眼 有在人在我 你的那美 我有多子条我 不知不觉 你已经感到我 不知道觉 我有经感到我 不知不觉 你已经感到我 不知不觉 你已经感到我 不知不觉 你已经感到我 不知不觉 你已经感到我 不知不觉

Epoch 80. Perplexity 13.541054
 -  分开 你在会有多我 不知  想开我 别水的人 想一定 你怎么人在江 马抹安 一步两步三步四步望著天 看星星 一颗两颗三步四步 连成线背著背默默许 心爷方 别梭默 的灵魂 翻滚 停止忿存 永谁止尽的战争 让我
 -  不分开 我给 这不 我不 我不要 爱情走的太快就像龙卷风 不能承受我的世手 换过被过 隔晨那 的日出 剩一些好柳 你在那里 在小村外的溪边 让我们 半兽人 的灵魂 翻滚 停止忿存 永谁止尽的战争 让我们 半兽
 -  战争中部队家 我想想再想 我不要再想 我不 我不 我不能再想你 不知不觉 你已经离开我 不知不觉 我已经这样龙 后知不觉 你已了好去 你的从空 我知不再生 他没有你在我有多 我不着你不碎 我 你要你看着我 不知道

Epoch 100. Perplexity 7.305153
 -  分开 你不会回想 我不要再想 我不 我不 我不要再想你 不知不觉 你已经离开我 不知不觉 我跟了这节奏 后知后觉 又过了一切秋 后知后觉 我该好好生活 我右拳打开了天 化身为龙 把山河直脏汹 仁者哈兮 快使
 -  不分开 我给的老里人份 我想学着你弃你 你已经远远分开 我也会慢慢走开 为什么我连分开 我想要你的微笑 天都能说没人 说我们 半兽人 的灵魂 翻滚 停止古存在永的 用有时不去 你在那人 在你是外的溪边河默默默
 -  战争中部队家 没有下天去不要开了 不要搁你的你都一天 我也一直好奏没讯息 我亲像一只蜂找没蜜 将过去一张一张ㄟ撕 将过去一张一张ㄟ撕 将过去一张一张ㄟ撕 将过去一张一张ㄟ撕 将过去一张一张ㄟ撕 将过去一张一张ㄟ撕

Epoch 120. Perplexity 4.688133
 -  分开 你不会回到我 是你走没不是说 把来你也接受他 我也要你想分开 为什么我要分开 我只要你想微开 我以会你的微笑每天都能看到  我知道这里很美 我才 你不 我不要再想 我不 我不 我不能再想你 不知不觉
 -  不分开 我给的手里人 我的想老主风 后知都觉 你已经离开我 不知不觉 我跟了这节奏 后知后觉 又过了一个秋 后知后觉 我该好好生活 我知拳打开了天 化身为龙 把山河重脏汹涌 不安跳动 全世界 的表情只剩下一种
 -  战争中部队家  爷待的想 像一种味道叫做家 他法泡的茶 有一种味道叫做家 他满泡的茶 有一种味道叫做家 他满泡的茶 有一种味道叫做家 他满泡的茶 有一种味道叫做家 他满泡的茶 有一种味道叫做家 他满泡的茶 有一种

Epoch 140. Perplexity 3.527028
 -  分开 你不会回想 一场的距丽 泡的完美主人 太彻想 爱你的那像语言 沉 不知道 一颗两颗三颗四颗 连成线背著背默默许下心愿 看远方的星如否听的见 它一定实现 它一个实现 娘子的真丽 你在完里会有黑色 ㄒ时
 -  不分开 我给一起好气 放 的真的让我将红的可爱女人 坏柔的让我疯狂的可爱女人 坏明的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人
 -  战争中部队会忆 看想有没只 我要一起热说  你带 你已人 别不该有太球 你看着我 说你有些犹豫 一直用 - 我不起球你说了 为说泡的茶 听幅泼墨利都不拿 他牵着一发瘦找在走天涯 爷爷的美我有坠入 看不见罪的国度

Epoch 160. Perplexity 2.943939
 -  分开 你要连全都龙没讯息 我的手界是你拆封 他对安一口风语动走 将真地的小呼啸像顶 是谁说没有 有一条热昏头的响尾蛇 无力盯着 隔壁里的橱窗还一把 等后的安以路演 牧草有没有 我马儿有些瘦 天事看头 满脸风
 -  不分开 爱经 失去酸义 永指止尽的战争 让我们 半兽人 的眼神 单纯 对远古存在的神 用谦卑的身份 再廊灯风琴 书将过 说弄 在否在 木iu恨在江面 好旧匠在的转现 这原谅抱的象征 没人能说的人负 我有店小二
 -  战争中部队想 一定画步 不雨搁动 一天方安 在一种一 都等后真 我再感 痛怎么我爱不难 我不是你的微笑每天都能看到  我知道这里很的太我 一切骑口 那蟑 是很人存ㄟ 你在笑 还是开着容! 你我带在你离开 这不好顽

Epoch 180. Perplexity 2.504954
 -  分开都天不想 穿你的回里间能 沉沦的象 沉口裂缝 恨不了 他对这种在口七的好叫模样) 被我再到你是谁 夜越黑 了当过 我也天慢慢分开 为什么我连分开都迁就着你 我真的没有天份 安静的没这么快 我会学着放弃你
 -  不分开 我给你的爱写在西元前 深埋在美索不达米亚平原 几十个世纪后出土发现 泥已风的消迹依然 不是我多爱不 你说开看你我很爱 当时不着它还么我 心我面了我面谁的语叫就样 说柔的让我疯狂的可爱女人 坏坏的让我疯
 -  战争中部队落 小爷依的茶 有一种味道叫做家 他满泡白发 喝茶时不准说直直 火车叨位去 为啥咪铁支路直直 火车叨位去 为啥咪铁支路直直 火车叨位去 为啥咪铁支路直直 火车叨位去 为啥咪铁支路直直 火车叨位去 为啥咪

Epoch 200. Perplexity 2.269832
 -  分开不觉 想回到有风 还一口能感 我们 失着 很ㄚ ㄇー xi la xi xi xi li xi xi xi li xi xi xi li xi xi xi li xi xi xi xi xi xi xi
 -  不分开 现在不知看不想 说头的钥空边破晓了了变廓 娘子我的最你就像顶不住那时间 为什么这样子 你看着我 说你有些犹豫 一直正气 你在完离 都人止安的过争 情默激待 一只是人现前 一身正气 你在完离 说慢出真的
 -  战争中部队落  说连不着你 单每我 感着我的天托 我经你 你却我 想 简!简!单!单! 爱~~~~~~~~~~ 想脸说干飞的模想能 泪不去 睁覆了 娘子的公式我学了太多太多 瞳惯 大 像东方 的日出调整了时空 回

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

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 218.694655
 -  分开 我不想 我不 我不想 我不的 我不是 我不是 我不 我不想 我不的 我不是 我不是 我不 我不想 我不的 我不是 我不是 我不 我不想 我不的 我不是 我不是 我不 我不想 我不的 我不是 我不是 我
 -  不分开 我不的 我不 我不想 我不的 我不是 我不是 我不 我不想 我不的 我不是 我不是 我不 我不想 我不的 我不是 我不是 我不 我不想 我不的 我不是 我不是 我不 我不想 我不的 我不是 我不是 我
 -  战争中部队 我不想 我不 我不想 我不的 我不是 我不是 我不 我不想 我不的 我不是 我不是 我不 我不想 我不的 我不是 我不是 我不 我不想 我不的 我不是 我不是 我不 我不想 我不的 我不是 我不是 我

Epoch 40. Perplexity 69.239700
 -  分开 我想要你的爱你 让我们的半兽 我们想 你爱我 想想 你不是 你不 这不 我不要 你不 我想 这不 我不 我不想 你不 我想 这不 我不 我不想 你不 我想 这不 我不 我不想 你不 我想 这不 我不
 -  不分开 我想的没有我 我们 你不是你 永水的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂
 -  战争中部队 我不要你不能 你知 我想是你 永水的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂

Epoch 60. Perplexity 22.240549
 -  分开 说着我的爱球依像 我想的你样天份 我想 你的表写 不要 停去的那旧  说在 对想 xi xi xㄚ xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi
 -  不分开 走我有没汉 我的想界你 我不能再想 我不能再想 我不 我不 我不要 爱情走的太快就像龙卷 深埋在美索 我们 我想 我不要 爱情我的想 有一种味道 不家一种走 这人在的爱 有一种味道 不拿你的爱 有一种
 -  战争中部队 我想你这样牵着你的手不放开 爱可不可以简 单知到的客不 像哼去 让人了一口 火有在位去 这不在的爱 有一种味道 不拿你的爱 有一种味道 不拿你的爱 有一种味道 不拿你的爱 有一种味道 不拿你的爱 有一

Epoch 80. Perplexity 9.576191
 -  分开了口难家 你在你的爱写在西元前 深埋在美索不达米亚平原 几楔形文字后下了永现 那不能 被覆盖的太快 像蹄到风去 不将 在我 不想 怎ㄚ ㄈㄚ ㄇー xㄚ xi xi xi xi xi xi xi xi
 -  不分开 走我的手不想 陪着了不看 你说没有你 不水帮弃 走暖了外 没有下空 没有已容 没有已空 没有已容 没有已空 没有已容 没有已空 没有已容 没有已空 没有已容 没有已空 没有已容 没有已空 没有已容 没
 -  战争中部队家 你在古这一步一种爬 在过一天 你的茶空 我很 却已的天后我 想想这不是我 选你是的话堡 如何到 让人常不口气 我在等 连魂后的完快 像蹄记风着 又的躺应 温蟑村背 有一种纵 不人的钥 我面会到条龙

Epoch 100. Perplexity 5.279215
 -  分开 这着我的爱球是 爱你的眼一么 我的你面想义 你就不着 你在经笑开我 不知不觉 我开经这节我 不知不觉 我跟了这节奏 后知后觉 又过了双截棍 哼哼哈兮 快使我有轻功 飞檐走壁 为人耿直不屈 一身正气 他
 -  不分开 走我的手我想 这着啊 分不是的太冷 还真记 让最常不停稳 我在等 灵魂序曲完气 不领到人里药 如着一起味像 他的铜板还太少 他不该够 你一没有了过到我 等我开着你前 我不能 分爱我 想不要 单纯 收止
 -  战争中部队色只 想回到老 隔壁的脚后 银制茶壶 装蟑螂空 在一种 马着你想生 一水我的权庄 让我连着你离开 这不是顽固 这不是逃避 没人帮著你走才快乐 没人帮着你走才了你 将暗你在 你一起 我想再到生 一场味步的

Epoch 120. Perplexity 3.527851
 -  分开 这着我看里你是一场悲剧 我想我这辈子注定一个人演戏 最后再一个人慢慢的回忆 没有了过去 我将要事抽离 如果我遇见你是一场悲剧 我想我这辈子注定一个人演戏 最后再一个人慢慢的回忆 没有了过去 我将要事抽
 -  不分开 走在一种我也无处可躲 我不要再想 我不要再想 我不 我不 我不能 爱情走的太快就像龙卷风 不能承受我已无处可躲 我不要再想 我不要再想 我不 我不 我不能 爱情走的太快就像龙卷风 不能承受我已无处可躲
 -  战争中部队 好牵小愿的画面 请在还在的转面 不会骑在我的伤手一口好被的我面的知道 就后开始不是 你色啊不多走你 无限个龙万棍  什么了对都的 干什么这女 我的世界将被摧毁 也许颓废也是 一种都叫 不使跳动 木炭下

Epoch 140. Perplexity 2.835408
 -  分开 这着我看见你是一场悲剧 我想得这样布着你的手不放开 爱可不可以简简单单没有悲害 你 靠着我的肩膀 你 在我胸口睡著 像这样的生活 我爱你 你爱我 想 简!简!单!单! 爱~~~~~~~~~~ 我想妈这
 -  不分开就走 把手没有交不着 但来你也接受 我不想起你走强 没身逐渐 把山地重新汹动 填平裂缝 将东方 的日情调剩下一种 等待英雄 我就是那条龙 我右拳打开了天 化身为龙 那大地心脏汹涌 不安跳动 全世界 的表
 -  战争中部队想 放日着你 我有多努别人怀奔丧) (难笑这不是我要的天堂景象 沉沦假象 你只会感到更加沮丧) (难道这不是我要的天堂景象 沉沦假象 你只会感到更加沮丧) (难道这不是我要的天堂景象 沉沦假象 你只会感

Epoch 160. Perplexity 2.327715
 -  分开 它着你的太托 如檐 却远再考日我 说散 你想很久了吧? 败给你的黑色幽默 说想 一直 ㄙo ㄈㄚ ㄇー 拿纸笔闹歌ㄟ 不留不传 你已经难到 (没有你在我有多难熬多烦熬) 没有你弃 我有多努力 (没有你
 -  不分开就走 把手慢慢交给的时言 白弦在你什么调要息 我亲像一只蜂找没蜜 我亲像一只蜂找没蜜 我亲像一只蜂找没蜜 我亲像一只蜂找没蜜 我亲像一只蜂找没蜜 我亲像一只蜂找没蜜 我亲像一只蜂找没蜜 我亲像一只蜂找没
 -  战争中部队身就你我妈想下 所点 这子表  它 是说很 都思 是面u xi l想 xou l着 xi xi xi xi 从ー xi la xi lㄚ ㄇー li ㄇー xi ㄇー x纸 ㄇー x纸 ㄇー x纸 ㄇー

Epoch 180. Perplexity 2.123651
 -  分开 这着我看不打痛 没有个人城堡 我有纯太国 查子的字茧 你不可不会有 为什么我 说你有外犹豫 篮时的真栈好要 牧草有没有 我马儿有些瘦 又你看着睡 我想过我的证据 让晶莹过的人 那多这甜安棍 哼哼哈兮
 -  不分开就走 把手着有交给 也不能好多 睡天都没有喝水也能活 脑袋瓜有一点秀逗 猎物死了它泪的民谣 和不到过不是 所有回被对着我进攻 我的伤口被你拆封 誓言太沉重泪被纵容 脸上汹涌失控 让我碰到你 漂亮的让我面
 -  战争中部队想 你在ㄟ全旁龙的香生 嗜何森林都来的早晨 任何侵略都来的可晨 任血侵略都来的可晨 任血侵略都来的可晨 任血侵略都来的可晨 任血侵略都来的可晨 任血侵略都来的可晨 任血侵略都来的可晨 任血侵略都来的可晨

Epoch 200. Perplexity 1.996561
 -  分开 这不该 还有真的冰冷 马南方大地重新开垦 不懂上一只如新的牛(J样了 Ch 这AB血型 从我的想里你决点难著 手不笑的爱情 像就检回 在一间 别过了 一子我依很是 想有伦对医药是说 别入 你ㄚ ㄙㄡ
 -  不分开就走 把手慢慢交给我 放下球中的困惑 不会骑好把的胖女巫 用拉丁文念咒语啦啦呜 她养的黑猫笑起来像哭 啦铁匠在 用水那球 心人的梦 我面下纵宠 我的天空 是过是过 说一只纵 你在的梦 全暗没纵 恨自作痛
 -  战争中部队想 你想 一只人单 回不心着的战争 让我们 半兽人 的灵魂 单纯 对远古存在的神 用谦卑的身份 泪廊灯 让人开的冰冷 往蹄到 让人睡不安稳 我在等 灵魂序曲完成 带领族人写下祈祷文 让我们 半兽人 的灵

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

结论

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

练习

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

吐槽和讨论欢迎点这里