注意力机制与Transformer#
循环网络的一个主要缺点是序列中的所有单词对结果的影响相同。这会导致标准的LSTM编码器-解码器模型在处理序列到序列任务(如命名实体识别和机器翻译)时表现不佳。实际上,输入序列中的某些特定单词对输出序列的影响往往比其他单词更大。
以序列到序列模型为例,比如机器翻译。它通过两个循环网络实现,其中一个网络(编码器)将输入序列压缩成隐藏状态,另一个网络(解码器)将隐藏状态展开成翻译结果。这种方法的问题在于,网络的最终状态很难记住句子的开头部分,从而导致模型在处理长句子时质量较差。
注意力机制提供了一种方法,可以对每个输入向量对RNN输出预测的上下文影响进行加权。它的实现方式是通过在输入RNN的中间状态和输出RNN之间创建快捷路径。这样,在生成输出符号$y_t$时,我们会考虑所有输入隐藏状态$h_i$,并赋予不同的权重系数$\alpha_{t,i}$。
Bahdanau等人,2015中的编码器-解码器模型,图片引用自这篇博客
注意力矩阵${\alpha_{i,j}}$表示某些输入单词在生成输出序列中某个单词时的影响程度。下面是一个这样的矩阵示例:

图片取自Bahdanau等人,2015(图3)
注意力机制是当前或接近当前自然语言处理领域的先进技术的核心。然而,添加注意力机制会显著增加模型参数的数量,这导致了循环网络的扩展问题。循环网络扩展的一个关键限制是模型的循环特性使得批量处理和训练并行化变得困难。在循环网络中,序列的每个元素都需要按顺序处理,这意味着它无法轻松并行化。
注意力机制的采用结合这一限制,促成了如今我们所熟知和使用的先进Transformer模型的诞生,从BERT到OpenGPT3。
Transformer模型#
与将每次预测的上下文传递到下一步评估不同,Transformer模型使用位置编码和注意力机制来捕获给定输入在提供的文本窗口中的上下文。下图展示了位置编码与注意力机制如何在给定窗口中捕获上下文。

由于每个输入位置可以独立映射到每个输出位置,Transformer比循环网络更容易并行化,这使得构建更大、更具表现力的语言模型成为可能。每个注意力头可以用来学习单词之间的不同关系,从而提升下游自然语言处理任务的效果。
构建简单的Transformer模型#
Keras中没有内置的Transformer层,但我们可以自己构建。和之前一样,我们将专注于AG News数据集的文本分类,但值得一提的是,Transformer模型在更复杂的自然语言处理任务中表现最佳。
import tensorflow as tf
from tensorflow import keras
import tensorflow_datasets as tfds
import numpy as np
ds_train, ds_test = tfds.load('ag_news_subset').values()
def extract_text(x):
return x['title']+' '+x['description']
def tupelize(x):
return (extract_text(x),x['label'])在 Keras 中,新层应继承 Layer 类,并实现 call 方法。让我们从 位置嵌入 层开始。我们将使用官方 Keras 文档中的一些代码。我们将假设我们将所有输入序列填充到长度 maxlen。
class TokenAndPositionEmbedding(keras.layers.Layer):
def __init__(self, maxlen, vocab_size, embed_dim):
super(TokenAndPositionEmbedding, self).__init__()
self.token_emb = keras.layers.Embedding(input_dim=vocab_size, output_dim=embed_dim)
self.pos_emb = keras.layers.Embedding(input_dim=maxlen, output_dim=embed_dim)
self.maxlen = maxlen
def call(self, x):
maxlen = self.maxlen
positions = tf.range(start=0, limit=maxlen, delta=1)
positions = self.pos_emb(positions)
x = self.token_emb(x)
return x+positions这一层由两个Embedding层组成:一个用于嵌入标记(以我们之前讨论的方式),另一个用于嵌入标记位置。标记位置是通过使用tf.range从0到maxlen生成的一系列自然数创建的,然后通过嵌入层处理。两个生成的嵌入向量随后相加,生成输入的基于位置嵌入的表示,其形状为maxlen$\times$embed_dim。
现在,让我们实现Transformer块。它将接收之前定义的嵌入层的输出:
class TransformerBlock(keras.layers.Layer):
def __init__(self, embed_dim, num_heads, ff_dim, rate=0.1):
super(TransformerBlock, self).__init__()
self.att = keras.layers.MultiHeadAttention(num_heads=num_heads, key_dim=embed_dim, name='attn')
self.ffn = keras.Sequential(
[keras.layers.Dense(ff_dim, activation="relu"), keras.layers.Dense(embed_dim),]
)
self.layernorm1 = keras.layers.LayerNormalization(epsilon=1e-6)
self.layernorm2 = keras.layers.LayerNormalization(epsilon=1e-6)
self.dropout1 = keras.layers.Dropout(rate)
self.dropout2 = keras.layers.Dropout(rate)
def call(self, inputs, training):
attn_output = self.att(inputs, inputs)
attn_output = self.dropout1(attn_output, training=training)
out1 = self.layernorm1(inputs + attn_output)
ffn_output = self.ffn(out1)
ffn_output = self.dropout2(ffn_output, training=training)
return self.layernorm2(out1 + ffn_output)现在,我们已经准备好定义完整的 Transformer 模型:
embed_dim = 32 # Embedding size for each token
num_heads = 2 # Number of attention heads
ff_dim = 32 # Hidden layer size in feed forward network inside transformer
maxlen = 256
vocab_size = 20000
model = keras.models.Sequential([
keras.layers.experimental.preprocessing.TextVectorization(max_tokens=vocab_size,output_sequence_length=maxlen, input_shape=(1,)),
TokenAndPositionEmbedding(maxlen, vocab_size, embed_dim),
TransformerBlock(embed_dim, num_heads, ff_dim),
keras.layers.GlobalAveragePooling1D(),
keras.layers.Dropout(0.1),
keras.layers.Dense(20, activation="relu"),
keras.layers.Dropout(0.1),
keras.layers.Dense(4, activation="softmax")
])
model.summary()Model: "sequential_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
text_vectorization (TextVect (None, 256) 0
_________________________________________________________________
token_and_position_embedding (None, 256, 32) 648192
_________________________________________________________________
transformer_block (Transform (None, 256, 32) 10656
_________________________________________________________________
global_average_pooling1d (Gl (None, 32) 0
_________________________________________________________________
dropout_2 (Dropout) (None, 32) 0
_________________________________________________________________
dense_2 (Dense) (None, 20) 660
_________________________________________________________________
dropout_3 (Dropout) (None, 20) 0
_________________________________________________________________
dense_3 (Dense) (None, 4) 84
=================================================================
Total params: 659,592
Trainable params: 659,592
Non-trainable params: 0
_________________________________________________________________
print('Training tokenizer')
model.layers[0].adapt(ds_train.map(extract_text))
model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer='adam')
model.fit(ds_train.map(tupelize).batch(128),validation_data=ds_test.map(tupelize).batch(128))Training tokenizer
938/938 [==============================] - 45s 39ms/step - loss: 0.4978 - acc: 0.8068 - val_loss: 0.2808 - val_acc: 0.9124
<tensorflow.python.keras.callbacks.History at 0x7f9c2427a0d0>BERT Transformer 模型#
BERT(双向编码器表示的变换器)是一个非常大的多层变换器网络,BERT-base 有 12 层,BERT-large 有 24 层。该模型首先在大规模文本数据(维基百科 + 书籍)上进行无监督训练(预测句子中被遮盖的单词)。在预训练过程中,模型吸收了大量的语言理解能力,随后可以通过微调与其他数据集结合使用。这一过程被称为 迁移学习。

变换器架构有许多变体,包括 BERT、DistilBERT、BigBird、OpenGPT3 等,这些都可以进行微调。
接下来,我们来看如何使用预训练的 BERT 模型来解决传统的序列分类问题。我们将借用 官方文档 中的一些思路和代码。
为了加载预训练模型,我们将使用 Tensorflow hub。首先,让我们加载 BERT 专用的向量化工具:
import tensorflow_text
import tensorflow_hub as hub
vectorizer = hub.KerasLayer('https://tfhub.dev/tensorflow/bert_en_uncased_preprocess/3')ModuleNotFoundError: No module named 'tensorflow_text'
[1;31m---------------------------------------------------------------------------[0m
[1;31mModuleNotFoundError[0m Traceback (most recent call last)
[1;32m~\AppData\Local\Temp/ipykernel_41180/4216669875.py[0m in [0;36m<module>[1;34m[0m
[1;32m----> 1[1;33m [1;32mimport[0m [0mtensorflow_text[0m[1;33m[0m[1;33m[0m[0m
[0m[0;32m 2[0m [1;32mimport[0m [0mtensorflow_hub[0m [1;32mas[0m [0mhub[0m[1;33m[0m[1;33m[0m[0m
[0;32m 3[0m [0mvectorizer[0m [1;33m=[0m [0mhub[0m[1;33m.[0m[0mKerasLayer[0m[1;33m([0m[1;34m'https://tfhub.dev/tensorflow/bert_en_uncased_preprocess/3'[0m[1;33m)[0m[1;33m[0m[1;33m[0m[0m
[1;31mModuleNotFoundError[0m: No module named 'tensorflow_text'vectorizer(['I love transformers']){'input_type_ids': <tf.Tensor: shape=(1, 128), dtype=int32, numpy=
array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
dtype=int32)>,
'input_word_ids': <tf.Tensor: shape=(1, 128), dtype=int32, numpy=
array([[ 101, 1045, 2293, 19081, 102, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0]], dtype=int32)>,
'input_mask': <tf.Tensor: shape=(1, 128), dtype=int32, numpy=
array([[1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
dtype=int32)>}使用与原始网络训练时相同的向量化工具是非常重要的。此外,BERT 向量化工具会返回三个组件:
input_word_ids,这是输入句子的标记编号序列input_mask,用于显示序列中哪些部分是实际输入,哪些部分是填充。这与Masking层生成的掩码类似input_type_ids,用于语言建模任务,允许在一个序列中指定两个输入句子。
然后,我们可以实例化 BERT 特征提取器:
bert = hub.KerasLayer('https://tfhub.dev/tensorflow/small_bert/bert_en_uncased_L-4_H-128_A-2/1')z = bert(vectorizer(['I love transformers']))
for i,x in z.items():
print(f"{i} -> { len(x) if isinstance(x, list) else x.shape }")pooled_output -> (1, 128)
encoder_outputs -> 4
sequence_output -> (1, 128, 128)
default -> (1, 128)
因此,BERT 层会返回一些有用的结果:
pooled_output是通过对序列中所有标记进行平均得到的结果。你可以将其视为整个网络的智能语义嵌入。它等同于我们之前模型中GlobalAveragePooling1D层的输出。sequence_output是最后一个 Transformer 层的输出(对应于我们上面模型中的TransformerBlock的输出)。encoder_outputs是所有 Transformer 层的输出。由于我们加载了一个 4 层的 BERT 模型(你可能已经从名字中包含的4_H猜到了),它有 4 个张量。最后一个张量与sequence_output相同。
现在我们将定义端到端的分类模型。我们将使用函数式模型定义,在定义模型输入后,通过一系列表达式计算其输出。我们还会将 BERT 模型的权重设置为不可训练,仅训练最终的分类器:
inp = keras.Input(shape=(),dtype=tf.string)
x = vectorizer(inp)
x = bert(x)
x = keras.layers.Dropout(0.1)(x['pooled_output'])
out = keras.layers.Dense(4,activation='softmax')(x)
model = keras.models.Model(inp,out)
bert.trainable = False
model.summary()Model: "model"
__________________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
==================================================================================================
input_1 (InputLayer) [(None,)] 0
__________________________________________________________________________________________________
keras_layer (KerasLayer) {'input_type_ids': ( 0 input_1[0][0]
__________________________________________________________________________________________________
keras_layer_1 (KerasLayer) {'pooled_output': (N 4782465 keras_layer[0][0]
keras_layer[0][1]
keras_layer[0][2]
__________________________________________________________________________________________________
dropout_4 (Dropout) (None, 128) 0 keras_layer_1[0][5]
__________________________________________________________________________________________________
dense_4 (Dense) (None, 4) 516 dropout_4[0][0]
==================================================================================================
Total params: 4,782,981
Trainable params: 516
Non-trainable params: 4,782,465
__________________________________________________________________________________________________
model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer='adam')
model.fit(ds_train.map(tupelize).batch(128),validation_data=ds_test.map(tupelize).batch(128))938/938 [==============================] - 528s 559ms/step - loss: 0.8056 - acc: 0.6983 - val_loss: 0.5953 - val_acc: 0.7888
<tensorflow.python.keras.callbacks.History at 0x7f9bb1e36d00>尽管可训练参数很少,但过程仍然相当缓慢,因为 BERT 特征提取器计算量很大。看起来我们无法达到合理的准确性,可能是由于训练不足或模型参数不足。
让我们尝试解冻 BERT 的权重并对其进行训练。这需要非常小的学习率,同时还需要更谨慎的训练策略,包括预热,并使用 AdamW 优化器。我们将使用 tf-models-official 包来创建优化器:
from official.nlp import optimization
bert.trainable=True
model.summary()
epochs = 3
opt = optimization.create_optimizer(
init_lr=3e-5,
num_train_steps=epochs*len(ds_train),
num_warmup_steps=0.1*epochs*len(ds_train),
optimizer_type='adamw')
model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer=opt)
model.fit(ds_train.map(tupelize).batch(128),validation_data=ds_test.map(tupelize).batch(128))Model: "model"
__________________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
==================================================================================================
input_1 (InputLayer) [(None,)] 0
__________________________________________________________________________________________________
keras_layer (KerasLayer) {'input_type_ids': ( 0 input_1[0][0]
__________________________________________________________________________________________________
keras_layer_1 (KerasLayer) {'pooled_output': (N 4782465 keras_layer[0][0]
keras_layer[0][1]
keras_layer[0][2]
__________________________________________________________________________________________________
dropout_4 (Dropout) (None, 128) 0 keras_layer_1[0][5]
__________________________________________________________________________________________________
dense_4 (Dense) (None, 4) 516 dropout_4[0][0]
==================================================================================================
Total params: 4,782,981
Trainable params: 4,782,980
Non-trainable params: 1
__________________________________________________________________________________________________
938/938 [==============================] - 629s 664ms/step - loss: 0.6344 - acc: 0.7658 - val_loss: 0.4876 - val_acc: 0.8247
<tensorflow.python.keras.callbacks.History at 0x7f9bb0bd0070>正如你所看到的,训练过程相当缓慢——但你可能想尝试训练模型几个周期(5-10),看看与我们之前使用的方法相比,是否能获得最佳结果。
Huggingface Transformers库#
另一种非常常见(且稍微简单一些)的使用Transformer模型的方法是HuggingFace包,它为不同的NLP任务提供了简单的构建模块。该库同时支持Tensorflow和PyTorch,这两个非常流行的神经网络框架。
注意:如果你对了解Transformers库的工作原理不感兴趣——可以直接跳到笔记本的最后部分,因为你不会看到与我们之前所做的有实质性不同的内容。我们将重复使用不同的库和更大的模型来训练BERT模型的相同步骤。因此,这个过程涉及一些相当长时间的训练,所以你可能只需要浏览代码即可。
让我们看看如何使用Huggingface Transformers来解决我们的问题。
首先,我们需要选择将要使用的模型。除了内置的一些模型之外,Huggingface还提供了一个在线模型库,社区中有许多预训练模型可供选择。所有这些模型都可以通过提供模型名称来加载和使用。模型所需的所有二进制文件会自动下载。
有时候,你可能需要加载自己的模型,这种情况下可以指定包含所有相关文件的目录,包括分词器的参数、包含模型参数的config.json文件、二进制权重等。
通过模型名称,我们可以实例化模型和分词器。让我们从分词器开始:
import transformers
# To load the model from Internet repository using model name.
# Use this if you are running from your own copy of the notebooks
bert_model = 'bert-base-uncased'
# To load the model from the directory on disk. Use this for Microsoft Learn module, because we have
# prepared all required files for you.
#bert_model = './bert'
tokenizer = transformers.BertTokenizer.from_pretrained(bert_model)
MAX_SEQ_LEN = 128
PAD_INDEX = tokenizer.convert_tokens_to_ids(tokenizer.pad_token)
UNK_INDEX = tokenizer.convert_tokens_to_ids(tokenizer.unk_token)tokenizer 对象包含 encode 函数,可直接用于编码文本:
tokenizer.encode('Tensorflow is a great framework for NLP')[101, 23435, 12314, 2003, 1037, 2307, 7705, 2005, 17953, 2361, 102]我们还可以使用分词器以适合传递给模型的方式对序列进行编码,例如包括 token_ids、input_mask 字段等。我们还可以通过提供 return_tensors='tf' 参数来指定我们需要 Tensorflow 张量:
tokenizer(['Hello, there'],return_tensors='tf'){'input_ids': <tf.Tensor: shape=(1, 5), dtype=int32, numpy=array([[ 101, 7592, 1010, 2045, 102]], dtype=int32)>, 'token_type_ids': <tf.Tensor: shape=(1, 5), dtype=int32, numpy=array([[0, 0, 0, 0, 0]], dtype=int32)>, 'attention_mask': <tf.Tensor: shape=(1, 5), dtype=int32, numpy=array([[1, 1, 1, 1, 1]], dtype=int32)>}在我们的案例中,我们将使用预训练的BERT模型,名为bert-base-uncased。Uncased表示该模型对大小写不敏感。
在训练模型时,我们需要提供经过分词的序列作为输入,因此我们将设计数据处理管道。由于tokenizer.encode是一个Python函数,我们将采用与上一单元相同的方法,通过调用py_function来实现:
def process(x):
return tokenizer.encode(x.numpy().decode('utf-8'),return_tensors='tf',padding='max_length',max_length=MAX_SEQ_LEN,truncation=True)[0]
def process_fn(x):
s = x['title']+' '+x['description']
e = tf.py_function(process,inp=[s],Tout=(tf.int32))
e.set_shape(MAX_SEQ_LEN)
return e,x['label']现在我们可以使用BertForSequenceClassification包加载实际模型。这确保了我们的模型已经具有分类所需的架构,包括最终的分类器。您会看到一条警告消息,指出最终分类器的权重尚未初始化,模型需要进行预训练——这完全没问题,因为这正是我们即将要做的!
model = transformers.TFBertForSequenceClassification.from_pretrained(bert_model,num_labels=4,output_attentions=False)model.summary()Model: "tf_bert_for_sequence_classification_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
bert (TFBertMainLayer) multiple 109482240
_________________________________________________________________
dropout_75 (Dropout) multiple 0
_________________________________________________________________
classifier (Dense) multiple 3076
=================================================================
Total params: 109,485,316
Trainable params: 109,485,316
Non-trainable params: 0
_________________________________________________________________
从 summary() 可以看出,该模型包含将近 1.1 亿个参数!据推测,如果我们希望在相对较小的数据集上进行简单的分类任务,我们不希望训练 BERT 基础层:
model.layers[0].trainable = False
model.summary()Model: "tf_bert_for_sequence_classification_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
bert (TFBertMainLayer) multiple 109482240
_________________________________________________________________
dropout_75 (Dropout) multiple 0
_________________________________________________________________
classifier (Dense) multiple 3076
=================================================================
Total params: 109,485,316
Trainable params: 3,076
Non-trainable params: 109,482,240
_________________________________________________________________
现在我们可以开始训练了!
注意:训练完整规模的BERT模型可能会非常耗时!因此,我们只会训练前32个批次。这只是为了展示模型训练的设置方式。如果你有兴趣尝试完整规模的训练,只需移除
steps_per_epoch和validation_steps参数,然后准备好等待吧!
model.compile('adam','sparse_categorical_crossentropy',['acc'])
tf.get_logger().setLevel('ERROR')
model.fit(ds_train.map(process_fn).batch(32),validation_data=ds_test.map(process_fn).batch(32),steps_per_epoch=32,validation_steps=2)32/32 [==============================] - 142s 4s/step - loss: 1.3896 - acc: 0.2500 - val_loss: 1.3863 - val_acc: 0.2480
<tensorflow.python.keras.callbacks.History at 0x7f1d40a4b6a0>如果你增加迭代次数并等待足够长的时间,同时进行多个训练周期,你可以预期 BERT 分类会给我们带来最佳的准确率!这是因为 BERT 已经对语言结构有了相当好的理解,我们只需要微调最终的分类器即可。然而,由于 BERT 是一个大型模型,整个训练过程需要很长时间,并且需要强大的计算能力!(GPU,最好是多块 GPU)。
Note: 在我们的示例中,我们使用的是最小的预训练 BERT 模型之一。还有更大的模型可能会带来更好的结果。
要点#
在本单元中,我们了解了基于transformers的最新模型架构。我们将其应用于文本分类任务,但同样,BERT模型也可以用于实体提取、问答以及其他自然语言处理任务。
Transformer模型代表了当前自然语言处理领域的最先进技术。在大多数情况下,当你开始尝试实现定制的自然语言处理解决方案时,它应该是首选。然而,如果你希望构建高级的神经网络模型,理解本模块中讨论的循环神经网络的基本原理是非常重要的。
免责声明:
本文档使用AI翻译服务Co-op Translator进行翻译。尽管我们努力确保翻译的准确性,但请注意,自动翻译可能包含错误或不准确之处。原始语言的文档应被视为权威来源。对于关键信息,建议使用专业人工翻译。我们不对因使用此翻译而产生的任何误解或误读承担责任。