数据 基础
每条 ML 流水线都绕不过的最少数据管道。5 个短小主题,涵盖数据集到底是什么、 特征 vs 标签的拆分、能让评估「守住底线」的训练 / 验证 / 测试划分、 每个字符串底下的字节(ASCII 和 UTF-8 —— LLM 真正吃进去的格式), 以及在模型见到任何数字之前默默运行的标准化与清洗。 数学很少,直觉很多。
数据集(Dataset)
一堆样本 —— 模型所有的「知识」其实都源自这里。
从机制上看,数据集就是一份清单。清单里的每一项, 都是你想让模型学的东西的一个例子 —— 一封邮件、一张图、一句话、一笔交易、一份 CT 扫描。 清单可以是 50 项,也可以是 500 亿项,原理一样。模型「会」什么, 归根结底都是因为它在足够多的样本上看到了同一种规律。
指代「一项」的词有一堆,平时混着用:样本、例子、实例、行、记录、数据点、观测。意思全一样 —— 训练时模型一次会看到的一个自洽单位。你团队习惯叫什么就叫什么,别纠结术语。
最简单的心智模型,是一张 电子表格:一行一个样本, 每列是关于这个样本的一条信息。下面是一张「预测房价」数据集的示意:
面积 卧室数 房龄 邮编 价格 ──────────────────────────────────────────── 850 1 12 94110 820,000 1450 3 8 94110 1,300,000 2100 4 15 94114 1,720,000 3200 5 3 94114 2,650,000 600 0 22 94103 480,000 ... ... ... ... ...
这张表就是数据集。这里展示了 5 个样本,实际上下面还藏着成千上万个。 每一列都是关于这套房子的一个事实,每一行都是一套房子。 只要「电子表格」这个比喻还讲得通,这就是「数据集」的本意。
三个值得在文章一开头就摆清楚的观察,后面几节都会用到:
- 规模重要,但不是全部。深度学习吃大数据 —— ImageNet 有 120 万张图,现代 LLM 在万亿 token 量级上训练 —— 但一份小而精的数据集,经常打赢一份巨大但粗糙的数据集。 「垃圾进、垃圾出」是 ML 最老的一条规则,至今依旧成立。
- 所有行要长得像。数据集里的每一行,应该是同一种东西, 有同样的列,采自你关心的同一个总体。把公寓和集装箱混在「房价」数据集里, 只是给模型平白增加任务难度。
- 数据未必是表格。图像是 3D 数组(高 × 宽 × 通道), 音频是一长串采样,文本就是字符串。电子表格这个比喻还能用 —— 每一行是一张图、一篇文档 —— 只是每个「格子」本身可能很大。
数据集要回答两个核心问题,接下来两节会展开:每一行带了什么信息?(特征 vs 标签),以及怎么让模型不作弊?(训练 / 验证 / 测试划分)。 后面所有内容都搭在这两块基石上。
在 Transformer 里:现代 LLM 的数据集,就是「能搜集到的所有文字」 —— Common Crawl、GitHub、书籍、论文、代码、对话,几万亿 token。旁边没有「标签文件」; prompt 本身就是问题,下一个 token 就是答案,一个 epoch 里要做几十亿次。 本文后面讲的特征、标签、划分、编码、清洗,在 LLM 这里全部适用 —— 只是术语换成了「字节序列」而不是「电子表格行」。
特征与标签(Features & Labels)
把每一行拆成两半 —— 「模型能看的」和「模型要预测的」。
第 1 节里的数据集只是一堆行。要把这堆行变成一个「学习问题」,你需要把每一行拆成两块:特征(features)——模型可以看到的列,以及标签(label)—— 你让它预测的那一列。这种拆分,在数据集每一行上重复一遍, 才让「训练一个模型」这件事真正有了意义。
几乎每篇 ML 论文都用同一套记号:
x—— 一个样本的特征。通常是一串数字向量;有时也可能是图、字符串、图结构。y—— 这个样本的标签。一个数、一类标签,有时也是个结构化的东西。- 数据集的一行 = 一对
(x, y)。整个数据集 = 一堆(x, y)对。
回到第 1 节的房价例子。要让模型学「给一套房子,预测它的价格」, 价格这一列就是标签,其余列就是特征:
特征 (x) 标签 (y) ──────────────────────────────── ─────────── 面积 卧室数 房龄 邮编 价格 ──────────────────────────────── ─────────── 850 1 12 94110 820,000 1450 3 8 94110 1,300,000 2100 4 15 94114 1,720,000 ...
怎么挑标签,基本就决定了你在解什么问题。同样一份数据集, 换个标签就是完全不同的模型:
- 标签 = 价格 → 一个估房屋价值的模型。
- 标签 = 30 天内是否售出? → 一个预测「这房子挂出来卖得多快」的模型。
- 标签 = 邮编(以面积、卧室、房龄为特征)→ 一个「从建筑特征猜街区」的模型。
标签的类型,决定了你该用什么模型和什么损失函数:
- 连续标签 → 回归(regression)。 美元计的房价、明天的温度、点击率。损失一般是均方误差。
- 二选一的离散标签 → 二分类。 是否垃圾邮件、是否违约。损失一般是二元交叉熵。
- 多选一的离散标签 → 多分类。 这张图属于 ImageNet 1000 类中的哪一类?损失是交叉熵。
- 结构化标签 → 目标检测(标签 = 一组边框 + 类别)、 翻译(标签 = 另一种语言的句子)、生成(标签 = 下一个 token)。
一个值得提一下的细节:不是所有数据集都自带标签。无监督学习只在纯 x 上工作,自己找结构 —— 聚类、降维、密度估计。 还有一个漂亮的中间地带叫做 自监督学习:数据本身就提供标签, 不需要人来标注。把图像盖掉一块,让模型补出来;把句子里一个词盖掉, 让模型猜回来。「标签」其实早就在数据里,只是要换种方式去看而已。
在 Transformer 里:LLM 是世界上最贵的一个自监督模型。 数据集就是「一大堆文本」。一个训练样本的特征,是「某段文本的前 N 个 token」; 标签就是「第 N+1 个 token」。仅此而已。从来没人专门写过标签文件。 模型通过反复预测「下一个 token」来学习,做几十亿次; 而几万亿 token 的训练文本里,藏着几十亿个完美对齐的 (x, y) 对, 就在表面之下。
训练集 / 验证集 / 测试集
把数据集切成三块,模型在「考试」时就没法作弊了。
想象一个老师先发了一沓练习题,然后考试还是同一沓题。每个学生都满分。 我们从这次考试里完全看不出谁真的学懂了。训练 / 验证 / 测试划分,要解决的就是这个问题。它把数据集切成三块互不相交的子集,各干各的活,让最终报告的「模型表现」 反映模型真正的能力,而不是它的记忆力。
三个角色:
- 训练集(training set) —— 模型在「拟合阶段」能看到的那部分行。 优化器看着它来调权重。通常占数据集的 70–90%。
- 验证集(validation set) —— 模型从不训练的那批行, 用来在多个候选模型之间挑选。学习率
1e-3还是1e-4? 12 层还是 24 层?谁的验证损失更低就选谁。常被叫做 dev set。通常 5–15%。 - 测试集(test set) —— 锁起来,项目尾声只打开一次, 用来报告最终成绩。如果你在确定模型之前就偷看了它,它就失去了存在的意义。 通常 10–20%。
在一份 10,000 行的数据集上做一次具体划分:
┌──────────── 10,000 行 ──────────────┐
│ 训练 8,000 验证 1,000 测试 1,000 │
└─────────────────────────────────────────┘
80% 10% 10%为什么要三块,不是两块?因为只要你用「验证结果」改了什么 —— 改个超参数、换个架构、换个分词器 —— 验证集的信息就开始往模型里渗透。 来回这样几轮之后,「验证准确率」就不再是一个公正的、对新数据上模型表现的估计。 测试集,直到项目末尾都不动一根头发,正是为了让你在终点线那里有一个可信的数字。
两种典型的翻车方式,值得提前记住,因为每个人都会撞上:
- 数据泄漏(data leakage)。测试集里的行偷偷溜进了训练。 可能是同一行的复制,也可能是近似复制(同一篇新闻被改写了一次), 还可能更隐蔽(用未来信息预测过去)。 一旦测试分数「好得不像真的」,八成就是泄漏了。
- 划分和现实的分布漂移。你用 2018 年的邮件按 80/10/10 切分, 但 2026 年才上线。测试集本身切得干净 —— 但它已经是 4 年前的样本。 测试看起来很好,部署起来照样可能很糟。
具体怎么切?多数数据集随便打乱再切片就行 —— 行与行可互换, 随机 80/10/10 切就行。三种情况例外,这时不能随便打乱:
- 时间序列。用未来数据训练再去预测明天股价,本质就是作弊。 要按时间切:训练用过去、验证用近段过去、测试用现在。
- 分组数据。100 张照片来自 10 只猫(每只 10 张)。 把同一只猫的不同照片分进训练和测试,就泄漏了 —— 模型学到的是「认这只猫」而不是「认猫」。要按猫(组)切,不是按照片切。
- 类别不平衡。99% 是正常流量、1% 是欺诈。 随便切一刀,测试集里可能一例欺诈都没有。 用分层抽样 —— 让每一块里的类别比例固定。
在 Transformer 里:LLM 对应的「验证 / 测试」是一组固定的留出文档 —— 通常是从 Common Crawl 里切出一片在训练前就放到一边,再加上一组精心挑选的评测集 (HellaSwag、MMLU、GSM8K 等)。留出文档上的损失,就是研究者报告的「验证损失」「eval loss」; 评测集则是公开的成绩榜。泄漏的风险确实存在:一旦评测题目跑进了训练语料, 这个评测就不再能衡量任何真实的东西。检测和剔除这类污染, 几乎要占现代 LLM 评估工作量的一半。
文本编码(Text Encoding)
「字符串」在计算机里不存在。字节才存在。
像 "hello" 这样的「字符串」,是你编程语言对你撒的一个善意谎言。 在底层,每一个文本文件、每一条聊天消息、每一行源码,本质都是一串字节——也就是 0 到 255 之间的整数。文本编码就是「人能读的字符」和「这些字节」之间的对应规则。 ML 关心两种编码:ASCII(简单的那种)和 UTF-8(整个现代互联网在用的那种)。
ASCII 是 1963 年的原始标准,给 128 个字符各分配一个字节。 拉丁字母、数字、标点、几个控制符 —— 就这些。在 1963 年这对英文够用了, 放到任何英文之外的场景就完全不够。
"hello" → 104 101 108 108 111
'h' 'e' 'l' 'l' 'o'
5 个字符 → 5 个字节(各占一个)。ASCII 一旦遇到它没预见的字母,立刻就完蛋。é?中?👋? 7 位的 ASCII 里它们都不存在。后来的解决方案是 Unicode: 给人类历史上出现过的每一个字符,分配一个唯一的数字,叫 码点(code point)。 如今已经有超过 15 万个,覆盖各种文字、符号、数学符号、emoji —— 基本上一切。h 是 U+0068,中 是 U+4E2D,👋 是 U+1F44B。
但码点只是抽象的数字,不是字节。要把它们写到磁盘上、传到网线里,还是得编码成字节。 这就轮到 UTF-8 出场了。UTF-8 是一种变长编码:
- ASCII 字符(码点 0–127)→ 1 字节。和 ASCII 完全一样。
- 拉丁带音符、希腊、西里尔、希伯来、阿拉伯(128–2047)→ 2 字节。
- 中文、日文、韩文、绝大多数其他文字(2048–65535)→ 3 字节。
- emoji、稀有符号、古文字(65536 以上)→ 4 字节。
一个例子,说明「字符串长度」这件事多么坑:
"hi 中 👋"
字符: h i ' ' 中 👋
码点: U+68 U+69 U+20 U+4E2D U+1F44B
UTF-8 字节: 68 69 20 E4 B8 AD F0 9F 91 8B
─1─ ─1─ ─1─ ──3── ───4───
眼睛看到 5 个字符 → 实际 10 个字节UTF-8 之所以是默认选择,有三个性质:
- 向后兼容 ASCII。任何旧的 ASCII 文件,本身就是一份合法的 UTF-8 —— 字节级一模一样。四十年的英文文本,直接能用。
- 自同步。「字符起始字节」和「字符延续字节」长得不一样, 就算你从字节流中间开始读,也能很快重新对齐。在丢包、分片的网络环境下很有用。
- 全人类通用。一种编码,覆盖整个人类文字史。 以前不同编码不匹配,是日常的痛苦来源(「乱码」:本该是中文的地方变成
çåüå€); UTF-8 把这种事情基本消灭了。
一个值得记住的大坑:字节数 ≠ 字符数 ≠ 显示宽度。 字符串 "中" 在 Python 里长度是 1(1 个字符),原始字节是 3, 在终端里大约占 2 个列。"👋" 用户眼里是 1 个东西, 但在 JavaScript 里 .length 是 2(它在 UTF-16 里是一个「代理对」), UTF-8 编码是 4 字节,在终端里取决于字体可能占 1 列也可能占 2 列。 天真地把字符串「截断到 100 字符」,第一次遇到 emoji 就会出各种奇怪问题。
在 Transformer 里:这事对 LLM 比对任何其他 ML 系统都更重要, 因为 LLM 处理的就是文本。LLM 最上层的分词器,并不在 Unicode 字符上工作 —— 它在 UTF-8 字节 上工作。现代分词器(GPT 类用的 BPE、其他不少用 SentencePiece) 从原始字节流出发,学着把频繁共同出现的字节序列合并成更大的单元。 所以单个 emoji 像 👋 经常会占多个 token —— 它的 4 个 UTF-8 字节 未必能合成一个。所以非英文文本的「token 数 / 字符数」往往比英文高。 所以「模型 128k 上下文」的意思是「12.8 万 token,大约对应 8 万英文单词, 但中文也许只有 4 万字」。一切让人困惑的「LLM token 计数」问题, 往下挖一层就是 UTF-8。
数据预处理(Preprocessing)
原始数据和模型真正看到的张量之间,那些不起眼但关键的步骤。
真实世界里的数据,从来不是「拿来就能训」的状态。有些行缺字段,有些列单位天差地别, 有些是手抖打错的。预处理(preprocessing)是「从原始数据到优化器看到的张量」之间 所有步骤的总称。这活儿看着不起眼,但你预处理的质量,往往比模型架构选什么影响更大。
两类常驻预处理:清洗(处理「错了的东西」)和标准化(处理「对了但量级不舒服的东西」)。
清洗负责一份「数据卫生清单」。跳过这些,模型就会学到垃圾:
- 缺失值。一行
age = NaN,任何要求「这里是数字」的模型都会卡住。 可以丢掉这一行;可以拿这一列的均值 / 中位数填上;也可以把「缺失」单独当一类。 各有取舍;最差的选项是「装作没这事」。 - 重复样本。两行完全一样,会把梯度往它编码的那种东西多推一倍。 先去重,再切分 —— 尤其在训练互联网文本时,同一段会被转发上千次。
- 离群值。1 美元的房子、999°C 的气温、300 岁的人。 多半是录入错误,有时是真实但罕见的事件。无论是哪种, 一个极端值都可能主宰梯度。可以截断、裁剪或剔除,但要在文档里记下你做了什么。
- 格式不一致。同一个国家,「USA」「United States」「U.S.」三种写法。 同一个日期,
2024-01-31和31/01/2024。 模型看到的是三个不同的字符串,而你想要的只是一个。早早归一化。
标准化解决「量级」问题。假设你的特征里有面积(600–4000)和房龄(0–80)。 两者完全不在一个尺度上。神经网络技术上能学这种差异, 但梯度下降在「每个特征都落在差不多的范围」时舒服得多。两套配方到处都在用:
- Z-score(标准化)。减去列均值,除以列标准差:
x' = (x − μ) / σ。每一列最终均值是 0、标准差是 1。 ML 里的主流选择。 - Min-max(归一化)。把每一列压到
[0, 1]:x' = (x − min) / (max − min)。 需要有界输入时用,比如把图像像素值压到[0, 1]。
具体例子:房龄和面积放在一起,z-score 之前 vs 之后:
原始 标准化(z-score)
───────────── ─────────────────────────
房龄 面积 房龄 面积
──── ────── ────── ──────
12 850 -0.32 -1.21
8 1450 -0.61 -0.55
15 2100 -0.10 0.17
3 3200 -0.96 1.39
22 600 0.40 -1.48
... ... ... ...
μ 11.9 1640 ≈ 0 ≈ 0
σ 7.0 900 ≈ 1 ≈ 1关于标准化,两条人人都用真实事故学会的规则:
- 只在训练集上拟合,然后套到所有数据上。只用训练集计算
μ和σ, 然后拿这同一组数字去变换训练 / 验证 / 测试集。 在测试集上重新拟合,等于让测试数据反过来塞进你的预处理 —— 这跟第 3 节警告的那种数据泄漏,只是换了个标准化的马甲。 - 上线时用同一套配方。训练时用了什么
μ和σ, 部署时输入也得用同一对数字去变换。如果训练用的是标准化数据、推理却灌的是原始数据, 模型相当于看到的是乱码 —— 是那种上线之后让人很难堪的生产 bug。
图像和音频各有专门变种:
- 图像。先除以 255 把像素压到
[0, 1], 再减去每通道均值(比如 ImageNet 那组著名的(0.485, 0.456, 0.406)), 再除以每通道标准差。剪裁、翻转、色彩抖动这些叫数据增强 —— 合成的额外行,让模型学会对无关因素保持不变。 - 音频。把原始波形变成频谱图,通常对幅度取对数,再 z-score。 到这一步它的形状已经像图像了 —— 一个 2D 张量,后面的预处理逻辑跟图像一样。
在 Transformer 里:现代 LLM 的预处理流水线分三段。 (1)过滤和去重:扔掉垃圾邮件、坏 HTML、重复文本、低质量文档 —— 光这一步,对下游质量的提升,经常比把模型规模翻倍还大。 (2)分词(tokenization):把 UTF-8 字节流(第 4 节)切成整数 token ID, 用的是 BPE 或 SentencePiece。 (3)序列拼接(sequence packing):把分好词的文档串接成等长序列, 让 GPU 永远不会看到「半空的 batch」。Z-score 那种标准化不会用在整数 token ID 上 —— 模型自己有 embedding 层把每个 token 映射成连续向量 —— 但模型内部每一个可学习参数, 都会被同样精心初始化、按层归一化(LayerNorm、RMSNorm), 让梯度保持温顺。这一思路,本质上跟上面「按列标准化」是一回事。