Transformer模型之Encoder结构在文本分类中的应用


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 构建编码器


编码器包含:

  • Input Embedding;

  • Positional Embedding;

  • N个编码层;

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科技园” 及 添加小编 进群交流。

喜欢的话请分享、点赞、在看吧~

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值