1
前言
首先看一下 Attention Is All You Need 一文中的整体结构,从整体到局部的看待问题。
1.1 Transformer 结构
首先将 Transformer 结构看作一个单独的黑盒,它作为一个整体的模型结构而存在。在机器翻译应用场景中,它需要一种语言的句子作为输入,然后输出另一种语言的翻译。Transformer 结构主要包含:编码(Encoders)和解码(Decoders)两部分,如下图所示:
本文只介绍 Encoder 结构的代码实现及应用。
1.2 Encoder 结构
Encoder结构中包含:
Input层:输入训练数据;
Embedding层:输入数据进行Embedding编码;
Positional Encoding层:对输入数据进行位置编码;
Nx层:表示多个EncoderLayer层;
a)Multi-Head Attention层:对输入的Embedding数据分成分为多个头,形成多个子空间,让模型去关注不同方面的信息,最后再将各个方面的信息综合起来。该方式能够起到增强模型的作用,也可以类比CNN中同时使用多个卷积核的作用,有助于网络捕捉到更丰富的特征和信息;
b)Add&Norm层:Add:将输入的Embedding数据和 a)中的Multi-Head Attention结果相加,减少信息的损失和防止梯度消失等问题。与ResNet残差网络有异曲同工之妙。Norm:对同一层网络的输出做一个标准化处理,使得LN不受batch size的影响;
c)Feed Forword层:前向网络层;
d)同 b);
2
Encoder结构中模块代码详解
2.1 首先定义一下位置编码
将位置编码矢量添加得到词嵌入,相同位置的词嵌入将会更接近,但并不能直接编码相对位置。基于角度的位置编码方法如下:
其中:
表示:单词所处的位置,从0到输入序列的最大长度;
表示:单词的Embedding编码所处的位置,从0到 ;
表示:单词编码的Embedding长度;
PositionalEncoding层的代码如下:
class PositionalEncoding(tf.keras.layers.Layer):
def __init__(self, sequence_len=None, embedding_dim=None, mode='sum', **kwargs):
self.sequence_len = sequence_len
self.embedding_dim = embedding_dim
self.mode = mode
super(PositionalEncoding, self).__init__(**kwargs)
def call(self, x):
if (self.embedding_dim == None) or (self.mode == 'sum'):
self.embedding_dim = int(x.shape[-1])
position_embedding = np.array([
[pos / np.power(10000, 2. * i / self.embedding_dim) for i in range(self.embedding_dim)]
for pos in range(self.sequence_len)])
position_embedding[:, 0::2] = np.sin(position_embedding[:, 0::2]) # dim 2i
position_embedding[:, 1::2] = np.cos(position_embedding[:, 1::2]) # dim 2i+1
position_embedding = tf.cast(position_embedding, dtype=tf.float32)
if self.mode == 'sum':
return position_embedding + x
elif self.mode == 'concat':
position_embedding = tf.reshape(
tf.tile(position_embedding, (int(x.shape[0]), 1)),
(-1, self.sequence_len, self.embedding_dim)
)
return tf.concat([position_embedding, x], 2)
def compute_output_shape(self, input_shape):
if self.mode == 'sum':
return input_shape
elif self.mode == 'concat':
return (input_shape[0], input_shape[1], input_shape[2]+self.embedding_dim)
2.2 定义 padding mask 功能函数
在NLP任务中,需要处理的文本一般是不定长的,所以在进行 batch训练之前,要先进行长度的统一,过长的句子可以通过truncating 截断到固定的长度,过短的句子可以通过 padding 增加到固定的长度,但是 padding 对应的字符只是为了统一长度,并没有实际的价值,因此希望在之后的计算中屏蔽它们,这时候就需要 Mask。
为了避免输入中padding的token对句子语义的影响,需要将padding位mark掉,原来为0的padding项的mark输出为1。
padding mask 的代码如下:
def padding_mask(seq):
# 获取为 0的padding项
seq = tf.cast(tf.math.equal(seq, 0), tf.float32)
# 扩充维度用于attention矩阵
return seq[:, np.newaxis, np.newaxis, :] # (batch_size, 1, 1, seq_len)
2.3 定义Multi-Head Attention层
公式表示为:
其中:
Multi-Head Attention包含3部分:
线性层,及分头;
缩放点积注意力层;
多头合并,及线性层;
每个多头注意块有三个输入; Q(Query),K(Key),V(Value)。它们通过第一层线性层并分成多个头。
Q,K和V不是一个单独的注意头,而是分成多个头,因为它允许模型共同参与来自不同表征空间的不同信息。在拆分之后,每个头部具有降低的维度,总计算成本与具有全维度的单个头部注意力相同。
注意:点积注意力时需要使用mask, 多头输出需要使用tf.transpose调整各维度。
# 构造 multi head attention 层
class MultiHeadAttention(tf.keras.layers.Layer):
def __init__(self, d_model, num_heads):
super(MultiHeadAttention, self).__init__()
self.num_heads = num_heads
self.d_model = d_model
# d_model 必须可以正确分为各个头
assert d_model % num_heads == 0
# 分头后的维度
self.depth = d_model // num_heads
self.wq = tf.keras.layers.Dense(d_model)
self.wk = tf.keras.layers.Dense(d_model)
self.wv = tf.keras.layers.Dense(d_model)
self.dense = tf.keras.layers.Dense(d_model)
def split_heads(self, x, batch_size):
# 分头, 将头个数的维度 放到 seq_len 前面
x = tf.reshape(x, (batch_size, -1, self.num_heads, self.depth))
return tf.transpose(x, perm=[0, 2, 1, 3])
def call(self, inputs):
q, k, v, mask = inputs
batch_size = tf.shape(q)[0]
# 分头前的前向网络,获取q、k、v语义
q = self.wq(q) # (batch_size, seq_len, d_model)
k = self.wk(k)
v = self.wv(v)
# 分头
q = self.split_heads(q, batch_size) # (batch_size, num_heads, seq_len_q, depth)
k = self.split_heads(k, batch_size) # (batch_size, num_heads, seq_len_k, depth)
v = self.split_heads(v, batch_size) # (batch_size, num_heads, seq_len_v, depth)
# 通过缩放点积注意力层
scaled_attention = scaled_dot_product_attention(q, k, v, mask) # (batch_size, num_heads, seq_len_q, depth)
# “多头维度” 后移
scaled_attention = tf.transpose(scaled_attention, [0, 2, 1, 3]) # (batch_size, seq_len_q, num_heads, depth)
# 合并 “多头维度”
concat_attention = tf.reshape(scaled_attention, (batch_size, -1, self.d_model))
# 全连接层
output = self.dense(concat_attention)
return output
其中:scaled_dot_product_attention结构如下:
公式表达为:
相应的代码为:
def scaled_dot_product_attention(q, k, v, mask):
matmul_qk = tf.matmul(q, k, transpose_b=True)
dim_k = tf.cast(tf.shape(k)[-1], tf.float32)
scaled_attention_logits = matmul_qk / tf.math.sqrt(dim_k)
if mask is not None:
scaled_attention_logits += (mask * -1e9)
attention_weights = tf.nn.softmax(scaled_attention_logits, axis=-1)
output = tf.matmul(attention_weights, v)
return output
2.4 Feed Forword前向网络层
Feed Forword前向网络表达式为:
包含两层全连接层,默认使用ReLU激活函数。
def point_wise_feed_forward_network(d_model, middle_units):
return tf.keras.Sequential([
tf.keras.layers.Dense(middle_units, activation='relu'),
tf.keras.layers.Dense(d_model)])
2.5 构建编码层
每个编码层包含以下子层:
Multi-head attention(带掩码)
Point wise feed forward networks
每个子层中都有残差连接,并最后通过一个正则化层。残差连接有助于避免深度网络中的梯度消失问题。每个子层输出是LayerNorm(x + Sublayer(x)),规范化是在d_model维的向量上。Transformer一共有n个编码层。
class EncoderLayer(tf.keras.layers.Layer):
def __init__(self, d_model, num_heads, middle_units, epsilon=1e-6, dropout_rate=0.1):
super(EncoderLayer, self).__init__()
self.mha = MultiHeadAttention(d_model, num_heads)
self.ffn = point_wise_feed_forward_network(d_model, middle_units)
self.layernorm1 = LayerNormalization()
self.layernorm2 = LayerNormalization()
self.dropout1 = tf.keras.layers.Dropout(dropout_rate)
self.dropout2 = tf.keras.layers.Dropout(dropout_rate)
def call(self, inputs, mask, training):
# 多头注意力网络
att_output = self.mha([inputs, inputs, inputs, mask])
att_output = self.dropout1(att_output, training=training)
out1 = self.layernorm1(inputs + att_output) # (batch_size, input_seq_len, d_model)
# 前向网络
ffn_output = self.ffn(out1)
ffn_output = self.dropout2(ffn_output, training=training)
out2 = self.layernorm2(out1 + ffn_output) # (batch_size, input_seq_len, d_model)
return out2
2.6 构建编码器
class Encoder(tf.keras.layers.Layer):
def __init__(self, n_layers, d_model, num_heads, middle_units,
max_seq_len, epsilon=1e-6, dropout_rate=0.1):
super(Encoder, self).__init__()
self.n_layers = n_layers
self.d_model = d_model
self.pos_embedding = PositionalEncoding(sequence_len=max_seq_len, embedding_dim=d_model)
self.encode_layer = [EncoderLayer(d_model=d_model, num_heads=num_heads,
middle_units=middle_units,
epsilon=epsilon, dropout_rate=dropout_rate)
for _ in range(n_layers)]
def call(self, inputs, mask, training):
emb = inputs
emb = self.pos_embedding(emb)
for i in range(self.n_layers):
emb = self.encode_layer[i](emb, mask, training)
return emb
3
使用Encoder模型结构做文本分类
IMDB数据集是Keras内部集成的电影评语数据集,这数据集包含了50000条偏向明显的评论,其中25000条作为训练集,25000作为测试集。label为pos(positive)和neg(negative)。
3.1 数据预处理
# 文本分类实验
from tensorflow.keras.preprocessing import sequence
from tensorflow.keras.datasets import imdb
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import SGD, Adam
from tensorflow.keras.layers import *
# 1. 数据信息
max_features = 20000
maxlen = 64
batch_size = 32
print('Loading data...')
(x_train, y_train), (x_test, y_test) = imdb.load_data(path="imdb.npz", \
num_words=max_features)
y_train, y_test = pd.get_dummies(y_train), pd.get_dummies(y_test)
print(len(x_train), 'train sequences')
print(len(x_test), 'test sequences')
x_train = sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = sequence.pad_sequences(x_test, maxlen=maxlen)
print('x_train shape:', x_train.shape)
print('x_test shape:', x_test.shape)
3.2 构造模型,及训练模型
# 2. 构造模型,及训练模型
inputs = Input(shape=(64,), dtype='int32')
embeddings = Embedding(max_features, 128)(inputs)
print("\n"*2)
print("embeddings:")
print(embeddings)
mask_inputs = padding_mask(inputs)
out_seq = Encoder(2, 128, 4, 256, maxlen)(embeddings, mask_inputs, False)
print("\n"*2)
print("out_seq:")
print(out_seq)
out_seq = GlobalAveragePooling1D()(out_seq)
print("\n"*2)
print("out_seq:")
print(out_seq)
out_seq = Dropout(0.3)(out_seq)
outputs = Dense(64, activation='relu')(out_seq)
out_seq = Dropout(0.3)(out_seq)
outputs = Dense(16, activation='relu')(out_seq)
out_seq = Dropout(0.3)(out_seq)
outputs = Dense(2, activation='softmax')(out_seq)
model = Model(inputs=inputs, outputs=outputs)
print(model.summary())
opt = Adam(lr=0.0002, decay=0.00001)
loss = 'categorical_crossentropy'
model.compile(loss=loss,
optimizer=opt,
metrics=['accuracy'])
print('Train...')
history = model.fit(x_train, y_train,
batch_size=batch_size,
epochs=10,
validation_data=(x_test, y_test))
模型参数信息:
模型训练情况:
具体代码已上传到Github,地址为:https://github.com/wziji/Transformer
欢迎关注 “python科技园” 及 添加小编 进群交流。
喜欢的话请分享、点赞、在看吧~