ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CNN从原理到实践:手写Python卷积网络与调参

CNN从原理到实践:手写Python卷积网络与调参 简介面向机器学习与深度学习初学者的PDF教程系统讲解用Python和TensorFlow从零实现卷积神经网络CNN的完整过程。内容覆盖MNIST数据读取、权重初始化、卷积层与池化层设计、ReLU激活、全连接层Softmax分类、Dropout防过拟合及交叉熵损失函数并附有可直接运行的代码段与训练、准确率评估流程。代码中的每个模块均配有注释与参数说明便于学习者逐行复现并理解网络结构。资源为单个PDF文档体积仅56KB内容精炼易读适合随时查阅文档在代码基础上还给出了增大训练集与增大卷积核数的多组对比实验直观展示数据量与网络结构对模型性能的影响帮助读者避开过拟合与欠拟合陷阱。该资源已有3760人学习适合需要快速上手CNN实战、完成图像识别小项目或课程设计的学习者参考。1. 别急着调参Python 里跑 CNN 前先弄清这张网络到底在算什么很多人第一次在 Python 里写卷积神经网络都以为难点在模型结构。真上手后你会发现把卷积、池化、全连接堆起来的代码不到五十行真正卡住你的是数据怎么进网络、张量形状怎么对齐、归一化参数怎么设。CNN 处理的是带空间结构的图像数据核心是“用小的卷积核在整张图上滑动提取局部特征”而代码落地的关键是把滑动过程表示成矩阵运算。这篇笔记针对想用 Python 把 CNN 从原理跑到落地的人先用手写实现拆开卷积网络的黑匣子再给一套能改的 PyTorch 骨架最后把参数设置和踩坑经验放在一起说。读完你不仅能把 MNIST 这类任务跑通也知道失败的时候该往哪个方向查。2. 从零手写卷积层用 NumPy 拆解 CNN 的四个核心组件2.1 卷积层与 im2col把滑窗计算变成矩阵乘法手写卷积层最常见也是最容易理解的做法是 im2col把输入图像上每一个卷积窗口对应的像素取出来排成一行把所有窗口排成矩阵再跟卷积核做矩阵乘法。这个方法比多层 for 循环快得多也是很多推理引擎真正在干的事。import numpy as np def im2col(img, kernel_h, kernel_w, stride1, pad0): 将输入图像展开为卷积窗口矩阵 img: (C, H, W) 的输入 返回: (窗口数, C * kernel_h * kernel_w) C, H, W img.shape out_h (H 2 * pad - kernel_h) // stride 1 out_w (W 2 * pad - kernel_w) // stride 1 img_padded np.pad(img, ((0, 0), (pad, pad), (pad, pad)), modeconstant) cols [] for h in range(0, out_h): for w in range(0, out_w): window img_padded[:, h * stride:h * stride kernel_h, w * stride:w * stride kernel_w] cols.append(window.reshape(-1)) return np.array(cols).T def conv_forward(img, kernel, bias, stride1, pad0): kernel: (C_out, C_in, k_h, k_w) 返回: (C_out, out_h, out_w) C_out, C_in, k_h, k_w kernel.shape cols im2col(img, k_h, k_w, stride, pad) # (C_in*k_h*k_w, 窗口数) ker kernel.reshape(C_out, -1) # (C_out, C_in*k_h*k_w) out ker cols bias.reshape(-1, 1) # 矩阵乘法完成全部窗口 return out.reshape(C_out, -1, -1) # 测试一张 1x4x4 的输入两个 3x3 卷积核 img np.random.randn(1, 4, 4) kernel np.random.randn(2, 1, 3, 3) bias np.zeros(2) out conv_forward(img, kernel, bias, stride1, pad0) print(输出形状:, out.shape)这里 im2col 返回的矩阵行是输入通道乘以窗口面积的展开列是所有滑窗位置。conv_forward 把卷积核也展开成二维矩阵一次 运算就把所有窗口的结果算完了。这个写法里 stride 和 pad 的公式是关键out_h (H 2 * pad - kernel_h) // stride 1。写代码实现时经常在这两个参数上翻车尤其是 pad 大于 1 时np.pad 的元组参数顺序对应输入维度对 (C, H, W) 的结构中间两个元组分别管 H 和 W写反了就会出现输出尺寸对不上。注意上面的卷积没有处理 batch 维度单张图够用。跑批量训练时需要在外面套一层循环或者把 (B, C, H, W) 的输入按 batch 展开追求性能就直接用框架实现手写版本更多是用来理解形状变化。2.2 池化层与激活函数下采样为什么稳ReLU 为什么活池化层做的事情是下采样把特征图分成小块每块取最大值或平均值从而减少参数、增大感受野、带来一定的平移不变性。CNN 代码实现里最常见的池化是 2×2、步长为 2 的最大池化输出尺寸直接减半。def max_pool2x2(img): 对 (C, H, W) 输入做 2x2 最大池化步长为 2 C, H, W img.shape out_h, out_w H // 2, W // 2 out np.zeros((C, out_h, out_w)) for c in range(C): for i in range(out_h): for j in range(out_w): out[c, i, j] img[c, i*2:i*22, j*2:j*22].max() return out这段代码逻辑很简单但有两个细节值得注意。一是 H 或 W 为奇数时边缘会被直接丢弃这就是为什么很多经典网络在进入池化前会用 padding 把特征图凑成偶数。二是最大池化在反向传播时只把梯度回传给窗口内最大值的位置其余位置梯度为 0如果某个窗口内最大值长期集中在一个位置其他位置的权重很难被更新。这也是为什么现代网络里池化层用得越来越少更多用步长为 2 的卷积做下采样。激活函数方面CNN 代码实现里现在几乎都是 ReLUmax(0, x)。它的优势不是精度而是梯度稳定。sigmoid 在输入绝对值较大时梯度趋近于 0多层反向传播一乘浅层几乎收不到梯度ReLU 在正区间梯度恒为 1让深层网络能真正训练起来。def relu(x): return np.maximum(0, x) def relu_grad(x): # 反向传播要用正数位置梯度为 1其余为 0 return (x 0).astype(np.float32)ReLU 也不是没有坑。输入全为负时输出恒为 0梯度也恒为 0一旦某层的权重初始化导致大量神经元输出为负这些神经元就“死”了。常见做法是用 He 初始化按 fan_in 计算方差而不是默认的均匀分布随机初始化。2.3 反向传播手写一次梯度才知道框架替你扛了什么很多人以为反向传播很玄学其实它就是链式法则。手写代码时最直观的思路是前向保存每一层的中间结果反向从损失出发逐层把梯度传回去。这里给出全连接层加 softmax 交叉熵的最小示例它和卷积层反向拼接后就是一个能训练的网络。def softmax_loss_grad(logits, labels): logits: (N, num_classes) 未过 softmax 的原始输出 labels: (N,) 类别索引 返回: 损失、logits 的梯度 N logits.shape[0] exp_logits np.exp(logits - logits.max(axis1, keepdimsTrue)) probs exp_logits / exp_logits.sum(axis1, keepdimsTrue) loss -np.log(probs[np.arange(N), labels] 1e-8).mean() dlogits probs.copy() dlogits[np.arange(N), labels] - 1 dlogits / N return loss, dlogits这里减掉 logits.max 是为了防止 exp 溢出这是新手最容易翻车的地方。交叉熵损失对 logits 的梯度有个非常漂亮的结论softmax 概率减去 one-hot 标签再除以 N。写代码时只要记住这一点全连接层的反向就只是转置乘梯度。卷积层的反向要处理 im2col 的逆操作把梯度按窗口位置累加回原图。这也是手写 CNN 代码实现里最绕的一段需要对每个窗口位置维护一份索引映射。我的建议是第一次手写不要追求高性能直接用 im2col 缓存窗口坐标反向时把梯度填回对应位置即可。框架存在的意义就是让你不必每次都写这些。2.4 最小前向验证用一张图走通全流程把上面几段拼起来做一个输入单张 1×8×8 图像的二分类前向。这里把卷积、ReLU、池化、全连接顺序串起来确认每一层输出形状符合预期这是所有 CNN 代码实现的第一步验证。np.random.seed(0) img np.random.randn(1, 8, 8) # 单通道 8x8 kernel np.random.randn(4, 1, 3, 3) * 0.1 # 4 个输出通道 bias np.zeros(4) h1 relu(conv_forward(img, kernel, bias, stride1, pad1)) # (4, 8, 8) p1 max_pool2x2(h1) # (4, 4, 4) flatten p1.reshape(-1) # 64 w np.random.randn(64, 2) * 0.01 logits flatten w # (2,) print(各层形状:, h1.shape, p1.shape, flatten.shape, logits.shape)这一段代码每一次输出形状都可以用公式验算卷积 pad1 保持尺寸不变池化减半最后拉平接全连接。如果某一个尺寸和你预期不一致问题通常出在 out_h / out_w 的计算公式上而不是网络本身。到这里一个最小 CNN 前向就算落地了。反向传播把全连接和卷积的梯度回传补齐后就能用梯度下降更新权重跑 MNIST。但手写版本调参和排错的时间成本偏高实际项目里大家几乎都用框架。下一章给你一套能直接改的 PyTorch 骨架把训练、验证、模型保存一次性跑通。3. 用 PyTorch 搭 CNN从环境准备到训练循环的三段式骨架3.1 环境准备python 安装、vscode 配置、依赖包清单在动手写 PyTorch 代码之前先把 Python 环境理顺。常见做法是用 conda 或 python venv 建一个独立环境避免把系统 Python 搞乱。这里给出一套最小命令。conda create -n cnn python3.10 -y conda activate cnn pip install torch torchvision matplotlib numpy如果你用的是 vscode装好 Python 插件后在命令面板里选解释器指向刚才创建环境的 python 路径即可。这一步没有技术含量但对新手来说是第一个坑torch 安装后 import 报错基本是环境没选对而不是包真的没装。验证安装是否正常用一段很短的代码就够了import torch import torchvision print(torch.__version__) print(torch.cuda.is_available())CPU 环境也能跑 CNN只是训练慢。注意 torchvision 和 torch 的版本要配套pip 安装时最好一起装不要分开装不同版本。我在实际项目里踩过的最深的一个坑是 torch 1.13 配了 torchvision 0.15 导致数据集加载报错所以这里特别提醒版本配套问题。3.2 Dataset 与 DataLoader归一化参数不能乱拍PyTorch 里数据加载的标准动作是定义 Dataset 子类重写len和getitem再用 DataLoader 分 batch、打乱、并行加载。对于 MNIST 和 Fashion-MNISTtorchvision 直接提供现成数据集但归一化参数值得多说两句。import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_data datasets.MNIST(./data, trainTrue, downloadTrue, transformtransform) train_loader DataLoader(train_data, batch_size64, shuffleTrue, num_workers2) x, y next(iter(train_loader)) print(x.shape, y.shape, x.dtype)Normalize 里的均值和标准差是数据集的统计值不是随便拍的。MNIST 官方给的是 0.1307 和 0.3081如果换了数据集第一件事就是用代码算一遍均值方差否则梯度下降会很不稳定。对于 Fashion-MNIST均值约 0.2860标准差约 0.3530可直接用。ToTensor 会把 PIL Image 转成 0~1 的 float32 tensor并自动把 HWC 变成 CHW。这里也藏着 CNN 代码实现最常见的低级错误如果数据是 numpy 数组你得自己保证通道维在最前面torch 不会帮你判断语义。DataLoader 的三个参数要调好batch_size 决定梯度更新频率shuffle 训练集必须为 Truenum_workers 在 Windows 上容易出问题可以先设 0跑通后再调大。3.3 定义网络结构三卷积两全连接的 LeNet 风格这里给出一个能直接训练 MNIST 的 CNN结构沿用 LeNet-5 的设计思路两个卷积块加一个全连接头。注释里标明每层输出的形状变化方便你改成自己的数据集时推算输入尺寸。import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes10): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 16, kernel_size3, stride1, padding1), # 1x28x28 - 16x28x28 nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 16x14x14 nn.Conv2d(16, 32, kernel_size3, stride1, padding1), # 32x14x14 nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 32x7x7 ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(32 * 7 * 7, 128), nn.ReLU(inplaceTrue), nn.Linear(128, num_classes) ) def forward(self, x): return self.classifier(self.features(x))这个网络的参数计算很容易验证第一层卷积权重是 1×16×3×3加 16 个 bias线性层输入维度是 32771568输出 128。用 summary 或逐层打印 shape 都能确认。把网络定义写成 nn.Sequential 的嵌套好处是结构一目了然坏处是如果你想在中间插入残差连接、跳层就得改成显式写法。在 ResNet 这类结构里更常见的写法是单独定义init里的层在 forward 里手工拼接。3.4 训练循环epoch、batch、学习率、损失曲线的观察方法CNN 代码实现里训练循环是最好抄也是最少被讲清楚的部分。一个规范的 PyTorch 训练循环包括模型 train 模式、优化器清零梯度、前向、计算损失、反向、更新外加每若干步打印一次训练信息。import torch.optim as optim model SimpleCNN() optimizer optim.Adam(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss() def train_one_epoch(model, loader, optimizer, criterion): model.train() total_loss, correct, total 0.0, 0, 0 for x, y in loader: optimizer.zero_grad() out model(x) # 前向 loss criterion(out, y) # 交叉熵损失 loss.backward() # 反向 optimizer.step() # 更新 total_loss loss.item() * x.size(0) correct (out.argmax(1) y).sum().item() total y.size(0) return total_loss / total, correct / total loss, acc train_one_epoch(model, train_loader, optimizer, criterion) print(fepoch 1 | loss {loss:.4f} | acc {acc:.4f})这里值得解释的是 CrossEntropyLoss 自带 softmax模型最后一层不要再用 softmax否则会得到错误结果。另外 optimizer.zero_grad() 必须在 loss.backward() 之前调用否则梯度会跨 batch 累加典型表现是 loss 在下降但曲线噪声很大。观察训练是否正常一是看每个 epoch 的训练 loss 是否单调下降二是第一轮结束后跑一次验证集对比训练和验证准确率。如果训练 loss 下降但验证不涨大概率过拟合如果 loss 完全不动先把学习率调大一个量级试一次别的先别动。4. CNN 必调的 6 个参数卷积核、层数、通道数、步长、填充、学习率与 Batch Size4.1 卷积核大小与层数堆叠 3×3 为什么比单个 5×5 划算一维卷积神经网络与二维 CNN 在很多原理上是相通的卷积核大小决定了一个输出点能“看到”的输入范围也就是感受野。两个 3×3 堆叠感受野是 5×5但参数量是 2×918比单个 5×5 的 25 少而且中间多了一次非线性激活表达能力反而更强。这是 VGG 之后主流网络普遍用 3×3 的原因。有种说法是卷积核越大越好实际项目中并不成立。大于 7×7 的卷积核参数量爆炸在小数据集上非常容易过拟合。如果你处理的是时序数据可以考虑 kernel_size5 或 7对图像来说kernel_size3 是最稳的起点。第一次实验用 3×3后面想提升精度优先加深层数而不是加大卷积核。4.2 通道数与下采样策略步长 2、填充到底怎么选通道数是一个黑匣子大多数从业者都是靠经验选第一层 16 或 32每经过一次下采样翻倍最后卷积层通常在 256~512 之间。这个策略的依据是空间分辨率减半时用更多通道补偿信息损失。通道数直接决定显存占用。一个 32×32 输入第一层 32 通道特征图是 32×32×32约 32K 个浮点数到第三层 128 通道、8×8 空间约 8K。真正占显存的大头是卷积层输出的特征图和反向传播保存的中间变量所以调参时先看显存是否够再决定要不要加通道。填充的作用是控制特征图边缘信息和输出尺寸。公式是 out (H 2*pad - kernel) // stride 1。想让卷积不改变尺寸pad 通常取 (kernel - 1) // 2这就是为什么 kernel 为奇数在工程上更顺手。步长每增加 1输出约减半计算量显著下降步长为 2 的下采样比 MaxPool 保留更多信息但参数量和计算量也更大。# 两种下采样的对比写法 pool_down nn.Sequential(nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1)) conv_down nn.Sequential(nn.Conv2d(64, 128, 3, stride2, padding1))两种写法输出尺寸相同但 conv_down 不会像 MaxPool 那样抹掉所有非最大值的信息。4.3 学习率与 Batch Size最明智的调参顺序调参不是全参数一锅乱调更稳妥的做法是先定结构再定学习率最后微调 batch size 和正则化。学习率是最重要的超参数Adam 默认 1e-3 是合理的起点如果 loss 震荡剧烈降到 3e-4 试一次如果 loss 几乎不动先升到 3e-3确认有下降后调回来。Batch size 影响的是梯度噪声。小的 batch16~32噪声大、容易跳出局部极小但收敛不稳大的 batch128~256梯度稳定、训练快但可能收敛到尖锐的局部最优点泛化略差。要加大 batch size 时学习率要同步调大原因是梯度噪声变小了可以走更大的步长。一个简单的法则是batch size 翻倍学习率也翻倍这是不知道内部细节时最可靠的近似。学习率调度器也不是非要不可。我的经验是先固定学习率跑 5 个 epoch如果 loss 还在稳步下降就不加调度如果后期震荡不收敛再加一个 cosine 或 step 调度。from torch.optim.lr_scheduler import CosineAnnealingLR scheduler CosineAnnealingLR(optimizer, T_max20, eta_min1e-5) # 每个 epoch 结束后调用 scheduler.step() 即可CosineAnnealingLR 的 T_max 设为预计的 epoch 总数eta_min 设为目标最低学习率。它的好处是前期下降慢、后期下降快比每隔固定步数除以 10 的 step 调度在分类任务上更稳。4.4 一张参数速查表整理一张可直接照抄的表按优先级排列。参数起点值调整方向常见症状学习率1e-3Adam震荡则降不降则升loss 震荡 / loss 不变卷积核3×3时序任务可试 5 或 7精度卡住层数卷积块 2~3 个小数据集别超过 5 个过拟合 / 训练太慢通道数16/32 起步翻倍显存够再翻过拟合 / OOM步长stride2 下采样大图先保持 stride2输出尺寸对不上填充pad(kernel-1)//2保持尺寸不变边缘精度差Batch size32 或 64与学习率同向调整收敛不稳或太慢这张表不是标准答案但它能帮你把 CNN 的首轮实验控制在一天内。调参过程中记录每一次改动和验证结果网络性能下降时难以定位是谁的问题多数原因不是玄学而是没有从基线逐项修改同时动了三个参数。提示每改一个参数先只跑 1 个 epoch 看趋势确认有效再继续。这样浪费的时间最少得到的结论最干净。5. 健壮性避坑与排查CNN 训练失败的五个典型现象5.1 现象损失不降反升或者从第一个 epoch 就出现 NaN原因常见有两类学习率过大导致梯度更新跨过极小点还有数据没有归一化导致特征值量级过大。NaN 更容易定位一般出在损失函数和输入数据上。解决先把学习率降到 1e-4 或 3e-4 试跑 10 个 batch检查输入数据是否在 0~1 或标准化后的合理范围。分类任务用 CrossEntropyLoss最后一层不要接 softmax回归任务用 MSELoss输出层不要接 ReLU。这两个配错loss 的数值会变得无法解释。5.2 现象训练准确率很高如 99%验证准确率只有 70%这是典型的过拟合在 CNN 里比全连接网络更隐蔽因为卷积本身有参数共享但数据量太小每类几百张照样过拟合。从 loss 曲线上看训练 loss 持续下降验证 loss 在第几个 epoch 后开始反弹就说明模型开始死记训练集了。解决优先加数据增强随机裁剪、水平翻转、颜色抖动这是最有效的正则化手段其次加 dropout放在全连接层之前比较有效最后再考虑减小模型规模。注意数据增强只在训练集做验证集只做标准化不做增强。很多人把增强加到了验证集上导致指标忽高忽低白白浪费时间。5.3 现象特征图全变成 NaN或者某一层输出恒为同一个值原因梯度爆炸或初始化不当。ReLU 网络里最常见的初始化是 He 初始化PyTorch 的 nn.Conv2d 默认初始化对深层网络来说方差偏大。特征图输出恒为同一个值通常是一整层的权重初始化全为 0或者 ReLU 把所有输入都压成了 0。解决在模型定义后显式初始化权重。def init_weights(m): if isinstance(m, nn.Conv2d) or isinstance(m, nn.Linear): nn.init.kaiming_normal_(m.weight, modefan_out, nonlinearityrelu) if m.bias is not None: nn.init.zeros_(m.bias) model.apply(init_weights)kaiming_normal_ 的 mode 建议用 fan_out它对反向传播更友好bias 全部置 0 不会影响训练因为卷积层的偏置梯度本来就比权重小得多。5.4 现象GPU 利用率很低训练速度没有随硬件升级变快原因数据加载是瓶颈网络在等 CPU 准备数据。Windows 下 num_workers 设大了反而报错很多新手直接设 0导致数据加载串行GPU 一直空转。另一个隐蔽的原因是每个 epoch 都重新做一次增强计算CPU 成了整个训练链路里的短板。解决先把 DataLoader 的 num_workers 从 2 开始调并加 pin_memoryTrue看看 GPU 利用率是否有明显提升。如果数据增强太重把增强后的结果缓存到磁盘或离线预处理后存成张量文件训练时直接读取。GPU 利用率低问题往往不在网络而在数据管道。5.5 现象同一个代码跑两次结果完全不一样甚至同样的随机种子也不一致原因PyTorch 的 CUDA 卷积算法本身有随机性某些算子如 atomicAdd在 float 类型下不保证完全确定。还有一个隐蔽的问题DataLoader 多进程下数据读取顺序不稳定。解决需要精确复现实验时在代码开头固定所有随机源。import random, numpy as np, torch random.seed(0) np.random.seed(0) torch.manual_seed(0) torch.cuda.manual_seed_all(0) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark Falsecudnn.deterministic 会降低训练速度benchmark 关闭后卷积自动调优也停了。实际项目里以精度对比为目的时开确定性追求训练速度时关闭。这也是很多框架代码里会保留开关的原因不是写给你看的是给不同使用场景准备的。注意验证集评估时记得调用 model.eval()否则 BatchNorm 和 dropout 的行为会跟训练时不一致得到的准确率会偏低的。这是最隐蔽的坑排查优先级永远排第一。6. 从“能跑”到“能交付”混淆矩阵与模型导出收尾6.1 混淆矩阵看清错在哪一类准确率只是全局指标CNN 代码实现的验收阶段更需要知道“错在哪一类”。比如数字识别里把 4 当成 9把 7 当成 1这两类错误对业务的影响完全不同。用混淆矩阵能直观看到哪一对类别最容易被混淆。from sklearn.metrics import confusion_matrix import matplotlib.pyplot as plt import seaborn as sns model.eval() all_pred, all_true [], [] with torch.no_grad(): for x, y in val_loader: out model(x) all_pred.extend(out.argmax(1).tolist()) all_true.extend(y.tolist()) cm confusion_matrix(all_true, all_pred) sns.heatmap(cm, annotTrue, fmtd, cmapBlues) plt.xlabel(predicted) plt.ylabel(true) plt.show()针对混淆集中的类别常见做法是收集该类别的错误样本单独建一个小校验集逐张看模型哪里错了——是图像本身模糊还是预处理方式和训练集不一致。这一步比盲目改网络结构有效得多。6.2 部署时导出 ONNX把模型交给生产环境训练完之后PyTorch 模型通常要转成 ONNX 才能在推理引擎或移动端跑。导出本身不算复杂坑在动态尺寸。model.eval() dummy torch.randn(1, 1, 28, 28) torch.onnx.export(model, dummy, model.onnx, input_names[input], output_names[output], opset_version12)opset_version 决定导出的算子集导出后建议用 onnxruntime 跑一遍推理对比 PyTorch 的预测结果是否一致。如果导出报错通常是你用了动态维度的层如自适应池化解决方法是把 dummy 输入换成代表性尺寸并固定或显式声明 dynamic_axes 控制允许变化的维度。我最早用 Python 做 CNN 时花了整整两个晚上排查一个“验证准确率停滞在 90%”的问题最后发现是测试集评估时忘了关 model.eval()BatchNorm 和 dropout 的行为全乱了。从那以后我给自己定了个习惯训练和验证代码永远分开写model.train() 和 model.eval() 必须成对出现在函数开头。这个习惯后来帮我省了很多次定位问题的时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表