概述
循環(huán)神經(jīng)網(wǎng)絡(luò)(Recurrent Neural Network,RNN)是一種具有循環(huán)連接的神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu),被廣泛應(yīng)用于自然語言處理、語音識別、時序數(shù)據(jù)分析等任務(wù)中。相較于傳統(tǒng)神經(jīng)網(wǎng)絡(luò),RNN的主要特點在于它可以處理序列數(shù)據(jù),能夠捕捉到序列中的時序信息。
RNN的基本單元是一個循環(huán)單元(Recurrent Unit),它接收一個輸入和一個來自上一個時間步的隱藏狀態(tài),并輸出當(dāng)前時間步的隱藏狀態(tài)。在傳統(tǒng)的RNN中,循環(huán)單元通常使用tanh或ReLU等激活函數(shù)。
基本循環(huán)神經(jīng)網(wǎng)絡(luò)
原理
基本的 循環(huán)神經(jīng)網(wǎng)絡(luò),結(jié)構(gòu)由 輸入層、一個隱藏層和輸出層 組成。
x
x
x是輸入向量,
o
o
o是輸出向量,
s
s
s表示隱藏層的值;
U
U
U是輸入層到隱藏層的權(quán)重矩陣,
V
V
V是隱藏層到輸出層的權(quán)重矩陣。循環(huán)神經(jīng)網(wǎng)絡(luò)的隱藏層的值s不僅僅取決于當(dāng)前這次的輸入
x
x
x,還取決于上一次隱藏層的值
s
s
s。權(quán)重矩陣W就是隱藏層上一次的值作為這一次的輸入的權(quán)重。
將上圖的基本RNN結(jié)構(gòu)在時間維度展開(RNN是一個鏈式結(jié)構(gòu),每個時間片使用的是相同的參數(shù),t表示t時刻):
現(xiàn)在看上去就會清楚許多,這個網(wǎng)絡(luò)在t時刻接收到輸入
x
t
x_t
xt?之后,隱藏層的值是
s
t
s_t
st?,輸出值是
o
t
o_t
ot?。關(guān)鍵一點是
s
t
s_t
st?的值不僅僅取決于
x
t
x_t
xt?,還取決于
s
t
?
1
s_{t?1}
st?1?。
公式1:
s
t
=
f
(
U
?
x
t
+
W
?
s
t
?
1
+
B
1
)
s_t=f(U?x_t+W?s_{t?1}+B1)
st?=f(U?xt?+W?st?1?+B1)
公式2:
o
t
=
g
(
V
?
s
t
+
B
2
)
o_t=g(V?s_t+B2)
ot?=g(V?st?+B2)
- 式1是隱藏層的計算公式,它是循環(huán)層。U是輸入x的權(quán)重矩陣,W是上一次隱藏層值 S t ? 1 S_{t?1} St?1?作為這一次的輸入的權(quán)重矩陣,f是激活函數(shù)。
- 式2是輸出層的計算公式,V是輸出層的權(quán)重矩陣,g是激活函數(shù),B1,B2是偏置假設(shè)為0。
隱含層有兩個輸入,第一是U與 x t x_t xt?向量的乘積,第二是上一隱含層輸出的狀態(tài) s t ? 1 s_t?1 st??1和W的乘積。等于上一個時刻計算的 s t ? 1 s_t?1 st??1需要緩存一下,在本次輸入 x t x_t xt?一起計算,共同輸出最后的 o t o_t ot?。
如果反復(fù)把式1帶入式2,我們將得到:
從上面可以看出,循環(huán)神經(jīng)網(wǎng)絡(luò)的輸出值ot,是受前面歷次輸入值、、、、、、、、
x
t
x_t
xt?、
x
t
?
1
x_{t?1}
xt?1?、
x
t
?
2
x_{t?2}
xt?2?、
x
t
?
3
x_{t?3}
xt?3?、…影響的,這就是為什么循環(huán)神經(jīng)網(wǎng)絡(luò)可以往前看任意多個輸入值的原因。這樣其實不好,因為如果太前面的值和后面的值已經(jīng)沒有關(guān)系了,循環(huán)神經(jīng)網(wǎng)絡(luò)還考慮前面的值的話,就會影響后面值的判斷。
上面是整個單向單層NN的前向傳播過程
為了更快理解輸入x輸入格式下面使用nlp中Word Embedding講解下。
Word Embedding
首先我們需要對輸入文本x進行編碼,使之成為計算機可以讀懂的語言,在編碼時,我們期望句子之間保持詞語間的相似行,詞的向量表示是進行機器學(xué)習(xí)和深度學(xué)習(xí)的基礎(chǔ)。
word embedding的一個基本思路就是,我們把一個詞映射到語義空間的一個點,把一個詞映射到低維的稠密空間,這樣的映射使得語義上比較相似的詞,他在語義空間的距離也比較近,如果兩個詞的關(guān)系不是很接近,那么在語義空間中向量也會比較遠。
如上圖英語和西班牙語映射到語義空間,語義相同的數(shù)字他們在語義空間分布的位置是相同的
簡單回顧一下word embedding,對于nlp來說,我們輸入的是一個個離散的符號,對于神經(jīng)網(wǎng)絡(luò)來說,它處理的都是向量或者矩陣。所以第一步,我們需要把一個詞編碼成向量。最簡單的就是one-hot的表示方法。如下圖所示:
python代碼(one-hot),比如
import numpy as np
word_array = ['apple', 'kiwi', 'mango']
word_dict = {'apple': 0, 'banana': 1, 'orange': 2, 'grape': 3, 'melon': 4, 'peach': 5, 'pear': 6, 'kiwi': 7, 'plum': 8, 'mango': 9}
# 創(chuàng)建一個全為0的矩陣
one_hot_matrix = np.zeros((len(word_array), len(word_dict)))
# 對每個單詞進行one-hot編碼
for i, word in enumerate(word_array):
word_index = word_dict[word]
one_hot_matrix[i, word_index] = 1
print(one_hot_matrix)
輸出:
[[1. 0. 0. 0. 0. 0. 0. 0. 0. 0.] #這就是apple的one-hot編碼
[0. 0. 0. 0. 0. 0. 0. 1. 0. 0.] #這就是kiwi的one-hot編碼
[0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]] #這就是mango的one-hot編碼
行表示每個單詞,列表示語料庫,每個列對應(yīng)一個語料單詞,也就是特征列
雖然one-hot編碼是一種簡單有效的特征表示方法,但它也存在一些缺點:
-
高維度表示:使用one-hot編碼時,每個特征都需要創(chuàng)建一個很大的稀疏向量,維度與特征的唯一值數(shù)量相等。這會導(dǎo)致高維度的輸入數(shù)據(jù),增加了計算和存儲的開銷。特別是在處理具有大量離散特征的問題時,會導(dǎo)致非常龐大的特征空間。
-
維度獨立性:one-hot編碼將每個特征都表示為獨立的二進制特征,沒有考慮到特征之間的相關(guān)性和語義關(guān)系。這可能會導(dǎo)致模型難以捕捉到特征之間的相互作用和關(guān)聯(lián)性,從而影響了模型的性能。
-
無法處理未知特征:one-hot編碼要求特征的唯一值在訓(xùn)練集中都出現(xiàn)過,否則會出現(xiàn)問題。如果在測試集或?qū)嶋H應(yīng)用中遇到了未在訓(xùn)練集中出現(xiàn)的特征值,就無法進行one-hot編碼,這可能導(dǎo)致模型無法處理這些未知特征。
-
特征稀疏性:由于one-hot編碼的特征向量是稀疏的,大部分元素都是0,這會導(dǎo)致數(shù)據(jù)稀疏性增加,對于一些算法(如線性模型)可能會帶來一些問題。
綜上所述,盡管one-hot編碼在某些情況下是一種簡單有效的特征表示方法,但它也存在一些缺點,特別是在處理高維度離散特征、考慮特征間關(guān)系和處理未知特征值時可能會遇到問題。
使用nn.Embedding替代one-hot編碼的原因主要有兩點:
-
維度靈活性:使用one-hot編碼時,每個特征都需要創(chuàng)建一個很大的稀疏向量,維度與特征的唯一值數(shù)量相等。這會導(dǎo)致高維度的輸入,增加了計算和存儲的開銷。而使用嵌入(embedding)可以將離散特征映射為低維度的連續(xù)向量表示,減少了存儲和計算的成本。
-
語義關(guān)系和相似性:嵌入向量可以捕捉到特征之間的語義關(guān)系和相似性。例如,在自然語言處理任務(wù)中,使用嵌入向量可以將單詞映射為連續(xù)的向量表示,使得具有相似語義含義的單詞在嵌入空間中距離較近。這樣的特性可以幫助模型更好地理解和學(xué)習(xí)特征之間的關(guān)系,提升模型的性能。
因此,使用nn.Embedding替代one-hot編碼可以提高模型的效率和性能,特別是在處理高維度的離散特征時。
好的,我們來看一個簡單的例子來手推nn.embedding的兩個參數(shù)的作用。
假設(shè)我們有一個句子分類任務(wù),我們的輸入是一個句子,每個單詞都是一個特征。我們有5個不同的單詞,分別是[“I”, “l(fā)ove”, “deep”, “l(fā)earning”, “!” ]。
我們可以使用nn.embedding來將這些單詞映射為嵌入向量(在坐標系中有一個位置指向了這個單詞)。假設(shè)我們將每個單詞嵌入為一個3維的向量。這里,num_embeddings為5,表示我們有5個不同的單詞;embedding_dim為3,表示每個單詞嵌入為一個3維的向量。
我們可以用下面的表格來表示每個單詞的嵌入向量:
單詞 | 嵌入向量 |
---|---|
“I” | [0.1, 0.2, 0.3] |
“l(fā)ove” | [0.4, 0.5, 0.6] |
“deep” | [0.7, 0.8, 0.9] |
“l(fā)earning” | [0.2, 0.3, 0.4] |
“!” | [0.5, 0.6, 0.7] |
通過nn.embedding,我們可以將句子中的每個單詞轉(zhuǎn)換為對應(yīng)的嵌入向量。例如,句子"I love deep learning!"可以轉(zhuǎn)換為以下嵌入向量序列:
[[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9], [0.2, 0.3, 0.4], [0.5, 0.6, 0.7]]
這樣,我們就可以將離散的單詞特征轉(zhuǎn)換為連續(xù)的嵌入向量,在深度學(xué)習(xí)模型中使用。
以下是pytorch(python入門)的使用
# 創(chuàng)建詞匯表
vocab = {"I": 0, "love": 1, "deep": 2, "learning": 3, "!": 4}
strings=["I", "love", "deep", "learning", "!" ]
# 將字符串序列轉(zhuǎn)換為整數(shù)索引序列
input = t.LongTensor([vocab[word] for word in strings])
#注意第一個參數(shù)是詞匯表的個數(shù),并不是輸入單詞的長度,你在這里就算填100也不影響最終的輸出維度,這個輸入值影響的是算出來的行向量值
#nn.Embedding模塊會隨機初始化嵌入矩陣。在深度學(xué)習(xí)中,模型參數(shù)通常會使用隨機初始化的方法來開始訓(xùn)練,以便模型能夠在訓(xùn)練過程中學(xué)習(xí)到合適的參數(shù)值。
#在nn.Embedding中,嵌入矩陣的每個元素都會被隨機初始化為一個小的隨機值,這些值將作為模型在訓(xùn)練過程中學(xué)習(xí)的可訓(xùn)練參數(shù),可以使用manual_seed固定。
t.manual_seed(1234)
embedding=nn.Embedding(len(vocab),3)
print(embedding(input))
輸出結(jié)果為:
tensor([[-0.1117, -0.4966, 0.1631],
[-0.8817, 0.0539, 0.6684],
[-0.0597, -0.4675, -0.2153],
[ 0.8840, -0.7584, -0.3689],
[-0.3424, -1.4020, 0.3206]], grad_fn=)
注意Embedding第一個參數(shù)不是輸入的字符的長度,而是詞匯表的長度,比如有詞匯表
{“I”: 0, “l(fā)ove”: 1, “deep”: 2, “l(fā)earning”: 3, “!”: 4},而輸入input可能是:i love,此時應(yīng)該傳入的是5而不是2,因為預(yù)測最后隱藏層需要做個全連接用來預(yù)測當(dāng)前輸入單詞對于整個詞匯表的所有單詞的概率。
pytorch rnn
以下是pytorch使用rnn最簡單的一個例子,用來熟悉pytorch rnn
注意pytorch的rnn并不處理隱藏層到輸出層的邏輯,他只是關(guān)注隱藏層的輸出結(jié)果,如果需要將隱藏層轉(zhuǎn)換為結(jié)果輸出,可以在添加一個全連接層即可,這里暫不關(guān)注這部分
#%%
import torch
import torch.nn as nn
# 定義輸入數(shù)據(jù)
input_size = 10 # 輸入特征的維度
sequence_length = 5 # 時間步個數(shù)
batch_size = 3 # 批次大小
# 創(chuàng)建隨機輸入數(shù)據(jù)
#輸入數(shù)據(jù)的維度為(sequence_length, batch_size, input_size),表示有sequence_length個時間步,
#每個時間步有batch_size個樣本,每個樣本的特征維度為input_size。
input_data = torch.randn(sequence_length, batch_size, input_size)
print("輸入數(shù)據(jù)",input_data)
# 定義RNN模型
# 定義RNN模型時,我們指定了輸入特征的維度input_size、隱藏層的維度hidden_size、隱藏層的層數(shù)num_layers等參數(shù)。
# batch_first=False表示輸入數(shù)據(jù)的維度中批次大小是否在第一個維度,我們在第二個維度上。
rnn = nn.RNN(input_size, hidden_size=20, num_layers=1, batch_first=False)
"""
在前向傳播過程中,我們將輸入數(shù)據(jù)傳遞給RNN模型,并得到輸出張量output和最后一個時間步的隱藏狀態(tài)hidden。
輸出張量的大小為(sequence_length, batch_size, hidden_size),表示每個時間步的隱藏層輸出。
最后一個時間步的隱藏狀態(tài)的大小為(num_layers, batch_size, hidden_size)。
"""
# 前向傳播,第二個參數(shù)h0未傳遞,默認為0
output, hidden = rnn(input_data)
print("最后一個隱藏層",hidden.shape)
print("輸出所有隱藏層",output.shape)
# 打印每個隱藏層的權(quán)重和偏置項
# weight_ih表示輸入到隱藏層的權(quán)重,weight_hh表示隱藏層到隱藏層的權(quán)重,注意這里使出是轉(zhuǎn)置的結(jié)果。
# bias_ih表示輸入到隱藏層的偏置,bias_hh表示隱藏層到隱藏層的偏置。
for name, param in rnn.named_parameters():
if 'weight' in name or 'bias' in name:
print(name, param.data)
輸出
最后一個隱藏層 torch.Size([1, 3, 20])
輸出所有隱藏層 torch.Size([5, 3, 20])
權(quán)重為什么是10行20列參數(shù)卷積神經(jīng)網(wǎng)絡(luò)的原理
數(shù)據(jù)最外層的行的長度決定了前向傳播時間序列的個數(shù)。
這個input_size是輸入數(shù)據(jù)的維度,比如一個單詞轉(zhuǎn)換為one-hot后列就是字典的特征長度
這個hidden_size是隱藏層神經(jīng)元的個數(shù)也就是最終隱藏層輸入的特征數(shù)。
num_layer中是堆疊的多層隱藏層。
常見的結(jié)構(gòu)
RNN(循環(huán)神經(jīng)網(wǎng)絡(luò))常用的結(jié)果類型包括單輸入單輸出、單輸入多輸出、多輸入多輸出和多輸入單輸出。下面我將詳細解釋每種結(jié)果類型以及它們的應(yīng)用場景。
-
單輸入單輸出(Single Input Single Output,SISO):這是最常見的RNN結(jié)果類型,輸入是一個序列,輸出是一個單一的預(yù)測值。例如,給定一段文本,預(yù)測下一個詞語;給定一段時間序列數(shù)據(jù),預(yù)測下一個時間步的值。這種結(jié)果類型適用于許多序列預(yù)測任務(wù),如語言模型、時間序列預(yù)測等。
舉個例子,假設(shè)我們要預(yù)測房屋價格,可能會使用多個特征,如房屋的面積、臥室數(shù)量、浴室數(shù)量等。這樣,我們可以將這些特征組合成一個特征向量作為模型的輸入,而模型的輸出則是預(yù)測的房屋價格。因此,線性回歸可以用來解決多特征到單個輸出的問題,因此被稱為單輸入單輸出模型。 -
單輸入多輸出(Single Input Multiple Output,SIMO):這種結(jié)果類型中,輸入是一個序列,但輸出是多個預(yù)測值。例如,給定一段文本,同時預(yù)測下一個詞語和該詞語的詞性標簽;給定一段音頻信號,同時預(yù)測語音情感和說話者身份。這種結(jié)果類型適用于需要同時預(yù)測多個相關(guān)任務(wù)的情況。
-
多輸入多輸出(Multiple Input Multiple Output,MIMO):這種結(jié)果類型中,有多個輸入序列和多個輸出序列。例如,在機器翻譯任務(wù)中,輸入是源語言的句子序列,輸出是目標語言的句子序列;在對話系統(tǒng)中,輸入是用戶的問題序列,輸出是系統(tǒng)的回答序列。這種結(jié)果類型適用于需要處理多個輸入和輸出序列的任務(wù),mimo有兩種一種輸入和輸出個數(shù)相等和不相等。
-
多輸入單輸出(Multiple Input Single Output,MISO):這種結(jié)果類型中,有多個輸入序列,但只有一個輸出。例如,在圖像描述生成任務(wù)中,輸入是圖像序列,輸出是對圖像的描述;在自動駕駛中,輸入是多個傳感器的數(shù)據(jù)序列,輸出是車輛的控制命令。這種結(jié)果類型適用于需要將多個輸入序列映射到單個輸出序列的任務(wù)。
線性回歸是一種簡單的機器學(xué)習(xí)模型,它的輸入可以是多個特征,但是輸出只有一個。這里的“單輸入單輸出”是指模型的輸入是一個向量(多個特征的組合),輸出是一個標量(一個預(yù)測值)。在線性回歸中,我們通過對輸入特征進行線性組合,得到一個預(yù)測值。因此,盡管輸入可以是多個元素,但輸出只有一個。
雙向循環(huán)神經(jīng)網(wǎng)絡(luò)
普通的RNN只能依據(jù)之前時刻的時序信息來預(yù)測下一時刻的輸出,但在有些問題中,當(dāng)前時刻的輸出不僅和之前的狀態(tài)有關(guān),還可能和未來的狀態(tài)有關(guān)系。
比如預(yù)測一句話中缺失的單詞不僅需要根據(jù)前文來判斷,還需要考慮它后面的內(nèi)容,真正做到基于上下文判斷。
BRNN有兩個RNN上下疊加在一起組成的,輸出由這兩個RNN的狀態(tài)共同決定。
先對圖片和公式中的符號集中說明,需要時方便查看:
- h t 1 h_t^1 ht1?表示t 時刻,Cell1 中從左到右獲得的 memory(信息);
- W 1 , U 1 W^1,U^1 W1,U1 表示圖中 Cell1 的可學(xué)習(xí)參數(shù),W是隱藏層的參數(shù)U是輸入層參數(shù);
- f 1 f_1 f1? 表示 Cell1 的激活函數(shù);
- h t 2 h_t^2 ht2? 表示 t 時刻,Cell2 中從右到左獲得的 memory;
- W 2 W^2 W2, U 2 U^2 U2 表示圖中 Cell2 的可學(xué)習(xí)參數(shù);
- f 2 f_2 f2? 表示 Cell2 的激活函數(shù);
- V V V 是輸出層的參數(shù),可以理解為 MLP;
- f 3 f_3 f3? 是輸出層的激活函數(shù);
- y t y_t yt? 是 t 時刻的輸出值;
在圖1-1中,對于 t 時刻的輸入
x
t
x_t
xt? ,可以結(jié)合從左到右的 memory
h
t
?
1
1
h^1_{t-1}
ht?11? , 獲得當(dāng)前時刻的 memory
h
t
1
h^1_t
ht1?:
同理也可以結(jié)合從右到左的 memory
h
t
?
1
2
h^2_{t?1}
ht?12? , 獲得當(dāng)前時刻的 memory
h
t
2
h^2_t
ht2?:
然后將
h
t
1
h^1_t
ht1? 和
h
t
2
h^2_t
ht2? 首尾級聯(lián)在一起通過輸出層網(wǎng)絡(luò)
V
V
V 得到輸出
y
t
y_t
yt? :
這樣對于任何一個時刻 t 可以看到從不同方向獲得的 memory, 使模型更容易優(yōu)化,加速了模型的收斂速度。
pytorch rnn
下面是一個使用PyTorch中nn.RNN模塊實現(xiàn)雙向RNN的最簡單例子:
import torch
import torch.nn as nn
# 定義輸入數(shù)據(jù)
input_size = 10 # 輸入特征的維度
sequence_length = 5 # 時間步個數(shù)
batch_size = 3 # 批次大小
# 創(chuàng)建隨機輸入數(shù)據(jù)
input_data = torch.randn(sequence_length, batch_size, input_size)
# 定義雙向RNN模型
rnn = nn.RNN(input_size, hidden_size=20, num_layers=1, batch_first=False, bidirectional=True)
# 前向傳播
output, hidden = rnn(input_data)
# 輸出結(jié)果
print("輸出張量大?。?, output.size())
print("最后一個時間步的隱藏狀態(tài)大?。?, hidden.size())
輸出
輸出張量大小: torch.Size([5, 3, 40])
最后一個時間步的隱藏狀態(tài)大?。?torch.Size([2, 3, 20])
這個例子中,輸入數(shù)據(jù)的維度和之前的例子相同。
定義雙向RNN模型時,我們在RNN模型的參數(shù)中設(shè)置bidirectional=True,表示我們希望構(gòu)建一個雙向RNN模型。
在前向傳播過程中,我們將輸入數(shù)據(jù)傳遞給雙向RNN模型,并得到輸出張量output和最后一個時間步的隱藏狀態(tài)hidden。輸出張量的大小為(sequence_length, batch_size, hidden_sizenum_directions),其中num_directions為2,表示正向和反向兩個方向。最后一個時間步的隱藏狀態(tài)的大小為(num_layersnum_directions, batch_size, hidden_size)。
雙向RNN可以同時利用過去和未來的信息,可以更好地捕捉到時間序列數(shù)據(jù)中的特征。你可以根據(jù)需要調(diào)整輸入數(shù)據(jù)的大小、RNN模型的參數(shù)等進行實驗。
雙向RNN的輸出通常是正向和反向隱藏狀態(tài)的組合,它們被存儲在一個數(shù)組中。具體來說,如果使用PyTorch中的nn.RNN模塊實現(xiàn)雙向RNN,輸出張量的形狀將是(sequence_length,batch_size, hidden_size * 2),其中hidden_size * 2表示正向和反向隱藏狀態(tài)的大小之和。這個輸出張量包含了每個時間步上正向和反向隱藏狀態(tài)的信息,可以在后續(xù)的任務(wù)中使用。
雙向rnn的最后的隱藏層大小是(2, batch_size, hidden_size)
Deep RNN(多層 RNN)
前文我們介紹的 RNN,是數(shù)據(jù)在時間維度上的變換。不論時間維度多長,只有一個 RNN 模塊, 即只有一組待學(xué)習(xí)參數(shù) (W, U),屬于單層 RNN。deep RNN 也叫做多層 RNN,顧名思義它由多個 RNN 級聯(lián)組成,是輸入數(shù)據(jù)在空間維度上變換。如圖, 這是 L 層的 RNN 架構(gòu)。每一層是一個單獨的RNN,共有L個RNN。
在每一層的水平方向,只有一組可學(xué)習(xí)參數(shù),如第
l
l
l 層的參數(shù)
W
l
U
l
W^lU^l
WlUl。水平方向是數(shù)據(jù)沿著時間維度變換,變換機制與單個 RNN 的機制一致,具體參考式上一篇文章。在每個時刻 t 的垂直方向,共有 L 組可學(xué)習(xí)參數(shù)(
W
i
,
U
i
W^i,U^i
Wi,Ui) i = 1, 2, …, L。在第
l
l
l 層的第 t 時刻 Cell 的輸入數(shù)據(jù)來自 2 個方向:一個是來自上一層的輸出
h
t
l
?
1
h^{l?1}_t
htl?1? :
一個是來自第
l
l
l 層,
t
?
1
t ? 1
t?1 時刻的 memory 數(shù)據(jù)
h
t
?
1
l
h^l_{t?1}
ht?1l? :
所以 Cell 的輸出
h
t
l
h^l_t
htl?:
本質(zhì)上,Deep RNN 在單個 RNN 的基礎(chǔ)上,將當(dāng)前時刻的輸入修改為上層的輸出。這樣 RNN 便完成了空間上的數(shù)據(jù)變換。額外提一下:DeepRNN的每一層也可以是一個雙向RNN。
pytorch rnn
下面是一個使用nn.RNN模塊實現(xiàn)多層RNN的最簡單例子:
import torch
import torch.nn as nn
# 定義輸入數(shù)據(jù)和參數(shù)
input_size = 5
hidden_size = 10
num_layers = 2
batch_size = 3
sequence_length = 4
# 創(chuàng)建輸入張量
input_tensor = torch.randn(sequence_length, batch_size, input_size)
# 創(chuàng)建多層RNN模型
rnn = nn.RNN(input_size, hidden_size, num_layers)
# 前向傳播
output, hidden = rnn(input_tensor)
# 打印輸出張量和隱藏狀態(tài)的大小
print("Output shape:", output.shape)
print("Hidden state shape:", hidden.shape)
在上面的例子中,我們首先定義了輸入數(shù)據(jù)的維度、RNN模型的參數(shù)(輸入大小、隱藏狀態(tài)大小和層數(shù)),以及批次大小和序列長度。然后,我們創(chuàng)建了一個輸入張量,其形狀為(sequence_length, batch_size, input_size)。接下來,我們使用nn.RNN模塊創(chuàng)建一個多層RNN模型,其中包含兩層。最后,我們通過將輸入張量傳遞給RNN模型的前向方法來進行前向傳播,并打印輸出張量和隱藏狀態(tài)的大小。
請注意,輸出張量的形狀為(sequence_length, batch_size, hidden_size),其中sequence_length和batch_size保持不變,hidden_size是隱藏狀態(tài)的大小。隱藏狀態(tài)的形狀為(num_layers, batch_size, hidden_size),其中num_layers是RNN模型的層數(shù)。
RNN缺點
梯度爆炸和消失問題
實踐中前面介紹的幾種RNNs并不能很好的處理較長的序列,RNN在訓(xùn)練中很容易發(fā)生梯度爆炸和梯度消失,這導(dǎo)致梯度不能在較長序列中一直傳遞下去,從而使RNN無法捕捉到長距離的影響。
通常來說,梯度爆炸更容易處理一些。因為梯度爆炸的時候,我們的程序會收到NaN錯誤。我們也可以設(shè)置一個梯度閾值,當(dāng)梯度超過這個閾值的時候可以直接截取。
梯度消失更難檢測,而且也更難處理一些??偟膩碚f,我們有三種方法應(yīng)對梯度消失問題:
1、合理的初始化權(quán)重值。初始化權(quán)重,使每個神經(jīng)元盡可能不要取極大或極小值,以躲開梯度消失的區(qū)域。
2、使用relu代替sigmoid和tanh作為激活函數(shù)。。
3、使用其他結(jié)構(gòu)的RNNs,比如長短時記憶網(wǎng)絡(luò)(LTSM)和Gated Recurrent Unit(GRU),這是最流行的做法
短期記憶
假如需要判斷用戶的說話意圖(問天氣、問時間、設(shè)置鬧鐘…),用戶說了一句“what time is it?”我們需要先對這句話進行分詞:
然后按照順序輸入 RNN ,我們先將 “what”作為 RNN 的輸入,得到輸出「01」
然后,我們按照順序,將“time”輸入到 RNN 網(wǎng)絡(luò),得到輸出「02」。
這個過程我們可以看到,輸入 “time” 的時候,前面 “what” 的輸出也產(chǎn)生了影響(隱藏層中有一半是黑色的)。
以此類推,前面所有的輸入都對未來的輸出產(chǎn)生了影響,大家可以看到圓形隱藏層中包含了前面所有的顏色。如下圖所示:
當(dāng)我們判斷意圖的時候,只需要最后一層的輸出「05」,如下圖所示:
RNN 的缺點也比較明顯
通過上面的例子,我們已經(jīng)發(fā)現(xiàn),短期的記憶影響較大(如橙色區(qū)域),但是長期的記憶影響就很?。ㄈ绾谏途G色區(qū)域),這就是 RNN 存在的短期記憶問題。
- RNN 有短期記憶問題,無法處理很長的輸入序列
- 訓(xùn)練 RNN 需要投入極大的成本
RNN 的優(yōu)化算法
LSTM – 長短期記憶網(wǎng)絡(luò)
RNN 是一種死板的邏輯,越晚的輸入影響越大,越早的輸入影響越小,且無法改變這個邏輯。
LSTM 做的最大的改變就是打破了這個死板的邏輯,而改用了一套靈活了邏輯——只保留重要的信息。
簡單說就是:抓重點!
舉個例子,我們先快速的閱讀下面這段話:
當(dāng)我們快速閱讀完之后,可能只會記住下面幾個重點:
LSTM 類似上面的劃重點,他可以保留較長序列數(shù)據(jù)中的「重要信息」,忽略不重要的信息。這樣就解決了 RNN 短期記憶的問題。
原理
原始RNN的隱藏層只有一個狀態(tài),即h,它對于短期的輸入非常敏感。那么如果我們再增加一個門(gate)機制用于控制特征的流通和損失,即c,讓它來保存長期的狀態(tài),這就是長短時記憶網(wǎng)絡(luò)(Long Short Term Memory,LSTM)。
新增加的狀態(tài)c,稱為單元狀態(tài)。我們把LSTM按照時間維度展開:
其中圖像上的標識
σ
\sigma
σ標識使用sigmod激活到[0-1],
tanh
?
\tanh
tanh激活到[-1,1]
? 是一個數(shù)學(xué)符號,表示逐元素乘積(element-wise product)或哈達瑪積(Hadamard product)。當(dāng)兩個相同維度的矩陣、向量或張量進行逐元素相乘時,可以使用 ? 符號來表示。
例如,對于兩個向量 [a1,a2,a3] ? [b1, b2, b3]=[a1b1,a2b2,a3*b3],它們的逐元素乘積可以表示
可以看到在t時刻,
LSTM的輸入有三個:當(dāng)前時刻網(wǎng)絡(luò)的輸出值 x t x_t xt?、上一時刻LSTM的輸出值 h t ? 1 h_{t?1} ht?1?、以及上一時刻的記憶單元向量 c t ? 1 c_{t?1} ct?1?;
LSTM的輸出有兩個:當(dāng)前時刻LSTM輸出值 h t h_t ht?、當(dāng)前時刻的隱藏狀態(tài)向量 h t h_t ht?、和當(dāng)前時刻的記憶單元狀態(tài)向量 c t c_t ct?。
注意:記憶單元c在LSTM 層內(nèi)部結(jié)束工作,不向其他層輸出。LSTM的輸出僅有隱藏狀態(tài)向量h。
LSTM 的關(guān)鍵是單元狀態(tài),即貫穿圖表頂部的水平線,有點像傳送帶。這一部分一般叫做單元狀態(tài)(cell state)它自始至終存在于LSTM的整個鏈式系統(tǒng)中。
遺忘門
f
t
f_t
ft?叫做遺忘門,表示
C
t
?
1
C_{t?1}
Ct?1?的哪些特征被用于計算
C
t
C_t
Ct?。
f
t
f_t
ft?是一個向量,向量的每個元素均位于(0~1)范圍內(nèi)。通常我們使用 sigmoid 作為激活函數(shù),sigmoid 的輸出是一個介于于(0~1)區(qū)間內(nèi)的值,但是當(dāng)你觀察一個訓(xùn)練好的LSTM時,你會發(fā)現(xiàn)門的值絕大多數(shù)都非常接近0或者1,其余的值少之又少。
輸入門
C
t
C_t
Ct? 表示單元狀態(tài)更新值,由輸入數(shù)據(jù)
x
t
x_t
xt?和隱節(jié)點
h
t
?
1
h_{t?1}
ht?1?經(jīng)由一個神經(jīng)網(wǎng)絡(luò)層得到,單元狀態(tài)更新值的激活函數(shù)通常使用tanh。
i
t
i_t
it?叫做輸入門,同
f
t
f_t
ft? 一樣也是一個元素介于(0~1)區(qū)間內(nèi)的向量,同樣由
x
t
x_t
xt?和
h
t
?
1
h_{t?1}
ht?1?經(jīng)由sigmoid激活函數(shù)計算而成
輸出門
最后,為了計算預(yù)測值
y
t
y^t
yt和生成下個時間片完整的輸入,我們需要計算隱節(jié)點的輸出
h
t
h_t
ht?。
lstm寫詩
首先我們研究下pytorch中l(wèi)stm的用法
單層lstm
sequence_length =3
batch_size =2
input_size =4
#這里如果是輸入比如[張三,李四,王五],一般實際使用需要通過embedding后生成一個[時間步是3,批量1(這里是1,但是如果是真實數(shù)據(jù)集可能有分批處理,就是實際的批次值),3(三個值的坐標表示一個張三或者李四)]
input=t.randn(sequence_length,batch_size,input_size)
lstmModel=nn.LSTM(input_size,3,1)
#其中,output是RNN每個時間步的輸出,hidden是最后一個時間步的隱藏狀態(tài)。
output, (h, c) =lstmModel(input)
#因為是3個時間步,每個時間步都有一個隱藏層,每個隱藏層都有2條數(shù)據(jù),隱藏層的維度是3,最終(3,2,3)
print("LSTM隱藏層輸出的維度",output.shape)
#
print("LSTM隱藏層最后一個時間步輸出的維度",h.shape)
print("LSTM隱藏層最后一個時間步細胞狀態(tài)",c.shape)
輸出
LSTM隱藏層輸出的維度 torch.Size([3, 2, 3])
LSTM隱藏層最后一個時間步輸出的維度 torch.Size([1, 2, 3])
LSTM隱藏層最后一個時間步細胞狀態(tài) torch.Size([1, 2, 3])
雙層lstm
sequence_length =3
batch_size =2
input_size =4
input=t.randn(sequence_length,batch_size,input_size)
lstmModel=nn.LSTM(input_size,3,num_layers=2)
#其中,output是RNN每個時間步的輸出,hidden是最后一個時間步的隱藏狀態(tài)。
output, (h, c) =lstmModel(input)
print("2層LSTM隱藏層輸出的維度",output.shape)
print("2層LSTM隱藏層最后一個時間步輸出的維度",h.shape)
print("2層LSTM隱藏層最后一個時間步細胞狀態(tài)",c.shape)
輸出:
2層LSTM隱藏層輸出的維度 torch.Size([3, 2, 3])
2層LSTM隱藏層最后一個時間步輸出的維度 torch.Size([2, 2, 3])
2層LSTM隱藏層最后一個時間步細胞狀態(tài) torch.Size([2, 2, 3])
2層的話輸出的是最后一層的隱藏層的輸出,h,c是一個時間步就有兩層的隱藏層和記憶細胞
開始寫詩的例子
這是項目的目錄結(jié)構(gòu)
加載數(shù)據(jù)
實驗數(shù)據(jù)來自Github上中文愛好者收集的5萬多首唐詩,作者在此基礎(chǔ)上進行了一些數(shù)據(jù)處理,由于數(shù)據(jù)處理很耗時間,且不是pytorch學(xué)習(xí)的重點,這里省略。作者提供了一個numpy的壓縮包tang.npz,下載地址
數(shù)據(jù)具體結(jié)構(gòu)可參考,以下代碼main部分
from torch.utils.data import Dataset,DataLoader
import numpy as np
class PoetryDataset(Dataset):
def __init__(self,root):
self.data=np.load(root, allow_pickle=True)
def __len__(self):
return len(self.data["data"])
def __getitem__(self, index):
return self.data["data"][index]
def getData(self):
return self.data["data"],self.data["ix2word"].item(),self.data["word2ix"].item()
if __name__=="__main__":
datas=PoetryDataset("./tang.npz").data
# data是一個57580 * 125的numpy數(shù)組,即總共有57580首詩歌,每首詩歌長度為125個字符(不足125補空格,超過125的丟棄)
print(datas["data"].shape)
#這里都字符已經(jīng)轉(zhuǎn)換成了索引
print(datas["data"][0])
# 使用item將numpy轉(zhuǎn)換為字典類型,ix2word存儲這下標對應(yīng)的字,比如{0: '憁', 1: '耀'}
ix2word = datas['ix2word'].item()
print(ix2word)
# word2ix存儲這字對應(yīng)的小標,比如{'憁': 0, '耀': 1}
word2ix = datas['word2ix'].item()
print(word2ix)
# 將某一首古詩轉(zhuǎn)換為索引表示,轉(zhuǎn)換后:[5272, 4236, 3286, 6933, 6010, 7066, 774, 4167, 2018, 70, 3951]
str="床前明月光,疑是地上霜"
print([word2ix[i] for i in str])
#將第一首古詩打印出來
print([ix2word[i] for i in datas["data"][0]])
定義模型
import torch.nn as nn
class Net(nn.Module):
"""
:param vocab_size 表示輸入單詞的格式
:param embedding_dim 表示將一個單詞映射到embedding_dim維度空間
:param hidden_dim 表示lstm輸出隱藏層的維度
"""
def __init__(self, vocab_size, embedding_dim, hidden_dim):
super(Net, self).__init__()
self.hidden_dim = hidden_dim
#Embedding層,將單詞映射成vocab_size行embedding_dim列的矩陣,一行的坐標代表第一行的詞
self.embeddings = nn.Embedding(vocab_size, embedding_dim)
#兩層lstm,輸入詞向量的維度和隱藏層維度
self.lstm = nn.LSTM(embedding_dim, self.hidden_dim, num_layers=2, batch_first=False)
#最后將隱藏層的維度轉(zhuǎn)換為詞匯表的維度
self.linear1 = nn.Linear(self.hidden_dim, vocab_size)
def forward(self, input, hidden=None):
#獲取輸入的數(shù)據(jù)的時間步和批次數(shù)
seq_len, batch_size = input.size()
#如果沒有傳入上一個時間的隱藏值,初始一個,注意是2層
if hidden is None:
h_0 = input.data.new(2, batch_size, self.hidden_dim).fill_(0).float()
c_0 = input.data.new(2, batch_size, self.hidden_dim).fill_(0).float()
else:
h_0, c_0 = hidden
#將輸入的數(shù)據(jù)embeddings為(input行數(shù),embedding_dim)
embeds = self.embeddings(input) # (seq_len, batch_size, embedding_dim), (1,1,128)
output, hidden = self.lstm(embeds, (h_0, c_0)) #(seq_len, batch_size, hidden_dim), (1,1,256)
output = self.linear1(output.view(seq_len*batch_size, -1)) # ((seq_len * batch_size),hidden_dim), (1,256) → (1,8293)
return output, hidden
訓(xùn)練
下述代碼:input, target = (data[:-1, :]), (data[1:, :])解釋:
在使用LSTM進行詞預(yù)測時,輸入和標簽的設(shè)置是為了將輸入序列和目標序列對齊。
在語言模型中,我們希望根據(jù)前面的單詞來預(yù)測下一個單詞。因此,輸入序列是前面的單詞,而目標序列是下一個單詞。
考慮以下例子:
假設(shè)我們有一個句子:“I love deep learning.”
我們可以將其分解為以下形式的輸入和目標序列:
輸入序列:[“I”, “l(fā)ove”, “deep”]
目標序列:[“l(fā)ove”, “deep”, “l(fā)earning”]
在這個例子中,輸入序列是前面的單詞[“I”, “l(fā)ove”, “deep”],而目標序列是相應(yīng)的下一個單詞[“l(fā)ove”, “deep”, “l(fā)earning”]。
在代碼中,data是一個包含所有單詞的數(shù)據(jù)集,其中每一行代表一個單詞。將data切片為input和target時,我們使用data[:-1, :]作為輸入序列,即除了最后一個單詞。而data[1:, :]作為目標序列,即從第二個單詞開始。
這樣設(shè)置輸入和目標序列的目的是為了將輸入和標簽對齊,使得模型可以根據(jù)前面的單詞來預(yù)測下一個單詞。
import fire
import torch.nn as nn
import torch as t
from data.dataset import PoetryDataset
from models.model import Net
num_epochs=5
data_root="./data/tang.npz"
batch_size=10
def train(**kwargs):
datasets=PoetryDataset(data_root)
data,ix2word,word2ix=datasets.getData()
lenData=len(data)
data = t.from_numpy(data)
dataloader = t.utils.data.DataLoader(data, batch_size=batch_size, shuffle=True, num_workers=1)
#總共有8293的詞。模型定義:vocab_size, embedding_dim, hidden_dim = 8293 * 128 * 256
model=Net(len(word2ix),128,256)
#定義損失函數(shù)
criterion = nn.CrossEntropyLoss()
model=model.cuda()
optimizer = t.optim.Adam(model.parameters(), lr=1e-3)
iteri=0
filename = "example.txt"
totalIter=lenData*num_epochs/batch_size
for epoch in range(num_epochs): # 最大迭代次數(shù)為8
for i, data in enumerate(dataloader): # 一批次數(shù)據(jù) 128*125
data = data.long().transpose(0,1).contiguous() .cuda()
optimizer.zero_grad()
input, target = (data[:-1, :]), (data[1:, :])
output, _ = model(input)
loss = criterion(output, target.view(-1)) # torch.Size([15872, 8293]), torch.Size([15872])
loss.backward()
optimizer.step()
iteri+=1
if(iteri%500==0):
print(str(iteri+1)+"/"+str(totalIter)+"epoch")
if (1 + i) % 1000 == 0: # 每575個batch可視化一次
with open(filename, "a") as file:
file.write(str(i) + ':' + generate(model, '床前明月光', ix2word, word2ix)+"\n")
t.save(model.state_dict(), './checkpoints/model_poet_2.pth')
def generate(model, start_words, ix2word, word2ix): # 給定幾個詞,根據(jù)這幾個詞生成一首完整的詩歌
txt = []
for word in start_words:
txt.append(word)
input = t.Tensor([word2ix['<START>']]).view(1,1).long() # tensor([8291.]) → tensor([[8291.]]) → tensor([[8291]])
input = input.cuda()
hidden = None
num = len(txt)
for i in range(48): # 最大生成長度
output, hidden = model(input, hidden)
if i < num:
w = txt[i]
input = (input.data.new([word2ix[w]])).view(1, 1)
else:
top_index = output.data[0].topk(1)[1][0]
w = ix2word[top_index.item()]
txt.append(w)
input = (input.data.new([top_index])).view(1, 1)
if w == '<EOP>':
break
return ''.join(txt)
if __name__=="__main__":
fire.Fire()
5epoch,10batch,普通pc,GTX1050,2GB顯存,訓(xùn)練時間30分鐘。
50epoch 128batch colab免費gpu 16GB顯存,訓(xùn)練時間1小時
測試
def test():
datasets = PoetryDataset(data_root)
data, ix2word, word2ix = datasets.getData()
modle = Net(len(word2ix), 128, 256) # 模型定義:vocab_size, embedding_dim, hidden_dim —— 8293 * 128 * 256
if t.cuda.is_available() == True:
modle.cuda()
modle.load_state_dict(t.load('./checkpoints/model_poet_2.pth'))
modle.eval()
name = input("請輸入您的開頭:")
txt = generate(modle, name, ix2word, word2ix)
print(txt)
由于才訓(xùn)練了5epoch效果不是太好,可視化loss后可多次epoch看看效果,還有個問題,如果輸入不變,生成的結(jié)果就是相同的,所以這個可能需要一個噪聲干擾。
5epoch版本效果
(env380) D:\code\deeplearn\learn_rnn\pytorch\4.nn模塊\案例\生成古詩\tang>python main.py test
請輸入您的開頭:唧唧復(fù)唧唧
唧唧復(fù)唧唧,不知何所如?君不見此地,不如此中生。一朝一杯酒,一日相追尋。一朝一杯酒,一醉一相逢。
(env380) D:\code\deeplearn\learn_rnn\pytorch\4.nn模塊\案例\生成古詩\tang>python main.py test
請輸入您的開頭:我兒小謙謙
我兒小謙謙,不是天地間。有時有所用,不是無為名。有時有所用,不是無生源。有時有所用,不是無生源。
50epoch版本效果
(env380) D:\code\deeplearn\learn_rnn\pytorch\4.nn模塊\案例\生成古詩\tang>python main.py test
請輸入您的開頭:我家小謙謙
我家小謙謙,今古何為郎。我生不相識,我心不可忘。我來不我見,我亦不得嘗。君今不我見,我亦不足傷。
(env380) D:\code\deeplearn\learn_rnn\pytorch\4.nn模塊\案例\生成古詩\tang>python main.py test
請輸入您的開頭:床前明月光
床前明月光,上客不可見。玉樓金閣深,玉瑟風(fēng)光緊。玉指滴芭蕉,飄飄出羅幕。玉堂無塵埃,玉節(jié)凌風(fēng)雷。
(env380) D:\code\deeplearn\learn_rnn\pytorch\4.nn模塊\案例\生成古詩\tang>python main.py test
請輸入您的開頭:唧唧復(fù)唧唧
唧唧復(fù)唧唧,胡兒女卿侯。妾本邯鄲道,相逢兩不游。妾心不可再,妾意不能休。妾本不相見,妾心如有鉤。
GRU
Gated Recurrent Unit – GRU 是 LSTM 的一個變體。他保留了 LSTM 劃重點,遺忘不重要信息的特點,在long-term 傳播的時候也不會被丟失。
LSTM 的參數(shù)太多,計算需要很長時間。因此,最近業(yè)界又提出了 GRU(Gated RecurrentUnit,門控循環(huán)單元)。GRU 保留了 LSTM使用門的理念,但是減少了參數(shù),縮短了計算時間。
相對于 LSTM 使用隱藏狀態(tài)和記憶單元兩條線,GRU只使用隱藏狀態(tài)。異同點如下:
GRU的計算圖
GRU計算圖,σ節(jié)點和tanh節(jié)點有專用的權(quán)重,節(jié)點內(nèi)部進行仿射變換(“1?”節(jié)點輸入x,輸出1 ? x)
GRU 中進行的計算由上述 4 個式子表示(這里 xt和 ht?1 都是行向量),如圖所示,GRU 沒有記憶單元,只有一個隱藏狀態(tài)h在時間方向上傳播。這里使用r和z共兩個門(LSTM 使用 3 個門),r稱為 reset 門,z稱為 update 門。
r(reset門)**決定在多大程度上“忽略”過去的隱藏狀態(tài)。根據(jù)公式2.3,如果r是 0,則新的隱藏狀態(tài)h~僅取決于輸入 x t x_t xt?。也就是說,此時過去的隱藏狀態(tài)將完全被忽略。文章來源:http://www.zghlxwxcb.cn/news/detail-501819.html
z(update門)**是更新隱藏狀態(tài)的門,它扮演了 LSTM 的 forget 門和input 門兩個角色。公式2.4 的(1?z)⊙ h t ? 1 h_{t?1} ht?1?部分充當(dāng) forget 門的功能,從過去的隱藏狀態(tài)中刪除應(yīng)該被遺忘的信息。z⊙ h ? h^~ h?的部分充當(dāng) input 門的功能,對新增的信息進行加權(quán)。文章來源地址http://www.zghlxwxcb.cn/news/detail-501819.html
到了這里,關(guān)于深度學(xué)習(xí)05-CNN循環(huán)神經(jīng)網(wǎng)絡(luò)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!