基于BP与CNN的图像分类模型构建、超参数优化及性能对比研究

发布于:2025-08-10 ⋅ 阅读:(31) ⋅ 点赞:(0)

一、实验目的

实验目标

构建基于神经网络模型的数据分析与模式识别框架,探明神经网络在大数据分析中的意义。

实验任务

构建基于深度 BP 神经网络与卷积神经网络的数据分析与模式识别框架,将数据集 MNISTCIFAR-10 分别在两种模型中训练,并比较测试效果。

使用数据集

  • MNIST 数据集

  • CIFAR-10 数据集

二、实验原理

阶段一分析:

分析问题需求,明确分类任务目标

在本阶段,我们的目标是构建一个图像分类系统,能够将输入图像准确分类到对应的类别。首先,搭建数据读取与预处理模块,需要调用MNIST和CIFAR-10两个标准图像数据集作为实验数据。然后,搭建数据与模型接口,为以后将这两个数据集放入模型中做好准备。接下来,实现模型评估模块,按照老师所讲的,计算分类评估指标错误率与精确度,最后在可视化中搭建roc曲线。

阶段二分析(BP神经网络):

BP (Back Propagation)神经网络也是前馈神经网络,只是它的参数权重值是由反向传播学习算法进行调整的

BP 神经网络模型拓扑结构包括输入层、隐层和输出层,利用激活函数来实现从输入到输出的任意非线性映射,从而模拟各层神经元之间的交互

基本步骤:初始化网络权值和神经元的阈值,一般通过随机的方式进行初始化前向传播: 计算隐层神经元和输出层神经元的输出后向传播: 根据目标函数公式修正权值。

BP 神经网络的核心思想是由后层误差推导前层误差,一层一层的反传,最终获得各层的误差估计,从而得到参数的权重值。由于权值参数的运算量过大,一般采用梯度下降法来实现

输入层是神经网络的起点,其作用是将外部数据输入模型。在图像分类任务中,图像需要先被展平为一维向量(如 MNIST 的 28x28 图像被展平为 784 维向量),并作为输入层的节点传入网络。输入层本身不做任何计算,只负责数据的传递。

神经网络隐藏层

隐藏层是网络中最核心的部分,用于提取特征与学习数据之间的非线性关系。每个隐藏层由多个神经元(节点)构成,节点之间通过权重连接。每个神经元会对其输入做一次线性加权求和,再通过激活函数进行非线性变换(如 ReLU、Sigmoid、Tanh 等),提高模型的拟合与表达能力。多个隐藏层串联构成了“深度”网络。

神经网络输出层

输出层的作用是将模型内部的高维特征最终映射为分类结果。对于多分类任务(如本实验中的 MNIST 和 CIFAR-10,均为 10 类),输出层一般设置为一个Linear全连接层,输出维度为类别数(10),并通过Softmax 函数 转换为概率形式,用于分类决策。

阶段三分析(CNN神经网络)

卷积神经网络是人工神经网络的一种,由对猫的视觉皮层的研究发展而来,视觉皮层细胞对视觉子空间更敏感,通过子空间的平铺扫描实现对整个视觉空间的感知。

卷积神经网络目前是深度学习领域的热点,尤其是图像识别和模式分类方面,优势在于具有共享权值的网络结构和局部感知(也称为稀疏连接)的特点,能够降低神经网络的运算复杂度。

卷积层和子采样层是特征提取功能的核心模块,卷积神经网络的低层由卷积层和子采样层交替组成,在保持特征不变的情况下减少维度空间和计算时间,更高层次是全连接层,输入是由卷积层和子采样层提取到的特征,最后一层是输出层,可以是一个分类器,采用逻辑回归、Softmax回归、支持向量机等进行模式分类,也可以直接输出某一结果。

卷积层

通过卷积层的运算,可以将输入信号在某一特征上加强,从而实现特征的提取,也可以排除干扰因素,从而降低特征的噪声。卷积操作是指将一个可移动的小窗口(称为数据窗口)与图像进行逐元素相乘然后相加的操作。这个小窗口其实是一组固定的权重,它可以被看作是一个特定的滤波器(filter)或卷积核。

池化层

池化层是一种向下采样的形式,在神经网络中也称之为子采样层。一般使用最大池化将特征区域中的最大值作为新的抽象区域的值,减少数据的空间大小。参数数量和运算量也会减少,减少全连接的数量和复杂度,一定程度上可以避免过拟合。池化的结果是特征减少、参数减少。

全连接层

卷积层得到的每张特征图表示输入信号的一种特征,而它的层数越高表示这一特征越抽象,为了综合低层的每个卷积层特征,用全连接层将这些特征结合到一起,然后用Softmax进行分类或逻辑回归分析。

三、实验代码

3.1 构建数据分析与模式识别框架(第四周)

搭建数据读取与预处理模块(支持 MNIST / CIFAR-10)

数据读取与预处理部分主要功能是根据用户选择加载 MNIST 或 CIFAR-10 数据集。

主要思路:使用torchvision.datasets 提供的接口自动下载并加载数据,同时通过 transforms 对图像进行预处理,包括将图像转换为张量 (ToTensor) 并进行标准化(使像素值服从指定均值和标准差的分布),从而提升模型训练的效果与稳定性。

最终返回处理后的训练集,并输出图像数量和尺寸信息,便于后续模型训练使用。

ps:使用 K-Fold 方法分解数据集(k=5)放在了另一模块,分解后会直接训练

def load_dataset(use_mnist=True):
    if use_mnist:
        transform = transforms.Compose([
            transforms.ToTensor(),
            transforms.Normalize((0.1307,), (0.3081,))
        ])
        dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
    else:
        transform = transforms.Compose([
            transforms.ToTensor(),
            transforms.Normalize((0.5,), (0.5,))
        ])
        dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
    print(f"加载数据集完成:{len(dataset)}张图像,尺寸为 {dataset[0][0].shape}")
    return dataset
搭建数据与模型接口模块

这一模块作为数据与模型的接口,核心目的是根据用户选择动态构建不同类型的神经网络模型。

函数 get_model 接收模型类型(bpcnn)和输入数据的形状等参数:当选择 bp 时,将图像展平成一维向量传入多层感知机 DeepBPNet;当选择 cnn 时,保留图像的通道信息并构建卷积神经网络 CustomCNN

同时通过 **kwargs 支持对网络结构参数(如初始化方式、层数等)进行灵活配置。该接口实现了模型结构的统一调用,便于后续训练与评估过程的模块化管理。

# ========== 数据与模型接口 ==========
def get_model(model_type='bp', input_shape=(1, 28, 28), num_classes=10, **kwargs):
    if model_type == 'bp':
        input_size = np.prod(input_shape)
        return DeepBPNet(input_size=input_size, num_classes=num_classes, **kwargs)
    elif model_type == 'cnn':
        return CustomCNN(in_channels=input_shape[0], num_classes=num_classes, **kwargs)
    else:
        raise ValueError("模型必须是'bp'或者'cnn'")
搭建模型评估模块

这一模块是整个实验的核心部分——模型评估模块。

其主要功能是在训练过程中使用 K-Fold 交叉验证 方法(本实验设定 k=5),将原始数据划分为训练集和验证集。

在每一折中,首先利用用户指定的模型类型(CNN 或 BP)通过 get_model 动态构建模型,并进行若干轮次的训练。

接着,在验证集中进行推理,计算预测结果与真实标签的准确率(Accuracy)与对数损失(Log Loss)。每折的结果都会记录并输出,

最终返回所有折次的评估指标和分类概率,为模型表现对比与后续可视化分析提供基础。

# ========== 模型评估模块 ==========
def evaluate_model_kfold(dataset, model_type='cnn', k_folds=5, batch_size=64, num_classes=10, device=None, epochs=1, **kwargs):
    if device is None:
        device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
​
    indices = list(range(len(dataset)))
    kf = KFold(n_splits=k_folds, shuffle=True, random_state=42)
​
    all_fold_metrics = []
    all_probs = []
    all_targets = []
​
    for fold, (train_idx, val_idx) in enumerate(kf.split(indices)):
        print(f"\n 训练轮数 {fold + 1}/{k_folds}")
​
        train_subset = Subset(dataset, train_idx)
        val_subset = Subset(dataset, val_idx)
        train_loader = DataLoader(train_subset, batch_size=batch_size, shuffle=True)
        val_loader = DataLoader(val_subset, batch_size=batch_size, shuffle=False)
​
        sample_input, _ = next(iter(train_loader))
        model = get_model(
            model_type=model_type,
            input_shape=sample_input.shape[1:],
            num_classes=num_classes,
            **kwargs
        ).to(device)
​
        criterion = nn.CrossEntropyLoss()
        optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
​
        # === 训练阶段 ===
        model.train()
        for epoch in range(epochs):
            for inputs, labels in train_loader:
                inputs, labels = inputs.to(device), labels.to(device)
                optimizer.zero_grad()
                outputs = model(inputs)
                loss = criterion(outputs, labels)
                loss.backward()
                optimizer.step()
​
        # === 验证阶段 ===
        model.eval()
        y_true, y_pred, y_prob = [], [], []
​
        with torch.no_grad():
            for inputs, labels in val_loader:
                inputs, labels = inputs.to(device), labels.to(device)
                outputs = model(inputs)
                probs = F.softmax(outputs, dim=1)
                preds = torch.argmax(probs, dim=1)
​
                y_true.extend(labels.cpu().numpy())
                y_pred.extend(preds.cpu().numpy())
                y_prob.extend(probs.cpu().numpy())
​
        acc = accuracy_score(y_true, y_pred)
        loss = log_loss(y_true, y_prob, labels=list(range(num_classes)))
​
        all_fold_metrics.append({'fold': fold + 1, 'accuracy': acc, 'loss': loss})
        all_probs.extend(y_prob)
        all_targets.extend(y_true)
​
        print(f" Fold {fold + 1} Accuracy: {acc:.4f}, Loss: {loss:.4f}")
​
    return all_fold_metrics, np.array(all_probs), np.array(all_targets)
​
搭建模型评估可视化模块

这一模块是模型评估可视化部分,主要功能是绘制多分类任务中的 ROC 曲线,帮助我们直观判断模型对每一类别的区分能力。

首先通过 label_binarize 对目标标签进行 One-Hot 编码,然后计算每个类别的真正率(TPR)与假正率(FPR),并进一步求得每类的 AUC(曲线下面积)作为性能指标。

最终利用 Matplotlib 对每个类别的 ROC 曲线进行绘图,并可选择保存或直接展示。该模块能有效展示模型对不同类别的分类效果,提供辅助判断和模型优化依据。

def plot_multiclass_roc(probs, targets, num_classes, save_path=None):
    targets_onehot = label_binarize(targets, classes=list(range(num_classes)))
​
    fpr = dict()
    tpr = dict()
    roc_auc = dict()
​
    for i in range(num_classes):
        fpr[i], tpr[i], _ = roc_curve(targets_onehot[:, i], probs[:, i])
        roc_auc[i] = auc(fpr[i], tpr[i])
​
    plt.figure(figsize=(10, 8))
    for i in range(num_classes):
        plt.plot(fpr[i], tpr[i], label=f"Class {i} (AUC = {roc_auc[i]:.2f})")
​
    plt.plot([0, 1], [0, 1], 'k--', label='Random')
    plt.xlim([0.0, 1.0])
    plt.ylim([0.95, 1.05])
    plt.xlabel('假警报率')
    plt.ylabel('识别率')
    plt.title('多分类模型的ROC曲线')
    plt.rcParams['font.sans-serif'] = ['SimHei']
    plt.legend(loc='lower right')
​
    if save_path:
        plt.savefig(save_path)
        print(f" ROC曲线已保存到 {save_path}")
    else:
        plt.show()

3.2 构建深层神经网络模型(第六周)

构建 10 层 BP 神经网络模型

该BP网络结构包括:

  • 输入层:将图像展平为一维向量,输入维度默认为 784,对应于 28×28 的灰度图(如 MNIST)。

  • 隐藏层:通过参数 num_layers=10 指定总层数,其中 hidden_size=128 表示每层的神经元数量,采用了 8 个中间隐藏层(10 层结构 = 输入层 + 8 个隐藏层 + 输出层),并使用 ReLU 激活函数进行非线性变换。

  • 输出层:将最后一层的输出映射到 num_classes=10,用于10类分类任务。

class DeepBPNet(nn.Module):
    def __init__(self, input_size=784, hidden_size=128, num_classes=10, num_layers=10, init_method='xavier'):
        super(DeepBPNet, self).__init__()
        assert num_layers >= 2, "网络层数必须 >= 2"
        self.input_layer = nn.Linear(input_size, hidden_size)
        self.hidden_layers = nn.ModuleList([
            nn.Linear(hidden_size, hidden_size) for _ in range(num_layers - 2)
        ])
        self.output_layer = nn.Linear(hidden_size, num_classes)
        self.init_weights(method=init_method)
​
    def forward(self, x):
        x = x.view(x.size(0), -1)
        x = F.relu(self.input_layer(x))
        for layer in self.hidden_layers:
            x = F.relu(layer(x))
        x = self.output_layer(x)
        return x

3.3 构建卷积神经网络模型(第七周)

构建 3 层 CNN 神经网络模型

本模块主要通过构建一个可配置的卷积神经网络(CustomCNN 类)实现图像分类任务,其核心思路是:利用多层卷积层、激活函数和池化操作提取图像特征,随后通过全连接层完成分类预测。

该 CNN 网络结构包括以下部分:

  • 输入层:接收尺寸为 28×28 的灰度图像,输入通道默认为 1(适用于 MNIST),也支持自定义为彩色图像的 3 通道(如 CIFAR-10)。

  • 卷积模块:通过参数 conv_layers=3 控制卷积层数,每层包含一个 3×3 的卷积核(padding=1 保持尺寸)、激活函数(如 ReLUTanh 等)以及 2×2 的最大池化操作(MaxPool2d),用于特征提取与降维。每一层的通道数由 base_channels=162^i 级数增长,即为 16 → 32 → 64

  • 全连接层:卷积模块输出展平后,通过两个线性全连接层处理。第一层映射到 128 个神经元,第二层输出 num_classes=10,用于多分类任务。

  • 激活函数与初始化:激活函数可选(如 ReLUSigmoid 等),通过参数 activation 控制;所有卷积层与线性层的权重初始化方式也可配置(如 xavierkaiming 等),增强模型灵活性与实验可控性。

class CustomCNN(nn.Module):
    def __init__(self, in_channels=1, num_classes=10, conv_layers=3, base_channels=16, activation='relu', init_method='xavier'):
        super(CustomCNN, self).__init__()
        assert conv_layers >= 1, "至少要有一个卷积层"
        self.activation_name = activation
        self.activation_fn = self._get_activation_fn(activation)
        self.conv_blocks = nn.ModuleList()
        channels = in_channels
        for i in range(conv_layers):
            out_channels = base_channels * (2 ** i)
            self.conv_blocks.append(nn.Sequential(
                nn.Conv2d(channels, out_channels, kernel_size=3, padding=1),
                self.activation_fn,
                nn.MaxPool2d(2, 2)
            ))
            channels = out_channels
        dummy_input = torch.zeros(1, in_channels, 28, 28)
        with torch.no_grad():
            for layer in self.conv_blocks:
                dummy_input = layer(dummy_input)
        flatten_dim = dummy_input.view(1, -1).shape[1]
        self.fc1 = nn.Linear(flatten_dim, 128)
        self.fc2 = nn.Linear(128, num_classes)
        self.init_weights(init_method)
​
    def forward(self, x):
        for layer in self.conv_blocks:
            x = layer(x)
        x = x.view(x.size(0), -1)
        x = self.activation_fn(self.fc1(x))
        x = self.fc2(x)
        return x

四、实验设计

4.1 数据集及数据集划分方式

MNIST 数据集:包含 70,000 张 28×28 像素的灰度手写数字图片,共 10 个类别(0~9)。本实验使用其中的 训练集部分(60,000 张)作为训练与验证数据。

CIFAR-10 数据集:包含 60,000 张 32×32 像素的彩色图像,分为 10 个类别,如飞机、汽车、猫、狗等。实验中使用其中的训练集部分(50,000 张)进行模型训练与评估。

为了更稳定且全面地评估模型性能,本实验采用了K 折交叉验证法(K-Fold Cross Validation)。我们将训练集划分为 5 份(k=5),每次选取其中一份作为验证集,其余部分用于训练,重复 5 次后计算每折的准确率与损失,并求取平均值,减小随机性影响,使结果更具参考价值。

4.2 实验选用的超参数

BP选用的超参数:
  • 初始化方式:Xavier / Kaiming / Normal / Uniform

  • 神经元个数:64、128、256、512

  • 网络层数:3、5、10、20

  • 激活函数:ReLU / LeakyReLU / Sigmoid / Tanh

CNN选用的超参数:
  • 初始化方式:Xavier / Kaiming / Normal / Uniform

  • 卷积核个数:8、16、32、64

  • 卷积层数:1、2、3、4

  • 激活函数:ReLU / LeakyReLU / Sigmoid / Tanh

五、实验结果展示与分析

5.1 对比图表

BPNet vs CNN 性能对比(进行完优化的对比图表)
模型类型 数据集 最佳准确率 最低损失 最优配置简述
BPNet MNIST 0.9575 0.1391 Xavier 128 3 ReLU
BPNet CIFAR-10 0.4389 1.5870 Xavier 128 3 ReLU
CNN MNIST 0.9853 0.0474 Xavier 64 3 LeakyReLU
CNN CIFAR-10 0.5647 1.2114 Kaiming 64 3 tanh

5.2 改变模型超参数

BP更换不同模块参数以探明作用:
  • 初始化方式:Xavier / Kaiming / Normal / Uniform

  • 神经元个数:64、128、256、512

  • 网络层数:3、5、10、20

  • 激活函数:ReLU / LeakyReLU / Sigmoid / Tanh

MINST

为了探究网络结构各项参数对模型性能的影响,我们在MNIST数据集上通过更改初始化方式神经元个数网络层数激活函数,对BP神经网络模型进行了系统性对比实验。

初始化方式对比表
初始化方式 神经元个数 网络层数 激活函数 Accuracy Loss
Xavier 128 10 ReLU 0.9464 0.1807
Kaiming 128 10 ReLU 0.9444 0.1896
Normal 128 10 ReLU 0.7369 0.7285
Uniform 128 10 ReLU 0.9358 0.2252

最优选择:Xavier 初始化

表明对于BP神经网络而言,权重初始化方式对训练过程收敛速度和最终性能具有显著影响。Xavier初始化能够保持每层输出的方差一致,避免梯度消失或爆炸,从而提升训练稳定性。Normal初始化可能导致初始权重偏离过大,造成梯度传播困难。

神经元个数对比表
初始化方式 神经元个数 网络层数 激活函数 Accuracy Loss
Xavier 64 10 ReLU 0.9389 0.2074
Xavier 128 10 ReLU 0.9464 0.1807
Xavier 256 10 ReLU 0.9497 0.1939
Xavier 512 10 ReLU 0.9421 0.2356

最佳神经元数量:256

实验发现,当神经元个数为 256 时,模型准确率达到最高(94.97%)。继续增大至512时,反而略有下降,说明神经元过多可能导致参数冗余,从而出现过拟合或训练不稳定。

网络层数对比表
初始化方式 神经元个数 网络层数 激活函数 Accuracy Loss
Xavier 128 3 ReLU 0.9575 0.1391
Xavier 128 5 ReLU 0.9528 0.1506
Xavier 128 10 ReLU 0.9464 0.1807
Xavier 128 20 ReLU 0.9013 0.3571

最佳网络层数:3层

实验比较了 3、5、10、20 层的BP神经网络,发现3层网络的性能反而最好,准确率为 95.75%,而层数越多,效果反而逐渐下降,尤其在20层时准确率骤降至 90.13%,且Loss显著增大。

这说明对于结构简单的MNIST图像识别任务而言,过深的网络不仅不能提升表现,反而可能因为梯度消失或过拟合而降低性能。合理控制网络深度有助于模型的高效训练。

激活函数对比表
初始化方式 神经元个数 网络层数 激活函数 Accuracy Loss
Xavier 128 10 ReLU 0.9464 0.1807
Xavier 128 10 LeakyReLU 0.9472 0.1801
Xavier 128 10 Sigmoid 0.3830 1.4522
Xavier 128 10 Tanh 0.9423 0.1984

最佳激活函数:LeakyReLU

在激活函数对比中,LeakyReLU略优于ReLU,表现为更低的Loss与更高的Accuracy。Tanh次之,而Sigmoid表现最差,准确率仅为38.30%,几乎无法完成任务。

CIFAR-10
初始化方式对比表
初始化方式 神经元个数 网络层数 激活函数 Accuracy Loss
Xavier 128 10 ReLU 0.4099 1.6549
Kaiming 128 10 ReLU 0.3876 1.7094
Normal 128 10 ReLU 0.1703 2.0517
Uniform 128 10 ReLU 0.3209 1.7992

最优选择:Xavier 初始化

Xavier 初始化适用于对称激活函数(如 ReLU、Tanh),其在浅层到中等深度的网络结构中表现稳定。相比之下,Normal 初始化不具备前向信号的控制能力,容易导致梯度消失或爆炸,效果最差。

神经元个数对比表
初始化方式 神经元个数 网络层数 激活函数 Accuracy Loss
Xavier 64 10 ReLU 0.4008 1.6731
Xavier 128 10 ReLU 0.4099 1.6549
Xavier 256 10 ReLU 0.4086 1.656
Xavier 512 10 ReLU 0.3893 1.7147

最佳神经元数量:128

进一步增加神经元并未带来显著提升,反而可能引起过拟合。

网络层数对比表
初始化方式 神经元个数 网络层数 激活函数 Accuracy Loss
Xavier 128 3 ReLU 0.4389 1.5870
Xavier 128 5 ReLU 0.4357 1.5930
Xavier 128 10 ReLU 0.4099 1.6549
Xavier 128 20 ReLU 0.2254 1.9535

最佳网络层数:3层

深层全连接网络在缺乏卷积提取能力的前提下,面对复杂图像如 CIFAR-10 会迅速增加训练难度,出现训练不稳定、梯度消失等问题。而浅层结构能更快地捕捉全局特征,反而带来更优表现。

激活函数对比表
初始化方式 神经元个数 网络层数 激活函数 Accuracy Loss
Xavier 128 10 ReLU 0.4099 1.6549
Xavier 128 10 LeakyReLU 0.4130 1.6456
Xavier 128 10 Sigmoid 0.1588 2.0878
Xavier 128 10 Tanh 0.3757 1.7548

最佳激活函数:LeakyReLU

LeakyReLU 在负区间仍保留微弱梯度,避免了神经元“失活”;但实际使用时,依然使用了ReLU测试数据。

CNN更换不同模块参数以探明作用:
  • 初始化方式:Xavier / Kaiming / Normal / Uniform

  • 卷积核个数:8、16、32、64

  • 卷积层数:1、2、3、4

  • 激活函数:ReLU / LeakyReLU / Sigmoid / Tanh

MNIST
初始化方式对比表
初始化方式 卷积核个数 卷积层数 激活函数 Accuracy Loss
Xavier 16 3 ReLU 0.9799 0.0637
Kaiming 16 3 ReLU 0.9776 0.0742
Normal 16 3 ReLU 0.9658 0.1085
Uniform 16 3 ReLU 0.9780 0.0713

Xavier > Uniform > Kaiming > Normal

最佳初始化方式:Xavier

从准确率与损失函数表现来看,Xavier 初始化以 0.9799 的 Accuracy 和 0.0637 的 Loss 表现最佳,说明其在权重初始化时有更好的稳定性与收敛效果。

卷积核个数对比表
初始化方式 卷积核个数 卷积层数 激活函数 Accuracy Loss
Xavier 8 3 ReLU 0.9799 0.0637
Xavier 16 3 ReLU 0.9728 0.0871
Xavier 32 3 ReLU 0.9803 0.0621
Xavier 64 3 ReLU 0.9842 0.0495

64 > 32 > 8 ≈ 16

最佳卷积核个数:64

卷积核个数从 8 到 64 呈现出较强的正向趋势,64 个卷积核时准确率最高,达到了 0.9842,且 Loss 最低,仅为 0.0495,说明在该任务中更丰富的卷积特征表达有助于提升分类性能。

卷积层数对比表
初始化方式 卷积核个数 卷积层数 激活函数 Accuracy Loss
Xavier 16 1 ReLU 0.9729 0.0905
Xavier 16 2 ReLU 0.9768 0.754
Xavier 16 3 ReLU 0.9799 0.0637
Xavier 16 4 ReLU 0.9752 0.0810

3 层 > 2 层 > 4 层 > 1 层

最佳卷积层数:3 层

卷积层数增加到 3 层时模型性能最优,再往上反而性能下降。这表明在 MNIST 这种简单数据集上,过深的网络并不一定带来提升,反而可能导致特征过拟合或训练困难。

激活函数对比表
初始化方式 卷积核个数 卷积层数 激活函数 Accuracy Loss
Xavier 16 3 ReLU 0.9799 0.0637
Xavier 16 3 LeakyReLU 0.9820 0.0580
Xavier 16 3 Sigmoid 0.9287 0.2467
Xavier 16 3 Tanh 0.9797 0.0649

LeakyReLU > ReLU ≈ Tanh > Sigmoid

最佳激活函数:LeakyReLU

在所有激活函数中,LeakyReLU 以 0.9820 的 Accuracy 和 0.0580 的 Loss 表现最优,略优于 ReLU,说明其在处理 ReLU 的“神经元死亡问题”方面更具优势。Sigmoid 明显不适合 CNN,收敛慢且梯度消失,效果最差。

CIFAR-10
初始化方式对比表
初始化方式 卷积核个数 卷积层数 激活函数 Accuracy Loss
Xavier 16 3 ReLU 0.5685 1.2097
Kaiming 16 3 ReLU 0.5762 1.2068
Normal 16 3 ReLU 0.4612 1.4741
Uniform 16 3 ReLU 0.5463 1.2650

Kaiming>Xavier > Uniform > Normal

推荐初始化方式:Kaiming

卷积核个数对比表
初始化方式 卷积核个数 卷积层数 激活函数 Accuracy Loss
Xavier 8 3 ReLU 0.5083 1.3704
Xavier 16 3 ReLU 0.5685 1.2097
Xavier 32 3 ReLU 0.6110 1.0978
Xavier 64 3 ReLU 0.6407 1.0130

64>32>16>8

最佳卷积核个数:64

卷积层数对比表
初始化方式 卷积核个数 卷积层数 激活函数 Accuracy Loss
Xavier 16 1 ReLU 0.5556 1.2541
Xavier 16 2 ReLU 0.5630 1.2301
Xavier 16 3 ReLU 0.5685 1.2097
Xavier 16 4 ReLU 0.5400 1.241

3 层 > 2 层 > 1层 > 4 层

最佳卷积层数:3 层

依然是在底层到高层数表现为先增长,后减少,存在最优层数,避免过拟合的同时,又存在较好结果。

激活函数对比表
初始化方式 卷积核个数 卷积层数 激活函数 Accuracy Loss
Xavier 16 3 ReLU 0.5685 1.2097
Xavier 16 3 LeakyReLU 0.5844 1.1684
Xavier 16 3 Sigmoid 0.3149 1.1278
Xavier 16 3 Tanh 0.6046 1.8902

Tanh>LeakyReLU > ReLU> Sigmoid

最佳激活函数:Tanh

Tanh中心对称的性质可能使得数据分布更加居中,有助于缓解梯度消失问题,从而在这个更大数据集,可能相对优势较大。

5.3 结果分析

在前面的参数对比实验中,分别找出了初始化方式、激活函数、神经元个数和网络层数的“单项最优配置”,期望它们的组合能够带来“最优整体性能”。然而,组合结果却略低于部分单项测试下的最佳结果(如3层网络+ReLU时Accuracy达到了 0.9575,而最终组合结果为 0.9557),这说明最佳参数并非简单叠加得出。

原因分析:

参数间存在依赖与耦合:多个参数组合在一起时,其交互效应可能会导致性能打折扣。

浅层网络限制了其他参数的潜力:3层网络作为最优层数,这是在保持训练简单的基础上表现最好的结构,但它可能无法充分发挥复杂初始化(如Xavier)或激活函数(如LeakyReLU)带来的优势。

超参数之间存在“过拟合风险叠加”:虽然每个参数单独设置下不会引发过拟合,但组合在一起时,可能出现过度表达能力,导致在验证集上性能下降,尤其是Loss值升高。

因此最终仍然选择Xavier 128 3 ReLU作为MNIST数据集最优配置。

同样,在实验中,依然尝试将所有在单项实验中表现最优的超参数组合起来,形成“综合最优配置”,但实际上依然不如之前的测试结果,因此最终仍然选择Xavier 128 3 ReLU作为MNIST数据集最优配置。

本次结果发现,综合采用各参数单项调优中表现最优的配置,即 Xavier 初始化、LeakyReLU 激活函数、3 层卷积结构以及64个基础卷积核,最终模型在 MNIST 数据集上取得了 0.9853 的平均准确率和 0.0474 的平均损失。但实际并非是组合起来的提升,因为我们看到这与所谓的”单项冠军“(Xavier 初始化、ReLU 激活函数、3 层卷积结构以及64个基础卷积核)只相差了激活函数,因此这项结论本质上还是单项的胜利,而非结合的结果。

在本次CNN模型训练CIFAR-10数据集中,成功取得综合最优的体现,通过枚举的方法,最终采用 Kaiming 初始化、Tanh 激活函数、3 层卷积结构以及卷积核基数为 64 的 CNN 模型在 CIFAR-10 数据集上取得了最佳性能,平均准确率达到 0.5647,损失值为 1.2114,相较于其他配置均有明显优势。

后经查询分析,得到原因可能如下:这一组合能够充分利用 Kaiming 初始化在深度网络中对梯度稳定的优化效果,同时 Tanh 激活函数在 CIFAR-10 这类自然图像数据上展现出更强的非线性表达能力和稳定性,配合较深且足够宽的网络结构,有助于提取更丰富的图像特征,从而在分类任务中表现更优。

六、实验结论

在本次实验中,我们系统性地掌握了深度学习模型的编程实现过程,包括如何使用 PyTorch 框架加载标准图像数据集(MNIST 与 CIFAR-10)、构建可配置的神经网络模型(BP 与 CNN)、执行 K 折交叉验证训练模型,以及如何灵活调整网络结构参数以优化模型性能。

通过本次实验,也加深了对超参数在模型训练中的影响的理解,我们观察到模型表现受初始化方式、激活函数、网络层数和神经元/卷积核数量等多个因素的影响,并非所有参数的“最优”组合都能带来“最优”的整体结果,说明它们之间存在复杂的相互作用和依赖关系。尤其是在浅层网络中,部分复杂配置的优势无法完全体现,甚至可能因参数冗余导致性能下降。

在未来的研究中,测试更多的可能数据,尝试更多的横向对比,同时进行更多的组合,但由于本次实验数据量和训练时间的限制(如果全部测出,会跑4^5=1024次代码),我们未能进行更大规模的实验;此外,还可以尝试引入参数敏感性分析或可视化方法,更量化地评估各超参数对模型性能的贡献,从而实现更高效、自动化的模型调优。

本次实验不仅提升了我们对深度学习模型的工程实践能力,也帮助我们理解了人工智能模型在海量数据中的构建与优化思路。这为以后进行海洋数据分析和处理,提供了很大的帮助!

七、实验数据与代码

实验源代码

​
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchvision import datasets, transforms
from torch.utils.data import DataLoader, Subset
from sklearn.model_selection import KFold
from sklearn.metrics import accuracy_score, log_loss, roc_curve, auc
from sklearn.preprocessing import label_binarize
import matplotlib.pyplot as plt
import numpy as np
​
# ========== 设置随机种子 ==========
seed = 42
torch.manual_seed(seed)
np.random.seed(seed)
​
# ========== 参数配置 ==========
k_folds = 5
batch_size = 64
use_mnist = True
model_type = 'cnn'
epochs = 1
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
​
# ========== 模型定义 ==========
class DeepBPNet(nn.Module):
    def __init__(self, input_size=784, hidden_size=128, num_classes=10, num_layers=10, activation='relu', init_method='xavier'):
        super(DeepBPNet, self).__init__()
        assert num_layers >= 2, "网络层数必须 >= 2"
        self.activation_fn = self._get_activation_fn(activation)
        self.input_layer = nn.Linear(input_size, hidden_size)
        self.hidden_layers = nn.ModuleList([
            nn.Linear(hidden_size, hidden_size) for _ in range(num_layers - 2)
        ])
        self.output_layer = nn.Linear(hidden_size, num_classes)
        self.init_weights(method=init_method)
​
    def forward(self, x):
        x = x.view(x.size(0), -1)
        x = self.activation_fn(self.input_layer(x))
        for layer in self.hidden_layers:
            x = self.activation_fn(layer(x))
        x = self.output_layer(x)
        return x
​
    def _get_activation_fn(self, name):
        return {
            'relu': nn.ReLU(),
            'tanh': nn.Tanh(),
            'sigmoid': nn.Sigmoid(),
            'leaky_relu': nn.LeakyReLU(0.1)
        }.get(name.lower(), nn.ReLU())
​
    def init_weights(self, method='xavier'):
        for m in self.modules():
            if isinstance(m, nn.Linear):
                if method == 'xavier':
                    nn.init.xavier_uniform_(m.weight)
                elif method == 'kaiming':
                    nn.init.kaiming_normal_(m.weight, nonlinearity='relu')
                elif method == 'normal':
                    nn.init.normal_(m.weight, mean=0.0, std=0.02)
                elif method == 'uniform':
                    nn.init.uniform_(m.weight, a=-0.1, b=0.1)
                if m.bias is not None:
                    nn.init.zeros_(m.bias)
​
class CustomCNN(nn.Module):
    def __init__(self, in_channels=1, input_size=(28,28), num_classes=10, conv_layers=3, base_channels=16, activation='relu', init_method='xavier'):
        super(CustomCNN, self).__init__()
        assert conv_layers >= 1, "至少要有一个卷积层"
        self.activation_fn = self._get_activation_fn(activation)
        self.conv_blocks = nn.ModuleList()
        channels = in_channels
​
        for i in range(conv_layers):
            out_channels = base_channels * (2 ** i)
            self.conv_blocks.append(nn.Sequential(
                nn.Conv2d(channels, out_channels, kernel_size=3, padding=1),
                self.activation_fn,
                nn.MaxPool2d(2, 2)
            ))
            channels = out_channels
​
        dummy_input = torch.zeros(1, in_channels, *input_size)
        with torch.no_grad():
            for layer in self.conv_blocks:
                dummy_input = layer(dummy_input)
        flatten_dim = dummy_input.view(1, -1).shape[1]
​
        self.fc1 = nn.Linear(flatten_dim, 128)
        self.fc2 = nn.Linear(128, num_classes)
        self.init_weights(init_method)
​
    def forward(self, x):
        for layer in self.conv_blocks:
            x = layer(x)
        x = x.view(x.size(0), -1)
        x = self.activation_fn(self.fc1(x))
        x = self.fc2(x)
        return x
​
    def _get_activation_fn(self, name):
        return {
            'relu': nn.ReLU(),
            'tanh': nn.Tanh(),
            'sigmoid': nn.Sigmoid(),
            'leaky_relu': nn.LeakyReLU(0.1)
        }.get(name.lower(), nn.ReLU())
​
    def init_weights(self, method='xavier'):
        for m in self.modules():
            if isinstance(m, (nn.Conv2d, nn.Linear)):
                if method == 'xavier':
                    nn.init.xavier_uniform_(m.weight)
                elif method == 'kaiming':
                    nn.init.kaiming_normal_(m.weight, nonlinearity='relu')
                elif method == 'normal':
                    nn.init.normal_(m.weight, mean=0.0, std=0.02)
                elif method == 'uniform':
                    nn.init.uniform_(m.weight, a=-0.1, b=0.1)
                if m.bias is not None:
                    nn.init.zeros_(m.bias)
​
# ========== 数据读取与预处理模块 ==========
def load_dataset(use_mnist=True):
    if use_mnist:
        transform = transforms.Compose([
            transforms.ToTensor(),
            transforms.Normalize((0.1307,), (0.3081,))
        ])
        dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
    else:
        transform = transforms.Compose([
            transforms.ToTensor(),
            transforms.Normalize((0.5,), (0.5,))
        ])
        dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
    print(f"加载数据集完成:{len(dataset)}张图像,尺寸为 {dataset[0][0].shape}")
    return dataset
​
# ========== 数据与模型接口 ==========
def get_model(model_type='bp', input_shape=(1, 28, 28), num_classes=10, **kwargs):
    if model_type == 'bp':
        input_size = np.prod(input_shape)
        return DeepBPNet(input_size=input_size, num_classes=num_classes, **kwargs)
    elif model_type == 'cnn':
        return CustomCNN(in_channels=input_shape[0], input_size=input_shape[1:], num_classes=num_classes, **kwargs)
    else:
        raise ValueError("模型必须是'bp'或者'cnn'")
​
​
​
# ========== 模型评估模块 ==========
def evaluate_model_kfold(dataset, model_type='cnn', k_folds=5, batch_size=64, num_classes=10, device=None, epochs=1, **kwargs):
    if device is None:
        device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
​
    indices = list(range(len(dataset)))
    kf = KFold(n_splits=k_folds, shuffle=True, random_state=42)
​
    all_fold_metrics = []
    all_probs = []
    all_targets = []
​
    for fold, (train_idx, val_idx) in enumerate(kf.split(indices)):
        print(f"\n 训练轮数 {fold + 1}/{k_folds}")
​
        train_subset = Subset(dataset, train_idx)
        val_subset = Subset(dataset, val_idx)
        train_loader = DataLoader(train_subset, batch_size=batch_size, shuffle=True)
        val_loader = DataLoader(val_subset, batch_size=batch_size, shuffle=False)
​
        sample_input, _ = next(iter(train_loader))
        model = get_model(
            model_type=model_type,
            input_shape=sample_input.shape[1:],
            num_classes=num_classes,
            **kwargs
        ).to(device)
​
        criterion = nn.CrossEntropyLoss()
        optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
​
        # === 训练阶段 ===
        model.train()
        for epoch in range(epochs):
            for inputs, labels in train_loader:
                inputs, labels = inputs.to(device), labels.to(device)
                optimizer.zero_grad()
                outputs = model(inputs)
                loss = criterion(outputs, labels)
                loss.backward()
                optimizer.step()
​
        # === 验证阶段 ===
        model.eval()
        y_true, y_pred, y_prob = [], [], []
​
        with torch.no_grad():
            for inputs, labels in val_loader:
                inputs, labels = inputs.to(device), labels.to(device)
                outputs = model(inputs)
                probs = F.softmax(outputs, dim=1)
                preds = torch.argmax(probs, dim=1)
​
                y_true.extend(labels.cpu().numpy())
                y_pred.extend(preds.cpu().numpy())
                y_prob.extend(probs.cpu().numpy())
​
        acc = accuracy_score(y_true, y_pred)
        loss = log_loss(y_true, y_prob, labels=list(range(num_classes)))
​
        all_fold_metrics.append({'fold': fold + 1, 'accuracy': acc, 'loss': loss})
        all_probs.extend(y_prob)
        all_targets.extend(y_true)
​
        print(f" Fold {fold + 1} Accuracy: {acc:.4f}, Loss: {loss:.4f}")
​
    return all_fold_metrics, np.array(all_probs), np.array(all_targets)
​
​
# ========== 模型评估可视化模块 ==========
def plot_multiclass_roc(probs, targets, num_classes, save_path=None):
    targets_onehot = label_binarize(targets, classes=list(range(num_classes)))
​
    fpr = dict()
    tpr = dict()
    roc_auc = dict()
​
    for i in range(num_classes):
        fpr[i], tpr[i], _ = roc_curve(targets_onehot[:, i], probs[:, i])
        roc_auc[i] = auc(fpr[i], tpr[i])
​
    plt.figure(figsize=(10, 8))
    for i in range(num_classes):
        plt.plot(fpr[i], tpr[i], label=f"Class {i} (AUC = {roc_auc[i]:.2f})")
​
    plt.plot([0, 1], [0, 1], 'k--', label='Random')
    plt.xlim([0.0, 1.0])
    plt.ylim([0.95, 1.05])
    plt.xlabel('假警报率')
    plt.ylabel('识别率')
    plt.title('多分类模型的ROC曲线')
    plt.rcParams['font.sans-serif'] = ['SimHei']
    plt.legend(loc='lower right')
​
    if save_path:
        plt.savefig(save_path)
        print(f" ROC曲线已保存到 {save_path}")
    else:
        plt.show()
​
​
​
# ========== 主程序入口 ==========
if __name__ == '__main__':
    # === 数据集选择 ===
    print("请选择数据集:")
    print("1 - MNIST")
    print("2 - CIFAR-10")
    dataset_choice = input("请输入选项(1 或 2):").strip()
    if dataset_choice == '1':
        use_mnist = True
    elif dataset_choice == '2':
        use_mnist = False
    else:
        print("无效输入,默认使用 MNIST")
        use_mnist = True
​
    # === 模型结构选择 ===
    print("\n请选择模型结构:")
    print("1 - CNN(卷积神经网络)")
    print("2 - BPNet(多层感知机)")
    model_choice = input("请输入选项(1 或 2):").strip()
    if model_choice == '1':
        model_type = 'cnn'
    elif model_choice == '2':
        model_type = 'bp'
    else:
        print("无效输入,默认使用 CNN")
        model_type = 'cnn'
​
    # === 初始化方式选择 ===
    print("\n请选择初始化方式:")
    print("1 - Xavier(推荐)")
    print("2 - Kaiming")
    print("3 - Normal")
    print("4 - Uniform")
    init_choice = input("请输入选项(1~4):").strip()
    init_method = {
        '1': 'xavier',
        '2': 'kaiming',
        '3': 'normal',
        '4': 'uniform'
    }.get(init_choice, 'xavier')
    if init_choice not in ['1', '2', '3', '4']:
        print("无效输入,默认使用 Xavier 初始化")
​
    # === 激活函数选择 ===
    print("\n请选择激活函数:")
    print("1 - ReLU")
    print("2 - LeakyReLU")
    print("3 - Tanh")
    print("4 - Sigmoid")
    act_choice = input("请输入选项(1~4):").strip()
    activation = {
        '1': 'relu',
        '2': 'leaky_relu',
        '3': 'tanh',
        '4': 'sigmoid'
    }.get(act_choice, 'relu')
    if act_choice not in ['1', '2', '3', '4']:
        print("无效输入,默认使用 ReLU 激活函数")
​
    # === 网络结构参数(根据模型类型设置) ===
    extra_args = {}
    if model_type == 'bp':
        hidden_size = input("\n请输入每层的神经元数量(默认128):").strip()
        num_layers = input("请输入网络层数(最少2层,默认10):").strip()
        extra_args['hidden_size'] = int(hidden_size) if hidden_size.isdigit() else 128
        extra_args['num_layers'] = int(num_layers) if num_layers.isdigit() else 10
        # 将激活函数传递给BP网络
        extra_args['activation'] = activation
    elif model_type == 'cnn':
        conv_layers = input("\n请输入卷积层数(默认3):").strip()
        base_channels = input("请输入基础卷积核个数(默认16):").strip()
        extra_args['conv_layers'] = int(conv_layers) if conv_layers.isdigit() else 3
        extra_args['base_channels'] = int(base_channels) if base_channels.isdigit() else 16
        extra_args['activation'] = activation
​
    extra_args['init_method'] = init_method
​
    # === 加载数据 & 开始训练 ===
    dataset = load_dataset(use_mnist=use_mnist)
    metrics, probs, targets = evaluate_model_kfold(
        dataset=dataset,
        model_type=model_type,
        k_folds=k_folds,
        batch_size=batch_size,
        num_classes=10,
        device=device,
        epochs=epochs,
        **extra_args  # 💡传入模型构建参数
    )
​
    print("\n 每折评估结果:")
    for result in metrics:
        print(f"Fold {result['fold']} - Accuracy: {result['accuracy']:.4f}, Loss: {result['loss']:.4f}")
​
    # === 绘制 ROC 曲线图 ===
    plot_multiclass_roc(probs=probs, targets=targets, num_classes=10)
​
    # === 输出整体实验配置信息和平均结果 ===
    avg_acc = np.mean([fold['accuracy'] for fold in metrics])
    avg_loss = np.mean([fold['loss'] for fold in metrics])
​
    print("\n 实验配置与结果汇总:")
    print(f"数据集         :{'MNIST' if use_mnist else 'CIFAR-10'}")
    print(f"模型结构       :{'BPNet' if model_type == 'bp' else 'CNN'}")
    print(f"初始化方式     :{init_method}")
    if model_type == 'cnn':
        print(f"激活函数       :{activation}")
        print(f"卷积层数       :{extra_args['conv_layers']}")
        print(f"卷积核基数     :{extra_args['base_channels']}")
    else:
        print(f"激活函数       :{activation}")
        print(f"神经元数量     :{extra_args['hidden_size']}")
        print(f"网络层数       :{extra_args['num_layers']}")
​
    print(f"\n 平均 Accuracy:{avg_acc:.4f}")
    print(f" 平均 Loss    :{avg_loss:.4f}")
​
​
​
​

网站公告

今日签到

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