☰
一周CNN一周ResNet:卷积神经网络与残差网络源码实战拆解
2026/9/28 2:56:20 网站建设 项目流程

简介:面向深度学习初学者与进阶者,这份Python项目包围绕CNN和ResNet的简易搭建展开,既解释ResNet是CNN扩展改进版本的概念,也通过两周目录逐步演示CNN基础组件与ResNet残差块的设计思路,帮助读者理解卷积、池化、跳跃连接等关键机制。zip压缩包共24个文件,约25.27MB,以H5数据集、Python脚本、Pyc缓存及XML/IML项目配置文件为主,覆盖数据加载、模型定义与训练调用等环节,便于直接运行和二次修改。已有1099人在CSDN学习或下载。配套内容分为1第一周、2第二周两个阶段:第一周侧重基础CNN模型搭建,第二周进入ResNet实现,并提供数据集与工具函数,可完成图像分类的完整训练流程。对希望快速上手TensorFlow/PyTorch,或想梳理CNN到ResNet演进逻辑的读者而言,这份代码包具备清晰的学习路径与实操价值。

1. 用一周把CNN跑通、再用一周吃透ResNet:这份Python源码包的真实拆解

先回答那个被问烂了的问题:ResNet是CNN吗?是,但不完全是。ResNet的全称是Residual Network,它没有跳出卷积神经网络的框架,而是在CNN的基础上加了跳跃连接(skip connection),让信息可以绕过一两个卷积层直接往后传。拆这份源码包之前,我建议你先把这个关系钉死在脑子里——别把ResNet当成CNN的对立面,它是CNN的一个进化版本。

这份资源是典型的“两周一疗程”:第一周目录叫1第一周,里面是CNN的基础实现,配套cnn_utils.py和手写符号数据集train_signs.h5;第二周目录叫2第二周,里面是ResNet的搭建,配套resnets_utils.py、kt_utils.py和两个数据集。适合谁?两类人:一类是刚看完卷积层概念想动手复现的初学者,另一类是已经跑过现成模型、但还没自己从零写过forward和backward的进阶者。它不能让你直接上手工业级图像识别,但能让你把CNN和ResNet从“黑匣子”变成“看得见每一层张量形状的透明盒子”。

2. 简易CNN搭建:卷积层、池化层与全连接层是怎么协同的

2.1 为什么先搭CNN再上ResNet:从feature map到分类输出的完整链路

CNN的套路说穿了就四步:卷积提特征、激活加非线性、池化压尺寸、全连接做分类。第一周项目让你干的活,就是把这几步串成一个能跑通的手写符号识别模型。

先看输入输出链路。输入是一张64x64x3的RGB图像,经过第一层卷积后变成64x64x8的特征图——这里通道数从3变成8是卷积核数量决定的,每个卷积核扫一遍图像产生一个通道。接着过ReLU激活,再经过池化层把尺寸缩小一半变成32x32x8。第二层卷积再把通道数往上抬,比如从8变16,尺寸继续缩。到最后,特征图被拉平成向量,送进全连接层,输出层用softmax给出每个类别的概率。这条链路就是CNN的骨架。

搭建顺序上有个多年踩出来的经验:先把网络结构写在一个函数里,前向传播能算出shape,再去想训练循环。很多新手一上来就写训练代码,结果卷积层参数一错,梯度更新时张量形状对不上,整个训练直接崩。第一周目录里的cnn_utils.py就是干这个的——它把初始化、前向传播、反向传播封装成工具函数,你只要按顺序调用。

下面是这一周的典型结构参数表,对应64x64x3输入的手写符号识别任务:

层类型输出形状参数说明
输入层(64, 64, 3)原始图像
Conv1 + ReLU(64, 64, 8)8个3x3卷积核,padding='same'
MaxPool1(32, 32, 8)2x2池化,步长2
Conv2 + ReLU(32, 32, 16)16个3x3卷积核,padding='same'
MaxPool2(16, 16, 16)2x2池化,步长2
Flatten(4096,)16x16x16=4096
Dense + ReLU(128,)全连接层128个神经元
Dense + Softmax(6,)6类符号输出

2.2 第一周代码拆解:cnn_utils.py里的初始化与前向传播

第一周项目的核心工具文件是cnn_utils.py。这里我按常见课程项目的写法拆解它的骨架——它负责把网络每一层的权重初始化成合理分布,并定义前向传播的计算顺序。

# 参考写法:cnn_utils.py 中的参数初始化部分 def initialize_parameters(): # 第一层卷积:输入3通道,输出8通道,卷积核3x3 W1 = np.random.randn(8, 3, 3, 3) * np.sqrt(2.0 / (3 * 3 * 3)) b1 = np.zeros((8, 1)) # 第二层卷积:输入8通道,输出16通道 W2 = np.random.randn(16, 8, 3, 3) * np.sqrt(2.0 / (8 * 3 * 3)) b2 = np.zeros((16, 1)) # 全连接层:输入4096,输出128 W3 = np.random.randn(128, 4096) * np.sqrt(2.0 / 4096) b3 = np.zeros((128, 1)) # 输出层:输入128,输出6个类别 W4 = np.random.randn(6, 128) * np.sqrt(2.0 / 128) b4 = np.zeros((6, 1)) parameters = {"W1": W1, "b1": b1, "W2": W2, "b2": b2, "W3": W3, "b3": b3, "W4": W4, "b4": b4} return parameters

这段代码里需要特别说明的是权重初始化方式:np.sqrt(2.0 / n)是He初始化,专门配合ReLU激活函数。它让权重方差和上一层神经元数量成反比,避免信号在逐层传播时指数级放大或缩小。我见过不少项目直接用np.random.randn裸初始化,结果ReLU之后特征值越来越大,没几个epoch就溢出了。参数W1的形状(8, 3, 3, 3)里,第一个8是输出通道数,第二个3是输入通道数,后两个3是卷积核尺寸——这个顺序在numpy实现里常见,但在PyTorch里却是(输入, 输出, 核高, 核宽),换框架时最容易搞混。

前向传播这边,核心是按顺序执行卷积、ReLU、池化、再卷积、再池化、展平、全连接:

# 参考写法:cnn_utils.py 中的前向传播 def forward_propagation(X, parameters): W1, b1, W2, b2 = parameters["W1"], parameters["b1"], parameters["W2"], parameters["b2"] # 第一层:卷积 + ReLU + 池化 Z1 = conv_forward(X, W1, b1, stride=1, padding=1) A1 = relu(Z1) P1 = pool_forward(A1, pool_size=2, stride=2) # 第二层:卷积 + ReLU + 池化 Z2 = conv_forward(P1, W2, b2, stride=1, padding=1) A2 = relu(Z2) P2 = pool_forward(A2, pool_size=2, stride=2) # 展平 + 全连接 P2_flat = flatten(P2) Z3 = np.dot(W3, P2_flat) + b3 A3 = relu(Z3) Z4 = np.dot(W4, A3) + b4 A4 = softmax(Z4) cache = (Z1, A1, P1, Z2, A2, P2, P2_flat, Z3, A3, Z4) return A4, cache

这里的conv_forward和pool_forward是cnn_utils里封装好的底层函数。前向传播的结果A4是6个类别的概率分布,cache里记录了每一层的中间结果——这些中间值不能丢,因为反向传播计算梯度时要靠它们做链式求导。我拆这个项目时反复提醒自己:前向传播不只是为了算loss,更大价值是为backward准备缓存。如果哪层忘了往cache里塞数据,反向传播到那层必然报KeyError。

2.3 test.py怎么跑:数据加载、迭代训练与损失曲线

第一周目录里的test.py是训练入口。它做的事情可以用一条流水线概括:加载train_signs.h5-> 做归一化和one-hot编码 -> 初始化参数 -> 迭代训练 -> 打印损失。

# 参考写法:test.py 的核心训练循环 import h5py import numpy as np # 1. 加载 h5 数据集 def load_signs_dataset(): train_dataset = h5py.File('datasets/train_signs.h5', "r") train_x = train_dataset['train_set_x'][:] train_y = train_dataset['train_set_y'][:] # 归一化:像素值从0-255缩放到0-1 train_x = train_x / 255.0 # 转成 (样本数, 通道, 高, 宽) 布局 train_x = train_x.transpose((0, 3, 1, 2)) # one-hot编码标签 train_y = one_hot(train_y, 6) return train_x, train_y # 2. 训练循环 X, Y = load_signs_dataset() parameters = initialize_parameters() for epoch in range(100): A4, cache = forward_propagation(X, parameters) cost = compute_cost(A4, Y) gradients = backward_propagation(X, Y, cache, parameters) parameters = update_parameters(parameters, gradients, learning_rate=0.01) if epoch % 10 == 0: print(f"epoch {epoch}, cost = {cost:.4f}")

训练循环的四个核心动作——前向、算cost、反向、更新参数——对应深度学习的全部底层逻辑。learning_rate=0.01是课程项目的常见默认值,实际跑的时候可以试0.001到0.1之间的几个量级。损失曲线如果前10个epoch从1.8往下降到0.8左右,说明网络在学习;如果原地不动甚至上升,先别调网络结构,优先检查数据归一化有没有做——train_x / 255.0这行就是血泪经验,忘记归一化时,大数值输入经过卷积后激活值很容易落在ReLU的饱和区,梯度直接消失。

提示:第一周完整项目里还有cnn_utils.cpython-37.pyc这个编译缓存文件,它只是Python的字节码缓存,不是项目代码的一部分,可以忽略。

3. 从CNN到ResNet:残差块怎么解决梯度消失,ResNet算不算CNN

3.1 网络加深为什么会退化:梯度消失与恒等映射

把CNN的层数从几层加到几十层,理论上特征提取能力应该更强,实际训练时却会出现反直觉的现象:层数越深,训练误差反而越高。这不是过拟合,因为训练集上的误差都下不去。吴恩达在课程里管这叫“退化问题”(degradation problem),它的根源一部分是梯度消失——反向传播时梯度要逐层回传,每经过一层卷积和激活函数,梯度范数就被乘上一个小于1的因子,几十层乘下来梯度就趋近于零,前面层的权重几乎收不到更新信号。

另一部分原因是恒等映射难学。设想网络已经收敛到最优解附近,此时理想的行为是让后面若干层退化成恒等映射——输入是什么就输出什么,不添乱。但让卷积层通过调权重去拟合恒等映射是件困难的事,因为卷积本质是特征变换,除非权重调到特定值,否则F(x) = x很难精确满足。ResNet的思路是不再要求底层网络去拟合H(x) = x,而是拟合残差F(x) = H(x) - x。当理想输出就是x时,残差趋近于0即可,这比让卷积层的输出直接等于输入要容易得多。

残差块的结构很直白:输入x经过两层卷积得到F(x),然后F(x) + x再经过ReLU输出。这个加法操作就是跳跃连接,也叫shortcut。它让梯度在反向传播时多了一条高速公路——梯度可以直接从输出层传到输入层而不经过卷积层,从而保住前面层的更新力度。这就是为什么ResNet能做到152层还能正常训练的原因。

3.2 残差块与跳跃连接:shortcut的实现与维度匹配

第二周项目里的核心是搭残差块。下面用PyTorch风格写一个基础残差块,这是课程项目常见的实现方式:

# 参考写法:PyTorch风格的BasicBlock import torch.nn as nn class BasicBlock(nn.Module): def __init__(self, in_channels, out_channels, stride=1): super(BasicBlock, self).__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(out_channels) self.relu = nn.ReLU(inplace=True) self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False) self.bn2 = nn.BatchNorm2d(out_channels) # shortcut 维度匹配:输入输出通道数不同或尺寸减半时,用1x1卷积调整 if stride != 1 or in_channels != out_channels: self.shortcut = nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False), nn.BatchNorm2d(out_channels) ) else: self.shortcut = nn.Identity() def forward(self, x): identity = self.shortcut(x) # 输入直接走短路 out = self.conv1(x) out = self.bn1(out) out = self.relu(out) out = self.conv2(out) out = self.bn2(out) out += identity # 残差相加 out = self.relu(out) return out

几个关键点值得拆开讲。stride参数决定特征图尺寸是否减半:当stride=1时输出尺寸和输入一致,shortcut直接用恒等映射;当stride=2时特征图尺寸减半,输出通道数翻倍,此时必须用1x1卷积对shortcut做投影变换,否则F(x)和x形状对不上,加法会直接报维度错误。bias=False在卷积层配合BatchNorm时是固定做法——因为BatchNorm层本身带偏置参数,卷积层的bias会被吸收掉,留了反而浪费参数。

第二周项目会把若干个残差块串成一个完整网络:先是输入卷积层把3通道图像变成64通道,然后依次堆叠多个stage,每个stage里第一个残差块的stride=2做下采样,最后是全局平均池化加全连接输出。课程里常用的ResNet-50结构就是把这种BasicBlock换成Bottleneck(三层卷积结构),但在2第二周这个项目里,重点是先吃透BasicBlock的加法逻辑,不是盲目堆层数。

3.3 resnets_utils.py与kt_utils.py:第二周工具文件里藏了什么

第二周目录里的工具文件有两个:resnets_utils.py和kt_utils.py。它们不是网络结构代码,而是数据加载和预处理工具。

resnets_utils.py主要面向手写符号数据集,里面的核心函数是load_happy_dataset()和load_signs_dataset()。它做的事情和第一周的load_signs_dataset类似:读取h5文件、归一化、one-hot编码、调整数据布局。但第二周的数据处理多了一步——它会根据ResNet的输入要求,把图像统一resize到64x64,并确保数据格式是(样本数, 高度, 宽度, 通道)的TensorFlow布局,或(样本数, 通道, 高度, 宽度)的PyTorch布局。跑第二周代码前,先确认你的resnets_utils.py里用的布局和你的后端框架一致,这是最常见的一个坑。

kt_utils.py则是第二周人脸表情识别任务专属的工具,对应train_happy.h5数据集。happy数据集是一个二分类任务——识别笑脸和非笑脸,图像尺寸相对统一,数据量不大,总共就几百张。它的load_dataset()会返回已经归一化好的(样本数, 64, 64, 3)数组和对应的0/1标签。因为数据量小,这个任务特别适合用来验证ResNet的收敛能力:如果模型在小数据集上能快速过拟合到100%训练精度,说明前向传播、反向传播和参数更新链路是通的。

注意:resnets_utils.py里的数据加载函数通常假设项目目录结构是project/datasets/xxx.h5。如果你把h5文件放错位置,最常见的报错是FileNotFoundError: [Errno 2] Unable to open file,排查时第一反应应该是检查相对路径,而不是怀疑代码逻辑。

4. 两套实战项目跑起来:手写符号识别与人脸表情识别的完整过程

4.1 从train_signs.h5到预测结果:CNN在符号识别上的标准流程

第一周项目的完整流程是:加载train_signs.h5-> 训练CNN -> 在测试集上计算精度。这里我把流程拆成三步,每一步都能直接对照项目里的文件。

第一步是理解数据集。train_signs.h5里存的是6类手写符号,训练集样本数是1080,每张图64x64x3。h5文件本质是一个层级化的键值存储,可以用h5py直接打开查看里面的键名。第一次跑项目的人,我强烈建议先做这一步——我之前跳过了它,直接加载数据,结果把train_set_x的维度含义搞错了,以为是(1080, 64, 64, 3),实际需要transpose变成(1080, 3, 64, 64)才符合numpy后端的输入布局。一个transpose的问题,浪费了整整一个晚上。

第二步是训练。用test.py跑100个epoch,每个epoch做一次全量前向和反向传播。注意compute_cost用的是交叉熵损失,配合softmax输出。损失曲线的预期走势是:前10个epoch快速下降到1.0以下,后90个epoch慢慢逼近0.2甚至更低。如果曲线在第20个epoch就接近0,警惕过拟合——训练集精度100%不代表测试集能泛化。

第三步是验证。在测试集上计算精度是检验模型的唯一标准。

# 参考写法:测试集精度验证 def predict(X, Y, parameters): A4, _ = forward_propagation(X, parameters) predictions = np.argmax(A4, axis=0) labels = np.argmax(Y, axis=0) accuracy = np.mean(predictions == labels) return accuracy # 加载测试集 test_dataset = h5py.File('datasets/test_signs.h5', "r") test_x = test_dataset['test_set_x'][:] / 255.0 test_x = test_x.transpose((0, 3, 1, 2)) test_y = one_hot(test_dataset['test_set_y'][:], 6) accuracy = predict(test_x, test_y, parameters) print(f"Test Accuracy: {accuracy * 100:.2f}%")

这段代码的逻辑很简单但容易被忽略:np.argmax(A4, axis=0)是按列取最大概率位置。如果你的A4形状是(6, 样本数),axis=0是对的;如果代码里换成了axis=1,精度直接掉到随机水平——大约16.7%(6类均匀分布),这是个典型的“框架布局不同导致argmax方向错”的玄学问题。第一周项目里最终精度应该落在80%上下,达不到就先看归一化和布局,别急着调网络结构。

4.2 train_happy.h5上的ResNet:小数据集训练要注意什么

第二周项目的核心任务是用ResNet在train_happy.h5上做人脸笑脸二分类。数据量小是它的显著特征,训练集600张、测试集150张,每张64x64x3。这个规模下,ResNet的训练表现和第一周CNN在大数据集上的表现有明显差异。

小数据集上的首要问题是过拟合。一个只有几万参数的ResNet在600张图上,训练精度能轻松到100%,但测试集可能只有75%。第二周项目里常用的缓解手段是早停:训练过程中每10个epoch记录一次验证精度,一旦验证精度连续20个epoch不提升,就停止训练并恢复到历史最佳参数。课程项目通常把最佳精度目标定在85%左右,如果能到90%,说明残差块的实现基本没有bug。

第二个问题是学习率的选择。数据量小意味着每个batch的梯度估计噪声更大,学习率调太大模型直接发散,调太小收敛慢。第二周项目的learning_rate建议从0.001开始试。如果你用的是优化器,Adam的默认学习率就是0.001,这个值在ResNet-50级别的模型上是一个不错的起点。

# 参考写法:happy数据集上的ResNet训练配置 optimizer = torch.optim.Adam(model.parameters(), lr=0.001) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.5) criterion = torch.nn.BCEWithLogitsLoss() # 二分类用二值交叉熵 for epoch in range(60): model.train() for images, labels in train_loader: optimizer.zero_grad() outputs = model(images) # shape: (batch, 1) loss = criterion(outputs.squeeze(), labels.float()) loss.backward() optimizer.step() scheduler.step()

scheduler是学习率衰减——每30个epoch把学习率乘以0.5,让训练后期梯度更新更细腻。BCEWithLogitsLoss把sigmoid激活合并进了损失函数,数值上比“先sigmoid再算交叉熵”更稳定,这是PyTorch的官方建议做法。第二周项目里如果你看到损失函数里用了torch.nn.Functional.binary_cross_entropy,记得先确认输出有没有过sigmoid——很多翻车现场就是忘了这层。

4.3 文件目录对比:第一周和第二周代码的真实分工

拆完两个项目后,我把文件结构和用途整理成了下面这张表,方便按图索骥:

文件/目录所属周次作用建议优先级
cnn_utils.py第一周CNN的初始化、前向、反向、更新参数工具必读
test.py(第一周目录)第一周训练入口,加载signs数据集并跑训练循环必读
train_signs.h5第一周手写符号训练数据集,1080张64x64x3图直接使用
test_signs.h5第一周手写符号测试集,用于验证精度直接使用
resnets_utils.py第二周ResNet的数据加载、预处理工具必读
kt_utils.py第二周happy数据集的加载工具阅读接口即可
train_happy.h5第二周笑脸识别训练集,600张图直接使用
test_happy.h5第二周笑脸识别测试集,150张图直接使用
.idea/*.iml周次无关PyCharm项目配置文件忽略
__pycache__目录周次无关Python字节码缓存忽略

这张表最右边的优先级一列是我个人建议。.idea、misc.xml这些是PyCharm的工程配置,属于IDE环境文件,跟模型代码没半毛钱关系。__pycache__里的cnn_utils.cpython-37.pyc只是编译缓存,删除也没影响。新手拆项目时最容易犯的错就是去读这些无关文件,把自己的思路带偏。优先读cnn_utils.py和resnets_utils.py,把两个数据集的加载逻辑吃透,再回到test.py看训练循环,整个项目的脉络就清晰了。

5. 避坑指南:搭建CNN和ResNet时最容易翻车的五个坑

5.1 数据维度不匹配:height、width、channels三者的顺序错位

现象:加载train_signs.h5后直接喂给卷积层,报错Shape (1080, 64, 64, 3) is incompatible with expected (3, 64, 64)或者反过来的错。

原因:numpy后端实现里,卷积层期望的输入布局是(样本数, 通道数, 高, 宽),也就是NCHW;而h5文件里原始存储通常是(样本数, 高, 宽, 通道数),即NHWC。两种布局不统一,导致第一层卷积的张量运算直接崩溃。

解决:在数据加载后强制加一行transpose,把维度顺序显式转换。我一般在load_dataset函数里固定写死布局转换,并在函数开头用注释标明最终布局,防止以后自己忘了:

# 强制转换为 NCHW 布局 (样本数, 通道, 高, 宽) train_x = train_x.transpose((0, 3, 1, 2))

5.2 池化层输出尺寸算错:下采样让卷积层参数对不上

现象:两层卷积之间报维度错误,比如第一层池化输出(16, 16, 16),第二层卷积初始化时却按(8, 8, 8)的尺寸分配权重。

原因:池化层的stride=2会把尺寸减半,但新手在计算特征图大小时经常忽略padding的影响。比如64x64输入经过padding=1的3x3卷积,输出还是64x64;但再经过2x2池化,就变成32x32。如果手动计算时漏掉池化这步,后面全连接层的输入维度就全错了。

解决:在搭建网络时,每个stage后打印一次当前张量的shape,逐层核对。我一般会在forward_propagation里加一行调试输出:print("After pool:", P1.shape)。等全部层形状确认无误后,再删掉调试代码。别信手算,让机器告诉你真实shape。

5.3 跳跃连接维度不一致:residual与shortcut相加时报错

现象:实现ResNet残差块时,out += identity这一行报错,提示The size of tensor a (32) must match the size of tensor b (16)。

原因:残差块内部经过卷积后,输出通道数变了,或者因为stride导致特征图尺寸减半,此时原始输入x的尺寸和卷积输出F(x)不一致,直接加法必然报错。

解决:给shortcut分支补上投影操作,用1x1卷积调整通道数和尺寸,步长与残差块主路径的stride保持一致:

if stride != 1 or in_channels != out_channels: self.shortcut = nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False), nn.BatchNorm2d(out_channels) )

5.4 批归一化顺序颠倒:BN放在激活前还是激活后

现象:训练时loss下降正常,但验证集精度波动剧烈,甚至在epoch 50后反而比epoch 30还低。

原因:批归一化层的顺序不统一会导致训练和推理时的数据分布不一致。常见实践是把BN放在卷积层之后、ReLU之前。但如果你在某个残差块里写成“卷积 -> ReLU -> BN”,在另一个块里写成“卷积 -> BN -> ReLU”,模型内部的归一化分布就乱了。

解决:统一使用“卷积 -> BN -> ReLU”的顺序,并在整个项目中保持一致。各家框架的预训练模型也遵循这个约定,自己搭网络时最好沿用,否则后面做迁移学习加载预训练权重也会出问题。

5.5 小数据集上直接过拟合:loss趋近于零但测试精度很低

现象:happy数据集上训练20个epoch后,训练loss降到0.01以下,训练精度100%,但测试精度只有70%左右。

原因:模型参数量远大于样本量,600张图对ResNet来说太少,网络直接把训练样本记住了,没有学到可泛化的模式。

解决:加上早停机制,用验证集监控泛化能力;同时引入数据增强——随机裁剪、水平翻转、颜色抖动,把有效样本量放大几倍。第二周项目本身规模小,我的做法是把训练脚本改成每隔10个epoch在测试集上打一次精度,取测试精度最高的那组参数保存下来,宁缺毋滥。

6. 把网络改造成你自己的:从验证精度到迁移学习的最后一公里

两个星期的项目跑通之后,别急着关掉编辑器。你的下一步是把这份代码改造成能复用到自己数据集上的工具。先做的第一件事,是写一个可复用的验证函数,把“训练完的模型能不能用”变成一行代码能回答的问题:

# 参考写法:统一的模型验证函数 def evaluate_model(model, data_loader, device='cpu'): model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in data_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() return correct / total

这个函数的要点有两个:model.eval()会关闭Dropout和BN的统计更新,保证推理时行为稳定;torch.no_grad()告诉框架不需要计算梯度,内存占用大幅下降。我一般在训练脚本的最后一行调用它,然后打印精度——这个习惯让我避免了很多次“训练完了却忘了测一下模型实际效果”的尴尬。

第二件事,是给自己的数据集做适配。train_signs.h5是6类,你的数据可能是10类、100类;图像可能是32x32而不是64x64。改两个地方就行:全连接输出层的神经元数量改成你的类别数;输入的通道数如果不是3,把第一层卷积的in_channels改掉。如果图像尺寸偏小,可以先去调输入层的padding或干脆resize到64x64。

第三件事,是尝试加载ResNet预训练权重做迁移学习。在resnets_utils.py的基础上,把网络结构定义成torchvision.models.resnet18(pretrained=True),然后只重新训练最后一层全连接。预训练模型在ImageNet上学到的低级特征——边缘、纹理、颜色块——对大多数图像任务都是通用的,冻结前面的卷积层参数,只训练分类头,几百张图也能有不错的效果。这时候你再回头想resnet是cnn吗这个问题,答案就变得更具体了:它是一个在CNN骨架上加了跳跃连接的进化版本,而这个进化让它能训练得更深、收敛得更稳。

那次我拿着这份源码包跑自己的数据集,第一周CNN试了十几次,测试精度一直卡在75%。后来发现是数据布局没统一:训练集用了NHWC,测试集忘了转成NCHW。从那以后我每次搭CNN和ResNet,都强制走一遍流程:先打印数据shape、再逐层打印特征图shape、验证集精度必须在训练脚本里实时输出。这套流程帮我避开了至少二十次翻车。希望你也能用这套流程,少踩几个我踩过的坑——希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询