混合比例与洗牌策略 | AI生成和翻译

首页 2026.06

Question: 训练 nanoGPT(或类似模型)时,如果使用 50% FineWeb / 50% GitHub 代码的混合数据,每个批次是否应该一半是代码一半是自然语言,并且训练数据是否应该被打乱?

Answer:

简短回答:你希望混合比例在期望上成立,而不是每个批次精确相等。并且是的,要打乱——但打乱的方式比“是否打乱”本身更重要。

不需要每个批次恰好一半一半

强制每个批次精确为 50% 代码 / 50% 文本属于分层抽样。这并非错误,但通常没必要且增加复杂性。对于一个 1024 序列的批次,从 50/50 的池中随机抽样自然会产生每批次约 512±16 个代码序列(二项式标准差 ≈ √(1024·0.5·0.5) ≈ 16)。梯度在几步之内就能看到正确的比例。这种方差是 SGD 可以很好吸收的噪声。

当每批次平衡确实重要时:

对于标准预训练,期望混合是常规做法。这正是大型实验的做法——为每个数据源设置采样权重并随机抽取。

真正的问题:token 比例 vs 文档比例

“50/50”是模糊的。你是指 50% 的文档,还是 50% 的 token?这两者差异很大,因为代码和散文的 token 化方式不同(代码有更多空白、更短的行、更多标点)。你几乎总是想要 50% 的 token,因为模型从 token 学习,而不是文档。先计算每个语料库的 token 数量,然后设置采样权重以达到你想要的 token 比例。

打乱:nanoGPT 的现实

nanoGPT 在训练时并不打乱文档列表。它预先将所有内容 token 化成一个巨大的扁平 train.bin(一个 uint16 的 memmap),然后随机采样偏移量:

# nanoGPT 数据加载的实际机制
data = np.memmap('train.bin', dtype=np.uint16, mode='r')

def get_batch():
    ix = torch.randint(len(data) - block_size, (batch_size,))
    x = torch.stack([torch.from_numpy(data[i:i+block_size].astype(np.int64)) for i in ix])
    y = torch.stack([torch.from_numpy(data[i+1:i+1+block_size].astype(np.int64)) for i in ix])
    return x, y

因此“打乱”实际上是随机偏移采样,而不是列表排列。关键后果:.bin 的排列方式决定了你的混合方式。 如果你先写入所有 FineWeb,再写入所有 GitHub,那么随机偏移仍然会在一个 epoch 内均匀覆盖两个区域——但任何单个批次的组成取决于偏移落在哪里,并且跨越边界序列是垃圾数据。

两种清晰的做法:

方法 A — 写入时交错(单个 .bin): 在 token 化之前打乱文档,使代码和文本在扁平缓冲区中交错排列。然后随机偏移采样自然会产生混合批次。

import numpy as np, random

# docs: token 数组列表,每个标记来源
docs = fineweb_docs + github_docs
random.shuffle(docs)  # 在缓冲区中交错排列来源

# 在文档之间用文档分隔符(EOT token)连接
sep = np.array([eot_token], dtype=np.uint16)
buf = np.concatenate([np.concatenate([d, sep]) for d in docs])
buf.tofile('train.bin')

方法 B — 独立的 .bin 文件 + 加权采样(更清晰,易于控制比例):fineweb.bingithub.bin 分开,然后按权重选择每个序列的来源。这样将混合比例与磁盘布局解耦,并且可以在不重新 token 化的情况下更改混合比例:

fw = np.memmap('fineweb.bin', dtype=np.uint16, mode='r')
gh = np.memmap('github.bin',  dtype=np.uint16, mode='r')

# token 加权:w = 你希望从每个来源获得的 token 比例
weights = {'fw': 0.5, 'gh': 0.5}

def get_batch(batch_size, block_size):
    xs, ys = [], []
    for _ in range(batch_size):
        src = fw if random.random() < weights['fw'] else gh
        i = random.randint(0, len(src) - block_size - 1)
        xs.append(torch.from_numpy(src[i:i+block_size].astype(np.int64)))
        ys.append(torch.from_numpy(src[i+1:i+1+block_size].astype(np.int64)))
    return torch.stack(xs), torch.stack(ys)

我会使用方法 B。这是生产环境数据加载器的工作方式(想想 Megatron 的 blended dataset / DataMix),能精确控制比例,并且可以廉价地扫描不同的混合比例。

两个人们常犯的错误

  1. 文档之间的 EOT 分隔符。 如果没有在连接的文档之间插入文本结束符,序列会跨越文档边界,模型会学到从 FineWeb 散文到代码文件的虚假延续。始终在文档之间插入 EOT token(对于 GPT-2 BPE 为 <|endoftext|> = 50256)。

  2. 代码的 tokenizer。 GPT-2 的 BPE 对代码处理不佳(没有良好的空白/缩进处理——空格序列会导致 token 数量激增)。如果代码占数据的 50%,这很重要。考虑使用具有空白合并功能的 tokenizer(通过 tiktoken 使用 GPT-4 的 cl100k/o200k,或在混合语料上训练自己的 BPE)。这也修正了你的 token 比例计算,因为 FineWeb 散文和代码在每 token 字节数上差异很大。

总结

参考文献:


AI生成和翻译,但整理分享仍需精力。如果觉得有帮助,欢迎 捐助支持。 微信:@lzwjavaWeChat QR · X: @lzwjava · 打个招呼 👋

返回 捐助