1. 程式人生 > >長短期記憶(LSTM)-tensorflow程式碼實現

長短期記憶(LSTM)-tensorflow程式碼實現

作者:Jason
時間:2017.10.17

    長短期記憶(LSTM)神經網路是之前講的RNN的一種升級版本,我們先來聊聊RNN的弊端。

RNN的弊端


這裡寫圖片描述

之前我們說過, RNN 是在有順序的資料上進行學習的. 為了記住這些資料, RNN 會像人一樣產生對先前發生事件的記憶. 不過一般形式的 RNN 就像一個老爺爺, 有時候比較健忘. 為什麼會這樣呢?


這裡寫圖片描述

想像現在有這樣一個 RNN, 他的輸入值是一句話: ‘我今天要做紅燒排骨, 首先要準備排骨, 然後…., 最後美味的一道菜就出鍋了’, shua ~ 說著說著就流口水了. 現在請 RNN 來分析, 我今天做的到底是什麼菜呢. RNN可能會給出“辣子雞”這個答案. 由於判斷失誤, RNN就要開始學習 這個長序列 X 和 ‘紅燒排骨’ 的關係 , 而RNN需要的關鍵資訊 ”紅燒排骨”卻出現在句子開頭.


只需當權重w稍微小於1時(反向傳播的誤差越來越小,梯度彌散
這裡寫圖片描述

只需當權重w稍微大於1時(反向傳播的誤差越來越大,梯度爆炸
這裡寫圖片描述

具體example:


圖1.1
這裡寫圖片描述

  如上圖,所示普通的RNN情況下,再來看看 RNN是怎樣學習的吧. 紅燒排骨這個資訊原的記憶要進過長途跋涉才能抵達最後一個時間點. 然後我們得到誤差, 而且在 反向傳遞 得到的誤差的時候, 他在每一步都會 乘以一個自己的引數 W. 如果這個 W 是一個小於1 的數, 比如0.99. 這個0.99 不斷乘以誤差, 誤差傳到初始時間點也會是一個接近於零的數, 所以對於初始時刻, 誤差相當於就消失了. 我們把這個問題叫做梯度消失或者梯度彌散 Gradient vanishing. 反之如果 W 是一個大於1 的數, 比如1.01 不斷累乘, 則到最後變成了無窮大的數, RNN被這無窮大的數撐死了, 這種情況我們叫做剃度爆炸(Gradient exploding)。
  如圖1.1所示當依次輸入1,0,0,0時,當W=0.99時,序列很長的情況下,最終的值為W的999次方,約等於0,遠遠小於1,LOSS/w=梯度也會變得特別小,當W=1.01時,序列很長的情況下,最終的值為W的999次方,約等於+00,遠遠大於1,LOSS/w=梯度也會變得特別大 。
  這就是普通 RNN 沒有辦法回憶起久遠記憶的原因.

RNN —>LSTM


這裡寫圖片描述

LSTM 就是為了解決這個問題而誕生的. LSTM 和普通 RNN 相比, 多出了三個控制器. (輸入控制, 輸出控制, 忘記控制). 現在, LSTM RNN 內部的情況是這樣.

他多了一個 控制全域性的記憶, 我們用粗線代替. 為了方便理解, 我們把粗線想象成電影或遊戲當中的 主線劇情. 而原本的 RNN 體系就是 分線劇情. 三個控制器都是在原始的 RNN 體系上, 我們先看 輸入方面 , 如果此時的分線劇情對於劇終結果十分重要, 輸入控制就會將這個分線劇情按重要程度 寫入主線劇情 進行分析. 再看 忘記方面, 如果此時的分線劇情更改了我們對之前劇情的想法, 那麼忘記控制就會將之前的某些主線劇情忘記, 按比例替換成現在的新劇情. 所以 主線劇情的更新就取決於輸入 和忘記 控制. 最後的輸出方面, 輸出控制會基於目前的主線劇情和分線劇情判斷要輸出的到底是什麼.基於這些控制機制, LSTM 就像延緩記憶衰退的良藥, 可以帶來更好的結果.
以下為traditional LSTM的公式:



這裡寫圖片描述

  前三行是三個門,分別是遺忘門 ft,輸入門 it,輸出門 ot,輸入都是 [xt,ht−1],只是引數不同,然後要經過一個啟用函式,把值放縮到 [0,1] 附近。第四行 ct 是 cell state,由上一時刻的 ct−1 和輸入得到。如果遺忘門 ft 取 0 的話,那麼上一時刻的狀態就會全部被清空(清空 or 遺忘?),然後只關注此時刻的輸入。輸入門 it 決定是否接收此時刻的輸入。最後輸出門 ot 決定是否輸出 cell state。
具體事例
下圖表示LSTM的結構圖:


這裡寫圖片描述

  如上圖所示,有4個輸入端,1個輸出端,明顯看出那些門(gate)所控制的方式就是乘上原先RNN上的值就可以達到控制輸入,控制儲存在cell的state和輸出值了。
  正如上面所提到的那樣,RNN問題就出在W不斷連乘積的問題上,所以我們要避免W不斷相乘,所以我們把memory複製到下一階段的方式改成不斷相加的方式,同時forget gate大多數情況下是接近1的(偏置設大些),允許add來避免y過小。來解決這一個問題。而梯度爆炸不是個嚴重的問題,我們可以通過clip(梯度)來限制梯度過大。
接下來我們來看看最簡單的LSTM的演算過程


這裡寫圖片描述

  如上圖所示:4個輸入端都輸入一個3維的元素,偏置項的權重為1——[3,1,0,1],分別與他們相對應的門的權重引數相乘,然後再經過啟用函式(f表示sigmod啟用,h表示線性啟用),sigmod啟用值屬於0到1之間。分別得到logits值為3,90,-10,110,通過啟用函式啟用得到3,~1,0,~1,(~表示約等於)。然後開始演算一下過程,3x1等於3—>3+0x1=3—>3x0=0。所以輸入[3,1,0]時,最終得到0。依次類推,依次輸入[4,1,0],[2,0,0],[1,0,1],[3,-1,0],最終得到的序列[0,0,0,7,0]
  所以LSTM的可訓練引數是RNN的4倍

實際上:LSTM的輸入值應該還包含了上一時期的cell狀態值和隱藏層輸出值


這裡寫圖片描述

LSTM的變體—GRU
GRU:通過將輸入門和忘記門聯動起來合併成一個門,意思就是舊的不去,新的不來,為了理解GRU的設計思想,我們再一次運用“三次簡化一張圖”的方法來進行分析:


這裡寫圖片描述
這四行公式解釋如下:
- zt 是 update gate,更新 activation 時的邏輯閘
- rt 是 reset gate,決定 candidate activation 時,是否要放棄以前的 activation ht
- h˜t 是 candidate activation,接收 [xt,ht−1]
- ht 是 activation,是 GRU 的隱層,接收 [ht−1,h˜t]

這裡寫圖片描述

  與LSTM相比,GRU將輸入門it和遺忘門ft融合成單一的更新門zt,並且融合了記憶單元ct和隱層單元ht,所以結構上比LSTM更簡單一些。根據這張圖,我們可以對GRU的各單元作用進行分析:
  重置門rtrt用於控制前一時刻隱層單元ht-1對當前詞xt的影響。如果ht-1對xt不重要,即從當前詞xt開始表述了新的意思,與上文無關, 那麼rt開關可以開啟, 使得ht-1對xt不產生影響。
  更新門ztzt用於決定是否忽略當前詞xt。類似於LSTM中的輸入門it, zt可以判斷當前詞xt對整體意思的表達是否重要。當zt開關接通下面的支路時,我們將忽略當前詞xt,同時構成了從ht-1到ht的”短路連線”,這梯度得已有效地反向傳播。和LSTM相同,這種短路機制有效地緩解了梯度消失現象, 這個機制於highwaynetworks十分相似,這樣一來可以減少模型的可訓練引數,提高模型的魯棒性。大量論文結果表現都是GRU的效果略由於LSTM,LSTM和GRU的效果要比RNN好得多。
  公式回顧
RNN:
LSTM:
GRU:


  tensorflow程式碼實現LSTM
"""
用自己建立的 sin 曲線預測一條 cos 曲線
PS:深度學習中經常看到epoch、 iteration和batchsize,下面按自己的理解說說這三個的區別:
(1)batchsize:批大小。在深度學習中,一般採用SGD訓練,即每次訓練在訓練集中取batchsize個樣本訓練;
(2)iteration:1個iteration等於使用batchsize個樣本訓練一次;
(3)epoch:1個epoch等於使用訓練集中的全部樣本訓練一次;
舉個例子,訓練集有1000個樣本,batchsize=10,那麼:
訓練完整個樣本集需要:
100次iteration,1次epoch。
"""
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt

# 定義超引數
BATCH_START = 0     # 建立 batch data 時候的 index
TIME_STEPS = 20     # time_steps也就是n_steps,等於序列的長度
BATCH_SIZE = 50     # 批次的大小
INPUT_SIZE = 1      # sin 資料輸入 size
OUTPUT_SIZE = 1     # cos 資料輸出 size
CELL_SIZE = 10      # 隱藏層規模
LR = 0.006          # 學習率

# 資料生成
# 生成一個批次大小的資料的 get_batch function:
def get_batch():
    global BATCH_START, TIME_STEPS
    # xs的shape是(50batch,20steps)
    xs = np.arange (BATCH_START, BATCH_START + TIME_STEPS * BATCH_SIZE).reshape((BATCH_SIZE , TIME_STEPS))/(10*np.pi)#定義x
    seq=np.sin(xs)
    res=np.cos(xs)
    BATCH_START+=TIME_STEPS
    #返回seq,res 的shape(batch,step,1),xs的shape為(batch_size,time_steps)
    #一般像這種[:,:,np.newaxis]叫做擴維技術,從2為變成3維,擴的維數為1。
    #seq[:, :, np.newaxis].shape=(50,20,1)
    # plt.plot (xs, seq, 'r-', xs, res, 'b-')
    # plt.show ()
    return [seq[:,:,np.newaxis],res[:,:,np.newaxis],xs]

#1. 使用tf.Variable()的時候,tf.name_scope()和tf.variable_scope() 都會給 Variable 和 op 的 name屬性加上字首。
#2. 使用tf.get_variable()的時候,tf.name_scope()就不會給 tf.get_variable()創建出來的Variable加字首。

#定義 LSTM 的主體結構
class LSTM(object):
    def __init__(self,n_steps,input_size,output_size,cell_size,batch_size):
        self.n_steps=n_steps
        self.input_size=input_size
        self.cell_size=cell_size
        self.batch_size=batch_size
        self.output_size=output_size
        #初始化幾個函式
        with tf.name_scope("inputs"):
            self.xs=tf.placeholder(tf.float32,[None,n_steps,self.input_size],name="xs")
            self.ys=tf.placeholder(tf.float32,[None,n_steps,self.output_size],name="ys")
        with tf.variable_scope("in_hidden"):
            self.add_input_layer()
        with tf.variable_scope("LSTM"):
            self.add_cell()
        with tf.variable_scope("out_hidden"):
            self.add_output_layer()
        with tf.name_scope("cost"):
            self.compute_cost()
        with tf.name_scope('train'):
            self.train_op=tf.train.AdamOptimizer(LR).minimize(self.cost)
    #定義三個變數
    def ms_error(self,labels,logits):
        return tf.square(tf.subtract(labels,logits))

    def _weight_variable(self,shape,name="weights"):
        initializer=tf.random_normal_initializer(mean=0,stddev=1,)
        return tf.get_variable(shape=shape,initializer=initializer,name=name)

    def _bias_variable(self,shape,name="biases"):
        initializer=tf.constant_initializer(0.1)
        return tf.get_variable(name=name,shape=shape,initializer=initializer)
    #接下來定義幾個函式
    def add_input_layer(self):
        #應該我們只能在二維資料上矩陣相乘,計算logits,之後在reshape成3維。以下同理
        l_in_x=tf.reshape(self.xs,[-1,self.input_size],name='2_2D')#輸入shape(batch_size*n_step,input_size)
        #權重的shape(in_size,cell_size)
        Ws_in=self._weight_variable([self.input_size,self.cell_size])
        #偏置的shape (cell_size,)
        bs_in=self._bias_variable([self.cell_size,])
        #l_in_y = (batch * n_steps ,cell_size)
        with tf.name_scope("Wx_puls_b"):
            l_in_y=tf.matmul(l_in_x,Ws_in)+bs_in
        #reshape l_in_y-->>=(batch,n_steps,cell_size)
        self.l_in_y=tf.reshape(l_in_y,[-1,self.n_steps,self.cell_size],name='2_3D')

    def add_cell(self):
        lstm_cell=tf.contrib.rnn.BasicLSTMCell(self.cell_size,forget_bias=1.0,state_is_tuple=True)
        with tf.name_scope("initial_state"):
            self.cell_init_state=lstm_cell.zero_state(self.batch_size,dtype=tf.float32,)
        self.cell_outputs,self.cell_final_state=tf.nn.dynamic_rnn(lstm_cell,
            self.l_in_y,initial_state=self.cell_init_state,time_major=False
        )#如果l_in_y的shape是(n_steps,batch,cell_size)的話,則對應的time_major=True

    def add_output_layer(self):
        #shape=(batch * n_steps,cell_size)
        l_out_x=tf.reshape(self.cell_outputs,[-1,self.cell_size],name="2_2D")
        Ws_out=self._weight_variable([self.cell_size,self.output_size])
        bs_out=self._bias_variable([self.output_size,])
        with tf.name_scope("Wx_plus_b"):
            self.pred=tf.matmul(l_out_x,Ws_out)+bs_out #shape=(batch*n_steps,output_size)

    def compute_cost(self):
        #計算一個batch內每一樣本的loss
        losses=tf.contrib.legacy_seq2seq.sequence_loss_by_example(
            [tf.reshape(self.pred,[-1],name="reshape_pred")],#平鋪一下維數
            [tf.reshape(self.ys,[-1],name="reshape_target")],
            [tf.ones([self.batch_size*self.n_steps],dtype=tf.float32)],
            average_across_timesteps=True,
            softmax_loss_function=self.ms_error,
            name="losses"
        )
        with tf.name_scope("average_cost"):
            #計算每一個batch的平均loss,因為梯度更新是在計算一個batch的平均誤差的基礎上進行更新的
            self.cost=tf.div(tf.reduce_sum(losses,name="losses_sum"),self.batch_size,name="average_cost")
            tf.summary.scalar("cost",self.cost)

if __name__=="__main__":
    model= LSTM(TIME_STEPS,INPUT_SIZE,OUTPUT_SIZE,CELL_SIZE,BATCH_SIZE)
    sess=tf.Session()
    merged=tf.summary.merge_all()
    writer=tf.summary.FileWriter("logs",sess.graph)
    #版本控制
    # tf.initialize_all_variables() no long valid from
    # 2017-03-02 if using tensorflow >= 0.12
    if int((tf.__version__).split('.')[1]) < 12 and   int((tf.__version__).split('.')[0]) < 1:
        init=tf.initialize_all_variables()
    else:
        init=tf.global_variables_initializer()
    sess.run(init)
    # relocate to the local dir and run this line to view it on
    # 在terminal中輸入$ tensorboard --logdir='logs',讓後在瀏覽器中Chrome (http://0.0.0.0:6006/)檢視tensorboard
    plt.ion()
    plt.show()
    for i in range(200):#訓練200次,訓練一次一個batch
        seq,res,xs=get_batch()#此時的seq,res都是3維陣列,shape=(batch,time_steps,1),這裡的1就是input_size
        if i==0:
            feed_dict={model.xs:seq,
                       model.ys:res
                       #建立初始狀態,這裡就開始體現類的優勢了,直接呼叫裡面的xs,ys,
                       }
        else:
            feed_dict={model.xs:seq,
                       model.ys:res,
                       model.cell_init_state:state#用最後的state代替初始化的state
            }
        _,cost,state,pred=sess.run(
            [model.train_op,model.cost,model.cell_final_state,model.pred],feed_dict=feed_dict)
    #輸出值和帶入的引數順序一一對應,cost對應model.cost,等等

    #xs[0,:],表示的是一個batch裡面的第一個序列,因為xs是由np.arange()函式生成的,
    # 所以xs在對於每一個batch來說,同一個batch裡面的每個序列都是一樣的
    #例如xs的batch_size=3,time_step=4,[[0,1,2,3],
    #                                 [0,1,2,3],
    #                                 [0,1,2,4]],shape=(3,4)
    #res[0].flatten()表示的是一個batch裡面的第一個序列,序列長度為time_steps * 1
        plt.plot(xs[0,:],res[0].flatten(),"r",xs[0,:],pred.flatten()[:TIME_STEPS],"b--")
        plt.ylim((-1.2,1.2))
        plt.draw()
        plt.pause(0.3)

        if i % 20 ==0:#每訓練20個批次來列印一次當時的cost
            print("cost:",round(cost,4))#輸出每一個batch的平均cost,約到零後面4位小數點
            result=sess.run(merged,feed_dict)
            writer.add_summary(result,i)

結果展示
這裡寫圖片描述