资讯动态

ResNet是CNN的进化形态:从手写CNN到残差网络的源码级拆解

发布时间:2026/10/19 6:58:33 来源:尧图企业网站定制
简介面向深度学习初学者的卷积神经网络与残差网络搭建项目基于Python实现涵盖从基础卷积网络到残差网络的完整流程。资源主要解决手工实现经典网络结构的代码组织与训练验证问题适合正在学习计算机视觉或神经网络原理的读者。压缩包共二十四个文件以Python脚本和HDF5数据集为主辅以编译文件和IDE配置其中数据集文件提供训练与测试数据脚本文件包含网络定义、工具函数与训练入口。整体约25兆字节结构按“第一周卷积网络”和“第二周残差网络”分目录组织便于按阶段查阅。已有1099人学习下载通过该资源读者可以对照代码理解卷积层、池化层、全连接层以及残差块、跳跃连接的具体实现学习如何在PyTorch中构建并训练模型并获得完整的项目目录划分、数据加载和验证思路为后续调整网络结构或开展图像分类实验打下基础。1. 先说结论ResNet 不是 CNN 的对手而是 CNN 的进化形态每次有人拿着“resnet是cnn吗”来问我的时候我第一反应不是急着回答是或不是而是建议他先把这份源码跑一遍。ResNet 不是 CNN 的对立面它是 CNN 的一个改进形态——把普通卷积块替换成带跳跃连接的残差块用一条 shortcut 把梯度直接传回浅层让网络从十几层堆到几十层甚至上百层都不崩。这份资源正好用两周代码把这个进化过程讲透了第一周从零写一个能跑通手写符号识别的简易 CNN第二周在同样的数据集上把网络升级成 ResNet。适合两类人刚学完反向传播、想看看完整代码长什么样的入门者以及想在现有网络上动手改结构、换数据集的进阶开发者。两套代码都属于能直接跑出训练曲线、能断点调试、能改着玩的类型。2. 从源码包看项目结构两周代码、两套数据集、同一条学习路径拿到压缩包别急着跑先把家底盘清楚。这份资源不是单个脚本而是按“第一周 CNN → 第二周 ResNet”组织好的两个独立工程每个工程都有自己完整的工具函数、测试脚本和 h5 数据集。2.1 文件清单第一周和第二周各交付了什么解压后你会看到两个目录没有多余的 README结构非常直白目录/文件作用1第一周/cnn_utils.py模型参数初始化、前向传播、成本函数等通用工具1第一周/test.py主入口脚本数据加载、训练、预测、准确率评估1第一周/datasets/train_signs.h5手写符号训练集6 类 0-5共约一千张1第一周/datasets/test_signs.h5对应的测试集约一百张2第二周/resnets_utils.pyResNet 的残差块定义、数据加载工具2第二周/kt_utils.py加载 happy 数据集的辅助函数2第二周/test.pyResNet50 的训练与测试主脚本2第二周/datasets/train_happy.h5笑脸/非笑脸二分类训练集这里有个容易忽略的点第一周和第二周都有自己的 test.py而且第二周用到了一个叫做 kt_utils 的辅助文件说明第二周同时准备了两份数据——signs 和 happy。训练时你可以任意切换测试不同数据集对模型能力的要求。两个目录下的 .idea、workspace.xml 是 IDE 的工程配置对学习没有影响直接忽略。pycache 也不需要管。2.2 h5 数据集与预处理train_signs.h5 里装的是什么h5 文件不是图片文件而是一种层级化数据容器。这套代码里的 h5 存取逻辑非常典型加载后你会得到两组数据样本数组和标签数组。以 signs 为例样本 shape 是(num_examples, 64, 64, 3)64×64 是分辨率3 是 RGB 通道标签是 one-hot 编码后的矩阵。这块预处理藏着一个新手最容易翻车的点数据加载函数返回的是train_x_orig和train_y_orig其中train_y_orig的 shape 是 (1, num_examples) 或者 (num_classes, num_examples)取决于原始文件的组织方式。代码里通常要做一次转置Y.T把标签变成 (num_examples, num_classes) 才能喂给损失函数。shape 不匹配时训练会直接报维度错误这是这套源码里最常见的启动失败原因。2.3 技术选型手写前向传播还是框架自带 API这份资源的代码风格偏课程教学用的是 TensorFlow 1.x 的静态图写法——先定义参数、再建前向传播图、最后用 Session 跑。和现在主流的 PyTorch 动态图相比它多了一层“先构图再执行”的思维负担但好处是把每一层的输入输出 shape 都暴露在你面前对理解 CNN 结构非常有帮助。从文件分工也能看出来cnn_utils.py 是手写的参数初始化和前向传播函数test.py 是组装和训练流程。我一般建议初学者先不去碰 keras 的高层封装而是跟着这种手写方式把每个张量的 shape 在纸上画一遍。一旦你习惯了 NHWC 的通道顺序和 TensorFlow 的 padding 策略再去切 PyTorch 也就只是换一套 API 的事。这套代码里对 padding 的处理尤其值得注意SAME模式下卷积不会改变特征图尺寸池化层会把尺寸减半这两条规则是后面推导所有层输出 shape 的基础。3. 第一周核心代码拆解手写 CNN 从参数初始化到反向传播第一周的目标很明确不借助高层框架用手写的方式搭一个能跑通 6 类符号识别的卷积神经网络。理解了这一周你就能看懂后面 ResNet 里所有基础组件的来源。3.1 参数初始化四组参数的 shape 设计网络结构是常见的两层卷积 两层全连接。第一层卷积用了 8 个 3×3 滤波器第二层用了 16 个 3×3 滤波器全连接层先压到 120 维最后输出 6 类。代码开头长这样def initialize_parameters(): W1 np.random.randn(8, 3, 3, 3) * 0.1 # 8 个滤波器每个 3x3x3输入3通道 b1 np.zeros((1, 1, 1, 8)) W2 np.random.randn(16, 3, 3, 8) * 0.1 # 16 个滤波器输入是上一层的 8 通道 b2 np.zeros((1, 1, 1, 16)) W3 np.random.randn(4096, 120) * 0.1 # 展平后 4096 维 - 120 维 b3 np.zeros((1, 120)) W4 np.random.randn(120, 6) * 0.1 # 120 - 6 类输出 b4 np.zeros((1, 6)) parameters {W1: W1, b1: b1, W2: W2, b2: b2, W3: W3, b3: b3, W4: W4, b4: b4} return parameters这里W1的 shape 是(8, 3, 3, 3)对应 TensorFlow 的卷积核约定前两维是卷积核高宽第三维是输入通道数第四维是输出通道数。* 0.1是缩小初始化尺度防止激活值过大。最后一个全连接层W4的 6 对应类别数如果换自己的数据集这里要跟着改。整个初始化没有用 Xavier 或 He 初始化而是简单高斯缩放对浅层网络够用但如果你把网络加深到 50 层以上这种朴素初始化会让训练变得很玄学后面改 ResNet 时要注意。3.2 前向传播链路一张图从输入到输出的流动前向传播是这套代码最值得精读的部分。它完整展示了 CNN 的三段式卷积提取特征、池化压缩尺寸、全连接做分类。代码逻辑如下def forward_propagation(X, parameters): W1 parameters[W1]; b1 parameters[b1] W2 parameters[W2]; b2 parameters[b2] W3 parameters[W3]; b3 parameters[b3] W4 parameters[W4]; b4 parameters[b4] # 第一层卷积(8个滤波器) - ReLU - 最大池化(2x2) Z1 tf.nn.conv2d(X, W1, strides[1,1,1,1], paddingSAME) A1 tf.nn.relu(Z1) P1 tf.nn.max_pool(A1, ksize[1,2,2,1], strides[1,2,2,1], paddingSAME) # 第二层卷积(16个滤波器) - ReLU - 最大池化(2x2) Z2 tf.nn.conv2d(P1, W2, strides[1,1,1,1], paddingSAME) A2 tf.nn.relu(Z2) P2 tf.nn.max_pool(A2, ksize[1,2,2,1], strides[1,2,2,1], paddingSAME) # 展平后进入全连接层 P2 tf.contrib.layers.flatten(P2) Z3 tf.matmul(P2, W3) b3 A3 tf.nn.relu(Z3) Z4 tf.matmul(A3, W4) b4 return Z4输入是 64×64×3 的图片经过第一层卷积后由于 padding 是SAME特征图仍是 64×64×8池化后变 32×32×8第二层卷积后 32×32×16池化后 16×16×16展平后正好是 16×16×164096 维。这个 4096 不是凭空来的你改输入图片尺寸或卷积层数这个数字就会变全连接层的输入维度必须跟着调。tf.contrib.layers.flatten在 TensorFlow 2.x 里已经移除了如果你用的是新版环境需要用tf.reshape(P2, [P2.shape[0], -1])替换。3.3 成本函数与反向传播梯度到底在每层干了什么第一周的损失函数和优化器用的是全量梯度下降配交叉熵代码很短但它是整条训练链路的终点def compute_cost(Z4, Y): logits tf.transpose(Z4) labels tf.transpose(Y) cost tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits( logitslogits, labelslabels)) return cost optimizer tf.train.AdamOptimizer(learning_rate0.001).minimize(cost)注意这里连着做了两次转置Z4 的 shape 是 (batch, 6)Y 的 shape 在数据预处理后是 (batch, 6)理论上不需要转置。之所以还要转是因为数据集里 Y 的布局在不同加载函数下可能不一致这是很多旧课程代码的常见写法我在自己项目里会直接统一成(batch, num_classes)省得来回转。反向传播这部分代码里通常没有手写而是直接让 TensorFlow 的自动微分来算。这并不代表反向传播不重要——恰恰相反你在纸上推导卷积层的dW时会发现它是输入A_prev和梯度dA的卷积操作全连接层的dW就是简单的矩阵乘法。第一周源码的价值在于让你看到前向传播的长相而反向传播的细节需要在纸上补课否则后面 ResNet 的维度推导你会完全懵。4. 第二周核心代码拆解ResNet 的残差块到底多做了什么第二周在第一周的基础上加了一个关键动作给卷积块接上一条“捷径”。这就是 ResNet 和普通 CNN 的本质区别也是回答“resnet是cnn吗”的核心证据——它依然是卷积层、池化层、激活函数的组合只是多了一组跳跃连接。4.1 identity_block输入输出维度一致时的捷径当残差块的输入和输出通道数、尺寸完全一致时直接用恒等映射作为 shortcut。代码实现如下def identity_block(X, f, filters): F1, F2, F3 filters X_shortcut X # 主路径1x1卷积降维 - 3x3卷积 - 1x1卷积升维 X Conv2D(F1, (1, 1), strides(1, 1), paddingvalid)(X) X BatchNormalization(axis3)(X) X Activation(relu)(X) X Conv2D(F2, (f, f), strides(1, 1), paddingsame)(X) X BatchNormalization(axis3)(X) X Activation(relu)(X) X Conv2D(F3, (1, 1), strides(1, 1), paddingvalid)(X) X BatchNormalization(axis3)(X) # 捷径直接相加不经过任何变换 X Add()([X, X_shortcut]) X Activation(relu)(X) return X这里filters是一个三元组比如(64, 64, 256)三个数字分别对应三层卷积的输出通道数。axis3指定 BN 在通道维做归一化对应 NHWC 布局。主路径的前两个 1×1 卷积负责降维减少 3×3 卷积的计算量最后一个 1×1 卷积负责把通道数升回去保证和 shortcut 相加时维度一致。4.2 convolutional_block维度不匹配时用 1x1 卷积调整捷径identity_block 要求输入输出相同但网络在 stage 切换时需要把通道数翻倍、尺寸减半这时捷径也要跟着变。convolutional_block 的关键就在这个 short-cut 的处理上def convolutional_block(X, f, filters, s2): F1, F2, F3 filters X_shortcut X # 主路径注意第一层卷积 stride 用 s X Conv2D(F1, (1, 1), strides(s, s), paddingvalid)(X) X BatchNormalization(axis3)(X) X Activation(relu)(X) X Conv2D(F2, (f, f), strides(1, 1), paddingsame)(X) X BatchNormalization(axis3)(X) X Activation(relu)(X) X Conv2D(F3, (1, 1), strides(1, 1), paddingvalid)(X) X BatchNormalization(axis3)(X) # 捷径用 1x1 卷积调整尺寸和通道数 X_shortcut Conv2D(F3, (1, 1), strides(s, s), paddingvalid)(X_shortcut) X_shortcut BatchNormalization(axis3)(X_shortcut) X Add()([X, X_shortcut]) X Activation(relu)(X) return X对比两段代码能明显看到identity_block 的X_shortcut X是零成本恒等映射而 convolutional_block 的 shortcut 走了一个 1×1 卷积stride 与主路径第一层一致输出通道数等于F3。这样做之后两条路径在相加时的 shape 完全相同。如果漏掉这个 1×1 卷积直接相加TensorFlow 会直接报维度不匹配这是改代码时最常踩的坑。4.3 组装 ResNet从 conv1 到 Softmax 的完整数据流把两种残差块串联起来就得到一个完整的 ResNet 结构。第二周的 test.py 里模型的组装逻辑是先一个 7×7 卷积加池化做浅层特征提取后面跟四个 stage每个 stage 由一个 convolutional_block 开头、若干 identity_block 堆叠最后是全局平均池化和全连接输出。Stage块类型输出尺寸conv17x7, 64, stride232x32x64stage2conv_block 2×identity_block16x16x256stage3conv_block 3×identity_block8x8x512stage4conv_block 5×identity_block4x4x1024stage5conv_block 2×identity_block2x2x2048输出层全局平均池化 全连接6 类这个结构对应的是 50 层左右的深度其中大部分层来自 stage3 和 stage4 的 identity_block 堆叠。训练 happy 数据集时只需要把最后的 6 类改成 2 类其他结构不用动。我建议你在跑之前先把每个 stage 的输出尺寸算一遍因为这是这份源码里最容易出维度错误的地方——改一处滤波器数量后面所有块的 shape 都要连锁调整。5. 常见问题与排查跑通这套源码的五个关键坑代码本身能跑但它在不同环境、不同 Python 版本下的表现会差很多。这一章我按“现象 → 原因 → 解决”整理跑代码时最常遇到的五个问题基本覆盖了我自己跟进这个项目时踩过的所有坑。5.1 数据 shape 对不上报错出现在全连接层现象前向传播里卷积和池化都能通过一到tf.matmul(Flatten, W3)就报维度不匹配。原因输入图片尺寸或通道数和你初始化的W3第一维对不上。这套代码默认输入是 64×64×3如果你换成自己的数据是 128×128 或灰度单通道展平后的维度就变了。解决先加载数据后打印train_x_orig.shape然后手算池化后的尺寸再改W3的初始化参数。这里面“通道数”是个隐蔽变量灰度图是 1RGB 是 3搞错的话第一层卷积核的第四个维度也要跟着改。5.2 训练时最后一小批数据导致 batch 越界现象训练到每个 epoch 快结束时某一步突然报IndexError或者 loss 变为 NaN。原因minibatch 梯度下降时如果总样本数不能被 batch_size 整除最后一批的实际数量会小于 batch_size。代码里如果直接按固定索引切片就会越界如果用了固定维度NaN 通常来自空批次。解决取批次时用min(batch_size, m - start)动态计算实际数量保证最后一个 batch 也能正常前向和反向。5.3 BN 和 ReLU 的顺序弄反训练曲线震荡现象网络能跑但 loss 降不下去偶尔还飙升。原因ResNet 代码里 BN 应该放在卷积之后、ReLU 之前如果手欠调换成了先激活后归一化梯度信号会被严重扭曲。解决严格按代码顺序执行即Conv2D - BatchNormalization - Activation。这类问题最坑的是不报错只表现在训练行为上属于典型的深度学习玄学问题。你只能逐层检查网络定义没有捷径。5.4 残差块 shortcut 维度没对齐Add 报错现象报错信息里提到Add操作的两个输入 shape 不一致。原因在改网络深度或滤波器数量时只改了主路径的滤波器忘了同步修改 shortcut 的 1×1 卷积输出通道数。解决convolutional_block 里 shortcut 的输出通道数必须等于主路径第三个卷积的输出通道数F3stride 必须等于主路径第一个卷积的 stride。这两个值不一致时把 shortcut 的Conv2D参数复制主路径的对应值即可。5.5 损失函数不下降一开始就卡住现象训练 20 个 epoch 后准确率还在随机水平。原因常见的有三类——学习率过大导致 loss 震荡参数初始化尺度太大导致激活饱和数据归一化漏了像素值还是 0-255。解决先确认X / 255.这一步做了再把学习率从 0.001 往下调最后看初始化代码里有没有* 0.01或* 0.1的缩放。这套代码里如果直接复制的默认参数换数据集时初始化尺度不变大概率是能用的但当你自己把网络加深时建议改用 He 初始化。6. 二次开发把这套代码改造成自己的网络和数据集跑通源码只是起点真正有价值的是把它改造成属于你自己的图像分类工具。这一章给三条改造路径难度从低到高你可以按需选一条。6.1 换自己的数据集图片目录打包成 h5 文件这套代码的入口都是 h5 文件所以第一个要掌握的技能就是把一堆图片转成 h5。常见做法是用 h5py 写一个转换脚本把图片读成 NumPy 数组后存盘。这里给一个最小可用的打包逻辑import h5py import numpy as np from PIL import Image import os images, labels [], [] for label, cls in enumerate([cat, dog]): path fdata/{cls} for name in os.listdir(path): img Image.open(os.path.join(path, name)).resize((64, 64)) images.append(np.array(img)) labels.append(label) X np.stack(images) Y np.eye(2)[labels] # one-hot with h5py.File(my_data.h5, w) as f: f.create_dataset(train_x, dataX) f.create_dataset(train_y, dataY)注意图片统一 resize 到 64×64因为后续全连接层的维度是按这个尺寸推的。标签的 one-hot 编码直接决定了全连接输出层的节点数比如二分类就是 2六分类就是 6。这个脚本可以套用在你自己的图片目录上核心是保证所有图片尺寸一致否则np.stack会直接报错。6.2 改网络深度从 ResNet50 缩到 ResNet18如果你的数据集只有几千张图跑 50 层的 ResNet 很容易过拟合。缩到 18 层只需要砍掉 stage4 里多余的 identity_block把[3, 4, 6, 3]的块数量改成[2, 2, 2, 2]。这个改动涉及的不只是循环次数stage 的输出通道数也要同步调整砍薄之后显存占用和训练时间都会大幅下降。改完先跑一个单 batch 过拟合测试确认 loss 能降到接近 0再上全量数据。6.3 迁移到 PyTorch结构映射和踩坑提醒习惯了这套代码后想切到 PyTorch 做研究映射关系其实很清晰TensorFlow 的Conv2D(filters, (kernel_size))对应torch.nn.Conv2d(in_channels, out_channels, kernel_size)BatchNormalization(axis3)对应torch.nn.BatchNorm2d不需要指定 axis默认就是通道维Add()([X, shortcut])对应X shortcutFlatten对应torch.flatten。最需要注意的是输入布局PyTorch 默认 NCHW你在 forward 里要把图片从 (batch, channel, height, width) 传入这和源码里的 NHWC 顺序相反。很多人在迁移时网络定义完全正确只是在测试时忘了tensor.permute转维度导致结果莫名变差。我把这套源码前前后后梳理过好几遍每次带着学员做二次开发都会强制自己从头把每个 stage 的输出 shape 推演一遍。当初第一次修改残差块数量时我没注意 shortcut 的维度联动Add 报错之后折腾了半个下午才发现是 1×1 卷积的输出通道没跟上从那以后我每次改网络结构都会先画一张各层 shape 的对照表再动手写代码这个习惯帮我避开了至少十次类似的低级错误。希望这份拆解能让你少走这些弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑