Datasets 文档

Dataset 和 IterableDataset 的区别

Hugging Face's logo
加入 Hugging Face 社区

并获得增强的文档体验

开始使用

Dataset 和 IterableDataset 的区别

有两种数据集对象:DatasetIterableDataset。您选择使用或创建哪种类型的数据集取决于数据集的大小。通常,对于大型数据集(想想数百 GB!),IterableDataset 是理想的选择,因为它具有惰性行为和速度优势,而 Dataset 则适用于其他所有情况。本页将比较 DatasetIterableDataset 之间的区别,以帮助您选择适合您的数据集对象。

下载与流式处理

当您有一个普通的 Dataset 时,您可以使用 my_dataset[0] 来访问它。这提供了对行进行随机访问的能力。这类数据集也称为“映射式”数据集。例如,您可以像这样下载 ImageNet-1k 并访问任何一行

from datasets import load_dataset

imagenet = load_dataset("timm/imagenet-1k-wds", split="train")  # downloads the full dataset
print(imagenet[0])

但是有一个缺点是,您必须将整个数据集存储在磁盘或内存中,这会阻止您访问大于磁盘容量的数据集。由于它可能对大型数据集造成不便,因此存在另一种类型的数据集,即 IterableDataset。当您拥有一个 IterableDataset 时,您可以通过 for 循环来访问它,在迭代数据集的过程中逐步加载数据。这样,只有一小部分示例会被加载到内存中,而您不会在磁盘上写入任何内容。

例如,您可以流式处理 ImageNet-1k 数据集,而无需将其下载到磁盘

from datasets import load_dataset

imagenet = load_dataset("timm/imagenet-1k-wds", split="train", streaming=True)  # will start loading the data when iterated over
for example in imagenet:
    print(example)
    break

流式处理可以读取在线数据而无需将任何文件写入磁盘。例如,您可以流式处理由多个分片组成的数据集,每个分片都达数百 GB,例如 C4LAION-2B。在 数据集流式处理指南 中了解更多关于如何流式处理数据集的信息。

但这并非唯一的区别,因为 IterableDataset 的“惰性”行为在数据集的创建和处理方面也同样存在。

创建映射式数据集和可迭代数据集

您可以使用列表或字典创建 Dataset,数据将完全转换为 Arrow,因此您可以轻松访问任何一行。

my_dataset = Dataset.from_dict({"col_1": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
print(my_dataset[0])

另一方面,要创建 IterableDataset,您必须提供一种“惰性”加载数据的方式。在 Python 中,我们通常使用生成器函数。这些函数一次 yield 一个示例,这意味着您无法像访问常规 Dataset 那样通过切片来访问一行。

def my_generator(n):
    for i in range(n):
        yield {"col_1": i}

my_iterable_dataset = IterableDataset.from_generator(my_generator, gen_kwargs={"n": 10})
for example in my_iterable_dataset:
    print(example)
    break

全部和逐步加载本地文件

可以使用 load_dataset() 将本地或远程数据文件转换为 Arrow Dataset

data_files = {"train": ["path/to/data.csv"]}
my_dataset = load_dataset("csv", data_files=data_files, split="train")
print(my_dataset[0])

然而,这需要一个从 CSV 到 Arrow 格式的转换步骤,如果数据集很大,这会消耗时间和磁盘空间。

为了节省磁盘空间并跳过转换步骤,您可以定义一个 IterableDataset,直接从本地文件进行流式处理。这样,在迭代数据集时,数据会逐步从本地文件读取。

data_files = {"train": ["path/to/data.csv"]}
my_iterable_dataset = load_dataset("csv", data_files=data_files, split="train", streaming=True)
for example in my_iterable_dataset:  # this reads the CSV file progressively as you iterate over the dataset
    print(example)
    break

支持许多文件格式,如 CSV、JSONL 和 Parquet,以及图像和音频文件。您可以在加载 表格文本视觉音频 数据集的相应指南中找到更多信息。

即时数据处理与惰性数据处理

当您使用 Dataset.map() 处理 Dataset 对象时,整个数据集会立即被处理并返回。这与 pandas 的工作方式类似。

my_dataset = my_dataset.map(process_fn)  # process_fn is applied on all the examples of the dataset
print(my_dataset[0])

另一方面,由于 IterableDataset 的“惰性”特性,调用 IterableDataset.map() 不会对整个数据集应用您的 map 函数。相反,您的 map 函数是即时应用的。

因此,您可以链式处理多个处理步骤,当您开始迭代数据集时,所有这些步骤都会一次性运行。

my_iterable_dataset = my_iterable_dataset.map(process_fn_1)
my_iterable_dataset = my_iterable_dataset.filter(filter_fn)
my_iterable_dataset = my_iterable_dataset.map(process_fn_2)

# process_fn_1, filter_fn and process_fn_2 are applied on-the-fly when iterating over the dataset
for example in my_iterable_dataset:  
    print(example)
    break

精确和快速近似洗牌

当您使用 Dataset.shuffle() 来洗牌 Dataset 时,您会应用一次精确的洗牌。它通过获取一个索引列表 [0, 1, 2, ... len(my_dataset) - 1] 并打乱这个列表来工作。然后,访问 my_dataset[0] 会返回洗牌后的索引映射的第一个元素定义的行和索引。

my_dataset = my_dataset.shuffle(seed=42)
print(my_dataset[0])

由于在 IterableDataset 的情况下我们无法随机访问行,因此我们无法使用打乱的索引列表并访问任意位置的行。这阻止了精确洗牌的使用。相反,在 IterableDataset.shuffle() 中使用了一种快速的近似洗牌。它使用一个洗牌缓冲区来迭代地从数据集中采样随机示例。由于数据集仍然是迭代读取的,因此提供了出色的速度性能。

my_iterable_dataset = my_iterable_dataset.shuffle(seed=42, buffer_size=100)
for example in my_iterable_dataset:
    print(example)
    break

但仅使用洗牌缓冲区不足以提供令人满意的模型训练洗牌。因此 IterableDataset.shuffle() 还会洗牌数据集分片,如果您的数据集由多个文件或源组成。

# Stream from the internet
my_iterable_dataset = load_dataset("deepmind/code_contests", split="train", streaming=True)
my_iterable_dataset.num_shards  # 39

# Stream from local files
data_files = {"train": [f"path/to/data_{i}.csv" for i in range(1024)]}
my_iterable_dataset = load_dataset("csv", data_files=data_files, split="train", streaming=True)
my_iterable_dataset.num_shards  # 1024

# From a generator function
def my_generator(n, sources):
    for source in sources:
        for example_id_for_current_source in range(n):
            yield {"example_id": f"{source}_{example_id_for_current_source}"}

gen_kwargs = {"n": 10, "sources": [f"path/to/data_{i}" for i in range(1024)]}
my_iterable_dataset = IterableDataset.from_generator(my_generator, gen_kwargs=gen_kwargs)
my_iterable_dataset.num_shards  # 1024

速度差异

常规 Dataset 对象基于 Arrow,它提供了对行的快速随机访问。由于内存映射和 Arrow 是内存格式,从磁盘读取数据不会进行昂贵地系统调用和反序列化。通过迭代连续的 Arrow 记录批次,在迭代时提供了更快的数据加载。

然而,一旦您的 Dataset 拥有一个索引映射(例如通过 Dataset.shuffle()),速度可能会慢 10 倍。这是因为需要额外一步来获取要读取的行索引,并且最重要的是,您不再读取连续的数据块。为了恢复速度,您需要使用 Dataset.flatten_indices() 将整个数据集重写到磁盘上,这会移除索引映射。但这可能需要很长时间,具体取决于数据集的大小。

my_dataset[0]  # fast
my_dataset = my_dataset.shuffle(seed=42)
my_dataset[0]  # up to 10x slower
my_dataset = my_dataset.flatten_indices()  # rewrite the shuffled dataset on disk as contiguous chunks of data
my_dataset[0]  # fast again

在这种情况下,我们建议切换到 IterableDataset,并利用其快速近似洗牌方法 IterableDataset.shuffle()。它只洗牌分片的顺序并将一个洗牌缓冲区添加到您的数据集中,从而保持数据集的最佳速度。您也可以轻松地重新洗牌数据集。

for example in enumerate(my_iterable_dataset):  # fast
    pass

shuffled_iterable_dataset = my_iterable_dataset.shuffle(seed=42, buffer_size=100)

for example in enumerate(shuffled_iterable_dataset):  # as fast as before
    pass

shuffled_iterable_dataset = my_iterable_dataset.shuffle(seed=1337, buffer_size=100)  # reshuffling using another seed is instantaneous

for example in enumerate(shuffled_iterable_dataset):  # still as fast as before
    pass

如果您在多个 epoch 中使用数据集,洗牌缓冲区中洗牌分片顺序的有效种子是 seed + epoch。这使得在 epoch 之间轻松地重新洗牌数据集。

for epoch in range(n_epochs):
    my_iterable_dataset.set_epoch(epoch)
    for example in my_iterable_dataset:  # fast + reshuffled at each epoch using `effective_seed = seed + epoch`
        pass

要重新启动映射式数据集的迭代,您可以简单地跳过前面的示例。

my_dataset = my_dataset.select(range(start_index, len(dataset)))

但是,如果您使用带 SamplerDataLoader,您应该保存您的采样器的状态(您可能已经编写了一个允许恢复的自定义采样器)。

另一方面,可迭代数据集不提供对特定示例索引的随机访问以从中恢复。但是,您可以使用 IterableDataset.state_dict()IterableDataset.load_state_dict() 从检查点恢复,这与您对模型和优化器所做的操作类似。

>>> iterable_dataset = Dataset.from_dict({"a": range(6)}).to_iterable_dataset(num_shards=3)
>>> # save in the middle of training
>>> state_dict = iterable_dataset.state_dict()
>>> # and resume later
>>> iterable_dataset.load_state_dict(state_dict)

在底层,可迭代数据集会跟踪当前正在读取的分片以及当前分片中的示例索引,并将这些信息存储在 state_dict 中。

要从检查点恢复,数据集会跳过所有先前读取过的分片,以从当前分片开始。然后它会读取该分片并跳过示例,直到到达检查点确切的位置。

因此,重启数据集非常快,因为它不会重新读取已经迭代过的分片。尽管如此,恢复数据集通常不是瞬时的,因为它必须从当前分片的开头重新开始读取,并跳过示例,直到到达检查点位置。

这可以与 torchdata 中的 StatefulDataLoader 一起使用,请参阅 使用 PyTorch DataLoader 进行流式处理

从映射式切换到可迭代式

如果您想利用 IterableDataset 的“惰性”行为或其速度优势,您可以将您的映射式 Dataset 切换到 IterableDataset

my_iterable_dataset = my_dataset.to_iterable_dataset()

如果您想洗牌您的数据集或 与 PyTorch DataLoader 一起使用,我们建议生成一个分片式 IterableDataset

my_iterable_dataset = my_dataset.to_iterable_dataset(num_shards=1024)
my_iterable_dataset.num_shards  # 1024
在 GitHub 上更新

© . This site is unofficial and not affiliated with Hugging Face, Inc.