# 6.4 循環神經網絡的從零開始實現
在本節中,我們將從零開始實現一個基于字符級循環神經網絡的語言模型,并在周杰倫專輯歌詞數據集上訓練一個模型來進行歌詞創作。首先,我們讀取周杰倫專輯歌詞數據集:
``` 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.4.1 one-hot向量
為了將詞表示成向量輸入到神經網絡,一個簡單的辦法是使用one-hot向量。假設詞典中不同字符的數量為`$ N $`(即詞典大小`vocab_size`),每個字符已經同一個從0到`$ N-1 $`的連續整數值索引一一對應。如果一個字符的索引是整數`$ i $`, 那么我們創建一個全0的長為`$ N $`的向量,并將其位置為`$ i $`的元素設成1。該向量就是對原字符的one-hot向量。下面分別展示了索引為0和2的one-hot向量,向量長度等于詞典大小。
> pytorch沒有自帶one-hot函數(新版好像有了),下面自己實現一個
``` python
def one_hot(x, n_class, dtype=torch.float32):
# X shape: (batch), output shape: (batch, n_class)
x = x.long()
res = torch.zeros(x.shape[0], n_class, dtype=dtype, device=x.device)
res.scatter_(1, x.view(-1, 1), 1)
return res
x = torch.tensor([0, 2])
one_hot(x, vocab_size)
```
我們每次采樣的小批量的形狀是(批量大小, 時間步數)。下面的函數將這樣的小批量變換成數個可以輸入進網絡的形狀為(批量大小, 詞典大小)的矩陣,矩陣個數等于時間步數。也就是說,時間步`$ t $`的輸入為`$ \boldsymbol{X}_t \in \mathbb{R}^{n \times d} $`,其中`$ n $`為批量大小,`$ d $`為輸入個數,即one-hot向量長度(詞典大小)。
``` python
# 本函數已保存在d2lzh_pytorch包中方便以后使用
def to_onehot(X, n_class):
# X shape: (batch, seq_len), output: seq_len elements of (batch, n_class)
return [one_hot(X[:, i], n_class) for i in range(X.shape[1])]
X = torch.arange(10).view(2, 5)
inputs = to_onehot(X, vocab_size)
print(len(inputs), inputs[0].shape)
```
輸出:
```
5 torch.Size([2, 1027])
```
## 6.4.2 初始化模型參數
接下來,我們初始化模型參數。隱藏單元個數 `num_hiddens`是一個超參數。
``` python
num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size
print('will use', device)
def get_params():
def _one(shape):
ts = torch.tensor(np.random.normal(0, 0.01, size=shape), device=device, dtype=torch.float32)
return torch.nn.Parameter(ts, requires_grad=True)
# 隱藏層參數
W_xh = _one((num_inputs, num_hiddens))
W_hh = _one((num_hiddens, num_hiddens))
b_h = torch.nn.Parameter(torch.zeros(num_hiddens, device=device, requires_grad=True))
# 輸出層參數
W_hq = _one((num_hiddens, num_outputs))
b_q = torch.nn.Parameter(torch.zeros(num_outputs, device=device, requires_grad=True))
return nn.ParameterList([W_xh, W_hh, b_h, W_hq, b_q])
```
## 6.4.3 定義模型
我們根據循環神經網絡的計算表達式實現該模型。首先定義`init_rnn_state`函數來返回初始化的隱藏狀態。它返回由一個形狀為(批量大小, 隱藏單元個數)的值為0的`NDArray`組成的元組。使用元組是為了更便于處理隱藏狀態含有多個`NDArray`的情況。
``` python
def init_rnn_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device), )
```
下面的`rnn`函數定義了在一個時間步里如何計算隱藏狀態和輸出。這里的激活函數使用了tanh函數。3.8節(多層感知機)中介紹過,當元素在實數域上均勻分布時,tanh函數值的均值為0。
``` python
def rnn(inputs, state, params):
# inputs和outputs皆為num_steps個形狀為(batch_size, vocab_size)的矩陣
W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
for X in inputs:
H = torch.tanh(torch.matmul(X, W_xh) + torch.matmul(H, W_hh) + b_h)
Y = torch.matmul(H, W_hq) + b_q
outputs.append(Y)
return outputs, (H,)
```
做個簡單的測試來觀察輸出結果的個數(時間步數),以及第一個時間步的輸出層輸出的形狀和隱藏狀態的形狀。
```python
state = init_rnn_state(X.shape[0], num_hiddens, device)
inputs = to_onehot(X.to(device), vocab_size)
params = get_params()
outputs, state_new = rnn(inputs, state, params)
print(len(outputs), outputs[0].shape, state_new[0].shape)
```
輸出:
```
5 torch.Size([2, 1027]) torch.Size([2, 256])
```
## 6.4.4 定義預測函數
以下函數基于前綴`prefix`(含有數個字符的字符串)來預測接下來的`num_chars`個字符。這個函數稍顯復雜,其中我們將循環神經單元`rnn`設置成了函數參數,這樣在后面小節介紹其他循環神經網絡時能重復使用這個函數。
``` python
# 本函數已保存在d2lzh_pytorch包中方便以后使用
def predict_rnn(prefix, num_chars, rnn, params, init_rnn_state,
num_hiddens, vocab_size, device, idx_to_char, char_to_idx):
state = init_rnn_state(1, num_hiddens, device)
output = [char_to_idx[prefix[0]]]
for t in range(num_chars + len(prefix) - 1):
# 將上一時間步的輸出作為當前時間步的輸入
X = to_onehot(torch.tensor([[output[-1]]], device=device), vocab_size)
# 計算輸出和更新隱藏狀態
(Y, state) = rnn(X, state, params)
# 下一個時間步的輸入是prefix里的字符或者當前的最佳預測字符
if t < len(prefix) - 1:
output.append(char_to_idx[prefix[t + 1]])
else:
output.append(int(Y[0].argmax(dim=1).item()))
return ''.join([idx_to_char[i] for i in output])
```
我們先測試一下`predict_rnn`函數。我們將根據前綴“分開”創作長度為10個字符(不考慮前綴長度)的一段歌詞。因為模型參數為隨機值,所以預測結果也是隨機的。
``` python
predict_rnn('分開', 10, rnn, params, init_rnn_state, num_hiddens, vocab_size,
device, idx_to_char, char_to_idx)
```
輸出:
```
'分開西圈緒升王凝瓜必客映'
```
## 6.4.5 裁剪梯度
循環神經網絡中較容易出現梯度衰減或梯度爆炸。我們會在6.6節(通過時間反向傳播)中解釋原因。為了應對梯度爆炸,我們可以裁剪梯度(clip gradient)。假設我們把所有模型參數梯度的元素拼接成一個向量 `$ \boldsymbol{g} $`,并設裁剪的閾值是`$ \theta $`。裁剪后的梯度
```[tex]
\min\left(\frac{\theta}{\|\boldsymbol{g}\|}, 1\right)\boldsymbol{g}
```
的`$ L_2 $`范數不超過`$ \theta $`。
``` python
# 本函數已保存在d2lzh_pytorch包中方便以后使用
def grad_clipping(params, theta, device):
norm = torch.tensor([0.0], device=device)
for param in params:
norm += (param.grad.data ** 2).sum()
norm = norm.sqrt().item()
if norm > theta:
for param in params:
param.grad.data *= (theta / norm)
```
## 6.4.6 困惑度
我們通常使用困惑度(perplexity)來評價語言模型的好壞。回憶一下3.4節(softmax回歸)中交叉熵損失函數的定義。困惑度是對交叉熵損失函數做指數運算后得到的值。特別地,
* 最佳情況下,模型總是把標簽類別的概率預測為1,此時困惑度為1;
* 最壞情況下,模型總是把標簽類別的概率預測為0,此時困惑度為正無窮;
* 基線情況下,模型總是預測所有類別的概率都相同,此時困惑度為類別個數。
顯然,任何一個有效模型的困惑度必須小于類別個數。在本例中,困惑度必須小于詞典大小`vocab_size`。
## 6.4.7 定義模型訓練函數
跟之前章節的模型訓練函數相比,這里的模型訓練函數有以下幾點不同:
1. 使用困惑度評價模型。
2. 在迭代模型參數前裁剪梯度。
3. 對時序數據采用不同采樣方法將導致隱藏狀態初始化的不同。相關討論可參考6.3節(語言模型數據集(周杰倫專輯歌詞))。
另外,考慮到后面將介紹的其他循環神經網絡,為了更通用,這里的函數實現更長一些。
``` python
# 本函數已保存在d2lzh_pytorch包中方便以后使用
def train_and_predict_rnn(rnn, get_params, init_rnn_state, num_hiddens,
vocab_size, device, corpus_indices, idx_to_char,
char_to_idx, is_random_iter, num_epochs, num_steps,
lr, clipping_theta, batch_size, pred_period,
pred_len, prefixes):
if is_random_iter:
data_iter_fn = d2l.data_iter_random
else:
data_iter_fn = d2l.data_iter_consecutive
params = get_params()
loss = nn.CrossEntropyLoss()
for epoch in range(num_epochs):
if not is_random_iter: # 如使用相鄰采樣,在epoch開始時初始化隱藏狀態
state = init_rnn_state(batch_size, num_hiddens, device)
l_sum, n, start = 0.0, 0, time.time()
data_iter = data_iter_fn(corpus_indices, batch_size, num_steps, device)
for X, Y in data_iter:
if is_random_iter: # 如使用隨機采樣,在每個小批量更新前初始化隱藏狀態
state = init_rnn_state(batch_size, num_hiddens, device)
else:
# 否則需要使用detach函數從計算圖分離隱藏狀態, 這是為了
# 使模型參數的梯度計算只依賴一次迭代讀取的小批量序列(防止梯度計算開銷太大)
for s in state:
s.detach_()
inputs = to_onehot(X, vocab_size)
# outputs有num_steps個形狀為(batch_size, vocab_size)的矩陣
(outputs, state) = rnn(inputs, state, params)
# 拼接之后形狀為(num_steps * batch_size, vocab_size)
outputs = torch.cat(outputs, dim=0)
# Y的形狀是(batch_size, num_steps),轉置后再變成長度為
# batch * num_steps 的向量,這樣跟輸出的行一一對應
y = torch.transpose(Y, 0, 1).contiguous().view(-1)
# 使用交叉熵損失計算平均分類誤差
l = loss(outputs, y.long())
# 梯度清0
if params[0].grad is not None:
for param in params:
param.grad.data.zero_()
l.backward()
grad_clipping(params, clipping_theta, device) # 裁剪梯度
d2l.sgd(params, lr, 1) # 因為誤差已經取過均值,梯度不用再做平均
l_sum += l.item() * y.shape[0]
n += y.shape[0]
if (epoch + 1) % pred_period == 0:
print('epoch %d, perplexity %f, time %.2f sec' % (
epoch + 1, math.exp(l_sum / n), time.time() - start))
for prefix in prefixes:
print(' -', predict_rnn(prefix, pred_len, rnn, params, init_rnn_state,
num_hiddens, vocab_size, device, idx_to_char, char_to_idx))
```
## 6.4.8 訓練模型并創作歌詞
現在我們可以訓練模型了。首先,設置模型超參數。我們將根據前綴“分開”和“不分開”分別創作長度為50個字符(不考慮前綴長度)的一段歌詞。我們每過50個迭代周期便根據當前訓練的模型創作一段歌詞。
``` python
num_epochs, num_steps, batch_size, lr, clipping_theta = 250, 35, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 50, 50, ['分開', '不分開']
```
下面采用隨機采樣訓練模型并創作歌詞。
``` python
train_and_predict_rnn(rnn, get_params, init_rnn_state, num_hiddens,
vocab_size, device, corpus_indices, idx_to_char,
char_to_idx, True, num_epochs, num_steps, lr,
clipping_theta, batch_size, pred_period, pred_len,
prefixes)
```
輸出:
```
epoch 50, perplexity 70.039647, time 0.11 sec
- 分開 我不要再想 我不能 想你的讓我 我的可 你怎么 一顆四 一顆四 我不要 一顆兩 一顆四 一顆四 我
- 不分開 我不要再 你你的外 在人 別你的讓我 狂的可 語人兩 我不要 一顆兩 一顆四 一顆四 我不要 一
epoch 100, perplexity 9.726828, time 0.12 sec
- 分開 一直的美棧人 一起看 我不要好生活 你知不覺 我已好好生活 我知道好生活 后知不覺 我跟了這生活
- 不分開堡 我不要再想 我不 我不 我不要再想你 不知不覺 你已經離開我 不知不覺 我跟了好生活 我知道好生
epoch 150, perplexity 2.864874, time 0.11 sec
- 分開 一只會停留 有不它元羞 這蝪什么奇怪的事都有 包括像貓的狗 印地安老斑鳩 平常話不多 除非是烏鴉搶
- 不分開掃 我不你再想 我不能再想 我不 我不 我不要再想你 不知不覺 你已經離開我 不知不覺 我跟了這節奏
epoch 200, perplexity 1.597790, time 0.11 sec
- 分開 有杰倫 干 載顆拳滿的讓空美空主 相愛還有個人 再狠狠忘記 你愛過我的證 有晶瑩的手滴 讓說些人
- 不分開掃 我叫你爸 你打我媽 這樣對嗎干嘛這樣 何必讓它牽鼻子走 瞎 說底牽打我媽要 難道球耳 快使用雙截
epoch 250, perplexity 1.303903, time 0.12 sec
- 分開 有杰人開留 仙唱它怕羞 蜥蝪橫著走 這里什么奇怪的事都有 包括像貓的狗 印地安老斑鳩 平常話不多
- 不分開簡 我不能再想 我不 我不 我不能 愛情走的太快就像龍卷風 不能承受我已無處可躲 我不要再想 我不能
```
接下來采用相鄰采樣訓練模型并創作歌詞。
``` python
train_and_predict_rnn(rnn, get_params, init_rnn_state, num_hiddens,
vocab_size, device, corpus_indices, idx_to_char,
char_to_idx, False, num_epochs, num_steps, lr,
clipping_theta, batch_size, pred_period, pred_len,
prefixes)
```
輸出:
```
epoch 50, perplexity 59.514416, time 0.11 sec
- 分開 我想要這 我想了空 我想了空 我想了空 我想了空 我想了空 我想了空 我想了空 我想了空 我想了空
- 不分開 我不要這 全使了雙 我想了這 我想了空 我想了空 我想了空 我想了空 我想了空 我想了空 我想了空
epoch 100, perplexity 6.801417, time 0.11 sec
- 分開 我說的這樣笑 想你都 不著我 我想就這樣牽 你你的回不笑多難的 它在云實 有一條事 全你了空
- 不分開覺 你已經離開我 不知不覺 我跟好這節活 我該好好生活 不知不覺 你跟了離開我 不知不覺 我跟好這節
epoch 150, perplexity 2.063730, time 0.16 sec
- 分開 我有到這樣牽著你的手不放開 愛可不可以簡簡單單沒有傷 古有你煩 我有多煩惱向 你知帶悄 回我的外
- 不分開覺 你已經很個我 不知不覺 我跟了這節奏 后知后覺 又過了一個秋 后哼哈兮 快使用雙截棍 哼哼哈兮
epoch 200, perplexity 1.300031, time 0.11 sec
- 分開 我想要這樣牽著你的手不放開 愛能不能夠永遠單甜沒有傷害 你 靠著我的肩膀 你 在我胸口睡著 像這樣
- 不分開覺 你已經離開我 不知不覺 我跟了這節奏 后知后覺 又過了一個秋 后知后覺 我該好好生活 我該好好生
epoch 250, perplexity 1.164455, time 0.11 sec
- 分開 我有一這樣布 對你依依不舍 連隔壁鄰居都猜到我現在的感受 河邊的風 在吹著頭發飄動 牽著你的手 一
- 不分開覺 你已經離開我 不知不覺 我跟了這節奏 后知后覺 又過了一個秋 后知后覺 我該好好生活 我該好好生
```
## 小結
* 可以用基于字符級循環神經網絡的語言模型來生成文本序列,例如創作歌詞。
* 當訓練循環神經網絡時,為了應對梯度爆炸,可以裁剪梯度。
* 困惑度是對交叉熵損失函數做指數運算后得到的值。
-----------
> 注:除代碼外本節與原書此節基本相同,[原書傳送門](https://zh.d2l.ai/chapter_recurrent-neural-networks/rnn-scratch.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 機器翻譯