# 6.5 循環神經網絡的簡潔實現
本節將使用PyTorch來更簡潔地實現基于循環神經網絡的語言模型。首先,我們讀取周杰倫專輯歌詞數據集。
``` python
import time
import math
import numpy as np
import torch
from torch import nn, optim
import torch.nn.functional as F
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
(corpus_indices, char_to_idx, idx_to_char, vocab_size) = d2l.load_data_jay_lyrics()
```
## 6.5.1 定義模型
PyTorch中的`nn`模塊提供了循環神經網絡的實現。下面構造一個含單隱藏層、隱藏單元個數為256的循環神經網絡層`rnn_layer`。
``` python
num_hiddens = 256
# rnn_layer = nn.LSTM(input_size=vocab_size, hidden_size=num_hiddens) # 已測試
rnn_layer = nn.RNN(input_size=vocab_size, hidden_size=num_hiddens)
```
與上一節中實現的循環神經網絡不同,這里`rnn_layer`的輸入形狀為(時間步數, 批量大小, 輸入個數)。其中輸入個數即one-hot向量長度(詞典大小)。此外,`rnn_layer`作為`nn.RNN`實例,在前向計算后會分別返回輸出和隱藏狀態h,其中輸出指的是隱藏層在**各個時間步**上計算并輸出的隱藏狀態,它們通常作為后續輸出層的輸入。需要強調的是,該“輸出”本身并不涉及輸出層計算,形狀為(時間步數, 批量大小, 隱藏單元個數)。而`nn.RNN`實例在前向計算返回的隱藏狀態指的是隱藏層在**最后時間步**的隱藏狀態:當隱藏層有多層時,每一層的隱藏狀態都會記錄在該變量中;對于像長短期記憶(LSTM),隱藏狀態是一個元組(h, c),即hidden state和cell state。我們會在本章的后面介紹長短期記憶和深度循環神經網絡。關于循環神經網絡(以LSTM為例)的輸出,可以參考下圖([圖片來源](https://stackoverflow.com/questions/48302810/whats-the-difference-between-hidden-and-output-in-pytorch-lstm/48305882))。
<div align=center>
<img width="500" src="images/6.5.png"/>
</div>
<div align=center>循環神經網絡(以LSTM為例)的輸出</div>
來看看我們的例子,輸出形狀為(時間步數, 批量大小, 輸入個數),隱藏狀態h的形狀為(層數, 批量大小, 隱藏單元個數)。
``` python
num_steps = 35
batch_size = 2
state = None
X = torch.rand(num_steps, batch_size, vocab_size)
Y, state_new = rnn_layer(X, state)
print(Y.shape, len(state_new), state_new[0].shape)
```
輸出:
```
torch.Size([35, 2, 256]) 1 torch.Size([2, 256])
```
> 如果`rnn_layer`是`nn.LSTM`實例,那么上面的輸出是什么?
接下來我們繼承`Module`類來定義一個完整的循環神經網絡。它首先將輸入數據使用one-hot向量表示后輸入到`rnn_layer`中,然后使用全連接輸出層得到輸出。輸出個數等于詞典大小`vocab_size`。
``` python
# 本類已保存在d2lzh_pytorch包中方便以后使用
class RNNModel(nn.Module):
def __init__(self, rnn_layer, vocab_size):
super(RNNModel, self).__init__()
self.rnn = rnn_layer
self.hidden_size = rnn_layer.hidden_size * (2 if rnn_layer.bidirectional else 1)
self.vocab_size = vocab_size
self.dense = nn.Linear(self.hidden_size, vocab_size)
self.state = None
def forward(self, inputs, state): # inputs: (batch, seq_len)
# 獲取one-hot向量表示
X = d2l.to_onehot(inputs, self.vocab_size) # X是個list
Y, self.state = self.rnn(torch.stack(X), state)
# 全連接層會首先將Y的形狀變成(num_steps * batch_size, num_hiddens),它的輸出
# 形狀為(num_steps * batch_size, vocab_size)
output = self.dense(Y.view(-1, Y.shape[-1]))
return output, self.state
```
## 6.5.2 訓練模型
同上一節一樣,下面定義一個預測函數。這里的實現區別在于前向計算和初始化隱藏狀態的函數接口。
``` python
# 本函數已保存在d2lzh_pytorch包中方便以后使用
def predict_rnn_pytorch(prefix, num_chars, model, vocab_size, device, idx_to_char,
char_to_idx):
state = None
output = [char_to_idx[prefix[0]]] # output會記錄prefix加上輸出
for t in range(num_chars + len(prefix) - 1):
X = torch.tensor([output[-1]], device=device).view(1, 1)
if state is not None:
if isinstance(state, tuple): # LSTM, state:(h, c)
state = (state[0].to(device), state[1].to(device))
else:
state = state.to(device)
(Y, state) = model(X, state)
if t < len(prefix) - 1:
output.append(char_to_idx[prefix[t + 1]])
else:
output.append(int(Y.argmax(dim=1).item()))
return ''.join([idx_to_char[i] for i in output])
```
讓我們使用權重為隨機值的模型來預測一次。
``` python
model = RNNModel(rnn_layer, vocab_size).to(device)
predict_rnn_pytorch('分開', 10, model, vocab_size, device, idx_to_char, char_to_idx)
```
輸出:
```
'分開戲想暖迎涼想征涼征征'
```
接下來實現訓練函數。算法同上一節的一樣,但這里只使用了相鄰采樣來讀取數據。
``` python
# 本函數已保存在d2lzh_pytorch包中方便以后使用
def train_and_predict_rnn_pytorch(model, num_hiddens, vocab_size, device,
corpus_indices, idx_to_char, char_to_idx,
num_epochs, num_steps, lr, clipping_theta,
batch_size, pred_period, pred_len, prefixes):
loss = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
model.to(device)
state = None
for epoch in range(num_epochs):
l_sum, n, start = 0.0, 0, time.time()
data_iter = d2l.data_iter_consecutive(corpus_indices, batch_size, num_steps, device) # 相鄰采樣
for X, Y in data_iter:
if state is not None:
# 使用detach函數從計算圖分離隱藏狀態, 這是為了
# 使模型參數的梯度計算只依賴一次迭代讀取的小批量序列(防止梯度計算開銷太大)
if isinstance (state, tuple): # LSTM, state:(h, c)
state = (state[0].detach(), state[1].detach())
else:
state = state.detach()
(output, state) = model(X, state) # output: 形狀為(num_steps * batch_size, vocab_size)
# Y的形狀是(batch_size, num_steps),轉置后再變成長度為
# batch * num_steps 的向量,這樣跟輸出的行一一對應
y = torch.transpose(Y, 0, 1).contiguous().view(-1)
l = loss(output, y.long())
optimizer.zero_grad()
l.backward()
# 梯度裁剪
d2l.grad_clipping(model.parameters(), clipping_theta, device)
optimizer.step()
l_sum += l.item() * y.shape[0]
n += y.shape[0]
try:
perplexity = math.exp(l_sum / n)
except OverflowError:
perplexity = float('inf')
if (epoch + 1) % pred_period == 0:
print('epoch %d, perplexity %f, time %.2f sec' % (
epoch + 1, perplexity, time.time() - start))
for prefix in prefixes:
print(' -', predict_rnn_pytorch(
prefix, pred_len, model, vocab_size, device, idx_to_char,
char_to_idx))
```
使用和上一節實驗中一樣的超參數(除了學習率)來訓練模型。
```python
num_epochs, batch_size, lr, clipping_theta = 250, 32, 1e-3, 1e-2 # 注意這里的學習率設置
pred_period, pred_len, prefixes = 50, 50, ['分開', '不分開']
train_and_predict_rnn_pytorch(model, num_hiddens, vocab_size, device,
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 10.658418, time 0.05 sec
- 分開始我媽 想要你 我不多 讓我心到的 我媽媽 我不能再想 我不多再想 我不要再想 我不多再想 我不要
- 不分開 我想要你不你 我 你不要 讓我心到的 我媽人 可愛女人 壞壞的讓我瘋狂的可愛女人 壞壞的讓我瘋狂的
epoch 100, perplexity 1.308539, time 0.05 sec
- 分開不會痛 不要 你在黑色幽默 開始了美麗全臉的夢滴 閃爍成回憶 傷人的美麗 你的完美主義 太徹底 讓我
- 不分開不是我不要再想你 我不能這樣牽著你的手不放開 愛可不可以簡簡單單沒有傷害 你 靠著我的肩膀 你 在我
epoch 150, perplexity 1.070370, time 0.05 sec
- 分開不能去河南嵩山 學少林跟武當 快使用雙截棍 哼哼哈兮 快使用雙截棍 哼哼哈兮 習武之人切記 仁者無敵
- 不分開 在我會想通 是誰開沒有全有開始 他心今天 一切人看 我 一口令秋軟語的姑娘緩緩走過外灘 消失的 舊
epoch 200, perplexity 1.034663, time 0.05 sec
- 分開不能去嗎周杰倫 才離 沒要你在一場悲劇 我的完美主義 太徹底 分手的話像語言暴力 我已無能為力再提起
- 不分開 讓我面到你 愛情來的太快就像龍卷風 離不開暴風圈來不及逃 我不能再想 我不能再想 我不 我不 我不
epoch 250, perplexity 1.021437, time 0.05 sec
- 分開 我我外的家邊 你知道這 我愛不看的太 我想一個又重來不以 迷已文一只剩下回憶 讓我叫帶你 你你的
- 不分開 我我想想和 是你聽沒不 我不能不想 不知不覺 你已經離開我 不知不覺 我跟了這節奏 后知后覺
```
## 小結
* PyTorch的`nn`模塊提供了循環神經網絡層的實現。
* PyTorch的`nn.RNN`實例在前向計算后會分別返回輸出和隱藏狀態。該前向計算并不涉及輸出層計算。
-----------
> 注:除代碼外本節與原書此節基本相同,[原書傳送門](https://zh.d2l.ai/chapter_recurrent-neural-networks/rnn-gluon.html)
- Home
- Introduce
- 1.深度學習簡介
- 深度學習簡介
- 2.預備知識
- 2.1環境配置
- 2.2數據操作
- 2.3自動求梯度
- 3.深度學習基礎
- 3.1 線性回歸
- 3.2 線性回歸的從零開始實現
- 3.3 線性回歸的簡潔實現
- 3.4 softmax回歸
- 3.5 圖像分類數據集(Fashion-MINST)
- 3.6 softmax回歸的從零開始實現
- 3.7 softmax回歸的簡潔實現
- 3.8 多層感知機
- 3.9 多層感知機的從零開始實現
- 3.10 多層感知機的簡潔實現
- 3.11 模型選擇、反向傳播和計算圖
- 3.12 權重衰減
- 3.13 丟棄法
- 3.14 正向傳播、反向傳播和計算圖
- 3.15 數值穩定性和模型初始化
- 3.16 實戰kaggle比賽:房價預測
- 4 深度學習計算
- 4.1 模型構造
- 4.2 模型參數的訪問、初始化和共享
- 4.3 模型參數的延后初始化
- 4.4 自定義層
- 4.5 讀取和存儲
- 4.6 GPU計算
- 5 卷積神經網絡
- 5.1 二維卷積層
- 5.2 填充和步幅
- 5.3 多輸入通道和多輸出通道
- 5.4 池化層
- 5.5 卷積神經網絡(LeNet)
- 5.6 深度卷積神經網絡(AlexNet)
- 5.7 使用重復元素的網絡(VGG)
- 5.8 網絡中的網絡(NiN)
- 5.9 含并行連結的網絡(GoogLeNet)
- 5.10 批量歸一化
- 5.11 殘差網絡(ResNet)
- 5.12 稠密連接網絡(DenseNet)
- 6 循環神經網絡
- 6.1 語言模型
- 6.2 循環神經網絡
- 6.3 語言模型數據集(周杰倫專輯歌詞)
- 6.4 循環神經網絡的從零開始實現
- 6.5 循環神經網絡的簡單實現
- 6.6 通過時間反向傳播
- 6.7 門控循環單元(GRU)
- 6.8 長短期記憶(LSTM)
- 6.9 深度循環神經網絡
- 6.10 雙向循環神經網絡
- 7 優化算法
- 7.1 優化與深度學習
- 7.2 梯度下降和隨機梯度下降
- 7.3 小批量隨機梯度下降
- 7.4 動量法
- 7.5 AdaGrad算法
- 7.6 RMSProp算法
- 7.7 AdaDelta
- 7.8 Adam算法
- 8 計算性能
- 8.1 命令式和符號式混合編程
- 8.2 異步計算
- 8.3 自動并行計算
- 8.4 多GPU計算
- 9 計算機視覺
- 9.1 圖像增廣
- 9.2 微調
- 9.3 目標檢測和邊界框
- 9.4 錨框
- 10 自然語言處理
- 10.1 詞嵌入(word2vec)
- 10.2 近似訓練
- 10.3 word2vec實現
- 10.4 子詞嵌入(fastText)
- 10.5 全局向量的詞嵌入(Glove)
- 10.6 求近義詞和類比詞
- 10.7 文本情感分類:使用循環神經網絡
- 10.8 文本情感分類:使用卷積網絡
- 10.9 編碼器--解碼器(seq2seq)
- 10.10 束搜索
- 10.11 注意力機制
- 10.12 機器翻譯