简介: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 python=3.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 numpy==1.19.5 h5py==3.1.0 matplotlib==3.3.4 tensorflow==2.4.0为什么要锁版本号?这里有个常见的翻车点:新版 numpy(1.24+)移除了np.float等旧别名,而很多课程代码里会用到np.float;新版 h5py(3.8+)对旧格式 h5 文件的兼容性也没有老版本稳。TensorFlow 2.4.0 是 Python 3.7 环境下比较稳定的一个版本,既支持tf.keras导入方式,也保留了足够的兼容性。如果你机器是 M 系列芯片的 Mac,直接 pip 装会有兼容问题,建议用 conda 安装tensorflow=2.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,那就不用动。更省事的方案是统一装keras==2.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_utils
cnn_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(shape=input_shape) # 第一组卷积:8 个 3x3 滤波器,提取浅层边缘特征 x = tf.keras.layers.Conv2D(8, (3, 3), strides=(1, 1), padding='same')(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), padding='same')(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, activation='relu')(x) outputs = tf.keras.layers.Dense(6, activation='softmax')(x) model = tf.keras.Model(inputs=inputs, outputs=outputs, name='simple_cnn') return model这段代码的关键参数在卷积层:Conv2D(8, (3, 3))意思是第一层用 8 个 3×3 卷积核,padding='same'保证卷积后图像尺寸不变。为什么用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), padding='same')(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), padding='same')(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), padding='same')(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 里按 Ctrl+Shift+R,选择项目目录,替换所有匹配项,然后删除__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 和输入尺寸在个人电脑上可能超出显存;如果用的是 CPU,ResNet 训练速度会非常感人。
解决:先把batch_size从 64 调到 32 甚至 16,显存紧张时 8 也能接受。其次把epochs调小,验证网络能否跑通不需要 50 个 epoch,5-10 个 epoch 看到 loss 在下降就能确认代码没问题。如果本机实在跑不动,建议把代码和数据集整体搬到云 GPU 环境上,成本低见效快。
6. 装完怎么验证跑通:三分钟检查清单
代码不报错不等于训练正确,验证要按“环境→数据→单步训练→完整训练”四层走。
第一层验证环境:在 Python 交互环境执行import tensorflow as tf; print(tf.__version__),输出 2.x 且不报错,说明环境就绪。
第二层验证数据:运行load_dataset()后打印返回值的 shape,X应该是(m, height, width, channels)的四维数组,Y的 one-hot 形状第二维等于分类数。对不上就说明工具函数调错了。
第三层验证单步训练:在模型编译后加一行model.fit(X_train, Y_train, epochs=1, batch_size=16),能看到一条 loss 数值输出,就说明前向传播、反向传播、梯度更新全部走通。很多代码问题在这一步就会现出原形,不要直接跑几十个 epochs。
第四层验证完整流程:跑完test.py,记录最后的训练集准确率和测试集准确率。第一周 CNN 在手语六分类上测试集准确率一般能在 0.7 以上;第二周 ResNet 在笑脸二分类上准确率能到 0.9 以上。数值明显低于这个区间,优先回去查归一化和 one-hot 编码。准确率才 0.2 的话,基本可以认定是训练流程没跑通,而不是模型能力问题。
从那以后我每次拿到这类课程代码,都强制自己按“环境版本-check 一遍、数据 shape-check 一遍、单步训练-check 一遍”的流程走,十分钟能定位完所有环境问题。这套简易 CNN 和 ResNet 搭建资源最大的价值就是让你在一个可复现的环境里看清卷积网络从浅到深的变化过程,希望帮到你。
本文还有配套的精品资源,点击获取