【从零开始学习深度学习】34. Pytorch-RNN项目实战:RNN创作歌词案例--使用周杰伦专辑歌词训练模型并创作歌曲【含数据集与源码】
目錄
- RNN項目實戰使用周杰倫專輯歌詞訓練模型并創作歌曲
- 1.語言模型數據集預處理
- 1.1 讀取數據集
- 1.2 建立字符索引
- 1.3 時序數據的2種采樣方式
- 1.3.1 隨機采樣
- 1.3.2 相鄰采樣
- 小結
- 2. 從零實現循環神經網絡并進行訓練預測
- 2.1 one-hot向量表示
- 2.2 初始化模型參數
- 2.3 定義模型
- 2.4 定義預測函數
- 2.5 裁剪梯度
- 2.6 困惑度
- 2.7 定義模型訓練函數
- 2.8 訓練模型并創作歌詞
- 小結
- 3. 基于Pytorch-RNN進行訓練預測
- 3.1 定義模型
- 3.2 訓練模型
- 小結
RNN項目實戰使用周杰倫專輯歌詞訓練模型并創作歌曲
本文將介紹如何預處理一個語言模型數據集,并將其轉換成字符級循環神經網絡所需要的輸入格式。然后通過循環神經網絡RNN進行模型訓練,然后使用訓練好的模型創作歌曲。
語言模型數據集采用的是我最喜歡的歌手周杰倫第一張專輯《Jay》到第十張專輯《跨時代》中的所有歌詞,下面來開始我們的項目吧。
1.語言模型數據集預處理
1.1 讀取數據集
首先讀取這個周杰倫專輯歌詞的數據集,并顯示前100個字符。
import torch import random import zipfilewith zipfile.ZipFile('./RNN-JayZhou/jaychou_lyrics.txt.zip' as zin:with zin.open('jaychou_lyrics.txt') as f:corpus_chars = f.read().decode('utf-8') corpus_chars[:100]輸出:
'想要有直升機\n想要和你飛到宇宙去\n想要和你融化在一起\n融化在宇宙里\n我每天每天每天在想想想想著你\n這樣的甜蜜\n讓我開始鄉相信命運\n感謝地心引力\n讓我碰到你\n漂亮的讓我面紅的可愛女人\n溫柔的讓我心疼的可'這個數據集共有6萬多個字符。為了打印方便,我們把換行符替換成空格。為了節省模型計算時間,后續僅使用前1萬個字符來訓練模型。
corpus_chars = corpus_chars.replace('\n', ' ').replace('\r', ' ') corpus_chars = corpus_chars[0:10000] #取前10000個字符進行后續模型訓練1.2 建立字符索引
我們將每個字符映射成一個從0開始的連續整數,又稱索引,來方便之后的數據處理。為了得到索引,我們將數據集里所有不同字符取出來,然后將其逐一映射到索引來構造詞典。詞典中不同字符的個數vocab_size稱為詞典大小。
idx_to_char = list(set(corpus_chars)) char_to_idx = dict([(char, i) for i, char in enumerate(idx_to_char)]) vocab_size = len(char_to_idx) vocab_size # 1027之后,將訓練數據集中每個字符轉化為索引,并打印前20個字符及其對應的索引。
corpus_indices = [char_to_idx[char] for char in corpus_chars] sample = corpus_indices[:20] print('chars:', ''.join([idx_to_char[idx] for idx in sample])) print('indices:', sample)輸出:
chars: 想要有直升機 想要和你飛到宇宙去 想要和 indices: [250, 164, 576, 421, 674, 653, 357, 250, 164, 850, 217, 910, 1012, 261, 275, 366, 357, 250, 164, 850]我們定義一個函數load_data_jay_lyrics,返回corpus_indices【數據集字符對應的索引】、char_to_idx【詞典不同字符對應索引】、idx_to_char【詞典索引對應不同字符】和vocab_size【數據集不同字符數量】這4個變量。后續直接調用這個函數來獲取數據集對應的這4個變量。
def load_data_jay_lyrics(path):# path為數據集jaychou_lyrics.txt.zip路徑"""加載周杰倫歌詞數據集"""with zipfile.ZipFile(path) as zin:with zin.open('jaychou_lyrics.txt') as f:corpus_chars = f.read().decode('utf-8')corpus_chars = corpus_chars.replace('\n', ' ').replace('\r', ' ')corpus_chars = corpus_chars[0:10000]idx_to_char = list(set(corpus_chars))char_to_idx = dict([(char, i) for i, char in enumerate(idx_to_char)])vocab_size = len(char_to_idx)corpus_indices = [char_to_idx[char] for char in corpus_chars]return corpus_indices, char_to_idx, idx_to_char, vocab_size1.3 時序數據的2種采樣方式
在訓練中我們需要每次隨機讀取小批量樣本和標簽,時序數據的一個樣本通常包含連續的字符。假設時間步數為5,樣本序列為5個字符,即“想”“要”“有”“直”“升”。該樣本的標簽序列為這些字符分別在訓練集中的下一個字符,即“要”“有”“直”“升”“機”。表示如下:
樣本序列1:“想”“要”“有”“直”“升” ----> 標簽序列:“要”“有”“直”“升”“機”
對于時序數據采樣方式通常有兩種:隨機采樣和相鄰采樣。下面分別介紹這兩種采樣方式。
我們有兩種方式對時序數據進行采樣,分別是隨機采樣和相鄰采樣。
1.3.1 隨機采樣
下面的代碼每次從數據里隨機采樣一個小批量。其中批量大小batch_size指每個小批量的樣本數,num_steps為每個樣本所包含的時間步數。
在隨機采樣中,每個樣本是原始序列上任意截取的一段序列。相鄰的兩個隨機小批量在原始序列上的位置不一定相毗鄰。因此,我們無法用一個小批量最終時間步的隱藏狀態來初始化下一個小批量的隱藏狀態。在訓練模型時,每次隨機采樣前都需要重新初始化隱藏狀態。
讓我們輸入一個從0到29的連續整數的人工序列。設批量大小和時間步數分別為2和6。打印隨機采樣每次讀取的小批量樣本的輸入X和標簽Y。可見,相鄰的兩個隨機小批量在原始序列上的位置不一定相毗鄰。
my_seq = list(range(30)) for X, Y in data_iter_random(my_seq, batch_size=2, num_steps=6):print('X: ', X, '\nY:', Y, '\n')輸出:
X: tensor([[18., 19., 20., 21., 22., 23.],[12., 13., 14., 15., 16., 17.]]) Y: tensor([[19., 20., 21., 22., 23., 24.],[13., 14., 15., 16., 17., 18.]]) X: tensor([[ 0., 1., 2., 3., 4., 5.],[ 6., 7., 8., 9., 10., 11.]]) Y: tensor([[ 1., 2., 3., 4., 5., 6.],[ 7., 8., 9., 10., 11., 12.]])1.3.2 相鄰采樣
相鄰采樣指相鄰的兩個隨機小批量在原始序列上的位置相毗鄰。這時候,我們就可以用一個小批量最終時間步的隱藏狀態來初始化下一個小批量的隱藏狀態,從而使下一個小批量的輸出也取決于當前小批量的輸入,并如此循環下去。
這種方式對實現循環神經網絡有兩方面影響:
一方面,在訓練模型時,我們只需在每一個迭代周期開始時初始化隱藏狀態;
另一方面,當多個相鄰小批量通過傳遞隱藏狀態串聯起來時,模型參數的梯度計算將依賴所有串聯起來的小批量序列。同一迭代周期中,隨著迭代次數的增加,梯度的計算開銷會越來越大。
為了使模型參數的梯度計算只依賴一次迭代讀取的小批量序列,我們可以在每次讀取小批量前將隱藏狀態從計算圖中分離出來。
同樣的設置下,打印相鄰采樣每次讀取的小批量樣本的輸入X和標簽Y。相鄰的兩個隨機小批量在原始序列上的位置相毗鄰。
for X, Y in data_iter_consecutive(my_seq, batch_size=2, num_steps=6):print('X: ', X, '\nY:', Y, '\n')輸出:
X: tensor([[ 0., 1., 2., 3., 4., 5.],[15., 16., 17., 18., 19., 20.]]) Y: tensor([[ 1., 2., 3., 4., 5., 6.],[16., 17., 18., 19., 20., 21.]]) X: tensor([[ 6., 7., 8., 9., 10., 11.],[21., 22., 23., 24., 25., 26.]]) Y: tensor([[ 7., 8., 9., 10., 11., 12.],[22., 23., 24., 25., 26., 27.]])小結
- 時序數據采樣方式包括隨機采樣和相鄰采樣。使用這兩種方式的循環神經網絡訓練在實現上略有不同。
2. 從零實現循環神經網絡并進行訓練預測
在本節中,我們將從零開始實現一個基于字符級循環神經網絡的語言模型,并在周杰倫專輯歌詞數據集上訓練一個模型來進行歌詞創作。首先,我們讀取周杰倫專輯歌詞數據集:
import time import math import numpy as np import torch from torch import nn, optim import torch.nn.functional as Fimport sys 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) = load_data_jay_lyrics('./RNN-JayZhou/jaychou_lyrics.txt.zip')2.1 one-hot向量表示
為了將詞表示成向量輸入到神經網絡,一個簡單的辦法是使用one-hot向量。假設詞典中不同字符的數量為 N N N(即詞典大小vocab_size),每個字符已經同一個從0到 N ? 1 N-1 N?1的連續整數值索引一一對應。如果一個字符的索引是整數 i i i, 那么我們創建一個全0的長為 N N N的向量,并將其位置為 i i i的元素設成1。該向量就是對原字符的one-hot向量表示。
我們每次采樣的小批量的形狀是(批量大小, 時間步數)。下面的函數to_onehot將這樣的小批量變換成數個可以輸入進網絡的形狀為(批量大小, 詞典大小)的矩陣,矩陣個數等于時間步數。也就是說,時間步 t t t的輸入為 X t ∈ R n × d \boldsymbol{X}_t \in \mathbb{R}^{n \times d} Xt?∈Rn×d,其中 n n n為批量大小, d d d為輸入個數,即one-hot向量長度(詞典大小)。
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 resdef 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])2.2 初始化模型參數
初始化模型參數,隱藏單元個數 num_hiddens是一個超參數。
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])2.3 定義模型
根據循環神經網絡的計算表達式實現該模型。首先定義init_rnn_state函數來返回初始化的隱藏狀態。它返回由一個形狀為(批量大小, 隱藏單元個數)的值為0的NDArray組成的元組。使用元組是為了更便于處理隱藏狀態含有多個NDArray的情況。
def init_rnn_state(batch_size, num_hiddens, device):# 返回初始化的隱藏狀態,形狀:(批量大小, 隱藏單元個數)return (torch.zeros((batch_size, num_hiddens), device=device), )下面的rnn函數定義了在一個時間步里如何計算隱藏狀態和輸出。這里的激活函數使用了tanh函數。當元素在實數域上均勻分布時,tanh函數值的均值為0。
def rnn(inputs, state, params):# inputs和outputs皆為num_steps個形狀為(batch_size, vocab_size)的矩陣# state為初始的隱藏狀態W_xh, W_hh, b_h, W_hq, b_q = paramsH, = stateoutputs = []for X in inputs:# 計算隱藏狀態HH = torch.tanh(torch.matmul(X, W_xh) + torch.matmul(H, W_hh) + b_h)# 計算輸出Y = torch.matmul(H, W_hq) + b_qoutputs.append(Y)return outputs, (H,)做個簡單的測試來觀察輸出結果的個數(時間步數),以及第一個時間步的輸出層輸出的形狀和隱藏狀態的形狀。
# state初始隱藏狀態,形狀:(批量大小, 隱藏單元個數) state = init_rnn_state(X.shape[0], num_hiddens, device) # 輸入one_hot形式 inputs = to_onehot(X.to(device), vocab_size) # 獲取各層參數 params = get_params() # 輸出:outputs,state_new新的隱藏狀態 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])2.4 定義預測函數
以下函數基于前綴prefix(含有數個字符的字符串)來預測接下來的num_chars個字符。
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個字符(不考慮前綴長度)的一段歌詞。因為模型參數為隨機值,所以預測結果也是隨機的。
predict_rnn('分開', 10, rnn, params, init_rnn_state, num_hiddens, vocab_size,device, idx_to_char, char_to_idx)輸出:
'分開西圈緒升王凝瓜必客映'2.5 裁剪梯度
循環神經網絡中較容易出現梯度衰減或梯度爆炸。為了應對梯度爆炸,可以裁剪梯度(clip gradient)。假設我們把所有模型參數梯度的元素拼接成一個向量 g \boldsymbol{g} g,并設裁剪的閾值是 θ \theta θ。裁剪后的梯度
min ? ( θ ∥ g ∥ , 1 ) g \min\left(\frac{\theta}{\|\boldsymbol{g}\|}, 1\right)\boldsymbol{g} min(∥g∥θ?,1)g
的 L 2 L_2 L2?范數不超過 θ \theta θ。
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)2.6 困惑度
我們通常使用困惑度(perplexity)來評價語言模型的好壞。困惑度是對交叉熵損失函數做指數運算后得到的值。特別地,
- 最佳情況下,模型總是把標簽類別的概率預測為1,此時困惑度為1;
- 最壞情況下,模型總是把標簽類別的概率預測為0,此時困惑度為正無窮;
- 基線情況下,模型總是預測所有類別的概率都相同,此時困惑度為類別個數。
顯然,任何一個有效模型的困惑度必須小于類別個數。在本例中,困惑度必須小于詞典大小vocab_size。
2.7 定義模型訓練函數
與之前CNN模型訓練函數相比,這里的模型訓練函數有以下幾點不同:
定義模型
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):```num_epochs:迭代的周期數num_steps:每個樣本所包含的時間步數batch_size:批大小pred_period:在本函數中用于控制pred_period個迭代周期打印一次預測結果pred_len:預測的字符數prefixes:預測前綴,依據該前綴進行預測```if is_random_iter:# 是否是隨機采樣還是相鄰采樣data_iter_fn = d2l.data_iter_randomelse:data_iter_fn = d2l.data_iter_consecutiveparams = get_params()loss = nn.CrossEntropyLoss()for epoch in range(num_epochs):# is_random_iter:True表示隨機采樣,False表示相鄰采樣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_size * num_steps 的向量,這樣跟輸出的行一一對應y = torch.transpose(Y, 0, 1).contiguous().view(-1)# 使用交叉熵損失計算平均分類誤差l = loss(outputs, y.long())# 梯度清0if 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))2.8 訓練模型并創作歌詞
現在我們可以訓練模型了。首先,設置模型超參數。我們將根據前綴“分開”和“不分開”分別創作長度為50個字符(不考慮前綴長度)的一段歌詞。我們每過50個迭代周期便根據當前訓練的模型創作一段歌詞。
num_epochs, num_steps, batch_size, lr, clipping_theta = 250, 35, 32, 1e2, 1e-2 pred_period, pred_len, prefixes = 50, 50, ['分開', '不分開']下面采用隨機采樣訓練模型并創作歌詞。
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 69.127998, time 1.30 sec- 分開 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我- 不分開 我想要你的你 一知哈覺 我已了這 你我不能 你怎么 別怪我 我不要 一沉我 一子就 一直我 一子 epoch 100, perplexity 10.106804, time 1.39 sec- 分開 一只用雙留 誰人它 一皮箱 一顆四有 在人憶 的片段 有 是對舊 快顆都顆 全小村空 在人海中 你- 不分開嗎 我后好這生 我不能再想 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 epoch 150, perplexity 2.871314, time 1.30 sec- 分開 一直在停留 誰讓它停留的 為什么我女朋友場外加油 你卻還讓我出糗 可小睡耳濡目染 什么刀槍跟棍棒 - 不分開嗎 我不能再想你 不知都覺 你已經離開我 不知不覺 我跟了這節奏 后知后覺 又過了雙截棍 哼哼哈兮 epoch 200, perplexity 1.543714, time 1.46 sec- 分開 有直在對醫 有思寄人牛 三里什么奇怪的事都有 包括像貓的狗 印地安老斑鳩 平常話不多 除非是烏鴉搶- 不分開掃 然后將過去 慢慢溫習 讓我愛上你 那場悲劇 是你完美演出的一場戲 寧愿心碎哭泣 再狠狠不記 你非 epoch 250, perplexity 1.298522, time 1.60 sec- 分開 有雙去不 你作常怎球出的手尾椅 用括像貓前年的誓言 紀錄又一只遇見的你 Jay Chou 如果- 不分開期把的胖女巫 用拉丁文念咒語啦啦嗚 她養的黑貓笑起來像哭 啦啦啦嗚 在時內懸 我不懂 我不走 裝壺就接下來采用相鄰采樣訓練模型并創作歌詞。
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.174035, time 1.30 sec- 分開 我想要這愛 我不要再不 我不要再不 我不要再想 我不要再不 我不要再想 我不要再不 我不要再想 我- 不分開你 你想我有你的讓我 你不我有你不著你的手不女 我愛你的愛寫 一哼哈兮 快使用雙截棍 一直哈兮 快使 epoch 100, perplexity 6.633354, time 1.35 sec- 分開 我說的這樣 你一在熱抽 仙蝪什么羞 仙人什么走 三人掌么走 三人什么走 三人什么走 三人什么走 三- 不分開柳 你是那過了 我兩就開 我有懂不么我有你的惱言 我有你再是 有人銀夠不夠 景色入秋 漫使用雙截棍 epoch 150, perplexity 1.969446, time 1.31 sec- 分開 我說的讓我每去 卻發現跟了當 快使用雙截棍 哼哼哈兮 快使用雙截棍 哼哼哈兮 快果我有輕功 飛檐走- 不分開覺 你已經過了我 不知不覺 我跟了這節奏 后知后覺 又過了一個秋 后知后覺 我該好好生活 我該好好生 epoch 200, perplexity 1.292166, time 1.29 sec- 分開 問候的人我 上的完美 我的完不面 但沒在美主 我被它拖煩 靜有悄煩快著 快使溫籃 說你得動防 - 不分開覺 你已經離開我 不知不覺 我跟了這節奏 后知后覺 又過了一個秋 后知后覺 我該好好生活 我該好好生 epoch 250, perplexity 1.181980, time 1.33 sec- 分開 問候的讓我面紅的可愛就人風榜你 可知前 語給兩步三步四步望著天 看星星 一顆兩顆三顆四顆 連成線背- 不分開覺 你已經離開我 不知不覺 我跟了這節奏 后知后覺 又過了一個秋 后知后覺 我該好好生活 我該好好生小結
- 可以用基于字符級循環神經網絡的語言模型來生成文本序列,例如創作歌詞。
- 當訓練循環神經網絡時,為了應對梯度爆炸,可以裁剪梯度。
- 困惑度是對交叉熵損失函數做指數運算后得到的值。
3. 基于Pytorch-RNN進行訓練預測
使用PyTorch可以更簡潔地實現基于循環神經網絡的語言模型。首先,我們讀取周杰倫專輯歌詞數據集。
import time import math import numpy as np import torch from torch import nn, optim import torch.nn.functional as Fimport sys 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) = load_data_jay_lyrics('./RNN-JayZhou/jaychou_lyrics.txt.zip')3.1 定義模型
PyTorch中的nn模塊提供了循環神經網絡的實現。下面構造一個含單隱藏層、隱藏單元個數為256的循環神經網絡層rnn_layer。
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為例)的輸出,可以參考下圖。
舉個例子:輸出形狀為(時間步數, 批量大小, 隱藏單元個數),隱藏狀態h的形狀為(層數, 批量大小, 隱藏單元個數)。
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])接下來我們繼承Module類來定義一個完整的循環神經網絡。它首先將輸入數據使用one-hot向量表示后輸入到rnn_layer中,然后使用全連接輸出層得到輸出。輸出個數等于詞典大小vocab_size。
class RNNModel(nn.Module):def __init__(self, rnn_layer, vocab_size):super(RNNModel, self).__init__()self.rnn = rnn_layer# rnn_layer.bidirectional,如果是雙向循環網絡則為2,單向則為1self.hidden_size = rnn_layer.hidden_size * (2 if rnn_layer.bidirectional else 1) self.vocab_size = vocab_sizeself.dense = nn.Linear(self.hidden_size, vocab_size)self.state = Nonedef forward(self, inputs, state): # inputs: (batch, seq_len)# 獲取one-hot向量表示X = d2l.to_onehot(inputs, self.vocab_size) # X是個listY, 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.state3.2 訓練模型
定義預測函數。這里的實現與第二節中的區別在于前向計算和初始化隱藏狀態的函數接口。
def predict_rnn_pytorch(prefix, num_chars, model, vocab_size, device, idx_to_char,char_to_idx):state = Noneoutput = [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])讓我們使用權重為隨機值的模型來預測一次。
model = RNNModel(rnn_layer, vocab_size).to(device) predict_rnn_pytorch('分開', 10, model, vocab_size, device, idx_to_char, char_to_idx)輸出:
'分開戲想暖迎涼想征涼征征'實現訓練函數,這里只使用相鄰采樣來讀取數據。
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 = Nonefor 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_size * num_steps 的向量,這樣跟輸出的行一一對應y = torch.transpose(Y, 0, 1).contiguous().view(-1)# y.long()表示向下取整 l = loss(output, y.long())'''y = torch.transpose(Y, 0, 1).contiguous().view(-1)作用Y = tensor([[ 7., 8., 9., 10., 11., 12.],[22., 23., 24., 25., 26., 27.]])則y = torch.transpose(Y, 0, 1).contiguous().view(-1)結果為:tensor([ 7., 22., 8., 23., 9., 24., 10., 25., 11., 26., 12., 27.])'''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))使用和2.8中一樣的超參數(除了學習率)來訓練模型。
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 7.681733, time 0.40 sec- 分開 我想了這樣牽 不的 我 你的一場 我 你的那里 我想要你的微笑 我想要你的微笑 我想要你的微笑 我- 不分開不 我不開始不 不要再想要你 一場去 后知后覺 我想你這樣牽著你的手不放人 你是你人我 別不能再 epoch 100, perplexity 1.258844, time 0.46 sec- 分開 我跟了這節奏 后知后覺 又過了一個秋 后知后覺 我該好好生活 我該好好生活 不知不覺 你已經離開我- 不分開不你是我不懂 想你怎么面對我 甩開球我滿腔的怒火 我想揍你已經很久 別想躲 說你眼睛看著我 別發抖 epoch 150, perplexity 1.064266, time 0.46 sec- 分開 我跟再這樣 思要你被你都我的錯 愛知走 杵在伊斯坦堡 卻只想你和漢堡 我想要你的微笑每天都能看- 不分開不 是我的話 穿人的沒躲 說你的 從小到大的片來敗的窩對我的味道 又過了人慢慢 輕知再覺 你已經 epoch 200, perplexity 1.031409, time 0.47 sec- 分開 我過再這樣 想要你 升機我想要和你飛到宇宙去 想要和你融化在一起 融化在宇宙里 我每天每天每天在- 不分開 是從的起 它給的進愛 在的感受 已邊的話過 我不啊 平常話不多 一定會有護三馬到也什么在這樣對 epoch 250, perplexity 1.056751, time 0.46 sec- 分開 我過再這樣 思出你 我知多的熬 難沒 什么 干什么 我被開口吳儂軟語沉默 娘子她依舊每日折一枝- 不分開 了是我不起 我給 這些功 快使用雙截棍 哼哼哈兮 快使用雙截棍 哼哼哈兮 習武之人切記 仁者無敵小結
- PyTorch的nn模塊提供了循環神經網絡層的實現。
- PyTorch的nn.RNN實例在前向計算后會分別返回輸出和隱藏狀態。該前向計算并不涉及輸出層計算。
如果內容對你有幫助,感謝點贊+關注哦!
關注下方GZH:阿旭算法與機器學習,回復:“RNN創作歌詞”即可獲取本文數據集、源碼與項目文檔,歡迎共同學習交流
總結
以上是生活随笔為你收集整理的【从零开始学习深度学习】34. Pytorch-RNN项目实战:RNN创作歌词案例--使用周杰伦专辑歌词训练模型并创作歌曲【含数据集与源码】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深圳python培训学习班
- 下一篇: Envoy 调试流量的常用技巧直播分享及