
简介从零搭建CNN与ResNet的入门实战代码包面向深度学习初学者和想厘清“ResNet与CNN关系”的开发者可直接用于计算机视觉自学或课程设计参考。资源共24个文件包含5个Python训练与工具脚本、6个h5格式数据集文件交通标志、手势、笑脸等另有PyCharm工程配置xml/iml与运行生成的pyc缓存压缩包整体约25.27MB代码按“第一周CNN、第二周ResNet”两阶段组织目录清晰便于逐周学习。目前已有1099人学习下载。源码完整展现了CNN的卷积层、池化层、全连接层以及ResNet残差块、跳跃连接和批量归一化的实际写法可直接运行查看训练过程配套h5数据集支持从数据加载、模型搭建到损失计算与验证的完整闭环帮助读者快速掌握PyTorch/TensorFlow建模思路并深入理解残差连接如何缓解深层网络梯度消失问题。1. 先解决最大困惑ResNet到底是不是CNN第一次看到“简易CNN和ResNet搭建Python源码”这份资源时我脑子里最大的问号不是“怎么搭建”而是一个更基础的问题ResNet是CNN吗如果你也有同款困惑那这篇入门文章就是写给你的。CNN和ResNet的关系可以类比成同一道菜的基础版和改良版CNN是深度学习里最常用的图像识别模型ResNet是在CNN骨架上加装了“残差块”的升级型号所以它本质上还是CNN只不过更聪明、更适合往深了堆。这份资源里的代码用Python带你按两周的节奏搭出这两个模型。今天我们不急着刷原理我带你把它读成能上手的东西先搞懂两个概念再看看资料里到底有什么最后落在你电脑上能跑通的第一个例子。2. 先搞懂地基CNN是怎么“看”一张图的不管第一周还是第二周你写的每一段代码都是在做同一件事让电脑从图片里认出东西来。CNN解决的就是这个问题它仿照人眼的工作方式一层一层地从图里找特征——先找边缘再找纹理最后拼出“这是一只猫”的结论。要理解ResNet你得先知道CNN的四个积木块下面我们一块一块来。2.1 图像在电脑眼里是一堆数字你在屏幕上看到的照片对电脑来说只是一个巨大的数字矩阵。一张灰度图就是一个二维数组每个数字代表一个像素点的亮度范围通常是0到2550是纯黑255是纯白。彩色图则稍微复杂一点它由三个矩阵叠在一起组成分别对应红、绿、蓝三个颜色通道。你可以用几行代码亲手验证这件事。打开任意一个Python文件敲进下面这段import numpy as np # 用随机数模拟一张 4x4 的灰度图 fake_image np.random.randint(0, 256, size(4, 4)) print(fake_image)跑一下你就会发现图像在程序眼里根本不是“画面”而是一张写满数字的网格。这里有两个值得记住的点第一这张数字网格就是CNN的“输入层”接收的数据第二数字的取值范围很关键后面你训练网络时经常要把这些数字除以255让它们落在0到1之间这个操作叫归一化很多新手漏了这一步导致模型怎么训都不收敛。2.2 卷积层用一个小窗口“扫描”整张图既然图是数字网格那怎么从中找出特征最笨的办法是把所有数字拉平成一条长向量交给全连接层处理。但这样做有个致命问题一张100×100的图拉平后有一万个数字全连接层要为每个数字配一大堆权重参数数量爆炸训练极慢还容易过拟合。卷积层换了个思路它不用看整张图只用一个小窗口比如3×3的小方块在图上滑动每滑到一个位置就做一次“逐点相乘再求和”得出来的结果构成一张新的小图叫特征图。这个小窗口就是滤波器也叫卷积核。一个滤波器只能找一种特征——有的负责找竖线有的负责找横线有的负责找拐角。想让网络认识更复杂的东西就多放几个滤波器所以卷积层的输出通常有好多张特征图每一张是“某个特征在图上哪里出现了”的地图。这里有两个概念会在代码里反复出现步长和填充。步长就是窗口每次往右或往下挪几格步长越大输出图越小填充是在原图四周补一圈0用来控制输出尺寸。比如输入是32×32的图卷积核是3×3填充设为1输出还是32×32如果不填充输出就变成30×30。动手改代码时常会因为这两个参数没算清楚导致后面全连接层维度不匹配这个是后话你现在先有个印象。2.3 激活函数和池化层给特征“表态”再“瘦身”卷积层是线性操作就算堆一百层本质上还是线性变换的组合学不到复杂规律。这时候就需要激活函数出场。CNN里最常用的是ReLU公式极其简单负数全部变0正数原样保留。import torch # 模拟ReLU把负数清零 x torch.tensor([-2.0, -1.0, 0.0, 1.0, 3.0]) y torch.relu(x) print(y) # 输出: tensor([0., 0., 0., 1., 3.])为什么要把负数清零因为清零操作引入了非线性让网络有能力拟合各种弯弯曲曲的边界。你可以把ReLU理解成“表态”这个位置没找到特征就直接闭嘴输出0找到了就大声报数。池化层接在卷积和激活后面最常见的是最大池化在一个2×2的小窗口里取最大值然后窗口滑过整张图。这样做的效果是图像尺寸缩小一半但最强的特征被保留下来计算量也大幅下降。而且池化让网络对轻微的位移不那么敏感——小猫在照片里稍微偏了半个像素提取到的特征几乎不变。要注意的是池化层没有可学习的参数它是个纯“瘦身”操作新手常误以为它和卷积一样需要训练。2.4 全连接层收尾把特征“拉平投票”经过几轮“卷积→激活→池化”之后原始图片已经变成一堆小而深的特征图。接下来要做的是把这些特征图拉平成一维向量送进全连接层做最终判断。全连接层就像一个投票委员会每个输入特征有权重经过加权求和后输出每个类别的得分。分类任务通常在最后一层接softmax把这些得分换算成概率比如“是猫的概率0.8是狗的概率0.2”。完整的CNN流程你可以记成一句话输入→卷积→激活→池化→重复若干次→拉平→全连接→输出概率。下面用一个极简的CNN把这句话落到代码上import torch import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self): super().__init__() # 卷积块1输入3通道彩色图输出8个特征图 self.conv1 nn.Conv2d(in_channels3, out_channels8, kernel_size3, padding1) self.pool1 nn.MaxPool2d(kernel_size2, stride2) # 卷积块2输入8个特征图输出16个特征图 self.conv2 nn.Conv2d(in_channels8, out_channels16, kernel_size3, padding1) self.pool2 nn.MaxPool2d(kernel_size2, stride2) # 全连接层把拉平后的特征映射到10个类别 self.fc nn.Linear(16 * 8 * 8, 10) def forward(self, x): x torch.relu(self.conv1(x)) # 先卷积再ReLU激活 x self.pool1(x) # 池化缩小尺寸 x torch.relu(self.conv2(x)) x self.pool2(x) x x.view(x.size(0), -1) # 拉平成一维向量 x self.fc(x) # 全连接输出10个类别的分数 return x上面这段代码里的模型假设输入是32×32的彩色图。数据流向是这样的32×32输入经过conv1padding1还是32×32经过pool1变成16×16再走一轮变成8×8这时有16个特征图拉平后就是16×8×81024个数全连接层把这1024个数映射到10个类别。如果你想换个数据集只需要改两处全连接层的输入维度以及最后的输出类别数。第一次跑模型时建议在forward里加一行print(x.shape)看看每一步数据尺寸变成什么样这是理解CNN最快的方式。3. ResNet为什么是CNN的升级版残差块到底在做什么现在你已经知道CNN的四个积木块了。但你可能有个疑问既然卷积、池化、全连接一套流程能把猫和狗分开那为什么还要搞一个ResNet出来答案很简单因为人们发现CNN越深反而越难训练而ResNet专治这个毛病。这一章我们解决两个问题普通CNN到底遇到了什么困难以及残差块是用什么思路化解困难的。3.1 普通CNN加深后为什么会“翻车”直觉告诉我们层数越多网络应该越强——毕竟能提取更细的特征。但研究者实验后发现普通CNN在层数超过一定值之后训练集上的错误率反而升高了不是过拟合是根本学不动。我第一学到这里时也不懂后来才明白主要有两个原因在捣乱。第一个原因是梯度消失。深度学习靠“反向传播”更新参数误差要从最后一层传回第一层每经过一层就要乘一次梯度。如果梯度值小于1乘上几十次之后传到前面的梯度已经小到接近0前面的卷积层几乎学不到东西陷入“瘫痪”。第二个原因是退化问题理论上更深的网络至少应该能模仿浅层网络的性能只要后面几层学“什么都不做”就行但实践证明让普通网络学会“什么都不做”这件事本身就非常困难。总结成一句话层数加深后网络不是不想学是传不下去、学不动。这时候需要的不是更复杂的结构而是一条能让信息“抄近路”的通道。3.2 残差块的设计思路y F(x) xResNet解决这个问题的办法看起来简单到让人怀疑在模块外面加一条短路把输入直接加到输出上。一个残差块的输出不是“经过卷积之后的结果”而是“卷积结果加上原始输入”。用公式写就是y F(x) x其中F(x)是几层卷积和激活的组合。这条加法操作网上各种文章叫“跳跃连接”“恒等映射”“shortcut”听起来很高深其实你只需要理解它给了网络一条短路通道信息可以不经过中间的卷积层直接往后传。这样一来即使某一层卷积学得很差原始输入x也能原样保留。更妙的是如果这一层确实没什么可学的网络只需要让F(x)趋近于0输出就约等于输入x相当于这一层自动变成了“透明层”。相比之下普通CNN没有这条通道每一层都必须“硬学”一个结果学不好反而添乱。打个比方普通CNN像一条必须每道工序都做好的流水线ResNet则在每段工序旁边修了一条应急通道师傅状态不好时东西可以直接滑到下一站不影响整体进度。这就是为什么ResNet能堆到50层、101层甚至更深而普通CNN堆到20多层就学不动了。3.3 用一张表回答“ResNet是CNN吗”很多人纠结这个问题其实答案很明确是的ResNet就是CNN的一种是CNN的改进版本。那种“ResNet和CNN是两套东西”的感觉来自对比时把“普通CNN”默认成了“CNN”本身。下面这张对比表帮你把关系看清楚。对比维度普通CNNResNet基本积木块卷积、激活、池化、全连接同样是卷积、激活、池化、全连接特殊结构没有跳跃连接在卷积块之间加了残差连接shortcut能加深吗十几层后容易梯度消失可以轻松堆到几十上百层典型代表简易CNN、VGG系列ResNet-18、ResNet-50训练难度浅层好训加深后变难深度较大时依然稳定所以如果你在新手期听人说“ResNet不是CNN它和CNN是两种网络”那这话是错的。更准确的说法是ResNet属于CNN家族它是为了解决CNN加深时“学不动”的问题在结构上做的一个巧妙升级。搞懂这一点你就不会再被这两个词绕晕了。3.4 动手写一个残差块BasicBlock说了这么多不如直接看代码。下面是PyTorch里一个最基础的残差块对应的是ResNet-18这类浅层ResNet里使用的结构通常叫BasicBlockimport torch import torch.nn as nn class BasicBlock(nn.Module): def __init__(self, in_channels, out_channels, stride1): super().__init__() # 两个3x3卷积每个后面跟一个批量归一化 self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(out_channels) self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) # 当输入输出形状不一致时用1x1卷积调整x的形状 self.shortcut nn.Sequential() if stride ! 1 or in_channels ! out_channels: self.shortcut nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(out_channels) ) def forward(self, x): identity x # 先保存原始输入这就是“跳跃连接”的起点 out torch.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) out identity # 关键一步卷积结果和原始输入相加 out torch.relu(out) return out和上一章的SimpleCNN相比这个残差块多了三样东西批量归一化、identity变量、相加操作。批量归一化的作用是在每层卷积之后把数据分布拉回正规范围让训练更稳定这也是ResNet敢训练得很深的原因之一。identity保存的是刚进入模块时的原始数据它没有经过任何卷积操作相当于抄了近路。最后out identity这一行就是跳跃连接本身它会先在通道数上对齐两个矩阵如果通道不一致shortcut里的1×1卷积会负责把identity调整成和out一样的大小再逐元素相加。把几个这样的BasicBlock串起来最后接一个全连接层就组成一个微型ResNet。你在第4章会看到项目第二周做的事情本质上就是重复练习组装这种残差块。4. 拿到这份Python资源之后先认识文件再准备环境概念有了代码也见过了接下来该真正动手了。但打开资源包的那一刻很多新手会被吓一跳又是“1第一周”又是“2第二周”里面还有一堆以.idea、.iml结尾的陌生文件。别慌这一章我带你逐个认门把文件夹里每一类文件是干什么的弄清楚然后装好环境让代码跑起来。4.1 资源里到底有什么按文件夹认门这份代码是按两周的学习节奏组织的两个文件夹的名字已经说明了内容第一周搭简易CNN第二周搭ResNet。下面这张表把核心文件列出来你对照着看就不迷路。文件/文件夹作用备注1第一周/test.py第一周的训练/测试主流程从这里开始看1第一周/cnn_utils.pyCNN相关的辅助函数包含数据加载、参数初始化等1第一周/datasets/train_signs.h5第一周训练数据集手势数字图片1第一周/datasets/test_signs.h5第一周测试数据集用来验证训练效果2第二周/resnets_utils.pyResNet相关的辅助函数第二周的核心工具文件2第二周/kt_utils.py笑脸数据加载工具配合train_happy使用2第二周/datasets/train_happy.h5第二周附加数据集笑脸识别任务.idea、*.iml、workspace.xmlIDE配置文件可以完全忽略pycachePython运行缓存可以完全忽略你可能会注意到第二周的datasets里也有train_signs.h5。这说明同一个手势数据集被两套模型各跑了一遍正好可以用来对比普通CNN和ResNet谁的准确率更高、训练过程更稳这是一个非常直观的体验设计。至于kt_utils.py和train_happy.h5它们是另一个小任务——区分图片里是笑脸还是非笑脸适合你在学会ResNet之后拿来做二次练习。第一次看代码时我建议你先打开test.py别看细节只追主线加载数据→建立模型→训练→测试。你会发现流程和上一章的SimpleCNN几乎一样区别只是模型换成了ResNet。辅助函数先不细读等主线通了再回头看效率会高很多。另外资源里的缓存文件名带了cpython-37字样说明作者在Python 3.7版本下运行过你现在用更新的Python版本大概率也能跑但要注意依赖库的版本兼容。4.2 跑起来之前先装好三样东西动手前需要准备的东西不多就三样Python环境、PyTorch库和h5py库。如果你的电脑里还没有打开终端执行下面这条命令可以一次装齐pip install torch h5py numpy matplotlib装完之后建议你先用下面两行代码验证环境是否正常import torch import h5py print(torch.__version__)能打印出torch的版本号说明环境没问题了。这里有个重要的心态调整新手阶段完全可以用CPU训练不需要急着买显卡。第一周的CNN模型很小用CPU跑几分钟也能完成训练足够你看清楚整个流程。为什么要提这一点因为经常有新手第一反应是“我没有GPU是不是学不了”然后在配环境中卡了三天。事实是你只需要先把逻辑跑通GPU是后面提速的事不是入门的门槛。如果你电脑上同时装了多个Python环境建议用虚拟环境工具比如conda或venv给这个项目单独开一个环境。虚拟环境相当于给项目一个独立的“小房间”里面装的库互不干扰以后出问题了直接扔掉重建比在全局环境里折腾省心得多。这也是我踩过坑之后才养成的习惯——曾经因为全局环境里某个库版本冲突连续两次安装失败后来一律开新环境再没遇到过这个问题。4.3 读懂h5数据项目里的第一道关卡安装好环境之后你碰到的第一个实际障碍大概率是数据集文件。这套资源的所有数据都打包在.h5文件里新手看到这种格式往往不知道从哪下手。简单说.h5就是一种可以同时存储多个数组的文件格式里面数据按“键值对”组织类似一个字典。要查看文件里有哪些键用h5py写几行代码就行import h5py # 先打开文件看看里面存了什么 with h5py.File(1第一周/datasets/train_signs.h5, r) as f: print(list(f.keys()))运行后你会看到类似[train_x, train_y]这样的键名列表。这时你再读取就顺理成章了import h5py import numpy as np with h5py.File(1第一周/datasets/train_signs.h5, r) as f: X np.array(f[train_x]) # 图像数据 y np.array(f[train_y]) # 标签 print(X.shape, y.shape)打印出来的形状信息非常重要一般你会看到(样本数, 高, 宽, 通道数)这种结构。这里埋伏着新手最容易踩的一个坑h5文件里存的图像维度顺序是“高、宽、通道”而PyTorch的卷积层要求输入是“通道、高、宽”。如果你不管三七二十一直接把数据喂给模型会立刻报维度错误。解决办法是交换轴的顺序代码写成X np.transpose(X, (0, 3, 1, 2))同时再除以255做归一化。项目里的utils文件通常已经把这些工作封装好了所以新手第一周直接用现成的加载函数最稳等你理解了这个步骤再自己去写也不迟。5. 避坑指南搭CNN和ResNet最容易翻车的五个坑代码看得再多不如自己报错一次学得快。我下面列出的五个坑几乎每个新手都会踩中至少两三个每一个我都会按“现象→原因→解决”的顺序讲清楚你碰到时可以一条条对着排查。5.1 环境与路径还没开始训练就卡住的两个坑坑1找不到数据集文件。现象是运行test.py时抛出FileNotFoundError提示找不到datasets/train_signs.h5。原因是代码里写的是相对路径而你是从别的目录启动脚本的Python按照当前工作目录去找文件自然找不到。解决方法是先确认你当前的运行目录在代码里加一行import os; print(os.getcwd())看输出是不是项目所在目录如果不是用os.chdir(项目绝对路径)切换过去或者把数据路径改成绝对路径。另外提醒一句文件夹名带了“1第一周”这种中文极少数情况下在某些开发环境会出编码问题稳妥的做法是进到文件夹内部运行不要从外部拼接层次过深的路径。坑2提示缺少模块。现象是报ModuleNotFoundError: No module named h5py或类似错误。原因很简单新环境里没有安装全依赖库。解决方法是执行pip install h5py补齐缺失的库。但我更推荐的做法是不要挨个手动装先把环境中已装的库导出成清单文件以后换电脑直接一键还原。如果你发现torch装好后导入报错先检查是不是装成了CPU版本但想用GPU或者反过来入门阶段统一装CPU版本即可省去一堆CUDA配置问题。5.2 维度与数据格式一跑就报错的经典原因坑3矩阵乘法维度不匹配。现象是训练脚本跑到forward时报RuntimeError: size mismatch。原因大多是你改了网络结构或输入图片尺寸但全连接层的输入维度没有跟着算清楚。我在第2章说过全连接层的输入等于“最后一个池化层输出的特征图数量×宽×高”这个值只要卷积、步长、池化任何一处改动数字就会变。解决方法是给网络写一个“维度探测”流程在forward里临时插入print(x.shape)跑一次就能看到每一层输出的实际尺寸确定后再把数字填进全连接层。刚学的时候觉得加print很笨但它比闭眼硬算快得多。坑4图像数据的维度顺序不对。现象是数据能加载但送给模型时shape是(64, 64, 3)而模型期望(3, 64, 64)报错信息看起来像“Expected 4D input, got 3D input”。原因是h5文件里存的是“高、宽、通道”顺序而PyTorch卷积层要的是“通道、高、宽”。解决方法是做一次轴交换np.transpose(X, (0, 3, 1, 2))把通道维从最后挪到第二位。这也是项目里utils文件的核心功能之一所以你用现成函数时会觉得很顺自己重写时就容易翻车。5.3 训练不收敛跑起来了但loss不降坑5loss一直不下降。现象是训练了好几个epoch损失函数值几乎没变准确率还在瞎猜的水平。原因通常是下面几个中的一个输入数据没归一化像素还停留在0~255、学习率设得太离谱太大震荡、太小蜗牛、或者输出层接法有问题。解决方法是按顺序排查检查输入数据有没有除以255把学习率调到0.001这个常用起点如果用了CrossEntropyLoss交叉熵损失网络最后一层不要再额外加softmax因为这个损失函数内部已经包含了softmax操作重复加会导致梯度异常。如果你想让实验结果可复现还可以在代码开头加上torch.manual_seed(42)固定随机种子这样每次跑出来的结果基本一致不会出现“明明没改代码效果却忽好忽坏”的玄学情况。6. 下一步怎么学从跑通代码到真正吃透ResNet到这里你已经知道CNN和ResNet是什么关系也能看懂资源里的文件结构了。接下来的学习路径我建议你按下面这张表的顺序走步骤任务目标第一步运行第一周的test.py完整走通“加载数据→训练→测试”流程第二步精读cnn_utils.py读懂数据加载和参数初始化细节第三步自己写一个SimpleCNN不看源码也能搭出CNN第四步运行第二周的ResNet代码感受残差块的训练效果第五步改写一个BasicBlock能解释每一行在干嘛每一步的验收标准都很明确能跑通且能向别人讲清楚代码在做什么。特别是走到第四步时建议你在同一份手势数据集上分别用第一周的CNN和第二周的ResNet训练对比两者的最终准确率和loss曲线。你大概率会发现ResNet收敛得更稳、准确率更高——这种“亲手看到差距”的体验比背十遍理论都管用。走完基础流程后可以试着把数据集换成train_happy.h5做一次笑脸识别。这仍然属于分类任务但数据分布变了你会被迫去检查维度、类别数、归一化方式这些细节。接着可以挑战一下传说中的ResNet-18它其实就是把你学过的BasicBlock串联4组每组分别重复2次再接全连接层。你在第3章见过的每个零件到这里全部派上用场。如果想更进一步试着把学习率从0.001改成0.0001或者把batch size从32改成64观察训练曲线变化建立对超参数的“手感”。最后说点我自己的教训。刚上手时我总喜欢跳过看数据这一步拿起来就训练结果经常被维度报错和loss不降折腾一晚上。后来我强制自己每次拿到新项目先打印数据shape、再打印标签类别数、最后才写模型。这个习惯让我少走了很多弯路。从那以后每次拿到新项目我都会先走一遍这个流程再开始跑代码。希望帮到你——现在放心地去跑你的第一个test.py吧跑通的那一刻你会觉得这一切并不难。本文还有配套的精品资源点击获取