《动手学深度学习》
Table Of Contents
《动手学深度学习》
Table Of Contents

循环神经网络的 Gluon 实现

本节将使用 Gluon 来实现基于循环神经网络的语言模型。首先,我们读取周杰伦专辑歌词数据集。

In [1]:
import gluonbook as gb
import math
from mxnet import autograd, gluon, init, nd
from mxnet.gluon import loss as gloss, nn, rnn
import time

(corpus_indices, char_to_idx, idx_to_char,
 vocab_size) = gb.load_data_jay_lyrics()

定义模型

Gluon 的rnn模块提供了循环神经网络的实现。下面构造一个单隐藏层、隐藏单元个数为 256 的循环神经网络层rnn_layer,并对权重做初始化。

In [2]:
num_hiddens = 256
rnn_layer = rnn.RNN(num_hiddens)
rnn_layer.initialize()

接下来调用rnn_layer的成员函数begin_state来返回初始化的隐藏状态列表。它有一个形状为(隐藏层个数,批量大小,隐藏单元个数)的元素。

In [3]:
batch_size = 2
state = rnn_layer.begin_state(batch_size=batch_size)
state[0].shape
Out[3]:
(1, 2, 256)

与上一节里实现的循环神经网络不同,这里rnn_layer的输入形状为(时间步数,批量大小,输入个数)。其中输入个数即 one-hot 向量长度(词典大小)。此外,rnn_layer作为 Gluon 的rnn.RNN实例,在前向计算后会分别返回输出和隐藏状态。其中的输出指的是隐藏层在各个时间步上计算并输出的隐藏状态,它们通常作为后续输出层的输入。需要强调的是,该“输出”本身并不涉及输出层计算,形状为(时间步数,批量大小,隐藏单元个数)。而rnn.RNN实例在前向计算返回的隐藏状态指的是隐藏层在最后时间步的可用于初始化下一时间步的隐藏状态:当隐藏层有多层时,每一层的隐藏状态都会记录在该变量中;对于像长短期记忆这样的循环神经网络,该变量还会包含其他信息。我们会在本章后面的小节介绍长短期记忆和深度循环神经网络。

In [4]:
num_steps = 35
X = nd.random.uniform(shape=(num_steps, batch_size, vocab_size))
Y, state_new = rnn_layer(X, state)
Y.shape, len(state_new), state_new[0].shape
Out[4]:
((35, 2, 256), 1, (1, 2, 256))

接下来我们继承 Block 类来定义一个完整的循环神经网络。它首先将输入数据使用 one-hot 向量表示后输入到rnn_layer中,然后使用全连接输出层得到输出。输出个数等于词典大小vocab_size

In [5]:
# 本类已保存在 gluonbook 包中方便以后使用。
class RNNModel(nn.Block):
    def __init__(self, rnn_layer, vocab_size, **kwargs):
        super(RNNModel, self).__init__(**kwargs)
        self.rnn = rnn_layer
        self.vocab_size = vocab_size
        self.dense = nn.Dense(vocab_size)

    def forward(self, inputs, state):
        # 将输入转置成(num_steps,batch_size)后获取 one-hot 向量表示。
        X = nd.one_hot(inputs.T, self.vocab_size)
        Y, state = self.rnn(X, state)
        # 全连接层会首先将 Y 的形状变成(num_steps * batch_size,num_hiddens),
        # 它的输出形状为(num_steps * batch_size,vocab_size)。
        output = self.dense(Y.reshape((-1, Y.shape[-1])))
        return output, state

    def begin_state(self, *args, **kwargs):
        return self.rnn.begin_state(*args, **kwargs)

模型训练

同前一节一样,以下定义了一个预测函数。这里的实现区别在于前向计算和初始化隐藏状态的函数接口。

In [6]:
# 本函数已保存在 gluonbook 包中方便以后使用。
def predict_rnn_gluon(prefix, num_chars, model, vocab_size, ctx, idx_to_char,
                      char_to_idx):
    # 使用 model 的成员函数来初始化隐藏状态。
    state = model.begin_state(batch_size=1, ctx=ctx)
    output = [char_to_idx[prefix[0]]]
    for t in range(num_chars + len(prefix) - 1):
        X = nd.array([output[-1]], ctx=ctx).reshape((1, 1))
        (Y, state) = model(X, state)  # 前向计算不需要传入模型参数。
        if t < len(prefix) - 1:
            output.append(char_to_idx[prefix[t + 1]])
        else:
            output.append(int(Y.argmax(axis=1).asscalar()))
    return ''.join([idx_to_char[i] for i in output])

让我们使用权重为随机值的模型来预测一次。

In [7]:
ctx = gb.try_gpu()
model = RNNModel(rnn_layer, vocab_size)
model.initialize(force_reinit=True, ctx=ctx)
predict_rnn_gluon('分开', 10, model, vocab_size, ctx, idx_to_char, char_to_idx)
Out[7]:
'分开练能别撒蝪拥大变钩抱'

接下来实现训练函数。它的算法同上一节一样,但这里只使用了相邻采样来读取数据。

In [8]:
# 本函数已保存在 gluonbook 包中方便以后使用。
def train_and_predict_rnn_gluon(model, num_hiddens, vocab_size, ctx,
                                corpus_indices, idx_to_char, char_to_idx,
                                num_epochs, num_steps, lr, clipping_theta,
                                batch_size, pred_period, pred_len, prefixes):
    loss = gloss.SoftmaxCrossEntropyLoss()
    model.initialize(ctx=ctx, force_reinit=True, init=init.Normal(0.01))
    trainer = gluon.Trainer(model.collect_params(), 'sgd',
                            {'learning_rate': lr, 'momentum': 0, 'wd': 0})

    for epoch in range(num_epochs):
        loss_sum, start = 0.0, time.time()
        data_iter = gb.data_iter_consecutive(
            corpus_indices, batch_size, num_steps, ctx)
        state = model.begin_state(batch_size=batch_size, ctx=ctx)
        for t, (X, Y) in enumerate(data_iter):
            for s in state:
                s.detach()
            with autograd.record():
                (output, state) = model(X, state)
                y = Y.T.reshape((-1,))
                l = loss(output, y).mean()
            l.backward()
            # 梯度裁剪。
            params = [p.data() for p in model.collect_params().values()]
            gb.grad_clipping(params, clipping_theta, ctx)
            trainer.step(1)  # 因为已经误差取过均值,梯度不用再做平均。
            loss_sum += l.asscalar()

        if (epoch + 1) % pred_period == 0:
            print('epoch %d, perplexity %f, time %.2f sec' % (
                epoch + 1, math.exp(loss_sum / (t + 1)), time.time() - start))
            for prefix in prefixes:
                print(' -', predict_rnn_gluon(
                    prefix, pred_len, model, vocab_size,
                    ctx, idx_to_char, char_to_idx))

使用和上一节实验中一样的超参数来训练模型。

In [9]:
num_epochs, batch_size, lr, clipping_theta = 200, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 50, 50, ['分开', '不分开']
train_and_predict_rnn_gluon(model, num_hiddens, vocab_size, ctx,
                            corpus_indices, idx_to_char, char_to_idx,
                            num_epochs, num_steps, lr, clipping_theta,
                            batch_size, pred_period, pred_len, prefixes)
epoch 50, perplexity 79.496973, time 0.17 sec
 - 分开 我不能再  我有你烦 我不了 我给我 想你的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让
 - 不分开 我想在你 在我的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我
epoch 100, perplexity 14.413494, time 0.17 sec
 - 分开 娘子 娘子 是后 我爱要你想 我不能我想 我不 我不 我不 我不 我不 我不 我不 我不 我不 我
 - 不分开 你在 有想  爱有你烦我有多难恼多 恼  的 我有多烦想  没有我遇见你是  这过伊 娘子的 娘时
epoch 150, perplexity 4.366292, time 0.17 sec
 - 分开 娘养她 一颗两颗 一根看 怎在我的脚画面 经过苏美 在小村外 没有没空 你想没空 恨我跟懂 说以没
 - 不分开 不要我不了 是不是用 我想要声是你 不作不觉 我想著这生奏 我该好好生活 不知不觉 你已经离开我
epoch 200, perplexity 2.317015, time 0.17 sec
 - 分开 你在的黑丽笑起来像哭 啦啦啦呜 用水晶球替人占卜 她是下午重点阳光  像 的生段 我的你乐家 我起
 - 不分开 你在轻我 全场盯人防守 篮下禁区游走 快攻抢篮板球 得分都我 快场盯人防守 篮下无区 我想一定生活

小结

  • Gluon 的rnn模块提供了循环神经网络层的实现。
  • Gluon 的rnn.RNN实例在前向计算后会分别返回输出和隐藏状态。该前向计算并不涉及输出层计算。

练习

  • 比较跟前一节的实现。看看 Gluon 的实现是不是运行速度更快?如果你觉得差别明显,试着找找原因。

扫码直达讨论区

image0