深度学习——04 卷积神经网络CNN

发布于:2025-08-15 ⋅ 阅读:(12) ⋅ 点赞:(0)

1 图像基础知识

  • 图像由一个个像素点拼接而成,每个像素点用 0 - 255 的数值表示亮度:

    • 数值越接近 0 → 颜色越暗(趋近黑色);数值越接近 255 → 颜色越亮(趋近白色);
    • 简单说,像素值就是“亮度开关”,控制画面从黑到白的渐变;
  • 深度学习常用彩色图像,这类图靠 RGB 三个通道“混合”呈现色彩:

    • R(Red):红色通道,负责控制红色分量

    • G(Green):绿色通道,负责控制绿色分量

    • B(Blue):蓝色通道,负责控制蓝色分量

    • 三个通道各自调整像素亮度,再叠加起来,就能组合出丰富的色彩(比如 R+G 偏黄、G+B 偏青)

    在这里插入图片描述

  • 理解这些概念,是处理图像的基础:

    • 后续深度学习(如图像识别、处理)中,模型会拆解、分析 RGB 通道的像素数据,提取特征;

    • 调整通道数值,还能实现“换色”“调亮度”等效果,本质就是改变像素的 RGB 组合。

2 CNN概述

  • CNN(卷积神经网络),本质是带卷积层的神经网络,专门用来“自动学习、提取图像特征”。比如识别图片里的汽车、行人,CNN 能从图像里挖出关键特征帮你判断;

  • CNN 主要靠卷积层 + 池化层 + 全连接层配合干活:

    • 卷积层(CONV):当“特征挖掘机”,从图像里抓局部特征(比如边缘、纹理、形状),层层提取更复杂的特征;

    • 池化层(POOL):当“简化大师”,把卷积层的结果“降维”,减少计算量(比如把 4×4 的特征图缩成 2×2),还能保留关键信息;

    • 全连接层(FC):当“最终裁判”,把前面提取的特征汇总,输出结果(比如判断图像是“汽车”还是“飞机”);

  • 看下图,输入一张汽车图片,流程是:

    在这里插入图片描述

    1. 卷积+激活(RELU):多层卷积层(CONV)反复提取特征,RELU 是激活函数,让网络学会“非线性关系”(否则就是简单加权,学不到复杂特征);
    2. 池化(POOL):隔一段来次池化,压缩数据量,加速计算;
    3. 全连接(FC):最后全连接层汇总特征,输出分类结果(图里输出“car”,判断是汽车)。

3 卷积层

3.1 卷积计算

  • 基础概念

    • Input(输入):要处理的图像,用矩阵表示像素值(比如这里是 5×5 的矩阵);

    • Filter(卷积核/滤波矩阵):一个小矩阵(这里是 3×3),像“筛子”一样在 Input 上滑动,提取特征;

    • Output(特征图):Input 经过 Filter 计算后,得到的新矩阵,里面存的是提取出的特征;

      在这里插入图片描述

  • 卷积的本质:卷积核在输入矩阵上滑动,逐区域做“点积运算”。步骤如下:

    1. 滑动窗口:Filter 从 Input 的左上角开始,每次移动一个像素(默认步长 1),覆盖一个和 Filter 一样大的区域(比如 3×3);

    2. 做点积:把 Filter 和覆盖区域的对应位置数值相乘,再把结果相加;

      • 比如第二张图里,第一个窗口的计算:对应位置相乘再求和,得到特征图的第一个数值 4
        (1×1)+(1×0)+(1×1)+(0×0)+(1×1)+(1×0)+(0×1)+(0×0)+(1×1)=4 (1×1)+(1×0)+(1×1)+(0×0)+(1×1)+(1×0)+(0×1)+(0×0)+(1×1) = 4 (1×1)+(1×0)+(1×1)+(0×0)+(1×1)+(1×0)+(0×1)+(0×0)+(1×1)=4

      在这里插入图片描述

    3. 逐次滑动:Filter 一路滑过 Input 的所有可能区域,每个区域算一次点积,最终拼出特征图(Output)

      在这里插入图片描述

3.2 填充(Padding)

  • 卷积计算时,卷积核在图像上滑动,会让输出的特征图(Feature Map)比原图小(比如 5×5 原图,用 3×3 卷积核,不填充的话,特征图会变成 3×3)。但有时候我们想让卷积后的图像大小不变(比如保留边缘信息、控制特征图尺寸),这就需要 Padding;

  • 简单说,就是在原始图像的周围“加一圈(或多圈)像素”(可以是 0 或其他值),让图像变大,这样卷积核滑动时,边缘区域也能被覆盖到,最终输出的特征图尺寸就和原图一样(或按需调整);

  • 如下图:

    在这里插入图片描述

    • 左边的“Stride 1 with Padding”: 浅蓝色是原始图像,周围的浅绿、灰色是填充的 Padding

    • 卷积核(隐含的 3×3 之类的)滑动时,因为有了填充,边缘区域也能参与计算,输出的特征图(右边的“Feature Map”)尺寸就能和“填充后的有效计算区域”匹配,实现保持图像大小的效果。

  • Padding 主要用来:

    • 控制特征图尺寸(让卷积前后尺寸不变,或按公式计算特定大小);

    • 保留图像边缘信息(避免卷积时边缘像素只用一次,丢失细节)。

3.3 步长(Stride)

  • Stride 翻译为“步长”,指卷积核在图像上滑动时,每次移动的像素数。比如步长=1,就是每次挪1个像素;步长=2,就是每次挪2个像素;

  • 步长=1 的情况:

    • 卷积核每次只移动1个像素,一点点“扫过”整个图像,所以特征图(Feature Map)的像素会逐点填充(从第1步的1个点,到第9步填满);
    • 这样提取的特征图更细致,但计算量也大(因为滑动次数多);

    在这里插入图片描述

  • 步长=2 的情况:

    • 卷积核每次跳2个像素,滑动次数变少,特征图的像素会稀疏填充(每次填一块,最终特征图更小);
    • 这样计算更快(减少卷积次数),但会丢失一些细节(因为跳过了中间像素);

    在这里插入图片描述

  • Stride 是调节 CNN 效率和特征提取的“开关”:

    • 步长小(如1):特征图大、细节多,但计算慢;

    • 步长大(如2):特征图小、细节少,但计算快,还能起到“降维”效果(压缩数据量)。

3.4 多通道卷积计算

  • 实际图像(比如彩色图)是多通道的(如 RGB 有红、绿、蓝 3 个通道)。卷积不能只处理单通道,得让每个通道都参与,才能提取完整的彩色特征;

  • 多通道卷积的流程

    在这里插入图片描述

    • 输入(Input Image):是 5×5×3 的彩色图(5 宽×5 高×3 通道),3 个通道分别对应 R、G、B;

    • 卷积核(Filter):也是 3 通道的(3×3×3),每个通道对应一个小卷积核,分别处理 R、G、B 通道;

    • 计算方式

      1. 每个通道单独做“单通道卷积”(通道对齐,比如 R 通道的图像和 R 通道的卷积核计算);
      2. 把 3 个通道的卷积结果相加,得到最终的特征图(Feature Map)(比如下图 3 通道结果相加:-78 + (-81) + 88 = -71 ,对应特征图的一个像素);

      在这里插入图片描述

3.5 多卷积核卷积计算

  • 基础概念

    • 输入(Input Image):5×5×3 的彩色图(3 通道:R/G/B)
    • 多卷积核(Filter):用 2 个 3×3×3 的卷积核(每个核对应 3 通道,匹配输入的 3 通道)
    • 特征图(Feature Map):每个卷积核单独和输入做卷积,得到 2 张 3×3 的特征图(Feature Map #1、#2)
    • 堆叠(Stack):把 2 张特征图“叠”(升维)起来,得到 3×3×2 的输出(通道数=卷积核数量)

    在这里插入图片描述

  • 下图展示了带 padding(输入变成 7×7×3)的多卷积核计算:

    • 输入(Input Volume):7×7×3 的图像(加了 padding 后变大,方便控制输出尺寸)

    • 卷积核(Filter W0、W1):每个核是 3×3×3,还有各自的偏置(Bias b0、b1)

    • 逐点计算:每个卷积核在输入上滑动,对应通道做点积 + 偏置,得到输出特征图(Output Volume 3×3×2);

    在这里插入图片描述

  • 为啥用多个卷积核?

    • 每个卷积核提取不同的特征(比如一个抓边缘,一个抓纹理);

    • 多个核“合作”,让网络能学到更丰富的特征,提升识别、分类的效果。

3.6 特征图大小

  • 先明确几个核心参数(以正方形图像为例,方便计算,长方形原理相同):

    • 输入图像大小W×WW \times WW×W(比如 5×5)

    • 卷积核大小F×FF \times FF×F(比如 3×3)

    • 步长(Stride)SSS(卷积核每次滑动的像素数,比如 1)

    • 填充(Padding)PPP(图像边缘补充的像素层数,比如 1)

    • 输出特征图大小N×NN \times NN×N(最终要算的结果,比如 5×5)

  • 输出特征图的尺寸遵循这个公式:
    N=W−F+2PS+1 N = \frac{W - F + 2P}{S} + 1 N=SWF+2P+1

    • 分子部分(W−F+2PW - F + 2PWF+2P:调整输入尺寸,考虑填充后“有效计算区域”的大小(填充让边缘也能参与计算);

    • 除以步长(÷S\div S÷S:因为卷积核每次跳SSS个像素,所以要算能跳多少次;

    • 加 1(+1+1+1:保证最后一步滑动的区域被计算(比如步长 1 时,5×5 图像用 3×3 核,不加 1 会少算一行/列);

  • 例:

    • 输入W=5W = 5W=5,卷积核F=3F = 3F=3,步长S=1S = 1S=1,填充P=1P = 1P=1

    • 代入公式:
      N=5−3+2×11+1=41+1=5 N = \frac{5 - 3 + 2×1}{1} + 1 = \frac{4}{1} + 1 = 5 N=153+2×1+1=14+1=5

    • 所以输出特征图是 5×5,和原图尺寸一样(因为填充和步长配合,抵消了卷积核带来的尺寸压缩)。

  • 上面这个公式帮我们精准控制特征图尺寸

    • 想让特征图变小(压缩数据、提取高层特征)→ 增大步长SSS或减小填充PPP

    • 想让特征图不变(保留边缘信息、对齐尺寸)→ 用公式算出需要的PPP(比如例子里P=1P=1P=1让 5×5 图过 3×3 核后不变)。

3.7 API

  • 下面代码会创建一个 2D 卷积层,让输入的图像/特征图经过卷积操作,输出新的特征图;

    conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding)
    
    • in_channels:输入的通道数。比如输入是 RGB 彩色图,就是 3(R、G、B 三个通道);如果是灰度图,就是 1;如果是上一层卷积的输出(比如上一层输出 16 通道),这里就填 16

    • out_channels:输出的通道数,等价于卷积核的数量。每个卷积核会提取一组特征,out_channels 设为 6,就会生成 6 个不同的特征图(每个核对应一个);

    • kernel_size:卷积核的尺寸(高和宽)。一般用奇数(如 3 表示 3×3 卷积核),方便在中心对齐,常用 357 等;

    • stride:卷积核移动的步长。比如 stride=1 是每次移动 1 个像素;stride=2 是每次跳 2 个像素,会让输出特征图变小(加速计算、降维);

    • padding:图像边缘填充的像素数。填 0 是不填充(默认);填 1 是在图像四周补 1 层像素(常用 0 或 1),用来控制输出特征图的尺寸(比如让输出和输入尺寸一样);

  • 例:

    import matplotlib.pyplot as plt
    import torch
    import torch.nn as nn
    
    img = plt.imread('data/img.jpg')
    # 显示图像
    plt.imshow(img)
    # 展示图像窗口
    plt.show()
    # 打印图像数组的形状,输出格式为(高度, 宽度, 通道数)
    print(img.shape)
    

    在这里插入图片描述

    # 将NumPy数组转换为PyTorch张量,并进行维度调整:
    # 1. permute(2, 0, 1):将通道维度移到最前面,形状变为(通道数, 高度, 宽度)
    # 2. unsqueeze(0):在第0维添加一个批次维度,符合PyTorch的输入格式要求
    # 最终形状为(批次大小, 通道数, 高度, 宽度)
    print(torch.tensor(img).permute(2, 0, 1).unsqueeze(0).shape)
    # 完成上述转换并将数据类型转换为float(PyTorch卷积层通常需要float类型输入)
    # 得到符合卷积层输入要求的张量
    img_t = torch.tensor(img).permute(2, 0, 1).unsqueeze(0).float()
    

    在这里插入图片描述

    # 定义一个2D卷积层:
    # - in_channels=3:输入通道数为3(对应RGB图像)
    # - out_channels=5:输出通道数为5(使用5个不同的卷积核)
    # - kernel_size=3:卷积核大小为3×3
    # - stride=2:步长为2(每次滑动2个像素)
    # - padding=0:不使用填充
    conv = nn.Conv2d(in_channels=3, out_channels=5, kernel_size=3, stride=2, padding=0)
    # 将处理好的图像张量传入卷积层,得到特征图
    feature_map = conv(img_t)
    # 打印卷积后的特征图(张量形式)
    print(feature_map)
    # 打印特征图的形状,格式为(批次大小, 输出通道数, 特征图高度, 特征图宽度)
    # 特征图的尺寸可通过公式计算:N = (W - F + 2P) / S + 1
    # 其中W为输入尺寸,F为卷积核大小,P为填充,S为步长
    print(feature_map.shape)
    
    tensor([[[[ 37.2262,  38.3072,  38.8884,  ...,  46.7872,  45.9860,  43.6856],
              [ 37.6177,  38.8311,  39.7673,  ...,  48.5804,  47.5607,  44.8240],
              [ 38.2571,  38.8595,  39.0986,  ...,  46.3630,  43.1798,  39.6248],
              ...,
              [ 31.3799,  32.4860,  33.6094,  ...,  38.7840,  42.4886,  43.9096],
              [ 15.6627,  17.2249,  16.9028,  ...,  22.9045,  25.6382,  27.6716],
              [-70.2792, -70.0976, -69.4878,  ..., -60.1271, -56.1115, -54.0182]],
    
             [[-32.6652, -33.5260, -33.6521,  ..., -41.3492, -36.4856, -33.0663],
              [-32.5468, -33.2193, -33.4424,  ..., -37.8777, -34.4112, -31.1155],
              [-32.2119, -33.0693, -33.0804,  ..., -34.3828, -31.2587, -29.9524],
              ...,
              [-26.5160, -28.6130, -31.0046,  ..., -31.0635, -33.4776, -36.6271],
              [-21.6037, -21.4986, -22.5574,  ..., -27.2931, -30.6879, -33.4142],
              [-26.9093, -26.2654, -25.7613,  ..., -32.2512, -34.0911, -36.3150]],
    
             [[-69.5624, -70.0809, -70.3195,  ..., -43.6152, -35.0180, -28.3564],
              [-69.1709, -69.6287, -69.9137,  ..., -36.3166, -27.5693, -22.2837],
              [-69.3779, -69.2347, -69.1908,  ..., -28.6328, -20.6041, -18.7608],
              ...,
              [-19.5806, -19.4940, -19.8871,  ..., -28.9880, -33.3475, -37.3717],
              [-19.4366, -19.2948, -19.2835,  ..., -30.0720, -35.3198, -39.5398],
              [-47.8041, -47.6739, -47.9317,  ..., -54.0827, -58.0802, -59.6470]],
    
             [[-51.1677, -50.8738, -51.5760,  ..., -32.2707, -26.1747, -20.9306],
              [-51.0603, -50.5401, -50.7060,  ..., -25.3513, -19.0576, -16.0896],
              [-50.3795, -50.1814, -50.0971,  ..., -18.6564, -15.1480, -14.1541],
              ...,
              [-14.7519, -15.9627, -18.1150,  ..., -18.7094, -21.7697, -24.8304],
              [-29.0825, -29.1188, -29.9929,  ..., -34.1509, -37.7280, -40.6062],
              [-65.6864, -65.7057, -65.4743,  ..., -70.5306, -73.0763, -74.7875]],
    
             [[-22.6594, -23.2875, -22.8007,  ...,  -6.7893,  -4.1577,  -3.6396],
              [-22.7054, -23.3115, -23.5371,  ...,  -4.8020,  -4.4991,  -3.0407],
              [-23.1746, -23.3149, -23.1325,  ...,  -3.0394,  -1.3144,  -0.8948],
              ...,
              [ -3.0622,  -3.5161,  -2.8403,  ..., -10.8515, -12.2700, -13.0355],
              [  4.2112,   4.2549,   4.7745,  ...,  -1.1944,  -2.8455,  -2.1904],
              [ 22.2717,  21.1726,  20.5021,  ...,  18.5477,  17.3071,  17.4235]]]],
           grad_fn=<ConvolutionBackward0>)
    torch.Size([1, 5, 319, 319])
    

4 池化层

4.1 池化层计算

  • 池化层的作用是 “降低维度、缩减模型大小、提高计算速度”

    • 把大的特征图缩小(比如 3×3→2×2),减少计算量;

    • 保留关键特征(比如最大池化抓最突出的,平均池化抓整体趋势),让模型更高效;

  • 最大池化

    • 输入:3×3 的特征图(数值 0-8);

    • 操作:用 2×2 的“窗口”,每次选窗口里最大的数,作为输出;

      • 第一块窗口(0,1,3,4)→ 最大是 4;
      • 第二块窗口(1,2,4,5)→ 最大是 5;
      • 第三块窗口(3,4,6,7)→ 最大是 7;
      • 第四块窗口(4,5,7,8)→ 最大是 8;
    • 输出:2×2 的特征图(4,5,7,8);

    • 特点:突出局部最显著的特征(比如图像里最亮的边缘、最大的纹理);

    在这里插入图片描述

  • 平均池化

    • 输入:同样 3×3 的特征图

    • 操作:用 2×2 的“窗口”,每次算窗口里所有数的平均值,作为输出

      • 第一块窗口(0,1,3,4)→ 平均是 (0+1+3+4)/4 = 2;
      • 第二块窗口(1,2,4,5)→ 平均是 (1+2+4+5)/4 = 3;
      • 第三块窗口(3,4,6,7)→ 平均是 (3+4+6+7)/4 = 5;
      • 第四块窗口(4,5,7,8)→ 平均是 (4+5+7+8)/4 = 6;
    • 输出:2×2 的特征图(2,3,5,6)

    • 特点:保留局部的“平均趋势”,让特征更平滑(适合需要整体信息的场景)

    在这里插入图片描述

  • 不管最大还是平均池化,都是用**固定大小的窗口(如 2×2)**在特征图上滑动,按规则(取最大/平均)压缩数据。这样做的好处是:

    • 减少计算量(特征图变小,后续层计算更快);

    • 让特征更“鲁棒”(比如最大池化不怕小的干扰,平均池化能平滑噪声)。

4.2 填充(Padding)

在这里插入图片描述

4.3 步长(Stride)

在这里插入图片描述

4.4 多通道池化层计算

在这里插入图片描述

4.5 API

  • 最大池化与平均池化的API:

    # 最大池化
    nn.MaxPool2d(kernel_size=2, stride=2, padding=1)
    # 平均池化
    nn.AvgPool2d(kernel_size=2, stride=1, padding=0)
    
  • 例:

    import torch
    import torch.nn as nn
    
    # 定义输入数据,这里构造了一个 3 通道、3 行 3 列的三维张量(形状:[3, 3, 3]  通道数×高度×宽度)
    # 可以理解为 3 张 3×3 的特征图堆叠在一起
    inputs = torch.tensor([[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
                           [[10, 20, 30], [40, 50, 60], [70, 80, 90]],
                           [[11, 22, 33], [44, 55, 66], [77, 88, 99]]]).float()
    # 定义一个最大池化层(MaxPool2d)
    # kernel_size=3:池化窗口的大小是 3×3,会在输入特征图上取 3×3 区域的最大值
    # stride=1:池化窗口每次滑动的步长是 1 个像素
    # padding=0:不对输入特征图边缘做填充
    max_pool = nn.MaxPool2d(kernel_size=3,stride=1,padding=0)
    # 对输入数据执行最大池化操作,并打印输出结果的形状
    # 由于输入是 3×3×3,3×3 的窗口滑动步长 1 且无填充,最终每个通道只能得到 1×1 的结果
    # 所以输出形状是 [3, 1, 1](通道数保持 3,高度和宽度变为 1)
    print(max_pool(inputs).shape)
    

    在这里插入图片描述

    # 定义一个平均池化层(AvgPool2d)
    # kernel_size=2:池化窗口的大小是 2×2,会在输入特征图上取 2×2 区域的平均值
    # stride=1:池化窗口每次滑动的步长是 1 个像素
    # padding=0:不对输入特征图边缘做填充
    avg_pool = nn.AvgPool2d(kernel_size=2,stride=1,padding=0)
    # 对输入数据执行平均池化操作,并打印输出结果的形状
    # 输入是 3×3×3,2×2 的窗口滑动步长 1 且无填充,最终每个通道会得到 2×2 的结果 
    # 所以输出形状是 [3, 2, 2](通道数保持 3,高度和宽度变为 2)
    print(avg_pool(inputs).shape)
    

    在这里插入图片描述

5 图像分类案例

5.1 导包

import torch
import torch.nn as nn
from torchvision.datasets import CIFAR10
from torchvision.transforms import ToTensor
from torchvision.transforms import Compose
import torch.optim as optim
from torch.utils.data import DataLoader
import time
import matplotlib.pyplot as plt
from torchsummary import summary
BATCH_SIZE = 8
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

5.2 加载 CIFAR10 数据集

  • CIFAR-10数据集有5万张训练图像、1万张测试图像,共10个类别,每个类别有6k张图像,每张图像的大小是32×32×3。下图列举了10个类别,每一类随机展示了10张图片:

    在这里插入图片描述

  • PyTorch 中的torchvision.datasets计算机视觉模块封装了 CIFAR10 数据集:

    # 加载 CIFAR10 数据集
    def create_dataset():
        train_dataset = CIFAR10(root='data', train=True, transform=Compose([ToTensor()]), download=True)
        valid_dataset = CIFAR10(root='data', train=False, transform=Compose([ToTensor()]), download=True)
        return train_dataset, valid_dataset
    
    train_dataset, valid_dataset = create_dataset()
    print("数据集类别:", train_dataset.class_to_idx)
    print("训练集数据集:", train_dataset.data.shape)
    print("测试集数据集:", valid_dataset.data.shape)
    # 图像展示
    plt.figure(figsize=(2, 2))
    plt.imshow(train_dataset.data[1])
    plt.title(train_dataset.targets[1])
    plt.show()
    

    在这里插入图片描述

5.3 搭建图像分类网络

  • 要搭建的网络结构如下:

    • 输入是 32×32 的图像(CIFAR10 数据集的图像尺寸)
    • 经过 卷积层→池化层→卷积层→池化层→全连接层→全连接层→输出层
    • 最终输出 10 维结果(对应 CIFAR10 的 10 类物体)

    在这里插入图片描述

  • 流程解读:

    • 输入层。输入形状:32×32(通道数默认 3,对应 RGB 彩色图,所以实际输入是 1×3×32×32,1 是批次,3 是通道);

    • 第一个卷积层

      • 输入通道:3(对应 RGB 3 通道);
      • 输出通道:6(用 6 个不同的卷积核,提取 6 种特征);
      • 卷积核:3×3(小窗口提取局部特征);
      • 操作:3×3 卷积核在 32×32 图像上滑动,输出 30×30 的特征图(因为 32-3+1=30 ,没填充);
      • 激活:卷积后接 ReLU 激活函数(给网络加“非线性”,让它能学复杂模式);
    • 第一个池化层

      • 输入:30×30(卷积层的输出);
      • 池化核:2×2,步长 2(每次跳 2 个像素);
      • 操作:把 30×30 压缩成 15×15(30/2=15 ,因为步长 2),起到降维、提速的作用;
    • 第二个卷积层

      • 输入通道:6(第一个池化层的输出通道数);
      • 输出通道:16(用 16 个卷积核,提取更复杂的特征);
      • 卷积核:3×3
      • 操作:在 15×15 特征图上卷积,输出 13×13(15-3+1=13);
      • 激活:同样接 ReLU
    • 第二个池化层

      • 输入:13×13(第二个卷积层的输出);
      • 池化核:2×2,步长 2
      • 操作:压缩成 6×6(13//2=6 ,整数除法);
    • 第一个全连接层

      • 输入:576 维(6×6×16 展开成一维向量:6×6×16=576);

      • 输出:120 维(把卷积提取的特征,映射到 120 维的新特征空间);

    • 第二个全连接层

      • 输入:120 维(上一层输出);

      • 输出:84 维(继续映射特征);

    • 输出层

      • 输入:84 维(上一层输出);

      • 输出:10 维(对应 10 分类,输出每个类别的概率);

  • 网络设计的逻辑

    • 卷积+池化:先通过卷积提取局部特征(边缘、纹理…),池化压缩数据、保留关键信息,让网络又快又准;

    • 全连接层:把卷积提取的“局部特征”汇总成“全局特征”,最终输出分类结果;

    • ReLU 激活:给网络加非线性,否则多层网络会退化成“线性组合”,学不到复杂模式;

  • 代码:

    # 搭建图像分类网络
    class ImageClassification(nn.Module):
        def __init__(self):
            super(ImageClassification, self).__init__()
            
            # 第一个卷积层:输入3通道(RGB图像),输出6通道(6个卷积核),卷积核大小3×3,步长为1
            self.conv1 = nn.Conv2d(in_channels=3, out_channels=6, kernel_size=3, stride=1)
            # 第一个最大池化层:池化窗口2×2,步长2(特征图尺寸减半)
            self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
            
            # 第二个卷积层:输入6通道(上一层输出),输出16通道,卷积核大小3×3
            self.conv2 = nn.Conv2d(in_channels=6, out_channels=16, kernel_size=3, stride=1)
            # 第二个最大池化层:池化窗口2×2,步长2
            self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
            
            # 第一个全连接层:输入576维(由卷积层输出特征图展平得到),输出120维
            self.conv3 = nn.Linear(in_features=576, out_features=120)
            # 第二个全连接层:输入120维,输出84维
            self.conv4 = nn.Linear(in_features=120, out_features=84)
            # 输出层:输入84维,输出10维(对应10个类别)
            self.conv5 = nn.Linear(in_features=84, out_features=10)
    
        # 前向传播方法,定义数据在网络中的流动路径
        def forward(self, x):
            # 输入x经过第一个卷积层,再通过ReLU激活函数
            x1 = torch.relu(self.conv1(x))
            # 激活后的数据经过第一个池化层
            x1 = self.pool1(x1)
            
            # 池化后的数据经过第二个卷积层,再通过ReLU激活
            x2 = torch.relu(self.conv2(x1))
            # 激活后的数据经过第二个池化层
            x2 = self.pool2(x2)
            
            # 将卷积特征图展平为一维向量(保留批次维度,其他维度合并)
            # x2.size(0)是批次大小,-1表示自动计算剩余维度的乘积
            x2 = x2.reshape(x2.size(0), -1)
            
            # 展平后的向量经过第一个全连接层,再通过ReLU激活
            x3 = torch.relu(self.conv3(x2))
            # 经过第二个全连接层,再通过ReLU激活
            x4 = torch.relu(self.conv4(x3))
            # 最后通过输出层,得到10个类别的原始分数
            out = self.conv5(x4)
            
            return out
    model = ImageClassification().to(device)
    summary(model, input_size=(3, 32, 32), batch_size=1)
    

    在这里插入图片描述

5.4 编写训练函数

# 定义训练函数,参数是模型(model)和训练数据集(train_dataset)
def train(model, train_dataset):
    # 1. 构建损失函数:CrossEntropyLoss 适用于多分类任务,计算预测与真实标签的交叉熵
    criterion = nn.CrossEntropyLoss()
    # 2. 构建优化器:使用 Adam 优化器,学习率设为 1e-3,传入模型参数
    optimizer = optim.Adam(model.parameters(), lr=1e-3)
    # 3. 训练轮数:整个数据集会被遍历 100 次
    epoch = 100
    
    # 外层循环:遍历训练轮数
    for epoch_idx in range(epoch):
        # 构建数据加载器:按批次加载数据,shuffle=True 表示每个 epoch 打乱数据
        dataloader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
        # 样本数量统计,用于计算平均损失
        sam_num = 0
        # 损失总和,用于计算平均损失
        total_loss = 0.0
        # 记录训练开始时间
        start = time.time()
        
        # 内层循环:遍历一个 epoch 的所有批次数据
        for x, y in dataloader:
            # 前向传播:将输入 x 传入模型,得到预测输出 output
            output = model(x)
            # 计算损失:用损失函数对比 output 和真实标签 y 的差异
            loss = criterion(output, y)
            # 梯度清零:每次反向传播前,清除模型参数的历史梯度(避免累积影响)
            optimizer.zero_grad()
            # 反向传播:计算损失对模型参数的梯度
            loss.backward()
            # 参数更新:根据梯度和优化器规则,更新模型参数
            optimizer.step()
            # 累加损失值(item() 取出张量的数值,避免计算图占用内存)
            total_loss += loss.item()
            # 累加样本数量(一个批次的样本数通常是 BATCH_SIZE)
            sam_num += 1
        
        # 每个 epoch 结束后,打印训练信息
        # 输出格式:epoch 序号、平均损失(总损失/样本数)、耗时(当前 epoch 训练时长)
        print('epoch:%2s loss:%.5f time:%.2fs' % (epoch_idx + 1, total_loss / sam_num, time.time() - start))
        # 保存模型:每个 epoch 结束后,把模型当前的参数(state_dict)保存到指定路径
        torch.save(model.state_dict(), 'model/image_classification.pth')

# 数据集加载
train_dataset, valid_dataset = create_dataset()
# 模型实例化
model = ImageClassification()
# 模型训练
train(model,train_dataset)

在这里插入图片描述

  • 因为训练时间较长,就将次数修改为10次。

5.5 编写预测函数

def test(valid_dataset):
    # 1. 构建数据加载器:按批次加载验证集数据
    dataloader = DataLoader(valid_dataset, batch_size=BATCH_SIZE, shuffle=False)
    
    # 2. 加载模型并初始化
    model = ImageClassification()
    # 加载训练好的参数:从保存的路径中读取模型权重
    model.load_state_dict(torch.load('model/image_classification.pth'))
    # 切换模型到评估模式:关闭 Dropout、BatchNorm 等训练时的特殊行为
    model.eval()
    
    # 3. 初始化精度统计变量
    # 正确预测的样本总数
    total_correct = 0
    # 参与测试的样本总数
    total_samples = 0
    
    # 4. 遍历验证集数据,逐批测试
    for x, y in dataloader:
        # 前向传播:获取模型预测结果
        output = model(x)
        # 计算预测正确的数量:
        # torch.argmax(output, dim=-1) 取每个样本预测概率最大的类别
        # 与真实标签 y 对比,相等为 1,不等为 0,sum() 统计一个批次中正确的数量
        total_correct += (torch.argmax(output, dim=-1) == y).sum()
        # 累加样本总数(一个批次的样本数是 len(y))
        total_samples += len(y)
    
    # 5. 计算并打印精度
    # 精度 = 正确数 / 总样本数,格式化为百分比输出(保留两位小数)
    print('Acc: %.2f' % (total_correct / total_samples))
test(valid_dataset)

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到