☰
深度学习四架构开篇:TensorFlow+Keras实现CNN图像分类全流程
2026/10/14 9:00:45 网站建设 项目流程

1. 四个核心网络架构的定位与学习路线

在动手写代码之前,先用几分钟把标题里这四个架构串起来讲清楚。这一篇是整个系列的开篇,我把它定位成“搭好框架 + 跑通第一条完整链路”:先把开发环境、数据处理流程、模型训练评估的整个闭环走一遍,然后完完整整实现一个CNN作为主力示例。Transformer、GAN和胶囊网络会在后几篇中逐个展开,但第一篇里我会把这些架构之间的关联讲透,这样你到后面不会觉得每个网络都是孤立的知识点。

先说结论:CNN 是视觉任务的绝对主力,Transformer 是序列建模和大模型的基础骨架,GAN 解决的是数据生成问题,而胶囊网络是对 CNN 的一种结构改进尝试。四者之间并不是互斥的竞争关系,而是一条演化链。CNN 在图像特征提取上的成功,让研究者思考“能不能用注意力机制替代卷积”,这才有了视觉 Transformer;GAN 的生成器里通常也内嵌 CNN 来做图像生成;胶囊网络则是从 CNN 的池化缺陷出发,试图用向量化的胶囊表达更精确的空间层次关系。

所以系列的学习顺序也是有讲究的:先CNN,再Transformer,然后GAN,最后胶囊网络。CNN 能帮你建立“卷积核感受野、特征图、空间不变性”这些基础直觉,理解这些之后,Transformer 的注意力机制才能真正读懂——它其实就是一种全局动态感受野;GAN 的对抗训练逻辑和CNN、Transformer 完全不同,单独开一篇更合适;胶囊网络实现细节多、踩坑率高,放到最后当进阶篇,技术上最合理。

我用一个表格把这四个架构的核心信息先汇总,后面详细展开:

架构适用场景核心思想典型结构组件
CNN图像分类、目标检测、图像分割局部感受野 + 权值共享 + 池化卷积层、激活层、池化层、全连接层
Transformer自然语言处理、视觉特征建模自注意力机制 + 位置编码Multi-Head Attention、FFN、LayerNorm
GAN图像生成、风格迁移、超分生成器与判别器的对抗训练Generator、Discriminator
胶囊网络姿态估计、重叠目标的识别胶囊向量 + 动态路由PrimaryCaps、DigitCaps、Routing

2. 环境准备:版本搭配决定你的试错成本

2.1 为什么我用 TensorFlow 2.x + Keras 的组合

这个组合最大的优势就在一个词:省心。Keras 从 TensorFlow 2.0 开始就已经完全内置为官方高级 API,不再需要像早期那样单独安装。你写from tensorflow import keras就是在用 Keras,它的 Sequential 和 Model 子类化 API 能把模型搭建、训练、评测的复杂度藏得很深,做原型验证和教学讲解效率极高。

从严格的工程角度看,TensorFlow 2.x 采用 Eager Execution(动态图)机制,这意味着你不需要像 TensorFlow 1.x 时代那样先构建计算图再塞进 Session 里执行,而是像写普通 Python 代码一样逐行执行。调试体验也比 PyTorch 之外的其他框架更接近直觉。虽然 PyTorch 在研究圈里用得多,但TensorFlow + Keras 的组合在工业部署(TF Serving、TF Lite、TensorFlow.js)这条链路上更完整。第一篇选它作为主框架,安装成本低、报错信息友好、遇到问题时社区资料最丰富。

2.2 版本选型的硬性建议

这里我把踩过的坑直接说在前面:不要追求最新版本,追求稳定组合。我推荐的搭配是Python 3.10.x + TensorFlow 2.10.x + CUDA 11.2(可选)+ cuDNN 8.x(可选)。

为什么是这个组合?TensorFlow 2.10 是我测试下来 CPU 和 GPU 体验最均衡的一个版本,而且从 2.11 开始,Windows 原生 GPU 支持变得很别扭,需要走 Windows Subsystem for Linux (WSL2) 方案,对新手非常不友好。所以如果你是用 Windows 且想用 GPU,建议锁在 2.10 或更低;如果你本来就用的 WSL2 或 Linux 系统,那么 2.12 到 2.15 也可以接受。

安装命令很简单:

python -m venv tf_env source tf_env/bin/activate # Windows下用 tf_env\Scripts\activate pip install tensorflow==2.10 pip install numpy pandas matplotlib jupyter

GPU 版本验证安装是否成功的命令如下,CPU 版跑出来结果是空列表,GPU版跑出来能看到显卡型号:

import tensorflow as tf print("TF version:", tf.__version__) print("GPU available:", tf.config.list_physical_devices("GPU"))

2.3 一个小实验验证环境没问题

环境配好之后,先跑一个最简单的张量运算验证核心链路:

import tensorflow as tf # 构造一个随机张量并做矩阵乘法 a = tf.random.normal([3, 4]) b = tf.random.normal([4, 5]) c = tf.matmul(a, b) print(c.shape)

这一步能跑通说明 TensorFlow 核心安装没问题。如果报DLL load failed之类的错误,多半是 Visual C++ 运行库缺失,去官方渠道装一下最新的 VC_redist 就能解决。

注意:如果遇到 pip 下载慢甚至超时,可以临时换用国内 PyPI 镜像源,但是建议只做临时切换,装完核心依赖后记得改回默认源,避免后续其他包出现奇怪的解析问题。

3. 第一个完整实验:用 CNN 在 MNIST 上做手写数字分类

3.1 数据准备:Keras 内置数据集究竟怎么用

环境就绪之后,需要一个不能太复杂、但能完整体现训练流程的数据集。MNIST 手写数字集是最经典的选择:28×28 的灰度图像,训练集 60000 张,测试集 10000 张,总共 10 个类别(数字 0-9)。它唯一的缺点是太简单了——但我恰恰需要这个“简单”,因为在第一篇里,你要把注意力放在“理解网络是怎么工作的”上,而不是纠结数据预处理的各种边界问题。

Keras 提供了直接下载和加载数据集的 API,不需要手动下载文件:

from tensorflow.keras import datasets (x_train, y_train), (x_test, y_test) = datasets.mnist.load_data()

对照我的数据预处理常规操作:

# 1. 归一化:像素值从 [0, 255] 压到 [0, 1] x_train = x_train.astype("float32") / 255.0 x_test = x_test.astype("float32") / 255.0 # 2. 增加通道维:从 (60000, 28, 28) 变为 (60000, 28, 28, 1) x_train = x_train.reshape(-1, 28, 28, 1) x_test = x_test.reshape(-1, 28, 28, 1) # 3. 标签改成 one-hot 编码:3 -> [0, 0, 0, 1, 0, 0, 0, 0, 0, 0] y_train = tf.keras.utils.to_categorical(y_train, 10) y_test = tf.keras.utils.to_categorical(y_test, 10)

这里三个步骤分别解释一下,因为很多人就是在这里开始懵的。归一化不用多说,神经网络对输入的尺度很敏感,另外更大的数值范围并不是天然优势。增加通道维是因为 Keras 的Conv2D层要求输入格式是(height, width, channels),灰度图只有 1 个通道,所以必须在最后加一个维度。One-hot 编码是把整数标签变成向量,配合categorical_crossentropy损失函数来用;如果你坚持用整数标签,就得换sparse_categorical_crossentropy,两者千万别混。

3.2 模型结构:从零构建一个精悍可扩展的 CNN

现在搭模型。我用一套适合新手理解的 CNN 结构,但为了后面扩展 Transformer 和胶囊网络时不别扭,特意把结构设计得模块化一些:

from tensorflow.keras import layers, models def build_cnn(input_shape=(28, 28, 1), num_classes=10): model = models.Sequential([ # 第一个卷积块 layers.Conv2D(32, (3, 3), padding='same', activation='relu', input_shape=input_shape), layers.Conv2D(32, (3, 3), padding='same', activation='relu'), layers.MaxPooling2D(pool_size=(2, 2)), # 第二个卷积块 layers.Conv2D(64, (3, 3), padding='same', activation='relu'), layers.Conv2D(64, (3, 3), padding='same', activation='relu'), layers.MaxPooling2D(pool_size=(2, 2)), # 第三层卷积不用池化,直接过渡到全连接 layers.Conv2D(128, (3, 3), padding='same', activation='relu'), # 将三维特征图展平为一维向量 layers.Flatten(), layers.Dense(256, activation='relu'), layers.Dropout(0.5), layers.Dense(num_classes, activation='softmax') ]) return model model = build_cnn() model.summary()

几个设计决策值得展开讲讲,因为这决定了模型的性能上限和训练稳定性。

为什么卷积核都用 3×3?视觉领域长期实践发现,两个连续 3×3 卷积堆叠能达到 5×5 卷积的感受野,但参数量更少、非线性更强,训练起来也更稳。这不是什么高深理论,就是工程上的划算选择。

为什么会用到 Dropout?全连接层参数量大,很容易过拟合。Dropout 在训练时随机扔掉一半神经元,迫使网络学到更鲁棒的特征。预测时它会自动失效。放在全连接层之间是常规操作,放卷积层中间效果通常没那么明显。

为什么三个卷积块之后直接 Flatten 就进全连接?卷积层在提取空间特征,Flatten 后特征图变成一维向量,交给全连接层做最终分类决策。对 MNIST 来说,128 个 7×7 特征图展平后是 128×7×7=6272 维,并不算高维,所以不需要全局平均池化来压缩,直接接全连接完全没问题。如果是处理更大尺寸的图像,我应该会在 Flatten 之前加一层GlobalAveragePooling2D,以免全连接参数爆炸。

3.3 训练配置与完整训练流程

模型搭好之后,编译和训练的配置是决定训练质量的核心环节:

model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3), loss='categorical_crossentropy', metrics=['accuracy'] )

为什么用 Adam 而不是更传统的 SGD?Adam 能自适应调整每个参数的学习率,在 CNN 训练中收敛速度明显更快,而且对学习率初始值不那么敏感,对新手更友好。SGD 往往需要人工设计学习率衰减策略和动量配置,调参周期长,对刚刚起步的深度学习者不太合适。后续如果追求更高的最终精度,再换回 SGD 微调也不迟——很多经典模型确实靠 SGD 冲最后的精度尖峰。

训练过程的完整代码如下,这里用validation_split直接从 60000 张训练集里分出 10% 做验证,不需要单独处理验证集:

history = model.fit( x_train, y_train, batch_size=128, epochs=10, validation_split=0.1, shuffle=True, verbose=1 )

Batch size 选择 128 有几个理由:MNIST 单张图像很小,128 这个批次大小在显存和训练速度之间是很平衡的中间值。它同时意味着每个 epoch 大约有60000 × 0.9 / 128 ≈ 422个批次,10个epoch跑完也就是4000多次参数更新,在普通CPU上十几分钟也能跑完,体验非常合适。

3.4 训练后的效果评估与关键指标解读

训练结束后,先看测试集上的整体表现:

test_loss, test_acc = model.evaluate(x_test, y_test) print(f"Test accuracy: {test_acc:.4f}")

在我的实测中,上面这个结构跑到第 10 个 epoch,测试集准确率能到 99% 左右。如果你的结果明显低于 98%,大概率是数据预处理某个环节出了问题,优先检查归一化和 one-hot 编码是否正确。

但只看准确率是远远不够的。我建议新手养成两个好习惯:

第一,查看混淆矩阵。准确率会掩盖某些类别的表现差异,而混淆矩阵能直接暴露哪些数字之间容易混淆:

import numpy as np from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt y_pred = model.predict(x_test) y_pred_classes = np.argmax(y_pred, axis=1) y_true_classes = np.argmax(y_test, axis=1) cm = confusion_matrix(y_true_classes, y_pred_classes) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues') plt.xlabel('Predicted') plt.ylabel('True') plt.show()

第二,可视化训练曲线。history.history里存着每个 epoch 的 loss 和 accuracy,把它们画出来可以判断模型是否过拟合或欠拟合:

plt.figure(figsize=(12, 4)) plt.subplot(1, 2, 1) plt.plot(history.history['loss'], label='train_loss') plt.plot(history.history['val_loss'], label='val_loss') plt.legend() plt.title('Loss Curves') plt.subplot(1, 2, 2) plt.plot(history.history['accuracy'], label='train_acc') plt.plot(history.history['val_accuracy'], label='val_acc') plt.legend() plt.title('Accuracy Curves') plt.show()

4. 深入理解 CNN 的关键机制:卷积、池化与感受野

4.1 卷积层里的运算逻辑与参数含义

卷积到底在干什么?用一个生活场景来类比:想象你拿一个放大镜在照片上滑动,每次看到一个小区域,放大镜的框就是卷积核(通常 3×3 或 5×5),滑动步长就是 stride。每滑到一个位置,放大镜里的像素值和卷积核权重做加权求和,得到一个输出值,这就完成了一次卷积操作。放大镜滑完整个图像,形成一张新的“特征图”。

关键参数有三个:

  • filters:这一层用多少个卷积核。每个卷积核学习捕捉一种特征模式,比如边缘、圆角、交叉线等。32 个卷积核就代表这层输出 32 张特征图。
  • kernel_size:卷积核尺寸。3×3 是最常用的小卷积核,堆叠小卷积核比直接用大卷积核更高效。
  • padding='same':在图边缘补零,保持输出特征图尺寸与输入一致。如果不用 padding,每过一层卷积,图的尺寸都会缩小kernel_size - 1个像素,多个层堆叠后边缘信息丢失得很严重。

配合激活函数非常关键。代码里用的是 ReLU(修正线性单元),它的公式极简单:x > 0 时输出 x,反之输出 0。它消除了负值,让网络具备学习非线性特征的能力,同时避免了 sigmoid 家族在深层网络里常见的梯度消失问题。

4.2 池化的作用:不是压缩而是特征筛选

MaxPooling2D 池化层做一件很简单的事:在一个 2×2 窗口内取最大值,滑动步长为 2,于是特征图尺寸缩小一半。为什么是取最大值而不是平均值?最大池化保留的是“这个区域内最强烈的激活响应”,也就是说它保留了最强的特征信号,丢弃掉相对微弱的响应,这在视觉任务里往往更符合“检测关键模式”的诉求。平均池化更平滑,在部分场景有优势,但 MaxPooling 目前仍是主流选择。

注意,池化层没有可学习参数,它只是一个固定的下采样操作。它带来的额外好处是让特征对小幅平移更鲁棒——图像平移几个像素后,某个区域的最大激活值大概率保持不变,这让 CNN 具备一定程度的平移不变性。

4.3 从输入到输出:CNN 的空间尺寸变化全过程

我们以这个模型为例,按 28×28 输入完整走一遍:

第一卷积块:28×28 → 两个 padding='same' 的 3×3 卷积,输出仍为 28×28×32 → 2×2 最大池化后变成 14×14×32。第二卷积块:14×14 → 两次卷积保持 14×14×64 → 池化后 7×7×64。第三个卷积层仍是 3×3 卷积(padding='same'),输出 7×7×128。Flatten 展平后面是 256 个神经元的全连接层,最终经过 softmax 输出 10 类概率分布。

从感受野的角度看,更深层的神经元能看到输入图像更大的区域。第一层卷积核直接看到 3×3 的区域;堆叠两层后,第二层输出的每个点对应输入约 5×5 的区域;经过池化后,第三层能看到更大的范围。这就是深度 CNN 能够从局部细节到全局结构逐层抽象空间特征的原因。

我还可以在代码里直接打印每一层输出的张量形状来加深理解:

from tensorflow.keras.utils import plot_model plot_model(model, to_file='cnn_architecture.png', show_shapes=True, dpi=64)

运行这一步会生成一张清晰的网络结构图,建议训练之前先看一遍这张图,确认自己对每层输出形状有预期,再往下走。

5. 训练过程的实时监控与过拟合识别

5.1 训练日志里的信息到底在说什么

实际训练时,终端的 verbose=1 会输出类似这样的信息:

Epoch 1/10 422/422 [==============================] - 12s 28ms/step - loss: 0.2038 - accuracy: 0.9366 - val_loss: 0.0779 - val_accuracy: 0.9768 Epoch 2/10 422/422 [==============================] - 11s 26ms/step - loss: 0.0710 - accuracy: 0.9782 - val_loss: 0.0449 - val_accuracy: 0.9863

解读几个容易困惑的点。422/422表示一个 epoch 内的批次进度;loss是训练集上的损失;accuracy是训练集准确率;val_loss和val_accuracy是验证集上的表现。重点关注的是验证集指标——训练集准确率再高,都只是说明模型记住了训练数据。

一个比较理想的情况是:训练集和验证集准确率同时缓慢上升,差距维持在 2% 以内;val_loss 在前几个 epoch 下降,之后趋于平稳。如果出现训练集准确率接近 100% 但验证集准确率停滞甚至下降的情况,就是典型的过拟合,需要减少层数或参数、增加 Dropout,或者引入数据增强。

5.2 回调函数:训练早停与模型自动保存

训练 MNIST 这个规模的任务,10 个 epoch 还好,但以后跑更大的模型你就知道人工盯训练日志有多不靠谱。这里引入两个非常实用的 Keras 回调函数:

from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint callbacks = [ EarlyStopping(monitor='val_loss', patience=3, restore_best_weights=True), ModelCheckpoint('best_cnn.h5', monitor='val_loss', save_best_only=True) ] history = model.fit( x_train, y_train, batch_size=128, epochs=50, # 设大一点,让 EarlyStopping 决定何时停 validation_split=0.1, callbacks=callbacks )

EarlyStopping 的逻辑很简单:如果连续 patience 个 epoch 验证集损失没有改善,就停止训练并自动恢复历史最佳权重。这样你不需要纠结 epoch 到底设多少——设一个大的上限就行。ModelCheckpoint 配合save_best_only=True的话,只有验证集损失创新低时才会覆盖保存模型文件。这两个回调是深度学习中控制训练时长的标配组合。

5.3 模型保存与推理:训练完之后做什么

训练结束后的标准操作是保存模型和写推理代码。保存有两种常用格式:

# 方式一:保存完整模型(结构 + 权重 + 优化器状态),用于后续继续训练 model.save('mnist_cnn_full.h5') # 方式二:只保存权重,体积更小,加载时须有相同结构 model.save_weights('mnist_cnn_weights.h5')

推理代码就简单了。加载单张图片,预处理成 1×28×28×1 的批量格式,然后模型返回类别概率:

new_model = tf.keras.models.load_model('mnist_cnn_full.h5') # 假设 img 是通过某种方式得到的 28x28 灰度图像 img = img.astype('float32') / 255.0 img = img.reshape(1, 28, 28, 1) pred = new_model.predict(img) pred_class = pred.argmax(axis=1)[0] confidence = pred.max(axis=1)[0] print(f"Predicted class: {pred_class}, Confidence: {confidence:.2%}")

注意 predict 的输入必须包含样本维度。也就是说,单张图也必须 reshape 成 (1, 28, 28, 1),而不是 (28, 28, 1)。这个维度问题是最容易让人卡壳的地方,我把话放这儿,后面你写 Transformer 和 GAN 时同样会遇到。

6. 常见问题与排查技巧实录

问题现象可能原因排查方法
No module named 'tensorflow'环境没激活或安装失败确认当前环境激活,使用 `pip list
DLL load failed缺少 Visual C++ 运行库安装最新 VC_redist.x64.exe 或换 Python 3.10 版本重试
GPU 列表为空安装的是 CPU 版或 CUDA 未匹配运行pip list确认是 tensorflow 还是 tensorflow-cpu;检查 CUDA 版本
shape mismatch 报错输入形状和模型 input_shape 不一致打印x_train.shape对比身形;注意有没有通道维度
Failed to get convolution algorithmGPU 显存不够或 cuDNN 未正确安装降低 batch_size,检查 cuDNN 与 CUDA 版本对应关系
训练时 loss 变成 NaN学习率太高或数据有问题降低 learning_rate,检查是否有 NaN 值或未归一化的极端像素
验证准确率远低于训练准确率过拟合增加 Dropout,简化模型结构,或引入数据增强

6.1 最容易中招的三个“隐形坑”

MNIST 虽然简单,但它对应踩坑经验的迁移价值很高。第一个坑是忘记归一化。很多人直接拿 0~255 的原始像素去训练,模型不仅收敛慢,还容易在深层次网络里出现数值不稳定。所有图像任务,第一步永远是把数据尺度处理妥当。

第二个坑是把整数标签和 categorical_crossentropy 混用。如果你的标签还是整数数组,但损失函数写的categorical_crossentropy,会直接报 rank 不匹配的错误。要么对标签做一次性 one-hot 编码,要么就用sparse_categorical_crossentropy。两类方案在数学上等价,但实现上不可混搭。

第三个坑是在模型里忘记加激活函数或错误地在最后一层用 ReLU。最后一层分类必须是 softmax(二分类用 sigmoid),中间暂存层要避免因全用 ReLU 导致神经元死亡。如果你训练曲线出现异常平的阶段,先检查网络末尾的激活函数是不是对的。

6.2 调试经验:从模型 v1 到 v2 的调参思路

如果第一版模型测试准确率不如预期,调整顺序很重要。我个人的惯例是:先确认数据预处理无误,再看损失函数和最后一层激活是否匹配,然后检查学习率。很少有一个训练不好的模型是因为卷积核数量差几个造成的。

给一个可复现的对比实验思路:保持数据不变,只改一个超参(比如把第一个卷积块的 filters 从 32 改成 64),跑两轮对比训练曲线。这种做法虽然耗时,但第六感判断“哪里出了问题”比盲目改一批参数要可靠得多。

7. 系列规划的下一步蓝图

到了这篇的末尾,你的收获应该有三层:完整的开发环境、一个跑通的 CNN 图像分类全流程,以及对卷积运算、池化、感受野、训练监控这些核心概念的理解。不是说光发了代码叫“学会”了,而是你跟着亲自敲过一遍、跑过一遍、画出过曲线、调过一两个参数,这个“手感”比任何千字说明都值钱。

后续几篇的展开思路也提前预告一下:

  • 第二篇:Transformer。从注意力机制出发,先把自注意力的公式推明白,再用 Keras 的MultiHeadAttention层构建一个完整可训练的分类网络,并和第一篇的 CNN 做效果对比。
  • 第三篇:GAN。核心是生成器与判别器的对抗训练逻辑,会完整实现一个生成手写数字的 DCGAN,重点讲训练不稳定的各种表现和处理手段。
  • 第四篇:胶囊网络。深入动态路由机制,讲清楚它如何改进 CNN 的池化缺陷,并用 TensorFlow 原生 API 手写胶囊层实现。

第一篇踏踏实实跟下来,你在这个系列里的地基就打得很稳了。下一回,我们进入 Transformer 的世界。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询