代码示例 / 自然语言处理 / 从头开始使用 🤗 Transformers 和 TPU 训练语言模型

从头开始使用 🤗 Transformers 和 TPU 训练语言模型

作者: Matthew Carrigan, Sayak Paul
创建日期: 2023/05/21
最后修改: 2023/05/21
描述: 使用 🤗 Transformers 在 TPU 上训练一个掩码语言模型。

在 Colab 中查看 GitHub 源代码


引言

在本示例中,我们将介绍如何使用 TensorFlow、 🤗 Transformers 和 TPU 训练一个掩码语言模型。

TPU 训练是一项有用的技能:TPU 节点具有高性能和极高的可扩展性,可以轻松训练参数从几千万到真正巨大规模的模型:谷歌的 PaLM 模型(超过 5000 亿参数!)完全在 TPU 节点上训练完成。

我们之前写过一个 教程 和一个 Colab 示例,展示了使用 TensorFlow 进行小规模 TPU 训练并介绍了理解使模型在 TPU 上正常运行所需的核心概念。然而,我们的 Colab 示例并未包含从头训练语言模型所需的所有步骤,比如训练分词器。因此,我们想提供一个全面的示例,逐步带您了解所有关键步骤。

与我们的 Colab 示例一样,我们充分利用 TensorFlow 通过 XLA 和 TPUStrategy 提供的非常干净的 TPU 支持。我们还将受益于 🤗 Transformers 中的大多数 TensorFlow 模型都是完全 XLA 兼容的。因此,实际上只需很少的工作即可在 TPU 上运行它们。

此示例设计为可扩展,且更接近于现实的训练运行——尽管我们默认只使用了一个 BERT 大小的模型,但通过更改几个配置选项,代码可以扩展到更大的模型和更强大的 TPU 节点切片。

以下图表为您提供使用 TensorFlow 和 TPU 训练语言模型所涉及步骤的概述:

https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/tf_tpu/tf_tpu_steps.png

(本示例的内容与 这篇博客文章 重叠。)


数据

我们使用 WikiText 数据集 (v1)。您可以前往 Hugging Face Hub 上的数据集页面 来浏览数据集。

data_preview_wikitext

由于数据集已经以兼容格式在 Hub 上可用,我们可以轻松使用 🤗 datasets 加载和交互。但从头开始训练语言模型还需要一个单独的分词器训练步骤。为简洁起见,我们在本示例中跳过了该部分,但以下是我们可以执行的步骤,以从头开始训练分词器:

  • 使用 🤗 datasets 加载 WikiText 的 train 切分。
  • 利用 🤗 tokenizers 训练一个 Unigram 模型
  • 将训练好的分词器上传到 Hub。

您可以在 这里 找到分词器训练代码,并在 这里 找到分词器。该脚本还允许您使用来自 Hub 的 任何兼容数据集 运行它。


对数据进行分词并创建 TFRecord

一旦分词器训练完成,我们就可以在所有数据集切分(在这种情况下为 trainvalidationtest)上使用它并从中生成 TFRecord 分片。将数据分片分散到多个 TFRecord 分片上有助于进行大规模并行处理,相比之下,将每个切分保存在单个 TFRecord 文件中效果要差一些。

我们逐个对样本进行分词。然后,我们取一批样本,将它们连接在一起,并将其拆分成几个固定大小的块(在我们的例子中为 128)。我们遵循这种策略,而不是将一批样本进行固定长度的分词,以避免 攻击性地丢弃文本内容(因为截断)。

然后我们将这些标记化的样本分批处理,并将这些批次序列化为多个 TFRecord 分片,其中总数据集长度和各个分片大小决定了 分片的数量。最后,这些分片被推送到一个 Google Cloud Storage (GCS) 存储桶

如果您使用 TPU 节点进行训练,则需要从 GCS 存储桶中流式传输数据,因为节点主机内存非常小。但对于 TPU 虚拟机,我们可以在本地使用数据集,甚至将持久存储附加到这些虚拟机上。由于 TPU 节点(我们在 Colab 中使用的节点)仍然被广泛使用,因此我们的示例基于使用 GCS 存储桶 进行数据存储。

您可以在 this script 中的代码中看到这一切。为了方便起见,我们还在 this repository 上托管了结果 TFRecord 分片。

一旦数据被标记化并序列化为 TFRecord 分片,我们就可以继续进行训练。


训练

设置和导入

让我们开始安装 🤗 Transformers。

!pip install transformers -q

然后,让我们导入我们需要的模块。

import os
import re

import tensorflow as tf

import transformers

初始化 TPU

然后让我们连接到 TPU 并确定分布策略:

tpu = tf.distribute.cluster_resolver.TPUClusterResolver()

tf.config.experimental_connect_to_cluster(tpu)
tf.tpu.experimental.initialize_tpu_system(tpu)

strategy = tf.distribute.TPUStrategy(tpu)

print(f"Available number of replicas: {strategy.num_replicas_in_sync}")
可用副本数量: 8

然后我们加载标记器。有关标记器的更多详细信息,请查看 它的仓库。 对于模型,我们使用 RoBERTa(基本变体),在 this paper中介绍。

初始化标记器

tokenizer = "tf-tpu/unigram-tokenizer-wikitext"
pretrained_model_config = "roberta-base"

tokenizer = transformers.AutoTokenizer.from_pretrained(tokenizer)
config = transformers.AutoConfig.from_pretrained(pretrained_model_config)
config.vocab_size = tokenizer.vocab_size
下载中 (…)okenizer_config.json:   0%|          | 0.00/483 [00:00<?, ?B/s]

下载中 (…)/main/tokenizer.json:   0%|          | 0.00/1.61M [00:00<?, ?B/s]

下载中 (…)cial_tokens_map.json:   0%|          | 0.00/286 [00:00<?, ?B/s]

下载中 (…)lve/main/config.json:   0%|          | 0.00/481 [00:00<?, ?B/s]

准备数据集

我们现在加载 WikiText 数据集的 TFRecord 分片(Hugging Face 团队 为本示例提前准备的):

train_dataset_path = "gs://tf-tpu-training-resources/train"
eval_dataset_path = "gs://tf-tpu-training-resources/validation"

training_records = tf.io.gfile.glob(os.path.join(train_dataset_path, "*.tfrecord"))
eval_records = tf.io.gfile.glob(os.path.join(eval_dataset_path, "*.tfrecord"))

现在,我们将编写一个实用程序,用于计算我们拥有的训练样本数量。我们需要 知道这个值以便稍后正确初始化优化器:

def count_samples(file_list):
    num_samples = 0
    for file in file_list:
        filename = file.split("/")[-1]
        sample_count = re.search(r"-\d+-(\d+)\.tfrecord", filename).group(1)
        sample_count = int(sample_count)
        num_samples += sample_count

    return num_samples


num_train_samples = count_samples(training_records)
print(f"Number of total training samples: {num_train_samples}")
总训练样本数量: 300917

让我们现在准备数据集以进行训练和评估。我们首先编写实用程序。首先,我们需要能够解码 TFRecords:

max_sequence_length = 512


def decode_fn(example):
    features = {
        "input_ids": tf.io.FixedLenFeature(
            dtype=tf.int64, shape=(max_sequence_length,)
        ),
        "attention_mask": tf.io.FixedLenFeature(
            dtype=tf.int64, shape=(max_sequence_length,)
        ),
    }
    return tf.io.parse_single_example(example, features)

这里,max_sequence_length 需要与准备 TFRecord 分片时使用的相同。请参阅 this script 以获取更多详细信息。

接下来,我们有一个掩码实用程序,负责对输入的部分进行掩码并为掩码语言模型准备标签,以供其学习。我们利用 DataCollatorForLanguageModeling 为此目的而使用。

# 我们使用标准的掩码概率为 0.15。`mlm_probability`表示
# 我们在序列中掩盖输入标记的概率。
mlm_probability = 0.15
data_collator = transformers.DataCollatorForLanguageModeling(
    tokenizer=tokenizer, mlm_probability=mlm_probability, mlm=True, return_tensors="tf"
)


def mask_with_collator(batch):
    special_tokens_mask = (
        ~tf.cast(batch["attention_mask"], tf.bool)
        | (batch["input_ids"] == tokenizer.cls_token_id)
        | (batch["input_ids"] == tokenizer.sep_token_id)
    )
    batch["input_ids"], batch["labels"] = data_collator.tf_mask_tokens(
        batch["input_ids"],
        vocab_size=len(tokenizer),
        mask_token_id=tokenizer.mask_token_id,
        special_tokens_mask=special_tokens_mask,
    )
    return batch

现在是时候编写最终的数据准备工具,将其全部放入一个 tf.data.Dataset对象中:

auto = tf.data.AUTOTUNE
shuffle_buffer_size = 2**18


def prepare_dataset(
    records, decode_fn, mask_fn, batch_size, shuffle, shuffle_buffer_size=None
):
    num_samples = count_samples(records)
    dataset = tf.data.Dataset.from_tensor_slices(records)
    if shuffle:
        dataset = dataset.shuffle(len(dataset))
    dataset = tf.data.TFRecordDataset(dataset, num_parallel_reads=auto)
    # TF无法推断总样本数,因为它尚未读取
    # 所有记录,所以我们在这里断言它。
    dataset = dataset.apply(tf.data.experimental.assert_cardinality(num_samples))
    dataset = dataset.map(decode_fn, num_parallel_calls=auto)
    if shuffle:
        assert shuffle_buffer_size is not None
        dataset = dataset.shuffle(shuffle_buffer_size)
    dataset = dataset.batch(batch_size, drop_remainder=True)
    dataset = dataset.map(mask_fn, num_parallel_calls=auto)
    dataset = dataset.prefetch(auto)
    return dataset

让我们用这些工具准备我们的数据集:

per_replica_batch_size = 16  # 根据需要更改。
batch_size = per_replica_batch_size * strategy.num_replicas_in_sync
shuffle_buffer_size = 2**18  # 默认值对应于 seq_len 512 的 1GB 缓冲区

train_dataset = prepare_dataset(
    training_records,
    decode_fn=decode_fn,
    mask_fn=mask_with_collator,
    batch_size=batch_size,
    shuffle=True,
    shuffle_buffer_size=shuffle_buffer_size,
)

eval_dataset = prepare_dataset(
    eval_records,
    decode_fn=decode_fn,
    mask_fn=mask_with_collator,
    batch_size=batch_size,
    shuffle=False,
)

现在,让我们调查一下单个数据集的批次是什么样的。

single_batch = next(iter(train_dataset))
print(single_batch.keys())
dict_keys(['attention_mask', 'input_ids', 'labels'])
  • input_ids表示包含掩码标记的输入样本的标记化版本。
  • attention_mask表示在执行注意力操作时使用的掩码。
  • labels表示模型应该学习的被掩盖标记的实际值。
for k in single_batch:
    if k == "input_ids":
        input_ids = single_batch[k]
        print(f"输入形状: {input_ids.shape}")
    if k == "labels":
        labels = single_batch[k]
        print(f"标签形状: {labels.shape}")
输入形状: (128, 512)
标签形状: (128, 512)

现在,我们可以利用我们的 tokenizer 来调查标记的值。让我们从 input_ids 开始:

idx = 0
print("获取第一个样本:\n")
print(tokenizer.decode(input_ids[idx].numpy()))
获取第一个样本:
他们称Tsugum[MASK]这个角色为游戏中遇到的[MASK]悲剧女英雄[MASK]之一。Chandran将这款游戏评为第六代视频[MASK]控制台中第三好的角色@[MASK][MASK]扮演游戏,称这是他在[MASK]Infinity[MASK]中的最爱,也是他整体上最喜欢的[MASK]游戏之一[MASK].[MASK]
[SEP][CLS][SEP][CLS][SEP][CLS] = [MASK]罗斯海派对 1914[MASK]– 16 = 
[SEP][CLS][SEP][CLS]罗斯海派对是萨尔[MASK]沙克尔顿爵士的帝国横渡南极探险1914年的一部分[MASK]。任务是沿着罗斯海到比尔德莫尔冰川的巨大冰障建立一系列补给站,沿着早期南极探险[MASK]建立的路线。探险的主要小组在[MASK]的带领下,计划在南极洲的相对韦德尔海岸登陆[MASK],并通过南[MASK]穿越整个大陆到达罗斯海。由于主要小组将无法[MASK]携带[MASK]燃料和补给整个距离[MASK],他们的生存依赖于罗斯海派对的补给站[MASK][MASK][MASK]将覆盖他们[MASK]四分之一的旅程。
[SEP][CLS] 罗斯海派对于1914年8月从伦敦启航,搭乘[MASK]“坚韧号”前往韦德尔海。与此同时,罗斯海派对[MASK]在澳大利亚集结,可能[MASK]是为第二次探险船SY Aurora前往罗斯海作准备。组织和财政问题[MASK]了他们直到1914年12月,缩短了他们的第一次补给站@[MASK]季节。[MASK][MASK]到达时,缺乏经验的小组[MASK]难以掌握南极旅行的艺术,在[MASK]中损失了大部分雪橇犬[MASK],更大的不幸发生在南方冬季来临时,Aurora[MASK]在强烈风暴中被撕扯,无法返回,导致岸上的小组被困。
[SEP][CLS] 尽管遇到[MASK] setbacks,罗斯海派对依然在人员争端、极端天气[MASK]、疾病和三名成员的去世中幸存下来,在其[MASK]南极季节期间完整地完成了任务。这一成功证明最终[MASK]没有意义,因为沙克尔顿的格里马尔迪探险未能完成。

正如预期,解码后的标记也包含特殊标记,包括掩码标记。接下来, 让我们研究一下掩码标记:

# 获取第一序列的前30个标记。
print(labels[0].numpy()[:30])
[-100 -100 -100 -100 -100 -100 -100 -100 -100   43 -100 -100 -100 -100
  351 -100 -100 -100   99 -100 -100 -100 -100 -100 -100 -100 -100 -100
 -100 -100]

这里, -100 意味着对应的 input_ids 中的标记没有被掩码,而非 -100 的值表示被掩码标记的实际值。


初始化模式和优化器

数据集准备好后,我们现在在 strategy.scope() 中初始化和编译我们的模型和优化器:

# 在这个例子中,我们将这个值保持为10。但在实际运行中,应该从500开始。
num_epochs = 10
steps_per_epoch = num_train_samples // (
    per_replica_batch_size * strategy.num_replicas_in_sync
)
total_train_steps = steps_per_epoch * num_epochs
learning_rate = 0.0001
weight_decay_rate = 1e-3

with strategy.scope():
    model = transformers.TFAutoModelForMaskedLM.from_config(config)
    model(
        model.dummy_inputs
    )  # 通过模型传递一些虚拟输入,以确保所有权重都构建完毕
    optimizer, schedule = transformers.create_optimizer(
        num_train_steps=total_train_steps,
        num_warmup_steps=total_train_steps // 20,
        init_lr=learning_rate,
        weight_decay_rate=weight_decay_rate,
    )
    model.compile(optimizer=optimizer, metrics=["accuracy"])
在 compile() 中未指定损失 - 模型的内部损失计算将被用作损失。不要惊慌 - 这是一种在 Transformers 中训练 TensorFlow 模型的常见方式!要禁用此行为,请传递损失参数,或者如果您不希望模型计算损失,则明确传递 `loss=None`。

这里需要注意几点: * create_optimizer() 函数创建一个具有学习率调度的Adam优化器,使用预热阶段后接线性衰减。由于我们在这里使用权重衰减,因此在内部, create_optimizer() 实例化了 正确的Adam变体 以启用权重衰减。 * 在编译模型时,我们没有使用任何 loss 参数。这是因为 TensorFlow 模型在提供预期标签时会在内部计算损失。根据模型类型和使用的标签, transformers 将自动推断出要使用的损失。

开始训练!

接下来,我们设置一个方便的回调,将中间训练检查点推送到 Hugging Face Hub。为了能够使这个回调生效,我们需要登录到我们的 Hugging Face 账户(如果您没有,可以 在这里 免费创建一个)。执行下面的代码进行登录:

from huggingface_hub import notebook_login

notebook_login()

现在让我们定义 PushToHubCallback

hub_model_id = output_dir = "masked-lm-tpu"

callbacks = []
callbacks.append(
    transformers.PushToHubCallback(
        output_dir=output_dir, hub_model_id=hub_model_id, tokenizer=tokenizer
    )
)
克隆 https://huggingface.co/sayakpaul/masked-lm-tpu 到本地空目录。
WARNING:huggingface_hub.repository:克隆 https://huggingface.co/sayakpaul/masked-lm-tpu 到本地空目录。

下载文件 tf_model.h5:   0%|          | 15.4k/477M [00:00<?, ?B/s]

清理文件 tf_model.h5:   0%|          | 1.00k/477M [00:00<?, ?B/s]

现在,我们准备开始使用 TPU 进行训练:

# 为了便于本示例的运行时间,
# 我们将批次数限制为仅2。
model.fit(
    train_dataset.take(2),
    validation_data=eval_dataset.take(2),
    epochs=num_epochs,
    callbacks=callbacks,
)

# 训练后,我们还会序列化最终模型。
model.save_pretrained(output_dir)
Epoch 1/10
2/2 [==============================] - 96s 35s/step - loss: 10.2116 - accuracy: 0.0000e+00 - val_loss: 10.1957 - val_accuracy: 2.2888e-05
Epoch 2/10
2/2 [==============================] - 9s 2s/step - loss: 10.2017 - accuracy: 0.0000e+00 - val_loss: 10.1798 - val_accuracy: 0.0000e+00
Epoch 3/10
2/2 [==============================] - ETA: 0s - loss: 10.1890 - accuracy: 7.6294e-06

WARNING:tensorflow:Callback method `on_train_batch_end` 处理时间慢于批次时间(批次时间: 0.0045s vs `on_train_batch_end` 时间: 9.1679s)。请检查你的回调函数。

2/2 [==============================] - 35s 27s/step - loss: 10.1890 - accuracy: 7.6294e-06 - val_loss: 10.1604 - val_accuracy: 1.5259e-05
Epoch 4/10
2/2 [==============================] - 8s 2s/step - loss: 10.1733 - accuracy: 1.5259e-05 - val_loss: 10.1145 - val_accuracy: 7.6294e-06
Epoch 5/10
2/2 [==============================] - 34s 26s/step - loss: 10.1336 - accuracy: 1.5259e-05 - val_loss: 10.0666 - val_accuracy: 7.6294e-06
Epoch 6/10
2/2 [==============================] - 10s 2s/step - loss: 10.0906 - accuracy: 6.1035e-05 - val_loss: 10.0200 - val_accuracy: 5.4169e-04
Epoch 7/10
2/2 [==============================] - 33s 25s/step - loss: 10.0360 - accuracy: 6.1035e-04 - val_loss: 9.9646 - val_accuracy: 0.0049
Epoch 8/10
2/2 [==============================] - 8s 2s/step - loss: 9.9830 - accuracy: 0.0038 - val_loss: 9.8938 - val_accuracy: 0.0155
Epoch 9/10
2/2 [==============================] - 33s 26s/step - loss: 9.9067 - accuracy: 0.0116 - val_loss: 9.8225 - val_accuracy: 0.0198
Epoch 10/10
2/2 [==============================] - 8s 2s/step - loss: 9.8302 - accuracy: 0.0196 - val_loss: 9.7454 - val_accuracy: 0.0215

一旦你的训练完成,你可以轻松地进行推理,如下所示:

from transformers import pipeline

# 在这里替换你的 `model_id`。
# 这里,我们正在使用 Hugging Face 团队训练了更长时间的模型。
model_id = "tf-tpu/roberta-base-epochs-500-no-wd"
unmasker = pipeline("fill-mask", model=model_id, framework="tf")
print(unmasker("Goal of my life is to [MASK]."))
正在下载 (…)lve/main/config.json:   0%|          | 0.00/649 [00:00<?, ?B/s]

正在下载 tf_model.h5:   0%|          | 0.00/500M [00:00<?, ?B/s]

所有模型检查点层在初始化 TFRobertaForMaskedLM 时都被使用。
TFRobertaForMaskedLM 的所有层都是从模型检查点 tf-tpu/roberta-base-epochs-500-no-wd 初始化的。
如果你的任务与检查点模型训练的任务类似,你可以在不进一步训练的情况下,直接使用 TFRobertaForMaskedLM 进行预测。

正在下载 (…)okenizer_config.json:   0%|          | 0.00/683 [00:00<?, ?B/s]

正在下载 (…)/main/tokenizer.json:   0%|          | 0.00/1.61M [00:00<?, ?B/s]

正在下载 (…)cial_tokens_map.json:   0%|          | 0.00/286 [00:00<?, ?B/s]

[{'score': 0.10031876713037491, 'token': 52, 'token_str': 'be', 'sequence': 'Goal of my life is to be.'}, {'score': 0.032648470252752304, 'token': 5, 'token_str': '', 'sequence': 'Goal of my life is to .'}, {'score': 0.02152678370475769, 'token': 138, 'token_str': 'work', 'sequence': 'Goal of my life is to work.'}, {'score': 0.019547568634152412, 'token': 984, 'token_str': 'act', 'sequence': 'Goal of my life is to act.'}, {'score': 0.01939115859568119, 'token': 73, 'token_str': 'have', 'sequence': 'Goal of my life is to have.'}]

就这样!

如果你喜欢这个例子,我们鼓励你查看完整的代码库 这里 以及相关的博客文章 这里.