核查日期:2026-10-11。状态:本文是待执行的教学练习;代码、训练、保存和重载均未运行,未下载数据或安装依赖。准确率、耗时与费用目前留空。
这一节属于独立的“模型基础”栏目。Java Agent 路线学习怎样安全调用现成模型;这里学习模型怎样从数据计算、求梯度和更新参数。先完成自己需要的一条路线即可,不必同时安装两套框架。
目标与前置知识
完成后应能解释一批图片的形状和标签含义,检查模型的一次前向、反向与参数更新,并把同一模型的参数保存后重新加载。这里先证明数据和计算过程接得上,不设准确率目标。
需要会 Python 变量、函数、类和列表;能看懂矩阵的行列与基本导数。遇到概念问题,回到已有的张量文章、自动微分文章、多层感知机文章和分类损失文章,无需再学一套平行的概念课程。
先读 PyTorch 基础入口的使用说明,再按下方步骤阅读指定章节。官网当前教程页展示的构建版本是 2.14.0+cu130;这不是本练习已经安装、验证的版本组合,也不要求修改本站已有教程所用版本。练习者应在独立 Python 环境中记录自己选用且相互匹配的 torch、torchvision、Python 和操作系统版本。先使用 CPU;本次没有完成任何依赖安装或版本组合验证。
1. 先认识一批图片
固定使用 FashionMNIST:每张是 28×28 的灰度服饰图片,标签是 10 类之一。先只取 8 个样本,记录每个标签编号及对应类别。图片张量预期是 [8, 1, 28, 28]:8 张图片、1 个通道、高 28、宽 28;标签预期是 [8] 的整数。不要把标签当成连续数值去回归,也不要把一个批次当成一张图片。
原始来源:Datasets & DataLoaders,只读 Loading a Dataset 与 Preparing your data for training with DataLoaders;Tensors,只读 Attributes of a Tensor。数据集说明来自官网;下面的形状、类型和范围断言尚未实际执行。
特征转换选择 v2.ToImage() 加 v2.ToDtype(torch.float32, scale=True),把图像转换为浮点张量。标签保持整数类别编号。本练习采用 CrossEntropyLoss 的类别索引用法,因此不复制 Transforms 中单独示范的 one-hot 标签转换。
2. 走通前向、梯度与一次更新
先把 [8, 1, 28, 28] 展平为 [8, 784],经过一个 32 单元的隐藏层,输出 [8, 10]。输出是各类别的原始分数,直接交给交叉熵损失,训练前不再手动做 softmax。argmax(dim=1) 可以得到预测类别,但第一次预测是否正确不是这一步的验收条件。
一次更新按 zero_grad → forward → loss → backward → step 执行。backward() 计算梯度,step() 才修改参数;只看到损失值并不能证明参数更新发生。记录梯度是否存在、是否有限,并比较更新前后的参数。一次更新也不保证这一批损失下降,更不能证明新图片上的效果。
原始来源:Build the Neural Network 的 Define the Class 与 Model Layers;Automatic Differentiation 的 Computing Gradients;Optimizing Model Parameters 的 Loss Function 与 Optimizer。下面 784→32→10 的结构和断言是本站自拟练习。
3. 只保存参数,再用相同结构加载
保存 state_dict 后,新建相同结构的模型再加载。用固定的那一批图片,对比保存时模型与重载模型的输出。比较必须记录设备、版本、模式和容差;本练习只检查当前环境中的这一次往返,不承诺 CPU 与 GPU、不同系统或不同版本之间完全相同。
原始来源:Save and Load the Model 的 Saving and Loading Model Weights。该页用 VGG 展示保存原理;本练习继续用自己的小模型,不需要下载 VGG 预训练权重。只保存模型参数适合本节的推理重载检查;接着同一次训练还需要优化器等其他状态,见已有训练循环与检查点文章。
4. 分别验收评估模式、梯度记录和缓冲状态
适用阶段:能够解释本页的一次前向、损失、反向与参数更新,准备组织训练和验证循环。前置见自动微分与反向传播和训练循环与检查点;BatchNorm 的统计来源见同一个样本与批次伙伴。
原始核查:2026-10-20 12:11—12:12 JST。原资料记录 PyTorch 文档 v2.14.0;本次再次阅读固定 /docs/2.14/ 正文。Module 页未见独立发布日期,Autograd mechanics 标示创建于 2017-01-16、更新于 2026-08-03。文档版本是阅读依据,不是已经安装验证的环境,也不要求升级站内其他文章的 2.9 基线。下面练习均未运行。
两个开关分别控制什么
model.eval() 相当于 model.train(False),切换具有训练/评估差异的模块行为,例如 Dropout 与 BatchNorm;它不关闭自动微分。no_grad 控制上下文内的运算不记录反向图,不能代替模块模式切换。来源:Module.eval及Autograd 的局部梯度控制与评估模式。
本页原始 784→32→10 小网络没有 Dropout 或 BatchNorm,不应把“切到 eval 后输出必须变化”设为断言。原代码分别检查 model.training 和输出的 requires_grad,继续保留这个基线。后续对照请另建小模型,不把增加层后的结果冒充原基线结果。
inference_mode 也是梯度控制维度,并不替你调用 eval()。它对其中创建的张量有更严格的后续自动微分使用限制;不要仅凭名称替换 no_grad,或把本节当成已经测得推理更快、占用更少内存的证据。
建议练习一:四种组合分别填记录
准备含 Linear 与 Dropout 的小模型,固定权重和同一输入;明确 Linear 参数参与计算且 requires_grad=True,让正常梯度模式下确实有可记录的路径。对照四种组合,记录 model.training、输出 requires_grad、grad_fn 和重复前向输出。
| 模块模式 | 梯度上下文 | 要核对的模式标志 | 梯度图预期 |
|---|---|---|---|
train() | 正常梯度模式 | training=True | 本练习存在可训练参数路径,应记录 |
eval() | 正常梯度模式 | training=False | 仍应记录,eval 不关闭梯度 |
train() | no_grad() | training=True | 不记录,但 Dropout 仍处于训练行为 |
eval() | no_grad() | training=False | 不记录,Dropout 处于评估行为 |
这张表是有明确前提的验收预期,不是运行结果。若所有输入和参数都不需要梯度,正常模式也不会凭空生成需要梯度的输出,不能脱离前提照抄断言。
训练模式的 Dropout 可能使重复前向不同,但两次随机结果恰好一样也可能发生;不要只用“随机两次必须不同”判定模式是否正确。也不要据 eval() 推导所有硬件、算子和自定义层都绝对可复现。实际记录还需包括层配置、随机数设置、环境与比较容差。
建议练习二:没有更新参数,状态仍可能改变
创建两份初始状态相同的 BatchNorm 模型,明确设置 track_running_stats=True,用相同且满足该层形状约束的假数据分别执行 train()+no_grad() 与 eval()+no_grad() 的前向。为避免均值恰巧相同遮住现象,夹具应选择与初始运行统计明显不同的非退化批次;同时记录运行均值、运行方差及 num_batches_tracked 的前后值。
比较前应分别复制状态快照,而不是让第二次调用接着第一次已修改的状态。验收重点是区分“没有调用 optimizer.step”与“整个模型状态未改变”:默认跟踪运行统计的 BatchNorm 在训练模式前向仍可能更新缓冲区,关闭梯度不能阻止这种状态更新。标准评估模式使用保存的运行统计。若 track_running_stats=False,评估时也使用当前批统计,需另按配置设计检查。来源:BatchNorm1d 的运行统计与配置。
记录模式切换前后的状态,继续训练前恢复 train()。自定义层可能有额外行为,因此只检查根模块的一个标志也不一定覆盖全部配置;存在单独切换的子模块时,应记录相关子模块模式。
交付时分别提供模式记录、梯度图记录和缓冲区快照,不把它们合成一句“推理成功”。限制:本节只核对文档并设计练习,未安装 PyTorch、运行代码、训练、测量准确率或评估内存与性能;不会产生提升结论。
待执行的最小代码
下面代码需要练习者准备匹配的 PyTorch/TorchVision 环境。默认不联网下载;首次准备数据时,明确把 allow_download 改为 True。训练文件和权重写入自己的练习目录,不放进网站部署包。未准备数据时产生“数据不存在”错误,应记录为环境未就绪,不能写成实验通过。
展开收起代码 · 文中示例
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
from pathlib import Path
import platform
import torch
import torchvision
from torch import nn
from torch.utils.data import DataLoader, Subset
from torchvision import datasets
from torchvision.transforms import v2
# 教学草案,未运行。固定种子只辅助本次调试,不保证跨环境完全复现。
torch.manual_seed(20261011)
allow_download = False
device = torch.device("cpu")
transform = v2.Compose([
v2.ToImage(),
v2.ToDtype(torch.float32, scale=True),
])
data = datasets.FashionMNIST(
root="data", train=True, download=allow_download, transform=transform
)
indices = list(range(8))
loader = DataLoader(Subset(data, indices), batch_size=8,
shuffle=False, num_workers=0)
x, y = next(iter(loader))
x, y = x.to(device), y.to(device)
assert tuple(x.shape) == (8, 1, 28, 28)
assert tuple(y.shape) == (8,)
assert x.dtype == torch.float32 and y.dtype == torch.int64
assert torch.isfinite(x).all()
assert bool(((0 <= x) & (x <= 1)).all())
assert bool(((0 <= y) & (y < 10)).all())
print("environment", platform.python_version(), torch.__version__,
torchvision.__version__, platform.platform(), str(device))
print("data", indices, x.shape, x.dtype, y.shape, y.dtype)
print("labels", [(int(label), data.classes[int(label)]) for label in y])
def make_model():
return nn.Sequential(nn.Flatten(), nn.Linear(28 * 28, 32),
nn.ReLU(), nn.Linear(32, 10)).to(device)
model = make_model()
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)
loss_fn = nn.CrossEntropyLoss()
before = [p.detach().clone() for p in model.parameters()]
model.train()
optimizer.zero_grad(set_to_none=True)
logits = model(x)
assert tuple(logits.shape) == (8, 10)
loss = loss_fn(logits, y)
assert loss.ndim == 0 and torch.isfinite(loss)
loss.backward()
assert all(p.grad is not None and torch.isfinite(p.grad).all()
for p in model.parameters())
grad_norms = [float(p.grad.norm()) for p in model.parameters()]
optimizer.step()
changed = [not torch.equal(old, new.detach())
for old, new in zip(before, model.parameters())]
assert any(changed)
print("one_update", float(loss.detach()), grad_norms, changed)
# eval 不会自动关闭梯度;此网络没有 Dropout/BatchNorm,不要求输出变化。
model.eval()
assert model.training is False
with torch.enable_grad():
eval_with_grad = model(x)
assert eval_with_grad.requires_grad
with torch.no_grad():
reference = model(x).clone()
assert not reference.requires_grad
out = Path("artifacts")
out.mkdir(exist_ok=True)
checkpoint = out / "fashionmnist-weights.pt"
torch.save(model.state_dict(), checkpoint)
reloaded = make_model()
state = torch.load(checkpoint, map_location=device, weights_only=True)
reloaded.load_state_dict(state)
reloaded.eval()
with torch.no_grad():
restored = reloaded(x)
torch.testing.assert_close(restored, reference, rtol=1e-5, atol=1e-6)
print("reload_max_abs_diff", float((restored - reference).abs().max()))
验收与练习交付
提交脚本、版本记录、数据准备记录、实际运行日志和一页说明。最低验收是:能说清每一维;标签类型与损失用法一致;梯度非空且有限;参数确实发生更新;相同结构重载后的输出在约定容差内相符;能用两个独立检查区分 eval 与 no_grad。任何断言失败都保留原始信息并解释,不能先删断言再报告通过。
增加三个自拟对照练习:
- 把批次从 8 改为 4,先写下各层形状预测,再修改断言核对。只改输入数量,不顺手更换模型或损失。
- 在不更新参数的副本模型上,分别做“清零后一次反向”和“重新前向但不清零的第二次反向”,记录同一个参数的梯度。解释梯度累积,随后恢复清零流程;不要直接对已释放的同一计算图重复反向。
- 把最后一层改为 9 类后尝试加载 10 类的权重,保留不匹配信息,说明为什么重载需要匹配结构。不能用忽略缺失键来伪装通过。
本练习只处理训练集中的固定小批次,不报告测试集准确率。若继续做完整训练,应另行确定训练/验证/测试划分、停止条件和评测次数,保持测试集用途独立;不能把这 8 个样本上的结果当泛化证据。
| 记录项 | 预期或填写规则 | 实际结果 |
|---|---|---|
| 运行 ID、脚本版本 | 每次唯一,能定位实际脚本 | |
| Python、torch、torchvision、系统、设备 | 填实际版本,不复制资料页版本 | |
| 数据来源、目录、样本索引 | FashionMNIST;本练习取训练集 0–7 | |
| shape、dtype、标签范围 | 按脚本断言逐项核对 | |
| 梯度与参数变化 | 保存实际数值和断言结果 | |
| 权重文件、重载容差与最大差值 | 保存文件与比较记录 | |
| eval/no_grad 两个检查 | 分别记录,不合并成一句“推理成功” | |
| 准确率、耗时、费用 | 未测量就留空,不填 0 | |
| 失败分类与人工复核 | 环境、数据、形状、梯度、重载或解释问题 |
固定种子不保证跨版本、平台或 CPU/GPU 执行完全复现,见 PyTorch Reproducibility。本页现阶段只有来源核查和教学设计,以上运行项全部未验证。
5. 完整训练前,先验收数据家族与拟合边界
适用阶段:已经理解本页的一批图片、标签、前向与参数更新,准备把练习扩展为训练、验证和最终测试。这里验收数据流程,不要求把 PyTorch 项目改用 scikit-learn。先读站内训练集、验证集与测试集;想了解文本近重复检测,再读语料清洗、去重与污染检查。
原始核查:2026-10-19 06:09 JST。原资料记录 scikit-learn 文档版本 1.9.1,未标明准确发布日期;2026-10-20 再次核对官方正文时仍显示 1.9.1。以下是待执行的验收设计,未制作数据、调用划分库、训练模型或计算模型分数。站内既有 1.7.2 源码讲解继续保留其固定版本,本节不要求安装或升级依赖。
从“新图片”说清楚要测什么
本页原先取训练集中的 8 张图片,只检查计算流程。若要进一步说明对新样本的表现,先确定“新”指什么:是同一对象的新观测,还是从未见过的原始样本家族?这两个目标可能需要不同边界。本练习选择后者,为原始样本及其已知裁剪、轻微变换或改写副本保留自定义字段 sourceFamilyId,先分配家族,再只对训练部分做增强。字段是本练习的设计,不是数据集自带身份,也不是要求所有任务按商户 ID 划分。
官方交叉验证文档说明,GroupKFold 根据调用者提供的 groups,隔离同一折中的训练组与验证组。它并不自动识别图片副本或文本改写。家族定义错误时,形式上的组不重叠仍可能留下泄漏。来源:scikit-learn 分组交叉验证。
保留 FashionMNIST 原有的官方训练/测试边界;不要把官方测试集混回训练池后重新抽样。本节的家族切分练习使用另外构造的假样本。如果发现官方边界之间存在近重复疑点,应记录审计范围和事先约定的处理规则,不能据本节文字断言该数据集已发生或已消除泄漏。
建议练习一:ID 不同,不代表来源独立
准备几个虚构家族,每个包括一份原样本和两个已知变体,保存样本 ID、家族 ID、标签以及变体与原样本的关系。先故意逐行切分,检查是否有家族跨侧;再按家族分配训练、验证、测试,并逐对检查家族集合无交集。不要只核对训练和测试,漏掉验证这一侧。
同时记录每侧的样本数、家族数和类别分布。家族不等大时,组比例不等于样本比例;类别覆盖不足应作为设计问题记录,不能为了提高报告分数而临时移动难例。
增加两个反例:一个副本只换文件名,检查文件名不同不能证明内容不同;另一个文本措辞相近但标签不同,检查不能仅凭相似度自动归并。未知近重复需要独立的候选检测和人工复核规则,不能由“已知家族无交集”推导出所有重复都已被发现。此练习尚未运行,交集和计数结果留待实际填写。
建议练习二:改变保留数据,训练参数不应跟着变
scikit-learn 的数据泄漏说明要求从训练部分学习均值、缩放、特征选择等预处理参数,其他集合仅应用既定变换。交叉验证时,每折应在该折训练部分重新拟合;正确使用 Pipeline 有助于保持这条边界,但不能修复错误家族或未来字段。来源:Common pitfalls 的 Data leakage。
用独立的虚构数值夹具固定训练部分,只改变保留测试部分的数值分布。比较两条流程:正确流程先切分再只在训练部分拟合;故意错误的对照先用全量数据拟合再切分。验收的是前者训练均值、缩放参数或特征选择结果不依赖所改的测试数值,后者的参数则可能暴露这种依赖。这不是要求真实测试集反复改动,也不预设错误流程一定得到更高模型分数。
测试集只使用已经拟合的转换,不为让均值更接近零而再次 fit。训练/验证阶段确定方案后才作保留测试;若反复按测试成绩调整方案,应明确该测试已参与开发,不能继续称完全独立的最终评测。
留下能复核边界的记录
记录数据版本、评估目标、家族定义与来源、各集合 ID 清单或可核验摘要、样本数与家族数、类别分布、种子、预处理拟合范围、模型选择所用集合以及最终测试使用记录。固定种子帮助重现某次划分,不证明划分正确或无泄漏。
限制:以上两项练习均未运行,没有性能提升结论;家族划分不能替代时间泄漏、标签来源、分布偏移及未知近重复检查。实际模型效果仍需在按协议隔离的数据上另行测量。
6. 有了固定预测记录,再验收混淆矩阵
适用阶段:已经明确训练、验证、测试的职责,能按样本 ID 对齐真实标签与预测标签。概念前置见站内准确率与类别比例及同一批结果的 micro、macro、weighted F1。这里补标签顺序、归一化与零分母的报表验收,不重复推导三种汇总公式。
原始核查:2026-10-19 12:05—12:06 JST。原资料记录 scikit-learn 文档版本 1.9.1,未标准确发布日期;2026-10-20 重新核对两份官方 API 正文时仍显示 1.9.1。本节未安装或运行评测库,未训练模型、选择阈值或产生模型准确率;下列数字都是虚构夹具的手算值。
先固定轴、类别顺序与单位
scikit-learn 的 confusion_matrix 以真实类别为行、预测类别为列。labels 同时决定行列顺序;省略时,使用在真实或预测标签中出现过的类别并排序。界面上的类别名称必须与这份顺序一致,不能只改标题而保持矩阵位置不变。来源:confusion_matrix 的定义及 labels 参数。
不使用样本权重时,normalize=None 保存样本计数;normalize='true' 按真实类别的行归一化,'pred' 按预测类别的列归一化,'all' 按全部样本归一化。这里 'true' 是字符串选项,不是布尔值 True。始终保留原始计数,比例图不能代替样本量;若使用样本权重,另行说明矩阵累计的是加权量。
classification_report 提供逐类 precision、recall、F1 和 support;macro 对各类等权,weighted 按 support 加权。零分母由 zero_division 决定如何显示,默认 'warn' 返回 0 并警告。占位值不等于这个类别已有充分评测证据。来源:classification_report 的参数与返回说明。
建议练习一:九成正确,少数类仍可能一个都没找到
构造 20 条虚构评测记录,A 类 18 条、B 类 2 条,并预先约定规则“永远预测 A”。按 [A, B] 排列,手算原始矩阵是:
| 真实类别 | 预测 A | 预测 B | 真实数量 |
|---|---|---|---|
| A | 18 | 0 | 18 |
| B | 2 | 0 | 2 |
总体准确率为 18/20=90%,B 类召回率为 0/2=0。因为没有任何预测为 B 的记录,B 类 precision 的分母为零,报告必须同时说明所用展示策略。这是人为构造记录的算术结果,不是模型实测成绩,也不是 FashionMNIST 的类别分布。
若后续与真实模型比较,两者应使用同一份固定评测集;多数类基线的类别选择依据训练资料,不能先查看保留测试集,再挑最有利的固定标签。最终同时呈现逐类数量和错误,不能只凭超过某个总体百分比宣称每类都学会。macro 等权也不自动代表实际业务价值;漏判和误判成本需由任务另定。
建议练习二:交换标签顺序后,含义也必须对齐
另用真实标签 [A,A,B,B] 与预测标签 [A,B,B,B]。按 [A,B] 顺序手算,原始计数为两行 [1,1]、[0,2];按行归一化则为 [0.5,0.5]、[0,1]。后者表示比例,不是样本数量。A 行 B 列的 1 表示 A 被错判为 B,不能反过来读。
把 labels 顺序换为 [B,A],先在纸上重新排列,再核对显示名称、行列与计数对应;不要用同一套已排序数组同时产生实现值和所谓“独立答案”。随后在标签列表里增加一个没有真实评测样本的 C 类,明确显示 support=0,检查零分母策略如何影响展示。没有 C 类真实样本就不能评价其召回表现;显示 0 或 1 都不是能力结论。
以上两项报表练习均未运行。待实际接入后,应按样本 ID 保存真实/预测标签、完整标签顺序、数据与模型版本、样本权重设置、normalize、zero_division、逐类 support 和选定汇总方式,并保留失败样本。
限制:夹具只用于核对报表实现,不代表生产分布。只有两条少数类记录时,比例很不稳定,不能外推泛化能力。模型、阈值和报告主指标的选择继续遵守测试集不参与调参的边界;实际结果未测量时留空,不填 0 冒充测量值。