Transformer解讀


本文結合原理和源代碼分析Google提出的Transformer機制

首先看一些Transformer的整體結構:

 

 inputs:[batch_size,maxlen] #maxlen表示source文本的最大長度

 經過一次Embedding,首先根據隱藏節點的數目將inputs的維度變成[batch_size,maxlen,num_units]

def embedding(lookup_table,inputs,num_units,scale=False,scope='embedding',reuse=None):
    """
       查詢子詞向量
       :param lookup_table:
       :param inputs:
       :param num_units:
       :param scale:
       :param scope:
       :param reuse:
       :return: 詞向量表示的輸入
       """
    outputs = tf.nn.embedding_lookup(lookup_table, inputs)

    # 根據num_units對outputs進行縮放
    if scale:
        outputs = outputs * (num_units ** 0.5)

    return outputs

接下來由於Transformer舍去了RNN或CNN的結構,也就失去了序列的位置信息,因此需要對輸入進行位置編碼,論文中

def positional_encoding(inputs, num_units, zero_pad=True, scale=True, scope="positional_encoding", reuse=None):

    """
    位置編碼
    :param inputs:
    :param num_units:
    :param zero_pad:
    :param scale:
    :param scope:
    :param reuse:
    :return:
    """

    N, T = inputs.get_shape().as_list()
    with tf.variable_scope(scope, reuse=reuse):
        position_ind = tf.tile(tf.expand_dims(tf.range(T), 0), [N, 1])

        # First part of the PE function: sin and cos argument
        position_enc = np.array([
            [pos / np.power(10000, 2.*i/num_units) for i in range(num_units)]
            for pos in range(T)])

        # Second part, apply the cosine to even columns and sin to odds.
        position_enc[:, 0::2] = np.sin(position_enc[:, 0::2])  # dim 2i
        position_enc[:, 1::2] = np.cos(position_enc[:, 1::2])  # dim 2i+1

        # Convert to a tensor
        lookup_table = tf.convert_to_tensor(position_enc)

        if zero_pad:
            lookup_table = tf.concat((tf.zeros(shape=[1, num_units]),
                                      lookup_table[1:, :]), 0)
        outputs = tf.nn.embedding_lookup(lookup_table, position_ind)

        if scale:
            outputs = outputs * num_units**0.5

        return outputs

接下來是論文的核心:Self-attention機制,在編碼端,Q,K,V的值是相同的。作者並沒有僅使用一個attention,而是使用了多個attention,稱為Multi-Head Attention,具體的實現方式為:

(1)將Q,K,V輸入到8個Self-Attention中,得到8個加權后的矩陣Zi

(2)將8個Zi拼接成一個大的特征矩陣(按列拼接)

(3)經過一層全連接得到輸出Z

 下圖為一個Self-Attention的計算方式

 

 這里的mask操作體現在源碼中對keys和query的屏蔽,方式都是將0的位置變為極小的數(接近於0),這樣經過softmax后會變成一個接近於0的數字

 key masking目的是讓key值的unit為0的key對應的attention score極小,這樣加權計算value時相當於對結果不產生影響。Query Masking 要屏蔽的是被<PAD>所填充的內容。

 #Key Maksing
        #這里的目的是讓key值的unit為0的key對應的attention score極小,這樣加權計算value時相當於對結果不產生影響
        key_masks=tf.sign(tf.abs(tf.reduce_mean(keys,axis=-1))) #[N,T_k]
        key_masks=tf.tile(key_masks,[num_heads,1])  #[h*N,T_k]
        key_masks=tf.tile(tf.expand_dims(key_masks,1),[1,tf.shape(queries)[1],1]) #[h*N,T_q,T_k]

 # Query Masking
        # 要被屏蔽的,是本身不懈怠信息或暫時不利用其信息的內容
        # query mask要將初始值為0的queries屏蔽
        query_masks = tf.sign(tf.abs(tf.reduce_sum(queries, axis=-1)))  # (N, T_q)
        query_masks = tf.tile(query_masks, [num_heads, 1])  # (h*N, T_q)
        query_masks = tf.tile(tf.expand_dims(query_masks, -1), [1, 1, tf.shape(keys)[1]])  # (h*N, T_q, T_k)
        # 前三步與Key Mask方法類似,這里用softmax后的outputs的值與query_masks相乘,因此這一步后需要mask的權值會乘0,不需要mask
        # 的乘以之前取的正數的sign為1所以權值不變,從而實現了query_mask目的
        outputs *= query_masks  # broadcasting. (N, T_q, C)

對比於解碼器端,我們發現解碼器端有兩個Multi-Head,第一個Multi-Head使用了mask操作。原因在於我們訓練的過程中,預測當前的值,是不能看到未來的詞的,作者的實現方式是通過一個下三角矩陣

  # Causality = Future blinding
        # Causality 標識是否屏蔽未來序列的信息(解碼器self attention的時候不能看到自己之后的哪些信息)
        # 這里通過下三角矩陣的方式進行,依此表示預測第一個詞,第二個詞,第三個詞...
        if causality:
            diag_vals = tf.ones_like(outputs[0, :, :])  # (T_q, T_k)
            tril = tf.linalg.LinearOperatorLowerTriangular(diag_vals).to_dense()  # (T_q, T_k)
            masks = tf.tile(tf.expand_dims(tril, 0), [tf.shape(outputs)[0], 1, 1])  # (h*N, T_q, T_k)

            paddings = tf.ones_like(masks) * (-2 ** 32 + 1)
            outputs = tf.where(tf.equal(masks, 0), paddings, outputs)  # (h*N, T_q, T_k)

對於第一個MultiHead Attention,Q,K,V都是解碼器的輸入,在訓練階段是目標(target),在預測階段因為沒有信息,全部填充<PAD>進行替代。

對於第二個MultiHead Attention,Q是第一個MultiHead Attention的輸出,K和V都是編碼器段的輸出。

上述全部過程的源代碼如下:

def multihead_attention(queries,keys,num_units=None,num_heads=8,dropout_rate=0,is_training=True,
                        causality=False,scope='multihead_attention',reuse=None):
    """
    :param queries:[batch_size,maxlen,hidden_unit]
    :param keys: 和value的值相同
    :param num_units:縮放因子,attention的大小
    :param num_heads:8
    :param dropout_rate:
    :param is_training:
    :param causality: 如果為True的話表明進行attention的時候未來的units都被屏蔽
    :param scope:
    :param reuse:
    :return:[bactch_size,maxlen,hidden_unit]
    """
    with tf.variable_scope(scope,reuse=reuse):
        # Set the fall back option for num_units
        if num_units is None:
            num_units = queries.get_shape().as_list[-1]
        #線性映射
        #先對Q,K,V進行全連接變化
        Q=tf.layers.dense(queries,num_units,activation=tf.nn.relu)
        K=tf.layers.dense(keys,num_units,activation=tf.nn.relu)
        V=tf.layers.dense(keys,num_units,activation=tf.nn.relu)

        #Split and concat
        #將上一步的Q,K,V從最后一維分成num_heads=8份,並把分開的張量在第一個維度拼接起來
        Q_=tf.concat(tf.split(Q,num_heads,axis=2),axis=0)  #[h*N,T_q,C/h]
        K_=tf.concat(tf.split(K,num_heads,axis=2),axis=0)  #[h*N,T_k,C/h]
        V_=tf.concat(tf.split(V,num_heads,axis=2),axis=0)  #[h*N,T_k,C/h]

        #Multiplication
        #Q*K轉置:  這一步將K轉置后和Q_進行了矩陣乘法的操作,也就是在通過點成方法進行attention score的計算
        outputs=tf.matmul(Q_,tf.transpose(K_,[0,2,1])) #[0,2,1]表示第二維度和第三維度進行交換[h*N,C/h,T_k] 三維矩陣相乘變為[h*N,T_q,T_k]

        #scale 除以調節因子
        outputs=outputs/(K_.get_shape().as_list()[-1]**0.5)  #開根號

        #Key Maksing
        #這里的目的是讓key值的unit為0的key對應的attention score極小,這樣加權計算value時相當於對結果不產生影響
        key_masks=tf.sign(tf.abs(tf.reduce_mean(keys,axis=-1))) #[N,T_k]
        key_masks=tf.tile(key_masks,[num_heads,1])  #[h*N,T_k]
        key_masks=tf.tile(tf.expand_dims(key_masks,1),[1,tf.shape(queries)[1],1]) #[h*N,T_q,T_k]

        paddings=tf.ones_like(outputs)*(-2 **32+1) #創建一個全為1的tensor變量
        outputs=tf.where(tf.equal(key_masks,0),paddings,outputs) #對為0的位置,用很小的padding進行替代 為1的地方返回padding,否則返回outputs

        # Causality = Future blinding
        # Causality 標識是否屏蔽未來序列的信息(解碼器self attention的時候不能看到自己之后的哪些信息)
        # 這里通過下三角矩陣的方式進行,依此表示預測第一個詞,第二個詞,第三個詞...
        if causality:
            diag_vals = tf.ones_like(outputs[0, :, :])  # (T_q, T_k)
            tril = tf.linalg.LinearOperatorLowerTriangular(diag_vals).to_dense()  # (T_q, T_k)
            masks = tf.tile(tf.expand_dims(tril, 0), [tf.shape(outputs)[0], 1, 1])  # (h*N, T_q, T_k)

            paddings = tf.ones_like(masks) * (-2 ** 32 + 1)
            outputs = tf.where(tf.equal(masks, 0), paddings, outputs)  # (h*N, T_q, T_k)

        # Activation
        # 對Attention score進行softmax操作
        outputs = tf.nn.softmax(outputs)  # (h*N, T_q, T_k)

        # Query Masking
        # 要被屏蔽的,是本身不懈怠信息或暫時不利用其信息的內容
        # query mask要將初始值為0的queries屏蔽
        query_masks = tf.sign(tf.abs(tf.reduce_sum(queries, axis=-1)))  # (N, T_q)
        query_masks = tf.tile(query_masks, [num_heads, 1])  # (h*N, T_q)
        query_masks = tf.tile(tf.expand_dims(query_masks, -1), [1, 1, tf.shape(keys)[1]])  # (h*N, T_q, T_k)
        # 前三步與Key Mask方法類似,這里用softmax后的outputs的值與query_masks相乘,因此這一步后需要mask的權值會乘0,不需要mask
        # 的乘以之前取的正數的sign為1所以權值不變,從而實現了query_mask目的
        outputs *= query_masks  # broadcasting. (N, T_q, C)

        # Dropouts
        outputs = tf.layers.dropout(outputs, rate=dropout_rate, training=tf.convert_to_tensor(is_training))

        # Weighted sum
        # 用outputs和V_加權和計算出多個頭attention的結果
        outputs = tf.matmul(outputs, V_)  # ( h*N, T_q, C/h)

        # Restore shape
        # 上步得到的是多頭attention的結果在第一個維度疊着,所以把它們split開重新concat到最后一個維度上
        outputs = tf.concat(tf.split(outputs, num_heads, axis=0), axis=2)  # (N, T_q, C)

        # Residual connection
        # outputs加上一開始的queries, 是殘差的操作
        outputs += queries

        # Normalize
        outputs = normalize(outputs)  # (N, T_q, C)

    return outputs

最后經過一個前向神經網絡和layer Norm操作,我們可以得到最終輸出

在前向神經網絡部分,用一維卷積替代全連接操作:

def feedforward(inputs,num_units=[2048,512],scope='multihead_attention',reuse=None):
    """
    將多頭attention的輸出送入全連接網絡
    :param inputs:  shape的形狀為[N,T,C]
    :param num_units:
    :param scope:
    :param reuse:
    :return:
    """
    with tf.variable_scope(scope,reuse=reuse):
        # Inner layer
        params={
            "inputs":inputs,
            "filters":num_units[0],
            "kernel_size":1,
            "activation":tf.nn.relu,
            "use_bias":True
        }
        #利用一維卷積進行網絡的設計
        outputs=tf.layers.conv1d(**params) #一維卷積最后一個維度變為2048

        #Readout layer
        params={
            "inputs": outputs,
            "filters": num_units[1],
            "kernel_size": 1,
            "activation": None,
            "use_bias": True
        }
        outputs=tf.layers.conv1d(**params)

        #Residual connection
        #加上inputs的殘差
        outputs+=inputs

        #Normalize
        outputs=normalize(outputs)

    return outputs

layer normalization:(歸一化,模型優化)https://zhuanlan.zhihu.com/p/33173246

def normalize(inputs,epsilon=1e-8,scope="ln",reuse=None):
    """
    :param inputs:[batch_size,..]2維或多維
    :param epsilon:
    :param scope:
    :param reuse:是否重復使用權重
    :return: A tensor with the same shape and data dtype as `inputs`.
    """
    with tf.variable_scope(scope,reuse=reuse):
        inputs_shape=inputs.get_shape()
        params_shape=inputs_shape[-1:]

        mean,variance=tf.nn.moments(inputs,[-1],keep_dims=True)
        beta=tf.Variable(tf.zeros(params_shape))
        gamma=tf.Variable(tf.ones(params_shape))
        normalized=(inputs-mean)/((variance+epsilon)**(.5))
        outputs=gamma*normalized+beta

    return outputs

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM