自然語言處理N天-實現(xiàn)一個Transformer構(gòu)建模塊

新建 Microsoft PowerPoint 演示文稿 (2).jpg

這個算是在課程學(xué)習(xí)之外的探索,不過希望能盡快用到項目實踐中。在文章里會引用較多的博客,文末會進行reference。
搜索Transformer機制,會發(fā)現(xiàn)高分結(jié)果基本上都源于一篇論文Jay Alammar的《The Illustrated Transformer》(圖解Transformer),提到最多的Attention是Google的《Attention Is All You Need》。

  • 對于Transformer的運行機制了解即可,所以會基于這篇論文來學(xué)習(xí)Transformer,結(jié)合《Sklearn+Tensorflow》中Attention注意力機制一章完成基本的概念學(xué)習(xí);
  • 找一個基于Transformer的項目練手

5.代碼實現(xiàn)

構(gòu)建Transformer模塊

在這里,作者是嚴(yán)格按照《attention is all you need》中的推導(dǎo)步驟來做的。我們可以參考論文來進行學(xué)習(xí)。《attention is all you need》
本文還參考了整理 聊聊 Transformer

引入必要庫
import numpy as np
import tensorflow as tf
實現(xiàn)層歸一

參看論文3.1 Encoder and Decoder Stacks(編碼和解碼堆棧),Transformer由encoder和decoder構(gòu)成。
encoder由6個相同的層組成,每一層分別由2部分組成:

  • 第一部分是 multi-head self-attention
  • 第二部分是 position-wise feed-forward network,是一個全連接層
    在每兩個子層(sub-layers)之間使用殘差連接(residual connection),再接一個層歸一(layer normalization)

decoder由6個相同的層組成,每一層分別由3部分組成:

  • 第一個部分是 multi-head self-attention mechanism
  • 第二部分是 multi-head context-attention mechanism
  • 第三部分是一個 position-wise feed-forward network
    在每三個子層(sub-layers)之間使用殘差連接(residual connection),再接一個層歸一(layer normalization)

tensorflow 在實現(xiàn) Batch Normalization(各個網(wǎng)絡(luò)層輸出的歸一化)時,主要用到nn.moments和batch_normalization

  • moments作用是統(tǒng)計矩,mean 是一階矩,variance 則是二階中心矩
  • tf.nn.moments 計算返回的 mean 和 variance 作為 tf.nn.batch_normalization 參數(shù)進一步調(diào)用
def ln(inputs, epsilon=1e-8, scope='ln'):
    '''
    使用層歸一layer normalization
    tensorflow 在實現(xiàn) Batch Normalization(各個網(wǎng)絡(luò)層輸出的歸一化)時,主要用到nn.moments和batch_normalization
    其中moments作用是統(tǒng)計矩,mean 是一階矩,variance 則是二階中心矩
    tf.nn.moments 計算返回的 mean 和 variance 作為 tf.nn.batch_normalization 參數(shù)進一步調(diào)用
    :param inputs: 一個有2個或更多維度的張量,第一個維度是batch_size
    :param epsilon: 很小的數(shù)值,防止區(qū)域劃分錯誤
    :param scope: 
    :return: 返回一個與inputs相同shape和數(shù)據(jù)的dtype
    '''
    with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
        inputs_shape = inputs.get_shape()
        params_shape = inputs_shape[-1:]

        mean, variance = tf.nn.moments(inputs, [-1], keep_dims=True)
        beta = tf.get_variable("beta", params_shape, initializer=tf.ones_initializer())
        gamma = tf.get_variable("gamma", params_shape, initializer=tf.ones_initializer())

        normalized = (inputs - mean) / ((variance + epsilon) ** (.5))
        outputs = gamma * normalized + beta
    return outputs
構(gòu)建token嵌入

這里做的就是注意力,也就是加權(quán)值

def get_token_embeddings(vocab_size, num_units, zero_pad=True):
    '''
    構(gòu)建token嵌入矩陣
    :param vocab_size: 標(biāo)量V
    :param num_units: 嵌入維度E
    :param zero_pad: 布爾值。如果為True,則第一行(id = 0)的所有值應(yīng)為常數(shù)零
    要輕松應(yīng)用查詢/鍵掩碼,請打開零鍵盤。
    :return: 權(quán)重參數(shù)(V,E)
    '''
    with tf.variable_scope("shared_weight_matrix"):
        embeddings = tf.get_variable(
            'weight_mat',
            dtype=tf.float32,
            shape=(vocab_size, num_units),
            initializer=tf.contrib.layers.xavier_initializer()
        )
        if zero_pad:
            embeddings = tf.concat((tf.zeros(shape=[1, num_units]), embeddings[1:, :]), 0)
    return embeddings
構(gòu)建decoder的mask

mask 表示掩碼,它對某些值進行掩蓋,使其在參數(shù)更新時不產(chǎn)生效果。Transformer 模型里面涉及兩種 mask,分別是 padding mask 和 sequence mask。

  • padding mask 在所有的 scaled dot-product attention 里面都需要用到
  • sequence mask 只有在 decoder 的 self-attention 里面用到
def mask(inputs, queries=None, keys=None, type=None):
    '''
    對Keys或Queries進行遮蓋
    :param inputs: (N, T_q, T_k)
    :param queries: (N, T_q, d)
    :param keys: (N, T_k, d)
    :return: 
    '''
    padding_num = -2 ** 32 + 1
    if type in ("k", "key", "keys"):
        # Generate masks
        masks = tf.sign(tf.reduce_sum(tf.abs(keys), axis=-1))  # (N, T_k)
        masks = tf.expand_dims(masks, 1)  # (N, 1, T_k)
        masks = tf.tile(masks, [1, tf.shape(queries)[1], 1])  # (N, T_q, T_k)

        # Apply masks to inputs
        paddings = tf.ones_like(inputs) * padding_num
        outputs = tf.where(tf.equal(masks, 0), paddings, inputs)  # (N, T_q, T_k)
    elif type in ("q", "query", "queries"):
        # Generate masks
        masks = tf.sign(tf.reduce_sum(tf.abs(queries), axis=-1))  # (N, T_q)
        masks = tf.expand_dims(masks, -1)  # (N, T_q, 1)
        masks = tf.tile(masks, [1, 1, tf.shape(keys)[1]])  # (N, T_q, T_k)

        # Apply masks to inputs
        outputs = inputs * masks
    elif type in ("f", "future", "right"):
        diag_vals = tf.ones_like(inputs[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(inputs)[0], 1, 1])  # (N, T_q, T_k)

        paddings = tf.ones_like(masks) * padding_num
        outputs = tf.where(tf.equal(masks, 0), paddings, inputs)
    else:
        print("Check if you entered type correctly!")

    return outputs
構(gòu)建Context-Attention

查看原論文中3.2.1attention計算公式。
context-attention 是 encoder 和 decoder 之間的 attention,是兩個不同序列之間的attention,與來源于自身的 self-attention 相區(qū)別。context-attention有很多,這里使用的是scaled dot-product。
通過 query 和 key 的相似性程度來確定 value 的權(quán)重分布。

def scaled_dot_product_attention(Q, K, V, causality=False, dropout_rate=0., training=True,
                                 scope='scaled_dot_product_attention'):
    '''
    查看原論文中3.2.1attention計算公式:Attention(Q,K,V)=softmax(Q K^T /√dk ) V
    :param Q: 查詢,三維張量,[N, T_q, d_k].
    :param K: keys值,三維張量,[N, T_k, d_v].
    :param V: values值,三維張量,[N, T_k, d_v].
    :param causality: 布爾值,如果為True,就會對未來的數(shù)值進行遮蓋
    :param dropout_rate: 0到1之間的一個數(shù)值
    :param training: 布爾值,用來控制dropout
    :param scope: 
    '''
    with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
        d_k = Q.get_shape().as_list()[-1]
        # dot product
        outputs = tf.matmul(Q, tf.transpose(K, [0, 2, 1]))  # (N, T_q, T_k)
        # scale
        outputs /= d_k ** 0.5
        # key mask
        outputs = mask(outputs, Q, K, type="key")
        # causality or future blinding masking
        if causality:
            outputs = mask(outputs, type='future')

        outputs = tf.nn.softmax(outputs)
        attention = tf.transpose(outputs, [0, 2, 1])
        tf.summary.image("attention", tf.expand_dims(attention[:1], -1))

        outputs = mask(outputs, Q, K, type="query")
        # dropout
        outputs = tf.layers.dropout(outputs, rate=dropout_rate, training=training)

        # weighted sum (context vectors)
        outputs = tf.matmul(outputs, V)  # (N, T_q, d_v)
    return outputs
構(gòu)建Multi-head attention

論文提到,他們發(fā)現(xiàn)將 Q、K、V 通過一個線性映射之后,分成 h 份,對每一份進行 scaled dot-product attention 效果更好。然后,把各個部分的結(jié)果合并起來,再次經(jīng)過線性映射,得到最終的輸出。這就是所謂的 multi-head attention。上面的超參數(shù) h 就是 heads 的數(shù)量。論文默認(rèn)是 8。

def multihead_attention(queries, keys, values,
                        num_heads=8,
                        dropout_rate=0,
                        training=True,
                        causality=False,
                        scope="multihead_attention"):
    '''
    查看原論文中3.2.2中multihead_attention構(gòu)建,這里是將不同的Queries、Keys和values方式線性地投影h次是有益的。線性投影分別為dk,dk和dv尺寸。在每個預(yù)計版本進行queries、keys、values,然后并行執(zhí)行attention功能,產(chǎn)生dv維輸出值。這些被連接并再次投影,產(chǎn)生最終值
    :param queries: 三維張量[N, T_q, d_model]
    :param keys: 三維張量[N, T_k, d_model]
    :param values: 三維張量[N, T_k, d_model]
    :param num_heads: heads數(shù)
    :param dropout_rate: 
    :param training: 控制dropout機制
    :param causality: 控制是否遮蓋
    :param scope: 
    :return: 三維張量(N, T_q, C) 
    '''
    d_model=queries.get_shape().as_list()[-1]
    with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
        # Linear projections
        Q = tf.layers.dense(queries, d_model)  # (N, T_q, d_model)
        K = tf.layers.dense(keys, d_model)  # (N, T_k, d_model)
        V = tf.layers.dense(values, d_model)  # (N, T_k, d_model)

        # Split and concat
        Q_ = tf.concat(tf.split(Q, num_heads, axis=2), axis=0)  # (h*N, T_q, d_model/h)
        K_ = tf.concat(tf.split(K, num_heads, axis=2), axis=0)  # (h*N, T_k, d_model/h)
        V_ = tf.concat(tf.split(V, num_heads, axis=2), axis=0)  # (h*N, T_k, d_model/h)

        # Attention
        outputs=scaled_dot_product_attention(Q_,K_,V_,causality,dropout_rate,training)

        outputs=tf.concat(tf.split(outputs,num_heads,axis=0),axis=2)

        outputs+= queries
        # 歸一
        outputs=ln(outputs)

    return outputs
神經(jīng)網(wǎng)絡(luò)的前向傳播
def ff(inputs, num_units,scope='positionwise_feedforward'):
    '''
    參看論文3.3,實現(xiàn)feed forward net
    :param inputs: 
    :param num_units: 
    :param scope: 
    :return: 
    '''
    with tf.variable_scope(scope,reuse=tf.AUTO_REUSE):
        # Inner layer
        outputs = tf.layers.dense(inputs, num_units[0], activation=tf.nn.relu)

        # Outer layer
        outputs = tf.layers.dense(outputs, num_units[1])

        # Residual connection
        outputs += inputs

        # Normalize
        outputs = ln(outputs)

    return outputs

def label_smoothing(inputs,epsilon=0.1):
    '''
    參看論文5.4,這會降低困惑,因為模型學(xué)習(xí)會更加不確定,提高了準(zhǔn)確性和BLEU分?jǐn)?shù)
    :param inputs: 
    :param epsilon: 
    :return: 
    '''
    V = inputs.get_shape().as_list()[-1]  # number of channels
    return ((1 - epsilon) * inputs) + (epsilon / V)
實現(xiàn)Positional Embedding

現(xiàn)在的 Transformer 架構(gòu)還沒有提取序列順序的信息,這個信息對于序列而言非常重要,如果缺失了這個信息,可能我們的結(jié)果就是:所有詞語都對了,但是無法組成有意義的語句。
因此,模型對序列中的詞語出現(xiàn)的位置進行編碼。在偶數(shù)位置,使用正弦編碼,在奇數(shù)位置,使用余弦編碼。

def positional_encoding(inputs,maxlen,masking=True,scope="positional_encoding"):
    '''
    參看論文3.5,由于模型沒有循環(huán)和卷積,為了讓模型知道句子的編號,就必須加入某些絕對位置信息,來表示token之間的關(guān)系。  
    positional encoding和embedding有相同的維度,這兩個能夠相加。
    :param inputs: 
    :param maxlen: 
    :param masking: 
    :param scope: 
    :return: 
    '''
    E = inputs.get_shape().as_list()[-1]  # static
    N, T = tf.shape(inputs)[0], tf.shape(inputs)[1]  # dynamic
    with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
        # position indices
        position_ind = tf.tile(tf.expand_dims(tf.range(T), 0), [N, 1])  # (N, T)

        # 根據(jù)論文給的公式,構(gòu)造出PE矩陣
        position_enc = np.array([
            [pos / np.power(10000, (i - i % 2) / E) for i in range(E)]
            for pos in range(maxlen)])

        # 在偶數(shù)位置,使用正弦編碼,在奇數(shù)位置,使用余弦編碼。
        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
        position_enc = tf.convert_to_tensor(position_enc, tf.float32)  # (maxlen, E)

        # lookup
        outputs = tf.nn.embedding_lookup(position_enc, position_ind)

        # masks
        if masking:
            outputs = tf.where(tf.equal(inputs, 0), inputs, outputs)

    return tf.to_float(outputs)
Noam計劃學(xué)習(xí)率衰減
def noam_scheme(init_lr, global_step, warmup_steps=4000.):
    '''
    
    :param init_lr: 
    :param global_step: 
    :param warmup_steps: 
    :return: 
    '''
    step = tf.cast(global_step + 1, dtype=tf.float32)
    return init_lr * warmup_steps ** 0.5 * tf.minimum(step * warmup_steps ** -1.5, step ** -0.5)

最關(guān)鍵的模塊構(gòu)建到這里就完成了,總結(jié)一下會發(fā)現(xiàn)總共有以下九個模塊。

  • ln:層歸一模塊
  • get_token_embeddings:token嵌入模塊
  • positional_encoding:位置模塊
  • mask:掩碼模塊
  • scaled_dot_product_attention:自注意(self-attention)模塊
  • multihead_attention:多頭注意(multi-head attention)模塊
  • ff:前向傳播模塊
  • label_smoothing:標(biāo)簽平滑模塊
  • noam_scheme
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容