人工智能——CNN基础:卷积和池化

发布于:2025-08-14 ⋅ 阅读:(17) ⋅ 点赞:(0)

一、CNN入门介绍

1、卷积神经网络(Convolutional Neural Network,简称 CNN)是一种专门为处理具有网格结构数据(如图像、音频)而设计的深度学习模型。

在传统的全连接神经网络(FNN)中,输入的每个神经元都与下一层的所有神经元相连,这会导致参数数量随着输入维度的增加而急剧增长,并且无法有效利用数据的空间结构信息。(更多的只是关注了全局的特征)

而 CNN 则通过卷积层、池化层等特殊结构,大大减少了模型的参数数量,同时能够自动提取数据中的局部特征,并且对平移、缩放等变换具有一定的不变性。。在图像识别、目标检测、语义分割等计算机视觉任务广泛运用

2、卷积的思想

在之前的OpenCv的学习中,我们接触到了很多的算子,比如Roberts算子,拉普拉斯算子等等,这些都是运用卷积的思想,对图像操作进行边缘提取。

可以把卷积理解为一种 “特征提取器”。想象你有一张藏宝图(图像),上面隐藏着各种宝藏线索(特征),但是线索分布比较杂乱。而卷积操作就像是一个带有不同形状 “放大镜”(卷积核)的寻宝工具。

每个 “放大镜” 都有自己特定的形状和功能,比如有的专门找直线形状的线索(检测边缘),有的找圆形的线索(检测类似眼睛的部件) 。当我们拿着这些 “放大镜” 在藏宝图上一格一格地移动(卷积核在图像上滑动),去观察每个小区域(感受野),就可以把对应的线索找出来,也就是提取出了图像的局部特征

二、卷积层

1、卷积核

卷积核其实是一个小矩阵,在定义时需要考虑以下几方面的内容:

  • 卷积核的个数:卷积核(过滤器)的个数决定了其输出特征矩阵的通道数。每一个卷积核对应一个通道数,也就对应一张特征图

  • 卷积核的值:卷积核的值是初始化好的,后续进行更新。

  • 卷积核的大小:常见的卷积核有1×1、3×3、5×5等,一般都是奇数 × 奇数

2、卷积计算过程:

卷积的过程是将卷积核在图像上进行滑动计算,每次滑动到一个新的位置时,卷积核和图像进行点对点的计算,并将其求和得到一个新的值,然后将这个新的值加入到特征图中,最终得到一个新的特征图

  1. input 表示输入的图像

  2. filter 表示卷积核, 也叫做滤波器

  3. input 经过 filter 的得到输出为最右侧的图像,该图叫做特征图,所有的特征图的数量就是输出通道数,在代码中通常用out_channels(其实就是特征图的数量)

代码:了解卷积

from matplotlib import pyplot as plt
import torch.nn as nn
import os
import torch

image_path= os.path.relpath('../data/彩色.png')
img_data = plt.imread(image_path)
#shape是(高,宽,通道)(H, W, C),要转变为(N, C, H, W)---->先转为(C, H, W),再转(N, C, H, W)
print(img_data.shape)

img_data =torch.tensor(img_data.transpose(2,0,1)).unsqueeze(0)

print(img_data.shape)

#卷积层
conv = nn.Conv2d(
    in_channels=4,  #输入通道数
    out_channels=1,  #输出通道数(卷积核个数,对应输出的特征个数)
    kernel_size=3, #卷积核大小
    stride=1,  #步长
)
#conv传入的参数是(N, C, H, W)
output = conv(img_data)#输出(N, C, H, W)
print(output.shape)

#还原成图片
out = output.squeeze(0).data.numpy()#去掉batch维度,转成numpy数组
out = out.transpose(1,2,0)
#绘图
plt.imshow(out)
plt.show()

正常图片输入的shape是(高,宽,通道)(H, W, C),进行卷积的API:nn.Conv2d(),输入的数据格式应该是四维的:(N, C, H, W),在传入img_data前需要经过转换,先用transpose方法转为(C, H, W)再用unqueeze在0维上升维,转为(N, C, H, W)

Conv2d的使用

conv = nn.Conv2d(

    in_channels=4,  #输入通道数

    out_channels=1,  #输出通道数(卷积核个数,对应输出的特征个数)

    kernel_size=3, #卷积核大小

    stride=1,  #步长

)

        我们需要去理解,这里的in_channels和out_channels到底是什么,在进行多层卷积神经网络时才能正确传入参数。

        其中in_channels,输入的通道数,也就是(N, C, H, W)里面的C,对于初始我们给的图像而言,常常就是单通道(1),三通道(3),四通道(4)

        但是经过卷积操作后,这里的out_channels输出通道就不再是简单的图像上颜色对应的通道了,而是特征通道(经过每个卷积核操作后生成的特征图个数,也就是卷积核个数)

        例如:   

  • 第一层卷积conv1in_channels=1(输入是单通道灰度图),out_channels=16
    这意味着卷积层会创建16 个不同的 3×3 卷积核,每个卷积核专门检测一种局部特征(如:

    • 第 1 个卷积核:检测水平边缘
    • 第 2 个卷积核:检测垂直边缘
    • 第 3 个卷积核:检测 45° 角纹理
    • ...
    • 第 16 个卷积核:检测特定的斑点 / 明暗变化)

    每个卷积核与输入图像卷积后,会输出一张 “特征图”(通道),16 个卷积核就得到 16 个通道,每个通道的数值表示 “该位置是否包含对应特征” 以及 “特征强度”。

3、边缘填充

在上面的图像中可以看出,经过卷积计算后特征图的尺寸会比原始图像尺寸小,这是由于在卷积过程中边缘的像素值计算后放在卷积核中心去了,如果想要保持图像大小不变, 可在原图周围添加padding来实现(边缘填充)。

在代码中,就是在conv = nn.Conv2d方法中加入参数padding=n,其中n表示用什么来填充,一般默认是0

4、步长
一般步长是根据图像的大小而定,在图像尺寸很小时,例如只有20*20,总共一行都没多少个像素值,所以步长肯定不宜过大。当图像比较大时,也可以适当的设置为2、3等等。需要去实验

stride太小:重复计算较多,计算量大,训练效率降低;

stride太大:会造成信息遗漏,无法有效提炼数据背后的特征;

5、多通道卷积计算

  1. 当输入有多个通道(Channel), 例如RGB三通道, 此时要求卷积核需要有相同的通道数。

  2. 卷积核通道与对应的输入图像通道进行卷积。

  3. 将每个通道的卷积结果按位相加得到最终的特征图

如图:

6、多卷积核计算

实际对图像进行特征提取时, 我们需要使用多个卷积核进行特征提取,通过多个卷积核从输入数据中提取多种类型的特征

其实就是上面讲Conv2d方法中提到的out_channels

用 “团队分工” 类比多卷积核

假设你需要分析一张动物图片:

  • 有人负责观察 “边缘”(比如动物的轮廓)
  • 有人负责观察 “纹理”(比如毛发的粗细)
  • 有人负责观察 “颜色块”(比如是否有黑白条纹)
  • 每个人专注于一种特征,最后汇总所有观察结果,才能全面判断这是猫、狗还是老虎

7、特征图大小

  1. size: 卷积核/过滤器大小,一般会选择为奇数,比如有 1×1, 3×3, 5×5

  2. Padding: 零填充的方式

  3. Stride: 步长

那计算方法如下图所示:

  1. 输入图像大小: W x W

  2. 卷积核大小: F x F

  3. Stride: S

  4. Padding: P

  5. 输出图像大小: N x N

8、局部特征提取——多层多通道卷积核进行特征提取

本质是通过层级化的特征组合,从原始数据中逐步提炼出从简单到复杂、从局部到全局的特征

用 “拼图游戏” 类比多层特征提取

想象你在拼一幅复杂的动物拼图:

 
  • 第一层(浅层):你先找所有带 “直边”“曲边” 的拼图块(对应低级特征:边缘、纹理)。
  • 第二层(中层):你把 “直边 + 曲边” 组合成 “耳朵”“爪子” 等部件(对应中级特征:局部部件)。
  • 第三层(深层):你把 “耳朵 + 爪子 + 身体” 组合成完整的 “猫”(对应高级特征:整体物体)。

每一层基于上一层的特征,通过多通道卷积核进行 “特征组合”,逐步构建更抽象、更有意义的表示

示例:

我们以 “RGB 图像(3 通道)→ 3 层卷积” 为例,详细拆解每一层的输入、卷积核作用和输出特征

1. 输入层:原始图像(物理通道)

输入是一张 \(224 \times 224 \times 3\) 的 RGB 图像

        这些是最原始的像素信息,没有经过任何特征提取

2. 第一层卷积(提取低级特征)
  • 输入通道:3(RGB 三通道)
  • 卷积核设置:16 个卷积核,每个尺寸为 \(3 \times 3 \times 3\)(深度 = 输入通道数 3,确保能覆盖所有颜色通道)
  • 操作逻辑:每个卷积核在输入图像上滑动,对 3 个颜色通道的对应区域同时计算(每个通道与卷积核的对应切片做点积,再求和),输出 1 个特征图。16 个卷积核共输出 16 个通道。
  • 输出特征:16 个 \(222 \times 222\) 的特征图(假设 stride=1,padding=0),每个通道对应一种低级特征:
    • 通道 0:检测 “水平红色边缘”
    • 通道 1:检测 “45° 绿色纹理”
    • 通道 2:检测 “垂直蓝色边缘”
    • ...(16 种不同的边缘、纹理、颜色块特征)这些特征我们也无法确认,是计算机自行去计算,我们也不需要去搞懂到底提取了哪些特征
3. 第二层卷积(组合低级特征为中级特征)
  • 输入通道:16(第一层输出的 16 种低级特征)
  • 卷积核设置:32 个卷积核,每个尺寸为 \(3 \times 3 \times 16\)(深度 = 输入通道数 16,能覆盖所有低级特征)
  • 操作逻辑:每个卷积核滑动时,会同时 “观察” 第一层的 16 个特征通道(比如:“水平边缘通道” 的响应 +“垂直边缘通道” 的响应),通过权重加权求和,输出 1 个新的特征图。32 个卷积核输出 32 个通道。
  • 输出特征:32 个 \(220 \times 220\) 的特征图,每个通道对应中级特征
4. 第三层卷积(组合中级特征为高级特征)
  • 输入通道:32(第二层输出的 32 种中级特征)
  • 卷积核设置:64 个卷积核,每个尺寸为 \(3 \times 3 \times 32\)
  • 操作逻辑:每个卷积核整合 32 种中级特征(比如:“角点 + 斑点 + 短线条” 的特定组合),输出代表更复杂模式的特征图。
  • 输出特征:64 个 \(218 \times 218\) 的特征图,每个通道对应高级特征:
    • 通道 0:“眼睛”(圆形斑点 + 周围的边缘轮廓)
    • 通道 1:“车轮”(圆形纹理 + 辐射状线条)
    • 通道 2:“文字笔画”(特定方向的线条组合)
    • ...(64 种接近 “物体部件” 的特征)

多层多通道的核心机制:特征的 “层级抽象” 与 “通道协同”

  1. 层级抽象
    浅层(1-2 层)→ 低级特征(边缘、纹理、颜色)
    中层(3-4 层)→ 中级特征(角点、斑点、局部部件)
    深层(5 层以上)→ 高级特征(物体部件、整体轮廓)
    每一层的特征都是上一层特征的 “组合与抽象”,就像从 “字母” 到 “单词” 再到 “句子” 的过程。

  2. 通道协同
    每个卷积核的深度 = 输入通道数,意味着它能 “同时关注多个输入特征”。例如,检测 “眼睛” 的卷积核会重点关注:

    • 输入通道中 “圆形纹理” 的高响应区域
    • 输入通道中 “边缘轮廓” 的闭合区域
    • 输入通道中 “肤色(特定颜色)” 的区域
      这种多通道协同,让网络能学习 “特征之间的关联”(比如 “圆形 + 闭合边缘 + 肤色 = 眼睛”)。
  3. 感受野扩大
    深层卷积的 “感受野”(能影响输出的输入区域)比浅层大。例如:

    • 第一层 3×3 卷积的感受野是 3×3(只看 3×3 像素)
    • 第二层 3×3 卷积的感受野是 5×5(因为它的输入是第一层的 3×3 区域,对应原始图像 5×5)
    • 第三层 3×3 卷积的感受野是 7×7
      这使得深层特征能捕捉更大范围的局部信息,为组合高级特征提供基础。

        什么是感受野:

感受野(Receptive Field) 是指输出特征图上的一个像素点,对应输入图像(或前层特征图)上的区域大小

用 “望远镜视野” 类比感受野

想象你用望远镜观察风景:

 
  • 当你用低倍镜(类似 CNN 浅层卷积)时,视野小(感受野小),只能看清眼前的细节(如一片树叶的纹理)。
  • 当你换高倍镜(类似 CNN 深层卷积)时,视野大(感受野大),能看到更大范围的场景(如整棵树的轮廓)。

CNN 中:

  • 浅层卷积层的感受野小,只能 “看到” 输入图像的局部区域(如几个像素组成的边缘)。
  • 深层卷积层的感受野大,能 “看到” 输入图像的更大范围(如多个局部特征组合成的部件)。

感受野的意义:

  1. 特征层级与感受野的关系

    • 浅层(感受野小):捕捉边缘、纹理等局部特征(如眼睛的轮廓、毛发的纹理)。
    • 深层(感受野大):捕捉部件、整体等全局特征(如人脸的整体结构、汽车的轮廓)。

    例如,在识别 “猫” 的任务中:

    • 第 1 层可能检测 “胡须的边缘”(感受野 3×3)。
    • 第 3 层可能检测 “眼睛的形状”(感受野 7×7)。
    • 第 5 层可能检测 “整个猫脸”(感受野 21×21)。

多层卷积代码示例:

from matplotlib import pyplot as plt
import torch.nn as nn
import os
import torch

image_path= os.path.relpath('../data/彩色.png')
img_data = plt.imread(image_path)
#shape是(高,宽,通道)(H, W, C),要转变为(N, C, H, W)---->先转为(C, H, W),再转(N, C, H, W)
print(img_data.shape)

img_data =torch.tensor(img_data.transpose(2,0,1)).unsqueeze(0)

print(img_data.shape)

#卷积层
#进入时的形状:(1,4,501,500)
conv1 = nn.Conv2d(
    in_channels=4,#输入通道数(N,C, H, W)
    out_channels=16,
    kernel_size=3,
    stride=1,
)
#conv1输出的形状:(1,16,499,498)

conv2 = nn.Conv2d(
    in_channels=16,
    out_channels=32,
    kernel_size=3,
    stride=1,
)
#conv2输出的形状:(1,32,497,496)

conv3 = nn.Conv2d(
    in_channels=32,
    out_channels=1,
    kernel_size=3,
    stride=1,
)
#conv3输出的形状:(1,1,495,494)

#conv传入的参数是(N, C, H, W)
output = conv1(img_data)#输出(N, C, H, W)
output = conv2(output)
output = conv3(output)
# print(output.shape)

#还原成图片
out = output.squeeze(0).data.numpy()#去掉batch维度,转成numpy数组
out = out.transpose(1,2,0)
#绘图
plt.imshow(out)
plt.show()

三、池化层

1、池化层的概述

池化层的核心思想是“下采样”,通过对卷积层输出的特征图进行局部区域的聚合操作(如取最大值、平均值),在保留关键特征的同时,降低特征图的尺寸和数据量。

可以把池化层类比为 “信息压缩器”:卷积层提取的特征图往往包含冗余信息(比如相邻像素的特征响应很相似),池化层就像给特征图 “降分辨率”,只保留每个局部区域中最关键的信息(如最强响应、平均响应)

常见的池化方式有两种:

  • 最大池化(Max Pooling):取局部区域的最大值(最常用)。
  • 平均池化(Average Pooling):取局部区域的平均值。

2、池化层的计算:

池化的计算逻辑与卷积类似:用一个固定大小的 “池化窗口” 在特征图上按指定步长滑动,对窗口内的元素进行聚合操作,生成下采样后的特征图

和卷积一样也有步长Stride、Padding

另外注意:在处理多通道输入数据时,池化层对每个输入通道分别池化,而不是像卷积层那样将各个通道的输入相加。这意味着池化层的输出和输入的通道数是相等。

3、池化层的作用

池化操作的优势有:

  1. 通过降低特征图的尺寸,池化层能够减少计算量,从而提升模型的运行效率。

  2. 池化操作可以带来特征的平移、旋转等不变性,这有助于提高模型对输入数据的鲁棒性。

  3. 池化层通常是非线性操作,例如最大值池化,这样可以增强网络的表达能力,进一步提升模型的性能。

但是池化也有缺点:

  1. 池化操作会丢失一些信息,这是它最大的缺点;

import torch
import torch.nn as nn

def test01():
    #设置随机种子
    input_map = torch.randn(1, 1, 7, 7)
    print(input_map)
    print("-"* 50)

    pool1 = nn.MaxPool2d(
        kernel_size=2,
        stride=1,
        return_indices=True#是否返回索引
    )
    output,indices = pool1(input_map)
    print(output,indices)
    print("-"*50)

def test02():
    input_map = torch.randn(1, 1, 7, 7)
    pool1 = nn.AvgPool2d(
        kernel_size=2,
        stride=1,
    )
    output = pool1(input_map)
    print(output.shape)

def test03():
    input_map = torch.randn(1, 1, 7, 7)
    pool1 = nn.AdaptiveMaxPool2d(
        output_size=(3,3)
    )

if __name__ == '__main__':
    test01()
    test02()

运行结果:

对于最大值池化。其返回值有两个:output,indices

1. output(输出特征图)

池化操作后的结果,即每个池化窗口中选出的最大值(对于最大池化)或平均值(对于平均池化)

2. indices(索引)

在最大池化中,记录每个池化窗口中最大值在原特征图中的位置索引

四、自定义网络

思路:

1、自定义网络就是写一个模型的类,首先定义一个类继承于父类nn.Module

2、进行卷积操作,提取特征。定义三层卷积层,每一层进行一次池化和激活(经过池化后要算清楚输出的图像尺寸变化)

3、进行全连接,计算经过卷积池化操作后,总的像素点。这是全连接层特征的输入值

4、前向传播

5、调用模型

import torch
import torch.nn as nn

class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        #第一卷积层
        self.conv1 = nn.Conv2d(
            in_channels=1,#输入的通道数
            out_channels= 16,#输出的通道数,其实就是输出多少张特征图
            kernel_size=3,
            stride=1,
            padding=1
        )
        #激活函数
        self.relu = nn.ReLU()
        #池化层
        self.pool = nn.AdaptiveMaxPool2d(
            output_size=(22,22),
        )
        #第二卷积层
        self.conv2 = nn.Conv2d(
            in_channels=16,
            out_channels=32,
            kernel_size=3,
            stride=1,
            padding=1
        )
        self.pool2 = nn.AdaptiveMaxPool2d(
            output_size=(16,16),
        )
        #全连接层(要判断输入特征就是计算像素点)
        self.fc1 = nn.Linear(
            in_features=32*16*16,
            out_features=10  #输出类别数
        )
    def forward(self, x):
        #x(1,16,26,26)
        x =self.conv1(x)
        #x(1,16,22,22)
        x = self.pool(x)
        #x(1,32,20,20)
        x = self.conv2(x)
        #x(1,32,16,16)
        x = self.pool2(x)

        out = self.fc1(x.view(-1,x.size(1)*x.size(2)*x.size(3)))
        return out

if __name__ == '__main__':
    input_data = torch.randn(1, 1, 28, 28)
    model = MyModel()
    output = model(input_data)
    print(output.shape)

代码讲解:

此处的out_channels前面已经讲过,就是卷积核的个数,经过卷积后输出多少张特征图

单纯的卷积后,函数也还是线性关系,所以i需要激活函数

选用自适应池化

第一步经过卷积后,图像的尺寸变成了(原图像大小是28*28)26*26。

经过池化后,图像的特征减少(也就是像素减少),尺寸就变成了22*22

第二层卷积与第一层类似

全连接层,用nn.Linear,其中输入的特征值,其实就是经过卷积池化后,此时图像的像素值(通道数*高*宽)

而输出特征呢?因为我们目标是做10分类,所以最后训练出来输出的特征值就应该是10个类别

前向传播,每次卷积和池化后,其x的形状会发生改变,追踪每次的改变。

为什么这么做:因为卷积神经网络里,conv(卷积)、pool(池化)层输出的是 4 维张量 (N, C, H, W) (N 批大小、C 通道、H 高、W 宽 ),但全连接层(nn.Linear )要求输入是 2 维张量 (N, feature_num) (feature_num 是一维特征数量 )。所以必须用 view 把 (N, C, H, W) 转成 (N, C*H*W) ,才能让全连接层处理

  • -1 的含义
    让 PyTorch 自动计算该维度的大小。比如 x.view(-1, A*B*C) ,-1 所在维度会被自动填充为 总元素数 / (A*B*C) ,保证元素总数不变。

  • 具体到代码
    假设卷积、池化后,x 的形状是 (N, C, H, W) (N 是 batch 数、C 是通道数、H 高、W 宽 )。
    执行 x.view(-1, x.size(1)*x.size(2)*x.size(3)) 后:

    • 第一个维度填 -1 ,PyTorch 会自动算出是 N(因为总元素数是 N*C*H*W ,第二个维度是 C*H*W ,所以 N = 总元素数 / (C*H*W) )。
    • 第二个维度是 C*H*W ,把每个样本的 (C, H, W) 三维特征图,压成 长度为 C*H*W 的一维向量 。

举个实际例子:
如果 x 是 (2, 16, 22, 22) (2 个样本、16 通道、22 高、22 宽 ),执行 x.view(-1, 16*22*22) :

  • 第一个维度自动算成 2(总元素数 2*16*22*22 = 15488 ,16*22*22=7744 ,15488 / 7744=2 )。
  • 第二个维度是 7744 ,最终形状变成 (2, 7744)