NLP 课程文档

大数据?🤗 数据集来帮忙!

Hugging Face's logo
加入 Hugging Face 社区

并获得增强型文档体验

开始使用

大数据?🤗 数据集来帮忙!

Ask a Question Open In Colab Open In Studio Lab

如今,使用多吉字节数据集并不少见,尤其是当你计划从头开始预训练像 BERT 或 GPT-2 这样的 Transformer 时。在这些情况下,即使是加载数据也可能是一个挑战。例如,用于预训练 GPT-2 的 WebText 语料库包含超过 800 万个文档和 40 GB 的文本 - 将其加载到你的笔记本电脑的 RAM 中可能会让它崩溃!

幸运的是,🤗 Datasets 旨在克服这些限制。它通过将数据集视为内存映射文件,让你免受内存管理问题的困扰,并且通过流式传输语料库中的条目来摆脱硬盘空间限制。

在本节中,我们将使用一个名为 the Pile 的巨大 825 GB 语料库来探索 🤗 Datasets 的这些特性。让我们开始吧!

什么是 the Pile?

the Pile 是一个由 EleutherAI 创建的英语文本语料库,用于训练大规模语言模型。它包括各种各样的数据集,涵盖科学文章、GitHub 代码库和过滤后的网络文本。训练语料库以 14 GB 块 的形式提供,你也可以下载一些 单独的组件。让我们从看看 PubMed Abstracts 数据集开始,这是一个包含来自 PubMed 上 1500 万份生物医学出版物的摘要的语料库。该数据集采用 JSON Lines 格式,并使用 zstandard 库进行压缩,所以首先我们需要安装它

!pip install zstandard

接下来,我们可以使用我们在 第 2 节 中学习到的远程文件方法加载数据集

from datasets import load_dataset

# This takes a few minutes to run, so go grab a tea or coffee while you wait :)
data_files = "https://the-eye.eu/public/AI/pile_preliminary_components/PUBMED_title_abstracts_2019_baseline.jsonl.zst"
pubmed_dataset = load_dataset("json", data_files=data_files, split="train")
pubmed_dataset
Dataset({
    features: ['meta', 'text'],
    num_rows: 15518009
})

我们可以看到,我们的数据集中有 15,518,009 行和 2 列 - 非常多!

✎ 默认情况下,🤗 Datasets 会解压缩加载数据集所需的 文件。如果你想节省硬盘空间,可以将 DownloadConfig(delete_extracted=True) 传递给 load_dataset()download_config 参数。有关更多详细信息,请参阅 文档

让我们检查一下第一个示例的内容

pubmed_dataset[0]
{'meta': {'pmid': 11409574, 'language': 'eng'},
 'text': 'Epidemiology of hypoxaemia in children with acute lower respiratory infection.\nTo determine the prevalence of hypoxaemia in children aged under 5 years suffering acute lower respiratory infections (ALRI), the risk factors for hypoxaemia in children under 5 years of age with ALRI, and the association of hypoxaemia with an increased risk of dying in children of the same age ...'}

好的,这看起来像一篇医学文章的摘要。现在让我们看看我们用来加载数据集的 RAM 量!

内存映射的魔力

在 Python 中测量内存使用的一种简单方法是使用 psutil 库,可以使用 pip 如下安装

!pip install psutil

它提供了一个 Process 类,允许我们检查当前进程的内存使用情况,如下所示

import psutil

# Process.memory_info is expressed in bytes, so convert to megabytes
print(f"RAM used: {psutil.Process().memory_info().rss / (1024 * 1024):.2f} MB")
RAM used: 5678.33 MB

这里 rss 属性指的是驻留集大小,即进程在 RAM 中占据的内存部分。此测量还包括 Python 解释器和我们加载的库使用的内存,因此加载数据集实际使用的内存量会略小一些。为了比较,让我们看看数据集在磁盘上有多大,使用 dataset_size 属性。由于结果像以前一样以字节表示,我们需要手动将其转换为千兆字节

print(f"Number of files in dataset : {pubmed_dataset.dataset_size}")
size_gb = pubmed_dataset.dataset_size / (1024**3)
print(f"Dataset size (cache file) : {size_gb:.2f} GB")
Number of files in dataset : 20979437051
Dataset size (cache file) : 19.54 GB

不错 - 尽管它将近 20 GB 大,但我们仍然可以用更少的 RAM 加载和访问数据集!

✏️ 试试看!从 the Pile 中选择一个比你的笔记本电脑或台式机的 RAM 更大的 子集,用 🤗 Datasets 加载它,并测量所使用的 RAM 量。请注意,要获得准确的测量值,你可能需要在一个新的进程中执行此操作。你可以在 the Pile 论文 的表 1 中找到每个子集的解压缩大小。

如果你熟悉 Pandas,这个结果可能会让你感到意外,因为 Wes Kinney 的著名 经验法则 是,你通常需要 5 到 10 倍于数据集大小的 RAM。那么,🤗 Datasets 如何解决这个内存管理问题呢? 🤗 Datasets 将每个数据集视为一个 内存映射文件,它在 RAM 和文件系统存储之间提供映射,允许库访问和操作数据集中的元素,而无需将整个数据集完全加载到内存中。

内存映射文件也可以在多个进程之间共享,这使得像 Dataset.map() 这样的方法能够并行化,而无需移动或复制数据集。在幕后,所有这些功能都是由 Apache Arrow 内存格式和 pyarrow 库实现的,它们使数据加载和处理速度飞快。(有关 Apache Arrow 的更多详细信息以及与 Pandas 的比较,请查看 Dejan Simic 的博文。)为了实际体验,让我们运行一个简单的速度测试,迭代 PubMed Abstracts 数据集中的所有元素

import timeit

code_snippet = """batch_size = 1000

for idx in range(0, len(pubmed_dataset), batch_size):
    _ = pubmed_dataset[idx:idx + batch_size]
"""

time = timeit.timeit(stmt=code_snippet, number=1, globals=globals())
print(
    f"Iterated over {len(pubmed_dataset)} examples (about {size_gb:.1f} GB) in "
    f"{time:.1f}s, i.e. {size_gb/time:.3f} GB/s"
)
'Iterated over 15518009 examples (about 19.5 GB) in 64.2s, i.e. 0.304 GB/s'

这里我们使用了 Python 的 timeit 模块来测量 code_snippet 的执行时间。你通常能够以每秒几十分之几 GB 到几 GB 的速度迭代数据集。这对于绝大多数应用程序来说效果很好,但有时你必须处理一个即使存储在笔记本电脑的硬盘驱动器上也过于庞大的数据集。例如,如果我们尝试下载整个 the Pile,我们将需要 825 GB 的可用磁盘空间!为了处理这些情况,🤗 Datasets 提供了一个流式传输功能,它允许我们按需下载和访问元素,而无需下载整个数据集。让我们看看它是如何工作的。

💡 在 Jupyter 笔记本中,你也可以使用 %%timeit 魔术函数 来计时单元格。

流式数据集

要启用数据集流式处理,只需将 `streaming=True` 参数传递给 `load_dataset()` 函数即可。例如,让我们再次加载 PubMed Abstracts 数据集,但在流式模式下

pubmed_dataset_streamed = load_dataset(
    "json", data_files=data_files, split="train", streaming=True
)

返回的对象不是我们在这章其他地方遇到的熟悉的 `Dataset`,而是 `IterableDataset`。顾名思义,要访问 `IterableDataset` 的元素,我们需要对其进行迭代。我们可以如下访问流式数据集的第一个元素

next(iter(pubmed_dataset_streamed))
{'meta': {'pmid': 11409574, 'language': 'eng'},
 'text': 'Epidemiology of hypoxaemia in children with acute lower respiratory infection.\nTo determine the prevalence of hypoxaemia in children aged under 5 years suffering acute lower respiratory infections (ALRI), the risk factors for hypoxaemia in children under 5 years of age with ALRI, and the association of hypoxaemia with an increased risk of dying in children of the same age ...'}

流式数据集中的元素可以使用 `IterableDataset.map()` 在运行时进行处理,这在训练过程中非常有用,尤其当你需要对输入进行分词时。这个过程与我们在 第 3 章 中用于对数据集进行分词的过程完全相同,唯一的区别是输出是一次返回一个

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
tokenized_dataset = pubmed_dataset_streamed.map(lambda x: tokenizer(x["text"]))
next(iter(tokenized_dataset))
{'input_ids': [101, 4958, 5178, 4328, 6779, ...], 'attention_mask': [1, 1, 1, 1, 1, ...]}

💡 为了加快流式分词速度,你可以传递 `batched=True`,正如我们在上一节中看到的那样。它将分批处理示例;默认批次大小为 1,000,可以通过 `batch_size` 参数指定。

你还可以使用 `IterableDataset.shuffle()` 对流式数据集进行洗牌,但与 `Dataset.shuffle()` 不同的是,它只对预定义的 `buffer_size` 中的元素进行洗牌

shuffled_dataset = pubmed_dataset_streamed.shuffle(buffer_size=10_000, seed=42)
next(iter(shuffled_dataset))
{'meta': {'pmid': 11410799, 'language': 'eng'},
 'text': 'Randomized study of dose or schedule modification of granulocyte colony-stimulating factor in platinum-based chemotherapy for elderly patients with lung cancer ...'}

在这个例子中,我们从缓冲区中前 10,000 个示例中随机选取了一个示例。一旦访问了某个示例,它的缓冲区位置就会被语料库中的下一个示例填补(例如,上面的情况是第 10,001 个示例)。你也可以使用 `IterableDataset.take()` 和 `IterableDataset.skip()` 函数从流式数据集选择元素,它们的作用类似于 `Dataset.select()`。例如,要选择 PubMed Abstracts 数据集中的前 5 个示例,我们可以执行以下操作

dataset_head = pubmed_dataset_streamed.take(5)
list(dataset_head)
[{'meta': {'pmid': 11409574, 'language': 'eng'},
  'text': 'Epidemiology of hypoxaemia in children with acute lower respiratory infection ...'},
 {'meta': {'pmid': 11409575, 'language': 'eng'},
  'text': 'Clinical signs of hypoxaemia in children with acute lower respiratory infection: indicators of oxygen therapy ...'},
 {'meta': {'pmid': 11409576, 'language': 'eng'},
  'text': "Hypoxaemia in children with severe pneumonia in Papua New Guinea ..."},
 {'meta': {'pmid': 11409577, 'language': 'eng'},
  'text': 'Oxygen concentrators and cylinders ...'},
 {'meta': {'pmid': 11409578, 'language': 'eng'},
  'text': 'Oxygen supply in rural africa: a personal experience ...'}]

同样,你也可以使用 `IterableDataset.skip()` 函数从洗牌后的数据集中创建训练集和验证集,如下所示

# Skip the first 1,000 examples and include the rest in the training set
train_dataset = shuffled_dataset.skip(1000)
# Take the first 1,000 examples for the validation set
validation_dataset = shuffled_dataset.take(1000)

让我们用一个常见的应用来总结一下数据集流式处理:将多个数据集组合在一起以创建一个单一语料库。🤗 Datasets 提供了一个 `interleave_datasets()` 函数,它将 `IterableDataset` 对象列表转换为一个单一的 `IterableDataset`,其中新数据集的元素通过在源示例之间交替获取。当你想将大型数据集组合在一起时,这个函数特别有用,所以作为一个例子,让我们流式处理 Pile 的 FreeLaw 子集,这是一个来自美国法院的 51 GB 法律意见数据集

law_dataset_streamed = load_dataset(
    "json",
    data_files="https://the-eye.eu/public/AI/pile_preliminary_components/FreeLaw_Opinions.jsonl.zst",
    split="train",
    streaming=True,
)
next(iter(law_dataset_streamed))
{'meta': {'case_ID': '110921.json',
  'case_jurisdiction': 'scotus.tar.gz',
  'date_created': '2010-04-28T17:12:49Z'},
 'text': '\n461 U.S. 238 (1983)\nOLIM ET AL.\nv.\nWAKINEKONA\nNo. 81-1581.\nSupreme Court of United States.\nArgued January 19, 1983.\nDecided April 26, 1983.\nCERTIORARI TO THE UNITED STATES COURT OF APPEALS FOR THE NINTH CIRCUIT\n*239 Michael A. Lilly, First Deputy Attorney General of Hawaii, argued the cause for petitioners. With him on the brief was James H. Dannenberg, Deputy Attorney General...'}

这个数据集足够大,可以使大多数笔记本电脑的内存超负荷,但我们已经能够在不费吹灰之力的情况下加载和访问它!现在让我们使用 `interleave_datasets()` 函数将 FreeLaw 和 PubMed Abstracts 数据集中的示例组合起来

from itertools import islice
from datasets import interleave_datasets

combined_dataset = interleave_datasets([pubmed_dataset_streamed, law_dataset_streamed])
list(islice(combined_dataset, 2))
[{'meta': {'pmid': 11409574, 'language': 'eng'},
  'text': 'Epidemiology of hypoxaemia in children with acute lower respiratory infection ...'},
 {'meta': {'case_ID': '110921.json',
   'case_jurisdiction': 'scotus.tar.gz',
   'date_created': '2010-04-28T17:12:49Z'},
  'text': '\n461 U.S. 238 (1983)\nOLIM ET AL.\nv.\nWAKINEKONA\nNo. 81-1581.\nSupreme Court of United States.\nArgued January 19, 1983.\nDecided April 26, 1983.\nCERTIORARI TO THE UNITED STATES COURT OF APPEALS FOR THE NINTH CIRCUIT\n*239 Michael A. Lilly, First Deputy Attorney General of Hawaii, argued the cause for petitioners. With him on the brief was James H. Dannenberg, Deputy Attorney General...'}]

在这里我们使用了来自 Python `itertools` 模块的 `islice()` 函数从组合数据集中选取了前两个示例,我们可以看到它们与两个源数据集中的第一个示例相匹配。

最后,如果你想流式处理整个 825 GB 的 Pile,你可以如下获取所有准备好的文件

base_url = "https://the-eye.eu/public/AI/pile/"
data_files = {
    "train": [base_url + "train/" + f"{idx:02d}.jsonl.zst" for idx in range(30)],
    "validation": base_url + "val.jsonl.zst",
    "test": base_url + "test.jsonl.zst",
}
pile_dataset = load_dataset("json", data_files=data_files, streaming=True)
next(iter(pile_dataset["train"]))
{'meta': {'pile_set_name': 'Pile-CC'},
 'text': 'It is done, and submitted. You can play “Survival of the Tastiest” on Android, and on the web...'}

✏️ 试试看! 使用像 mc4 或者 oscar 这样的大型 Common Crawl 语料库,创建一个流式多语言数据集,它反映了你所选国家或地区的语言使用比例。例如,瑞士的四种国家语言是德语、法语、意大利语和罗曼语,所以你可以尝试根据它们的语言使用比例对 Oscar 子集进行采样,以创建一个瑞士语料库。

你现在拥有了加载和处理各种形状和大小的数据集所需的所有工具——但除非你非常幸运,否则在你的 NLP 之旅中,你总会遇到需要创建数据集来解决手边问题的时刻。这是下一节的主题!