即將取代RNN結構的Transformer(及代碼實現)

Transformer以前

clipboard.png

上圖是經典的雙向RNN模型,咱們知道該模型是經過遞歸的方式運行,雖然適合對序列數據建模,可是缺點也很明顯「它沒法並行執行」也就沒法利用GPU強大的並行能力,再加上各類門控機制,運行速度很慢。通常而言,編碼器輸出編碼向量C做爲解碼器輸入,可是因爲編碼向量C中全部的編碼器輸入值貢獻相同,致使序列數據越長信息丟失越多。算法

clipboard.png

CNN網絡相比RNN網絡,它雖然能夠並行執行,可是沒法一次捕獲全局信息,經過上圖可得咱們須要屢次遍歷,多個卷積層疊加增大感覺野。
谷歌的作法是Attention is All You Need !網絡

Transformer

clipboard.png

如圖所示是Transformer的總體結構,咱們將詳細介紹每一部分,先從左邊的編碼器開始。dom

clipboard.png

A: 這一步,我想你們已經很是熟悉了,將詞彙錶轉爲embedding維度的向量(onehot和embedding區別)。
B: 僅僅使用attention有一個致命短板,它對序列數據的順序免疫,即:沒法捕獲序列的順序。好比對翻譯任務,咱們知道順序很是重要,單詞順序變更甚至會產生徹底不一樣的意思。所以增長Position Embedding給每一個位置編號,每一個編號對應一個向量,這樣每一個詞向量都會有一個位置向量,以此來定位。函數

clipboard.png

如圖所示,Position Embedding計算公式,將id爲p的位置映射爲一個dpos維的位置向量,這個向量的第i個元素的數值就是PEi(p),位置編碼算法固然不止一種,可是不一樣算法必需要解決的的問題就是可以處理未知長度的序列。假設位置向量有4維,實際位置向量可能以下所示:性能

clipboard.png

結合位置向量和詞向量咱們有兩種方式,一種是將二者拼接成一個新向量,另外一種是使二者維度相同而後相加獲得新向量。編碼

clipboard.png

C:殘差鏈接,隨後是D: layer-normalization。spa

clipboard.png

隨着網絡層數的加深,會帶來梯度消失,梯度爆炸以及過擬合問題。針對此問題,咱們通常採用權重正則化,批標準化,更換激活函數等等措施,可是當網絡層數進一步增長,會出現網絡退化問題,即:訓練集精度開始降低。使用殘差能夠解決此問題,目前使用殘差的網絡能夠達到幾百層。

E:Multi-head注意力機制.net

clipboard.png

上圖是attention計算過程,咱們分解步驟,依次來看。翻譯

  1. 生成「q」,「k」,「v」向量,由輸入embedding向量與圖示右側對應權重矩陣相乘。須要注意的是,此處的三個權重矩陣爲全部輸入共享。若是每一個詞獨享一個權重矩陣,我的認爲並不會提高性能,有可能還會下降。

clipboard.png

  1. 計算attention score,計算方式如圖所示:

clipboard.png

  1. 使用softmax歸一化數值,softmax上面的相除操做主要是調解內積不要太大。

clipboard.png

  1. softmax歸一化後的值與「v」向量矩陣相乘,將全部加權向量加和,產生該位置的self-attention的輸出結果。

clipboard.png

multi-headed attention:就是有多組上面那樣的attention,最後將結果拼接起來,其中,每組attention權重不共享。code

clipboard.png

計算公式以下:

clipboard.png
clipboard.png

總體計算過程以下圖所示:

clipboard.png

F:全鏈接網絡,兩個線性函數,一個非線性函數(Relu):

clipboard.png

解碼器:

clipboard.png

A:解碼器attention計算的內部向量和編碼器的輸出向量,計算源句和目標句之間的關係,在Transformer以前,attention機制就應用在這裏。
B:線性層是一個全鏈接層,神經元數量和詞表長度相等,而後添加softmax層,將機率最高值對應的詞做爲輸出。

clipboard.png

代碼實現(tensorflow實現)

Position_embedding:

def Position_Embedding(inputs,position_size):
    """
    :param inputs: shape=(batch_size,timestep,word_size)
    :param position_size: int_
    :return: shape=(1,seq_len.size,position_size)
    """

    # inputs: shape=(batch_size,timestep,word_size)
    batch_size,seq_len=tf.shape(inputs)[0],tf.shape(inputs)[1]

    # shape=(position_size,)
    position_j=1./tf.pow(10000.,2*tf.range(position_size,dtype=tf.float32)/position_size)

    # shape=(1,position_size)
    position_j=tf.expand_dims(position_j,axis=0)

    # shape=(seq_len.size,)
    position_i=tf.range(tf.cast(seq_len,tf.float32),dtype=tf.float32)

    # shape=(seq_len.size,1)
    position_i=tf.expand_dims(position_i,axis=1)

    # 這是上面維度擴展的緣由
    position_ij=tf.matmul(position_i,position_j)

    # shape=(seq_len.size,position_size)
    # 在axis=1,即:seq_len.size 拼接
    position_ij=tf.concat([tf.cos(position_ij),tf.sin(position_ij)],axis=1)

    # shape=(1,seq_len.size,position_size)
    position_embedding=tf.expand_dims(position_ij,axis=0)+tf.zeros((batch_size,seq_len,position_size))

    return position_embedding

Mask:

def Mask(inputs,seq_len,mode='mul'):
    """
    :param mode: 'mul':將多餘部分置零,用於全鏈接層以前 'add':將多餘部分減去一個大的常數,用於softmax以前

    """
    if seq_len == None:
        return inputs
    else:

        # shape=(seq_len.size,seq_len.size)
        mask=tf.cast(tf.sequence_mask(seq_len,dtype=tf.float32))

        for _ in range(len(inputs.shape)-2):

            # shape=(seq_len.size,seq_len.size,1)
            mask=tf.expand_dims(mask,2)

            if mode == 'mul':
                return inputs*mask
            if mode == 'add':
                return inputs-(1-mask)*1e12

tf.sequence_mask 使用示例:

mask=tf.sequence_mask([1,2,3,4,5],maxlen=5)
print(mask)

clipboard.png

Dense:

def Dense(inputs,output_size,bias=True,seq_len=None):
    input_size=int(inputs.shape[-1])

    # shape=(input_size,output_size)均勻分佈,取值區間(-0.005,0.005)
    W=tf.Variable(tf.random_uniform([input_size,output_size],minval=-0.005,maxval=0.005))

    # 是否使用'b'
    # (咱們在使用BN的時候,'b'能夠不使用。緣由是,BN改變數據分佈,'b'的調解會被BN覆蓋)
    if bias == True:
        b=tf.Variable(tf.random_uniform([output_size],minval=-0.005,maxval=0.005))
    else:
        b=0
    # reshape 到2維
    outputs=tf.matmul(tf.reshape(inputs,(-1,input_size)),W)+b

    # reshape 到3維
    outputs=tf.reshape(outputs,tf.concat([tf.shape(inputs)[:-1],[output_size]],axis=0))

    if seq_len != None:
        outputs=Mask(outputs,seq_len,'mul')
    return outputs

Attention:

def Attention(Q,K,V,nb_head,size_per_head,Q_len=None,V_len=None):

    # 對Q、K、V分別做線性映射

    Q = Dense(Q, nb_head * size_per_head, False)
    Q = tf.reshape(Q, (-1, tf.shape(Q)[1], nb_head, size_per_head))
    Q = tf.transpose(Q, [0, 2, 1, 3])
    K = Dense(K, nb_head * size_per_head, False)
    K = tf.reshape(K, (-1, tf.shape(K)[1], nb_head, size_per_head))
    K = tf.transpose(K, [0, 2, 1, 3])
    V = Dense(V, nb_head * size_per_head, False)
    V = tf.reshape(V, (-1, tf.shape(V)[1], nb_head, size_per_head))
    V = tf.transpose(V, [0, 2, 1, 3])

    # 計算內積,而後mask,而後softmax

    A = tf.matmul(Q, K, transpose_b=True) / tf.sqrt(float(size_per_head))
    A = tf.transpose(A, [0, 3, 2, 1])
    A = Mask(A, V_len, mode='add')
    A = tf.transpose(A, [0, 3, 2, 1])
    A = tf.nn.softmax(A)

    # 輸出並mask

    O = tf.matmul(A, V)
    O = tf.transpose(O, [0, 2, 1, 3])
    O = tf.reshape(O, (-1, tf.shape(O)[1], nb_head * size_per_head))
    O = Mask(O, Q_len, 'mul')

    return O

總結

Transformer如今大有取代RNN之勢,但依然存在一些缺點。首先,Transformer雖然使用到了位置向量,可是對序列位置要求很高的項目作的並很差。

本文內容部分參考The Illustrated Transformer

相關文章
相關標籤/搜索