{ "cells": [ { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "_vq8h-FOTHoX" }, "source": [ "# 门控循环单元(GRU)\n", "\n", "上一节介绍了循环神经网络中的梯度计算方法。我们发现,当时间步数较大或者时间步较小时,循环神经网络的梯度较容易出现衰减或爆炸。虽然裁剪梯度可以应对梯度爆炸,但无法解决梯度衰减的问题。通常由于这个原因,循环神经网络在实际中较难捕捉时间序列中时间步距离较大的依赖关系。\n", "\n", "门控循环神经网络(gated recurrent neural network)的提出,正是为了更好地捕捉时间序列中时间步距离较大的依赖关系。它通过可以学习的门来控制信息的流动。其中,门控循环单元(gated recurrent unit,GRU)是一种常用的门控循环神经网络 [1, 2]。另一种常用的门控循环神经网络则将在下一节中介绍。\n", "\n", "\n", "## 门控循环单元\n", "\n", "下面将介绍门控循环单元的设计。它引入了重置门(reset gate)和更新门(update gate)的概念,从而修改了循环神经网络中隐藏状态的计算方式。\n", "\n", "### 重置门和更新门\n", "\n", "如图6.4所示,门控循环单元中的重置门和更新门的输入均为当前时间步输入$\\boldsymbol{X}_t$与上一时间步隐藏状态$\\boldsymbol{H}_{t-1}$,输出由激活函数为sigmoid函数的全连接层计算得到。\n", "\n", "\n", "![门控循环单元中重置门和更新门的计算](../img/gru_1.svg)\n", "\n", "\n", "具体来说,假设隐藏单元个数为$h$,给定时间步$t$的小批量输入$\\boldsymbol{X}_t \\in \\mathbb{R}^{n \\times d}$(样本数为$n$,输入个数为$d$)和上一时间步隐藏状态$\\boldsymbol{H}_{t-1} \\in \\mathbb{R}^{n \\times h}$。重置门$\\boldsymbol{R}_t \\in \\mathbb{R}^{n \\times h}$和更新门$\\boldsymbol{Z}_t \\in \\mathbb{R}^{n \\times h}$的计算如下:\n", "\n", "$$\n", "\\begin{aligned}\n", "\\boldsymbol{R}_t = \\sigma(\\boldsymbol{X}_t \\boldsymbol{W}_{xr} + \\boldsymbol{H}_{t-1} \\boldsymbol{W}_{hr} + \\boldsymbol{b}_r),\\\\\n", "\\boldsymbol{Z}_t = \\sigma(\\boldsymbol{X}_t \\boldsymbol{W}_{xz} + \\boldsymbol{H}_{t-1} \\boldsymbol{W}_{hz} + \\boldsymbol{b}_z),\n", "\\end{aligned}\n", "$$\n", "\n", "其中$\\boldsymbol{W}_{xr}, \\boldsymbol{W}_{xz} \\in \\mathbb{R}^{d \\times h}$和$\\boldsymbol{W}_{hr}, \\boldsymbol{W}_{hz} \\in \\mathbb{R}^{h \\times h}$是权重参数,$\\boldsymbol{b}_r, \\boldsymbol{b}_z \\in \\mathbb{R}^{1 \\times h}$是偏差参数。[“多层感知机”](../chapter_deep-learning-basics/mlp.ipynb)一节中介绍过,sigmoid函数可以将元素的值变换到0和1之间。因此,重置门$\\boldsymbol{R}_t$和更新门$\\boldsymbol{Z}_t$中每个元素的值域都是$[0, 1]$。\n", "\n", "### 候选隐藏状态\n", "\n", "接下来,门控循环单元将计算候选隐藏状态来辅助稍后的隐藏状态计算。如图6.5所示,我们将当前时间步重置门的输出与上一时间步隐藏状态做按元素乘法(符号为$\\odot$)。如果重置门中元素值接近0,那么意味着重置对应隐藏状态元素为0,即丢弃上一时间步的隐藏状态。如果元素值接近1,那么表示保留上一时间步的隐藏状态。然后,将按元素乘法的结果与当前时间步的输入连结,再通过含激活函数tanh的全连接层计算出候选隐藏状态,其所有元素的值域为$[-1, 1]$。\n", "\n", "![门控循环单元中候选隐藏状态的计算。这里的$\\odot$是按元素乘法](../img/gru_2.svg)\n", "\n", "具体来说,时间步$t$的候选隐藏状态$\\tilde{\\boldsymbol{H}}_t \\in \\mathbb{R}^{n \\times h}$的计算为\n", "\n", "$$\\tilde{\\boldsymbol{H}}_t = \\text{tanh}(\\boldsymbol{X}_t \\boldsymbol{W}_{xh} + \\left(\\boldsymbol{R}_t \\odot \\boldsymbol{H}_{t-1}\\right) \\boldsymbol{W}_{hh} + \\boldsymbol{b}_h),$$\n", "\n", "其中$\\boldsymbol{W}_{xh} \\in \\mathbb{R}^{d \\times h}$和$\\boldsymbol{W}_{hh} \\in \\mathbb{R}^{h \\times h}$是权重参数,$\\boldsymbol{b}_h \\in \\mathbb{R}^{1 \\times h}$是偏差参数。从上面这个公式可以看出,重置门控制了上一时间步的隐藏状态如何流入当前时间步的候选隐藏状态。而上一时间步的隐藏状态可能包含了时间序列截至上一时间步的全部历史信息。因此,重置门可以用来丢弃与预测无关的历史信息。\n", "\n", "### 隐藏状态\n", "\n", "最后,时间步$t$的隐藏状态$\\boldsymbol{H}_t \\in \\mathbb{R}^{n \\times h}$的计算使用当前时间步的更新门$\\boldsymbol{Z}_t$来对上一时间步的隐藏状态$\\boldsymbol{H}_{t-1}$和当前时间步的候选隐藏状态$\\tilde{\\boldsymbol{H}}_t$做组合:\n", "\n", "$$\\boldsymbol{H}_t = \\boldsymbol{Z}_t \\odot \\boldsymbol{H}_{t-1} + (1 - \\boldsymbol{Z}_t) \\odot \\tilde{\\boldsymbol{H}}_t.$$\n", "\n", "\n", "![门控循环单元中隐藏状态的计算。这里的$\\odot$是按元素乘法](../img/gru_3.svg)\n", "\n", "\n", "值得注意的是,更新门可以控制隐藏状态应该如何被包含当前时间步信息的候选隐藏状态所更新,如图6.6所示。假设更新门在时间步$t'$到$t$($t' < t$)之间一直近似1。那么,在时间步$t'$到$t$之间的输入信息几乎没有流入时间步$t$的隐藏状态$\\boldsymbol{H}_t$。实际上,这可以看作是较早时刻的隐藏状态$\\boldsymbol{H}_{t'-1}$一直通过时间保存并传递至当前时间步$t$。这个设计可以应对循环神经网络中的梯度衰减问题,并更好地捕捉时间序列中时间步距离较大的依赖关系。\n", "\n", "我们对门控循环单元的设计稍作总结:\n", "\n", "* 重置门有助于捕捉时间序列里短期的依赖关系;\n", "* 更新门有助于捕捉时间序列里长期的依赖关系。\n", "\n", "## 读取数据集\n", "\n", "为了实现并展示门控循环单元,下面依然使用周杰伦歌词数据集来训练模型作词。这里除门控循环单元以外的实现已在[“循环神经网络”](rnn.ipynb)一节中介绍过。以下为读取数据集部分。" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "colab": {}, "colab_type": "code", "id": "7wZP7IdFTIe7" }, "outputs": [], "source": [ "import d2ltorch as d2lt\n", "import torch\n", "from torch import nn\n", "\n", "(corpus_indices, char_to_idx, idx_to_char,\n", " vocab_size) = d2lt.load_data_jay_lyrics()" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "z9vulEvjTNna" }, "source": [ "## 从零开始实现\n", "\n", "我们先介绍如何从零开始实现门控循环单元。\n", "\n", "### 初始化模型参数\n", "\n", "下面的代码对模型参数进行初始化。超参数`num_hiddens`定义了隐藏单元的个数。" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "colab": {}, "colab_type": "code", "id": "ShVlY2soTOU-" }, "outputs": [], "source": [ "num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size\n", "device = d2lt.try_gpu()\n", "\n", "def get_params():\n", " def _one(shape):\n", " return torch.normal(mean=torch.zeros(shape), std=0.01).to(device)\n", " \n", " def _three():\n", " return (_one((num_inputs, num_hiddens)),\n", " _one((num_hiddens, num_hiddens)),\n", " torch.zeros(num_hiddens).to(device))\n", " \n", " W_xz, W_hz, b_z = _three() # 更新门参数\n", " W_xr, W_hr, b_r = _three() # 重置门参数\n", " W_xh, W_hh, b_h = _three() # 候选隐藏状态参数\n", " # 输出层参数\n", " W_hq = _one((num_hiddens, num_outputs))\n", " b_q = torch.zeros(num_outputs).to(device)\n", " # 附上梯度\n", " params = [W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q]\n", " for param in params:\n", " param.requires_grad_()\n", " return params" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "6-yCo5JjTT4r" }, "source": [ "### 定义模型\n", "\n", "下面的代码定义隐藏状态初始化函数`init_gru_state`。同[“循环神经网络的从零开始实现”](rnn-scratch.ipynb)一节中定义的`init_rnn_state`函数一样,它返回由一个形状为(批量大小, 隐藏单元个数)的值为0的`tensor`组成的元组。" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "colab": {}, "colab_type": "code", "id": "8uzZqB3XTU4s" }, "outputs": [], "source": [ "def init_gru_state(batch_size, num_hiddens, device):\n", " return (torch.zeros(batch_size, num_hiddens).to(device), )" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "M85XAerUTZwg" }, "source": [ "下面根据门控循环单元的计算表达式定义模型。" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "colab": {}, "colab_type": "code", "id": "6mr2zsRiTa71" }, "outputs": [], "source": [ "def gru(inputs, state, params):\n", " W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q = params\n", " H, = state\n", " outputs = []\n", " for X in inputs:\n", " Z = torch.sigmoid(torch.mm(X, W_xz) + torch.mm(H, W_hz) + b_z)\n", " R = torch.sigmoid(torch.mm(X, W_xr) + torch.mm(H, W_hr) + b_r)\n", " H_tilda = torch.tanh(torch.mm(X, W_xh) + torch.mm(R * H, W_hh) + b_h)\n", " H = Z * H + (1 - Z) * H_tilda\n", " Y = torch.mm(H, W_hq) + b_q\n", " outputs.append(Y)\n", " return outputs, (H,)" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "zRO-E0e0TfCx" }, "source": [ "### 训练模型并创作歌词\n", "\n", "我们在训练模型时只使用相邻采样。设置好超参数后,我们将训练模型并根据前缀“分开”和“不分开”分别创作长度为50个字符的一段歌词。" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "colab": {}, "colab_type": "code", "id": "HGJ4xZ7bTgxH" }, "outputs": [], "source": [ "num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e2, 1e-2\n", "pred_period, pred_len, prefixes = 40, 50, ['分开', '不分开']" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "yFAT6_TYTjg-" }, "source": [ "我们每过40个迭代周期便根据当前训练的模型创作一段歌词。" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 253 }, "colab_type": "code", "id": "XN3QK8IuTla9", "outputId": "d0e71c0b-88a4-47ef-ebea-ff5075a0833a" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "epoch 40, perplexity 150.776099, time 0.40 sec\n", " - 分开 我不不 我不不 我不不 我不不 我不不 我不不 我不不 我不不 我不不 我不不 我不不 我不不 我\n", " - 不分开 我想你的让我 我想你 我不不 我不不 我不不 我不不 我不不 我不不 我不不 我不不 我不不 我不\n", "epoch 80, perplexity 33.364443, time 0.39 sec\n", " - 分开 我想要这样 我不要 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 \n", " - 不分开 我不要再想 我不要再想 我不再再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我\n", "epoch 120, perplexity 5.929063, time 0.41 sec\n", " - 分开我想要 你的黑笑 我有你烦恼活 我该好好生活 静静悄悄默默离开 我不能 整沉的黑色幽默 想是 说 我\n", " - 不分开 不知不觉 我不再再想 我不能再想 我不 我不 我不要再想你 不知 我想要的你笑 我想你这样经你在一\n", "epoch 160, perplexity 1.786226, time 0.38 sec\n", " - 分开 一直到 告诉我的证据 让晶莹的泪滴 闪烁成回忆 伤人的美丽 你的完美主义 太彻底 让我连恨都难以 \n", " - 不分开让 她知我开多过猜 但我叫你已经很久 想和你看棒很著 说和你 你笑我妈家汉 没有了其实我 甩散\n" ] } ], "source": [ "d2lt.train_and_predict_rnn(gru, get_params, init_gru_state, num_hiddens,\n", " vocab_size, device, corpus_indices, idx_to_char,\n", " char_to_idx, False, num_epochs, num_steps, lr,\n", " clipping_theta, batch_size, pred_period, pred_len,\n", " prefixes)" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "rLB2S4VXUOKY" }, "source": [ "## 简洁实现\n", "\n", "在PyTorch中我们直接调用`nn`模块中的`GRU`类即可。" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 253 }, "colab_type": "code", "id": "N_eEaCCzUQT6", "outputId": "895dc29c-a6e5-410b-b306-ed72ba361c5b" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "epoch 40, perplexity 7.726168, time 0.08 sec\n", " - 分开始乡相思寄红豆 在小村外的溪边河口默默默等著我 娘子依旧每日折一枝杨柳 在小村外的溪边河口默默默等著\n", " - 不分开始呼呼啸而过 印地安斑鸠 平常话不及逃 我不能再想 我不能再想 我不能再想 我不能再想 我不能再想 \n", "epoch 80, perplexity 1.385477, time 0.08 sec\n", " - 分开始 像能够翻力 我不能 爱你一埋在停留 谁让它停留的谁乐美索不想 想著你这这样的甜蜜 然后悔着对你依\n", " - 不分开始 可爱可以简单单伤狼狼藤藤蔓器最喜欢 双截棍柔中带刚 想要去河南嵩山 学少林跟武当 快使用双截棍 \n", "epoch 120, perplexity 1.090553, time 0.07 sec\n", " - 分开的话像 如果我遇见你是一场悲剧 我想我这辈子注定一个人演戏 最后再一个人慢慢的回忆 没有了过去 我\n", " - 不分开始 风 心中 我环大自然 小弄堂 是属于那年代白墙黑瓦的淡淡的忧伤 消失的 旧时光 一九四三 回头\n", "epoch 160, perplexity 1.061235, time 0.07 sec\n", " - 分开的话像语沉默 娘子她人在江南等我 泪不休 语沉默 一壶好酒 再来一碗热粥 配上几斤的牛肉 我说店小二\n", " - 不分开始共能拿反反驳到到整倦窝 它一定实现它一定实现 载著你 彷彿载著阳光 不管到哪里都是晴天 蝴蝶自在飞\n" ] } ], "source": [ "gru_layer = nn.GRU(num_inputs, num_hiddens)\n", "model = d2lt.RNNModel(gru_layer, num_hiddens, vocab_size)\n", "d2lt.train_and_predict_rnn_nn(model, num_hiddens, vocab_size, device,\n", " corpus_indices, idx_to_char, char_to_idx,\n", " num_epochs, num_steps, lr, clipping_theta,\n", " batch_size, pred_period, pred_len, prefixes)" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "SnIfmFrwVFwv" }, "source": [ "## 小结\n", "\n", "* 门控循环神经网络可以更好地捕捉时间序列中时间步距离较大的依赖关系。\n", "* 门控循环单元引入了门的概念,从而修改了循环神经网络中隐藏状态的计算方式。它包括重置门、更新门、候选隐藏状态和隐藏状态。\n", "* 重置门有助于捕捉时间序列里短期的依赖关系。\n", "* 更新门有助于捕捉时间序列里长期的依赖关系。\n", "\n", "\n", "## 练习\n", "\n", "* 假设时间步$t' < t$。如果只希望用时间步$t'$的输入来预测时间步$t$的输出,每个时间步的重置门和更新门的理想的值是多少?\n", "* 调节超参数,观察并分析对运行时间、困惑度以及创作歌词的结果造成的影响。\n", "* 在相同条件下,比较门控循环单元和不带门控的循环神经网络的运行时间。\n", "\n", "\n", "\n", "## 参考文献\n", "\n", "[1] Cho, K., Van Merriënboer, B., Bahdanau, D., & Bengio, Y. (2014). On the properties of neural machine translation: Encoder-decoder approaches. arXiv preprint arXiv:1409.1259.\n", "\n", "[2] Chung, J., Gulcehre, C., Cho, K., & Bengio, Y. (2014). Empirical evaluation of gated recurrent neural networks on sequence modeling. arXiv preprint arXiv:1412.3555.\n", "\n", "## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/4042)\n", "\n", "![](../img/qr_gru.svg)" ] } ], "metadata": { "accelerator": "GPU", "colab": { "collapsed_sections": [], "name": "rnn-nn.ipynb", "provenance": [], "version": "0.3.2" }, "kernelspec": { "display_name": "Python [conda env:pytorch]", "language": "python", "name": "conda-env-pytorch-py" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.9" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": true, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": false, "toc_position": {}, "toc_section_display": true, "toc_window_display": false } }, "nbformat": 4, "nbformat_minor": 4 }