
1. 項目概述一次從理論到實踐的CNN圖像分類實戰李宏毅老師的機器學習課程在圈內一直以理論扎實、作業硬核著稱。2023年的HW03作業聚焦于卷積神經網絡CNN進行圖像分類這不僅是課程的一個關鍵里程碑也是很多同學從“看懂公式”到“跑通模型”的第一次深度實戰。我花了大約一周的時間從理解題目、數據預處理、模型搭建、調參優化到最終提交整個過程踩了不少坑也積累了一些心得。這篇內容就來詳細拆解HW03的每一個環節分享可復現的代碼和那些在官方文檔里找不到的“實戰經驗”。無論你是正在苦戰這份作業的同學還是想通過一個具體項目來鞏固CNN和PyTorch的初學者相信都能從中找到直接的參考和啟發。這份作業的核心任務是利用CNN對食物圖片進行分類共11個類別。它模擬了一個真實的機器學習項目流程數據加載與探索、模型架構設計、訓練策略制定、結果分析與提交。難點不在于模型的復雜性通常一個幾層的CNN即可而在于如何正確地處理數據、組織代碼、調試模型以及理解訓練過程中的各種現象。接下來我會按照實際操作的順序逐一拆解。2. 作業整體思路與核心設計解析2.1 任務目標與環境搭建作業的目標非常明確給定一個食物圖片數據集訓練集、驗證集和測試集你需要構建一個CNN模型在驗證集上獲得盡可能高的準確率并對測試集進行預測生成提交文件。數據集的圖片尺寸統一但可能存在光照、角度、背景等差異這正是一個典型的圖像分類任務。首先環境是基石。我強烈建議使用Python 3.8和PyTorch 1.12的環境。Anaconda虛擬環境管理是首選它能避免包版本沖突。除了PyTorch你還需要安裝torchvision用于圖像處理和預訓練模型、pandas、numpy、matplotlib和tqdm用于進度條。使用CUDA版本的PyTorch可以極大加速訓練過程只要你的顯卡支持。注意在Windows系統上安裝PyTorch的CUDA版本時務必通過PyTorch官網提供的命令進行安裝明確指定CUDA版本如cu117對應CUDA 11.7并提前在系統中安裝對應版本的NVIDIA驅動和CUDA Toolkit。版本不匹配是新手最常見的環境報錯原因。2.2 數據加載與預處理策略數據是模型的“糧食”處理得好壞直接決定模型的上限。作業提供的數據通常以文件夾形式組織每個子文件夾代表一個類別里面存放著該類別的圖片。torchvision.datasets.ImageFolder是處理這種結構的神器它能自動根據文件夾結構生成標簽。預處理transforms是關鍵一步。我們需要將圖片轉換為模型能處理的張量Tensor并進行歸一化。常見的操作組合如下from torchvision import transforms # 訓練集的預處理通常包含數據增強 train_transform transforms.Compose([ transforms.RandomHorizontalFlip(p0.5), # 隨機水平翻轉 transforms.RandomRotation(degrees15), # 隨機旋轉 transforms.ColorJitter(brightness0.2, contrast0.2), # 顏色抖動 transforms.Resize((128, 128)), # 調整大小根據你的輸入尺寸定 transforms.ToTensor(), # 轉換為Tensor并歸一化到[0,1] transforms.Normalize(mean[0.485, 0.456, 0.406], # ImageNet的均值 std[0.229, 0.224, 0.225]) # ImageNet的標準差 ]) # 驗證集和測試集的預處理不進行數據增強只需Resize和歸一化 test_transform transforms.Compose([ transforms.Resize((128, 128)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])這里有幾個重要的設計考量數據增強Data Augmentation僅對訓練集使用。通過隨機翻轉、旋轉、顏色調整等可以人為增加數據的多樣性相當于讓模型看到了更多可能的圖片變體能有效防止過擬合提升模型的泛化能力。這是提升小數據集性能的必備技巧。歸一化參數為什么使用ImageNet的均值和標準差因為許多預訓練模型是在ImageNet上訓練的其卷積核已經適應了這種數據分布。即使我們不使用預訓練模型使用這個通用的統計值也是一個不錯的起點它有助于穩定訓練過程。如果你的數據集與ImageNet差異極大可以計算自己數據集的均值和標準差但作業數據通常接近自然圖像直接用問題不大。輸入尺寸(128, 128)是一個示例你可以根據計算資源調整。更大的尺寸如224 256可能帶來更好的性能但也會顯著增加顯存消耗和訓練時間。需要在性能和效率間權衡。使用ImageFolder和DataLoader加載數據from torchvision import datasets from torch.utils.data import DataLoader train_dataset datasets.ImageFolder(root./data/train, transformtrain_transform) valid_dataset datasets.ImageFolder(root./data/valid, transformtest_transform) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers4, pin_memoryTrue) valid_loader DataLoader(valid_dataset, batch_size64, shuffleFalse, num_workers4, pin_memoryTrue)batch_size一次訓練所選取的樣本數。太大可能導致顯存不足太小則梯度更新噪聲大、訓練慢。64或32是常見的起點。shuffle訓練集必須打亂防止模型學習到數據順序。num_workers用于數據加載的子進程數可以加快數據讀取速度。在Windows上有時設為0可避免問題。pin_memory當使用GPU時設置為True可以將數據鎖頁內存加速數據從CPU到GPU的傳輸。3. CNN模型架構設計與實現細節3.1 從零搭建一個基礎CNN模型對于HW03我們完全可以自己搭建一個輕量級的CNN。一個典型的模式是多個“卷積層 - 激活層 - 池化層”的堆疊最后接全連接層進行分類。下面是一個示例模型import torch.nn as nn import torch.nn.functional as F class MyCNN(nn.Module): def __init__(self, num_classes11): super(MyCNN, self).__init__() # 卷積塊1: 輸入3通道輸出16通道 self.conv1 nn.Conv2d(3, 16, kernel_size3, padding1) # padding1保持尺寸 self.bn1 nn.BatchNorm2d(16) # 批歸一化加速收斂 self.pool nn.MaxPool2d(2, 2) # 2x2最大池化尺寸減半 # 卷積塊2 self.conv2 nn.Conv2d(16, 32, kernel_size3, padding1) self.bn2 nn.BatchNorm2d(32) # 卷積塊3 self.conv3 nn.Conv2d(32, 64, kernel_size3, padding1) self.bn3 nn.BatchNorm2d(64) # 全連接層 # 假設輸入圖片是128x128經過3次pooling后是16x16 (128 - 64 - 32 - 16) self.fc1 nn.Linear(64 * 16 * 16, 512) # 需要根據實際尺寸計算 self.dropout nn.Dropout(p0.5) # Dropout防止過擬合 self.fc2 nn.Linear(512, num_classes) def forward(self, x): x self.pool(F.relu(self.bn1(self.conv1(x)))) x self.pool(F.relu(self.bn2(self.conv2(x)))) x self.pool(F.relu(self.bn3(self.conv3(x)))) # 將特征圖展平成一維向量 x x.view(x.size(0), -1) x F.relu(self.fc1(x)) x self.dropout(x) x self.fc2(x) return x關鍵設計點解析卷積核與通道數卷積核大小常用3x3這是VGG網絡推廣的高效選擇。通道數如16, 32, 64逐層增加讓網絡能夠學習到從低級邊緣、紋理到高級物體部件的越來越復雜的特征。Paddingpadding1配合kernel_size3可以保持特征圖的空間尺寸不變output_size input_size這樣在計算經過池化后的尺寸時更簡單。批歸一化BatchNorm這是現代深度網絡的標配。它對每一批batch的數據進行歸一化減均值、除標準差使得中間層的輸出分布更加穩定。這帶來了三大好處允許使用更大的學習率、減少對參數初始化的依賴、有一定的正則化效果。通常放在卷積層之后、激活函數之前。激活函數ReLURectified Linear Unit是最常用的因為它計算簡單且能緩解梯度消失問題。池化層最大池化MaxPooling用于下采樣逐步減少特征圖尺寸增加感受野同時提供一定的平移不變性。2x2池化是最常見的。全連接層與Dropout將卷積學習到的空間特征映射到樣本標記空間。在第一個全連接層后加入Dropout隨機“丟棄”一部分神經元置零是防止過擬合的強大正則化手段。p0.5是一個常用值。展平操作在進入全連接層前必須將多維的特征圖“拉平”成一維向量。x.view(x.size(0), -1)中的-1表示自動計算該維度的大小。實操心得計算全連接層輸入維度是新手最容易出錯的地方。一個可靠的方法是先寫一個print(x.shape)在view操作之前運行一次前向傳播用一個小批量數據查看展平前的x的形狀例如可能是[batch_size, 64, 16, 16]那么展平后的維度就是64*16*1616384。將這個值填入nn.Linear的第一個參數。3.2 使用預訓練模型進行遷移學習如果你的目標是獲得更高的分數遷移學習幾乎是必選項。其思想是利用在超大規模數據集如ImageNet上預訓練好的模型權重作為我們模型的起點然后針對我們的食物分類任務進行微調Fine-tuning。torchvision.models提供了豐富的預訓練模型如ResNet, VGG, EfficientNet等。以ResNet18為例import torchvision.models as models class PretrainedModel(nn.Module): def __init__(self, num_classes11): super(PretrainedModel, self).__init__() # 加載預訓練的ResNet18并獲取其特征提取部分去掉最后的全連接層 backbone models.resnet18(pretrainedTrue) # 凍結所有卷積層的參數在初始階段不更新它們 for param in backbone.parameters(): param.requires_grad False # 替換最后的全連接層以適應我們的11分類任務 num_features backbone.fc.in_features backbone.fc nn.Linear(num_features, num_classes) self.model backbone def forward(self, x): return self.model(x)微調策略詳解凍結Freeze與解凍Unfreeze一開始我們凍結了預訓練模型的所有層requires_gradFalse這意味著在訓練初期只有我們新替換的全連接層fc的參數會被更新。這是為了讓模型先適應新任務的新“頭部”。訓練幾個epoch后可以解凍所有層或部分深層卷積層用較小的學習率進行整體微調。這種分階段訓練策略非常有效。學習率設置對于新添加的層fc可以使用一個相對較大的學習率如0.01對于預訓練層如果解凍了應該使用一個非常小的學習率如0.001的十分之一以免破壞已經學到的寶貴特征。模型選擇ResNet18/34比較輕量訓練快ResNet50/101性能更強但更耗資源。EfficientNet系列在精度和效率上平衡得更好。根據你的硬件和時間選擇。4. 訓練流程的完整實現與調參技巧4.1 訓練循環的構建有了模型和數據接下來就是編寫訓練循環。這是PyTorch訓練的標準模板但細節決定成敗。import torch import torch.optim as optim from tqdm import tqdm device torch.device(cuda if torch.cuda.is_available() else cpu) model MyCNN().to(device) # 或 PretrainedModel().to(device) criterion nn.CrossEntropyLoss() # 多分類任務使用交叉熵損失 optimizer optim.Adam(model.parameters(), lr0.001) # Adam優化器 scheduler optim.lr_scheduler.StepLR(optimizer, step_size5, gamma0.1) # 學習率調度器 num_epochs 30 best_acc 0.0 for epoch in range(num_epochs): # 訓練階段 model.train() train_loss 0.0 train_correct 0 train_total 0 # 使用tqdm包裝數據加載器顯示進度條 pbar tqdm(train_loader, descfEpoch {epoch1}/{num_epochs} [Train]) for images, labels in pbar: images, labels images.to(device), labels.to(device) # 前向傳播 outputs model(images) loss criterion(outputs, labels) # 反向傳播與優化 optimizer.zero_grad() # 清空過往梯度至關重要 loss.backward() # 反向傳播計算梯度 optimizer.step() # 更新參數 # 統計 train_loss loss.item() * images.size(0) _, predicted torch.max(outputs.data, 1) train_total labels.size(0) train_correct (predicted labels).sum().item() # 更新進度條信息 pbar.set_postfix({Loss: loss.item()}) train_loss train_loss / len(train_dataset) train_acc 100.0 * train_correct / train_total # 驗證階段 model.eval() valid_loss 0.0 valid_correct 0 valid_total 0 with torch.no_grad(): # 關閉梯度計算節省內存和計算 for images, labels in valid_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) valid_loss loss.item() * images.size(0) _, predicted torch.max(outputs.data, 1) valid_total labels.size(0) valid_correct (predicted labels).sum().item() valid_loss valid_loss / len(valid_dataset) valid_acc 100.0 * valid_correct / valid_total # 學習率調度 scheduler.step() # 打印日志 print(fEpoch [{epoch1}/{num_epochs}], fTrain Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}%, fValid Loss: {valid_loss:.4f}, Valid Acc: {valid_acc:.2f}%) # 保存最佳模型 if valid_acc best_acc: best_acc valid_acc torch.save(model.state_dict(), best_model.pth) print(f - Best model saved with acc: {best_acc:.2f}%)核心環節拆解.train()和.eval()模式model.train()會啟用Dropout和BatchNorm的訓練行為如用當前batch的統計量進行歸一化。model.eval()則會關閉這些行為使用訓練階段累積的移動平均統計量進行歸一化這對驗證和測試的一致性至關重要。optimizer.zero_grad()PyTorch的梯度是累加的。如果在每次backward()前不清零梯度梯度會不斷累積導致更新方向錯誤。這是最常見的錯誤之一。with torch.no_grad()在驗證和測試時我們不需要計算梯度。這個上下文管理器可以禁用自動求導大幅減少內存消耗并加速計算。損失和準確率計算損失是每個樣本損失的平均。注意在累加總損失時我們乘以了images.size(0)即當前batch的大小最后再除以數據集總大小這是為了得到整個epoch的平均損失即使最后一個batch可能不滿。模型保存我們保存驗證集上性能最好的模型state_dict而不是最后一個epoch的模型。這可以防止模型在訓練后期過擬合導致驗證集性能下降。4.2 超參數調優與訓練監控訓練深度學習模型很大程度上是在調參。以下是一些核心超參數和經驗值超參數常見范圍/選擇作用與調參心得學習率 (lr)1e-4 到 1e-2最重要的參數。太大導致震蕩不收斂太小則收斂慢。Adam優化器下從3e-4開始嘗試是個好選擇。使用學習率調度器如StepLR, ReduceLROnPlateau在訓練中后期降低學習率有助于模型收斂到更優的局部最優點。批大小 (batch_size)32, 64, 128受限于GPU顯存。更大的batch_size使梯度估計更準確訓練更穩定但可能降低泛化能力。通常設為能占滿顯存的最大2的冪次。優化器 (Optimizer)Adam, AdamW, SGDAdam自適應學習率對初始學習率不敏感通常作為默認選擇收斂快。SGD with momentum配合學習率衰減最終性能可能更好但需要更多調參。AdamW解決了Adam的權重衰減問題現在被認為是更優的選擇尤其是配合預訓練模型。權重衰減 (Weight Decay)1e-4, 1e-5一種L2正則化防止模型權重過大緩解過擬合。對于Adam使用AdamW并設置weight_decay參數。Epoch數20-100觀察訓練/驗證損失曲線。當驗證損失連續多個epoch不再下降甚至上升時應提前停止Early Stopping防止過擬合。訓練監控技巧繪制損失/準確率曲線這是診斷訓練過程最直觀的工具。理想情況是訓練損失穩步下降驗證損失先降后升過擬合拐點。如果訓練損失都不降可能是學習率太小、模型能力不足或數據有問題。使用TensorBoard或Weights Biases這些工具可以實時可視化損失、準確率、權重分布、梯度直方圖等對于復雜調參和實驗管理非常有幫助。5. 測試集預測、結果分析與常見問題排查5.1 生成提交文件訓練出最佳模型后我們需要在測試集上運行生成符合Kaggle或課程平臺要求的提交文件通常是CSV格式。import pandas as pd from PIL import Image import os # 加載最佳模型 model.load_state_dict(torch.load(best_model.pth)) model.eval() test_data_path ./data/test submission [] # 注意測試集可能沒有標簽需要按文件名順序讀取 test_image_names sorted(os.listdir(test_data_path)) # 確保順序一致 with torch.no_grad(): for img_name in tqdm(test_image_names, descPredicting): img_path os.path.join(test_data_path, img_name) # 用PIL打開圖片并應用與驗證集相同的預處理 image Image.open(img_path).convert(RGB) image test_transform(image).unsqueeze(0).to(device) # 增加batch維度 output model(image) _, predicted torch.max(output, 1) # 假設文件名就是ID或者從文件名中提取ID submission.append([img_name.split(.)[0], predicted.item()]) # 保存ID和預測類別 # 創建DataFrame并保存為CSV df pd.DataFrame(submission, columns[Id, Category]) df.to_csv(submission.csv, indexFalse) print(Submission file saved to submission.csv)重要提示測試集的預處理必須與驗證集完全一致相同的Resize尺寸、相同的歸一化均值標準差。任何不一致都會導致模型性能的不可預測下降。5.2 結果分析與模型診斷提交后你會得到一個在測試集或公開驗證集上的分數。如果分數不理想如何排查過擬合Overfitting訓練準確率遠高于驗證準確率。對策增加數據增強的強度加大Dropout比率添加更多的正則化如權重衰減使用更簡單的模型收集更多數據。欠擬合Underfitting訓練和驗證準確率都很低。對策增加模型復雜度更多層、更多通道減少正則化延長訓練時間檢查數據預處理是否有誤如歸一化參數錯了嘗試使用預訓練模型。訓練不穩定Loss震蕩或NaN檢查學習率學習率可能太高嘗試降低一個數量級。檢查數據數據中是否有損壞的圖片或異常的標簽歸一化后數據值是否在合理范圍如-3到3之間檢查梯度可以添加梯度裁剪torch.nn.utils.clip_grad_norm_防止梯度爆炸。驗證集性能停滯嘗試學習率調度使用ReduceLROnPlateau在驗證損失停滯時自動降低學習率。解凍預訓練層如果使用遷移學習且還凍結著嘗試解凍后面幾層進行微調。集成Ensemble訓練多個不同初始化或不同結構的模型對它們的預測結果進行平均或投票這是提升分數的“大殺器”但會增加計算成本。5.3 進階優化思路如果基礎模型已經跑通想沖擊更高分數可以嘗試以下方向更強大的數據增強除了基本的翻轉旋轉可以嘗試RandAugment或AutoAugment這類自動搜索或預設的增強策略包它們組合了多種增強方式效果顯著。模型集成訓練多個模型如ResNet18, ResNet50, EfficientNet-B0在預測時取它們輸出的概率平均能有效提升魯棒性和準確率。測試時增強Test Time Augmentation, TTA對一張測試圖片進行多種增強如原圖、水平翻轉、垂直翻轉等分別預測然后對結果取平均。這相當于給了模型多次“觀察”的機會通常能提升一點性能。標簽平滑Label Smoothing在計算損失時不直接使用硬標簽如[0,0,1,0]而是使用平滑后的軟標簽如[0.01, 0.01, 0.96, 0.01]這可以減輕模型對標簽的過度自信有正則化效果可能提升泛化能力。混合精度訓練使用torch.cuda.amp進行自動混合精度訓練可以在幾乎不損失精度的情況下大幅減少顯存占用從而允許使用更大的batch_size或更大的模型同時還能加速訓練。完成HW03的整個過程遠比單純實現一個CNN類要豐富。它涵蓋了數據管道構建、模型設計、訓練調試、結果分析這一完整機器學習閉環。最大的收獲不是調出了一個高分的模型而是學會了如何系統地診斷和解決訓練中遇到的各種問題。當你看到自己的模型在驗證集上的準確率一點點爬升最終生成那個可以提交的CSV文件時那種親手搭建的系統跑通了的成就感是只看理論無法比擬的。希望這份詳細的解析和代碼能幫你更順暢地完成這次實戰少走一些我走過的彎路。