文章目录
- 卷积神经网络深度学习笔记
- [1. 图像的数据基础](#1. 图像的数据基础)
-
- [1.1 图像分类](#1.1 图像分类)
- [1.2 通道顺序与维度位置](#1.2 通道顺序与维度位置)
- [1.3 图像的加载与显示](#1.3 图像的加载与显示)
- [2. 卷积神经网络总览](#2. 卷积神经网络总览)
-
- [2.1 为什么需要 CNN](#2.1 为什么需要 CNN)
- [2.2 CNN 的三大构成](#2.2 CNN 的三大构成)
- [2.3 CNN 与普通神经网络的关系](#2.3 CNN 与普通神经网络的关系)
- [2.4 数据在 CNN 中如何流动](#2.4 数据在 CNN 中如何流动)
- [3. 卷积层](#3. 卷积层)
-
- [3.1 是什么与为什么](#3.1 是什么与为什么)
- [3.2 卷积核](#3.2 卷积核)
- [3.3 卷积运算的具体过程](#3.3 卷积运算的具体过程)
-
- [3.3.1 单通道卷积](#3.3.1 单通道卷积)
- [3.3.2 多通道卷积](#3.3.2 多通道卷积)
- [3.4 Padding:填充](#3.4 Padding:填充)
-
- [3.4.1 卷积带来的问题](#3.4.1 卷积带来的问题)
- [3.4.2 Padding 是什么](#3.4.2 Padding 是什么)
- [3.4.3 三种 Padding 类型](#3.4.3 三种 Padding 类型)
- [3.5 Stride](#3.5 Stride)
-
- [3.5.1 Stride 是什么](#3.5.1 Stride 是什么)
- [3.5.2 感受野](#3.5.2 感受野)
- [3.6 特征图尺寸计算](#3.6 特征图尺寸计算)
- [3.7 PyTorch API:nn.Conv2d](#3.7 PyTorch API:nn.Conv2d)
- [4. 池化层](#4. 池化层)
-
- [4.1 是什么与为什么](#4.1 是什么与为什么)
- [4.2 最大池化与平均池化](#4.2 最大池化与平均池化)
- [4.3 池化层的核心特点](#4.3 池化层的核心特点)
- [4.4 PyTorch API:nn.MaxPool2d / nn.AvgPool2d](#4.4 PyTorch API:nn.MaxPool2d / nn.AvgPool2d)
- [5. 综合案例:完整的 CNN](#5. 综合案例:完整的 CNN)
卷积神经网络深度学习笔记
这篇笔记是在学习卷积神经网络过程中的归纳梳理。卷积神经网络和之前学的全连接神经网络最大的不同,在于它处理的对象是图像,而图像本身有自己独特的存储方式和数据结构。所以这篇先解决两个问题:第一,图像在计算机里到底长什么样、怎么存、怎么读、维度怎么排;第二,卷积神经网络整体是一个什么样的框架,数据在里面怎么流、每一步维度怎么变、各个组件分别在哪一步起作用。
1. 图像的数据基础
在动手写卷积网络之前,得先弄清楚一件事:我们眼睛看到的"一张图",到了计算机里就是一堆数字。
图像的本质:一个由像素点(pixel)组成的矩阵,每个像素点是图像的最小单元,它存的就是一个数值,这个数值代表了这个点的颜色深浅或颜色本身。
按每个像素点存储方式的不同,图像可以分成以下几种常见类型。
1.1 图像分类
-
二值图像
特点 :二值图像是最简单的一种,每个像素点只可能是两个值:0 或 1 。0 代表黑色,1 代表白色。整幅图就是一张只由 0 和 1 组成的二维矩阵 。

应用场景:因为只有黑白两种状态,特别适合表达那些"要么是、要么不是"的场景,典型用途有两个:
- 文字、线条图的扫描识别(比如 OCR 里先把扫描件二值化,让文字笔画和背景分开);
- 掩膜图像(mask)的存储,用 1 标出感兴趣的区域,0 标出背景。
二值图像可以看成是灰度图像的一个特例,它把灰度的连续取值压缩成了最极端的两种。
-
灰度图像
特点 :灰度图像里,每个像素点的取值范围是 0 到 255 ,一共 256 个等级。0 表示纯黑色,255 表示纯白色,中间的数值从小到大对应由黑到白的过渡色,越靠近 0 越暗,越靠近 255 越亮。
所以灰度图像也是用一个二维矩阵就够了,矩阵里每个元素是一个 0 到 255 的整数,代表这一个像素的亮度。
应用场景:比如常学习的 MNIST 手写数字举例时,那一张张 28×28 的图就是灰度图,每个像素点是一个灰度值。
-
索引图像
索引图像比灰度图多了一层"查找"的机制。它存的数据分成两部分:
- 颜色索引矩阵(也叫调色板、颜色映射表,通常记作 MAP):它是一个二维表,每一行代表一种颜色,这一行的三个元素分别指定该颜色的红、绿、蓝三个分量值。
- 真正的图像数据 :但图像里存的不是颜色本身,而是指向 MAP 某一行的一个索引值。
读取时,先看这个像素存的是索引几,再去 MAP 里找到对应那一行,读出它的 R、G、B,才知道这个像素到底是什么颜色。它的好处是省内存:一张图颜色再丰富,也只用一张 MAP 表来统一记录所有用到的颜色,图像数据本身只存一个个小整数索引。这种存储方式常见于 GIF 这类带调色板的格式。

-
RGB 彩色图像
特点 :RGB 图像是我们深度学习里最常打交道的形式。它用红(R)、绿(G)、蓝(B) 三种原色的组合来表示每个像素的颜色,每个像素点需要三个数值 ,分别记录这个像素在红、绿、蓝三个通道上的强度。同样是 0 表示该通道纯黑、255 表示纯白,三个通道各自取不同的值,组合起来就形成了千变万化的颜色。比如纯红色就是 (R=255, G=0, B=0),纯白色是 (255, 255, 255),纯黑色是 (0, 0, 0)。
和索引图像不同,RGB 图像 把颜色值直接存放在图像数据里 ,不经过索引查表。假设图像有 M 行 N 列,那么它其实是三个 M×N 的二维矩阵叠在一起:一个矩阵放所有像素的 R 值,一个放 G 值,一个放 B 值。这三个矩阵就是我们常说的三个通道(channel) 。

总结理解 :
用一句话把四种图像串起来:图像由像素点组成,像素点存的是 0 到 255 之间的数值;二值图只有 0/1 两个值,灰度图是一个二维矩阵,索引图靠查表得到颜色,RGB 图则用三个通道直接存颜色。 深度学习中我们处理的大多是 RGB 彩色图,所以后面讲到"通道"时,默认指的就是 R、G、B 这三个通道。
1.2 通道顺序与维度位置
这里有一个特别容易绕晕的点:同一个"三个通道"的图像,在不同库里的顺序和摆放位置是不一样的。
| 库 | 读取图像的通道顺序 | 返回的数据类型 |
|---|---|---|
matplotlib(plt.imread) |
RGB | numpy 数组 |
PIL / Pillow(Image.open) |
RGB | PIL Image 对象 |
OpenCV(cv2.imread) |
BGR | numpy 数组 |
torchvision(read_image) |
RGB | 张量 |
如果你用 OpenCV 读图,读出来是 BGR;但如果用 matplotlib 或 PIL 读,读出来就是 RGB。 一旦把 OpenCV 读的图直接用 matplotlib 显示,红蓝就会互换,颜色看起来就怪怪的,根源就在这里。
再说通道放在哪个维度。图像数据本身有"高 H、宽 W、通道 C"三个维度,还有一个是"图片张数 N"(batch)。这几个维度怎么排,不同框架也有不同约定:
| 约定 | 维度顺序 | 谁在用 | 含义 |
|---|---|---|---|
| 通道在后 | (H, W, C) |
numpy 数组、matplotlib、PIL | 高、宽、通道 |
| 通道在前 | (N, C, H, W) |
PyTorch 张量 | 张数、通道、高、宽 |
| 通道在后 | (N, H, W, C) |
TensorFlow | 张数、高、宽、通道 |
也就是说,plt.imread 读出来的是 (H, W, C),通道在最后一个维度;而 PyTorch 训练用的张量要求 (N, C, H, W),通道在第二个维度。
为什么 PyTorch 要选通道在前?是因为 GPU 的内存布局和卷积运算的向量化实现更适应"通道在前"的排列方式,这样同一通道的连续内存可以被高效地并行读取。所以需要记住从 numpy 读图到 PyTorch 训练之间,通常要做一次维度调整 ,把 (H, W, C) 变成 (C, H, W)(或 (N, C, H, W)),这个后面讲图像加载时会用代码演示。
1.3 图像的加载与显示
-
为什么用 matplotlib
读图、显示图这些操作,numpy 本身做不了,因为它只管数值计算,没有内置图像解码和渲染的能力;PyTorch 的张量也没有直接"显示一张图"的函数。而 matplotlib 是一个专门做数据可视化的库,它既能读图像文件、又能把数组画出来,所以处理图像的展示环节时用它最方便。简单说:读图和画图交给 matplotlib,数值计算交给 numpy/PyTorch。
-
读取图像:plt.imread
pythonimport matplotlib.pyplot as plt img = plt.imread("cat.jpg") # 读取图像文件 print(img.shape) # (H, W, 3) 高、宽、通道 print(img.dtype) # uint8,取值 0~255plt.imread的返回值是一个 numpy 数组 ,读进来的不是别的,就是一个三维数组,三个维度分别是高、宽、通道,每个元素是 0 到 255 的整数(uint8类型),通道顺序是 RGB。 -
显示图像:plt.imshow
pythonplt.imshow(img) # 把数组渲染成图显示出来 plt.axis("off") # 隐藏坐标轴 plt.show()plt.imshow接收一个(H, W)或(H, W, 3)或(H, W, 4)的数组,把它渲染成图像。如果传入的是 RGB 三通道数组,就按 RGB 显示;如果数组来自 OpenCV(是 BGR 顺序),直接显示颜色就会红蓝颠倒,这时需要先把通道换回来。 -
保存图像:plt.imsave
pythonplt.imsave("new_cat.jpg", img) # 把数组保存成图像文件plt.imsave把数组按指定路径存成图像文件,是imread的逆操作。plt.imsave 负责把数组编码成图像文件(后缀决定格式,没有后缀默认 PNG),plt.imread 负责把图像文件解码成数组。如果只想保存/加载数组本身,应该用 np.save 和 np.load。
-
从 numpy 数组到 PyTorch 张量
深度学习训练要的是 PyTorch 张量,而且要求通道在前
(C, H, W)。所以从plt.imread拿到(H, W, C)的数组后,要做两件事:- 转 成张量、必要时把整数类型转成浮点。
- 把维度从通道在后换成通道在前。
pythonimport torch img = plt.imread("cat.jpg") # (H, W, 3), uint8, RGB img_tensor = torch.from_numpy(img) # 数组 → 张量,仍是 (H, W, 3) img_tensor = img_tensor.permute(2, 0, 1) # (H, W, 3) → (C, H, W),通道移到最前 img_tensor = img_tensor.float() / 255.0 # 转成 0~1 的浮点,便于训练 print(img_tensor.shape) # (3, H, W)permute(2, 0, 1)就是完成那次维度调换:把原来第 2 维(通道)放到第 0 维。除以 255 把像素值从 0 到 255 归一化到 0 到 1,这是训练时最常用的输入尺度。实际工程里,torchvision 提供的transforms.ToTensor()会把"转张量 + 通道前置 + 归一化"这三步一次性做完,原理就是上面这几行。
2. 卷积神经网络总览
2.1 为什么需要 CNN
卷积神经网络(Convolutional Neural Network, CNN) 是深度学习在计算机视觉领域的突破性成果,专门用来处理图像、视频这类有网格结构的数据。
那么它和前面学的全连接神经网络是什么关系,为什么要单独发明它?
先看全连接网络直接处理图像会遇到的两个麻烦:
-
参数爆炸。全连接网络里,输入层的每个特征都要和隐藏层每个神经元连一条权重。一张 32×32 的彩色图,摊平成向量就是 32×32×3 = 3072 个输入特征,如果第一个隐藏层有 1000 个神经元,仅这一层就要 3072×1000 ≈ 307 万个参数。图像尺寸稍微大一点,参数就多到根本训练不动。
这里思考的"全连接一个像素一个输入神经元"理解是对的:把图像拉直后,每个像素(每个通道值)就是一个输入特征,对应一个输入神经元,每个都要和后面的每个神经元全相连,连线数量随像素数急剧膨胀。
-
丢失空间结构。把图像拉直成一维向量,本质上打散了像素之间的二维位置关系。原来相邻的两个像素,拉直后可能被一堆别的像素隔开,图像里"这块区域是一个边缘""这几个像素是同一个物体的局部"这种空间信息就丢了,网络很难利用它。
CNN 针对这两个问题给出了根本的改进:
- 局部连接(稀疏连接):一个神经元不再连接全部输入,只连接图像里一小块局部区域(比如 3×3 的小窗),这块区域对应的权重就是卷积核。参数数量因此大幅下降。
- 权重共享:同一个卷积核在整张图上滑动,所有位置共用这一套权重。不管图像多大,一个卷积核的参数量都固定不变。
- 空间不变性 :由于卷积操作是局部的并且采用权重共享,卷积层在处理图像时具有平移不变性。也就是说,不论物体出现在图像的哪个位置,卷积层都能有效地检测到这些物体的特征。
这样,把"处理图像"的参数规模和计算量压到了可以训练的程度,同时天然保留了像素的局部空间关系。这就是 CNN 相比全连接网络的优势与作用。
2.2 CNN 的三大构成
CNN 有一个很简洁的定义:含有卷积层的神经网络就叫卷积神经网络。
一个典型的 CNN 由三部分构成:
原始图像
│
▼
[卷积层] 提取局部特征(边缘、纹理、形状...),一层层从低级到高级
│
▼
[池化层] 大幅降低空间尺寸(降维),减少计算量、增强鲁棒性
│
▼
[全连接层] 把提取好的特征汇总,输出最终的分类/回归结果
- 卷积层(Convolution Layer) :负责特征提取,是 CNN 的核心。它用卷积核在图像上滑动做局部加权求和,从低级特征(边缘、角点、纹理)逐层学到高级特征(形状、物体部件、整体语义)。
- 池化层(Pooling Layer) :负责降维。它不学参数,只是按窗口把特征图缩小,比如 2×2 的窗口里取最大值,让特征图高宽各减半,从而大幅减少后续的计算量,同时让特征对微小平移更鲁棒。
- 全连接层(Fully Connected Layer) :负责输出结果。卷积和池化已经提取好特征,全连接层把这些特征汇总起来,最后用 Softmax 输出每个类别的概率。它就像普通神经网络一样做"加权求和 + 激活",本质是把特征向量映射到最终决策。

卷积层管特征提取,池化层管降维,全连接层管最终输出。 后面第 3 章、第 4 章会逐个展开卷积层和池化层;全连接层在前面神经网络那篇已经讲解,这里就不再重复,只强调它在 CNN 里的角色与作用。
2.3 CNN 与普通神经网络的关系
深度思考与理解 :卷积神经网络和"输入层 + 隐藏层 + 输出层、神经元 = 加权求和 + 激活函数"这套框架,到底是什么关系?
答案是:CNN 是神经网络的一个特例,它的每个组件都能映射回普通神经网络的概念,只是把"加权求和"这一环换成了"卷积"这种特殊的加权求和方式。
- 普通神经网络里,一个神经元做的事是:接收上一层全部输入,做加权求和
z = Σwᵢxᵢ + b,再过激活函数。 - CNN 的卷积层做的事,逻辑上完全一样:卷积运算就是在局部区域上做点积,本质上就是一次加权求和,只是这次求和不看"全部输入",只看卷积核覆盖的那一小块局部区域,权重就是卷积核里的数值,最后再加上偏置 b。
| 普通神经网络 | 卷积神经网络 | 变化点 |
|---|---|---|
| 神经元接收全部输入 | 卷积核只看一块局部区域 | 全连接 → 局部连接 |
| 每个连接各有一个权重 w | 一个卷积核一套权重,全局共享 | 参数不共享 → 权重共享 |
加权求和 Σwᵢxᵢ + b |
卷积核与局部区域做点积 + b |
加权求和 → 卷积 |
| 激活函数(如 ReLU) | 激活函数(如 ReLU) | 完全一样,仍然存在 |
| 每个神经元输出一个标量 | 每个卷积核输出一张特征图 | 输出标量 → 输出二维图 |
所以卷积层的神经元做的事仍然是加权求和 + 激活函数 ,只是加权求和用的是卷积 这种方式;激活函数在 CNN 里依然存在 ,通常放在卷积层之后(Conv → ReLU 的顺序),作用还是引入非线性。这一点和普通神经网络一模一样,没有因为用了卷积就被去掉;池化层则是一个全新的、专门用来降维的组件,它替代不了激活函数,也不做特征提取。
2.4 数据在 CNN 中如何流动
理解了 CNN 和普通神经网络的关系,接下来就是最关键的一步:把一张图从头到尾走一遍,看清它每一步变成了什么形状、每个组件在哪一步起作用。
我们全程用同一个例子贯穿这篇:
text
一张 32×32×3 的彩色图(3 通道 RGB)→ 分类成 10 个类别之一
网络结构定为:
Conv1(3→16, 3×3) → ReLU → MaxPool(2×2) → Conv2(16→32, 3×3) → ReLU → MaxPool(2×2) → Flatten → FC1(2048→128) → ReLU → FC2(128→10) → Softmax
下面这张表把每一步的形状变化、每个组件的位置、以及它对应"是什么/为什么"都串起来:
| 步骤 | 组件 | 运算 | 形状变化 | 这一步在做什么 |
|---|---|---|---|---|
| ① 输入 | 输入层 | 读入图像 | (1, 3, 32, 32) |
1 张图,3 通道,32×32 |
| ② 卷积 | Conv1 | 16 个卷积核滑窗点积 | (1, 16, 32, 32) |
提取 16 种低级特征(边缘、纹理) |
| ③ 激活 | ReLU | 逐元素 max(0,x) |
(1, 16, 32, 32) |
引入非线性 |
| ④ 池化 | MaxPool | 2×2 窗口取最大 | (1, 16, 16, 16) |
高宽各减半,降维 |
| ⑤ 卷积 | Conv2 | 32 个卷积核滑窗点积 | (1, 32, 16, 16) |
在低级特征上提取更高级特征 |
| ⑥ 激活 | ReLU | 逐元素 max(0,x) |
(1, 32, 16, 16) |
引入非线性 |
| ⑦ 池化 | MaxPool | 2×2 窗口取最大 | (1, 32, 8, 8) |
高宽再减半,降维 |
| ⑧ 展平 | Flatten | 拉成一维向量 | (1, 2048) |
8×8×32 = 2048,为进全连接做准备 |
| ⑨ 全连接 | FC1 | 加权求和 + ReLU | (1, 128) |
汇总特征 |
| ⑩ 全连接 | FC2 | 加权求和(logits) | (1, 10) |
输出 10 个类别的原始得分 |
| ⑪ 分类 | Softmax | 归一化成概率 | (1, 10) |
10 个概率值,和为 1,取最大为预测类别 |
注意理解:
- 一个"卷积核"对于 3 通道输入来说,内部包含 3 组不同的参数(3 个子核)。但我们在 API 和形状表示中,把它看作一个整体,称为"一个卷积核"。也就是说输出形状 (1, 16, 32, 32) 中的 16 表示有 16 个这样的整体卷积核,每个整体卷积核内部都有 3 个子核,分别对应输入的 3 个通道。所以这里是16也是因为一个整体卷积核(包含 3 个子核)扫完整个输入后,只输出一张特征图。
- 输出宽高由 kernel_size、stride、padding 三者通过公式决定;输出通道数由卷积核个数决定。两者互相独立,互不影响。
┌─────────────────────────────────────────────────────────────────────┐
│ 一张 32×32×3 彩色图的完整数据流 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 输入 (1,3,32,32) │
│ │ │
│ ▼ │
│ ┌─ 卷积层 Conv1 ─────────────────────────────────────────────┐ │
│ │ · 16 个卷积核,每个 3×3×3,在图像上滑窗做点积 │ │
│ │ · 局部连接 + 权重共享 │ │
│ │ · padding=1 保持尺寸(3.4),stride=1(3.5) │ │
│ │ → (1,16,32,32) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ ReLU(激活函数,引入非线性) │
│ │ │
│ ▼ │
│ ┌─ 池化层 MaxPool ───────────────────────────────────────────┐ │
│ │ · 2×2 窗口取最大值,高宽各减半 │ │
│ │ · 降维、增强平移鲁棒性(4.1) │ │
│ │ → (1,16,16,16) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ Conv2(16→32) + ReLU + MaxPool → (1,32,8,8) (同上) │
│ │ │
│ ▼ Flatten 展平 → (1,2048) │
│ │ │
│ ▼ │
│ ┌─ 全连接层 ──────────────────────────────────────────────────┐ │
│ │ FC1(2048→128) + ReLU → (1,128) │ │
│ │ FC2(128→10) → logits → (1,10) │ │
│ │ Softmax → 10 个概率,和为 1 │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 输出:概率最大的类别(预测结果) │
│ │
└─────────────────────────────────────────────────────────────────────┘
至此,CNN 的完整骨架就立起来了:卷积层提取特征,池化层降维,展平后交给全连接层做分类输出。 每个知识点的职责和位置都也很清晰。接下来就按这个顺序,从卷积层开始逐个学习。
3. 卷积层
3.1 是什么与为什么
卷积层是 CNN 的核心,它承担的任务就是特征提取。
在普通神经网络里,神经元通过"加权求和 + 激活函数"来对上一层的输出做加工,从而提取特征;卷积层做的其实是同一件事,只是把"加权求和"这一步换成了"卷积运算",工具就是卷积核(也叫滤波器,filter)。
卷积层为什么能提取特征?
因为卷积核里存的就是权重 w,而梯度下降优化的也正是这些卷积核里的数值 。训练开始时,卷积核里的值是随机初始化的,随着训练进行,网络会不断调整这些数值,让卷积核逐渐变成"某个特征的探测器",比如某个卷积核最终学到"对竖直边缘响应强烈",另一个学到"对横线敏感"。这和我们之前理解的"不同神经元因为权重不同而对不同特征敏感"是同一个道理,只不过这里一个卷积核就扮演了一个神经元的角色。
还有一个更深的对应关系:卷积层的特征提取也是逐层抽象的,和全连接层"组合的组合"一致。
第一层卷积核提取的是低级特征(边缘、角点、纹理),第二层卷积在低级特征的基础上再提取中级特征(形状、部件),越往深层,提取的特征越接近语义概念(物体、人脸)。
3.2 卷积核
要把卷积层理解透彻,关键是先建立"卷积核 = 神经元"这个对应关系。
回顾普通神经网络里一个神经元的参数结构:它接收 n 个输入特征 x₁...xₙ,对应 n 个权重 w₁...wₙ,再加一个偏置 b,计算 w₁x₁ + w₂x₂ + ... + wₙxₙ + b。如果输入是 3 个特征,就是 w₁x₁ + w₂x₂ + w₃x₃ + b。
现在把同样的思路套到卷积上。一张 RGB 图有 3 个通道,正好对应"3 个特征 x"。那么一个神经元就需要 3 套权重分别去处理这 3 个通道,最后加起来加偏置:
w₁x₁ + w₂x₂ + w₃x₃ + b (普通神经元,3 个特征)
↓ 对应
卷积核·通道1 + 卷积核·通道2 + 卷积核·通道3 + b (卷积神经元,3 个通道)
也就是说,一个"卷积神经元"(一个卷积核)其实由 3 个二维小核组成,分别作用在 R、G、B 三个通道上,各自做一次卷积,把三个结果逐位置相加,再加偏置 b,得到一张特征图。
理解清楚卷积核的定位之后,根据神经网络讲解权重的思维逻辑,理解卷积核内部和卷积核之间的权重差异:
| 维度 | 是否一致 | 说明 |
|---|---|---|
| 同一卷积核内部,同一个通道 | 权重共享(一致) | 同一个核在整张图上滑动,各处用同一套权重 ,这也对应着我们之前讲解局部连接的道理 |
| 同一卷积核内部,不同通道 | 不一致 | 处理 R、G、B 的三个小核各自独立、数值不同 |
| 不同卷积核之间 | 不一致 | 每个卷积核(神经元)有自己的一套权重,各自学不同特征 |
后面两条和普通神经网络完全一致:就像不同神经元权重不同、一个神经元对不同特征的权重也不同。CNN 真正新增的只有第一条:单个卷积核在空间上共享权重。这也是我们之前讲解强调的一个神经元不再连接全部输入,只连接图像里一小块局部区域,防止参数爆炸。
这里再次解释和澄清一个维度上的细节:卷积核的"大小"(比如 3×3)是整层统一设置的 ,这一层所有卷积核都必须是 3×3;但每个卷积核里的"数值"是各自独立学习出来的,可以各不相同。数值各自不同,但大小(kernel_size)在同一层内是统一的,由层参数统一指定,不能一个核 3×3、另一个核 5×5。
3.3 卷积运算的具体过程
3.3.1 单通道卷积
先从最简单的单通道开始理解。
卷积运算的本质,就是在卷积核和输入数据的局部区域之间做点积:把两个矩阵对应位置的元素两两相乘,再把所有乘积加起来。


用一个 5×5 的输入、一个 3×3 的卷积核举例,步长 stride=1、不填充:
输入(5×5):
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20
21 22 23 24 25
卷积核(3×3,一个检测"左亮右暗"垂直边缘的核):
1 0 -1
1 0 -1
1 0 -1
第一步,把卷积核盖在输入的左上角,覆盖住 3×3 的局部区域:
1 2 3 1 0 -1
6 7 8 ⊙ 1 0 -1
11 12 13 1 0 -1
点积计算:
1×1 + 2×0 + 3×(-1) + 6×1 + 7×0 + 8×(-1) + 11×1 + 12×0 + 13×(-1)
= (1 + 6 + 11) - (3 + 8 + 13)
= 18 - 24
= -6
这个 -6 就是输出特征图左上角的值。然后卷积核向右移动一格(stride=1),对下一个 3×3 区域再做一次点积,得到输出特征图的下一个值。就这样从左到右、从上到下滑动,每覆盖一个位置算一个点积,最终铺满整张图,得到一张 3×3 的输出特征图。
滑动的顺序有一个规律,就是"换行扫描":在一个方向上一直走,走不动了才换到下一个方向,像看书一样,先从左往右走完一行,再跳到下一行开头继续。不会出现"水平走一点、垂直又走一点"这种交替走法。当卷积核右边贴到图像边界、再也无法完整覆盖一个 3×3 区域时,就下移一行,从最左边重新开始。
3.3.2 多通道卷积
把单通道的规则推广到 RGB 三通道,就得到了多通道卷积。它多出的只是一个"累加"的步骤:每个通道各自用一个小核做一次卷积,然后把三个通道的结果逐位置相加,最后加偏置 b,得到一张输出特征图。

R 通道 ⊕ 卷积核_R → 结果1
G 通道 ⊕ 卷积核_G → 结果2 三者逐位置相加 + b → 一张输出特征图
B 通道 ⊕ 卷积核_B → 结果3

三个通道的结果已经在这一步"融合"成了一张。所以三通道卷积的输出不是三张图,而是一张图。
- 一个卷积核(含深度,RGB 时为 3 层小核)→ 输出一张特征图;
- 有 N 个卷积核 → 输出 N 张特征图,也就是输出数为 N。
所以这就是理解数据维度变换的关键:输出数 = 卷积核个数 = 这一层"神经元"的个数。

3.4 Padding:填充
3.4.1 卷积带来的问题
第一个问题:特征图尺寸会越缩越小。5×5 的输入经过 3×3 的核,输出变成 3×3;再来一层,就更小。网络一深,特征图很快就缩没了,深层网络根本搭不起来。
第二个问题:边缘信息被忽略。那个 5×5 的例子里,左上角的像素 1 只参与了一次点积计算,而中心像素 13 参与了多次。图像边缘的像素参与计算的次数明显比中心像素少,这意味着边缘信息在特征提取过程中容易被"怠慢"、被丢失。
3.4.2 Padding 是什么
Padding(填充)就是这两个问题的一种解法:在输入特征图的边界周围额外补一圈像素,通常是补 0。 补了之后,原来的边缘像素就变成了"靠里的像素",参与计算的次数增加,边缘信息被更好地保留;同时补进去的圈让输出尺寸不再一味缩小,甚至能保持和输入一样大。
注意我们设置padding数值的具体含义:padding 的数值指的是"四周各补几层",四个方向(上、下、左、右)同时补。 padding=1 就是上下左右各补 1 层 0,高和宽各增加 2;padding=2 就是四周各补 2 层,高宽各增加 4。不存在"只补一边"或"左右各补一半"的说法,它是四边对称的。

3.4.3 三种 Padding 类型
| 类型 | 做法 | 输出尺寸变化 | 适用场景 |
|---|---|---|---|
| Valid Padding | 不填充,卷积核只在有效区域内滑动 | 变小 | 默认做法,不在乎尺寸收缩时 |
| Same Padding | 补到输出尺寸和输入一致 | 不变 | 想保持空间尺寸、方便堆深层时 |
| Full Padding | 尽可能多补,让核的每个元素都至少滑过一次 | 变大 | 少用,只在特殊需求下 |
实际工程里不需要人为去判断边缘重不重要,选择 padding 的依据是"你想让输出保持什么尺寸"。 想让尺寸不变(这在现代 CNN 里是常态,方便叠加很多层)就用 Same Padding;对尺寸收缩无所谓就用 Valid。它归根结底是一个关于"输出尺寸"的工程选择,而不是去猜测"边缘到底重不重要"。
Same Padding 的填充量有个固定公式(核为奇数、stride=1 时):padding = (kernel_size - 1) / 2。所以 3×3 的核配 padding=1,5×5 的核配 padding=2,输出尺寸就恰好和输入一样。
3.5 Stride
3.5.1 Stride 是什么
Stride(步长)就是卷积核每滑动一步移动多少个像素。
stride=1 表示每次移动 1 个像素,stride=2 表示每次移动 2 个像素,步长越大,覆盖的位置越稀疏,输出特征图越小。
步长的大小直接影响输出尺寸 ,进而影响一个更本质的概念:感受野。

3.5.2 感受野
感受野(Receptive Field)指的是输出特征图上某个位置的值,能"看到"原始输入图像里的多大一片区域。理解它可以分两个层面。
层面一,单个卷积层内:一个用 3×3 核的神经元,感受野就是 3×3;用 5×5 核,感受野就是 5×5。核越大、步长越大,单个神经元一步能覆盖的范围就越大。
层面二,多层堆叠后 :这是感受野更重要的意义。第二层卷积的每个位置,是由第一层输出上 3×3 的区域算出来的,而这 3×3 又各自对应原始图像上更大的区域,层层回溯,第二层的一个神经元实际上"看到"的是原始图像上更大的范围。卷积层每叠一层,感受野就往外扩一圈,高层神经元看到的原始图像范围就越广。

这正是前面说的"逐层抽象"的另一个表达:感受野随着层数增加而扩大,浅层看局部细节(边缘),深层看全局语义(整体形状)。 它和全连接网络里"深层神经元考虑的是浅层特征组合的组合"是同一个道理,只是 CNN 用"感受野"这个词把这种"看得越来越广"的层次性表达得更具体。
3.6 特征图尺寸计算
输出特征图的大小,由三个参数决定:
- 卷积核大小
- padding
- stride
计算公式是:
H out = ⌊ H in + 2 × padding − kernel_size stride ⌋ + 1 H_{\text{out}} = \left\lfloor \frac{H_{\text{in}} + 2 \times \text{padding} - \text{kernel\_size}}{\text{stride}} \right\rfloor + 1 Hout=⌊strideHin+2×padding−kernel_size⌋+1
W out = ⌊ W in + 2 × padding − kernel_size stride ⌋ + 1 W_{\text{out}} = \left\lfloor \frac{W_{\text{in}} + 2 \times \text{padding} - \text{kernel\_size}}{\text{stride}} \right\rfloor + 1 Wout=⌊strideWin+2×padding−kernel_size⌋+1
这三个参数里,卷积核大小和 padding 决定了"多大窗口、补多少",stride 决定了"滑动多快"。三者的选择共同决定了输出尺寸和感受野,是搭 CNN 时每层都要先想清楚的三个量。
3.7 PyTorch API:nn.Conv2d
所属模块 :torch.nn,助记:nn = Neural Network,神经网络 。卷积层和之前的 nn.Linear、nn.ReLU 一样,都是继承自 nn.Module 的层,在 __init__ 里定义、在 forward 里调用。
核心 API:
python
torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0,
dilation=1, groups=1, bias=True, padding_mode='zeros')
常用参数:
| 参数 | 含义 | 默认值 | 典型设置 |
|---|---|---|---|
in_channels |
输入通道数(由输入图决定) | 必填 | 灰度图 1,RGB 图 3 |
out_channels |
输出通道数 = 卷积核个数 = 神经元个数 | 必填 | 16、32、64...逐层递增 |
kernel_size |
卷积核大小 | 必填 | 3(最常用),5,一般选奇数 |
stride |
步长 | 1 | 1(保持),2(下采样) |
padding |
四周各补几层 0 | 0 | 1(配 3×3 保持尺寸) |
bias |
是否加偏置 b | True | 默认即可 |
返回值含义 :nn.Conv2d 是一个层,对输入张量做卷积后,输出形状为 (N, out_channels, H_out, W_out) 的特征图张量。注意输入张量必须是 (N, C, H, W) 的四维张量(通道在前)。
在例子网络中的代码:
python
import torch
import torch.nn as nn
# Conv1:输入 3 通道(RGB),输出 16 通道,3×3 核,padding=1 保持尺寸
self.conv1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, padding=1)
# Conv2:输入 16 通道(上一层输出),输出 32 通道
self.conv2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=3, padding=1)
这里要注意 in_channels 是"接上一层的输出通道数",第一层是图像本身的 3,第二层就变成第一层的 out_channels=16。所以搭网络时,每一层的 in_channels 必须等于上一层的 out_channels,这个"通道对账"是搭 CNN 时最常出错的地方。
4. 池化层
4.1 是什么与为什么
池化层(Pooling Layer)的作用是降低输入数据的空间维度(高度和宽度),从而减少计算量、减少内存消耗,并提高模型的鲁棒性。
为什么需要池化?卷积层只管提取特征,不做尺寸收缩(如果用了 Same Padding 尺寸还不变)。但图像像素太多,如果每层卷积后特征图的高宽都维持原样,越往后计算量和内存占用就越大,尤其是最后接全连接层时,特征图太大意味着展平后的向量太长、全连接层参数爆炸。池化层就是来解决这个的:它每隔一段时间把特征图缩小一次,让计算量一路可控。
这里要澄清一个对比:池化层和激活函数不是一回事。激活函数做的是逐元素的非线性变换,只改变数值大小,不改变尺寸,作用是引入非线性;池化层做的是空间下采样,改变的是尺寸(高宽),不引入非线性。二者作用不同,也不能互相替代。
池化层还有一个额外的好处,就是平移不变性:因为池化是在一个小窗口里取极值或均值,特征图里某个特征稍微挪动一点位置,池化后的结果往往变化不大。这让模型对图像里物体位置的微小偏移不那么敏感,鲁棒性更强。
4.2 最大池化与平均池化
池化同样有"窗口"的概念,这个窗口和卷积核很像,也有大小、步长、填充这些参数,但它和卷积层的卷积核是相互独立的,池化窗口多大和卷积核多大没有必然联系,各设各的。比如 3×3 卷积之后接一个 2×2 的池化,再正常不过。
两种最常用的池化方式:
| 池化方式 | 做法 | 特点 |
|---|---|---|
| 最大池化(Max Pooling) | 取窗口内所有值中的最大值作为输出 | 最常用,保留最显著的特征 |
| 平均池化(Avg Pooling) | 取窗口内所有值的均值作为输出 | 更平滑,保留整体信息 |

以 2×2 窗口、stride=2 为例,输入一个 4×4 的特征图:
1 3 2 4
5 6 7 8 2×2 窗口,不重叠滑动
9 4 3 2
1 2 3 4
最大池化:第一个窗口 [1,3;5,6] 取最大 6,第二个窗口 [2,4;7,8] 取最大 8,以此类推,得到:
6 8
9 4
平均池化:第一个窗口 [1,3;5,6] 取均值 (1+3+5+6)/4 = 3.75,得到:
3.75 5.25
4.0 3.0
这里顺带解释那个"这里数值可以是小数"的困惑:窗口大小、步长这些参数必须是整数(代表几个像素),但池化的输出值可以是小数,最典型的就是平均池化,它取均值自然会产生小数。卷积层的输出同理,卷积核权重是小数时,点积结果也是小数。"必须是整数"的只是核大小、步长这类"几个像素"的配置项,不是输出的数值。
4.3 池化层的核心特点
-
没有可学习参数:池化只做"取最大值"或"取均值"这样的固定运算,不像卷积核有需要训练的权重。所以池化层不参与梯度更新的参数学习,它只是对数据做一次确定性的缩放。
-
只改变高宽,不改变通道数 :一个 2×2 的池化把高宽各减半,但通道数保持不变。
(1, 16, 32, 32)经过池化变成(1, 16, 16, 16),通道还是 16。在处理多通道输入数据时,池化层对每个输入通道分别池化 ,而不是像卷积层那样将各个通道的输入相加。这意味着池化层的输出和输入的通道数是相等。池化只在宽高维度上池化 ,在通道上是不发生池化(池化前后,多少个通道还是多少个通道) -
通常 stride 等于窗口大小:这样窗口不重叠,降维效果最直接。2×2 窗口配 stride=2,高宽正好减半。
4.4 PyTorch API:nn.MaxPool2d / nn.AvgPool2d
所属模块 :torch.nn,和卷积层一样,是继承自 nn.Module 的层。
最大池化:
python
torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1,
return_indices=False, ceil_mode=False)
平均池化:
python
torch.nn.AvgPool2d(kernel_size, stride=None, padding=0, ceil_mode=False,
count_include_pad=True, divisor_override=None)
常用参数(两者通用):
| 参数 | 含义 | 默认值 | 典型设置 |
|---|---|---|---|
kernel_size |
池化窗口大小 | 必填 | 2(最常用,高宽减半) |
stride |
步长 | 等于 kernel_size |
通常不单独设,默认不重叠 |
padding |
四周补 0 | 0 | 一般不填充 |
return_indices |
是否返回最大值的索引(仅 MaxPool) | False | 需要上采样反池化时设为 True |
返回值含义 :对输入张量做池化后,输出形状为 (N, C, H_out, W_out) 的特征图。H_out、W_out 的计算公式和卷积层一致,只是 kernel_size、stride、padding 换成池化窗口的参数。
在例子网络中的代码:
python
self.pool = nn.MaxPool2d(kernel_size=2, stride=2) # 2×2 窗口,高宽各减半
使用上,池化层通常放在卷积层和激活函数之后(Conv → ReLU → Pool 的顺序),形成一个"提取特征 → 引入非线性 → 降维"的标准组合,这个组合在 CNN 里反复出现。
5. 综合案例:完整的 CNN
最后用贯穿全篇的例子(32×32×3 的彩色图,10 分类),把所有知识点拼成一个完整的可运行代码。每个组件对应的章节都标注在注释里。
python
import torch
import torch.nn as nn
# ============================================================
# 网络结构定义(第 2、3、4 章)
# 输入:一张 32×32×3 的彩色图 → 输出:10 个类别的概率
# ============================================================
class SimpleCNN(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
# ------ 卷积层(第 3 章)------
# Conv1:3 通道(RGB)→ 16 通道,3×3 核,padding=1 保持 32×32
self.conv1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, padding=1)
# Conv2:16 通道 → 32 通道
self.conv2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=3, padding=1)
# ------ 激活函数 ------
self.relu = nn.ReLU()
# ------ 池化层(第 4 章)------
# 2×2 窗口、stride=2,高宽各减半
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
# ------ 全连接层(第 2 章)------
# 展平后 8×8×32 = 2048 个特征 → 128 → 10(输出 logits,不加 Softmax)
self.fc1 = nn.Linear(2048, 128)
self.fc2 = nn.Linear(128, num_classes)
def forward(self, x):
# x: (batch_size, 3, 32, 32)
x = self.relu(self.conv1(x)) # → (batch_size, 16, 32, 32)
x = self.pool(x) # → (batch_size, 16, 16, 16)
x = self.relu(self.conv2(x)) # → (batch_size, 32, 16, 16)
x = self.pool(x) # → (batch_size, 32, 8, 8)
x = x.view(x.size(0), -1) # 展平 → (batch_size, 2048)
x = self.relu(self.fc1(x)) # → (batch_size, 128)
x = self.fc2(x) # → (batch_size, 10),logits
return x
# ============================================================
# 训练配置
# ============================================================
model = SimpleCNN(num_classes=10)
# 损失函数:CrossEntropyLoss 内部已含 Softmax,所以模型输出 logits 即可
criterion = nn.CrossEntropyLoss()
# 优化器:Adam,weight_decay 实现 L2 正则化
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)
# ============================================================
# 训练循环(batch / epoch / iteration)
# ============================================================
def train(model, dataloader, criterion, optimizer, num_epochs=20):
for epoch in range(num_epochs):
model.train() # 训练模式
total_loss = 0
for batch_x, batch_y in dataloader: # batch_x: (bs, 3, 32, 32)
optimizer.zero_grad() # ① 清空上一 batch 的梯度
logits = model(batch_x) # ② 前向传播 → (bs, 10)
loss = criterion(logits, batch_y) # ③ 计算损失
loss.backward() # ④ 反向传播,梯度存入 .grad
optimizer.step() # ⑤ 更新参数
total_loss += loss.item()
print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {total_loss/len(dataloader):.4f}')
# ============================================================
# 评估
# ============================================================
def evaluate(model, dataloader):
model.eval() # 评估模式
correct, total = 0, 0
with torch.no_grad(): # 不计算梯度
for batch_x, batch_y in dataloader:
logits = model(batch_x)
_, predicted = torch.max(logits, dim=1) # 取概率最大的类别
total += batch_y.size(0)
correct += (predicted == batch_y).sum().item()
return correct / total
数据流回顾:
(bs, 3, 32, 32) → Conv1(3→16,3×3,p=1) + ReLU → (bs, 16, 32, 32)
→ MaxPool(2×2) → (bs, 16, 16, 16)
→ Conv2(16→32,3×3,p=1) + ReLU → (bs, 32, 16, 16)
→ MaxPool(2×2) → (bs, 32, 8, 8)
→ view 展平 → (bs, 2048)
→ FC1(2048→128) + ReLU → (bs, 128)
→ FC2(128→10) → (bs, 10) logits
→ Softmax(在 CrossEntropyLoss 内部)→ 10 类概率
以上为个人学习总结,旨在梳理个人理解。如有疏漏或不当之处,欢迎指正与交流。如果文章对你有帮助,别忘了点个赞、留个言,让更多的小伙伴看到~