学习初稿 · 第 9 课。 本篇已开放网页阅读,用于先建立完整认识。文中手算值和示意记录不代表真实训练成绩;真实数据训练、预训练权重和板端测试以本篇具体说明为准,本篇已附代码练习,实际验证范围见文末与包内记录。
模型学的是数据中提供的任务
已经能运行训练循环后,最容易忽略的问题是:标签究竟在描述什么,验证集是否真的独立?本课先把数据整理清楚,再继续增加训练技巧。
以飞机与汽车二分类为例,标签 0 和 1 只是编号。必须在一个确定的位置保存 0=飞机、1=汽车,训练、评估和部署共用这份约定。文件夹按字母排序得到的编号,可能与手写列表不同,不能凭印象推断。
每个样本除了图片,还需要来源
一个最小清单可以包含:相对路径、类别、来源组、划分。来源组可能是视频编号、拍摄批次或实物编号。还可记录图像宽高、标注版本和哈希,用来发现坏图与重复文件。
|
这只是格式示意,不是已存在的数据文件。实际清单的类别与划分必须从本次数据导出。不要把后续实验运行所需的真实数据藏在其他文章的代码目录中。
三个集合承担不同角色
训练集用于计算梯度和更新参数;验证集用于选择训练轮数、超参数和阈值;测试集在方案冻结后用于最终评估。三者可以有标签,但只有训练集参与参数更新。
训练过程中反复查看测试成绩再改参数,实际上把测试集变成了验证工具。即使没有对测试样本调用 backward(),这种选择过程仍会使报告偏乐观。
常见比例只是起点,不是定律。样本很少时,先保证每类、每种来源在相应集合中有足够覆盖,再考虑比例。若少数类别只有两三个独立来源,盲目做 8:1:1 无法得到可靠估计,应补数据或采用合适的分组交叉验证。
最危险的泄漏通常来自相似样本
从同一视频抽取相邻帧,再按图片随机拆分,会把几乎相同的画面分到训练和验证。模型可能只需记住背景和物体外观就能获得高分。对同一图片先做增强,再将原图和增强图分别拆分,也存在同样的问题。
正确顺序通常是先按独立来源划分,再只对训练集合实施随机增强。若任务要求在新人物、新设备或新地点上泛化,就应按对应实体划分,而不是仅按文件名随机。
下面用标准库演示分组互斥,不代表它已经完成类别均衡分配:
|
真实项目还应统计每组类别分布,避免某一类只出现在测试集。数据泄漏可以通过检查来源、哈希和近似重复共同发现,单纯设置随机种子不能防止它。
Dataset 与 DataLoader 的分工
Dataset 负责“按索引取一个样本和标签”,DataLoader 负责把多个样本组织成批次,并控制打乱顺序与加载方式。它们不会自动理解来源组,也不会替你判断标签是否正确。PyTorch 数据接口教程
训练集通常打乱顺序;验证与测试使用确定的处理,方便比较。训练与验证如果共享同一个带随机增强的底层 Dataset,可能使验证也发生随机变化。稳妥做法是从同一原始数据建立不同 transform 的视图,再使用事先保存的互斥索引。
CIFAR-10 已提供训练与测试部分。本系列在其训练部分内部再划出验证集,原测试部分暂不用于选模型。换成自己的摄像头数据后,必须重新按来源设计划分,不能认为公开数据的拆分策略自动适用。
本课应交付的东西
一份类别表、一份样本清单、一份固定划分及每类统计,比“我随便挑了几张测试”更有用。后续每次实验记录清单版本或哈希,这样比较模型时才知道是否真的用了同一批样本。
练习
- 一段视频的前 80% 帧训练,后 20% 验证,是否一定独立?
- 把训练图旋转后放入测试集,可以测泛化吗?
- 为什么验证集不能每次随机重新划分?
参考答案
不一定,同一场景和实体可能高度相似,应按实际泛化目标选择独立来源。旋转副本与原图高度相关,不能作为独立测试证据。不断重新划分会改变评估难度,也可能让曾被训练使用的样本进入验证,导致结果不可比;应固定划分版本。
下一课讨论损失与优化器。先确认数据表达的任务正确,再解释怎样优化它。
综合实践
想把这些知识接成一套可运行的代码,可以进入实现 MLP:从损失计算、训练到 ONNX 推理。实践包含自生成数据、训练与验证、模型保存、独立推理、ONNX 导出和数值对照;遇到问题可按实践中的索引返回对应理论章节。
配套代码练习
实验:按来源分组划分数据
查看完整代码:main.py
"""按来源分组划分数据。运行方式与练习见 README.md。"""
import random
# 同一来源的两帧必须一起分配;构造清单不含真实图片。
samples = [{'id':f'{c}-{g}-{f}', 'label':c, 'group':f'{c}-{g}'}
for c in range(2) for g in range(10) for f in range(2)]
# print(f"{samples}")
print(f"{samples[0]}")
splits = {name:[] for name in ['train','val','test']}
print(f"{splits.keys()}")
print(f"{splits.values()}")
print(f"{splits.items()}")
for label in range(2):
groups = sorted({s['group'] for s in samples if s['label']==label})
print(f'类别 {label} 的来源组: {groups}')
random.Random(7+label).shuffle(groups)
print(f'类别 {label} 的来源组: {groups}')
for name, chosen in zip(splits, [groups[:6],groups[6:8],groups[8:]]):
splits[name].extend(s for s in samples if s['group'] in chosen)
print(f"{splits}")
print(f"{splits.values()}")
group_sets = [{s['group'] for s in rows} for rows in splits.values()]
for i in range(3):
for j in range(i): assert group_sets[i].isdisjoint(group_sets[j])
assert sum(map(len,splits.values())) == len(samples)
for name, rows in splits.items():
print(name, len(rows), '类别计数', [sum(s['label']==c for s in rows) for c in range(2)])
在项目根目录执行;下载包解压后,在解压目录执行 python3 main.py。
cd 'content/posts/AI-深度学习/从零搭建视觉模型/09-数据集标签与划分/code' && python3 main.py
本篇已附独立实验包,在代码卡片中查看源码、下载或复制运行命令。解压后执行 python3 main.py;依赖安装见包内 README.md。
观察目标: 训练/验证/测试分别 24/8/8 条,两类均衡,来源无交集。
动手修改: 故意按帧随机划分,打印交叉的 group;再改回按来源划分。为什么同一个 seed 不能自动防止泄漏?
所有输入均在本篇代码中构造;运行范围和验证记录见包内 verification.md。
