【深度学习|DAY05】卷积神经网络深度学习笔记

文章目录

  • 卷积神经网络深度学习笔记
  • [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 组成的二维矩阵

    应用场景:因为只有黑白两种状态,特别适合表达那些"要么是、要么不是"的场景,典型用途有两个:

    1. 文字、线条图的扫描识别(比如 OCR 里先把扫描件二值化,让文字笔画和背景分开);
    2. 掩膜图像(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

    python 复制代码
    import matplotlib.pyplot as plt
    
    img = plt.imread("cat.jpg")   # 读取图像文件
    print(img.shape)              # (H, W, 3)  高、宽、通道
    print(img.dtype)              # uint8,取值 0~255

    plt.imread 的返回值是一个 numpy 数组 ,读进来的不是别的,就是一个三维数组,三个维度分别是高、宽、通道,每个元素是 0 到 255 的整数(uint8 类型),通道顺序是 RGB。

  • 显示图像:plt.imshow

    python 复制代码
    plt.imshow(img)      # 把数组渲染成图显示出来
    plt.axis("off")      # 隐藏坐标轴
    plt.show()

    plt.imshow 接收一个 (H, W)(H, W, 3)(H, W, 4) 的数组,把它渲染成图像。如果传入的是 RGB 三通道数组,就按 RGB 显示;如果数组来自 OpenCV(是 BGR 顺序),直接显示颜色就会红蓝颠倒,这时需要先把通道换回来。

  • 保存图像:plt.imsave

    python 复制代码
    plt.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) 的数组后,要做两件事:

    1. 成张量、必要时把整数类型成浮点。
    2. 把维度从通道在后成通道在前。
    python 复制代码
    import 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) 是深度学习在计算机视觉领域的突破性成果,专门用来处理图像、视频这类有网格结构的数据。

那么它和前面学的全连接神经网络是什么关系,为什么要单独发明它?

先看全连接网络直接处理图像会遇到的两个麻烦:

  1. 参数爆炸。全连接网络里,输入层的每个特征都要和隐藏层每个神经元连一条权重。一张 32×32 的彩色图,摊平成向量就是 32×32×3 = 3072 个输入特征,如果第一个隐藏层有 1000 个神经元,仅这一层就要 3072×1000 ≈ 307 万个参数。图像尺寸稍微大一点,参数就多到根本训练不动。

    这里思考的"全连接一个像素一个输入神经元"理解是对的:把图像拉直后,每个像素(每个通道值)就是一个输入特征,对应一个输入神经元,每个都要和后面的每个神经元全相连,连线数量随像素数急剧膨胀。

  2. 丢失空间结构。把图像拉直成一维向量,本质上打散了像素之间的二维位置关系。原来相邻的两个像素,拉直后可能被一堆别的像素隔开,图像里"这块区域是一个边缘""这几个像素是同一个物体的局部"这种空间信息就丢了,网络很难利用它。

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,取最大为预测类别

注意理解:

  1. 一个"卷积核"对于 3 通道输入来说,内部包含 3 组不同的参数(3 个子核)。但我们在 API 和形状表示中,把它看作一个整体,称为"一个卷积核"。也就是说输出形状 (1, 16, 32, 32) 中的 16 表示有 16 个这样的整体卷积核,每个整体卷积核内部都有 3 个子核,分别对应输入的 3 个通道。所以这里是16也是因为一个整体卷积核(包含 3 个子核)扫完整个输入后,只输出一张特征图。
  2. 输出宽高由 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 特征图尺寸计算

输出特征图的大小,由三个参数决定:

  1. 卷积核大小
  2. padding
  3. 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.Linearnn.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_outW_out 的计算公式和卷积层一致,只是 kernel_sizestridepadding 换成池化窗口的参数。

在例子网络中的代码

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 类概率

以上为个人学习总结,旨在梳理个人理解。如有疏漏或不当之处,欢迎指正与交流。如果文章对你有帮助,别忘了点个赞、留个言,让更多的小伙伴看到~

相关推荐
蒸蒸yyyyzwd1 小时前
cpp 选手备战秋招学习笔记 day11
redis·笔记·求职招聘
ElectroComp1 小时前
选对1.5uH功率电感:线艺XEL4030V与同于的兼容评估笔记
笔记
你要飞2 小时前
VSP3 转 STL
笔记·github
盼小辉丶2 小时前
PyTorch强化学习实战——用预训练语言模型和ChatGPT玩转文本游戏
pytorch·深度学习·语言模型·chatgpt·强化学习
Lethehong2 小时前
把Blinko放进极空间:Docker 部署自托管笔记,并实现多设备远程访问
笔记·docker·容器
Shaoxi Zhang12 小时前
JAVA学习笔记035——对象和JSON格式
java·笔记·学习
Oll Correct12 小时前
Adobe illustrator 案例六:手枪图标绘制
笔记·ui·adobe·illustrator
摇滚侠14 小时前
《Docker技术入门与实战 第4版》阅读笔记 2 Docker 镜像
笔记·docker·容器
菜冻鱼16 小时前
Python-pytorch-高级技巧
开发语言·人工智能·pytorch·python·深度学习·神经网络·聚类