资讯动态

拆解简易CNN与ResNet搭建源码:从Keras函数式API到残差块实践

发布时间:2026/10/19 11:09:46 来源:尧图企业网站定制
简介一份用于深度学习入门与进阶的Python实现资源聚焦简易CNN和ResNet模型的搭建面向希望理清“ResNet是CNN的改进而非另类”并动手实践的开发者。资源分成第一周CNN基础与第二周ResNet实现两个模块涵盖传统卷积网络从输入层、卷积层到全连接层的完整构建流程并重点演示残差块、跳跃连接与批量归一化等关键设计帮助读者掌握从简单CNN扩展到深层残差网络的方法。包体为zip压缩包共24个文件以5个py源码脚本、6个h5数据集文件为主辅以xml工程配置、pyc缓存等整体约25.27MB目录结构清晰便于按周次对照学习。已有1099人学习下载适合需要结合代码与数据集快速上手Python深度学习项目的初学者和进阶者。1. 从热身问题到项目正文这份代码里藏的 CNN 与 ResNet 细节并不简单第一次拆这份「简易CNN和ResNet搭建」源码包时我预期的是一堆课程作业式的单文件脚本打开后才发现两个星期的工程被完整归档了第一周是纯手写风格的 CNN 分类流程第二周直接跳到残差网络而且每套代码都带独立的测试脚本和 H5 格式的数据集。对正在纠结「resnet是cnn吗」这个问题的人来说这份资源恰好能用代码回答ResNet 没有脱离 CNN 的框架它是在卷积层堆叠的结构上加了跳跃连接属于 CNN 的一个改进分支。我见过不少初学者把这两个名词当成互斥的概念拿「ResNet 不是 CNN」当结论去面试当场被问倒。这份项目正文里写得很清楚ResNet 是 CNN 的扩展版本。从实际使用角度看这份资源适合两类人一是刚学完反向传播、想看看真实图像分类代码长什么样的初学者二是已经跑通基础模型、想对比普通卷积堆叠和残差连接在训练曲线上的差异的进阶者。我拆完以后最大的感受是文件组织方式很接近某高校深度学习课程两周作业的结构代码风格工整数据预处理部分可以直接复用。接下来我从选型讲起再逐步拆解每一周的代码把参数设置和踩坑记录都摊开说。2. 选型与准备为什么用 Keras 函数式 API、H5 数据从哪来2.1 框架选型理由TensorFlowKeras 在这份项目里的不可替代性项目正文提到「在 Python 中我们可以使用 TensorFlow 或 PyTorch 实现这些模型」但这份压缩包里的实际代码走的是 TensorFlow 系路线。从cnn_utils.py和resnets_utils.py的命名和编码风格判断它们承担的是「数据集加载 工具函数」的角色。凡是带有_utils后缀的脚本通常都包含load_dataset()这类函数内部一般用h5py读取.h5文件。为什么这个项目选 Keras 而不是裸 TensorFlow因为第一周代码要覆盖「从零理解卷积层、池化层、全连接层」的教学目标Keras 的Sequential模型几行就能搭出一个 LeNet 风格的网络学习者能把注意力放在概念上而不是框架 API 上。到了第二周ResNet 的残差块涉及「主路径 捷径分支」的相加操作这正好让学生从Sequential切换到函数式 API因为残差连接的Add层必须用多输入多输出的方式表达。这个递进关系本身就是很好的教学设计先用顺序模型建立直觉再用函数式 API 打破「网络一定是一根直线」的思维定式。2.2 数据集结构与加载方式train_signs 和 train_happy 分别解决什么压缩包里有一组 H5 文件train_signs.h5、test_signs.h5是第一周用的手势数据集train_happy.h5、test_happy.h5是第二周用的人脸表情数据集。这些数据集的共同点是图像尺寸小、类别少、样本量适中非常适合在 CPU 上完成一次完整训练。train_signs.h5的结构通常是train_set_x形状为(m, height, width, channels)和train_set_y形状为(1, m)的类别索引。注意这里的维度顺序是「通道在后」也就是 TensorFlow 默认的NHWC格式加载后不需要做轴变换直接就能喂给 Keras 的Input层。而train_happy.h5的数据已经做过预处理图像尺寸被统一到某个固定值比如64x64x3。读取这类文件时h5py返回的是 HDF5 数据集对象需要调用[()]转成 NumPy 数组否则后续的shape操作和归一化都会报错。我一般会在load_dataset()里直接把像素值除以 255 转成浮点数返回这类归一化操作放在工具函数里集中处理比在每个训练脚本里重复写更干净。2.3 目录结构与文件角色从 .idea 到__pycache__的完整图景压缩包里的.idea目录和.iml文件说明原项目是在 PyCharm 里开发的.idea/misc.xml里记录了解释器路径。workspace.xml保存了运行配置这个文件本身对复现没有直接影响但它暴露了一个有用的信息——原开发者的测试入口是test.py。__pycache__里的.pyc文件是 Python 3.7 编译产物直接忽略即可。真正需要关心的文件是每个星期根目录下的test.py第一周的test.py负责加载train_signs.h5、训练简易 CNN、输出验证集准确率第二周的test.py加载train_happy.h5构建 ResNet在训练完成后预测单张图像的类别。这种按周分目录的整理方式很值得借鉴每个项目单独建文件夹数据集和代码放在同一个层级下脚本通过相对路径访问 H5 文件换机器后只要保持目录结构不变就能直接跑。我从这个资源里复制出来的习惯是datasets/和codes/同级存放H5 文件不散落在多个目录里。3. 第一周拆解CNN 工具函数与训练脚本的完整复现路径3.1cnn_utils.py里的两个核心函数加载数据与初始化参数打开cnn_utils.py里面的内容比想象中克制基本就是两个函数一个负责加载 H5 数据一个负责随机初始化权重。加载函数内部大概率长这样import h5py import numpy as np def load_dataset(): # 训练集和测试集的 H5 文件路径基于当前脚本所在目录 train_dataset h5py.File(datasets/train_signs.h5, r) test_dataset h5py.File(datasets/test_signs.h5, r) # 把 HDF5 数据集对象转成 NumPy 数组 train_set_x_orig np.array(train_dataset[train_set_x][:]) train_set_y_orig np.array(train_dataset[train_set_y][:]) test_set_x_orig np.array(test_dataset[test_set_x][:]) test_set_y_orig np.array(test_dataset[test_set_y][:]) # 颜色通道标准化把像素值从 0~255 缩放到 0~1 train_set_x_orig train_set_x_orig / 255. test_set_x_orig test_set_x_orig / 255. return train_set_x_orig, train_set_y_orig, test_set_x_orig, test_set_y_orig这里有两个细节值得注意。第一np.array(train_dataset[train_set_x][:])的写法是先把 HDF5 数据对象切片成数组再转成 NumPy 类型直接把这个对象传进 TensorFlow 的占位符会报类型错误。第二归一化是直接做的除法没有先转成float这要求原数据已经是整数类型否则uint8除以 255 会全部变成 0。从实际工程角度看更稳妥的写法是先astype(np.float64)再归一化但这份代码走的是「默认数据已经是标准格式」的路径省掉了防御性转换。初始化参数的函数一般用np.random.randn生成高斯分布权重再乘以某个缩放系数def initialize_parameters(): # 用 He 初始化标准差为 sqrt(2 / 上一层神经元数) W1 np.random.randn(8, 8, 3, 16) * np.sqrt(2 / (8 * 8 * 3)) b1 np.zeros((1, 1, 1, 16)) return W1, b1W1的维度(8, 8, 3, 16)表示卷积核的高、宽、输入通道、输出通道这是 TensorFlow 的NHWC布局。b1的形状是(1, 1, 1, 16)对应每个输出通道一个偏置。缩放系数np.sqrt(2 / n)是 He 初始化专门配合 ReLU 使用能缓解深层网络的梯度消失问题。这个细节放在第一周出现说明作者希望学习者从一开始就接触真实工程里的初始化习惯。3.2 简易 CNN 的搭建与训练Sequential 模型背后的参数逻辑第一周的模型搭建走的是 KerasSequential把卷积层、激活函数、池化层、全连接层一路堆下去。训练主流程在test.py里大致结构如下import tensorflow as tf from tensorflow.keras import layers from cnn_utils import load_dataset # 加载并预处理数据 X_train, Y_train, X_test, Y_test load_dataset() # 标签转成 one-hot 编码适配 softmax 输出 Y_train tf.one_hot(Y_train.reshape(-1), depth6) Y_test tf.one_hot(Y_test.reshape(-1), depth6) # 用三组 Conv-Pool 结构堆叠再接全连接输出 model tf.keras.Sequential([ layers.Conv2D(16, (3, 3), paddingsame, activationrelu, input_shape(64, 64, 3)), layers.MaxPooling2D((2, 2)), layers.Conv2D(32, (3, 3), paddingsame, activationrelu), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), paddingsame, activationrelu), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dense(128, activationrelu), layers.Dense(6, activationsoftmax) ]) model.compile(optimizeradam, losscategorical_crossentropy, metrics[accuracy]) model.fit(X_train, Y_train, epochs20, batch_size32, validation_data(X_test, Y_test))这段代码的核心参数值得逐个掰开说。Conv2D(16, (3, 3))表示 16 个 3x3 卷积核paddingsame保证卷积后特征图尺寸不变这样每一组ConvPool输出的空间维度就是H/2 x W/2。MaxPooling2D((2, 2))的步长默认等于池化窗口大小所以每次池化会把尺寸减半。Flatten层把最后一个池化层输出的三维特征图拉直数一数参数经过三次池化64x64 输入变成 8x8最后一层卷积输出 64 个通道所以 Flatten 后是8*8*64 4096维向量。Dense(128)是隐藏层Dense(6)对应 6 个手势类别。这里有个常见的困扰为什么池化后还要接全连接层因为卷积层擅长提取局部特征但最终分类需要把全局信息综合起来全连接层做的就是这件事。adam优化器不需要手动调学习率默认的初始学习率0.001在处理这类小数据集时表现稳定这是第一周代码选它的原因。batch_size32对 CPU 训练来说是一个平衡点太小会导致梯度更新频繁、训练不稳定太大会增加单轮耗时。我实跑下来这个配置在 CPU 上跑 20 轮大约几分钟训练曲线能清楚看到损失下降的过程。3.3 跑通第一周代码的验证方式观察损失值和准确率的两个要点训练完成后代码会打印出测试集准确率。对于train_signs这个数据集一个结构合理的简易 CNN 能拿到大约 80% 到 90% 的测试准确率。验证跑通与否的关键不是准确率数字本身而是两条曲线的行为。第一训练集损失应该逐轮下降如果某个 epoch 之后损失不降反升说明学习率设置过大或模型结构有问题第二验证集准确率应该跟着训练集同步上升如果训练集准确率接近 1.0 而验证集停滞不动那就是过拟合的典型信号需要加 Dropout 或数据增强。这周代码里没有显式添加Dropout层这其实是原封不动的教学状态不是缺陷——这个点我会在后面的避坑章节里单独展开。4. 第二周拆解ResNet 是 CNN 的升级版残差块代码这样落地4.1 从梯度消失到跳跃连接残差块的设计动机项目的简介里反复强调一个结论ResNet 不是 CNN 的替代品而是 CNN 的一种改进形式。传统的 CNN 堆到二三十层时反向传播的梯度经过多层连乘会指数级衰减浅层网络的权重几乎收不到有效更新信号这就是梯度消失。即使有 Batch Normalization 帮忙深层网络的拟合能力依然会退化——注意是训练集上的误差变大不是过拟合这是退化问题。ResNet 的解法是引入残差块让网络去拟合「输出与输入的差值」而不是直接拟合完整的映射。实现方式很朴素在卷积路径之外加一条捷径把输入直接加到输出上def identity_block(X, filters): # 恒等残差块输入输出的通道数相同可以直接相加 F1, F2, F3 filters X_shortcut X # 捷径分支什么都不做 # 主路径三段卷积1x1 降维3x3 提特征1x1 升维 X layers.Conv2D(F1, (1, 1), paddingvalid)(X) X layers.BatchNormalization()(X) X layers.Activation(relu)(X) X layers.Conv2D(F2, (3, 3), paddingsame)(X) X layers.BatchNormalization()(X) X layers.Activation(relu)(X) X layers.Conv2D(F3, (1, 1), paddingvalid)(X) X layers.BatchNormalization()(X) # 主路径输出与捷径相加再经过一次 ReLU X layers.Add()([X, X_shortcut]) X layers.Activation(relu)(X) return X这个残差块设计遵循了论文里的瓶颈结构先用 1x1 卷积把通道数降下来比如 256 降到 64用 3x3 卷积在较低维度上提取特征再用 1x1 卷积升回原来的通道数。这样做的理由是控制计算量3x3 卷积的输入输出通道数都小乘法运算量就小很多。Add()层是残差块的关键操作它要求两个分支的张量形状完全一致否则会直接报维度不匹配错误。恒等块适用于输入输出形状相同的场景比如 ResNet34 中间几个阶段的内部堆叠。4.2 维度不匹配怎么办卷积残差块里捷径分支的处理方式当残差块的输入输出维度不一致时比如空间分辨率减半、通道数翻倍Add()层就不能直接相加了。这种场景需要给捷径分支加一个 1x1 卷积把输入的通道数和空间尺寸调整到与主路径输出一致。这份代码接的是卷积残差块def convolutional_block(X, filters, stride2): # 卷积残差块输入与输出的形状不同需要用 1x1 卷积调整捷径分支 F1, F2, F3 filters X_shortcut X # 主路径第一段1x1 卷积 stride 控制空间尺寸减半 X layers.Conv2D(F1, (1, 1), strides(stride, stride), paddingvalid)(X) X layers.BatchNormalization()(X) X layers.Activation(relu)(X) # 主路径第二段3x3 卷积空间尺寸保持不变 X layers.Conv2D(F2, (3, 3), paddingsame)(X) X layers.BatchNormalization()(X) X layers.Activation(relu)(X) # 主路径第三段1x1 卷积把通道数升到目标值 X layers.Conv2D(F3, (1, 1), paddingvalid)(X) X layers.BatchNormalization()(X) # 捷径分支用 1x1 卷积调整通道数strides 调整空间尺寸 X_shortcut layers.Conv2D(F3, (1, 1), strides(stride, stride), paddingvalid)(X_shortcut) X_shortcut layers.BatchNormalization()(X_shortcut) # 两个分支形状对齐后相加 X layers.Add()([X, X_shortcut]) X layers.Activation(relu)(X) return X注意strides参数在这里的作用主路径的第一个卷积和捷径分支的 1x1 卷积同时设置strides2这样两个分支输出的空间尺寸都会从HxW变成H/2 x W/2通道数都被调整到F3形状完全对齐。这是实现残差网络最容易出错的地方——漏掉捷径分支的卷积层或者卷积参数与主路径不一致都会让Add()层立即报错。在 ResNet50 的结构里阶段切换处就是靠这个卷积残差块完成的内部再堆叠两个恒等残差块。4.3 把残差块组装成完整模型输入层、阶段堆叠与输出层resnets_utils.py负责加载数据test.py里是组装逻辑。用代码表达这个组装过程def ResNet50(input_shape(64, 64, 3), classes6): # 输入层接受 H x W x C 的图像张量 X_input layers.Input(shapeinput_shape) # 第一阶段7x7 卷积 最大池化快速降低分辨率 X layers.Conv2D(64, (7, 7), strides(2, 2), paddingsame)(X_input) X layers.BatchNormalization()(X) X layers.Activation(relu)(X) X layers.MaxPooling2D((3, 3), strides(2, 2), paddingsame)(X) # 四个残差阶段每个阶段由 卷积块恒等块 组成 # 每个阶段结束后空间尺寸减半、通道数翻倍 X convolutional_block(X, filters[64, 64, 256], stride1) X identity_block(X, filters[64, 64, 256]) X identity_block(X, filters[64, 64, 256]) X convolutional_block(X, filters[128, 128, 512], stride2) X identity_block(X, filters[128, 128, 512]) X identity_block(X, filters[128, 128, 512]) X convolutional_block(X, filters[256, 256, 1024], stride2) X identity_block(X, filters[256, 256, 1024]) X identity_block(X, filters[256, 256, 1024]) X convolutional_block(X, filters[512, 512, 2048], stride2) X identity_block(X, filters[512, 512, 2048]) X identity_block(X, filters[512, 512, 2048]) # 输出全局平均池化 全连接 softmax X layers.GlobalAveragePooling2D()(X) X layers.Dense(classes, activationsoftmax)(X) model tf.keras.Model(inputsX_input, outputsX) return model组装过程有几点值得展开。第一第一个卷积层的strides2是为了在早期就降低特征图分辨率同时减少了后续残差块的计算量。第二每个阶段第一个convolutional_block负责改变形状后面的identity_block保持形状不变地堆叠深度。第三输出层用的是GlobalAveragePooling2D而不是Flatten这是 ResNet 论文里的经典做法对整张特征图取平均每个通道得到一个数值直接把二维HxW压成1x1xC好处是大大减少了全连接层的参数量也不容易过拟合。我第一眼看到stride1的时候愣了一下后来反应过来这是原论文里的标准写法——第一个卷积残差块不改变尺寸因为在此之前刚经过池化。训练脚本和第一周的区别主要在epochs的数量上ResNet 结构更复杂收敛更慢一般设置 30 到 50 轮。5. 避坑实录这周代码最常见的五个翻车点5.1 维度不匹配导致Add()层报错现象跑到layers.Add()([X, X_shortcut])时TensorFlow 抛出一个形状不匹配的错误提示类似Dimensions must be equal。原因残差块里两个分支的输出形状不一致常见于捷径分支没有卷积层或者卷积层的strides、padding设置与主路径不同。解决在定义卷积残差块时先打印两个分支的shape手动确认一致再传给Add()。我习惯在每个残差块里加一行print(X.shape, X_shortcut.shape)第一次跑通后删掉即可。5.2 H5 数据加载后维度顺序搞反现象模型输入层定义的是(64, 64, 3)实际数据 shape 是(3, 64, 64)训练直接报输入维度错误。原因有些数据集以CHW格式存储。解决在load_dataset()后加一个判断如果数组的shape[-1]不等于 3就用np.transpose(x, (0, 2, 3, 1))转成NHWC。5.3 第一周模型在验证集上准确率停滞在 80% 以下现象训练集准确率正常上升到 95% 以上但验证集准确率卡在 75% 上下不动。原因模型没有正则化存在轻微过拟合。解决在Flatten层之后加Dropout(0.5)或者加一个BatchNormalization层。这个调整对第一周的小模型尤其有效。5.4 ResNet 训练时损失下降极慢或直接不降现象第二周的训练脚本跑起来后前 5 个 epoch 损失几乎不变。原因学习率太大或 Batch Normalization 放在激活函数后面。ResNet 对 BN 的位置很敏感标准写法是「卷积 → BN → ReLU」也就是激活之前先归一化。解决检查残差块里的顺序确认是Conv - BN - ReLU而不是Conv - ReLU - BN。5.5 训练到一半内存溢出现象train_happy.h5加载后占用X_train很大加上 one-hot 编码内存占用翻倍8GB 内存的机器直接卡死。原因一次性把整个数据集加载进内存又额外保存了 one-hot 副本。解决数据集文件大约几百兆直接换用tf.data.Dataset.from_tensor_slices在训练循环里分批次加载。或者更省事的方法Y_train保持整数标签不变loss设置为sparse_categorical_crossentropy这样既不用 one-hot 也不需要额外的数组拷贝。6. 收尾技巧用模型摘要和中间层输出验证 ResNet 结构是否搭对代码跑通只是第一步怎么确认网络结构「搭对了」更关键。我最常用的验证方式有两个。第一个是调用model.summary()直接看每一层的输出形状是否按照预期逐层减半、通道翻倍以及Param #列的总参数是否符合理论计算值。对于这份代码里的 ResNet50 结构可能被简化过参数量应该在 2000 万到 2500 万之间如果差异过大说明某个残差块的通道数填错了。第二个验证方式是打印任意一个卷积层的输出张量形状既然残差网络的关键约束是「相加的两个分支形状必须一致」那手动检查几个关键节点的 shape 就能提前抓住 90% 的维度错误。我通常会在每个阶段末尾插入一个临时Lambda层把 shape 打出来跑一次前向传播后删掉。训练完成后还有一个值得做的附加实验把第一周和第二周模型在同一个测试集上的预测结果做对比观察准确率差异和训练轮数的关系。能明显看到 ResNet 在收敛速度和最终准确率上的优势——这就是「resnet 是 cnn 吗」这个问题最直观的答案它是 CNN但是一个更能扛深度的 CNN。从那以后我每次搭残差结构都会强制走一遍「先打印 shape再跑单批次前向」的流程这十几分钟的检查帮我省掉了后面好几个小时的瞎调参。这份资源的代码组织和数据预处理思路整体上是可以直接复用的希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑