一、實驗目的
實驗目標
構建基于神經網絡模型的數據分析與模式識別框架,探明神經網絡在大數據分析中的意義。
實驗任務
構建基于深度 BP 神經網絡與卷積神經網絡的數據分析與模式識別框架,將數據集 MNIST 與 CIFAR-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
接收模型類型(bp
或 cnn
)和輸入數據的形狀等參數:當選擇 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
保持尺寸)、激活函數(如ReLU
、Tanh
等)以及2×2
的最大池化操作(MaxPool2d
),用于特征提取與降維。每一層的通道數由base_channels=16
按2^i
級數增長,即為16 → 32 → 64
。全連接層:卷積模塊輸出展平后,通過兩個線性全連接層處理。第一層映射到
128
個神經元,第二層輸出num_classes=10
,用于多分類任務。激活函數與初始化:激活函數可選(如
ReLU
、Sigmoid
等),通過參數activation
控制;所有卷積層與線性層的權重初始化方式也可配置(如xavier
、kaiming
等),增強模型靈活性與實驗可控性。
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 = activationself.activation_fn = self._get_activation_fn(activation)self.conv_blocks = nn.ModuleList()channels = in_channelsfor 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_channelsdummy_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 = Trueelif dataset_choice == '2':use_mnist = Falseelse: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 128extra_args['num_layers'] = int(num_layers) if num_layers.isdigit() else 10# 將激活函數傳遞給BP網絡extra_args['activation'] = activationelif 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 3extra_args['base_channels'] = int(base_channels) if base_channels.isdigit() else 16extra_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}") ? ? ? ?