从环境配置到代码复现:简易CNN与ResNet搭建实战指南

发布时间:2026/10/19 7:07:45
从环境配置到代码复现:简易CNN与ResNet搭建实战指南
简介CNN与ResNet是深度学习中两个既有联系又有区别的重要结构。这份Python源码包面向深度学习初学者与入门进阶者以两周为学习路径用PyTorch或TensorFlow从零搭建简易CNN和ResNet。通过训练与调参流程帮助读者理解CNN中卷积、池化、全连接层的组织方式以及ResNet如何借助残差块和跳跃连接解决深层网络的梯度消失问题从而明确ResNet是CNN的扩展改进版本而非与其割裂的另一类模型。资源共24个文件压缩包约25.27MB以py源码脚本、h5数据集与权重文件、xml工程配置为主便于直接运行、查看训练过程和验证结果按周划分的目录结构让从CNN到ResNet的过渡更循序渐进。目前已有1099人学习适合作为图像分类任务入门、课程实验或自学参考的实用素材学习者可对照源码与数据集快速掌握搭建、训练和验证流程。1. 从课程作业到能跑的代码先别急着双击 test.py很多第一次接触简易 CNN 和 ResNet 搭建的人拿到这份 Python 源码后的第一反应是直接跑 test.py结果十个人里有八个卡在环境上——要么 Python 版本不对要么 TensorFlow 和 Keras 的导入路径对不上要么 h5 数据集路径读不到。这套资源的价值恰恰不在“代码多高级”而在于它把第一周的 CNN 基线和第二周的 ResNet 扩展按周组织好代码量控制在能读懂的范围内非常适合刚上完深度学习课、需要交作业或者复现模型的开发者。你不需要先啃完整本深度学习教材只要能装好环境、改对路径、按顺序把两个星期的代码跑通模型结构和训练流程就都清楚了。本文会从环境版本、目录解读、代码参数一路讲到常见坑和验证方法照着走完就能跑通。2. 环境准备先把 Python 和 TensorFlow 的版本账算清楚2.1 看见 cpython-37.pyc就知道这套代码要 Python 3.7解压这个压缩包后第一件事不是看代码而是看__pycache__目录里的文件名。里面躺着一个cnn_utils.cpython-37.pyc这个37就是 Python 3.7 的字节码标记。也就是说这套代码最早是在 Python 3.7 环境下写出来并跑通过的。你如果本机装的是 Python 3.10 或 3.11直接运行大概率会遇到语法兼容问题或者依赖库装不上。我一般会在项目根目录单独建一个虚拟环境把 Python 版本固定在 3.7。创建命令conda create -n dl_course python3.7 -y conda activate dl_course这里用 conda 而不是 venv是因为 Python 3.7 的某些依赖包尤其是 TensorFlow 1.x 的 wheel 包在 pip 里可能需要额外处理conda 的预编译包管理更省心。如果你机器上没装 conda也可以用pyenv装 3.7或者直接装 Anaconda。版本别贪新这套代码的依赖是围绕 3.7 写的用 3.8、3.9 虽然大部分情况下也能跑但遇到底层库冲突时排查成本会翻倍。2.2 依赖清单numpy、h5py、matplotlib、tensorflow数据加载靠 h5py数值计算靠 numpy画损失曲线靠 matplotlib模型搭建靠 tensorflow。这几个就是全部重依赖。在激活了 3.7 环境的终端里执行pip install numpy1.19.5 h5py3.1.0 matplotlib3.3.4 tensorflow2.4.0为什么要锁版本号这里有个常见的翻车点新版 numpy1.24移除了np.float等旧别名而很多课程代码里会用到np.float新版 h5py3.8对旧格式 h5 文件的兼容性也没有老版本稳。TensorFlow 2.4.0 是 Python 3.7 环境下比较稳定的一个版本既支持tf.keras导入方式也保留了足够的兼容性。如果你机器是 M 系列芯片的 Mac直接 pip 装会有兼容问题建议用 conda 安装tensorflow2.4的 arm64 版本或者换用 Google Colab 跑。2.3 TensorFlow 与 Keras 的版本匹配keras 到底该从哪导入这是这套代码最容易踩的坑。第一周的test.py导入方式可能是from keras.layers import Conv2D也可能写的是from tensorflow.keras.layers import Conv2D。这两种写法在 TensorFlow 2.0 之后有本质区别独立的keras包和tensorflow.keras是两个不同的东西。TensorFlow 2.4 内置了 Keras API正确姿势是用tensorflow.keras如果在 TensorFlow 2.x 环境里强制装独立的keras库两个版本会互相打架模型层定义和训练接口经常出现莫名其妙的属性报错。我拿到这套代码后的习惯是打开每个.py文件先看导入语句。如果是from keras.layers需要全局替换成from tensorflow.keras.layers如果是from tensorflow.keras那就不用动。更省事的方案是统一装keras2.4.0让独立 keras 包和 TensorFlow 内部版本保持一致两种导入方式都能用——但这只是过渡方案从长期看还是建议用tensorflow.keras因为后续版本迭代方向就是合并。提示改完导入路径后记得删除项目里的__pycache__目录再重新运行否则 Python 优先加载旧的 .pyc 字节码缓存改了源码也等于白改。3. 资源结构与数据准备先把目录和 h5 文件认清楚3.1 两周代码的文件组织逻辑压缩包解压后根目录下是两个按周组织的文件夹1第一周和2第二周外加各自带一个.idea目录——那是写代码的人当时用某 IDE 留下的配置内容只是编辑器状态和项目索引信息跑代码完全用不到可以直接忽略。1第一周里的核心是cnn_utils.py和test.py。cnn_utils.py提供工具函数test.py是模型定义和训练的入口。2第二周里则是resnets_utils.py、kt_utils.py和test.py。每个周的目录都自带一个datasets子目录里面放着对应实验的 h5 数据集文件。工作目录的组织规律是把代码文件和 datasets 放在同级运行test.py时的当前路径决定了数据集能不能被找到。我在实际跑代码时会先做一件事把两个周的.idea目录全部删掉只保留.py和datasets。这样目录干净也避免 IDE 配置文件里记录的绝对路径干扰判断——之前遇到过某 IDE 的 workspace.xml 里写死了旧环境路径导致我以为代码有问题排查半天结果是编辑器加载了旧配置。3.2 三个工具模块cnn_utils、resnets_utils、kt_utilscnn_utils.py是第一周 CNN 实验的后勤保障主要提供加载手语数据集、随机打乱数据、one-hot 编码这三个功能。核心函数包括load_dataset()和convert_to_one_hot()前者读取 h5 文件并把图像数据转成适合输入卷积层的四维张量格式后者把分类标签转成 one-hot 向量。resnets_utils.py是第二周 ResNet 实验的数据加载工具除了加载签名数据集还提供了load_happy_dataset()这样的辅助函数。kt_utils.py则专门负责加载笑脸数据集文件名叫kt_utils是因为承接的上一份代码是某个课程的作业框架这个模块只干一件事读取train_happy.h5和test_happy.h5返回已经归一化好的图像数据。这三个工具模块不需要你自己改代码但要明白它们各自服务于哪个实验第一周 CNN 用手语数据集train_signs.h5 / test_signs.h5第二周 ResNet 同时用到手语数据集和笑脸数据集train_happy.h5 / test_happy.h5。写作业时经常有人把kt_utils加载的数据传给 CNN 模型然后困惑为什么维度对不上——这是因为工具函数返回的数据形状是按模型设计好的用错工具模块就等于拿错了数据。3.3 datasets 里五个 h5 文件都是给谁用的datasets 目录下总共有五个 h5 文件train_signs.h5、test_signs.h5在两个周里都出现train_happy.h5和test_happy.h5只在第二周出现。文件角色对照文件用途标签含义train_signs.h5 / test_signs.h5第一周 CNN 和第二周 ResNet 的手语数字识别0-5 六个手势类别train_happy.h5 / test_happy.h5第二周 ResNet 的笑脸分类0 非笑脸1 笑脸h5 文件本质是分层数据格式PyTables 和 h5py 都能读。这类课程数据集通常只包含两个 key一个存图像数组一个存标签数组。加载时如果不确定内部结构可以先用h5py打开看一眼 key 名。这个习惯比盲改代码有用得多——我见过有人改了十行代码发现跑不通最后发现仅仅是 h5 里的 key 名字和他以为的不一样。4. 核心代码解读从 CNN 到 ResNet 是怎么搭起来的4.1 CNN 基线模型卷积、激活、池化、全连接一条线走下来第一周的test.py里定义了一个常规的卷积神经网络分类模型。去掉封装后模型主体结构大概是这样的def convolutional_model(input_shape): # 输入层接受 64x64 的三通道图像 inputs tf.keras.Input(shapeinput_shape) # 第一组卷积8 个 3x3 滤波器提取浅层边缘特征 x tf.keras.layers.Conv2D(8, (3, 3), strides(1, 1), paddingsame)(inputs) x tf.keras.layers.Activation(relu)(x) # 第一组池化2x2 最大池化尺寸从 64x64 降到 32x32 x tf.keras.layers.MaxPooling2D(pool_size(2, 2))(x) # 第二组卷积16 个 3x3 滤波器提取更抽象的特征 x tf.keras.layers.Conv2D(16, (3, 3), strides(1, 1), paddingsame)(x) x tf.keras.layers.Activation(relu)(x) # 第二组池化尺寸从 32x32 降到 16x16 x tf.keras.layers.MaxPooling2D(pool_size(2, 2))(x) # 展平把 16x16x16 的特征图拉成一维向量 x tf.keras.layers.Flatten()(x) # 全连接层先降维到 128 个神经元再接输出层 x tf.keras.layers.Dense(128, activationrelu)(x) outputs tf.keras.layers.Dense(6, activationsoftmax)(x) model tf.keras.Model(inputsinputs, outputsoutputs, namesimple_cnn) return model这段代码的关键参数在卷积层Conv2D(8, (3, 3))意思是第一层用 8 个 3×3 卷积核paddingsame保证卷积后图像尺寸不变。为什么用same而不是valid因为后面紧跟池化层如果卷积把尺寸缩了池化再缩一层特征图会快速变得太小影响小数据集上的表现。激活函数用的 ReLU它的作用是给线性卷积引入非线性。池化层选择最大池化而不是平均池化因为在图像分类任务里最大池化保留了区域内的最强响应对边缘、角点这类特征更敏感。训练时把输入图像 resize 到 64×64三个通道对应 RGB。这个模型对应摘要里说的 CNN 五个组成部分输入层接收 64×64×3 的原始图像卷积层提取特征ReLU 提供非线性池化层降维全连接层做最终分类。第一周跑通这个模型的核心收获不是模型有多强而是看明白数据从四维张量到一维向量再到 6 个类别概率的完整流动过程。4.2 ResNet 残差块跳跃连接改写了梯度传播第二周的核心是残差块。和第一周 CNN 的直筒式结构不同残差块在卷积路径之外加了一条“短路”这条短路把输入直接加到输出上def identity_block(X, filters): # 提取三个卷积层的滤波器数量 f1, f2, f3 filters # 保存输入用于后面的残差连接 X_shortcut X # 三个卷积层1x1 降维、3x3 卷积、1x1 升维 X tf.keras.layers.Conv2D(f1, (1, 1), strides(1, 1), paddingsame)(X) X tf.keras.layers.BatchNormalization()(X) X tf.keras.layers.Activation(relu)(X) X tf.keras.layers.Conv2D(f2, (3, 3), strides(1, 1), paddingsame)(X) X tf.keras.layers.BatchNormalization()(X) X tf.keras.layers.Activation(relu)(X) X tf.keras.layers.Conv2D(f3, (1, 1), strides(1, 1), paddingsame)(X) X tf.keras.layers.BatchNormalization()(X) # 残差连接输入直接加到卷积输出上 X tf.keras.layers.Add()([X, X_shortcut]) X tf.keras.layers.Activation(relu)(X) return X这里三个卷积层的通道数是 64、64、256或者按具体网络深度调整。第一层 1×1 卷积负责降维把 256 个通道压到 64第二层 3×3 卷积在低维空间提取特征第三层 1×1 卷积再把通道升回 256。这种“降维-卷积-升维”的瓶颈结构是为了让 3×3 卷积在更少的通道数上计算大幅减少参数量和计算量。残差连接的核心价值在反向传播没有跳跃连接时梯度要依次穿过每一层深度一上去梯度就越传越小有了跳跃连接梯度多了一条直达通道可以直接从输出流回输入。这就是摘要里所说的“解决了深度网络中的梯度消失问题”。4.3 一个概念澄清ResNet 是 CNN 的进化版不是另一套网络第二周代码里除了identity_block还有convolutional_block两者区别在于输入输出维度是否一致。identity_block的输入输出通道数相同可以直接相加convolutional_block的输入输出通道数不同所以短路路径上多了一个 1×1 卷积来调整维度。架构上 ResNet 叠加的依旧全是卷积层、池化层、激活层和第一周 CNN 没有本质区别。所以“resnet 是 cnn 吗”这个问题的答案ResNet 就是 CNN 的一种实现方式但它通过残差结构扩展了 CNN 的深度极限。传统 CNN 在层数超过二十层后训练误差反而升高ResNet 通过跳跃连接让上百层的网络也能稳定收敛。这种关系理解到位了再看代码就不会把两份模型文件当成两套完全不同的东西——第一周 CNN 是基础第二周 ResNet 是在基础上的深度扩展。5. 踩坑记录与常见问题排查5.1 from keras.layers 导入报 ModuleNotFoundError现象运行test.py第一行直接报ModuleNotFoundError: No module named keras。原因你的环境里只装了 TensorFlow而 TensorFlow 2.x 把 Keras 收编成了tf.keras不再单独提供keras模块或者你之前装的是 Keras 3.x它和 TensorFlow 2.4 不兼容。解决把每个.py文件里的from keras.layers全局替换成from tensorflow.keras.layers。在 PyCharm 里按 CtrlShiftR选择项目目录替换所有匹配项然后删除__pycache__再重跑。注意 batch normalization 等层所在的keras.layers也要一起替换不然第二个报错接着出现。5.2 训练时 Loss 不动准确率一直停在某个固定值现象打印的 loss 前几个 epoch 下降很慢或者完全不变准确率维持在 0.2、0.3 这种低水平。原因最常见的原因是 h5 数据没有归一化。像素值范围在 0-255直接送进网络会让梯度更新幅度过小另一个常见原因是标签没有做 one-hot 编码而模型的输出层用的是 softmax。解决检查工具函数里有没有把图像除以 255。没有的话在load_dataset()返回数据后手动加一行X X / 255.。检查标签形状如果是(m, 1)的整数数组需要先转成(m, classes)的 one-hot。这个环节属于代码里最容易被省略的细节我每次拿到新数据集都会先跑print(X.shape, X.min(), X.max())确认输入范围。5.3 加载 h5 报 OSError: Unable to open file现象运行到第几行突然冒出OSError: Unable to open file (unable to open file: name datasets/train_signs.h5)。原因工作目录不对。代码里load_dataset()用的是相对路径datasets/train_signs.h5如果你的终端当前目录不在项目根目录Python 按当前进程的工作路径去找文件自然找不到。解决在命令行cd到对应的周目录再运行或者在test.py开头把数据集路径改成绝对路径。推荐未来用绝对路径因为 IDE 运行按钮默认的工作目录往往和命令行不一样用绝对路径一劳永逸。我一般会在写完 load 调用后立刻打印数据集 shape 来验证路径是否真的对了。5.4 训练太慢或显存不足模型跑不起来现象第一周 CNN 还好第二周 ResNet 一跑就报ResourceExhaustedError或者训练时间长得离谱。原因ResNet 的卷积层更宽更深默认 batch size 和输入尺寸在个人电脑上可能超出显存如果用的是 CPUResNet 训练速度会非常感人。解决先把batch_size从 64 调到 32 甚至 16显存紧张时 8 也能接受。其次把epochs调小验证网络能否跑通不需要 50 个 epoch5-10 个 epoch 看到 loss 在下降就能确认代码没问题。如果本机实在跑不动建议把代码和数据集整体搬到云 GPU 环境上成本低见效快。6. 装完怎么验证跑通三分钟检查清单代码不报错不等于训练正确验证要按“环境→数据→单步训练→完整训练”四层走。第一层验证环境在 Python 交互环境执行import tensorflow as tf; print(tf.__version__)输出 2.x 且不报错说明环境就绪。第二层验证数据运行load_dataset()后打印返回值的 shapeX应该是(m, height, width, channels)的四维数组Y的 one-hot 形状第二维等于分类数。对不上就说明工具函数调错了。第三层验证单步训练在模型编译后加一行model.fit(X_train, Y_train, epochs1, batch_size16)能看到一条 loss 数值输出就说明前向传播、反向传播、梯度更新全部走通。很多代码问题在这一步就会现出原形不要直接跑几十个 epochs。第四层验证完整流程跑完test.py记录最后的训练集准确率和测试集准确率。第一周 CNN 在手语六分类上测试集准确率一般能在 0.7 以上第二周 ResNet 在笑脸二分类上准确率能到 0.9 以上。数值明显低于这个区间优先回去查归一化和 one-hot 编码。准确率才 0.2 的话基本可以认定是训练流程没跑通而不是模型能力问题。从那以后我每次拿到这类课程代码都强制自己按“环境版本-check 一遍、数据 shape-check 一遍、单步训练-check 一遍”的流程走十分钟能定位完所有环境问题。这套简易 CNN 和 ResNet 搭建资源最大的价值就是让你在一个可复现的环境里看清卷积网络从浅到深的变化过程希望帮到你。本文还有配套的精品资源点击获取