数据 基础

每条 ML 流水线都绕不过的最少数据管道。5 个短小主题,涵盖数据集到底是什么、 特征 vs 标签的拆分、能让评估「守住底线」的训练 / 验证 / 测试划分、 每个字符串底下的字节(ASCII 和 UTF-8 —— LLM 真正吃进去的格式), 以及在模型见到任何数字之前默默运行的标准化与清洗。 数学很少,直觉很多。

01

数据集(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
  ...        ...      ...   ...        ...
数据集 · 1 样本面积卧室房龄邮编价格85011294110820,000145038941101,300,0002100415941141,720,000320053941142,650,00060002294103480,000
1 / 5
每一行就是一个样本。数据集通过「再加一行」来变大。

这张表就是数据集。这里展示了 5 个样本,实际上下面还藏着成千上万个。 每一列都是关于这套房子的一个事实,每一行都是一套房子。 只要「电子表格」这个比喻还讲得通,这就是「数据集」的本意。

三个值得在文章一开头就摆清楚的观察,后面几节都会用到:

  • 规模重要,但不是全部。深度学习吃大数据 —— ImageNet 有 120 万张图,现代 LLM 在万亿 token 量级上训练 —— 但一份小而精的数据集,经常打赢一份巨大但粗糙的数据集。 「垃圾进、垃圾出」是 ML 最老的一条规则,至今依旧成立。
  • 所有行要长得像。数据集里的每一行,应该是同一东西, 有同样的列,采自你关心的同一个总体。把公寓和集装箱混在「房价」数据集里, 只是给模型平白增加任务难度。
  • 数据未必是表格。图像是 3D 数组(高 × 宽 × 通道), 音频是一长串采样,文本就是字符串。电子表格这个比喻还能用 —— 每一行是一张图、一篇文档 —— 只是每个「格子」本身可能很大。

数据集要回答两个核心问题,接下来两节会展开:每一行带了什么信息?(特征 vs 标签),以及怎么让模型不作弊?(训练 / 验证 / 测试划分)。 后面所有内容都搭在这两块基石上。

在 Transformer 里:现代 LLM 的数据集,就是「能搜集到的所有文字」 —— Common Crawl、GitHub、书籍、论文、代码、对话,几万亿 token。旁边没有「标签文件」; prompt 本身就是问题,下一个 token 就是答案,一个 epoch 里要做几十亿次。 本文后面讲的特征、标签、划分、编码、清洗,在 LLM 这里全部适用 —— 只是术语换成了「字节序列」而不是「电子表格行」。

02

特征与标签(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
  ...
面积卧室房龄邮编价格85011294110820,000145038941101,300,0002100415941141,720,000房价数据集里的三行。
1 / 3
同一张表,两个角色:模型看哪些列,以及它要预测哪一列。

怎么挑标签,基本就决定了你在解什么问题。同样一份数据集, 换个标签就是完全不同的模型:

  • 标签 = 价格 → 一个估房屋价值的模型。
  • 标签 = 30 天内是否售出? → 一个预测「这房子挂出来卖得多快」的模型。
  • 标签 = 邮编(以面积、卧室、房龄为特征)→ 一个「从建筑特征猜街区」的模型。

标签的类型,决定了你该用什么模型和什么损失函数:

  • 连续标签回归(regression)。 美元计的房价、明天的温度、点击率。损失一般是均方误差。
  • 二选一的离散标签二分类。 是否垃圾邮件、是否违约。损失一般是二元交叉熵。
  • 多选一的离散标签多分类。 这张图属于 ImageNet 1000 类中的哪一类?损失是交叉熵。
  • 结构化标签 → 目标检测(标签 = 一组边框 + 类别)、 翻译(标签 = 另一种语言的句子)、生成(标签 = 下一个 token)。

一个值得提一下的细节:不是所有数据集都自带标签。无监督学习只在纯 x 上工作,自己找结构 —— 聚类、降维、密度估计。 还有一个漂亮的中间地带叫做 自监督学习:数据本身就提供标签, 不需要人来标注。把图像盖掉一块,让模型补出来;把句子里一个词盖掉, 让模型猜回来。「标签」其实早就在数据里,只是要换种方式去看而已。

在 Transformer 里:LLM 是世界上最贵的一个自监督模型。 数据集就是「一大堆文本」。一个训练样本的特征,是「某段文本的前 N 个 token」; 标签就是「第 N+1 个 token」。仅此而已。从来没人专门写过标签文件。 模型通过反复预测「下一个 token」来学习,做几十亿次; 而几万亿 token 的训练文本里,藏着几十亿个完美对齐的 (x, y) 对, 就在表面之下。

03

训练集 / 验证集 / 测试集

把数据集切成三块,模型在「考试」时就没法作弊了。

想象一个老师先发了一沓练习题,然后考试还是同一沓题。每个学生都满分。 我们从这次考试里完全看不出谁真的学懂了。训练 / 验证 / 测试划分,要解决的就是这个问题。它把数据集切成三块互不相交的子集,各干各的活,让最终报告的「模型表现」 反映模型真正的能力,而不是它的记忆力。

三个角色:

  • 训练集(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%
10,000 行训练8,000 · 80%验证1,000 · 10%测试1,000 · 10%三块互不相交 —— 现在按下 ⏭ 看每一块的职责把数据集切成三块,各干各的活。
1 / 4
三个角色、三块互不相交的子集。测试集要等到最后才打开。

为什么要三块,不是两块?因为只要你用「验证结果」改了什么 —— 改个超参数、换个架构、换个分词器 —— 验证集的信息就开始往模型里渗透。 来回这样几轮之后,「验证准确率」就不再是一个公正的、对新数据上模型表现的估计。 测试集,直到项目末尾都不动一根头发,正是为了让你在终点线那里有一个可信的数字。

两种典型的翻车方式,值得提前记住,因为每个人都会撞上:

  • 数据泄漏(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 评估工作量的一半。

04

文本编码(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 —— 基本上一切。hU+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 字节hU+0068681字节iU+0069691字节·U+0020201字节U+4E2DE4B8AD3字节👋U+1F44BF09F918B4字节"hi 中 👋" —— 编辑器里看到的样子。
1 / 3
看上去 5 个字符,实际上是 10 字节。UTF-8 给 ASCII 一字节、给「中」三字节、给 emoji 四字节。

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。

05

数据预处理(Preprocessing)

原始数据和模型真正看到的张量之间,那些不起眼但关键的步骤。

真实世界里的数据,从来不是「拿来就能训」的状态。有些行缺字段,有些列单位天差地别, 有些是手抖打错的。预处理(preprocessing)是「从原始数据到优化器看到的张量」之间 所有步骤的总称。这活儿看着不起眼,但你预处理的质量,往往比模型架构选什么影响更大。

两类常驻预处理:清洗(处理「错了的东西」)和标准化(处理「对了但量级不舒服的东西」)。

清洗负责一份「数据卫生清单」。跳过这些,模型就会学到垃圾:

  • 缺失值。一行 age = NaN,任何要求「这里是数字」的模型都会卡住。 可以丢掉这一行;可以拿这一列的均值 / 中位数填上;也可以把「缺失」单独当一类。 各有取舍;最差的选项是「装作没这事」。
  • 重复样本。两行完全一样,会把梯度往它编码的那种东西多推一倍。 先去重,再切分 —— 尤其在训练互联网文本时,同一段会被转发上千次。
  • 离群值。1 美元的房子、999°C 的气温、300 岁的人。 多半是录入错误,有时是真实但罕见的事件。无论是哪种, 一个极端值都可能主宰梯度。可以截断、裁剪或剔除,但要在文档里记下你做了什么。
  • 格式不一致。同一个国家,「USA」「United States」「U.S.」三种写法。 同一个日期,2024-01-3131/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
房龄面积02212815322μ = 12.0 · σ = 6.403200850145021003200600μ = 1640 · σ = 937原始值:房龄 0–25,面积 0–4000。同一张图,两行差得天悬地隔。
1 / 3
原始值的量级差得太多。Z-score 让每一列都落到同一个、可比较的范围上。

关于标准化,两条人人都用真实事故学会的规则:

  • 只在训练集上拟合,然后套到所有数据上。只用训练集计算 μσ, 然后拿这同一组数字去变换训练 / 验证 / 测试集。 在测试集上重新拟合,等于让测试数据反过来塞进你的预处理 —— 这跟第 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), 让梯度保持温顺。这一思路,本质上跟上面「按列标准化」是一回事。