Transformer实战——BERT模型详解与实现

发布于:2025-07-30 ⋅ 阅读:(16) ⋅ 点赞:(0)

0. 前言

我们已经学习了如何使用 Hugging Face 的 transformers 库来应用经典 Transformer 模型,并了解了如何使用预训练或预构建的模型,但并未详细介绍具体模型及其训练的细节。在本节中,我们将学习如何从零开始训练自编码语言模型。训练过程包括模型的预训练和针对特定任务的训练。首先,学习 BERT (Bidirectional Encoder Representations from Transformer) 模型及其工作原理,然后,使用一个简单的小型语料库来训练语言模型,并将该模型应用于 Keras 模型中。

1. BERT 模型介绍

BERT (Bidirectional Encoder Representations from Transformer) 是最早利用编码器 Transformer 堆栈,并对其进行修改以用于语言建模的自编码语言模型之一。BERT 架构是基于原始 Transformer 实现的多层编码器。Transformer 模型最初是为机器翻译任务设计的,但 BERT 的主要改进是利用 Transformer 的编码器部分来提供更好的语言建模。语言模型经过预训练后,能够提供对其训练语言的全局理解。

1.1 BERT 语言模型预训练任务

为了清楚地理解 BERT 使用的掩码语言模型 (Masked Language Model, MLM),我们将对其进行更详细的定义。MLM 是在输入(包含一些掩码词元的句子)上训练模型,并输出填补了掩码词元的完整句子。这种方法能够帮助模型在下游任务(例如分类)中取得更好的结果,因为如果模型能够进行完型填空测试(这是一种通过填空来评估语言理解的语言学测试),那么它就有了对语言本身的广泛理解。对于其他任务,模型已经通过语言建模进行了预训练,因此能够表现得更好。
以下是一个完形填空测试的例子:George Washington was the first President of the ___ States。预期 “United” 应该填入空白处。对于掩码语言模型,会应用同样的任务,要求模型填补掩码词元,但掩码词元是从句子中随机选择的。
BERT 训练的另一个任务是下句预测 (Next Sentence Prediction, NSP)。这个预训练任务确保 BERT 不仅学习所有词元 (token) 之间的关系(通过预测掩蔽词元),还可以理解两句话之间的关系。选择一对句子并将其提供给 BERT,并在它们之间插入一个 [SEP] 分隔词元。数据集中也会告知第二个句子是否紧接在第一个句子之后。
以下是一个 NSP 的例子:It is required that the reader fill in the blanks. Make sure to check the compatibility and support status before using them in your projects.。在这个例子中,模型需要输出预测结果为负相关,即这两句话之间没有关系。
这两种预训练任务使 BERT 能够理解语言本身。BERT 的词元嵌入为每个词元提供了一个上下文嵌入,上下文嵌入意味着每个词元的嵌入完全依赖于其周围的词元。与 word2vec 等模型不同,BERT 为每个词元嵌入提供了更丰富的信息。NSP 任务则使 BERT 能够为 [CLS] 词元提供更好的嵌入,[CLS] 提供了整个输入的信息,能够用于分类任务,并在预训练阶段学习输入的整体嵌入。下图显示了 BERT 模型的概览以及 BERT 模型的输入和输出:

BERT

2. 深入理解 BERT 语言模型

分词器是许多自然语言处理 (Natural Language Processing, NLP) 应用程序中最重要的部分之一,在工作流程中起着关键作用。对于 BERT 来说,使用的是 WordPiece 分词技术。WordPieceSentencePiece 和字节对编码 (Byte Pair Encoding, BPE) 是三种最常见的分词器,用于不同的 Transformer 架构。这些分词器的主要区别在于它们的合并策略、单元表示(字节、字符或子词单元)以及在分词方案和分割模式上的灵活性。这些算法各有优缺点,可以根据具体任务的需求来选择。BERT 或任何其他基于 Transformer 的架构使用子词分词的主要原因在于,这类分词器能够处理未知的词汇。
BERT 使用位置编码以确保模型能够获取到词元 (token) 的位置。BERT 和类似的模型使用的是非顺序操作,而传统模型,如基于长短期记忆 (Long Short Term Memory, LSTM) 和循环神经网络 (Recurrent Neural Network, RNN) 的模型,关注自然语言中的顺序问题。为了向 BERT 提供额外的位置信息,引入了位置编码技术。
BERT 的预训练为模型提供了语言层面的信息,但在实际应用中,在处理不同问题时,如序列分类、词元分类或问答任务,会使用模型输出的不同部分。
例如,在序列分类任务中,如情感分析或句子分类,原始 BERT 中建议使用最后一层的 [CLS] 嵌入作为输入。然而,其他模型则使用不同的技术进行分类,例如使用所有词元的平均嵌入、在最后一层上部署 LSTM 或者卷积神经网络 (Convolutional Neural Network, CNN)。序列分类时,可以使用最后的 [CLS] 嵌入作为任意分类器的输入,但最常用的做法是使用一个全连接层,输入大小等于最终词元嵌入的大小,输出大小等于类别的数量,并使用 softmax 激活函数。当输出是多标签且问题本身是多标签分类问题时,使用 sigmoid 激活函数也是另一种可选方法。
为了更详细地说明 BERT 的工作原理,下图展示了一个 NSP 任务的示例。需要注意的是,这里对分词进行了简化,以便更好的理解:

NSP
BERT 模型有多种不同的变体和设置。例如,输入的大小是可变的。在上述示例中,输入大小设置为 512,模型可以处理的最大序列长度为 512,但这个大小包括了特殊词元 [CLS][SEP],所以实际输入的序列长度为 510。另一方面,使用 WordPiece 作为分词器会生成子词词元,序列大小在分词之前可能会包含较少的单词,但经过分词后,大小会增加,因为当分词器遇到在预训练语料库中不常见的词汇时,会将其拆分为子词。
下图展示了 BERT 在不同任务中的应用示例。在命名实体识别 (Named Entity Recognition, NER) 任务中,会使用每个词元的输出,而不是 [CLS]。在问答任务中,问题和答案会通过 [SEP] 分隔符词元连接起来,答案会使用 “Start/End” 标注,并且通过最后一层的输出进行标注。在这种情况下,段落是问题所询问的上下文:

BERT应用
无论是在哪种任务中,BERT 最重要的能力是其对文本的上下文表示。BERT 能够成功的运用于各种任务中的关键原因是其基于 Transformer 编码器架构,Transformer 架构将输入表示为密集向量,这些向量可以通过非常简单的分类器轻松转换为输出。
位置编码在保持单词顺序方面至关重要,通过向词嵌入中添加非常小的数值,以确保它们在语义上接近其含义,同时也保持特定的顺序。
我们已经了解了 BERT 及其工作原理,掌握了 BERT 用于不同任务的详细信息,并了解了该架构中的重要机制。接下来,我们将学习如何 BERT 进行预训练,并在训练后进行使用。

3. 自编码语言模型训练

我们已经讨论了 BERT 的工作原理,并且可以使用 Hugging Face 库提供的预训练模型。在本节中,将学习如何使用 Hugging Face 库训练 BERT 模型。
在开始之前,首先需要有训练数据用于语言建模。训练数据也称为语料库,通常是一个大规模的数据集。所选的语料库必须适合希望训练语言模型的使用场景,例如,如果希望为英语语言训练一个 BERT 模型,可以选择 Common Crawl 数据集。
在本节中,为了加快训练速度,我们使用一个较小的数据集,IMDB 数据集,该数据集包含 50K 条电影评论,是一个用于情感分析的大规模数据集,但如果将其作为语料库来训练语言模型,则相对较小。

(1) 下载 IMDB 数据集,然后将其保存为 .txt 格式,用于语言模型和分词器的训练:

import pandas as pd
imdb_df = pd.read_csv("IMDB Dataset.csv")
reviews = imdb_df.review.to_string(index=None) 
with open("corpus.txt", "w") as f: 
    f.writelines(reviews) 

(2) 准备好语料库后,必须训练分词器。tokenizers 库提供了快速和简便的训练方法,用于训练 WordPiece 分词器。在语料库上训练 WordPiece 分词器:

from tokenizers import BertWordPieceTokenizer
bert_wordpiece_tokenizer = BertWordPieceTokenizer()
bert_wordpiece_tokenizer.train("corpus.txt")

(3) 分词器训练完成后,可以通过使用训练后的分词器对象的 get_vocab() 函数访问训练好的词汇表:

bert_wordpiece_tokenizer.get_vocab()

输出结果如下所示:
输出结果
(4) 保存分词器以便之后使用非常重要。使用对象的 save_model() 函数并提供目录路径,可以保存分词器的词汇表以供后续使用:

bert_wordpiece_tokenizer.save_model("tokenizer")

(5) 可以通过使用 from_file() 函数重新加载分词器:

tokenizer = BertWordPieceTokenizer.from_file("tokenizer/vocab.txt")

(6) 通过 encode() 方法使用分词器:

tokenized_sentence = tokenizer.encode("Oh it works just fine")
tokenized_sentence.tokens
# ['[CLS]', 'oh', 'it', 'works', 'just', 'fine', '[SEP]']

由于 BERT 需要 [CLS][SEP] 特殊词元来处理输入,这些词元会自动添加到词元列表中。

(7) 使用分词器处理另一句子:

tokenized_sentence = tokenizer.encode("ohoh i thougt it might be workingg well")
print(tokenized_sentence.tokens)
# ['[CLS]', 'oh', '##o', '##h', 'i', 'thoug', '##t', 'it', 'might', 'be', 'working', '##g', 'well', '[SEP]']```

可以看到,训练后的分词器能够处理噪声和拼写错误文本。

(8) 训练并保存分词器后,开始训练 BERT 模型。第一步是使用 Transformers 库中的 BertTokenizerFast,需要加载训练好的分词器:

from transformers import BertTokenizerFast 
tokenizer = BertTokenizerFast.from_pretrained("tokenizer") 

Hugging Face 的文档建议使用 BertTokenizerFast,也可以使用 BertTokenizer,但根据库文档的定义,它的实现速度不如 BertTokenizerFast,大多数预训练模型的文档中推荐使用 BertTokenizerFast 版本。

(9) 准备语料库,以便加速训练:

from transformers import LineByLineTextDataset 
dataset = LineByLineTextDataset(tokenizer=tokenizer, file_path="corpus.txt", block_size=128) 

(10) 实例化一个用于 MLM 的数据整理器:

from transformers import DataCollatorForLanguageModeling 
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=True, mlm_probability=0.15) 

数据整理器 (DataCollator) 负责获取数据并为训练做准备。例如,以上数据整理器会将数据处理为 MLM 格式,并设置 15% 的掩码概率。使用这种机制的目的是在训练时实时地进行预处理,从而节省资源。但另一方面,它会减慢训练过程,因为每个样本在训练时都需要实时进行预处理。

(11) 训练参数 TrainingArguments 为训练阶段的训练器提供了所需的信息,可以使用以下方式进行设置:

from transformers import TrainingArguments 
training_args = TrainingArguments(output_dir="BERT", overwrite_output_dir=True, num_train_epochs=1, per_device_train_batch_size=32) 

(12) 接下来,创建 BERT 模型,并使用默认配置(如注意力头的数量、Transformer 编码器层数等):

from transformers import BertConfig, BertForMaskedLM 
bert = BertForMaskedLM(BertConfig()) 

(13) 创建一个 Trainer 对象:

from transformers import Trainer 
trainer = Trainer(model=bert, args=training_args, data_collator=data_collator, train_dataset=dataset) 

(14) 训练语言模型:

trainer.train()
# 'train_runtime': 680.7382

在模型训练过程中,使用一个名为 runs 的日志目录来存储检查点。

(15) 训练完成后,保存模型:

trainer.save_model("MyBERT")

通过以上步骤,我们可以为任意所需要的特定语言从零开始训练 BERT,了解了如何使用准备好的语料库训练分词器和 BERT 模型。

(16)BERT 提供的默认配置是训练过程中的一个关键部分,因为它定义了BERT的架构及其超参数。可以通过 BertConfig() 查看这些参数:

from transformers import BertConfig 
BertConfig() 

输出结果如下所示:

输出结果
如果想要复现原始 BERT 配置中的 tinyminismallbase相关模型,可以更改以下配置:

H=128 H=256 H=512 H=768
L=2 2/128 (BERT-Tiny) 2/256 2/512 2/768
L=4 4/128 4/256 (BERT-Mini) 4/512 (BERT-Small) 4/768
L=6 6/128 6/256 6/512 6/768
L=8 8/128 8/128 8/512 (BERT-Medium) 8/768
L=10 10/128 10/256 10/512 10/768
L=12 12/128 12/256 12/512 12/768 (BERT-Base)

需要注意的是,改变这些参数值,尤其是 max_position_embeddingnum_attention_headsnum_hidden_layersintermediate_sizehidden_size,会直接影响训练时间,增加这些参数值会显著增加大语料库的训练时间。

(17) 例如,创建一个用于更快训练的 tiny 版本 BERT 的新配置:

tiny_bert_config = BertConfig(max_position_embeddings=512, hidden_size=128, num_attention_heads=2, num_hidden_layers=2, intermediate_size=512) 
tiny_bert_config 

输出结果如下所示:

输出结果
(18) 根据此配置创建一个 tiny BERT 模型:

tiny_bert = BertForMaskedLM(tiny_bert_config) 

(19) 使用相同的训练参数,训练 tiny BERT 模型:

trainer = Trainer(model=tiny_bert, args=training_args, data_collator=data_collator, train_dataset=dataset) 
trainer.train() 
# 'train_runtime': 46.3783

可以看到,训练时间显著减少,但需要注意,这只是一个 tiny 版的 BERT,层数和参数较少,效果不如 BERT base 模型。
我们已经学习了如何从零开始训练 BERT 模型,但需要注意的是,在处理用于训练语言模型的数据集时,使用 datasets 库是一个更好的选择,或者利用它来执行特定任务的训练。

(20) BERT 语言模型还可以作为嵌入层与任意深度学习模型结合使用。例如,可以加载预训练的 BERT 模型,在 Keras 模型中使用:

from transformers import TFBertModel, BertTokenizerFast 
bert = TFBertModel.from_pretrained("bert-base-uncased") 
tokenizer = BertTokenizerFast.from_pretrained("bert-base-uncased") 

(21) 可以使用以下代码访问模型的层:

print(bert.layers)
# [<transformers.models.bert.modeling_tf_bert.TFBertMainLayer object at 0x7f89334f3700>]

如上所示,这只是一个实例化的 TFBertMainLayer 层,可以在 Keras 模型中访问它并获取其输出。在使用之前,最好先测试一下,以查看其输出形式:

tokenized_text = tokenizer.batch_encode_plus(["hello how is it going with you","lets test it"], return_tensors="tf", max_length=256, truncation=True, pad_to_max_length=True) 
bert(tokenized_text) 

输出结果如下所示:

输出结果
从结果可以看出,包含两个输出:一个是最后的隐藏状态 (last hidden state),另一个是池化器输出 (pooler output)。最后的隐藏状态提供了来自 BERT 的所有分词的嵌入,包括额外的 [CLS][SEP] 词元,分别位于开始和结束位置。

(22) 了解了 TensorFlow 版本的 BERT 后,可以使用这个新的嵌入创建一个 Keras 模型:

from tensorflow import keras 
import tensorflow as tf 
max_length = 256 
tokens = keras.layers.Input(shape=(max_length,), dtype=tf.dtypes.int32) 
masks = keras.layers.Input(shape=(max_length,), dtype=tf.dtypes.int32) 
embedding_layer = bert.layers[0]([tokens,masks])[0][:,0,:] 
dense = tf.keras.layers.Dense(units=2, activation="softmax")(embedding_layer) 
model = keras.Model([tokens,masks],dense) 

(23) model 对象是一个 Keras 模型,它有两个输入:一个是 token 输入,一个是 mask 输入。token 输入来自分词器的输出中的 input_ids,而 mask 输入是 attention_mask

tokenized = tokenizer.batch_encode_plus(["hello how is it going with you","hello how is it going with you"], return_tensors="tf", max_length= max_length, truncation=True, pad_to_max_length=True) 

使用分词器时,重要的是要使用 max_lengthtruncationpad_to_max_length 这几个参数。这些参数确保得到的输出是一个可用的输出,通过填充到预定义的最大长度 256

(24) 接下来,运行模型:

model([tokenized["input_ids"],tokenized["attention_mask"]]) 

输出结果如下所示:

<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[0.6148078 , 0.38519228],
       [0.6148078 , 0.38519228]], dtype=float32)>

(25) 在训练模型时,需要使用 compile 函数来编译模型:

model.compile(optimizer="Adam", loss="categorical_crossentropy", metrics=["accuracy"]) 
model.summary() 

输出结果如下所示:

模型结构
从模型结构信息中可以看到,模型有 109483778 个可训练参数,其中包括 BERT。如果已经预训练了 BERT 模型,并且希望在特定任务的训练中冻结其参数,可以使用以下命令:

model.layers[2].trainable = False 

由于嵌入层的层索引是 2,所以使用 model.layers[2] 可以直接冻结其参数。如果重新运行 summary() 函数,会看到可训练参数的数量减少到 1,538,即最后一层的参数数量:

模型结构
(26) 我们已经使用了 IMDB 情感分析数据集训练语言模型。接下来,可以使用训练完成的语言模型微调情感分析模型。首先,需要准备输入和输出数据:

import pandas as pd 
imdb_df = pd.read_csv("IMDB Dataset.csv") 
reviews = list(imdb_df.review) 
tokenized_reviews = tokenizer.batch_encode_plus(reviews, return_tensors="tf", max_length=max_length, truncation=True, pad_to_max_length=True) 

import numpy as np 
train_split = int(0.8 * len(tokenized_reviews["attention_mask"])) 
train_tokens = tokenized_reviews["input_ids"][:train_split] 
test_tokens = tokenized_reviews["input_ids"][train_split:] 
train_masks = tokenized_reviews["attention_mask"][:train_split] 
test_masks = tokenized_reviews["attention_mask"][train_split:] 
sentiments = list(imdb_df.sentiment) 
labels = np.array([[0,1] if sentiment == "positive" else [1,0] for sentiment in sentiments]) 
train_labels = labels[:train_split] 
test_labels = labels[train_split:] 

(27) 数据准备完毕后,开始训练模型:

model.fit([train_tokens,train_masks],train_labels, batch_size=32, epochs=5)

模型训练完成后,就可以使用该模型执行特定任务。

小结

自编码模型依赖于原始 Transformer 的编码器,并且在解决分类问题时非常高效。在本节中,从理论和实践两个方面学习了自编码模型。从 BERT 的基础原理开始,从零开始训练了 BERT 及其对应的分词器,还讨论了如何将其应用于其他深度学习框架(如 Keras)。

系列链接

Transformer实战——词嵌入技术详解
Transformer实战——循环神经网络详解
Transformer实战——从词袋模型到Transformer:NLP技术演进
Transformer实战——Hugging Face环境配置与应用详解
Transformer实战——Transformer模型性能评估
Transformer实战——datasets库核心功能解析


网站公告

今日签到

点亮在社区的每一天
去签到