在上一章中,我们研究了如何使用 Hugging Face 的 Transformers 来应用典型的 Transformer 模型。到目前为止,本书的所有章节都包括了如何使用预训练或预构建模型的说明,而关于特定模型及其训练的信息较少。
在本章中,我们将学习如何从头开始训练自动编码语言模型,并适用于任何给定的语言。此训练将包括模型的预训练和特定任务的训练。首先,我们将从学习 BERT 模型及其工作原理开始。然后,我们将使用一个简单的小语料库来训练语言模型。之后,我们将探讨如何在任何 Keras 模型中使用该模型。
为了概览本章将要学习的内容,我们将涵盖以下主题:
- Transformer 的双向编码器表示(BERT)——自动编码语言模型之一
- 任何语言的自动编码语言模型训练
- 与社区分享模型
- 理解其他自动编码模型
- 使用分词算法
- 技术要求
本章的技术要求如下:
- Anaconda
- transformers >= 4.0.0
- pytorch >= 1.0.2
- tensorflow >= 2.4.0
- datasets >= 1.4.1
- tokenizers
请同时查看第3章的对应 GitHub 代码:github.com/PacktPublis…
BERT——自动编码语言模型之一
BERT是最早利用编码器Transformer堆栈进行语言建模的自动编码语言模型之一,并对其进行了轻微修改。
BERT架构是基于原始Transformer实现的多层Transformer编码器。Transformer模型最初是为机器翻译任务设计的,但BERT的主要改进在于利用了架构的这一部分来提供更好的语言建模。这种语言模型在预训练之后,能够提供对其训练语言的全局理解。
在接下来的小节中,你将学习关于BERT等自动编码模型的知识。你还将学习如何预训练一个模型并将其与社区分享。在下一节中,将更详细地解释BERT。
BERT语言模型的预训练任务
为了清楚地理解BERT使用的MLM(掩码语言建模),我们来详细定义它。MLM的任务是对输入(一个包含部分掩码标记的句子)进行训练,并获取输出,即填充了掩码标记的完整句子。但这种方法如何以及为何能帮助模型在分类等下游任务中取得更好的结果?答案很简单:如果模型能够通过完形填空测试(通过填补空白来评估语言理解的语言学测试),那么它就对语言本身有了一个总体的理解。对于其他任务,它已经通过语言建模进行了预训练,因此表现会更好。
这里有一个完形填空测试的例子:George Washington was the first President of the ___ States.
预期答案是“United”应该填补空白。对于掩码语言模型,应用相同的任务,要求它填补掩码标记。然而,掩码标记是从句子中随机选择的。
BERT训练的另一个任务是下一句预测(NSP)。这个预训练任务确保BERT不仅能够在预测掩码标记时学习所有标记之间的关系,还能帮助它理解两个句子之间的关系。一对句子被选择并通过[SEP]分隔符标记提供给BERT。数据集中还指明了第二句是否在第一句之后。
以下是NSP的一个示例:要求读者填补空白。比特币的价格相比其他替代币高得多。
在这个例子中,模型需要预测该句子的关系是否为负面(即句子彼此不相关)。
这两个预训练任务使BERT能够理解语言本身。BERT的标记嵌入为每个标记提供了上下文嵌入。上下文嵌入意味着每个标记的嵌入完全与周围的标记相关。与word2vec和其他类似模型不同,BERT为每个标记嵌入提供了更好的信息。另一方面,NSP任务使BERT能够为[CLS]标记提供更好的嵌入。正如第一章所讨论的,[CLS]标记提供了关于整个输入的信息。[CLS]用于分类任务,并在预训练部分学习整个输入的总体嵌入。下图展示了BERT模型的概览及其相应的输入和输出:
让我们进入下一节!
深入了解BERT语言模型
分词器是许多NLP应用中非常重要的部分。在BERT中,使用的是WordPiece分词器。通常,WordPiece、SentencePiece和字节对编码(BPE)是最广为人知的三种分词器,它们被不同的基于Transformer的架构所使用,这些内容也将在后续章节中涉及。它们的主要区别在于合并策略、单位表示(字节、字符或子词单元),以及在分词方案和分割模式上的灵活性。这些算法各有优势,可以根据具体任务的要求进行选择。BERT或其他任何基于Transformer的架构使用子词分词的主要原因是这些分词器能够处理未知标记。
BERT还使用位置编码来确保模型能够接收到标记的位置信息。如果你还记得第一章的内容,BERT和类似的模型使用的是非顺序操作,如密集神经层。传统模型,如基于LSTM和RNN的模型,会关注自然位置。为了向BERT提供这些额外的信息,位置编码显得尤为重要。
BERT的预训练,如自动编码模型,为模型提供了语言层面的信息,但在实际操作中,当处理不同的问题,如序列分类、标记分类或问答时,模型输出的不同部分会被使用。
例如,在序列分类任务(如情感分析或句子分类)中,原始的BERT论文建议使用最后一层的CLS嵌入。然而,其他研究则使用不同的方法来利用BERT进行分类(例如使用所有标记的平均嵌入,或者在最后一层上部署LSTM,甚至在最后一层之上使用CNN)。序列分类任务中的最后[CLS]嵌入可以被任何分类器使用,但最常见的建议是使用一个密集层,其输入大小等于最终标记嵌入的大小,输出大小等于类别数,并且使用softmax激活函数。当输出可能是多标签且问题本身是多标签分类问题时,使用sigmoid也是另一种选择。
为了让你更详细地了解BERT的实际工作方式,下图展示了一个NSP任务的示例。请注意,这里的分词简化了,以便更好地理解:
BERT模型有不同的变体,并具有不同的设置。例如,输入的大小是可变的。在前面的例子中,输入大小设置为512,模型可以接收的最大序列长度是512。然而,这个大小包括特殊标记[CLS]和[SEP],因此实际有效长度将减少到510。另一方面,使用WordPiece作为分词器会产生子词标记,序列大小在分词前可能会包含较少的单词,但在分词后,大小会增加,因为分词器会将不常见的词拆分为子词。
下面的截图展示了BERT在不同任务中的应用示例。对于NER任务,使用的是每个标记的输出,而不是[CLS]。在问答任务中,问题和答案使用[SEP]分隔符标记连接,答案通过“Start/End”以及最后一层的跨度输出进行标注。在这种情况下,Paragraph是Question所询问的上下文:
不论这些任务如何,BERT最重要的能力在于其对文本的上下文表示。它在各种任务中取得成功的原因在于其Transformer编码器架构,这种架构将输入表示为密集向量。这些向量可以通过非常简单的分类器轻松转换为输出。
位置编码在保持单词顺序方面至关重要。它为单词嵌入添加了非常小的数值,以确保它们在语义上接近它们的含义,同时也具有特定的顺序。
到目前为止,你已经了解了BERT及其工作原理。你学到了关于BERT可以用于的各种任务的详细信息以及该架构的重要点。
在下一节中,你将学习如何预训练BERT以及如何在训练后使用它。
任何语言的自动编码语言模型训练
我们已经讨论了BERT的工作原理,并且可以使用Hugging Face仓库中提供的预训练版本。在本节中,你将学习如何使用Hugging Face库来训练你自己的BERT。
在开始之前,拥有良好的训练数据至关重要,因为它将用于语言建模。这些数据被称为语料库,通常是大量的数据(有时经过预处理和清理)。这个无标签的语料库必须适合你希望进行语言模型训练的使用场景;例如,如果你想为英语语言训练一个特定的BERT模型。尽管有很多巨大的、优质的数据集,如Common Crawl(commoncrawl.org/),但我们更倾向于选择一个较小的数据集以加快训练速度。
IMDB的50,000条电影评论数据集(可在www.kaggle.com/lakshmi25np…获取)是一个用于情感分析的大型数据集,但如果你将其用作训练语言模型的语料库,它相对较小:
你可以使用以下代码轻松下载并以.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)
在准备好语料库后,需要训练分词器。tokenizers库提供了快速且简单的WordPiece分词器训练。为了在你的语料库上训练分词器,你需要运行以下代码:
from tokenizers import BertWordPieceTokenizer
bert_wordpiece_tokenizer = BertWordPieceTokenizer()
bert_wordpiece_tokenizer.train("corpus.txt")
这将训练分词器。你可以通过使用训练后的分词器对象的get_vocab()函数来访问训练好的词汇表。你可以使用以下代码获取词汇表:
bert_wordpiece_tokenizer.get_vocab()
输出结果如下:
{'almod': 9111, 'events': 3710, 'bogart': 7647, 'slapstick': 9541, 'terrorist': 16811, 'patter': 9269, '183': 16482, '##cul': 14292, 'sophie': 13109, 'thinki': 10265, 'tarnish': 16310, '##outh': 14729, 'peckinpah': 17156, 'gw': 6157, '##cat': 14290, '##eing': 14256, 'successfully': 12747, 'roomm': 7363, 'stalwart': 13347,...}
为了方便后续使用,必须保存分词器。使用对象的save_model()函数并提供目录,将保存分词器词汇表以供进一步使用:
bert_wordpiece_tokenizer.save_model("tokenizer")
你可以使用from_file()函数重新加载它:
tokenizer = BertWordPieceTokenizer.from_file("tokenizer/vocab.txt")
你可以通过以下示例使用分词器:
tokenized_sentence = tokenizer.encode("Oh it works just fine")
tokenized_sentence.tokens
输出结果为:
['[CLS]', 'oh', 'it', 'works', 'just', 'fine','[SEP]']
特殊的[CLS]和[SEP]标记将自动添加到标记列表中,因为BERT需要它们来处理输入。
让我们用分词器尝试另一个句子:
tokenized_sentence = tokenizer.encode("ohoh i thougt it might be workingg well")
输出结果为:
['[CLS]', 'oh', '##o', '##h', 'i', 'thoug', '##t', 'it', 'might', 'be', 'working', '##g', 'well', '[SEP]']
看起来这是一个适用于噪声和拼写错误文本的分词器。现在你已经准备好并保存了分词器,你可以训练你自己的BERT。第一步是使用Transformers库中的BertTokenizerFast。你需要通过以下命令加载前一步训练的分词器:
from transformers import BertTokenizerFast
tokenizer = BertTokenizerFast.from_pretrained("tokenizer")
我们使用BertTokenizerFast是因为Hugging Face文档建议使用它。还有BertTokenizer,根据库文档的定义,其实现速度没有快速版本快。在大多数预训练模型的文档和卡片中,强烈建议使用BertTokenizerFast版本。
下一步是使用以下命令准备语料库,以加快训练速度:
from transformers import LineByLineTextDataset
dataset = LineByLineTextDataset(tokenizer=tokenizer,
file_path="corpus.txt",
block_size=128)
你需要为MLM提供数据整理器:
from transformers import DataCollatorForLanguageModeling
data_collator = DataCollatorForLanguageModeling(
tokenizer=tokenizer,
mlm=True,
mlm_probability=0.15)
数据整理器获取数据并将其准备好用于训练。例如,前面的数据整理器以0.15的概率准备数据用于MLM。使用这种机制的目的是在训练时实时进行预处理,这使得可以使用更少的资源。另一方面,它会减慢训练过程,因为每个样本在训练时都必须实时进行预处理。
训练参数还为训练阶段的训练器提供信息,可以通过以下命令设置:
from transformers import TrainingArguments
training_args = TrainingArguments(
output_dir="BERT",
overwrite_output_dir=True,
num_train_epochs=1,
per_device_train_batch_size=128)
现在我们将创建BERT模型本身,我们将使用默认配置(注意力头数量、Transformer编码器层数等):
from transformers import BertConfig, BertForMaskedLM
bert = BertForMaskedLM(BertConfig())
最后一步是创建一个Trainer对象:
from transformers import Trainer
trainer = Trainer(model=bert,
args=training_args,
data_collator=data_collator,
train_dataset=dataset)
最后,你可以使用以下命令训练你的语言模型:
trainer.train()
训练进度条将显示训练进展情况:
在模型训练过程中,一个名为“runs”的日志目录将被用来按步骤存储检查点:
在训练完成后,你可以使用以下命令轻松保存模型:
trainer.save_model("MyBERT")
到目前为止,你已经学习了如何从头开始为任何你想要的特定语言训练BERT。你已经学习了如何使用你准备的语料库来训练分词器和BERT模型。
你为BERT提供的默认配置是这个训练过程中的关键部分,因为它定义了BERT的架构及其超参数。你可以使用以下代码查看这些参数:
from transformers import BertConfig
BertConfig()
以下是输出结果:
如果你希望复制来自原始BERT配置的tiny、mini、small、base等相对模型(github.com/google-rese…),你可以更改这些设置:
请注意,修改这些参数,尤其是max_position_embedding、num_attention_heads、num_hidden_layers、intermediate_size和hidden_size,会直接影响训练时间。增加这些参数会显著延长大语料库的训练时间。
例如,你可以使用以下代码为BERT的tiny版本创建一个新的配置,以加快训练速度:
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
以下是代码的运行结果:
通过使用相同的方法,我们可以使用这个配置创建一个tiny BERT模型:
tiny_bert = BertForMaskedLM(tiny_bert_config)
使用相同的训练参数,你可以训练这个新的tiny BERT模型:
trainer = Trainer(
model=tiny_bert,
args=training_args,
data_collator=data_collator,
train_dataset=dataset
)
trainer.train()
以下是输出结果:
显然,训练时间大幅减少,但需要注意的是,这是一个具有较少层和参数的tiny版本的BERT,其性能不如BERT base模型。
到目前为止,你已经学习了如何从头开始训练自己的模型,但值得注意的是,在处理语言模型的训练数据集或执行特定任务的训练时,使用Datasets库是更好的选择。
BERT语言模型还可以作为嵌入层与任何深度学习模型结合使用。例如,你可以加载任何预训练的BERT模型,或者加载你在第19步中训练的自己的版本。以下代码展示了如何将其加载以便在Keras模型中使用:
from transformers import TFBertModel, BertTokenizerFast
bert = TFBertModel.from_pretrained("bert-base-uncased")
tokenizer = BertTokenizerFast.from_pretrained("bert-base-uncased")
你不需要整个模型;相反,你可以使用以下代码访问层:
bert.layers
[<Transformers.models.bert.modeling_tf_bert.TFBertMainLayer at 0x7f72459b1110>]
如你所见,这里只有一个来自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]标记。
现在你已经了解了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)
model对象是一个Keras模型,它有两个输入:一个用于tokens,一个用于masks。tokens包含从分词器输出的input_ids,而masks将包含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_length、truncation和pad_to_max_length非常重要。这些参数确保你获得的输出具有可用的形状,通过填充到之前定义的最大长度256。现在,你可以使用这个样本来运行模型:
model([tokenized["input_ids"], tokenized["attention_mask"]])
以下是输出结果:
在训练模型时,你需要使用compile函数对其进行编译:
model.compile(optimizer="Adam",
loss="categorical_crossentropy",
metrics=["accuracy"])
model.summary()
输出结果如下:
从模型的摘要中可以看到,该模型有109,483,778个可训练参数,包括BERT在内。然而,如果你已经预训练了BERT模型,并且希望在特定任务的训练中冻结它,你可以使用以下命令来实现:
model.layers[2].trainable = False
我们知道,嵌入层的层索引是2,因此我们可以简单地将其冻结。如果你重新运行summary函数,你会发现可训练参数减少到1,538,这正是最后一层的参数数量:
正如你所记得的,我们使用了IMDB情感分析数据集来训练语言模型。现在,你可以使用它来训练基于Keras的情感分析模型。但首先,你需要准备输入和输出数据:
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:]
最终,你的数据已经准备好,你可以开始训练你的模型:
model.fit([train_tokens, train_masks], train_labels, epochs=5)
在训练模型之后,你的模型已经可以使用了。到目前为止,你已经学习了如何为分类任务执行模型训练。你也学习了如何保存模型,并且在下一节中,你将学习如何与社区分享训练好的模型。
与社区分享模型
训练好的模型可以轻松地与社区分享,这能够提升你工作的知名度。Hugging Face 提供了一种非常易用的模型共享机制:
你可以使用以下 CLI 工具登录:
transformers-cli login
使用你的凭证登录后,你可以创建一个存储库:
transformers-cli repo create a-fancy-model-name
你可以为 a-fancy-model-name 参数选择任何模型名称,然后需要确保你已经安装了 git-lfs:
git lfs install
Git LFS 是一个用于处理大文件的 Git 扩展。Hugging Face 的预训练模型通常是大文件,因此需要使用 LFS 等额外的库来由 Git 处理。
然后,你可以克隆刚刚创建的存储库:
git clone https://huggingface.co/username/a-fancy-model-name
之后,你可以根据需要向存储库添加或删除文件,然后与 Git 的使用方式一样,你需要运行以下命令:
git add . && git commit -m "Update from $USER"
git push
自动编码模型依赖于原始 Transformer 的左侧编码器部分,在解决分类问题时非常高效。虽然 BERT 是典型的自动编码模型示例,但文献中还有许多其他替代方案。让我们来看看这些重要的替代方案。
其他自动编码模型
在本部分中,我们将回顾一些对原始BERT进行略微修改的自动编码模型替代方案。这些替代实现旨在通过利用多种资源来提升下游任务的表现:优化预训练过程、调整层数或注意力头的数量、提高数据质量、设计更好的目标函数等等。改进的来源大致分为两个部分:更好的架构设计选择和预训练控制。
最近分享了许多有效的替代方案,因此在这里不可能理解和解释所有这些模型。我们可以看看文献中引用最多、在NLP基准测试中使用最多的一些模型。首先,我们来看一下A Lite BERT(ALBERT),这是BERT的一种重新实现,特别注重架构设计的选择。
介绍ALBERT
语言模型的性能通常被认为会随着其规模的增大而提高。然而,由于内存限制和更长的训练时间,训练这些模型变得越来越具有挑战性。为了解决这些问题,Google团队提出了ALBERT模型,这实际上是对BERT架构的重新实现,利用了几种新技术来减少内存消耗并加快训练速度。新的设计使得语言模型在规模上比原始BERT更好。与BERT相比,ALBERT的参数减少了18倍,训练速度比原始的BERT大型模型快1.7倍。
ALBERT模型主要由三个对原始BERT的修改组成:
- 因式分解嵌入参数化
- 跨层参数共享
- 句间一致性损失
前两个修改是与原始BERT模型大小和内存消耗问题相关的参数减少方法。第三个对应的是一种新的目标函数:句子顺序预测(SOP),取代了原始BERT的NSP任务,导致模型更精简且性能更优。
因式分解嵌入参数化用于将大词汇嵌入矩阵分解为两个较小的矩阵。这些矩阵将隐藏层的大小与词汇的大小分开。这种分解将嵌入参数从O(V × H)减少到O(V × E + E × H),其中V是词汇量,H是隐藏层大小,E是嵌入。这种分解在满足H>>E的情况下使模型参数的总使用更加高效。
例如,考虑以下示例:在ALBERT大型模型中,H的值是1026,V的大小是30000。在这种情况下,假设E值是128,满足H>>E条件。VxH的值约为30M(1026 x 30,000)。然而,(30,000 × 128 + 128 x 1026)的值约为4M。这使得模型的效率提高了十倍。
跨层参数共享防止随着网络的加深,参数总数的增加。该技术被认为是提高参数效率的另一种方法,因为我们可以通过共享或复制来保持参数大小较小。在原始论文中,他们尝试了多种参数共享方式,例如仅在各层之间共享FF参数,或仅共享注意力参数,或共享整个参数。
ALBERT的另一个修改是句间一致性损失。正如我们之前讨论的,BERT架构利用了两种损失计算:MLM损失和NSP。NSP使用二元交叉熵损失来预测两个段落是否连续出现在原始文本中。负面示例是通过从不同文档中选择两个段落获得的。然而,ALBERT团队批评NSP是一个主题检测问题,这被认为是一个相对简单的问题。因此,该团队提出了一种主要基于一致性的损失,而不是主题预测。
在这种情况下使用了SOP损失函数。这个损失函数优先考虑句子之间的一致性建模,而不是关注主题分类。它对正面示例采用与BERT类似的技术,使用来自同一文档的两个连续段落。对于负面示例,它使用相同的两个连续段落,但顺序颠倒。这种方法使模型能够在语篇层面上学习更细微的一致性特征。
现在,让我们深入探讨并看看它在实践中是如何工作的:
让我们比较一下原始BERT和ALBERT配置与Transformers库中的实现。以下代码展示了如何配置BERT-Base初始模型。正如输出所示,参数数量约为110M:
#BERT-BASE (L=12, H=768, A=12, Total Parameters=110M)
from transformers import BertConfig, BertModel
bert_base = BertConfig()
model = BertModel(bert_base)
print(f"{model.num_parameters() /(10**6)} million parameters")
输出:
109.48224 million parameters
以下代码展示了如何使用Transformers库中的AlbertConfig和AlbertModel定义ALBERT模型:
# Albert-base Configuration
from transformers import AlbertConfig, AlbertModel
albert_base = AlbertConfig(
hidden_size=768,
num_attention_heads=12,
intermediate_size=3072,
)
model = AlbertModel(albert_base)
print(f"{model.num_parameters() /(10**6)} million parameters")
输出:
11.683584 million parameters
因此,默认的ALBERT配置指向Albert-xxlarge。我们需要设置隐藏层大小、注意力头数量和中间层大小以适应ALBERT基础模型。代码显示了ALBERT基础模型参数为11M,比BERT基础模型小10倍。ALBERT的原始论文报告了如下表所示的基准测试结果:
从这里开始,如果要从头训练一个ALBERT语言模型,我们需要经历与前面BERT训练部分(“深入了解BERT语言模型”)中所示的类似阶段,使用统一的Transformers API即可。这里无需再解释相同的步骤!相反,我们可以直接加载一个已经训练好的ALBERT语言模型,如下所示:
from transformers import AlbertTokenizer, AlbertModel
tokenizer = AlbertTokenizer.from_pretrained("albert-base-v2")
model = AlbertModel.from_pretrained("albert-base-v2")
text = "The cat is so sad ."
encoded_input = tokenizer(text, return_tensors='pt')
output = model(**encoded_input)
上述代码片段将从Hugging Face的中心或本地缓存目录(如果已缓存)下载ALBERT模型的权重和配置,这意味着你之前已经调用过AlbertTokenizer.from_pretrained()函数。由于model对象是一个预训练语言模型,目前我们能用这个模型做的事情是有限的。我们需要在下游任务中对其进行训练,以便能在推理中使用,这将是后续章节的主要内容。相反,我们可以利用它的掩码语言模型目标,如下所示:
from transformers import pipeline
fillmask = pipeline('fill-mask', model='albert-base-v2')
pd.DataFrame(fillmask("The cat is so [MASK] ."))
以下是输出结果:
fill-mask流水线使用softmax()函数计算每个词汇标记的分数,并对最可能的标记进行排序,其中“cute”以0.281的概率得分最高。你可能会注意到,token_str列中的条目以_字符开头,这是由于ALBERT的分词器中的metaspace组件所致。
接下来,我们来看看另一种替代方案,即鲁棒优化的BERT预训练方法(RoBERTa) ,它主要关注预训练阶段的优化。
RoBERTa
RoBERTa是另一种流行的BERT重新实现。与架构设计相比,它在训练策略上提供了更多的改进。在GLUE上的许多任务中,RoBERTa的表现优于BERT。动态掩码是其原始设计选择之一。尽管静态掩码在某些任务中表现更好,但RoBERTa团队展示了动态掩码在整体性能上也能表现出色。让我们讨论一下BERT到RoBERTa的更改,并总结其所有特性。
架构的更改如下:
- 移除了NSP训练目标
- 动态更改掩码模式,而不是静态掩码,这通过在每次将序列输入模型时生成掩码模式来实现
- 使用BPE子词分词器
训练的更改如下:
- 控制训练数据:使用了更多的数据,例如160 GB,而不是BERT最初使用的16 GB。RoBERTa模型不仅考虑了数据的大小,还考虑了数据的质量和多样性。
- 更长的预训练步骤,最多达到500 K。
- 更大的批次大小。
- 更长的序列,这减少了填充。
- 使用更大的50 K BPE词汇表,而不是30 K BPE词汇表。
感谢Transformers的统一API,如同前面的ALBERT模型流水线一样,我们可以按如下方式初始化RoBERTa模型:
from transformers import RobertaConfig, RobertaModel
conf = RobertaConfig()
model = RobertaModel(conf)
print(f"{model.num_parameters() /(10**6)} million parameters")
输出结果:
109.48224 million parameters
为了加载预训练模型,我们执行以下代码:
from transformers import RobertaTokenizer, RobertaModel
tokenizer = RobertaTokenizer.from_pretrained('roberta-base')
model = RobertaModel.from_pretrained('roberta-base')
text = "The cat is so sad ."
encoded_input = tokenizer(text, return_tensors='pt')
output = model(**encoded_input)
这些代码展示了模型如何处理给定的文本。目前最后一层的输出表示还没有实用价值。正如我们多次提到的,我们需要对主要的语言模型进行微调。以下执行代码使用roberta-base模型应用了fill-mask功能:
from transformers import pipeline
fillmask = pipeline("fill-mask", model="roberta-base", tokenizer=tokenizer)
pd.DataFrame(fillmask("The cat is so <mask> ."))
以下是输出结果:
像前面的ALBERT fill-mask模型一样,这个流水线对合适的候选词进行排名。请忽略标记中的Ġ前缀——这是字节级BPE分词器产生的编码空格字符,我们稍后会讨论。你应该已经注意到,在ALBERT和RoBERTa的流水线中,我们使用了[MASK]和<mask>标记来占位掩码标记。这是因为分词器的配置所致。要了解将使用哪个标记表达式,你可以检查tokenizer.mask_token。请看以下执行代码:
tokenizer = AlbertTokenizer.from_pretrained('albert-base-v2')
print(tokenizer.mask_token)
# 输出: [MASK]
tokenizer = RobertaTokenizer.from_pretrained('roberta-base')
print(tokenizer.mask_token)
# 输出: <mask>
为了确保正确使用掩码标记,我们可以在流水线中添加fillmask.tokenizer.mask_token表达式,如下所示:
fillmask(f"The cat is very {fillmask.tokenizer.mask_token}.")
在下一节中,你将更多地了解ELECTRA及其工作原理。
ELECTRA
ELECTRA模型(由Kevin Clark等人在2020年提出)专注于一种新的掩码语言模型,利用了替换标记检测的训练目标。在预训练过程中,模型被迫学习区分真实的输入标记和合成生成的替换标记,其中合成的负面示例是从可能的标记中抽取的,而不是随机抽取的标记。ALBERT模型批评了BERT的NSP目标,认为它是一个主题检测问题,并使用了低质量的负面示例。ELECTRA训练了两个神经网络,一个生成器和一个鉴别器,前者生成高质量的负面示例,而后者则区分原始标记和替换标记。我们知道,GAN网络来自计算机视觉领域,其中生成器(G)生成假图像并试图欺骗鉴别器(D),而鉴别器网络则试图避免被欺骗。ELECTRA模型几乎采用了相同的生成器-鉴别器方法,用高质量的、合理的替代示例来替换原始标记,而这些替代示例是合成生成的。
为了不重复与其他示例相同的代码,我们仅提供一个ELECTRA生成器的简单fill-mask示例,如下所示:
fillmask = pipeline("fill-mask", model="google/electra-small-generator")
fillmask(f"The cat is very {fillmask.tokenizer.mask_token} .")
你可以在huggingface.co/Transformer…
DeBERTa
当我们查看GLUE和SuperGLUE NLP基准测试时,可以说解码增强的BERT与解缠注意力机制(DeBERTa)模型受到了极大的关注。它最近在许多学术和工业项目中也取得了成功的结果。DeBERTa的以下两项技术对其性能有影响:
- 解缠注意力机制:这是基于这样一种假设,即一对词的重要性由其内容和相对位置共同决定。当词语并排使用时,它们之间的依赖性比在不同句子中使用时要强得多。
- 增强掩码解码器:在softmax层中,原始BERT模型根据词内容和位置的聚合上下文向量解码掩码词。DeBERTa在softmax层之前包括了绝对词位置嵌入。
DeBERTa受到了Hugging Face的Transformers库的支持,初始化代码如下所示:
from transformers import DebertaConfig, DebertaModel
configuration = DebertaConfig()
model = DebertaModel(configuration)
configuration = model.config
做得好!我们终于完成了自动编码模型部分。现在,我们将继续探讨对Transformers成功有重要影响的分词算法。
使用分词算法
在本章的开头部分,我们使用了特定的分词器(即BertWordPieceTokenizer)来训练BERT模型。现在,有必要在这里详细讨论分词过程。分词是一种将文本输入拆分为标记并为每个标记分配标识符的方式,然后将其输入到神经网络架构中。最直观的方式是根据空格将序列拆分为更小的块。然而,这种方法并不满足某些语言(如日语)的需求,并且可能导致巨大的词汇问题。几乎所有Transformer模型都利用子词分词器,不仅是为了减少维度,还为了对训练中未见过的罕见词(或未知词)进行编码。分词的思想是,每个词,包括罕见词或未知词,都可以分解成训练语料库中广泛见到的有意义的小块。
一些传统的分词器是在Moses和nltk库中开发的,应用了基于规则的高级技术。然而,与Transformer一起使用的分词算法是基于自监督学习的,从语料库中提取规则。基于规则的分词的简单直观解决方案是使用字符、标点符号或空格。基于字符的分词会导致语言模型失去输入的意义。尽管它可以减少词汇表的大小(这是好的),但它使模型难以通过“c”、“a”和“t”字符的编码来捕捉“cat”的意义。此外,输入序列的维度变得非常大。同样,基于标点符号的模型无法正确处理某些表达,例如“haven’t”或“ain’t”。
最近,几种先进的子词分词算法,如BPE,已成为Transformer架构的重要组成部分。这些现代分词过程包括两个阶段:预分词阶段,即使用空格或依赖语言的规则简单地将输入拆分为标记;以及分词训练阶段,即训练分词器并基于标记构建合理大小的基础词汇表。在训练我们自己的分词器之前,让我们加载一个预训练的分词器。以下代码从Transformers库中加载一个土耳其语的分词器(类型为BertTokenizerFast),其词汇大小为32 K:
from transformers import AutoModel, AutoTokenizer
tokenizerTUR = AutoTokenizer.from_pretrained("dbmdz/bert-base-turkish-uncased")
print(f"VOC size is: {tokenizerTUR.vocab_size}")
print(f"The model is: {type(tokenizerTUR)}")
输出结果为:
VOC size is: 32000
The model is: Transformers.models.bert.tokenization_bert_fast.BertTokenizerFast
以下代码加载用于bert-base-uncased模型的英语BERT分词器:
from transformers import AutoModel, AutoTokenizer
tokenizerEN = AutoTokenizer.from_pretrained("bert-base-uncased")
print(f"VOC size is: {tokenizerEN.vocab_size}")
print(f"The model is {type(tokenizerEN)}")
输出结果为:
VOC size is: 30522
The model is ... BertTokenizerFast
让我们看看它们是如何工作的!我们使用这两个分词器对单词“telecommunication”进行分词:
word_en="telecommunication"
print(f"is in Turkish Model ? {word_en in tokenizerTUR.vocab}")
print(f"is in English Model ? {word_en in tokenizerEN.vocab}")
输出结果为:
is in Turkish Model ? False
is in English Model ? True
word_en标记已在英语分词器的词汇中,但不在土耳其语分词器的词汇中。那么,让我们看看土耳其语分词器会发生什么:
tokens = tokenizerTUR.tokenize(word_en)
tokens
输出结果为:
['tel', '##eco', '##mm', '##un', '##ica', '##tion']
由于土耳其语分词器模型的词汇表中没有这个单词,它需要将单词拆分成它认为有意义的部分。所有这些拆分的标记已经存储在模型的词汇表中。请注意以下执行的输出:
[t in tokenizerTUR.vocab for t in tokens]
输出结果为:
[True, True, True, True, True, True]
让我们使用我们已经加载的英语分词器对同一个单词进行分词:
tokenizerEN.tokenize(word_en)
输出结果为:
['telecommunication']
由于英语模型的基础词汇中已经有单词“telecommunication”,它不需要将其拆分成部分,而是将其作为一个整体处理。通过从语料库中学习,分词器能够将一个单词转换为主要在语法上合理的子成分。让我们从土耳其语中选择一个困难的例子。作为一种粘着语,土耳其语允许我们向词干添加许多后缀来构建非常长的单词。以下是土耳其语中最常见的长单词之一(en.wikipedia.org/wiki/Longes…
Muvaffakiyetsizleştiricileştiriveremeyebileceklerimizdenmişsinizcesine
意思是:“好像你碰巧来自于那些我们无法轻易/迅速地使他们成为失败者制造者的人中之一。”土耳其语BERT分词器可能在训练中没有见过这个词,但它见过其中的部分;例如,muvaffak(成功的)作为词干,iyet(成功性),siz(失败),leş(变得失败)等等。土耳其语分词器在与维基百科文章的结果进行比较时提取了似乎对土耳其语在语法上合理的成分:
print(tokenizerTUR.tokenize(long_word_tur))
输出结果为:
['muvaffak', '##iyet', '##siz', '##les', '##tir', '##ici', '##les', '##tir', '##iver', '##emeye', '##bilecekleri', '##mi', '##z', '##den', '##mis', '##siniz', '##cesine']
土耳其语分词器是WordPiece算法的一个例子,因为它与BERT模型一起工作。几乎所有的语言模型,包括BERT、DistilBERT和ELECTRA,都需要使用WordPiece分词器。
现在,我们准备好看看Transformer中使用的分词方法。首先,我们将讨论广泛使用的BPE、WordPiece和SentencePiece分词,然后使用Hugging Face的快速分词器库对它们进行训练。
BPE
BPE是一种数据压缩技术。它扫描数据序列,并迭代地将最频繁的字节对替换为一个符号。它最初在《Neural Machine Translation of Rare Words with Subword Units》(Sennrich等,2015年)中被改编和提出,用于解决机器翻译中未知词和罕见词的问题。目前,它已成功应用于GPT-2和许多其他最先进的模型中。许多现代分词算法基于这种压缩技术。
它将文本表示为字符n-gram的序列,也称为字符级子词。训练最初从语料库中看到的所有Unicode字符(或符号)的词汇表开始。这对于英语来说可能很小,但对于字符丰富的语言(如日语)来说可能很大。然后,它迭代地计算字符二元组,并用特殊的新符号替换最频繁的字符对。例如,t和h是经常出现的符号。我们将连续的符号替换为th符号。这个过程不断迭代,直到词汇表达到了所需的词汇大小。最常见的词汇大小约为30K。
BPE在表示未知词方面特别有效。然而,它可能无法保证处理罕见词和/或包括罕见子词的词。在这种情况下,它将罕见字符与特殊符号<UNK>关联,这可能会导致词义的部分丧失。作为一种潜在的解决方案,字节级BPE(BBPE)被提出,使用256字节集的词汇表代替Unicode字符,以确保每个基本字符都包含在词汇表中。
WordPiece分词
WordPiece是另一种广泛用于BERT、DistilBERT和ELECTRA的流行词分割算法。它由Schuster和Nakajima在2012年提出,以解决日语和韩语的语音问题。这项工作的动机是,虽然对英语语言来说问题不大,但词分割对许多亚洲语言来说是重要的预处理步骤,因为在这些语言中空格很少使用。因此,我们在亚洲语言的NLP研究中更经常遇到词分割方法。与BPE类似,WordPiece使用大量语料库来学习词汇和合并规则。虽然BPE和BBPE基于共现统计学习合并规则,但WordPiece算法使用最大似然估计从语料库中提取合并规则。它首先用Unicode字符(也称为词汇符号)初始化词汇表。它将训练语料库中的每个单词视为符号列表(最初是Unicode字符),然后它通过最大化似然性而不是频率从所有可能的候选符号对中迭代生成新的符号。这个生产流水线会一直运行,直到达到所需的词汇大小。
SentencePiece分词
之前的分词算法将文本视为空格分隔的单词列表。这种基于空格的拆分在某些语言中不起作用。例如,在德语中,复合名词是连写的,没有空格,如“Menschenrechte”(人权)。解决方案是使用特定语言的预分词器。在德语中,NLP流水线利用复合词拆分模块来检查一个词是否可以拆分成更小的词。然而,东亚语言(例如中文、日语、韩语和泰语)不在单词之间使用空格。SentencePiece算法旨在克服这种空格限制,这是一种简单且与语言无关的分词器,由Kudo等人在2018年提出。它将输入视为一个原始输入流,其中空格是字符集的一部分。使用SentencePiece的分词器会产生_字符,这也是我们之前在ALBERT模型示例的输出中看到_字符的原因。其他使用SentencePiece的流行语言模型包括XLNet、Marian和T5。
到目前为止,我们讨论了子词分词方法。现在是时候开始使用tokenizers库进行训练实验了。
tokenizers库
你可能已经注意到,之前代码示例中使用的土耳其语和英语的预训练分词器是Transformers库的一部分。另一方面,Hugging Face团队独立于Transformers库提供了tokenizers库,使其更加快速并为我们提供了更多的自由。该库最初是用Rust编写的,这使得多核并行计算成为可能,并在Python中进行了封装(github.com/huggingface…
要安装tokenizers库,我们可以使用以下命令:
$ pip install tokenizers
tokenizers库提供了几个组件,使我们可以从原始文本的预处理到解码标记单元ID构建一个端到端的分词器:
- Normalizer → PreTokenizer → Modeling → Post-Processor → Decoding
以下图表展示了分词流水线:
这个流水线可以解释如下:
- Normalizer(标准化器) :允许我们应用原始文本处理,如小写化、去除空格、Unicode标准化和去除重音符号。
- PreTokenizer(预分词器) :为下一个训练阶段准备语料库。它根据规则(如空格)将输入拆分为标记。
- 模型训练:分词算法如BPE、BBPE和WordPiece已经讨论过。它发现子词/词汇并学习生成规则。
- 后处理(Post-processing) :提供与Transformers模型兼容的高级类构造,如BertProcessors。我们通常会在输入架构之前为分词输入添加特殊标记,如[CLS]和[SEP]。
- 解码器(Decoder) :负责将标记ID转换回原始字符串。主要用于检查处理过程中的情况。
训练BPE
让我们训练一个基于BPE的分词器,使用莎士比亚的戏剧:
加载代码如下:
import nltk
from nltk.corpus import gutenberg
nltk.download('gutenberg')
nltk.download('punkt')
plays = ['shakespeare-macbeth.txt','shakespeare-hamlet.txt',
'shakespeare-caesar.txt']
shakespeare = [" ".join(s) for ply in plays for s in gutenberg.sents(ply)]
我们将需要一个后处理器(TemplateProcessing)来处理所有的分词算法。我们需要自定义后处理器以使输入适合特定语言模型。例如,以下模板适用于BERT模型,因为它需要在输入的开头添加[CLS]标记,在中间和结尾添加[SEP]标记。
我们定义模板如下:
from tokenizers.processors import TemplateProcessing
special_tokens = ["[UNK]","[CLS]","[SEP]","[PAD]","[MASK]"]
temp_proc = TemplateProcessing(
single="[CLS] $A [SEP]",
pair="[CLS] $A [SEP] $B:1 [SEP]:1",
special_tokens=[
("[CLS]", special_tokens.index("[CLS]")),
("[SEP]", special_tokens.index("[SEP]")),
],
)
我们导入必要的组件来构建一个端到端的分词流水线:
from tokenizers import Tokenizer
from tokenizers.normalizers import (
Sequence,Lowercase, NFD, StripAccents)
from tokenizers.pre_tokenizers import Whitespace
from tokenizers.models import BPE
from tokenizers.decoders import BPEDecoder
首先,我们实例化BPE,如下所示:
tokenizer = Tokenizer(BPE())
预处理部分包含两个组件:标准化器和预分词器。我们可以有多个标准化器,因此我们组合了一系列包含多个标准化器的组件,其中NFD()是一个Unicode标准化器,StripAccents()用于去除重音符号。对于预分词,Whitespace()根据空格轻柔地分隔文本。由于解码器组件必须与模型兼容,因此为BPE模型选择了BPEDecoder:
tokenizer.normalizer = Sequence(
[NFD(),Lowercase(),StripAccents()])
tokenizer.pre_tokenizer = Whitespace()
tokenizer.decoder = BPEDecoder()
tokenizer.post_processor = temp_proc
现在我们准备好在数据上训练分词器了。以下执行代码实例化BpeTrainer(),它帮助我们通过设置超参数来组织整个训练过程。由于我们的莎士比亚语料库相对较小,我们将词汇大小参数设置为5000。对于大规模项目,我们使用更大的语料库,通常将词汇大小设置为大约30K:
from tokenizers.trainers import BpeTrainer
trainer = BpeTrainer(vocab_size=5000,
special_tokens= special_tokens)
tokenizer.train_from_iterator(shakespeare, trainer=trainer)
print(f"Trained vocab size: {tokenizer.get_vocab_size()}")
输出结果:
Trained vocab size: 5000
我们已经完成了训练!
注意:从文件系统中训练:为了开始训练过程,我们将莎士比亚对象作为字符串列表传递给tokenizer.train_from_iterator()。对于大型项目,我们需要设计一个Python生成器,通过从文件系统而不是内存存储中读取文件来生成字符串行。你还应该检查tokenizer.train(),以便像前面BERT训练部分中那样从文件系统存储中进行训练。
让我们从《麦克白》随便找一句话,称为sen,并使用我们的新分词器对其进行分词:
sen = "Is this a dagger which I see before me, the handle toward my hand?"
sen_enc = tokenizer.encode(sen)
print(f"Output: {format(sen_enc.tokens)}")
输出结果:
Output: ['[CLS]', 'is', 'this', 'a', 'dagger', 'which', 'i', 'see', 'before', 'me', ',', 'the', 'hand', 'le', 'toward', 'my', 'hand', '?', '[SEP]']
由于之前的后处理器功能,我们在适当的位置看到了额外的[CLS]和[SEP]标记。只有一个拆分词handle(hand, le),因为我们传递给模型的句子来自模型已经知道的《麦克白》。此外,我们使用了一个小语料库,分词器没有被迫使用压缩。让我们传递一个分词器可能不知道的具有挑战性的短语“Hugging Face”:
sen_enc2 = tokenizer.encode("Macbeth and Hugging Face")
print(f"Output: {format(sen_enc2.tokens)}")
输出结果:
Output: ['[CLS]', 'macbeth', 'and', 'hu', 'gg', 'ing', 'face', '[SEP]']
术语“Hugging”是小写的,并被拆分为三个部分hu,gg和ing,因为模型词汇表包含了所有其他标记,但不包括Hugging。现在让我们传递两个句子:
two_enc = tokenizer.encode("I like Hugging Face!", "He likes Macbeth!")
print(f"Output: {format(two_enc.tokens)}")
输出结果:
Output: ['[CLS]', 'i', 'like', 'hu', 'gg', 'ing', 'face', '!', '[SEP]', 'he', 'li', 'kes', 'macbeth', '!', '[SEP]']
请注意,后处理器注入了[SEP]标记作为指示符。
是时候保存模型了。我们可以保存子词分词模型或整个分词流水线。首先,让我们只保存BPE模型:
tokenizer.model.save('.')
输出结果:
['./vocab.json', './merges.txt']
模型保存了两个与词汇表和合并规则相关的文件。merges.txt文件包含4948个合并规则:
$ wc -l ./merges.txt
4948 ./merges.txt
前五个合并规则如下所示,我们看到[t, h]是排名第一的规则,因为这是最频繁的配对。在测试中,模型扫描文本输入并尝试首先合并这两个符号(如果适用):
$ head -3 ./merges.txt
#version: 0.2 - Trained by `huggingface/tokenizers`
t h
o u
a n
th e
r e
BPE算法根据频率对规则进行排序。当你手动计算莎士比亚语料库中的字符二元组时,你会发现[t, h]是最常见的配对。
现在让我们保存并加载整个分词流水线:
tokenizer.save("MyBPETokenizer.json")
tokenizerFromFile = Tokenizer.from_file("MyBPETokenizer.json")
sen_enc3 = tokenizerFromFile.encode("I like Hugging Face and Macbeth")
print(f"Output: {format(sen_enc3.tokens)}")
输出结果:
Output: ['[CLS]', 'i', 'like', 'hu', 'gg', 'ing', 'face', 'and', 'macbeth', '[SEP]']
我们成功重新加载了分词器!
训练WordPiece模型
在这一部分中,我们将训练WordPiece模型:
我们首先导入必要的模块:
from tokenizers.models import WordPiece
from tokenizers.decoders import WordPiece as WordPieceDecoder
from tokenizers.normalizers import BertNormalizer
以下几行代码实例化一个空的WordPiece分词器,并准备进行训练。BertNormalizer是一个预定义的标准化器序列,包含清理文本、转换重音、处理中文字符和小写化的过程:
tokenizer = Tokenizer(WordPiece())
tokenizer.normalizer = BertNormalizer()
tokenizer.pre_tokenizer = Whitespace()
tokenizer.decoder = WordPieceDecoder()
现在,我们为WordPiece实例化一个合适的训练器,即WordPieceTrainer(),以组织训练过程:
from tokenizers.trainers import WordPieceTrainer
trainer = WordPieceTrainer(vocab_size=5000,
special_tokens=["[UNK]","[CLS]","[SEP]","[PAD]","[MASK]"])
tokenizer.train_from_iterator(shakespeare, trainer=trainer)
output = tokenizer.encode(sen)
print(output.tokens)
输出结果为:
['is', 'this', 'a', 'dagger', 'which', 'i', 'see', 'before', 'me', ',', 'the', 'hand', '##le', 'toward', 'my', 'hand', '?']
让我们使用WordPieceDecoder()来正确处理句子:
tokenizer.decode(output.ids)
输出结果为:
'is this a dagger which i see before me, the handle toward my hand?'
在输出中,我们没有遇到任何[UNK]标记,因为分词器在某种程度上知道或拆分了输入以进行编码。让我们通过以下代码强制模型生成[UNK]标记。我们传递一个土耳其语句子给我们的分词器:
tokenizer.encode("Kralsın aslansın Macbeth!").tokens
输出结果为:
['[UNK]', '[UNK]', 'macbeth', '!']
做得好!我们得到了一些未知标记,因为分词器无法根据合并规则和基本词汇表对给定的词进行分解。
到目前为止,我们已经从标准化组件到解码器组件设计了我们的分词流水线。另一方面,tokenizers库为我们提供了一个已经准备好的(未训练的)空分词流水线,带有适当的组件,用于快速构建生产原型。以下是一些预制的分词器:
- CharBPETokenizer:原始的BPE
- ByteLevelBPETokenizer:BPE的字节级版本
- SentencePieceBPETokenizer:与SentencePiece使用的BPE实现兼容的版本
- BertWordPieceTokenizer:著名的BERT分词器,使用WordPiece
以下代码导入了这些流水线:
from tokenizers import (ByteLevelBPETokenizer,
CharBPETokenizer,
SentencePieceBPETokenizer,
BertWordPieceTokenizer)
这些流水线已经为我们设计好了。其余的过程(如训练、保存模型和使用分词器)与我们之前的BPE和WordPiece训练过程相同。
做得好!我们已经取得了很大进展,训练了我们的第一个Transformer模型以及相应的分词器。
总结
在本章中,我们在理论和实践上体验了自动编码模型。从学习BERT的基础知识开始,我们从头开始训练了它以及相应的分词器。我们还讨论了如何在其他框架(如Keras)中工作。除了BERT之外,我们还审查了其他自动编码模型,如ALBERT、RoBERTa、ELECTRA和DeBERTa。为了避免过多的代码重复,我们没有提供训练其他模型的完整实现。在BERT训练期间,我们训练了WordPiece分词算法。在最后一部分中,我们研究了其他分词算法,因为值得讨论和理解它们,因为不同的Transformer架构使用不同的分词算法。
自动编码模型使用原始Transformer的左侧解码器,主要针对分类问题进行微调。在下一章中,我们将讨论并学习Transformer的右侧解码器部分,以实现语言生成模型。