6.4 循环神经网络的从零开始实现
读取周杰伦专辑歌词数据集:
1 | import time |
one-hot向量
假设词典中不同字符的数量为\(N\)(即词典大小vocab_size
),每个字符已经同一个从0到\(N-1\)的连续整数值索引一一对应。如果一个字符的索引是整数\(i\), 那么创建一个全0的长为\(N\)的向量,并将其位置为\(i\)的元素设成1。该向量就是对原字符的one-hot向量。
1 | def one_hot(x, n_class, dtype=torch.float32): |
每次采样的小批量的形状是(批量大小, 时间步数)。下面的函数将这样的小批量变换成数个可以输入进网络的形状为(批量大小, 词典大小)的矩阵,矩阵个数等于时间步数。也就是说,时间步\(t\)的输入为\(\boldsymbol{X}_t \in \mathbb{R}^{n \times d}\),其中\(n\)为批量大小,\(d\)为输入个数,即one-hot向量长度(词典大小)。
1 | def to_onehot(X, n_class): |
初始化模型参数
隐藏单元个数 num_hiddens
是一个超参数。
1 | num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size |
定义模型
根据循环神经网络的计算表达式实现该模型。首先定义init_rnn_state
函数来返回初始化的隐藏状态。它返回由一个形状为(批量大小, 隐藏单元个数)的值为0的NDArray
组成的元组。使用元组是为了更便于处理隐藏状态含有多个NDArray
的情况。
1 | def init_rnn_state(batch_size, num_hiddens, device): |
下面的rnn
函数定义了在一个时间步里如何计算隐藏状态和输出。这里的激活函数使用了tanh函数。
1 | def rnn(inputs, state, params): |
定义预测函数
以下函数基于前缀prefix
(含有数个字符的字符串)来预测接下来的num_chars
个字符。
1 | def predict_rnn(prefix, num_chars, rnn, params, init_rnn_state, |
裁剪梯度
循环神经网络中较容易出现梯度衰减或梯度爆炸。为了应对梯度爆炸,可以裁剪梯度(clip gradient)。假设把所有模型参数梯度的元素拼接成一个向量 \(\boldsymbol{g}\),并设裁剪的阈值是\(\theta\)。裁剪后的梯度
\[ \min\left(\frac{\theta}{\|\boldsymbol{g}\|}, 1\right)\boldsymbol{g} \]
的\(L_2\)范数不超过\(\theta\)。
如果梯度的范数 \(\|\boldsymbol{g}\|\) 小于或等于阈值 \(\theta\),则不需要裁剪,直接使用原始梯度 \(\boldsymbol{g}\)。
如果梯度的范数 \(\|\boldsymbol{g}\|\) 大于阈值 \(\theta\),则需要对梯度进行缩放,裁剪后的梯度为:
\[ \frac{\theta}{\|\boldsymbol{g}\|} \boldsymbol{g} \]
这相当于将梯度向量按比例缩小,使其范数变为 \(\theta\)。
1 | # 本函数已保存在d2lzh_pytorch包中方便以后使用 |
困惑度
通常使用困惑度(perplexity)来评价语言模型的好坏。困惑度是对交叉熵损失函数做指数运算后得到的值。特别地,
- 最佳情况下,模型总是把标签类别的概率预测为1,此时困惑度为1;
- 最坏情况下,模型总是把标签类别的概率预测为0,此时困惑度为正无穷;
- 基线情况下,模型总是预测所有类别的概率都相同,此时困惑度为类别个数。
显然,任何一个有效模型的困惑度必须小于类别个数。在本例中,困惑度必须小于词典大小vocab_size
。
定义模型训练函数
1 | def train_and_predict_rnn(rnn, get_params, init_rnn_state, num_hiddens, |
训练模型并创作歌词
设置模型超参数。将根据前缀“分开”和“不分开”分别创作长度为50个字符(不考虑前缀长度)的一段歌词。每过50个迭代周期便根据当前训练的模型创作一段歌词。
1 | num_epochs, num_steps, batch_size, lr, clipping_theta = 250, 35, 32, 1e2, 1e-2 |
下面采用随机采样训练模型并创作歌词。
1 | train_and_predict_rnn(rnn, get_params, init_rnn_state, num_hiddens, |
接下来采用相邻采样训练模型并创作歌词。
1 | train_and_predict_rnn(rnn, get_params, init_rnn_state, num_hiddens, |