8. 理解 Dataset

理解 LLM 的数据加载流水线

模型结构决定能力上限,数据流水线决定训练效率和最终效果。

在工业界,大模型训练中花时间最多的往往不是模型,而是数据。

可以把整个训练过程想象成一家大型食品工厂:

原始数据
   ↓
清洗
   ↓
切块(Tokenize)
   ↓
装箱(Padding)
   ↓
打包(Batch)
   ↓
送入模型
   ↓
计算Loss
   ↓
反向传播

一、数据是怎么进入模型的?

假设你有一条训练数据:

"我喜欢学习人工智能"

模型根本看不懂中文。

它只能看数字。

所以第一步:

文本
 ↓
Tokenizer
 ↓
Token ID

例如:

我 喜 欢 学 习 人 工 智 能

↓

[35, 88, 29, 63, 91, 17, 45, 73, 12]

这时模型终于能处理了。


二、Dataset:仓库管理员

Dataset 本质上就是:

告诉 PyTorch 去哪里拿数据。

例如:

class MyDataset(Dataset):

    def __getitem__(self, idx):
        return self.data[idx]

    def __len__(self):
        return len(self.data)

作用:

数据文件
 ↓
Dataset
 ↓
一条一条样本

像仓库管理员:

用户:
给我第100条数据

Dataset:
好的,拿去

例如:

dataset[0]

返回:

{
    "text":"我喜欢AI"
}

三、DataLoader:流水线工人

Dataset 一次只能取一个样本。

训练时需要:

一次喂几十条
甚至几百条

所以需要:

DataLoader

负责:

Dataset
 ↓
组装 Batch
 ↓
送给 GPU

例如:

loader = DataLoader(
    dataset,
    batch_size=32,
    shuffle=True
)

含义:

每次拿32条数据
随机打乱顺序
送入模型

流程:

Dataset

样本1
样本2
样本3
...
样本10000

      ↓

DataLoader

Batch1:
样本1~32

Batch2:
样本33~64

Batch3:
样本65~96

四、为什么需要 Padding?

这是很多新人第一次接触训练时最困惑的地方。

假设:

样本1:
我喜欢AI

长度=4
样本2:
今天天气不错

长度=5
样本3:
深度学习改变世界

长度=8

GPU 最喜欢:

矩形矩阵

例如:

3 × 8

而不是:

4
5
8

这种参差不齐的长度。

所以需要补齐。


Padding 之前

[我 喜 欢 AI]

[今 天 天 气 不 错]

[深 度 学 习 改 变 世 界]

Padding 之后

PAD = 特殊填充符

[我 喜 欢 AI PAD PAD PAD PAD]

[今 天 天 气 不 错 PAD PAD]

[深 度 学 习 改 变 世 界]

这样长度统一:

8
8
8

GPU 就能并行计算。


五、为什么需要 Truncation?

有些文章特别长。

例如:

长度 = 5000 Token

而模型最大长度:

max_seq_len = 2048

怎么办?

只能砍掉。

tokens = tokens[:2048]

变成:

前2048个保留

后2952个丢弃

这就叫:

Truncation(截断)

六、max_seq_len 到底是什么?

很多人误解:

max_seq_len = 训练时随便设置的数字

其实不是。

它决定:

模型一次最多能看到多少Token

例如:

max_seq_len = 4096

表示:

模型视野长度 ≈ 4096 Token

类似人眼视野:

太短:
看不见上下文

太长:
显存爆炸

因为 Attention:

O(n2)

长度翻倍:

4096 → 8192

计算量

≈ 4倍

七、真正重要的:Loss Mask

这是 SFT 和预训练最大的区别之一。

很多面试都会问。


预训练

数据:

我 喜 欢 学 习 AI

训练目标:

预测下一个词

例如:

我 → 喜

我 喜 → 欢

我 喜 欢 → 学

因此:

每个Token都有价值

Loss Mask:

[1 1 1 1 1]

除了 PAD:

[1 1 1 1 1 0 0]

SFT 为什么不同?

假设训练样本:

User:
什么是AI?

Assistant:
AI是人工智能。

拼接后:

<User>
什么是AI?
<Assistant>
AI是人工智能。

模型输入:

全部输入进去

但是监督目标不是全部。


我们只关心什么?

只关心:

Assistant 怎么回答

不关心:

User 怎么提问

所以:

User部分
不计算Loss

Loss Mask

<User>
什么是AI?
<Assistant>
AI是人工智能。

对应:

0 0 0 0 0 0

1 1 1 1 1 1

图示:

Prompt
↓↓↓↓↓↓↓

什么是AI?

000000000

Assistant回答
↓↓↓↓↓↓↓

AI是人工智能

111111111

为什么必须这样做?

假设不 Mask。

模型会同时学习:

如何提问
+
如何回答

梯度目标混在一起。

结果可能变成:

用户:什么是AI?

模型:
什么是AI?

因为模型发现:

训练数据里
用户说的话也算Loss

于是它会模仿用户。

这显然不是我们想要的。


八、Loss 是怎么计算的?

假设:

Loss = [2.1, 1.5, 3.0, 0.8]

对应 Mask:

[0, 0, 1, 1]

那么实际计算:

0×2.1
+
0×1.5
+
1×3.0
+
1×0.8

只保留:

3.0
+
0.8

最后平均:

(3.0 + 0.8) / 2

九、Batch Size 是什么?

假设:

10万条训练数据

不可能:

一次全塞GPU

所以:

分批

例如:

batch_size = 32

表示:

一次训练32条数据

流程:

Batch1
→ 更新参数

Batch2
→ 更新参数

Batch3
→ 更新参数

十、为什么还要梯度累积?

假设你希望:

batch_size = 256

训练更稳定。

但是显存只能放:

32

怎么办?

拆开。


原本:

256条
一起算

变成:

32
32
32
32
32
32
32
32

共:

8次

每次:

loss.backward()

只累计梯度。

不更新参数。

最后一次:

optimizer.step()

更新一次。

效果近似:

真正 batch_size=256

公式:

Effective Batch Size=batch size×gradient accumulation×GPU 数

例如:

batch_size = 32

grad_acc = 8

gpu = 4

则:

有效Batch = 32 × 8 × 4 = 1024

十一、完整训练流水线

把所有东西串起来:

Pasted image 20260625172819.png

如果只记住一句话:

Dataset 负责“取数据”,Tokenizer 负责“切词变数字”,Padding 负责“补齐长度”,Loss Mask 负责“告诉模型哪些地方该学习”,DataLoader 负责“打包成 Batch 送进 GPU”。这五个环节组成了 LLM 训练的数据处理流水线。