目录
[一、模型参数可视化:像 Keras 一样 "看透" 每一层](#一、模型参数可视化:像 Keras 一样 “看透” 每一层)
[二、优化器与训练循环:细节里藏着 "收敛密码"](#二、优化器与训练循环:细节里藏着 “收敛密码”)
[1. 优化器:选 SGD 还是 Adam?](#1. 优化器:选 SGD 还是 Adam?)
[2. 训练循环:"梯度清零" 是重中之重](#2. 训练循环:“梯度清零” 是重中之重)
[四、结尾:调参是个 "慢功夫",但每一步都有收获](#四、结尾:调参是个 “慢功夫”,但每一步都有收获)
给 CNN "做体检"!PyTorch 模型参数可视化 + 训练调优实战
今天接着折腾 CIFAR-10 图像分类的项目,发现 "训完模型就丢一边" 可不行 ------ 得知道模型每一层长啥样、参数有多少,训练时损失怎么变化,这样才能针对性调优。过程中我自己写了个 "参数统计工具",还摸清了优化器和训练循环的门道,分享一下~
一、模型参数可视化:像 Keras 一样 "看透" 每一层
之前用 PyTorch 搭 CNN,只能靠print(net)
看个大概,层数一多,"每一层输入输出是啥、有多少参数、哪些能训练" 根本理不清。今天学着写了个函数,能像 Keras 的model.summary()
一样,把模型每一层的细节都列得明明白白。
核心思路是用 "钩子(hook)" 记录前向传播时的层信息,代码逻辑(加了自己的理解注释)大概是这样:
python
import collections
import torch
import torch.nn as nn
def params_summary(input_size, model):
def register_hook(module):
def hook(module, input, output):
# 提取层的类名(比如Conv2d、Linear)
class_name = str(module.__class__).split('.')[-1].split("'")[0]
# 给每层编个号,方便区分
module_idx = len(summary)
m_key = f'{class_name}-{module_idx+1}'
summary[m_key] = collections.OrderedDict()
# 记录输入形状(批量大小设为-1,代表"任意批量")
summary[m_key]['input_shape'] = list(input[0].size())
summary[m_key]['input_shape'][0] = -1
# 记录输出形状
summary[m_key]['output_shape'] = list(output.size())
summary[m_key]['output_shape'][0] = -1
params = 0
# 统计权重参数(层如果有weight,就计算它的参数数量)
if hasattr(module, 'weight') and hasattr(module.weight, 'size'):
params = torch.prod(torch.LongTensor(list(module.weight.size())))
# 标记该层参数是否可训练
summary[m_key]['trainable'] = module.weight.requires_grad
# 统计偏置参数(层如果有bias,就加上bias的参数数量)
if hasattr(module, 'bias') and hasattr(module.bias, 'size'):
params += torch.prod(torch.LongTensor(list(module.bias.size())))
summary[m_key]['params'] = params
# 只对"非容器类"的层(比如Conv2d、Linear,不是Sequential、ModuleList)注册钩子
if not isinstance(module, nn.Sequential) and \
not isinstance(module, nn.ModuleList) and \
not (module == model):
hooks.append(module.register_forward_hook(hook))
# 生成一个随机张量,用于触发前向传播(方便记录每层形状)
if isinstance(input_size, tuple):
x = torch.rand(1, *input_size)
else:
x = torch.rand(1, *input_size[0])
summary = collections.OrderedDict()
hooks = []
# 给模型的每个层都注册"钩子"
model.apply(register_hook)
# 执行一次前向传播,让钩子记录信息
model(x)
# 用完钩子后移除,避免影响后续训练
for h in hooks:
h.remove()
return summary
用这个函数看我之前搭的 CNN,能清晰看到每一层的细节:
- 卷积层
Conv2d-1
:输入是(3, 32, 32)
(CIFAR-10 的彩色图),输出是(16, 28, 28)
,参数有3×16×5×5 + 16 = 1216
个; - 池化层
MaxPool2d-2
:不改变通道数,只把特征图尺寸缩到(16, 14, 14)
,没有可训练参数; - 最后全连接层
Linear-5
:输入是1296
维(前面卷积、池化后展平的结果),输出是10
类(对应 CIFAR-10 的 10 个类别),参数有1296×10 + 10 = 12970
个。
这下对模型 "长啥样" 心里有底了,调参时也能更有针对性 ------ 比如知道哪层参数多,会不会容易过拟合。
二、优化器与训练循环:细节里藏着 "收敛密码"
训练模型的核心是 "优化器" 和 "训练循环",今天把这部分的细节扒得更透了。
1. 优化器:选 SGD 还是 Adam?
这次我用的是带动量的 SGD,代码如下:
python
import torch.optim as optim
LR = 0.001
criterion = nn.CrossEntropyLoss() # 分类任务常用交叉熵损失
optimizer = optim.SGD(net.parameters(), lr=LR, momentum=0.9)
lr=0.001
:学习率是个 "玄学但关键" 的参数 ------ 太大,损失会 "上蹿下跳" 不收敛;太小,训练速度慢得像蜗牛;momentum=0.9
:动量的作用是让梯度更新更 "顺滑",避免在局部极小值附近来回震荡(想象成 "滚下坡", momentum 让下坡更流畅)。
之前我也试过把优化器换成 Adam(就是代码里注释掉的optimizer = optim.Adam(...)
),发现 Adam 前期收敛更快,但后期 SGD 加动量的稳定性更好。所以像 CIFAR-10 这种小数据集,用 SGD + 动量,调好了学习率也很能打~
2. 训练循环:"梯度清零" 是重中之重
训练循环的模板代码大家都见过,但optimizer.zero_grad()
这行极易被忽略,我就踩过坑:
python
for epoch in range(10): # 总共训练10轮
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
# 取出数据,放到设备(CPU/GPU)上
inputs, labels = data
inputs, labels = inputs.to(device), labels.to(device)
# 梯度清零!!!每次迭代都要清,否则梯度会累加
optimizer.zero_grad()
# 前向传播 → 计算损失 → 反向传播 → 更新参数
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 打印损失,观察训练趋势
running_loss += loss.item()
if i % 2000 == 1999: # 每2000批打印一次平均损失
print(f'[{epoch + 1}, {i + 1}] loss: {running_loss / 2000:.3f}')
running_loss = 0.0
踩坑提醒 :有一次我忘了写optimizer.zero_grad()
,结果损失值 "越训越大",模型直接发散了。原来 PyTorch 的梯度会自动累加,所以每次迭代必须手动清零!
三、训练观察:从损失曲线看模型学习状态
训完 10 轮,损失从最开始的2.100
慢慢降到1.3
左右,说明模型在 "学习",但离 "学好" 还有距离:
- 损失下降的趋势是对的,但速度不算快(可能学习率还能调大一点,或者训练轮数再加多一些);
- 如果损失降到一定程度后 "不动了",说明模型可能过拟合,或者学习率太小,卡在局部最优里出不来。
后续我打算试试这些优化方向:
- 把训练轮数加到 20 轮,看损失能不能继续下降;
- 用 TensorBoard 把损失曲线画出来,更直观地分析训练过程;
- 对模型结构做微调(比如加 BatchNorm 层,让训练更稳定)。
四、结尾:调参是个 "慢功夫",但每一步都有收获
今天最大的感受是:深度学习不是 "跑个代码等结果",而是要看懂模型、看懂训练过程。自己写参数可视化工具,能明白每一层的作用;盯着损失曲线调优化器和学习率,能摸清模型 "学习的节奏"。
接下来继续折腾,争取把 CIFAR-10 的准确率再提一提~如果有同样在调参的朋友,欢迎分享你们的 "稳准狠" 调参技巧~