天天看點

循環神經網絡RNN與LSTMRNN單元與網絡RNN的實際使用LSTM - 長短時記憶(Long Short-Term Memory)

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

RNN單元與網絡

循環神經網絡RNN與LSTMRNN單元與網絡RNN的實際使用LSTM - 長短時記憶(Long Short-Term Memory)

       循環神經網絡是能夠根據前文的内容去預測下文的,RNN的單元結構圖如上。圖中的a代表上一個時間步長的隐藏内容,Waa代表隐藏狀态的權重,x代表目前時間步長的輸入,Wax代表輸入權重,Wya代表輸出層權重。根據圖中公式連起來得到一個循環神經網絡

循環神經網絡RNN與LSTMRNN單元與網絡RNN的實際使用LSTM - 長短時記憶(Long Short-Term Memory)

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、采樣

       采樣的過程其實應該放到最後來講,因為采樣是來檢測我們的輸出的,也就是我們給定上文,網絡來幫我們預測下文。是以這部分是放在網絡訓練的最後,隻有在有了網絡參數之後才能對網絡進行采樣。

       采樣是在做什麼樣的事呢?

循環神經網絡RNN與LSTMRNN單元與網絡RNN的實際使用LSTM - 長短時記憶(Long Short-Term Memory)

       從圖上可以看出,每一層的輸入等于上一層的輸出,當我們輸入一個初始值便開始往下預測直到結束。

       代碼實作:初始值是一個獨熱向量 ,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的運用更加廣泛。它更複雜也更好

循環神經網絡RNN與LSTMRNN單元與網絡RNN的實際使用LSTM - 長短時記憶(Long Short-Term Memory)

        根據上面的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很像的......

循環神經網絡RNN與LSTMRNN單元與網絡RNN的實際使用LSTM - 長短時記憶(Long Short-Term Memory)

在整個向前傳播的模型中,我們同樣需要一個完整的時間步資料,一個隐藏輸入狀态。 記憶狀态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創作音樂的例子我并不會

繼續閱讀

繼續閱讀