AI-深度学习

从零搭建视觉模型 09:数据集、标签与数据划分

展开合集目录
  1. 从零搭建视觉模型 01:图片进入模型后发生了什么
  2. 从零搭建视觉模型 02:Python 起步,写给 C/C++ 开发者
  3. 从零搭建视觉模型 03:从像素到张量的基础概念
  4. 从零搭建视觉模型 04:图片如何变成张量
  5. 从零搭建视觉模型 05:MLP 入门,从手算到图片输入
  6. 从零搭建视觉模型 06:亲手搭建一个小型 CNN
  7. 从零搭建视觉模型 07:模型怎样预测类别:分数、概率与决策
  8. 从零搭建视觉模型 08:让模型开始学习,损失、梯度与训练循环
  9. 从零搭建视觉模型 09:数据集、标签与数据划分
  10. 从零搭建视觉模型 10:损失函数与优化器:怎样把错误变成更新
  11. 从零搭建视觉模型 11:批次、训练轮次与学习率
  12. 从零搭建视觉模型 12:训练第一个真实图片分类器
  13. 从零搭建视觉模型 13:过拟合、数据增强与正则化
  14. 从零搭建视觉模型 14:验证、测试与错误分析
  15. 从零搭建视觉模型 15:保存模型、恢复训练与独立推理
  16. 从零搭建视觉模型 · 综合实践:实现 MLP,从损失计算、训练到 ONNX 推理
  17. 从零搭建视觉模型 16:ResNet 与残差连接
  18. 从零搭建视觉模型 17:预训练、迁移学习与微调
  19. 从零搭建视觉模型 18:轻量模型与计算成本
  20. 从零搭建视觉模型 19:目标检测与 YOLO:从类别到目标框
  21. 从零搭建视觉模型 20:图像分割与 U-Net:给每个像素一个判断
  22. 从零搭建视觉模型 21:注意力与视觉 Transformer
  23. 从零搭建视觉模型 22:从训练模型到 ONNX
  24. 从零搭建视觉模型 23:量化与精度对比
  25. 从零搭建视觉模型 24:从摄像头输入到部署验收
  26. 从零搭建视觉模型 25:发展历史、主流分类与当前格局
从零搭建视觉模型 09:数据集、标签与数据划分的封面

学习初稿 · 第 9 课。 本篇已开放网页阅读,用于先建立完整认识。文中手算值和示意记录不代表真实训练成绩;真实数据训练、预训练权重和板端测试以本篇具体说明为准,本篇已附代码练习,实际验证范围见文末与包内记录。

模型学的是数据中提供的任务

已经能运行训练循环后,最容易忽略的问题是:标签究竟在描述什么,验证集是否真的独立?本课先把数据整理清楚,再继续增加训练技巧。

以飞机与汽车二分类为例,标签 0 和 1 只是编号。必须在一个确定的位置保存 0=飞机、1=汽车,训练、评估和部署共用这份约定。文件夹按字母排序得到的编号,可能与手写列表不同,不能凭印象推断。

每个样本除了图片,还需要来源

一个最小清单可以包含:相对路径、类别、来源组、划分。来源组可能是视频编号、拍摄批次或实物编号。还可记录图像宽高、标注版本和哈希,用来发现坏图与重复文件。

path,label,group,split
images/a01.jpg,0,session_a,train
images/a02.jpg,0,session_a,train
images/b01.jpg,1,session_b,val

这只是格式示意,不是已存在的数据文件。实际清单的类别与划分必须从本次数据导出。不要把后续实验运行所需的真实数据藏在其他文章的代码目录中。

三个集合承担不同角色

训练集用于计算梯度和更新参数;验证集用于选择训练轮数、超参数和阈值;测试集在方案冻结后用于最终评估。三者可以有标签,但只有训练集参与参数更新。

训练过程中反复查看测试成绩再改参数,实际上把测试集变成了验证工具。即使没有对测试样本调用 backward(),这种选择过程仍会使报告偏乐观。

常见比例只是起点,不是定律。样本很少时,先保证每类、每种来源在相应集合中有足够覆盖,再考虑比例。若少数类别只有两三个独立来源,盲目做 8:1:1 无法得到可靠估计,应补数据或采用合适的分组交叉验证。

最危险的泄漏通常来自相似样本

从同一视频抽取相邻帧,再按图片随机拆分,会把几乎相同的画面分到训练和验证。模型可能只需记住背景和物体外观就能获得高分。对同一图片先做增强,再将原图和增强图分别拆分,也存在同样的问题。

正确顺序通常是先按独立来源划分,再只对训练集合实施随机增强。若任务要求在新人物、新设备或新地点上泛化,就应按对应实体划分,而不是仅按文件名随机。

下面用标准库演示分组互斥,不代表它已经完成类别均衡分配:

import random

groups = [f'session_{i:02d}' for i in range(10)]
random.Random(7).shuffle(groups)
train, val, test = map(set, (groups[:6], groups[6:8], groups[8:]))
assert not (train & val or train & test or val & test)
assert train | val | test == set(groups)
print(len(train), len(val), len(test))

真实项目还应统计每组类别分布,避免某一类只出现在测试集。数据泄漏可以通过检查来源、哈希和近似重复共同发现,单纯设置随机种子不能防止它。

Dataset 与 DataLoader 的分工

Dataset 负责“按索引取一个样本和标签”,DataLoader 负责把多个样本组织成批次,并控制打乱顺序与加载方式。它们不会自动理解来源组,也不会替你判断标签是否正确。PyTorch 数据接口教程

训练集通常打乱顺序;验证与测试使用确定的处理,方便比较。训练与验证如果共享同一个带随机增强的底层 Dataset,可能使验证也发生随机变化。稳妥做法是从同一原始数据建立不同 transform 的视图,再使用事先保存的互斥索引。

CIFAR-10 已提供训练与测试部分。本系列在其训练部分内部再划出验证集,原测试部分暂不用于选模型。换成自己的摄像头数据后,必须重新按来源设计划分,不能认为公开数据的拆分策略自动适用。

本课应交付的东西

一份类别表、一份样本清单、一份固定划分及每类统计,比“我随便挑了几张测试”更有用。后续每次实验记录清单版本或哈希,这样比较模型时才知道是否真的用了同一批样本。

练习

  1. 一段视频的前 80% 帧训练,后 20% 验证,是否一定独立?
  2. 把训练图旋转后放入测试集,可以测泛化吗?
  3. 为什么验证集不能每次随机重新划分?
参考答案

不一定,同一场景和实体可能高度相似,应按实际泛化目标选择独立来源。旋转副本与原图高度相关,不能作为独立测试证据。不断重新划分会改变评估难度,也可能让曾被训练使用的样本进入验证,导致结果不可比;应固定划分版本。

下一课讨论损失与优化器。先确认数据表达的任务正确,再解释怎样优化它。

综合实践

想把这些知识接成一套可运行的代码,可以进入实现 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。


完整阅读目录 · 上一篇 · 下一篇