
這個算是在課程學(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