6.1 語言模型
語言模型(language model)是自然語言處理的重要技術(shù)鄙币。自然語言處理中最常見的數(shù)據(jù)是文本數(shù)據(jù)。我們可以把一段自然語言文本看作一段離散的時間序列怖现。假設(shè)一段長度為的文本中的詞依次為
茁帽,那么在離散的時間序列中,
(
)可看作在時間步(time step)
的輸出或標簽屈嗤。給定一個長度為
的詞的序列
潘拨,語言模型將計算該序列的概率:
語言模型可用于提升語音識別和機器翻譯的性能。例如饶号,在語音識別中铁追,給定一段“廚房里食油用完了”的語音,有可能會輸出“廚房里食油用完了”和“廚房里石油用完了”這兩個讀音完全一樣的文本序列茫船。如果語言模型判斷出前者的概率大于后者的概率脂信,我們就可以根據(jù)相同讀音的語音輸出“廚房里食油用完了”的文本序列癣蟋。在機器翻譯中,如果對英文“you go first”逐詞翻譯成中文的話狰闪,可能得到“你走先”“你先走”等排列方式的文本序列疯搅。如果語言模型判斷出“你先走”的概率大于其他排列方式的文本序列的概率,我們就可以把“you go first”翻譯成“你先走”埋泵。
6.1.1 語言模型的計算
既然語言模型很有用幔欧,那該如何計算它呢?假設(shè)序列中的每個詞是依次生成的丽声,我們有
例如礁蔗,一段含有4個詞的文本序列的概率
為了計算語言模型,我們需要計算詞的概率雁社,以及一個詞在給定前幾個詞的情況下的條件概率浴井,即語言模型參數(shù)。設(shè)訓(xùn)練數(shù)據(jù)集為一個大型文本語料庫霉撵,如維基百科的所有條目磺浙。詞的概率可以通過該詞在訓(xùn)練數(shù)據(jù)集中的相對詞頻來計算。例如徒坡,可以計算為
在訓(xùn)練數(shù)據(jù)集中的詞頻(詞出現(xiàn)的次數(shù))與訓(xùn)練數(shù)據(jù)集的總詞數(shù)之比撕氧。因此,根據(jù)條件概率定義喇完,一個詞在給定前幾個詞的情況下的條件概率也可以通過訓(xùn)練數(shù)據(jù)集中的相對詞頻計算伦泥。例如,
可以計算為
兩詞相鄰的頻率與
詞頻的比值锦溪,因為該比值即
與
之比不脯;而
同理可以計算為
、
和
三詞相鄰的頻率與
和
兩詞相鄰的頻率的比值刻诊。以此類推防楷。
6.1.2
元語法
當序列長度增加時,計算和存儲多個詞共同出現(xiàn)的概率的復(fù)雜度會呈指數(shù)級增加坏逢。元語法通過馬爾可夫假設(shè)(雖然并不一定成立)簡化了語言模型的計算域帐。這里的馬爾可夫假設(shè)是指一個詞的出現(xiàn)只與前面
個詞相關(guān)赘被,即
階馬爾可夫鏈(Markov chain of order
)是整。如果
,那么有
民假。如果基于
階馬爾可夫鏈浮入,我們可以將語言模型改寫為
以上也叫元語法(
-grams)。它是基于
階馬爾可夫鏈的概率語言模型羊异。當
分別為1事秀、2和3時彤断,我們將其分別稱作一元語法(unigram)、二元語法(bigram)和三元語法(trigram)易迹。例如宰衙,長度為4的序列
在一元語法、二元語法和三元語法中的概率分別為
當較小時睹欲,
元語法往往并不準確供炼。例如,在一元語法中窘疮,由三個詞組成的句子“你走先”和“你先走”的概率是一樣的袋哼。然而,當
較大時闸衫,
元語法需要計算并存儲大量的詞頻和多詞相鄰頻率涛贯。
那么,有沒有方法在語言模型中更好地平衡以上這兩點呢蔚出?我們將在本章探究這樣的方法弟翘。
小結(jié)
- 語言模型是自然語言處理的重要技術(shù)。
-
元語法是基于
階馬爾可夫鏈的概率語言模型身冬,其中
權(quán)衡了計算復(fù)雜度和模型準確性衅胀。
6.2 循環(huán)神經(jīng)網(wǎng)絡(luò)
上一節(jié)介紹的元語法中,時間步
的詞
基于前面所有詞的條件概率只考慮了最近時間步的
個詞酥筝。如果要考慮比
更早時間步的詞對
的可能影響滚躯,我們需要增大
。但這樣模型參數(shù)的數(shù)量將隨之呈指數(shù)級增長嘿歌。
本節(jié)將介紹循環(huán)神經(jīng)網(wǎng)絡(luò)掸掏。它并非剛性地記憶所有固定長度的序列,而是通過隱藏狀態(tài)來存儲之前時間步的信息宙帝。首先我們回憶一下前面介紹過的多層感知機丧凤,然后描述如何添加隱藏狀態(tài)來將它變成循環(huán)神經(jīng)網(wǎng)絡(luò)。
6.2.1 不含隱藏狀態(tài)的神經(jīng)網(wǎng)絡(luò)
讓我們考慮一個含單隱藏層的多層感知機步脓。給定樣本數(shù)為愿待、輸入個數(shù)(特征數(shù)或特征向量維度)為
的小批量數(shù)據(jù)樣本
。設(shè)隱藏層的激活函數(shù)為
靴患,那么隱藏層的輸出
計算為
其中隱藏層權(quán)重參數(shù),隱藏層偏差參數(shù)
农渊,
為隱藏單元個數(shù)。上式相加的兩項形狀不同或颊,因此將按照廣播機制相加砸紊。把隱藏變量
作為輸出層的輸入传于,且設(shè)輸出個數(shù)為
(如分類問題中的類別數(shù)),輸出層的輸出為
其中輸出變量, 輸出層權(quán)重參數(shù)
, 輸出層偏差參數(shù)
。如果是分類問題游添,我們可以使用
來計算輸出類別的概率分布盛末。
6.2.2 含隱藏狀態(tài)的循環(huán)神經(jīng)網(wǎng)絡(luò)
現(xiàn)在我們考慮輸入數(shù)據(jù)存在時間相關(guān)性的情況。假設(shè)是序列中時間步
的小批量輸入否淤,
是該時間步的隱藏變量悄但。與多層感知機不同的是,這里我們保存上一時間步的隱藏變量
石抡,并引入一個新的權(quán)重參數(shù)
檐嚣,該參數(shù)用來描述在當前時間步如何使用上一時間步的隱藏變量。具體來說啰扛,時間步
的隱藏變量的計算由當前時間步的輸入和上一時間步的隱藏變量共同決定:
與多層感知機相比,我們在這里添加了一項隐解。由上式中相鄰時間步的隱藏變量
和
之間的關(guān)系可知鞍帝,這里的隱藏變量能夠捕捉截至當前時間步的序列的歷史信息,就像是神經(jīng)網(wǎng)絡(luò)當前時間步的狀態(tài)或記憶一樣煞茫。因此帕涌,該隱藏變量也稱為隱藏狀態(tài)。由于隱藏狀態(tài)在當前時間步的定義使用了上一時間步的隱藏狀態(tài)续徽,上式的計算是循環(huán)的蚓曼。使用循環(huán)計算的網(wǎng)絡(luò)即循環(huán)神經(jīng)網(wǎng)絡(luò)(recurrent neural network)。
循環(huán)神經(jīng)網(wǎng)絡(luò)有很多種不同的構(gòu)造方法钦扭。含上式所定義的隱藏狀態(tài)的循環(huán)神經(jīng)網(wǎng)絡(luò)是極為常見的一種纫版。若無特別說明,本章中的循環(huán)神經(jīng)網(wǎng)絡(luò)均基于上式中隱藏狀態(tài)的循環(huán)計算客情。在時間步其弊,輸出層的輸出和多層感知機中的計算類似:
循環(huán)神經(jīng)網(wǎng)絡(luò)的參數(shù)包括隱藏層的權(quán)重膀斋、
和偏差
,以及輸出層的權(quán)重
和偏差
籽御。值得一提的是练慕,即便在不同時間步惰匙,循環(huán)神經(jīng)網(wǎng)絡(luò)也始終使用這些模型參數(shù)技掏。因此,循環(huán)神經(jīng)網(wǎng)絡(luò)模型參數(shù)的數(shù)量不隨時間步的增加而增長项鬼。
圖6.1展示了循環(huán)神經(jīng)網(wǎng)絡(luò)在3個相鄰時間步的計算邏輯粱栖。在時間步躁愿,隱藏狀態(tài)的計算可以看成是將輸入
和前一時間步隱藏狀態(tài)
連結(jié)后輸入一個激活函數(shù)為
的全連接層。該全連接層的輸出就是當前時間步的隱藏狀態(tài)
,且模型參數(shù)為
與
的連結(jié)馍刮,偏差為
赎瑰。當前時間步
的隱藏狀態(tài)
將參與下一個時間步
的隱藏狀態(tài)
的計算召庞,并輸入到當前時間步的全連接輸出層。
我們剛剛提到沦零,隱藏狀態(tài)中的計算等價于
與
連結(jié)后的矩陣乘以
與
連結(jié)后的矩陣祭隔。接下來,我們用一個具體的例子來驗證這一點路操。首先疾渴,我們構(gòu)造矩陣
X
、W_xh
屯仗、H
和W_hh
搞坝,它們的形狀分別為(3, 1)、(1, 4)魁袜、(3, 4)和(4, 4)桩撮。將X
與W_xh
、H
與W_hh
分別相乘峰弹,再把兩個乘法運算的結(jié)果相加距境,得到形狀為(3, 4)的矩陣。
import torch
X, W_xh = torch.randn(3, 1), torch.randn(1, 4)
H, W_hh = torch.randn(3, 4), torch.randn(4, 4)
torch.matmul(X, W_xh) + torch.matmul(H, W_hh)
輸出:
tensor([[ 5.2633, -3.2288, 0.6037, -1.3321],
[ 9.4012, -6.7830, 1.0630, -0.1809],
[ 7.0355, -2.2361, 0.7469, -3.4667]])
將矩陣X
和H
按列(維度1)連結(jié)垮卓,連結(jié)后的矩陣形狀為(3, 5)垫桂。可見粟按,連結(jié)后矩陣在維度1的長度為矩陣X
和H
在維度1的長度之和()诬滩。然后,將矩陣
W_xh
和W_hh
按行(維度0)連結(jié)灭将,連結(jié)后的矩陣形狀為(5, 4)疼鸟。最后將兩個連結(jié)后的矩陣相乘,得到與上面代碼輸出相同的形狀為(3, 4)的矩陣庙曙。
torch.matmul(torch.cat((X, H), dim=1), torch.cat((W_xh, W_hh), dim=0))
輸出:
tensor([[ 5.2633, -3.2288, 0.6037, -1.3321],
[ 9.4012, -6.7830, 1.0630, -0.1809],
[ 7.0355, -2.2361, 0.7469, -3.4667]])
6.2.3 應(yīng)用:基于字符級循環(huán)神經(jīng)網(wǎng)絡(luò)的語言模型
最后我們介紹如何應(yīng)用循環(huán)神經(jīng)網(wǎng)絡(luò)來構(gòu)建一個語言模型空镜。設(shè)小批量中樣本數(shù)為1,文本序列為“想”“要”“有”“直”“升”“機”。圖6.2演示了如何使用循環(huán)神經(jīng)網(wǎng)絡(luò)基于當前和過去的字符來預(yù)測下一個字符吴攒。在訓(xùn)練時张抄,我們對每個時間步的輸出層輸出使用softmax運算,然后使用交叉熵損失函數(shù)來計算它與標簽的誤差洼怔。在圖6.2中署惯,由于隱藏層中隱藏狀態(tài)的循環(huán)計算,時間步3的輸出取決于文本序列“想”“要”“有”镣隶。 由于訓(xùn)練數(shù)據(jù)中該序列的下一個詞為“直”极谊,時間步3的損失將取決于該時間步基于序列“想”“要”“有”生成下一個詞的概率分布與該時間步的標簽“直”。
因為每個輸入詞是一個字符轻猖,因此這個模型被稱為字符級循環(huán)神經(jīng)網(wǎng)絡(luò)(character-level recurrent neural network)。因為不同字符的個數(shù)遠小于不同詞的個數(shù)(對于英文尤其如此)域那,所以字符級循環(huán)神經(jīng)網(wǎng)絡(luò)的計算通常更加簡單蜕依。在接下來的幾節(jié)里,我們將介紹它的具體實現(xiàn)琉雳。
小結(jié)
- 使用循環(huán)計算的網(wǎng)絡(luò)即循環(huán)神經(jīng)網(wǎng)絡(luò)样眠。
- 循環(huán)神經(jīng)網(wǎng)絡(luò)的隱藏狀態(tài)可以捕捉截至當前時間步的序列的歷史信息。
- 循環(huán)神經(jīng)網(wǎng)絡(luò)模型參數(shù)的數(shù)量不隨時間步的增加而增長翠肘。
- 可以基于字符級循環(huán)神經(jīng)網(wǎng)絡(luò)來創(chuàng)建語言模型檐束。
6.3 語言模型數(shù)據(jù)集(周杰倫專輯歌詞)
本節(jié)將介紹如何預(yù)處理一個語言模型數(shù)據(jù)集,并將其轉(zhuǎn)換成字符級循環(huán)神經(jīng)網(wǎng)絡(luò)所需要的輸入格式束倍。為此被丧,我們收集了周杰倫從第一張專輯《Jay》到第十張專輯《跨時代》中的歌詞,并在后面幾節(jié)里應(yīng)用循環(huán)神經(jīng)網(wǎng)絡(luò)來訓(xùn)練一個語言模型绪妹。當模型訓(xùn)練好后甥桂,我們就可以用這個模型來創(chuàng)作歌詞。
6.3.1 讀取數(shù)據(jù)集
首先讀取這個數(shù)據(jù)集邮旷,看看前40個字符是什么樣的黄选。
import torch
import random
import zipfile
with zipfile.ZipFile('../../data/jaychou_lyrics.txt.zip') as zin:
with zin.open('jaychou_lyrics.txt') as f:
corpus_chars = f.read().decode('utf-8')
corpus_chars[:40]
輸出:
'想要有直升機\n想要和你飛到宇宙去\n想要和你融化在一起\n融化在宇宙里\n我每天每天每'
這個數(shù)據(jù)集有6萬多個字符。為了打印方便婶肩,我們把換行符替換成空格办陷,然后僅使用前1萬個字符來訓(xùn)練模型。
corpus_chars = corpus_chars.replace('\n', ' ').replace('\r', ' ')
corpus_chars = corpus_chars[0:10000]
6.3.2 建立字符索引
我們將每個字符映射成一個從0開始的連續(xù)整數(shù)律歼,又稱索引民镜,來方便之后的數(shù)據(jù)處理。為了得到索引险毁,我們將數(shù)據(jù)集里所有不同字符取出來制圈,然后將其逐一映射到索引來構(gòu)造詞典们童。接著,打印vocab_size
鲸鹦,即詞典中不同字符的個數(shù)慧库,又稱詞典大小。
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
之后亥鬓,將訓(xùn)練數(shù)據(jù)集中每個字符轉(zhuǎn)化為索引,并打印前20個字符及其對應(yīng)的索引域庇。
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]
我們將以上代碼封裝在d2lzh_pytorch
包里的load_data_jay_lyrics
函數(shù)中嵌戈,以方便后面章節(jié)調(diào)用。調(diào)用該函數(shù)后會依次得到corpus_indices
听皿、char_to_idx
熟呛、idx_to_char
和vocab_size
這4個變量。
6.3.3 時序數(shù)據(jù)的采樣
在訓(xùn)練中我們需要每次隨機讀取小批量樣本和標簽尉姨。與之前章節(jié)的實驗數(shù)據(jù)不同的是庵朝,時序數(shù)據(jù)的一個樣本通常包含連續(xù)的字符。假設(shè)時間步數(shù)為5又厉,樣本序列為5個字符九府,即“想”“要”“有”“直”“升”。該樣本的標簽序列為這些字符分別在訓(xùn)練集中的下一個字符覆致,即“要”“有”“直”“升”“機”侄旬。我們有兩種方式對時序數(shù)據(jù)進行采樣,分別是隨機采樣和相鄰采樣煌妈。
6.3.3.1 隨機采樣
下面的代碼每次從數(shù)據(jù)里隨機采樣一個小批量儡羔。其中批量大小batch_size
指每個小批量的樣本數(shù),num_steps
為每個樣本所包含的時間步數(shù)璧诵。
在隨機采樣中汰蜘,每個樣本是原始序列上任意截取的一段序列。相鄰的兩個隨機小批量在原始序列上的位置不一定相毗鄰之宿。因此族操,我們無法用一個小批量最終時間步的隱藏狀態(tài)來初始化下一個小批量的隱藏狀態(tài)。在訓(xùn)練模型時比被,每次隨機采樣前都需要重新初始化隱藏狀態(tài)坪创。
# 本函數(shù)已保存在d2lzh_pytorch包中方便以后使用
def data_iter_random(corpus_indices, batch_size, num_steps, device=None):
# 減1是因為輸出的索引x是相應(yīng)輸入的索引y加1
num_examples = (len(corpus_indices) - 1) // num_steps
epoch_size = num_examples // batch_size
example_indices = list(range(num_examples))
random.shuffle(example_indices)
# 返回從pos開始的長為num_steps的序列
def _data(pos):
return corpus_indices[pos: pos + num_steps]
if device is None:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
for i in range(epoch_size):
# 每次讀取batch_size個隨機樣本
i = i * batch_size
batch_indices = example_indices[i: i + batch_size]
X = [_data(j * num_steps) for j in batch_indices]
Y = [_data(j * num_steps + 1) for j in batch_indices]
yield torch.tensor(X, dtype=torch.float32, device=device), torch.tensor(Y, dtype=torch.float32, device=device)
讓我們輸入一個從0到29的連續(xù)整數(shù)的人工序列。設(shè)批量大小和時間步數(shù)分別為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.]])
6.3.3.2 相鄰采樣
除對原始序列做隨機采樣之外,我們還可以令相鄰的兩個隨機小批量在原始序列上的位置相毗鄰。這時候危喉,我們就可以用一個小批量最終時間步的隱藏狀態(tài)來初始化下一個小批量的隱藏狀態(tài)宋渔,從而使下一個小批量的輸出也取決于當前小批量的輸入,并如此循環(huán)下去辜限。這對實現(xiàn)循環(huán)神經(jīng)網(wǎng)絡(luò)造成了兩方面影響:一方面皇拣,
在訓(xùn)練模型時,我們只需在每一個迭代周期開始時初始化隱藏狀態(tài)薄嫡;另一方面氧急,當多個相鄰小批量通過傳遞隱藏狀態(tài)串聯(lián)起來時,模型參數(shù)的梯度計算將依賴所有串聯(lián)起來的小批量序列毫深。同一迭代周期中吩坝,隨著迭代次數(shù)的增加,梯度的計算開銷會越來越大哑蔫。
為了使模型參數(shù)的梯度計算只依賴一次迭代讀取的小批量序列钉寝,我們可以在每次讀取小批量前將隱藏狀態(tài)從計算圖中分離出來。我們將在下一節(jié)(循環(huán)神經(jīng)網(wǎng)絡(luò)的從零開始實現(xiàn))的實現(xiàn)中了解這種處理方式闸迷。
# 本函數(shù)已保存在d2lzh_pytorch包中方便以后使用
def data_iter_consecutive(corpus_indices, batch_size, num_steps, device=None):
if device is None:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
corpus_indices = torch.tensor(corpus_indices, dtype=torch.float32, device=device)
data_len = len(corpus_indices)
batch_len = data_len // batch_size
indices = corpus_indices[0: batch_size*batch_len].view(batch_size, batch_len)
epoch_size = (batch_len - 1) // num_steps
for i in range(epoch_size):
i = i * num_steps
X = indices[:, i: i + num_steps]
Y = indices[:, i + 1: i + num_steps + 1]
yield X, Y
同樣的設(shè)置下嵌纲,打印相鄰采樣每次讀取的小批量樣本的輸入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.]])
小結(jié)
- 時序數(shù)據(jù)采樣方式包括隨機采樣和相鄰采樣疹瘦。使用這兩種方式的循環(huán)神經(jīng)網(wǎng)絡(luò)訓(xùn)練在實現(xiàn)上略有不同。
6.5 循環(huán)神經(jīng)網(wǎng)絡(luò)的簡潔實現(xiàn)
本節(jié)將使用PyTorch來更簡潔地實現(xiàn)基于循環(huán)神經(jīng)網(wǎng)絡(luò)的語言模型巡球。首先言沐,我們讀取周杰倫專輯歌詞數(shù)據(jù)集。
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
模塊提供了循環(huán)神經(jīng)網(wǎng)絡(luò)的實現(xiàn)酣栈。下面構(gòu)造一個含單隱藏層险胰、隱藏單元個數(shù)為256的循環(huán)神經(jīng)網(wǎng)絡(luò)層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)
與上一節(jié)中實現(xiàn)的循環(huán)神經(jīng)網(wǎng)絡(luò)不同矿筝,這里rnn_layer
的輸入形狀為(時間步數(shù), 批量大小, 輸入個數(shù))起便。其中輸入個數(shù)即one-hot向量長度(詞典大小)窖维。此外榆综,rnn_layer
作為nn.RNN
實例,在前向計算后會分別返回輸出和隱藏狀態(tài)h铸史,其中輸出指的是隱藏層在各個時間步上計算并輸出的隱藏狀態(tài)鼻疮,它們通常作為后續(xù)輸出層的輸入。需要強調(diào)的是琳轿,該“輸出”本身并不涉及輸出層計算判沟,形狀為(時間步數(shù), 批量大小, 隱藏單元個數(shù))耿芹。而nn.RNN
實例在前向計算返回的隱藏狀態(tài)指的是隱藏層在最后時間步的隱藏狀態(tài):當隱藏層有多層時,每一層的隱藏狀態(tài)都會記錄在該變量中挪哄;對于像長短期記憶(LSTM)吧秕,隱藏狀態(tài)是一個元組(h, c),即hidden state和cell state迹炼。我們會在本章的后面介紹長短期記憶和深度循環(huán)神經(jīng)網(wǎng)絡(luò)砸彬。關(guān)于循環(huán)神經(jīng)網(wǎng)絡(luò)(以LSTM為例)的輸出,可以參考下圖(圖片來源)斯入。
來看看我們的例子砂碉,輸出形狀為(時間步數(shù), 批量大小, 隱藏單元個數(shù)),隱藏狀態(tài)h的形狀為(層數(shù), 批量大小, 隱藏單元個數(shù))咱扣。
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
類來定義一個完整的循環(huán)神經(jīng)網(wǎng)絡(luò)闹伪。它首先將輸入數(shù)據(jù)使用one-hot向量表示后輸入到rnn_layer
中,然后使用全連接輸出層得到輸出壮池。輸出個數(shù)等于詞典大小vocab_size
偏瓤。
# 本類已保存在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 訓(xùn)練模型
同上一節(jié)一樣椰憋,下面定義一個預(yù)測函數(shù)厅克。這里的實現(xiàn)區(qū)別在于前向計算和初始化隱藏狀態(tài)的函數(shù)接口。
# 本函數(shù)已保存在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])
讓我們使用權(quán)重為隨機值的模型來預(yù)測一次橙依。
model = RNNModel(rnn_layer, vocab_size).to(device)
predict_rnn_pytorch('分開', 10, model, vocab_size, device, idx_to_char, char_to_idx)
輸出:
'分開戲想暖迎涼想征涼征征'
接下來實現(xiàn)訓(xùn)練函數(shù)证舟。算法同上一節(jié)的一樣,但這里只使用了相鄰采樣來讀取數(shù)據(jù)窗骑。
# 本函數(shù)已保存在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函數(shù)從計算圖分離隱藏狀態(tài), 這是為了
# 使模型參數(shù)的梯度計算只依賴一次迭代讀取的小批量序列(防止梯度計算開銷太大)
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)女责,轉(zhuǎn)置后再變成長度為
# batch * num_steps 的向量,這樣跟輸出的行一一對應(yīng)
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))
使用和上一節(jié)實驗中一樣的超參數(shù)(除了學(xué)習(xí)率)來訓(xùn)練模型创译。
num_epochs, batch_size, lr, clipping_theta = 250, 32, 1e-3, 1e-2 # 注意這里的學(xué)習(xí)率設(shè)置
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
- 分開不能去河南嵩山 學(xué)少林跟武當 快使用雙截棍 哼哼哈兮 快使用雙截棍 哼哼哈兮 習(xí)武之人切記 仁者無敵
- 不分開 在我會想通 是誰開沒有全有開始 他心今天 一切人看 我 一口令秋軟語的姑娘緩緩走過外灘 消失的 舊
epoch 200, perplexity 1.034663, time 0.05 sec
- 分開不能去嗎周杰倫 才離 沒要你在一場悲劇 我的完美主義 太徹底 分手的話像語言暴力 我已無能為力再提起
- 不分開 讓我面到你 愛情來的太快就像龍卷風(fēng) 離不開暴風(fēng)圈來不及逃 我不能再想 我不能再想 我不 我不 我不
epoch 250, perplexity 1.021437, time 0.05 sec
- 分開 我我外的家邊 你知道這 我愛不看的太 我想一個又重來不以 迷已文一只剩下回憶 讓我叫帶你 你你的
- 不分開 我我想想和 是你聽沒不 我不能不想 不知不覺 你已經(jīng)離開我 不知不覺 我跟了這節(jié)奏 后知后覺
小結(jié)
- PyTorch的
nn
模塊提供了循環(huán)神經(jīng)網(wǎng)絡(luò)層的實現(xiàn)抵知。 - PyTorch的
nn.RNN
實例在前向計算后會分別返回輸出和隱藏狀態(tài)。該前向計算并不涉及輸出層計算软族。
6.6 通過時間反向傳播
在前面兩節(jié)中刷喜,如果不裁剪梯度,模型將無法正常訓(xùn)練立砸。為了深刻理解這一現(xiàn)象掖疮,本節(jié)將介紹循環(huán)神經(jīng)網(wǎng)絡(luò)中梯度的計算和存儲方法,即通過時間反向傳播(back-propagation through time)颗祝。
我們在3.14節(jié)(正向傳播氮墨、反向傳播和計算圖)中介紹了神經(jīng)網(wǎng)絡(luò)中梯度計算與存儲的一般思路纺蛆,并強調(diào)正向傳播和反向傳播相互依賴。正向傳播在循環(huán)神經(jīng)網(wǎng)絡(luò)中比較直觀规揪,而通過時間反向傳播其實是反向傳播在循環(huán)神經(jīng)網(wǎng)絡(luò)中的具體應(yīng)用桥氏。我們需要將循環(huán)神經(jīng)網(wǎng)絡(luò)按時間步展開,從而得到模型變量和參數(shù)之間的依賴關(guān)系猛铅,并依據(jù)鏈式法則應(yīng)用反向傳播計算并存儲梯度字支。
6.6.1 定義模型
簡單起見,我們考慮一個無偏差項的循環(huán)神經(jīng)網(wǎng)絡(luò)奸忽,且激活函數(shù)為恒等映射()堕伪。設(shè)時間步
的輸入為單樣本
,標簽為
栗菜,那么隱藏狀態(tài)
的計算表達式為
其中和
是隱藏層權(quán)重參數(shù)欠雌。設(shè)輸出層權(quán)重參數(shù)
,時間步
的輸出層變量
計算為
設(shè)時間步的損失為
疙筹。時間步數(shù)為
的損失函數(shù)
定義為
我們將稱為有關(guān)給定時間步的數(shù)據(jù)樣本的目標函數(shù)富俄,并在本節(jié)后續(xù)討論中簡稱為目標函數(shù)。
6.6.2 模型計算圖
為了可視化循環(huán)神經(jīng)網(wǎng)絡(luò)中模型變量和參數(shù)在計算中的依賴關(guān)系而咆,我們可以繪制模型計算圖霍比,如圖6.3所示。例如暴备,時間步3的隱藏狀態(tài)的計算依賴模型參數(shù)
悠瞬、
、上一時間步隱藏狀態(tài)
以及當前時間步輸入
涯捻。
6.6.3 方法
剛剛提到障癌,圖6.3中的模型的參數(shù)是 ,
和
凌外。與3.14節(jié)(正向傳播、反向傳播和計算圖)中的類似混弥,訓(xùn)練模型通常需要模型參數(shù)的梯度
趴乡、
和
。
根據(jù)圖6.3中的依賴關(guān)系蝗拿,我們可以按照其中箭頭所指的反方向依次計算并存儲梯度晾捏。為了表述方便,我們依然采用3.14節(jié)中表達鏈式法則的運算符prod哀托。
首先惦辛,目標函數(shù)有關(guān)各時間步輸出層變量的梯度很容易計算:
下面,我們可以計算目標函數(shù)有關(guān)模型參數(shù)的梯度
仓手。根據(jù)圖6.3胖齐,
通過
依賴
玻淑。依據(jù)鏈式法則,
其次呀伙,我們注意到隱藏狀態(tài)之間也存在依賴關(guān)系补履。
在圖6.3中,只通過
依賴最終時間步
的隱藏狀態(tài)
剿另。因此箫锤,我們先計算目標函數(shù)有關(guān)最終時間步隱藏狀態(tài)的梯度
。依據(jù)鏈式法則雨女,我們得到
接下來對于時間步, 在圖6.3中谚攒,
通過
和
依賴
查辩。依據(jù)鏈式法則晒旅,
目標函數(shù)有關(guān)時間步的隱藏狀態(tài)的梯度
需要按照時間步從大到小依次計算:
將上面的遞歸公式展開,對任意時間步名扛,我們可以得到目標函數(shù)有關(guān)隱藏狀態(tài)梯度的通項公式
由上式中的指數(shù)項可見讼稚,當時間步數(shù) 較大或者時間步
較小時括儒,目標函數(shù)有關(guān)隱藏狀態(tài)的梯度較容易出現(xiàn)衰減和爆炸。這也會影響其他包含
項的梯度乱灵,例如隱藏層中模型參數(shù)的梯度
和
塑崖。
在圖6.3中七冲,通過
依賴這些模型參數(shù)痛倚。
依據(jù)鏈式法則,我們有
我們已在3.14節(jié)里解釋過澜躺,每次迭代中蝉稳,我們在依次計算完以上各個梯度后,會將它們存儲起來掘鄙,從而避免重復(fù)計算耘戚。例如,由于隱藏狀態(tài)梯度被計算和存儲操漠,之后的模型參數(shù)梯度
和
的計算可以直接讀取
的值收津,而無須重復(fù)計算它們。此外浊伙,反向傳播中的梯度計算可能會依賴變量的當前值撞秋。它們正是通過正向傳播計算出來的。
舉例來說嚣鄙,參數(shù)梯度的計算需要依賴隱藏狀態(tài)在時間步
的當前值
(
是初始化得到的)吻贿。這些值是通過從輸入層到輸出層的正向傳播計算并存儲得到的。
小結(jié)
- 通過時間反向傳播是反向傳播在循環(huán)神經(jīng)網(wǎng)絡(luò)中的具體應(yīng)用哑子。
- 當總的時間步數(shù)較大或者當前時間步較小時舅列,循環(huán)神經(jīng)網(wǎng)絡(luò)的梯度較容易出現(xiàn)衰減或爆炸肌割。
6.7 門控循環(huán)單元(GRU)
上一節(jié)介紹了循環(huán)神經(jīng)網(wǎng)絡(luò)中的梯度計算方法。我們發(fā)現(xiàn)帐要,當時間步數(shù)較大或者時間步較小時把敞,循環(huán)神經(jīng)網(wǎng)絡(luò)的梯度較容易出現(xiàn)衰減或爆炸。雖然裁剪梯度可以應(yīng)對梯度爆炸榨惠,但無法解決梯度衰減的問題先巴。通常由于這個原因,循環(huán)神經(jīng)網(wǎng)絡(luò)在實際中較難捕捉時間序列中時間步距離較大的依賴關(guān)系冒冬。
門控循環(huán)神經(jīng)網(wǎng)絡(luò)(gated recurrent neural network)的提出伸蚯,正是為了更好地捕捉時間序列中時間步距離較大的依賴關(guān)系。它通過可以學(xué)習(xí)的門來控制信息的流動简烤。其中剂邮,門控循環(huán)單元(gated recurrent unit,GRU)是一種常用的門控循環(huán)神經(jīng)網(wǎng)絡(luò) [1, 2]横侦。另一種常用的門控循環(huán)神經(jīng)網(wǎng)絡(luò)則將在下一節(jié)中介紹挥萌。
6.7.1 門控循環(huán)單元
下面將介紹門控循環(huán)單元的設(shè)計。它引入了重置門(reset gate)和更新門(update gate)的概念枉侧,從而修改了循環(huán)神經(jīng)網(wǎng)絡(luò)中隱藏狀態(tài)的計算方式引瀑。
6.7.1.1 重置門和更新門
如圖6.4所示,門控循環(huán)單元中的重置門和更新門的輸入均為當前時間步輸入與上一時間步隱藏狀態(tài)
榨馁,輸出由激活函數(shù)為sigmoid函數(shù)的全連接層計算得到憨栽。
具體來說,假設(shè)隱藏單元個數(shù)為翼虫,給定時間步
的小批量輸入
(樣本數(shù)為
屑柔,輸入個數(shù)為
)和上一時間步隱藏狀態(tài)
。重置門
和更新門
的計算如下:
其中和
是權(quán)重參數(shù),
是偏差參數(shù)。3.8節(jié)(多層感知機)節(jié)中介紹過别凤,sigmoid函數(shù)可以將元素的值變換到0和1之間饰序。因此,重置門
和更新門
中每個元素的值域都是
闻妓。
6.7.1.2 候選隱藏狀態(tài)
接下來菌羽,門控循環(huán)單元將計算候選隱藏狀態(tài)來輔助稍后的隱藏狀態(tài)計算。如圖6.5所示,我們將當前時間步重置門的輸出與上一時間步隱藏狀態(tài)做按元素乘法(符號為)注祖。如果重置門中元素值接近0猾蒂,那么意味著重置對應(yīng)隱藏狀態(tài)元素為0,即丟棄上一時間步的隱藏狀態(tài)是晨。如果元素值接近1肚菠,那么表示保留上一時間步的隱藏狀態(tài)。然后罩缴,將按元素乘法的結(jié)果與當前時間步的輸入連結(jié)蚊逢,再通過含激活函數(shù)tanh的全連接層計算出候選隱藏狀態(tài),其所有元素的值域為
箫章。
具體來說烙荷,時間步的候選隱藏狀態(tài)
的計算為
其中和
是權(quán)重參數(shù)檬寂,
是偏差參數(shù)。從上面這個公式可以看出桶至,重置門控制了上一時間步的隱藏狀態(tài)如何流入當前時間步的候選隱藏狀態(tài)昼伴。而上一時間步的隱藏狀態(tài)可能包含了時間序列截至上一時間步的全部歷史信息。因此镣屹,重置門可以用來丟棄與預(yù)測無關(guān)的歷史信息圃郊。
6.7.1.3 隱藏狀態(tài)
最后,時間步的隱藏狀態(tài)
的計算使用當前時間步的更新門
來對上一時間步的隱藏狀態(tài)
和當前時間步的候選隱藏狀態(tài)
做組合:
值得注意的是女蜈,更新門可以控制隱藏狀態(tài)應(yīng)該如何被包含當前時間步信息的候選隱藏狀態(tài)所更新持舆,如圖6.6所示。假設(shè)更新門在時間步到
(
)之間一直近似1鞭光。那么吏廉,在時間步
到
之間的輸入信息幾乎沒有流入時間步
的隱藏狀態(tài)
泞遗。實際上惰许,這可以看作是較早時刻的隱藏狀態(tài)
一直通過時間保存并傳遞至當前時間步
。這個設(shè)計可以應(yīng)對循環(huán)神經(jīng)網(wǎng)絡(luò)中的梯度衰減問題史辙,并更好地捕捉時間序列中時間步距離較大的依賴關(guān)系汹买。
我們對門控循環(huán)單元的設(shè)計稍作總結(jié):
- 重置門有助于捕捉時間序列里短期的依賴關(guān)系;
- 更新門有助于捕捉時間序列里長期的依賴關(guān)系聊倔。
6.7.2 讀取數(shù)據(jù)集
為了實現(xiàn)并展示門控循環(huán)單元晦毙,下面依然使用周杰倫歌詞數(shù)據(jù)集來訓(xùn)練模型作詞。這里除門控循環(huán)單元以外的實現(xiàn)已在6.2節(jié)(循環(huán)神經(jīng)網(wǎng)絡(luò))中介紹過耙蔑。以下為讀取數(shù)據(jù)集部分见妒。
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.7.3 從零開始實現(xiàn)
我們先介紹如何從零開始實現(xiàn)門控循環(huán)單元。
6.7.3.1 初始化模型參數(shù)
下面的代碼對模型參數(shù)進行初始化甸陌。超參數(shù)num_hiddens
定義了隱藏單元的個數(shù)须揣。
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)
def _three():
return (_one((num_inputs, num_hiddens)),
_one((num_hiddens, num_hiddens)),
torch.nn.Parameter(torch.zeros(num_hiddens, device=device, dtype=torch.float32), requires_grad=True))
W_xz, W_hz, b_z = _three() # 更新門參數(shù)
W_xr, W_hr, b_r = _three() # 重置門參數(shù)
W_xh, W_hh, b_h = _three() # 候選隱藏狀態(tài)參數(shù)
# 輸出層參數(shù)
W_hq = _one((num_hiddens, num_outputs))
b_q = torch.nn.Parameter(torch.zeros(num_outputs, device=device, dtype=torch.float32), requires_grad=True)
return nn.ParameterList([W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q])
6.7.3.2 定義模型
下面的代碼定義隱藏狀態(tài)初始化函數(shù)init_gru_state
盐股。同6.4節(jié)(循環(huán)神經(jīng)網(wǎng)絡(luò)的從零開始實現(xiàn))中定義的init_rnn_state
函數(shù)一樣,它返回由一個形狀為(批量大小, 隱藏單元個數(shù))的值為0的Tensor
組成的元組耻卡。
def init_gru_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device), )
下面根據(jù)門控循環(huán)單元的計算表達式定義模型疯汁。
def gru(inputs, state, params):
W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
for X in inputs:
Z = torch.sigmoid(torch.matmul(X, W_xz) + torch.matmul(H, W_hz) + b_z)
R = torch.sigmoid(torch.matmul(X, W_xr) + torch.matmul(H, W_hr) + b_r)
H_tilda = torch.tanh(torch.matmul(X, W_xh) + torch.matmul(R * H, W_hh) + b_h)
H = Z * H + (1 - Z) * H_tilda
Y = torch.matmul(H, W_hq) + b_q
outputs.append(Y)
return outputs, (H,)
6.7.3.3 訓(xùn)練模型并創(chuàng)作歌詞
我們在訓(xùn)練模型時只使用相鄰采樣。設(shè)置好超參數(shù)后卵酪,我們將訓(xùn)練模型并根據(jù)前綴“分開”和“不分開”分別創(chuàng)作長度為50個字符的一段歌詞幌蚊。
num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 40, 50, ['分開', '不分開']
我們每過40個迭代周期便根據(jù)當前訓(xùn)練的模型創(chuàng)作一段歌詞。
d2l.train_and_predict_rnn(gru, get_params, init_gru_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 40, perplexity 149.477598, time 1.08 sec
- 分開 我不不你 我想你你的愛我 你不你的讓我 你不你的讓我 你不你的讓我 你不你的讓我 你不你的讓我 你
- 不分開 我想你你的讓我 你不你的讓我 你不你的讓我 你不你的讓我 你不你的讓我 你不你的讓我 你不你的讓我
epoch 80, perplexity 31.689210, time 1.10 sec
- 分開 我想要你 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不
- 不分開 我想要你 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不
epoch 120, perplexity 4.866115, time 1.08 sec
- 分開 我想要這樣牽著你的手不放開 愛過 讓我來的肩膀 一起好酒 你來了這節(jié)秋 后知后覺 我該好好生活 我
- 不分開 你已經(jīng)不了我不要 我不要再想你 我不要再想你 我不要再想你 不知不覺 我跟了這節(jié)奏 后知后覺 又過
epoch 160, perplexity 1.442282, time 1.51 sec
- 分開 我一定好生憂 唱著歌 一直走 我想就這樣牽著你的手不放開 愛可不可以簡簡單單沒有傷害 你 靠著我的
- 不分開 你已經(jīng)離開我 不知不覺 我跟了這節(jié)奏 后知后覺 又過了一個秋 后知后覺 我該好好生活 我該好好生活
6.7.4 簡潔實現(xiàn)
在PyTorch中我們直接調(diào)用nn
模塊中的GRU
類即可溃卡。
lr = 1e-2 # 注意調(diào)整學(xué)習(xí)率
gru_layer = nn.GRU(input_size=vocab_size, hidden_size=num_hiddens)
model = d2l.RNNModel(gru_layer, vocab_size).to(device)
d2l.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 40, perplexity 1.022157, time 1.02 sec
- 分開手牽手 一步兩步三步四步望著天 看星星 一顆兩顆三顆四顆 連成線背著背默默許下心愿 看遠方的星是否聽
- 不分開暴風(fēng)圈來不及逃 我不能再想 我不能再想 我不 我不 我不能 愛情走的太快就像龍卷風(fēng) 不能承受我已無處
epoch 80, perplexity 1.014535, time 1.04 sec
- 分開始想像 爸和媽當年的模樣 說著一口吳儂軟語的姑娘緩緩走過外灘 消失的 舊時光 一九四三 在回憶 的路
- 不分開始愛像 不知不覺 你已經(jīng)離開我 不知不覺 我跟了這節(jié)奏 后知后覺 又過了一個秋 后知后覺 我該好好
epoch 120, perplexity 1.147843, time 1.04 sec
- 分開都靠我 你拿著球不投 又不會掩護我 選你這種隊友 瞎透了我 說你說 分數(shù)怎么停留 所有回憶對著我進攻
- 不分開球我有多煩惱多 牧草有沒有危險 一場夢 我面對我 甩開球我滿腔的怒火 我想揍你已經(jīng)很久 別想躲 說你
epoch 160, perplexity 1.018370, time 1.05 sec
- 分開愛上你 那場悲劇 是你完美演出的一場戲 寧愿心碎哭泣 再狠狠忘記 你愛過我的證據(jù) 讓晶瑩的淚滴 閃爍
- 不分開始 擔(dān)心今天的你過得好不好 整個畫面是你 想你想的睡不著 嘴嘟嘟那可愛的模樣 還有在你身上香香的味道
小結(jié)
- 門控循環(huán)神經(jīng)網(wǎng)絡(luò)可以更好地捕捉時間序列中時間步距離較大的依賴關(guān)系溢豆。
- 門控循環(huán)單元引入了門的概念,從而修改了循環(huán)神經(jīng)網(wǎng)絡(luò)中隱藏狀態(tài)的計算方式瘸羡。它包括重置門沫换、更新門、候選隱藏狀態(tài)和隱藏狀態(tài)最铁。
- 重置門有助于捕捉時間序列里短期的依賴關(guān)系讯赏。
- 更新門有助于捕捉時間序列里長期的依賴關(guān)系。
6.8 長短期記憶(LSTM)
本節(jié)將介紹另一種常用的門控循環(huán)神經(jīng)網(wǎng)絡(luò):長短期記憶(long short-term memory冷尉,LSTM)[1]漱挎。它比門控循環(huán)單元的結(jié)構(gòu)稍微復(fù)雜一點。
6.8.1 長短期記憶
LSTM 中引入了3個門雀哨,即輸入門(input gate)磕谅、遺忘門(forget gate)和輸出門(output gate),以及與隱藏狀態(tài)形狀相同的記憶細胞(某些文獻把記憶細胞當成一種特殊的隱藏狀態(tài))雾棺,從而記錄額外的信息膊夹。
6.8.1.1 輸入門、遺忘門和輸出門
與門控循環(huán)單元中的重置門和更新門一樣捌浩,如圖6.7所示放刨,長短期記憶的門的輸入均為當前時間步輸入與上一時間步隱藏狀態(tài)
,輸出由激活函數(shù)為sigmoid函數(shù)的全連接層計算得到尸饺。如此一來进统,這3個門元素的值域均為
。
具體來說螟碎,假設(shè)隱藏單元個數(shù)為,給定時間步
的小批量輸入
(樣本數(shù)為
迹栓,輸入個數(shù)為
)和上一時間步隱藏狀態(tài)
掉分。
時間步的輸入門
、遺忘門
和輸出門
分別計算如下:
其中的和
是權(quán)重參數(shù),
是偏差參數(shù)消返。
6.8.1.2 候選記憶細胞
接下來载弄,長短期記憶需要計算候選記憶細胞。它的計算與上面介紹的3個門類似撵颊,但使用了值域在
的tanh函數(shù)作為激活函數(shù)宇攻,如圖6.8所示。
具體來說倡勇,時間步的候選記憶細胞
的計算為
其中和
是權(quán)重參數(shù),
是偏差參數(shù)夸浅。
6.8.1.3 記憶細胞
我們可以通過元素值域在的輸入門、遺忘門和輸出門來控制隱藏狀態(tài)中信息的流動扔役,這一般也是通過使用按元素乘法(符號為
)來實現(xiàn)的帆喇。當前時間步記憶細胞
的計算組合了上一時間步記憶細胞和當前時間步候選記憶細胞的信息,并通過遺忘門和輸入門來控制信息的流動:
如圖6.9所示亿胸,遺忘門控制上一時間步的記憶細胞中的信息是否傳遞到當前時間步坯钦,而輸入門則控制當前時間步的輸入
通過候選記憶細胞
如何流入當前時間步的記憶細胞。如果遺忘門一直近似1且輸入門一直近似0侈玄,過去的記憶細胞將一直通過時間保存并傳遞至當前時間步婉刀。這個設(shè)計可以應(yīng)對循環(huán)神經(jīng)網(wǎng)絡(luò)中的梯度衰減問題,并更好地捕捉時間序列中時間步距離較大的依賴關(guān)系序仙。
6.8.1.4 隱藏狀態(tài)
有了記憶細胞以后突颊,接下來我們還可以通過輸出門來控制從記憶細胞到隱藏狀態(tài)的信息的流動:
這里的tanh函數(shù)確保隱藏狀態(tài)元素值在-1到1之間。需要注意的是诱桂,當輸出門近似1時洋丐,記憶細胞信息將傳遞到隱藏狀態(tài)供輸出層使用;當輸出門近似0時挥等,記憶細胞信息只自己保留。圖6.10展示了長短期記憶中隱藏狀態(tài)的計算堤尾。
6.8.2 讀取數(shù)據(jù)集
下面我們開始實現(xiàn)并展示長短期記憶肝劲。和前幾節(jié)中的實驗一樣,這里依然使用周杰倫歌詞數(shù)據(jù)集來訓(xùn)練模型作詞。
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.8.3 從零開始實現(xiàn)
我們先介紹如何從零開始實現(xiàn)長短期記憶辞槐。
6.8.3.1 初始化模型參數(shù)
下面的代碼對模型參數(shù)進行初始化掷漱。超參數(shù)num_hiddens
定義了隱藏單元的個數(shù)。
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)
def _three():
return (_one((num_inputs, num_hiddens)),
_one((num_hiddens, num_hiddens)),
torch.nn.Parameter(torch.zeros(num_hiddens, device=device, dtype=torch.float32), requires_grad=True))
W_xi, W_hi, b_i = _three() # 輸入門參數(shù)
W_xf, W_hf, b_f = _three() # 遺忘門參數(shù)
W_xo, W_ho, b_o = _three() # 輸出門參數(shù)
W_xc, W_hc, b_c = _three() # 候選記憶細胞參數(shù)
# 輸出層參數(shù)
W_hq = _one((num_hiddens, num_outputs))
b_q = torch.nn.Parameter(torch.zeros(num_outputs, device=device, dtype=torch.float32), requires_grad=True)
return nn.ParameterList([W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q])
6.8.4 定義模型
在初始化函數(shù)中榄檬,長短期記憶的隱藏狀態(tài)需要返回額外的形狀為(批量大小, 隱藏單元個數(shù))的值為0的記憶細胞卜范。
def init_lstm_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device),
torch.zeros((batch_size, num_hiddens), device=device))
下面根據(jù)長短期記憶的計算表達式定義模型。需要注意的是鹿榜,只有隱藏狀態(tài)會傳遞到輸出層海雪,而記憶細胞不參與輸出層的計算。
def lstm(inputs, state, params):
[W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q] = params
(H, C) = state
outputs = []
for X in inputs:
I = torch.sigmoid(torch.matmul(X, W_xi) + torch.matmul(H, W_hi) + b_i)
F = torch.sigmoid(torch.matmul(X, W_xf) + torch.matmul(H, W_hf) + b_f)
O = torch.sigmoid(torch.matmul(X, W_xo) + torch.matmul(H, W_ho) + b_o)
C_tilda = torch.tanh(torch.matmul(X, W_xc) + torch.matmul(H, W_hc) + b_c)
C = F * C + I * C_tilda
H = O * C.tanh()
Y = torch.matmul(H, W_hq) + b_q
outputs.append(Y)
return outputs, (H, C)
6.8.4.1 訓(xùn)練模型并創(chuàng)作歌詞
同上一節(jié)一樣舱殿,我們在訓(xùn)練模型時只使用相鄰采樣奥裸。設(shè)置好超參數(shù)后,我們將訓(xùn)練模型并根據(jù)前綴“分開”和“不分開”分別創(chuàng)作長度為50個字符的一段歌詞沪袭。
num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 40, 50, ['分開', '不分開']
我們每過40個迭代周期便根據(jù)當前訓(xùn)練的模型創(chuàng)作一段歌詞湾宙。
d2l.train_and_predict_rnn(lstm, get_params, init_lstm_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 40, perplexity 211.416571, time 1.37 sec
- 分開 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我
- 不分開 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我
epoch 80, perplexity 67.048346, time 1.35 sec
- 分開 我想你你 我不要再想 我不要這我 我不要這我 我不要這我 我不要這我 我不要這我 我不要這我 我不
- 不分開 我想你你想你 我不要這不樣 我不要這我 我不要這我 我不要這我 我不要這我 我不要這我 我不要這我
epoch 120, perplexity 15.552743, time 1.36 sec
- 分開 我想帶你的微笑 像這在 你想我 我想你 說你我 說你了 說給怎么么 有你在空 你在在空 在你的空
- 不分開 我想要你已經(jīng)堡 一樣樣 說你了 我想就這樣著你 不知不覺 你已了離開活 后知后覺 我該了這生活 我
epoch 160, perplexity 4.274031, time 1.35 sec
- 分開 我想帶你 你不一外在半空 我只能夠遠遠著她 這些我 你想我難難頭 一話看人對落我一望望我 我不那這
- 不分開 我想你這生堡 我知好煩 你不的節(jié)我 后知后覺 我該了這節(jié)奏 后知后覺 又過了一個秋 后知后覺 我該
6.8.5 簡潔實現(xiàn)
在Gluon中我們可以直接調(diào)用rnn
模塊中的LSTM
類。
lr = 1e-2 # 注意調(diào)整學(xué)習(xí)率
lstm_layer = nn.LSTM(input_size=vocab_size, hidden_size=num_hiddens)
model = d2l.RNNModel(lstm_layer, vocab_size)
d2l.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 40, perplexity 1.020401, time 1.54 sec
- 分開始想擔(dān) 媽跟我 一定是我媽在 因為分手前那句抱歉 在感動 穿梭時間的畫面的鐘 從反方向開始移動 回到
- 不分開始想像 媽跟我 我將我的寂寞封閉 然后在這里 不限日期 然后將過去 慢慢溫習(xí) 讓我愛上你 那場悲劇
epoch 80, perplexity 1.011164, time 1.34 sec
- 分開始想擔(dān) 你的 從前的可愛女人 溫柔的讓我心疼的可愛女人 透明的讓我感動的可愛女人 壞壞的讓我瘋狂的可
- 不分開 我滿了 讓我瘋狂的可愛女人 漂亮的讓我面紅的可愛女人 溫柔的讓我心疼的可愛女人 透明的讓我感動的可
epoch 120, perplexity 1.025348, time 1.39 sec
- 分開始共渡每一天 手牽手 一步兩步三步四步望著天 看星星 一顆兩顆三顆四顆 連成線背著背默默許下心愿 看
- 不分開 我不懂 說了沒用 他的笑容 有何不同 在你心中 我不再受寵 我的天空 是雨是風(fēng) 還是彩虹 你在操縱
epoch 160, perplexity 1.017492, time 1.42 sec
- 分開始鄉(xiāng)相信命運 感謝地心引力 讓我碰到你 漂亮的讓我面紅的可愛女人 溫柔的讓我心疼的可愛女人 透明的讓
- 不分開 我不能再想 我不 我不 我不能 愛情走的太快就像龍卷風(fēng) 不能承受我已無處可躲 我不要再想 我不要再
小結(jié)
- 長短期記憶的隱藏層輸出包括隱藏狀態(tài)和記憶細胞冈绊。只有隱藏狀態(tài)會傳遞到輸出層侠鳄。
- 長短期記憶的輸入門、遺忘門和輸出門可以控制信息的流動死宣。
- 長短期記憶可以應(yīng)對循環(huán)神經(jīng)網(wǎng)絡(luò)中的梯度衰減問題畦攘,并更好地捕捉時間序列中時間步距離較大的依賴關(guān)系。
6.9 深度循環(huán)神經(jīng)網(wǎng)絡(luò)
本章到目前為止介紹的循環(huán)神經(jīng)網(wǎng)絡(luò)只有一個單向的隱藏層十电,在深度學(xué)習(xí)應(yīng)用里知押,我們通常會用到含有多個隱藏層的循環(huán)神經(jīng)網(wǎng)絡(luò),也稱作深度循環(huán)神經(jīng)網(wǎng)絡(luò)鹃骂。圖6.11演示了一個有個隱藏層的深度循環(huán)神經(jīng)網(wǎng)絡(luò)台盯,每個隱藏狀態(tài)不斷傳遞至當前層的下一時間步和當前時間步的下一層。
具體來說畏线,在時間步里静盅,設(shè)小批量輸入
(樣本數(shù)為
,輸入個數(shù)為
)寝殴,第
隱藏層(
)的隱藏狀態(tài)為
(隱藏單元個數(shù)為
)蒿叠,輸出層變量為
(輸出個數(shù)為
),且隱藏層的激活函數(shù)為
蚣常。第1隱藏層的隱藏狀態(tài)和之前的計算一樣:
其中權(quán)重、
和偏差
分別為第1隱藏層的模型參數(shù)施绎。
當時溯革,第
隱藏層的隱藏狀態(tài)的表達式為
其中權(quán)重谷醉、
和偏差
分別為第
隱藏層的模型參數(shù)。
最終俱尼,輸出層的輸出只需基于第隱藏層的隱藏狀態(tài):
其中權(quán)重和偏差
為輸出層的模型參數(shù)遇八。
同多層感知機一樣矛绘,隱藏層個數(shù)和隱藏單元個數(shù)
都是超參數(shù)。此外押蚤,如果將隱藏狀態(tài)的計算換成門控循環(huán)單元或者長短期記憶的計算蔑歌,我們可以得到深度門控循環(huán)神經(jīng)網(wǎng)絡(luò)。
小結(jié)
- 在深度循環(huán)神經(jīng)網(wǎng)絡(luò)中揽碘,隱藏狀態(tài)的信息不斷傳遞至當前層的下一時間步和當前時間步的下一層次屠。
6.10 雙向循環(huán)神經(jīng)網(wǎng)絡(luò)
之前介紹的循環(huán)神經(jīng)網(wǎng)絡(luò)模型都是假設(shè)當前時間步是由前面的較早時間步的序列決定的,因此它們都將信息通過隱藏狀態(tài)從前往后傳遞雳刺。有時候劫灶,當前時間步也可能由后面時間步?jīng)Q定。例如掖桦,當我們寫下一個句子時本昏,可能會根據(jù)句子后面的詞來修改句子前面的用詞。雙向循環(huán)神經(jīng)網(wǎng)絡(luò)通過增加從后往前傳遞信息的隱藏層來更靈活地處理這類信息枪汪。圖6.12演示了一個含單隱藏層的雙向循環(huán)神經(jīng)網(wǎng)絡(luò)的架構(gòu)涌穆。
下面我們來介紹具體的定義。
給定時間步的小批量輸入
(樣本數(shù)為
雀久,輸入個數(shù)為
)和隱藏層激活函數(shù)為
宿稀。在雙向循環(huán)神經(jīng)網(wǎng)絡(luò)的架構(gòu)中,
設(shè)該時間步正向隱藏狀態(tài)為(正向隱藏單元個數(shù)為
)赖捌,
反向隱藏狀態(tài)為(反向隱藏單元個數(shù)為
)祝沸。我們可以分別計算正向隱藏狀態(tài)和反向隱藏狀態(tài):
其中權(quán)重罩锐、
、
卤唉、
和偏差
、
均為模型參數(shù)境氢。
然后我們連結(jié)兩個方向的隱藏狀態(tài)和
來得到隱藏狀態(tài)
蟀拷,并將其輸入到輸出層碰纬。輸出層計算輸出
(輸出個數(shù)為
):
其中權(quán)重和偏差
為輸出層的模型參數(shù)悦析。不同方向上的隱藏單元個數(shù)也可以不同寿桨。
小結(jié)
- 雙向循環(huán)神經(jīng)網(wǎng)絡(luò)在每個時間步的隱藏狀態(tài)同時取決于該時間步之前和之后的子序列(包括當前時間步的輸入)。