这个算是在课程学习之外的探索,不过希望能尽快用到项目实践中。在文章里会引用较多的博客,文末会进行reference。
搜索Transformer机制,会发现高分结果基本上都源于一篇论文Jay Alammar的《The Illustrated Transformer》(图解Transformer),提到最多的Attention是Google的《Attention Is All You Need》。
对于Transformer的运行机制了解即可,所以会基于这篇论文来学习Transformer,结合《Sklearn+Tensorflow》中Attention注意力机制一章完成基本的概念学习;- 找一个基于Transformer的项目练手
5.代码实现
构建Transformer模块
在这里,作者是严格按照《attention is all you need》中的推导步骤来做的。我们可以参考论文来进行学习。《attention is all you need》
本文还参考了整理 聊聊 Transformer
引入必要库
import numpy as np
import tensorflow as tf
实现层归一
参看论文3.1 Encoder and Decoder Stacks(编码和解码堆栈),Transformer由encoder和decoder构成。
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 在实现 Batch Normalization(各个网络层输出的归一化)时,主要用到nn.moments和batch_normalization
- moments作用是统计矩,mean 是一阶矩,variance 则是二阶中心矩
- tf.nn.moments 计算返回的 mean 和 variance 作为 tf.nn.batch_normalization 参数进一步调用
def ln(inputs, epsilon=1e-8, scope='ln'):
'''
使用层归一layer normalization
tensorflow 在实现 Batch Normalization(各个网络层输出的归一化)时,主要用到nn.moments和batch_normalization
其中moments作用是统计矩,mean 是一阶矩,variance 则是二阶中心矩
tf.nn.moments 计算返回的 mean 和 variance 作为 tf.nn.batch_normalization 参数进一步调用
:param inputs: 一个有2个或更多维度的张量,第一个维度是batch_size
:param epsilon: 很小的数值,防止区域划分错误
:param scope:
:return: 返回一个与inputs相同shape和数据的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
构建token嵌入
这里做的就是注意力,也就是加权值
def get_token_embeddings(vocab_size, num_units, zero_pad=True):
'''
构建token嵌入矩阵
:param vocab_size: 标量V
:param num_units: 嵌入维度E
:param zero_pad: 布尔值。如果为True,则第一行(id = 0)的所有值应为常数零
要轻松应用查询/键掩码,请打开零键盘。
:return: 权重参数(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
构建decoder的mask
mask 表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果。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
构建Context-Attention
查看原论文中3.2.1attention计算公式。
context-attention 是 encoder 和 decoder 之间的 attention,是两个不同序列之间的attention,与来源于自身的 self-attention 相区别。context-attention有很多,这里使用的是scaled dot-product。
通过 query 和 key 的相似性程度来确定 value 的权重分布。
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,就会对未来的数值进行遮盖
:param dropout_rate: 0到1之间的一个数值
: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
构建Multi-head attention
论文提到,他们发现将 Q、K、V 通过一个线性映射之后,分成 h 份,对每一份进行 scaled dot-product attention 效果更好。然后,把各个部分的结果合并起来,再次经过线性映射,得到最终的输出。这就是所谓的 multi-head attention。上面的超参数 h 就是 heads 的数量。论文默认是 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构建,这里是将不同的Queries、Keys和values方式线性地投影h次是有益的。线性投影分别为dk,dk和dv尺寸。在每个预计版本进行queries、keys、values,然后并行执行attention功能,产生dv维输出值。这些被连接并再次投影,产生最终值
: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数
: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
神经网络的前向传播
def ff(inputs, num_units,scope='positionwise_feedforward'):
'''
参看论文3.3,实现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,这会降低困惑,因为模型学习会更加不确定,提高了准确性和BLEU分数
:param inputs:
:param epsilon:
:return:
'''
V = inputs.get_shape().as_list()[-1] # number of channels
return ((1 - epsilon) * inputs) + (epsilon / V)
实现Positional Embedding
现在的 Transformer 架构还没有提取序列顺序的信息,这个信息对于序列而言非常重要,如果缺失了这个信息,可能我们的结果就是:所有词语都对了,但是无法组成有意义的语句。
因此,模型对序列中的词语出现的位置进行编码。在偶数位置,使用正弦编码,在奇数位置,使用余弦编码。
def positional_encoding(inputs,maxlen,masking=True,scope="positional_encoding"):
'''
参看论文3.5,由于模型没有循环和卷积,为了让模型知道句子的编号,就必须加入某些绝对位置信息,来表示token之间的关系。
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)
# 根据论文给的公式,构造出PE矩阵
position_enc = np.array([
[pos / np.power(10000, (i - i % 2) / E) for i in range(E)]
for pos in range(maxlen)])
# 在偶数位置,使用正弦编码,在奇数位置,使用余弦编码。
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计划学习率衰减
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)
最关键的模块构建到这里就完成了,总结一下会发现总共有以下九个模块。
- ln:层归一模块
- get_token_embeddings:token嵌入模块
- positional_encoding:位置模块
- mask:掩码模块
- scaled_dot_product_attention:自注意(self-attention)模块
- multihead_attention:多头注意(multi-head attention)模块
- ff:前向传播模块
- label_smoothing:标签平滑模块
- noam_scheme