本文參考參考,在此基礎上對RNN所能實作的内容做一個簡述。
RNN單元與網絡

循環神經網絡是能夠根據前文的内容去預測下文的,RNN的單元結構圖如上。圖中的a代表上一個時間步長的隐藏内容,Waa代表隐藏狀态的權重,x代表目前時間步長的輸入,Wax代表輸入權重,Wya代表輸出層權重。根據圖中公式連起來得到一個循環神經網絡
RNN單元建構:注意中間的次元比對
def rnn_cell_forward(xt, a_prev, parameters):
"""
參數:
xt -- 時間步“t”輸入的資料,次元為(n_x, m)
a_prev -- 時間步“t - 1”的隐藏隐藏狀态,次元為(n_a, m)
parameters -- 字典,包含了以下内容:
Wax -- 矩陣,輸入乘以權重,次元為(n_a, n_x)
Waa -- 矩陣,隐藏狀态乘以權重,次元為(n_a, n_a)
Wya -- 矩陣,隐藏狀态與輸出相關的權重矩陣,次元為(n_y, n_a)
ba -- 偏置,次元為(n_a, 1)
by -- 偏置,隐藏狀态與輸出相關的偏置,次元為(n_y, 1)
傳回:
a_next -- 下一個隐藏狀态,次元為(n_a, m)
yt_pred -- 在時間步“t”的預測,次元為(n_y, m)
cache -- 反向傳播需要的元組,包含了(a_next, a_prev, xt, parameters)
"""
Wax = parameters["Wax"]
Waa = parameters["Waa"]
Wya = parameters["Wya"]
ba = parameters["ba"]
by = parameters["by"]
a_next = np.tanh(np.dot(Waa,a_prev)+np.dot(Wax,xt)+ba)
yt_pred = rnn_utils.softmax(np.dot(Wya, a_next) + by)
cache = (a_next,a_prev,xt,parameters)
return a_next,yt_pred,cache
有了RNN單元那就建構完整的RNN網絡:
1、由于網絡開始并沒有隐藏内容,是以要給出一個初始化的a0
2、輸入x為一個時間序列,x.shape = (n_x, m, T_x),每個時間步長輸入一組特征
def rnn_forward(x,a0,parameters):
"""
RNN的向前傳播
參數:
x - 輸入的全部資料(n_x, m, T_x),每個輸入都是一個二維矩陣,Tx代表時間序列
a0 - 初始化隐藏狀态(n_a, m)
parameters:
Wax:輸入權重矩陣,次元(n_a, n_x)
Waa: 隐藏狀态權重,次元(n_a, n_a)
Wya: 輸出權重矩陣,次元(n_y, n_a)
ba : 輸入偏置,次元(n_a, 1)
by : 輸出偏置,次元(n_y, 1)
"""
caches = []
n_x, m, T_x = x.shape #T_x:時間序列,每個事件序列中樣本的特征數量,m樣本數量
n_y, n_a = parameters['Wya'].shape #n_y輸出特征個數,n_a中間特征數量
#初始化a,y
a = np.zeros([n_a,m,T_x])
y_pred = np.zeros([n_y,m,T_x])
a_next = a0
for t in range(T_x):
a_next,yt_pred,cache = rnn_cell_forward(x[:,:,t],a_next,parameters)
a[:,:,t] = a_next
y_pred[:,:,t]=yt_pred
caches.append(cache)
caches= (caches,x)
return a, y_pred, caches
RNN的實際使用
知道了上述的架構,但是該怎麼使用呢?接下來邊舉個例子,由于架構會幫我們完成反向傳播,這裡就不研究了。
這裡将會完成一個恐龍名字預測的小例子。
1、資料集與預處理
讀取恐龍名稱的資料集,建立一個唯一的字元清單,計算資料集和詞彙量的大小。資料集中的内容就是一行一個恐龍......
# 擷取名稱
data = open("dinos.txt", "r").read()
# 轉化為小寫字元
data = data.lower()
# 轉化為無序且不重複的元素清單
chars = list(set(data))
# 擷取大小資訊
data_size, vocab_size = len(data), len(chars)
print(chars)
print("共計有%d個字元,唯一字元有%d個" % (data_size,vocab_size))
print(data)
"""
['l', 'o', 'h', 'm', 'y', 's', 'a', 'k', 'c', 'j', 't', 'r', 'i', 'e', 'z', 'n', 'f', 'p', 'w', '\n', 'q', 'b', 'g', 'x', 'u', 'v', 'd']
共計有19909個字元,唯一字元有27個
"""
總共有26個字元加上換行符 \n,接下來将這些字元進行編碼管理,char_to_index
char_to_ix = {ch:i for i, ch in enumerate(sorted(chars))}
ix_to_char = {i:ch for i, ch in enumerate(sorted(chars))}
print(char_to_ix)
print(ix_to_char)
"""
{'\n': 0, 'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6, 'g': 7, 'h': 8, 'i': 9, 'j': 10, 'k': 11, 'l': 12, 'm': 13, 'n': 14, 'o': 15, 'p': 16, 'q': 17, 'r': 18, 's': 19, 't': 20, 'u': 21, 'v': 22, 'w': 23, 'x': 24, 'y': 25, 'z': 26}
{0: '\n', 1: 'a', 2: 'b', 3: 'c', 4: 'd', 5: 'e', 6: 'f', 7: 'g', 8: 'h', 9: 'i', 10: 'j', 11: 'k', 12: 'l', 13: 'm', 14: 'n', 15: 'o', 16: 'p', 17: 'q', 18: 'r', 19: 's', 20: 't', 21: 'u', 22: 'v', 23: 'w', 24: 'x', 25: 'y', 26: 'z'}
"""
2、梯度修剪
說白了就是設定門檻值防止梯度爆炸
def clip(gradients, maxValue):
"""
使用maxValue來修剪梯度
參數:
gradients -- 字典類型,包含了以下參數:"dWaa", "dWax", "dWya", "db", "dby"
maxValue -- 門檻值,把梯度值限制在[-maxValue, maxValue]内
傳回:
gradients -- 修剪後的梯度
"""
# 擷取參數
dWaa, dWax, dWya, db, dby = gradients['dWaa'], gradients['dWax'], gradients['dWya'], gradients['db'], gradients['dby']
# 梯度修剪
for gradient in [dWaa, dWax, dWya, db, dby]:
np.clip(gradient, -maxValue, maxValue, out=gradient)
gradients = {"dWaa": dWaa, "dWax": dWax, "dWya": dWya, "db": db, "dby": dby}
return gradients
3、采樣
采樣的過程其實應該放到最後來講,因為采樣是來檢測我們的輸出的,也就是我們給定上文,網絡來幫我們預測下文。是以這部分是放在網絡訓練的最後,隻有在有了網絡參數之後才能對網絡進行采樣。
采樣是在做什麼樣的事呢?
從圖上可以看出,每一層的輸入等于上一層的輸出,當我們輸入一個初始值便開始往下預測直到結束。
代碼實作:初始值是一個獨熱向量 ,shape=(27, 1),用0初始化。a_prev用0初始化,shape= (n_a, 1)。為了防止無線循環無限輸出,利用換行符進行控制以及字元數小于50來控制。
不斷向前傳播,然後根據機率,在27個字元樣本中進行取樣,記錄到索引indices中,并實時更新輸入的獨熱向量和隐藏内容
def sample(parameters, char_to_ix, seed):
"""
根據RNN輸出的機率分布序列對字元序列進行采樣
參數:
parameters -- 包含了Waa, Wax, Wya, by, b的字典
char_to_ix -- 字元映射到索引的字典
seed -- 随機種子
傳回:
indices -- 包含采樣字元索引的長度為n的清單。
"""
# 從parameters 中擷取參數
Waa, Wax, Wya, by, b = parameters['Waa'], parameters['Wax'], parameters['Wya'], parameters['by'], parameters['b']
vocab_size = by.shape[0] #輸出層特征數,應該為27,即輸出的
n_a = Waa.shape[1] #隐藏層特征數
# 步驟1
## 建立獨熱向量x
x = np.zeros((vocab_size, 1))
## 使用0初始化a_prev
a_prev = np.zeros((n_a, 1))
# 建立索引的空清單,這是包含要生成的字元的索引的清單。
indices = []
# IDX是檢測換行符的标志,我們将其初始化為-1。
idx = -1
# 循環周遊時間步驟t。在每個時間步中,從機率分布中抽取一個字元,
# 并将其索引附加到“indices”上,如果我們達到50個字元,
#(我們應該不太可能有一個訓練好的模型),我們将停止循環,這有助于調試并防止進入無限循環
counter = 0
newline_character = char_to_ix["\n"]
while (idx != newline_character and counter < 50):
# 步驟2:使用公式1、2、3進行前向傳播
a = np.tanh(np.dot(Wax, x) + np.dot(Waa, a_prev) + b) #a.shape = (n_a, n_a)
z = np.dot(Wya, a) + by
y = cllm_utils.softmax(z)
# 設定随機種子
np.random.seed(counter + seed)
# 步驟3:從機率分布y中抽取詞彙表中字元的索引
idx = np.random.choice(list(range(vocab_size)), p=y.ravel()) #給每個字元賦予機率,然後随機抽取
# 添加到索引中
indices.append(idx) #儲存每一位的字元索引
# 步驟4:将輸入字元重寫為與采樣索引對應的字元。
x = np.zeros((vocab_size, 1))
x[idx] = 1
# 更新a_prev為a
a_prev = a
# 累加器
seed += 1
counter +=1
if(counter == 50):
indices.append(char_to_ix["\n"])
return indices
4、梯度下降
由于懶惰并沒有學習梯度下降的内容,但是架構還是要搭的。架構:向前傳播 -> 反向傳播 -> 更新參數
def optimize(X, Y, a_prev, parameters, learning_rate = 0.01):
"""
執行訓練模型的單步優化。
參數:
X -- 整數清單,其中每個整數映射到詞彙表中的字元。
Y -- 整數清單,與X完全相同,但向左移動了一個索引。
a_prev -- 上一個隐藏狀态
parameters -- 字典,包含了以下參數:
Wax -- 權重矩陣乘以輸入,次元為(n_a, n_x)
Waa -- 權重矩陣乘以隐藏狀态,次元為(n_a, n_a)
Wya -- 隐藏狀态與輸出相關的權重矩陣,次元為(n_y, n_a)
b -- 偏置,次元為(n_a, 1)
by -- 隐藏狀态與輸出相關的權重偏置,次元為(n_y, 1)
learning_rate -- 模型學習的速率
傳回:
loss -- 損失函數的值(交叉熵損失)
gradients -- 字典,包含了以下參數:
dWax -- 輸入到隐藏的權值的梯度,次元為(n_a, n_x)
dWaa -- 隐藏到隐藏的權值的梯度,次元為(n_a, n_a)
dWya -- 隐藏到輸出的權值的梯度,次元為(n_y, n_a)
db -- 偏置的梯度,次元為(n_a, 1)
dby -- 輸出偏置向量的梯度,次元為(n_y, 1)
a[len(X)-1] -- 最後的隐藏狀态,次元為(n_a, 1)
"""
# 前向傳播
#通過RNN向前傳播,計算交叉熵損失
#傳回損失值以及存儲在反向傳播中使用的緩存值
loss, cache = cllm_utils.rnn_forward(X, Y, a_prev, parameters)
# 反向傳播
#通過時間進行反向傳播,計算相對于參數的梯度損失。它還傳回所有隐藏的狀态
gradients, a = cllm_utils.rnn_backward(X, Y, parameters, cache)
# 梯度修剪,[-5 , 5]
gradients = clip(gradients,5)
# 更新參數
parameters = cllm_utils.update_parameters(parameters, gradients, learning_rate)
return loss, gradients, a[len(X)-1]
5、模型搭建
有了各個元件便來搭建一個完整的RNN模型來預測恐龍的名字。
1、初始化參數,得到一個所有參數的字典
2、初始化損失
3、讀取訓練資料(傳入的參數data就是資料,是以這邊的讀取可以省略),打亂資料用于随機梯度下降
4、初始化隐藏狀态。進入循環優化
5、使用index = j % len(examples) 來保證周遊樣本而不會重複,每次取出一行,将字元轉換為索引,對應的Y需要向左移動一個字元并且在最後加上換行符,保證輸入和輸出的對應,這樣一組樣本就制作完成了。
6、梯度下降,參數優化
7、每2000次進行一次采樣,這邊的采樣并沒有需要我們輸入一個字元(因為采樣的方法中初始化了輸入為0)
def model(data, ix_to_char, char_to_ix, num_iterations=3500,
n_a=50, dino_names=7,vocab_size=27):
"""
訓練模型并生成恐龍名字
參數:
data -- 語料庫
ix_to_char -- 索引映射字元字典
char_to_ix -- 字元映射索引字典
num_iterations -- 疊代次數
n_a -- RNN單元數量
dino_names -- 每次疊代中采樣的數量,疊代中生成測試樣本的個數
vocab_size -- 在文本中的唯一字元的數量
傳回:
parameters -- 學習後了的參數
"""
# 從vocab_size中擷取n_x、n_y
n_x, n_y = vocab_size, vocab_size
# 初始化參數
parameters = cllm_utils.initialize_parameters(n_a, n_x, n_y)
# 初始化損失
loss = cllm_utils.get_initial_loss(vocab_size, dino_names)
# 建構恐龍名稱清單
with open("dinos.txt") as f:
examples = f.readlines()
examples = [x.lower().strip() for x in examples]
# 打亂全部的恐龍名稱
np.random.seed(0)
np.random.shuffle(examples)
# 初始化LSTM隐藏狀态
a_prev = np.zeros((n_a,1))
# 循環
for j in range(num_iterations):
# 定義一個訓練樣本
index = j % len(examples)
X = [None] + [char_to_ix[ch] for ch in examples[index]] #X = 字元索引
Y = X[1:] + [char_to_ix["\n"]] #y = x 最後加換行
# 執行單步優化:前向傳播 -> 反向傳播 -> 梯度修剪 -> 更新參數
# 選擇學習率為0.01
curr_loss, gradients, a_prev = optimize(X, Y, a_prev, parameters) #更新a_prev
# 使用延遲來保持損失平滑,這是為了加速訓練。
loss = cllm_utils.smooth(loss, curr_loss)
# 每2000次疊代,通過sample()生成“\n”字元,檢查模型是否學習正确
if j % 2000 == 0:
print("第" + str(j+1) + "次疊代,損失值為:" + str(loss))
seed = 0
for name in range(dino_names):
# 采樣
sampled_indices = sample(parameters, char_to_ix, seed)
cllm_utils.print_sample(sampled_indices, ix_to_char)
# 為了得到相同的效果,随機種子+1
seed += 1
print("\n")
return parameters
嘗試一波,好像是有點樣子了......
#開始時間
start_time = time.clock()
#開始訓練
parameters = model(data, ix_to_char, char_to_ix, num_iterations=10000)
#結束時間
end_time = time.clock()
#計算時差
minium = end_time - start_time
print("執行了:" + str(int(minium / 60)) + "分" + str(int(minium%60)) + "秒")
"""
第1次疊代,損失值為:23.087336085484605
Nkzxwtdmfqoeyhsqwasjkjvu
Kneb
Kzxwtdmfqoeyhsqwasjkjvu
Neb
Zxwtdmfqoeyhsqwasjkjvu
Eb
Xwtdmfqoeyhsqwasjkjvu
第2001次疊代,損失值為:27.884160491415773
Liusskeomnolxeros
Hmdaairus
Hytroligoraurus
Lecalosapaus
Xusicikoraurus
Abalpsamantisaurus
Tpraneronxeros
第4001次疊代,損失值為:25.90181489335302
Mivrosaurus
Inee
Ivtroplisaurus
Mbaaisaurus
Wusichisaurus
Cabaselachus
Toraperlethosdarenitochusthiamamumamaon
"""
LSTM - 長短時記憶(Long Short-Term Memory)
LSTM的運用更加廣泛。它更複雜也更好
根據上面的LSTM單元寫代碼還是很簡單的。 可以看到,它比RNN多了很多道門而且還多了一個記憶狀态。也就是說我們不僅要考慮隐藏狀态的更新也要考慮記憶狀态的更新。
由于模型進行了更新,是以中間過程的實作方式部分也要有更新。之前的RNN網絡中隐藏狀态和目前時間步的輸入是不共用權重的,但是在LSTM中,他們是共用門的權重的,是以權重的次元要注意。
def lstm_cell_forward(xt, a_prev, c_prev, parameters):
"""
參數
xt - 時間t輸入的資料次元為(n_x, m)
a_prev - 上一個時間步t-1的隐藏狀态(n_a, m)
c_prev - 上一個時間步t-1的記憶狀态(n_a, m)
parameters:
Wf - 遺忘門的權重,次元為(n_a, n_a +n_x)
bf - 遺忘門的偏置,(n_a, 1)
Wi - 更新門的權重,次元為(n_a, n_a + n_x)
bi -- 更新門的偏置,次元為(n_a, 1)
Wc -- 第一個“tanh”的權值,次元為(n_a, n_a + n_x)
bc -- 第一個“tanh”的偏置,次元為(n_a, n_a + n_x)
Wo -- 輸出門的權值,次元為(n_a, n_a + n_x)
bo -- 輸出門的偏置,次元為(n_a, 1)
Wy -- 隐藏狀态與輸出相關的權值,次元為(n_y, n_a)
by -- 隐藏狀态與輸出相關的偏置,次元為(n_y, 1)
傳回:
a -- 所有時間步的隐藏狀态,次元為(n_a, m, T_x)
y_pred -- 所有時間步的預測,次元為(n_y, m, T_x)
caches -- 為反向傳播的儲存的元組,次元為(【清單類型】cache, x))
"""
# 從“parameters”中擷取相關值
Wf = parameters["Wf"]
bf = parameters["bf"]
Wi = parameters["Wi"]
bi = parameters["bi"]
Wc = parameters["Wc"]
bc = parameters["bc"]
Wo = parameters["Wo"]
bo = parameters["bo"]
Wy = parameters["Wy"]
by = parameters["by"]
n_x, m = xt.shape
n_y, n_a = Wy.shape
#連結a_prev 和 xt
contact = np.zeros([n_a + n_x, m])
contact[:n_a,:] = a_prev
contact[n_a:,:] = xt
#根據公式計算
#遺忘門 Wf.shape = (n_a, n_a +n_x)
ft = sigmoid(np.dot(Wf, contact)+bf) #ft.shape = (n_a, m)
#更新門
#Wi.shape = (n_a, n_a + n_x)
it = sigmoid(np.dot(Wi, contact)+bi) #it.shape = (n_a, m)
#更新單元
#Wc.shape = (n_a, n_a + n_x)
cct = np.tanh(np.dot(Wc,contact)+bc) #cct.shape = (n_a, m)
#新單元 = 遺忘門 * 過去的記憶狀态 + 更新門 * 更新單元
c_next = ft * c_prev + it * cct #(n_a, m) * (n_a, m) = (n_a, m)
#輸出門
#Wo.shape = (n_a, n_a + n_x)
ot = sigmoid(np.dot(Wo,contact)+bo) #ot.shape = (n_a, m)
#新隐藏狀态
a_next = ot*np.tanh(c_next) #(n_a, m) * (n_a, m) = (n_a, m)
#Wy.shape = (n_y, n_a)
yt_pred=softmax(np.dot(Wy,a_next)+by) #yt_pred.shape = (n_y, m)
cache = (a_next, c_next, a_prev, c_prev, ft, it, cct, ot, xt, parameters)
return a_next, c_next, yt_pred, cache
單元玩完了就要玩一個模型了,模型結構和RNN很像的......
在整個向前傳播的模型中,我們同樣需要一個完整的時間步資料,一個隐藏輸入狀态。 記憶狀态c是函數中初始化
def lstm_forward(x, a0, parameters):
"""
參數:
x -- 所有時間步的輸入資料,次元為(n_x, m, T_x)
a0 -- 初始化隐藏狀态,次元為(n_a, m)
parameters -- python字典,包含了以下參數:
Wf -- 遺忘門的權值,次元為(n_a, n_a + n_x)
bf -- 遺忘門的偏置,次元為(n_a, 1)
Wi -- 更新門的權值,次元為(n_a, n_a + n_x)
bi -- 更新門的偏置,次元為(n_a, 1)
Wc -- 第一個“tanh”的權值,次元為(n_a, n_a + n_x)
bc -- 第一個“tanh”的偏置,次元為(n_a, n_a + n_x)
Wo -- 輸出門的權值,次元為(n_a, n_a + n_x)
bo -- 輸出門的偏置,次元為(n_a, 1)
Wy -- 隐藏狀态與輸出相關的權值,次元為(n_y, n_a)
by -- 隐藏狀态與輸出相關的偏置,次元為(n_y, 1)
傳回:
a -- 所有時間步的隐藏狀态,次元為(n_a, m, T_x)
y -- 所有時間步的預測值,次元為(n_y, m, T_x)
caches -- 為反向傳播的儲存的元組,次元為(【清單類型】cache, x))
"""
caches = []
n_x, m, T_x = x.shape
n_y, n_a = parameters['Wy'].shape
#初始化a, c, y
a = np.zeros([n_a, m, T_x])
c = np.zeros([n_a, m, T_x])
y = np.zeros([n_y, m, T_x])
#初始化a_next, c_next
a_next = a0
c_next = np.zeros([n_a, m])
#周遊所有的時間步
for t in range(T_x):
#更新下一個隐藏狀态,下一個記憶狀态,計算預測值,擷取cache
a_next,c_next,yt_pred,cache = lstm_cell_forward(x[:,:,t], a_next, c_next, parameters)
#儲存新的下一個隐藏狀态到a中
a[:,:,t] = a_next
#儲存預測值
y[:,:,t] = yt_pred #yt_pred,shape = (n_y, m) 輸出機率
c[:,:,t] = c_next #儲存下一個單元狀态
caches.append(cache)
caches = (caches, x)
return a, y, c, caches
反向傳播由于懶惰并沒有學習。還有一個用LSTM創作音樂的例子我并不會