登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

Datasets流式读取与内存数据集的shuffle有什么区别

来源:17golang原创

时间:2026-10-13 20:39:08 130浏览 收藏

如果数据集能完整落到本地,Dataset.shuffle() 更接近全量随机排列;如果数据太大、只想边读边训练,streaming=True 得到的 IterableDataset 只能在分片和有限缓冲区内做近似打乱。两者都叫 shuffle,但访问模型、内存代价和随机范围并不相同。

官方地址:https://huggingface.co/docs/datasets/

先按“是否需要随机访问”选对象,再按“能接受多大的缓冲区”设置流式 shuffle。不要把流式数据的局部随机当成整个数据集的严格随机排列。

先看数据对象:能不能随机访问

普通 Dataset 是面向索引的对象,常见操作是 dataset[100] 或按列读取。它可以把数据组织成连续的 Arrow 存储,并在调用 shuffle 后维护一套行索引映射。索引映射带来随机访问能力,但读取不再天然保持连续,数据量很大时可能增加访问成本。

load_dataset(..., streaming=True) 返回的是 IterableDataset。它按迭代器消费数据,数据可以来自远端或本地文件分片,过滤、映射等处理通常在真正迭代时发生。它适合训练循环和快速抽样,却不适合通过下标直接定位第 N 条样本。

Dataset 与 IterableDataset 的索引访问和顺序迭代关系说明图
图1:对象关系说明图,重点看 Dataset 的索引访问与 IterableDataset 的顺序迭代边界。

内存数据集的 shuffle 是全量索引排列

对内存数据集调用 ds.shuffle(seed=42),可以理解为生成一套覆盖所有行的随机索引顺序。随机种子相同,输入数据和库行为保持一致时,得到的排列便于复现;但原始数据规模越大,重排索引和后续非连续读取的代价越明显。

from datasets import load_dataset

# 先加载成普通 Dataset,后续可以按下标访问。
dataset = load_dataset("cornell-movie-review-data/rotten_tomatoes", split="train")

# seed 固定排列,适合需要可复现的训练或离线切分。
shuffled = dataset.shuffle(seed=42)

# 这里是随机访问:可以直接取指定位置的样本。
sample = shuffled[100]
print(sample["label"], sample["text"][:40])

这种模式适合数据规模可控、需要多次随机访问、需要精确切分或希望对完整样本集合做离线处理的任务。若 shuffle 后读写变慢,通常不是“随机失效”,而是索引映射让底层读取失去了连续性。

流式数据集的 shuffle 是 shard 加 buffer 的近似随机

流式对象的 shuffle 不会先把全部样本收进内存。它会先调整数据分片的顺序,再填充一个由 buffer_size 控制的缓冲区;每次从缓冲区抽取样本,并用后续样本补位。因此,缓冲区越大,局部随机范围通常越宽,但启动等待和内存占用也会上升。

例如数据总量是一百万条而 buffer_size=10000,开始阶段只会在缓冲区覆盖的范围内抽样,之后随着新样本补入继续前进。它的目标是以较小内存维持吞吐,而不是构造一份全量随机索引表。

Dataset.shuffle 全量排列与 IterableDataset.shuffle 缓冲区抽样关系说明图
图2:随机范围说明图,区分全量索引排列、有限 buffer 抽样与每轮 epoch 的种子变化。
from datasets import load_dataset

# streaming 模式只在迭代时读取,避免先下载完整数据集。
streamed = load_dataset("HuggingFaceFW/fineweb", split="train", streaming=True)

# buffer_size 决定局部随机范围;seed 让分片和抽样顺序可复现。
shuffled = streamed.shuffle(seed=42, buffer_size=10_000)

# take 只取前几条用于观察,真实训练可以直接交给训练循环。
for row in shuffled.take(3):
    print(row["text"][:80])

训练时怎样选 buffer_size、seed 和 epoch

如果训练只需要持续消费样本,流式模式通常更合适。先给出一个能容纳的 buffer_size,再观察吞吐、启动延迟和样本混合程度:显存不是唯一约束,shuffle buffer 本身也会占用主机内存。

多轮训练时,可以在每轮开始调用 set_epoch()。官方文档把有效种子理解为初始 seed 加当前 epoch,这样每轮会重新打乱分片和缓冲区抽样顺序。

from datasets import load_dataset

# 分片数量有助于多 worker 或分布式读取时分配工作。
dataset = load_dataset("ethz/food101", split="train")
streamed = dataset.to_iterable_dataset(num_shards=64)
streamed = streamed.shuffle(seed=42, buffer_size=10_000)

for epoch in range(3):
    # 改变 epoch 后,流式 shuffle 使用新的有效种子。
    streamed.set_epoch(epoch)
    for row in streamed:
        # 这里放 tokenizer、batch 组装或模型训练逻辑。
        consume(row)
        break

还要注意调用顺序:如果已经用 take() 或 skip() 固定了分片顺序,后续再调用 shuffle 可能无法重新改变分片顺序。要切分流式数据,通常先 shuffle,再 take 或 skip。

一张表记住两种模式

比较项DatasetIterableDataset
读取方式索引访问,可随机定位顺序迭代,惰性读取
shuffle 核心全量行的索引排列分片顺序 + 有限 buffer 抽样
随机程度可接近完整数据集随机近似随机,受 buffer 大小影响
适合场景离线处理、精确切分、随机访问超大数据、远端数据、训练流水线

所以,“内存数据集还是流式数据集”不是 API 写法偏好,而是访问契约:需要第 N 条样本、完整重排和精确切分时选 Dataset;只需按批次向前消费、数据规模超出本地资源时选 IterableDataset,并把 buffer_size 当作随机性与资源之间的调节旋钮。

常见问题

流式 shuffle 能保证每个 epoch 都完全随机吗?

不能把它当成严格全量随机。它通过 shard 顺序和有限 buffer 提供近似随机;增大 buffer、设置固定 seed,并在每轮调用 set_epoch(),可以让训练顺序更充分且可复现。

为什么 shuffle 后 take 要放在后面?

take() 和 skip() 会固定部分分片顺序。若先切分再 shuffle,后续 shuffle 的可调整范围会受限,因此想抽取随机样本时应先 shuffle,再 take。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>