Atri Website

Back

多层感知机:分类问题#

CIFAR-10 数据集是深度学习中常用的数据集,其包含 60000 张 32×32 色图像,分为 10 个类,每类 6000 张。有 50000 张训练图片和 10000 张测试图片。

请基于该数据集搭建包含三个及以上的卷积层的多层感知机网络,以解决 10 分类问题。

(1) 输出网络结构;(1 分)

(2) 使用 tensorboard 对训练过程中的 loss 和 accuracy 进行可视化展示;(2 分)

(3) 保存训练模型为模型文件,并使用训练好的模型对测试集中的图片进行预测,输出预测结果与预测概率;(2 分)

(4) 画出训练集和验证集的混淆矩阵;(2 分)

(5) 分析网络参数(例如网络深度、不同的激活函数、神经元数量等)对预测结果的影响;(2 分)

(6) 在损失函数为交叉熵的情况下,对比网络最后一层是否使用 softmax 的性能差异并分析其产生的原因。(1 分)


核心任务:使用 CIFAR-10 数据集,搭建一个卷积神经网络 (CNN) 来解决 10 分类的图像问题。

注意:题目中写的是“多层感知机网络”,但紧接着要求“包含三个及以上的卷积层”。这是一个典型的卷积神经网络 (CNN) 任务,而不是传统意义上只包含全连接层的多层感知机 (MLP)。MLP 直接处理拉平的图像向量效果会很差,使用 CNN 是正确的解法

内容要求分解#

  1. (1) 输出网络结构:
    • 要求:展示你设计的网络结构
    • 做什么:使用 PyTorch (torch.nn.Module) 或 TensorFlow (tf.keras.Model) 定义一个 CNN 模型。这个模型必须至少包含 3 个卷积层 (nn.Conv2d)。一个经典的结构是 (Conv -> ReLU -> Pool) * N -> Flatten -> Linear -> ReLU -> Linear
    • 实验报告中要写什么:在代码中 print(your_model),将打印出的网络结构文本复制到报告中。也可以使用 torchsummary 等库来更详细地展示
  2. (2) TensorBoard 可视化:
    • 要求:使用 TensorBoard 展示训练过程中的 loss 和 accuracy
    • 做什么
      • 在 PyTorch 中,使用 from torch.utils.tensorboard import SummaryWriter
      • 在训练循环中,记录每个 epoch 或每个 batch 的训练/验证 loss 和 accuracy,并使用 writer.add_scalar() 函数将它们写入日志
      • 训练完成后,在命令行启动 tensorboard —logdir=runs (runs 是你的日志文件夹),然后将生成的图表截图
    • 实验报告中要写什么:粘贴 TensorBoard 中显示的 loss 曲线和 accuracy 曲线的截图
  3. (3) 模型保存与预测:
    • 要求:保存训练好的模型,并用它来预测测试集图片
    • 做什么
      • 保存:训练结束后,使用 torch.save(model.state_dict(), ‘cifar10_cnn.pth’) 保存模型参数
      • 加载:创建一个新的模型实例,然后使用 model.load_state_dict(torch.load(‘cifar10_cnn.pth’)) 加载参数
      • 预测:从测试集中随机挑选几张图片,输入到加载好的模型中。注意要将模型设置为评估模式 model.eval()
      • 输出概率:模型输出的是 logits,需要经过 torch.nn.functional.softmax 函数转换成概率分布
      • 输出结果:使用 torch.argmax() 从概率分布中找到概率最高的类别作为预测结果
    • 实验报告中要写什么:展示几张测试图片,以及模型对它们的预测类别和对应的概率
  4. (4) 混淆矩阵:
    • 要求:画出训练集和验证集的混淆矩阵
    • 做什么
      • 在整个训练集和验证集上运行预测,收集所有的真实标签和预测标签
      • 使用 sklearn.metrics.confusion_matrix 生成混淆矩阵。
      • 使用 seaborn.heatmap 或 sklearn.metrics.ConfusionMatrixDisplay 将混淆矩阵可视化。
    • 实验报告中要写什么:展示训练集和验证集的混淆矩阵热力图,并可以简要分析一下,比如哪些类别之间容易混淆(例如猫和狗)。
  5. (5) 网络参数影响分析:
    • 要求:分析网络参数(深度、激活函数、神经元数量等)对结果的影响。
    • 做什么:这是一个开放性的分析题。你需要进行对比实验。例如:
      • 网络深度:设计一个 3 层的 CNN 和一个 5 层的 CNN,比较它们的性能。
      • 激活函数:将模型中的 ReLU 替换为 LeakyReLU 或 Sigmoid,比较性能。
      • 神经元数量:改变全连接层 (nn.Linear) 的隐藏单元数量,或者改变卷积层 (nn.Conv2d) 的输出通道数(滤波器数量),比较性能。
    • 实验报告中要写什么:用表格或图表对比不同配置下的模型性能(如最终的测试准确率),并分析原因。例如,更深的网络可能性能更好但也更容易过拟合;Sigmoid 容易导致梯度消失等。
  6. (6) Softmax 与交叉熵损失函数:
    • 要求:对比网络最后一层是否使用 softmax 的性能差异,并分析原因。
    • 做什么
      • 实验对比
        • 正确做法:网络最后一层直接输出 logits (无激活函数),损失函数使用 torch.nn.CrossEntropyLoss。
        • 错误做法:网络最后一层接一个 nn.Softmax 层,损失函数依然使用 torch.nn.CrossEntropyLoss。
        • 训练这两个网络,对比它们的性能。
      • 原因分析:这是非常重要的一个知识点。PyTorch 的 CrossEntropyLoss 函数内部已经包含了 LogSoftmax 和 NLLLoss。如果你在模型末尾手动添加了 Softmax,那么在计算损失时就相当于进行了两次 Softmax 操作,这会导致梯度计算不正确,使得模型训练非常缓慢或无法收敛。
    • 实验报告中要写什么:展示两种做法的性能对比(比如 loss 曲线),并清晰地解释 CrossEntropyLoss 的工作原理,阐明为什么在它的前面不应该加 Softmax 层。

题目分析#

使用的 CIFAR-10 数据集是一个有十个类别的、大小为 3 X 32 X 32。

It has the classes: ‘airplane’, ‘automobile’, ‘bird’, ‘cat’, ‘deer’, ‘dog’, ‘frog’, ‘horse’, ‘ship’, ‘truck’. The images in CIFAR-10 are of size 3x32x32, i.e. 3-channel color images of 32x32 pixels in size.

它包含以下类别:‘飞机’、‘汽车’、‘鸟’、‘猫’、‘鹿’、‘狗’、‘青蛙’、‘马’、‘船’、‘卡车’。CIFAR-10 中的图像尺寸为 3x32x32,即 3 通道彩色图像,每个通道包含 32x32 像素。

题目要求”搭建包含三个及以上的卷积层的多层感知机网络”,指明了实际上要搭建的是一个 三层以上网络的 CNN 卷积神经网络

常用的卷积神经网络有:

  • LeNet-5: 早期用于手写数字识别的 CNN,是现代 CNN 的鼻祖
  • AlexNet: 在 ImageNet 竞赛中取得突破,引爆了深度学习革命
  • VGGNet: 展示了通过堆叠小的卷积核(3x3)可以构建出很深很有效的网络
  • GoogLeNet: 引入了“Inception 模块” ,可以在同一层使用不同尺寸的卷积核
  • ResNet: 里程碑式的模型。通过引入残差连接,成功训练了超过 100 层甚至 1000 层的超深网络,解决了深度网络的梯度消失和退化问题

对于本题,为方便起见,采用 VGGNet 风格来构建特征提取器,结合经典的 LeNet-5 全连接层结构,实现轻量级的 CNN。


(1) 输出网络结构#

即三个 “cov → Pool” 的 LeNet-5 结构,而每一个卷积层的核为 VGGNet 的 “3 x 3”样式,全连接层使用的经典的 LeNet-5 的 “120 → 84 → 10”的结构。

Net(
  (conv1): Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv2): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv3): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (fc1): Linear(in_features=1024, out_features=120, bias=True)
  (fc2): Linear(in_features=120, out_features=84, bias=True)
  (fc3): Linear(in_features=84, out_features=10, bias=True)
)
bash

(2) 使用 tensorboard 对训练过程中的 loss 和 accuracy 进行可视化展示#

相关代码:

# 使用 tensorboard 可视化训练过程
    from torch.utils.tensorboard import SummaryWriter
    writer = SummaryWriter('./logs')
    trained_model,history = train(
        model=model,
        device=device,
        trainloader=trainloader,
        loss_function=loss_function,
        optimizer=optimizer,
        epoches=20,
        writer=writer
    )

    writer.close()
    print('结束Summary的写入')
python

结果

准确率

损失值

(3) 保存训练模型为模型文件,并使用训练好的模型对测试集中的图片进行预测,输出预测结果与预测概率#

3.1.模型持久化#

    # 模型持久化区块
    PATH = './cifar_net.pth'
    save_model(trained_model,PATH)
python

3.2. 简单图片预测#

结果

GroundTruth:  cat   ship  ship  plane frog  frog  car   frog 
Predicted:  cat   ship  ship  plane frog  frog  car   deer 
bash

ad

3.3. 测试集整体准确率预测#

    model = Net()
    model.load_state_dict(torch.load(PATH, weights_only=True))
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    test_accuracy = pred_model(
        model=model,
        testloader=testloader,
        device=device
    )
    print(f'测试集准确率:{test_accuracy*100:.2f}%')
python

结果

测试集准确率:71.46%
bash

3.4. 各类别的准确率预测#

结果

Class: plane Accuracy: 69.0 %
Class: car   Accuracy: 87.0 %
Class: bird  Accuracy: 57.0 %
Class: cat   Accuracy: 51.6 %
Class: deer  Accuracy: 61.1 %
Class: dog   Accuracy: 64.2 %
Class: frog  Accuracy: 88.0 %
Class: horse Accuracy: 71.9 %
Class: ship  Accuracy: 83.5 %
Class: truck Accuracy: 81.3 %
bash

(4) 画出训练集和验证集的混淆矩阵#

使用 seaborn 绘制混淆矩阵

结果

训练集混淆矩阵

daw

测试集混淆矩阵

daw

(5) 分析网络参数(网络深度、不同的激活函数、神经元数量)对预测结果的影响#

5.1.网络深度对预测结果的影响#

三层网络#

即当前层数的网络:

Net(
  (conv1): Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv2): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv3): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (fc1): Linear(in_features=1024, out_features=120, bias=True)
  (fc2): Linear(in_features=120, out_features=84, bias=True)
  (fc3): Linear(in_features=84, out_features=10, bias=True)
)
bash

五层网络#

在三层网络的基础再增加两层 “Cov → Pool”的网络结构,将其扩展为五层。对于 CIFAR-10 数据集,采用 积极下采样的方式即可,若采用延迟下采样,使得输出维度变为 128 x 4 x 4 = 2048,在小型数据集 CIFAR-10 中容易造成过拟合

新的网络结构:

层级输出形状空间尺寸 (H x W)通道数 (语义深度)
输入[B, 3, 32, 32]大 (1024)低 (3)
块 1[B, 16, 16, 16]
块 2[B, 32, 8, 8]
块 3[B, 64, 4, 4]
块 4[B, 128, 2, 2]
块 5[B, 128, 1, 1]极小 (1)高 (128)

每一个块都是:“Cov => Pool”。

Net(
  (conv1): Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv2): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv3): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv4): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv5): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (fc1): Linear(in_features=128, out_features=120, bias=True)
  (fc2): Linear(in_features=120, out_features=84, bias=True)
  (fc3): Linear(in_features=84, out_features=10, bias=True)
)
bash

训练过程:

Epoch [19/20] | Train Loss: 0.3948 | Train Accuracy: 0.8601
Epoch [20/20] | Train Loss: 0.3675 | Train Accuracy: 0.8706
bash

可以看到训练集准确率高达 0.87,三层网络的训练集准确率为 0.76,低了 0.11。

结果分析#

1. 实验结果

预测结果如下(在测试集上预测):

类别三层网络五层网络
平均准确率71.46%70.75%
plane69.0 %72.2 %
car87.0 %81.9 %
bird57.0 %55.1 %
cat51.6 %55.5 %
deer61.1 %75.0 %
dog64.2 %67.1 %
frog88.0 %78.5 %
horse71.9 %77.1 %
ship83.5 %67.2 %
truck81.3 %77.9 %

3 层网络的最终测试集准确率为 71.46%,略高于 5 层网络的 70.75%;这个结果表明,对于本次实验任务,并非简单地增加网络深度就能带来性能的提升。

2. 原因分析

5 层网络性能反而下降的主要原因可能在于过度下采样导致的关键信息丢失

  • 3 层网络:

    ​ 输入图像的尺寸为 32x32,经过三次池化操作后,进入全连接层之前的特征图尺寸为 4x4。在这个尺度下,特征图依然保留了物体部分特征的相对空间位置信息,例如,“车轮”在“车身”的下方。

  • 5 层网络:

    ​ 输入图像同样为 32x32,但经过了五次池化操作,最终进入全连接层之前的特征图尺寸被压缩至 1x1。这意味着所有空间维度的信息都已丢失,网络只知道图像中“有些特征,但完全丢失了这些特征的排布和结构信息。

5 层网络过于激进的空间压缩,可能破坏了区分不同类别所必需的结构化信息,形成了一个信息瓶颈,从而限制了模型的最终性能。

除了过采样问题,还可能是由于反向传播路径较长出现了 梯度消失的问题,以及 过拟合风险

​ 综合来看,对于 CIFAR-10 数据集(32x32 像素),3 层卷积网络在模型的表达能力、信息保留和训练难度之间取得了更好的平衡。而 5 层网络虽然理论上更强大,但其结构设计中的过度下采样破坏了对分类任务至关重要的空间信息,同时增加了训练的难度和过拟合的风险,最终导致了性能的轻微下降。

5.2. 不同的激活函数对预测结果的影响#

由前文,对于该数据集,五层效果和三层差不多,甚至更差,因此在 5.2. 和 5.3. 中,采用三层的神经网络;并由于 20 轮训练时间较久,观察训练的每一轮的损失值和准确率,第十轮相对而言足够。

train accuracy :0.71504 | train loss:0.798097129379
bash

本节对比三种激活函数:

  • ReLU
  • LeakyReLU
  • Sigmoid

预测结果如下(在测试集上预测):

类别ReLULeakyReLUSigmoid
平均准确率68.43%70.86%49.01%
plane73.0 %73.2 %48.8 %
car77.5 %77.6 %66.7 %
bird49.5 %68.4 %29.3 %
cat45.4 %49.4 %30.8 %
deer59.3 %55.5 %46.9 %
dog69.2 %63.4 %32.1 %
frog84.8 %84.0 %59.8 %
horse74.2 %74.5 %62.4 %
ship77.8 %82.8 %60.7 %
truck73.6 %79.8 %52.6 %

结果分析

整体分析

激活函数整体测试集准确率性能排序
LeakyReLU70.86%最佳
ReLU68.43%良好
Sigmoid49.01%较差

ReLU 和 LeakyReLU 两种激活函数均表现出色,整体准确率分别达到了 68.43%和 70.86%,远超 Sigmoid 函数。这主要得益于它们有效缓解了深度网络中常见的梯度消失问题。当输入信号为正时,ReLU 和 LeakyReLU 的导数恒为 1,这保证了梯度在反向传播过程中可以顺畅地流过激活的神经元,使得网络能够进行有效学习。

从各类别准确率来看,LeakyReLU 在 ReLU 表现较差的类别上提升尤为明显,例如 ‘bird’(从 49.5%提升至 68.4%)和 ‘dog’(从 69.2%降至 63.4%,此处为个例,但总体提升显著),这表明 LeakyReLU 的鲁棒性可能更强,帮助模型学习到了更难区分的特征。

Sigmoid 激活函数的模型表现最差,整体准确率仅为 49.01%,远低于前两者。这清晰地暴露了 Sigmoid 函数在深度网络中的固有缺陷,即梯度饱和与梯度消失问题

5.3. 神经元数量对预测结果的影响#

5.3.1. 改变卷积层神经元数量#

将网络的维度:16 -> 32 -> 64 变为 32->64->128,使神经网络更加地宽,进而使其有能力学习更多的特征。

使用的损失函数为 LeakyReLU

1. 低维16 -> 32 -> 64

2. 高维32->64->128

  • 网络结构

    Net(
      (conv1): Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
      (conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
      (conv3): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (pool3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
      (fc1): Linear(in_features=2048, out_features=120, bias=True)
      (fc2): Linear(in_features=120, out_features=84, bias=True)
      (fc3): Linear(in_features=84, out_features=10, bias=True)
    )
    bash
  • 训练输出

    Epoch [9/10] | Train Loss: 0.4902 | Train Accuracy: 0.8282
    Epoch [10/10] | Train Loss: 0.4309 | Train Accuracy: 0.8495
    bash

    可以看到,加宽卷积层后,训练损失和准确率都达到了很好的状态,性能比低维度时好了许多。

3. 结果分析

类别低维高维
平均准确率70.86%75.41%
plane73.2 %84.4 %
car77.6 %85.8 %
bird68.4 %59.3 %
cat49.4 %56.6 %
deer55.5 %71.5 %
dog63.4 %68.3 %
frog84.0 %81.7 %
horse74.5 %72.8 %
ship82.8 %87.4 %
truck79.8 %86.3 %

将网络加宽(增加卷积层的神经元/通道数)后,模型的平均准确率从 70.86%提升至 75.41%,获得了 4.55%的显著增长

按照类别查看:

  1. 提升显著的类别

    类别准确率的增长率
    plane+11.2%
    car+8.2%
    deer+16.0%
    ship+4.6%
    truck+6.5%

    这些类别通常具有相对固定和明确的结构特征

    例如飞机有机翼,汽车/卡车有轮子和车窗,船有船体。这些物体的轮廓和关键部件在不同样本间的一致性较高。

  2. 提升不明显的类别

    类别准确率的增长率
    bird- 9.1%
    frog- 2.3%
    horse- 1.7%

    bird 是一个典型的类内差异巨大的类别。

    鸟有各种姿态(飞翔、站立)、各种大小(麻雀、鹰)、各种颜色,并且常常与复杂的背景(树林、天空)融为一体。高维网络可能在训练中过拟合了某些特定类型的鸟(比如,训练集中某种背景下的鸟),反而降低了对其他类型鸟的泛化能力。

  3. 稳定提升的类别

    类别准确率的增长率
    cat+ 7.2%
    dog+ 4.9%

    猫和狗是 CIFAR-10 中著名的难题,因为它们类间相似性高,且同样存在姿态、品种等类内差异。

    高维网络带来的性能提升,说明增加的容量确实帮助模型学习到了区分猫狗的更细微的特征,例如耳朵的形状、鼻子的轮廓等。虽然提升了,但它们的绝对准确率仍然不高。

5.3.2. 改变全连接层神经元数量#

全连接层影响网络最后的分类决策部分。

原始全连接层神经元数: 1024 -> 120 -> 84 -> 10,现修改为更大的全连接层神经元数:1024 -> 256 -> 128 -> 10

1. 原始网络

Net(
  (conv1): Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv2): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv3): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (fc1): Linear(in_features=1024, out_features=120, bias=True)
  (fc2): Linear(in_features=120, out_features=84, bias=True)
  (fc3): Linear(in_features=84, out_features=10, bias=True)
)
python

2. 更大的全连接层网络

Net(
  (conv1): Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv2): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv3): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (fc1): Linear(in_features=1024, out_features=256, bias=True)
  (fc2): Linear(in_features=256, out_features=128, bias=True)
  (fc3): Linear(in_features=128, out_features=10, bias=True)
)
bash

训练过程输出:

Epoch [9/10] | Train Loss: 0.6513 | Train Accuracy: 0.7714
Epoch [10/10] | Train Loss: 0.5971 | Train Accuracy: 0.7897
bash

3. 结果分析

类别原始网络宽全连接层
平均准确率70.86%72.77%
plane73.2 %77.0 %
car77.6 %88.0 %
bird68.4 %58.2 %
cat49.4 %49.1 %
deer55.5 %64.3 %
dog63.4 %60.6 %
frog84.0 %78.5 %
horse74.5 %85.3 %
ship82.8 %85.6 %
truck79.8 %81.1 %

从实验结果的核心指标来看,宽连接层网络取得了更好的整体性能

  • 原始网络平均准确率: 70.86%
  • 宽连接层网络平均准确率: 72.77%

这带来了 1.91% 的净准确率提升,是一个较小的改进。

原因分析

全连接层负责将卷积层提取出的高级特征(如物体的部件、纹理等)进行组合,并最终映射到具体的类别上。通过将全连接层的神经元数量从 (120, 84) 增加到 (256, 128),模型的容量显著的增加了。

结论

  1. 增加全连接层宽度是有效的: 从总体结果看,增加分类头的容量成功提升了模型的平均性能,证明原始网络的分类部分确实存在一定的表达能力瓶颈。
  2. 存在明显的权衡: 模型的总体精力是有限的。更宽的网络将更多的注意力和参数用于学习区分那些特征明显的类别,这在一定程度上牺牲了对那些形态多变、难以定义的类别的泛化能力。
  3. 参数量与过拟合风险: 宽连接层网络拥有更多的参数,这意味着它需要更多的计算资源进行训练,并且有过拟合的风险。虽然在本次实验中整体效果是正向的,但如果进一步增加宽度或在更小的数据集上训练,性能很可能会因严重的过拟合而下降。

(6) 在损失函数为交叉熵的情况下,对比网络最后一层是否使用 softmax 的性能差异并分析其产生的原因#

1.原始网络

原始网络结构为:

Net(
  (conv1): Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv2): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv3): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (fc1): Linear(in_features=1024, out_features=120, bias=True)
  (fc2): Linear(in_features=120, out_features=84, bias=True)
  (fc3): Linear(in_features=84, out_features=10, bias=True)
)
bash

查看末尾便可发现原始网络是直接输出 logits 的,因为PyTorch 的交叉熵损失函数自带了 Softmax,此时在网络末尾再添加 Softmax 会导致计算出的梯度值太过微弱,模型根本无法学习。

训练的损失、准确率曲线

adw

2.最后一层添加了 softmax 的网络

前向传播顺序

def forward(self, x):
    # 顺序通过卷积块
    x = self.pool1(F.leaky_relu(self.conv1(x)))
    x = self.pool2(F.leaky_relu(self.conv2(x)))
    x = self.pool3(F.leaky_relu(self.conv3(x)))
    x = torch.flatten(x, 1)
    # 全连接层
    x = F.leaky_relu(self.fc1(x))
    x = F.leaky_relu(self.fc2(x))
    x = self.fc3(x)
    x = F.softmax(x, dim=1) # ! 末尾添加softmax
    return x
python

训练的损失、准确率曲线

12

3. 结果分析

预测结果

类别最后一层不加 softmax最后一层加 softmax
平均准确率71.84%64.30%
plane69.0 %71.4 %
car82.2 %81.6 %
bird54.3 %58.5 %
cat43.5 %53.5 %
deer65.9 %56.8 %
dog62.8 %37.8 %
frog88.7 %75.0 %
horse79.1 %71.2 %
ship87.8 %84.2 %
truck85.1 %53.0 %

实验结果清晰地表明,不使用 Softmax 激活函数(直接输出 logits)的网络在各项性能指标上均显著优于使用 Softmax 的网络

  1. 训练过程分析(根据 loss 曲线和 accuracy 曲线):

    1. 损失值曲线

      • 初始损失与收敛速度:

        A. 不加 Softmax (正确模型): 训练从一个相对较低的损失值 (≈1.71) 开始,并且在前几个 epoch 中迅速下降,表现出高效的学习效率。

        B. 加 Softmax (错误模型): 训练从一个非常高的损失值 (≈2.13) 开始,且下降速度极为缓慢。这表明模型在初始阶段就接收到了非常微弱或不正确的梯度信号,导致学习困难。

      • 最终损失:

        A. 不加 Softmax: 经过 10 个 epoch,损失值稳定下降到了约0.66,说明模型较好地拟合了训练数据。

        B. 加 Softmax: 10 个 epoch 后,损失值仍然高达约1.78,几乎没有得到有效的优化。

    2. 训练集准确率曲线

      • 不加 Softmax: 训练准确率从 37%开始,稳步且快速地提升,最终达到了接近80% (0.793) 的高水平。
      • 加 Softmax: 训练准确率从约 32%开始,提升速度缓慢,最终仅达到68.6%,远低于正确模型的水平。这证明了无效的损失函数导致模型无法充分学习训练集中的特征。

    综合训练曲线来看,在最后一层添加 Softmax 函数的模型表现出明显的学习障碍,其损失函数很难被优化,从而导致准确率提升乏力。

  2. 测试集性能分析

    训练过程中的差异最终体现在模型对未知数据的泛化能力上。

    • 平均准确率:
      • 不加 Softmax 的正确模型在测试集上达到了**71.84%**的平均准确率。
      • 加 Softmax 的错误模型的平均准确率仅为64.30%
      • 两者相差超过7.5%,这是一个巨大的性能鸿沟,决定性地证明了正确做法的优越性。
    • 分项准确率:
      • 优势类别:正确模型在大多数类别上都取得了更好的性能,尤其是在 dog (62.8% vs 37.8%)、truck (85.1% vs 53.0%)和 frog (88.7% vs 75.0%) 这几个类别上,领先优势非常明显。
      • 异常类别:有趣的是,错误模型在 bird 和 cat 等少数几个类别上的准确率略高于正确模型。这可能是由于训练过程中的随机性,或是被扭曲的梯度恰好在某些批次的数据上对这几个类别的特征产生了微弱的有利影响。但这并不能改变其整体性能远逊于正确模型的事实。
  3. 原因分析

    是 torch.nn.CrossEntropyLoss 函数的核心工作原理:PyTorch 内置的交叉熵损失函数会对收到的 logits 进行一次 LogSoftmax,将其转换为对数概率;然后使用 NLLLoss 计算最终的损失值。

    因此,交叉熵损失函数本身就使用了 Softmax,如果再重复使用 Softmax,会导致参数数值再度被压缩,进而使梯度信号接近于 0,即梯度消失

源码(可忽略)#

深度学习实验二
Author Juyao Huang
Published at November 17, 2025
Comment seems to stuck. Try to refresh?✨