(二十四)训练与验证共用一轮读取流程
现在把前面的批次、模型、损失和更新组合为一个函数。
# 项目文件:training_project/src/readingtorch/engine.py
import json
import math
from pathlib import Path
import torch
from torch import nn
from .checkpoint import load_checkpoint, save_checkpoint, snapshot
from .data import load_bundle, make_loader
from .models import build_model
def one_epoch(model, loader, loss_fn, device, task, y_scale=1.0, optimizer=None):
training = optimizer is not None
model.train(training)
total, count, correct = 0.0, 0, 0
for x, y in loader:
x, y = x.to(device), y.to(device)
# 训练建立计算图,验证直接计算;模式切换和梯度开关各自设置。
with torch.set_grad_enabled(training):
if training:
optimizer.zero_grad(set_to_none=True)
output = model(x)
loss = loss_fn(output, y)
if not torch.isfinite(loss):
raise ValueError("损失出现了 NaN 或无穷值,请检查输入与学习率")
if training:
loss.backward()
optimizer.step()
total += loss.item() * len(x) # 按样本数加权,末批较小时仍统计正确。
count += len(x)
if task == "images":
correct += (output.argmax(1) == y).sum().item()
result = {"loss": total / count}
if task == "images":
result["accuracy"] = correct / count
else:
result["rmse"] = math.sqrt(result["loss"]) * y_scale
return result传入优化器时做训练,不传时做验证。model.train(training) 设置层行为,set_grad_enabled(training) 决定是否为本次计算记录求导信息;两者在函数里分别写出。
loss.item() * len(x) 把本批平均损失还原为本批损失和,所有批读完再除以总样本数。例如两批大小分别为 2、1,平均损失分别为 1、9,整轮应为 (1×2+9×1)/3=11/3,直接平均两批会错算成 5。
分类准确率为“判断正确的样本数除以样本总数”。回归的损失来自标准化目标,开方得到标准化 RMSE 后,再乘训练目标标准差 y_scale,就回到页数单位。