Pytorch
TensorFlow

#
处理数据

在这一小节你将学习 第一小节 中提到的“如何使用模型中心（hub）加载大型数据集”，下面是用模型中心的数据在 PyTorch 上训练句子分类器的一个例子：

    import torch
    from transformers import AdamW, AutoTokenizer, AutoModelForSequenceClassification

    # 和之前一样
    checkpoint = "bert-base-uncased"
    tokenizer = AutoTokenizer.from_pretrained(checkpoint)
    model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
    sequences = [
        "I've been waiting for a HuggingFace course my whole life.",
        "This course is amazing!",
    ]
    batch = tokenizer(sequences, padding=True, truncation=True, return_tensors="pt")

    # 新增部分
    batch["labels"] = torch.tensor([1, 1])

    optimizer = AdamW(model.parameters())
    loss = model(**batch).loss
    loss.backward()
    optimizer.step()

然而仅用两句话训练模型不会产生很好的效果，你需要准备一个更大的数据集才能得到更好的训练结果。

在本节中，我们以 MRPC（微软研究院释义语料库）数据集为例，该数据集由威廉·多兰和克里斯·布罗克特在 这篇文章 发布，由 5801 对句子组成，每个句子对带有一个标签来指示它们是否为同义（即两个句子的意思相同）。在本章选择该数据集的原因是它的数据体量小，容易对其进行训练。

##
从模型中心（Hub）加载数据集

模型中心（hub）不仅仅包含模型，还有许多别的语言的数据集。访问 Datasets 的链接即可进行浏览。我们建议你在完成本节的学习后阅读一下 加载和处理新的数据集 这篇文章，这会让你对 huggingface 的数据集理解更加清晰。现在让我们使用 MRPC 数据集中的 GLUE 基准测试数据集 作为我们训练所使用的数据集，它是构成 MRPC 数据集的 10 个数据集之一，作为一个用于衡量机器学习模型在 10 个不同文本分类任务中性能的学术基准。

🤗 Datasets 库提供了一条非常便捷的命令，可以在模型中心（hub）上下载和缓存数据集。你可以以下代码下载 MRPC 数据集：

⚠️ 警告 确保你已经运行 `pip install datasets` 安装了 `datasets`。然后，再继续下面的加载 MRPC 数据集和打印出来查看其内容。

    from datasets import load_dataset

    raw_datasets = load_dataset("glue", "mrpc")
    raw_datasets

    DatasetDict({
        train: Dataset({
            features: ['sentence1', 'sentence2', 'label', 'idx'],
            num_rows: 3668
        })
        validation: Dataset({
            features: ['sentence1', 'sentence2', 'label', 'idx'],
            num_rows: 408
        })
        test: Dataset({
            features: ['sentence1', 'sentence2', 'label', 'idx'],
            num_rows: 1725
        })
    })

现在我们获得了一个 `DatasetDict` 对象，这个对象包含训练集、验证集和测试集。每一个集合都包含 4 个列（ `sentence1` ， `sentence2` ， `label` 和 `idx` ）以及一个代表行数的变量（每个集合中的行的个数）。运行结果显示该训练集中有 3668 对句子，验证集中有 408 对，测试集中有 1725 对。

默认情况下，该命令会下载数据集并缓存到 `~/.cache/huggingface/datasets` 。回想在第 2 章中我们学到过，可以通过设置 `HF_HOME` 环境变量来自定义缓存的文件夹。

我们可以访问该数据集中的每一个 `raw_train_dataset` 对象，例如使用字典：

    raw_train_dataset = raw_datasets["train"]
    raw_train_dataset[0]

    {
        "idx": 0,
        "label": 1,
        "sentence1": 'Amrozi accused his brother , whom he called " the witness " , of deliberately distorting his evidence .',
        "sentence2": 'Referring to him as only " the witness " , Amrozi accused his brother of deliberately distorting his evidence .',
    }

现在可以看到标签已经是整数了，因此不需要对标签做任何预处理。如果想要知道不同数字对应标签的实际含义，我们可以查看 `raw_train_dataset` 的 `features` 。这告诉我们每列的类型：

    raw_train_dataset.features

    {
        "sentence1": Value(dtype="string", id=None),
        "sentence2": Value(dtype="string", id=None),
        "label": ClassLabel(
            num_classes=2, names=["not_equivalent", "equivalent"], names_file=None, id=None
        ),
        "idx": Value(dtype="int32", id=None),
    }

上面的例子中的 `Label（标签）` 是一种 `ClassLabel（分类标签）` ，也就是使用整数建立起类别标签的映射关系。 `0` 对应于 `not_equivalent（非同义）` ， `1` 对应于 `equivalent（同义）` 。

✏️ 试试看！ 查看训练集的第 15 行元素和验证集的 87 行元素。他们的标签是什么？

##
预处理数据集

为了预处理数据集，我们需要将文本转换为模型能够理解的数字。在 第二章 我们已经学习过。这是通过一个 Tokenizer 完成的，我们可以向 Tokenizer 输入一个句子或一个句子列表。以下代码表示对每对句子中的所有第一句和所有第二句进行 tokenize：

    from transformers import AutoTokenizer

    checkpoint = "bert-base-uncased"
    tokenizer = AutoTokenizer.from_pretrained(checkpoint)
    tokenized_sentences_1 = tokenizer(raw_datasets["train"]["sentence1"])
    tokenized_sentences_2 = tokenizer(raw_datasets["train"]["sentence2"])

不过在将两句话传递给模型，预测这两句话是否是同义之前，我们需要给这两句话依次进行适当的预处理。Tokenizer 不仅仅可以输入单个句子，还可以输入一组句子，并按照 BERT 模型所需要的输入进行处理：

    inputs = tokenizer("This is the first sentence.", "This is the second one.")
    inputs

    {
        "input_ids": [
            101,
            2023,
            2003,
            1996,
            2034,
            6251,
            1012,
            102,
            2023,
            2003,
            1996,
            2117,
            2028,
            1012,
            102,
        ],
        "token_type_ids": [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1],
        "attention_mask": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    }

我们在 第二章 讨论了 `输入词id(input_ids)` 和 `注意力遮罩(attention_mask)` ，但尚未讨论 `token类型ID(token_type_ids)` 。在本例中， `token类型ID(token_type_ids)` 的作用就是告诉模型输入的哪一部分是第一句，哪一部分是第二句。

✏️ 试试看！ 选取训练集中的第 15 个元素，将两句话分别进行tokenization。结果和上方的例子有什么不同？

如果将 `input_ids` 中的 id 转换回文字：

    tokenizer.convert_ids_to_tokens(inputs["input_ids"])

将得到：

    [
        "[CLS]",
        "this",
        "is",
        "the",
        "first",
        "sentence",
        ".",
        "[SEP]",
        "this",
        "is",
        "the",
        "second",
        "one",
        ".",
        "[SEP]",
    ]

所以我们看到模型需要输入的形式是 `[CLS] sentence1 [SEP] sentence2 [SEP]` 。所以当有两句话的时候， `token类型ID(token_type_ids)` 的值是：

    [
        "[CLS]",
        "this",
        "is",
        "the",
        "first",
        "sentence",
        ".",
        "[SEP]",
        "this",
        "is",
        "the",
        "second",
        "one",
        ".",
        "[SEP]",
    ]
    [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1]

现在输入中 `[CLS] sentence1 [SEP]` 它们的 `token_type_ids` 均为 `0` ，而其他部分例如 `sentence2 [SEP]` ，所有的 `token_type_ids` 均为 `1` 。

请注意，如果选择其他的 checkpoint，不一定具有 `token_type_ids` ，比如，DistilBERT 模型就不会返回。只有当 tokenizer 在预训练期间使用过这一层，也就是模型在构建时需要它们时，才会返回 `token_type_ids` 。

在这里，BERT 使用了带有 `token_type_ids` 的预训练 tokenizer，除了我们在 第一章 中讨论的掩码语言建模，还有一个额外的应用类型称为“下一句预测”。这个任务的目标是对句子对之间的关系进行建模。

在下一句预测任务中，会给模型输入成对的句子（带有随机遮罩的 token），并要求预测第二个句子是否紧跟第一个句子。为了使任务具有挑战性，提高模型的泛化能力，数据集中一有一半句子对中的句子在原始文档中顺序排列，另一半句子对中的两个句子来自两个不同的文档。

一般来说无需要担心在你的输入中是否需要有 `token_type_ids` 。只要你使用相同的 checkpoint 的 Tokenizer 和模型，Tokenizer 就会知道向模型提供什么，一切都会顺利进行。

现在我们已经了解了 Tokenizer 如何处理一对句子，我们可以用它来处理整个数据集：就像在 第二章 中一样，我们可以给 Tokenizer 提供一对句子，第一个参数是它第一个句子的列表，第二个参数是第二个句子的列表。这也与我们在 第二章 中看到的填充和截断选项兼容。因此预处理训练数据集的一种方法是：

    tokenized_dataset = tokenizer(
        raw_datasets["train"]["sentence1"],
        raw_datasets["train"]["sentence2"],
        padding=True,
        truncation=True,
    )

这种方法虽然有效，但有一个缺点是它返回的是一个字典（字典的键是 `输入词id(input_ids)` ， `注意力遮罩(attention_mask)` 和 `token类型ID(token_type_ids)` ，字典的值是键所对应值的列表）。这意味着在转换过程中要有足够的内存来存储整个数据集才不会出错。不过来自🤗 Datasets 库中的数据集是以 Apache Arrow 格式存储在磁盘上的，因此你只需将接下来要用的数据加载在内存中，而不是加载整个数据集，这对内存容量的需求比较友好。

我们将使用 Dataset.map() 方法将数据保存为 dataset 格式，如果我们需要做更多的预处理而不仅仅是 tokenization 它还支持了一些额外的自定义的方法。 `map()` 方法的工作原理是使用一个函数处理数据集的每个元素。让我们定义一个对输入进行 tokenize 的函数：

    def tokenize_function(example):
        return tokenizer(example["sentence1"], example["sentence2"], truncation=True)

该函数接收一个字典（与 dataset 的项类似）并返回一个包含 `输入词id(input_ids)` ， `注意力遮罩(attention_mask)` 和 `token_type_ids` 键的新字典。请注意，如果 `example` 字典所对应的值包含多个句子（每个键作为一个句子列表），那么它依然可以运行，就像前面的例子一样， `tokenizer` 可以处理成对的句子列表，这样的话我们可以在调用 `map()` 时使用该选项 `batched=True` ，这将显著加快处理的速度。 `tokenizer` 来自 🤗 Tokenizers 库，由 Rust 编写而成。当一次给它很多输入时，这个 `tokenizer` 可以处理地非常快。

请注意，我们暂时在 `tokenize_function` 中省略了 padding 参数。这是因为将所有的样本填充到最大长度有些浪费。一个更好的做法是：在构建 batch 的时候。这样我们只需要填充到每个 batch 中的最大长度，而不是整个数据集的最大长度。当输入长度不稳定时，这可以节省大量时间和处理能力！

下面是我们如何使用一次性 `tokenize_function` 处理整个数据集。我们在调用 `map` 时使用了 `batch =True` ，这样函数就可以同时处理数据集的多个元素，而不是分别处理每个元素，这样可以更快进行预处理。

以下是如何使用 tokenization 函数处理我们的整个数据集的方法。我们在调用 map 时使用了 `batched=True` ，因此该函数会一次性处理数据集的多个元素，而不是单独处理每个元素。这样可以实现更快的预处理。

    tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
    tokenized_datasets

🤗Datasets 库进行这种处理的方式是向数据集添加新的字段，每个字段对应预处理函数返回的字典中的每个键：

    DatasetDict({
        train: Dataset({
            features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'],
            num_rows: 3668
        })
        validation: Dataset({
            features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'],
            num_rows: 408
        })
        test: Dataset({
            features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'],
            num_rows: 1725
        })
    })

在使用预处理函数 `map()` 时，甚至可以通过传递 `num_proc` 参数并行处理。我们在这里没有这样做，因为在这个例子中🤗 Tokenizers 库已经使用多线程来更快地对样本 tokenize，但是如果没有使用该库支持的快速 tokenizer，使用 `num_proc` 可能会加快预处理。

我们的 `tokenize_function` 返回包含 `输入词id(input_ids)` ， `注意力遮罩(attention_mask)` 和 `token_type_ids` 键的字典，这三个字段被添加到数据集的三个集合里（训练集、验证集和测试集）。请注意，如果预处理函数 `map()` 为现有键返回一个新值，我们可以通过使用 `map()` 函数返回的新值修改现有的字段。

我们最后需要做的是将所有示例填充到该 batch 中最长元素的长度，这种技术被称为动态填充。

##
动态填充

负责在批处理中将数据整理为一个 batch 的函数称为 `collate 函数` 。这是一个可以在构建 `DataLoader` 时传递的一个参数，默认是一个将你的数据集转换为 PyTorch 张量并将它们拼接起来的函数（如果你的元素是列表、元组或字典，则会使用递归进行拼接）。这在本例子中下是不可行的，因为我们的输入的大小可能是不相同的。我们特意推迟了填充的时间，只在每个 batch 上进行必要的填充，以避免出现有大量填充的过长输入。这将大大加快训练速度，但请注意，如果你在 TPU 上训练，需要注意一个问题——TPU 喜欢固定的形状，即使这需要额外填充很多无用的 token。

为了解决句子长度不统一的问题，我们必须定义一个 collate 函数，该函数会将每个 batch 句子填充到正确的长度。幸运的是，🤗transformer 库通过 `DataCollatorWithPadding` 为我们提供了这样一个函数。当你实例化它时，它需要一个 tokenizer （用来知道使用哪种填充 token 以及模型期望在输入的左边填充还是右边填充），然后它会自动完成所有需要的操作：

    from transformers import DataCollatorWithPadding

    data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

为了测试这个新东西，让我们从我们的训练集中抽取几个样本，在这里，我们删除列 `idx` ， `sentence1` 和 `sentence2` ，因为不需要它们，而且删除包含字符串的列（我们不能用字符串创建张量），然后查看一个 batch 中每个条目的长度：

    samples = tokenized_datasets["train"][:8]
    samples = {k: v for k, v in samples.items() if k not in ["idx", "sentence1", "sentence2"]}
    [len(x) for x in samples["input_ids"]]

    [50, 59, 47, 67, 59, 50, 62, 32]

不出所料，我们得到了不同长度的样本，从 32 到 67。动态填充意味着这个 batch 都应该填充到长度为 67，这是这个 batch 中的最大长度。如果没有动态填充，所有的样本都必须填充到整个数据集中的最大长度，或者模型可以接受的最大长度。让我们再次检查 `data_collator` 是否正确地动态填充了这批样本：

    ```py
    batch = data_collator(samples)
    {k: v.shape for k, v in batch.items()}

    {
        "attention_mask": torch.Size([8, 67]),
        "input_ids": torch.Size([8, 67]),
        "token_type_ids": torch.Size([8, 67]),
        "labels": torch.Size([8]),
    }

看起来不错！现在，我们已经从原始文本转化为了模型可以处理的数据，我们准备好对其进行微调。

✏️ 试试看！ 在 GLUE SST-2 数据集上复刻上述预处理。它有点不同，因为它是由单句而不是成对的句子组成的，但是我们所做的其他事情看起来应该是一样的。另一个进阶的挑战是尝试编写一个可用于任何 GLUE 任务的预处理函数。

< > Update on GitHub