一、為什么需要 CNN?從圖像識別的 “麻煩” 說起
假設你想讓電腦識別一張圖片里有沒有貓。
如果用傳統神經網絡:
- 一張 100×100 的彩色圖片,有 100×100×3=30000 個像素點,每個像素點都是一個輸入神經元。
- 傳統網絡需要每個輸入神經元和隱藏層神經元全連接,參數數量會爆炸(比如隱藏層有 1000 個神經元,就有 30000×1000=3000 萬個參數),不僅計算慢,還容易 “學傻”(過擬合)。
CNN 的聰明之處:
- 模仿人類視覺:人眼看圖片時,先關注局部(比如貓的耳朵、胡須),再組合成整體。
- 減少參數:通過 “局部感知” 和 “權值共享”,讓電腦高效提取特征,避免重復計算。
二、CNN 的核心組件:像搭積木一樣理解
CNN 由幾個關鍵層組成,每一層都有明確的 “分工”,我們用 “識別貓的圖片” 來舉例:
1. 卷積層(Convolution Layer):找 “局部特征” 的 “過濾器”
- 作用:提取圖片中的局部特征(比如邊緣、顏色塊、簡單形狀)。
- 類比:
就像用不同的 “放大鏡” 觀察圖片:- 有的放大鏡專門找 “橫線”(比如貓的胡須),
- 有的專門找 “圓形”(比如貓的眼睛),
- 有的專門找 “黃色塊”(比如橘貓的毛)。
- 核心概念:
- 過濾器(Filter):也叫 “卷積核”,是一個小矩陣,負責檢測特定特征。比如 3×3 的過濾器在圖片上滑動,每次只看 3×3 的局部區域(局部感知)。
- 權值共享:同一個過濾器在圖片上滑動時,參數(權重)不變,就像用同一個 “放大鏡” 掃描全圖,減少參數數量。
- 輸出特征圖(Feature Map):過濾器掃描后,會生成一張新圖,記錄 “哪里有這個特征”。比如檢測 “橫線” 的過濾器,會在胡須位置輸出高值。
舉個例子:
輸入一張貓的圖片,經過一個 “邊緣檢測” 過濾器,輸出的特征圖會突出顯示貓的輪廓邊緣,忽略顏色和紋理細節。
2. 池化層(Pooling Layer):“壓縮” 信息的 “簡化器”
- 作用:縮小特征圖尺寸,保留關鍵信息,減少計算量,同時讓特征更 “抗干擾”(比如貓的位置稍微移動,特征仍能識別)。
- 類比:
就像看地圖時,從 “街道級” 縮放成 “區域級”,忽略細節,只保留主要地標。 - 常見類型:
- 最大池化(Max Pooling):取每個小區域的最大值(比如 2×2 區域),相當于保留最明顯的特征。
- 平均池化(Average Pooling):取平均值,保留整體趨勢。
- 效果:
比如一個 100×100 的特征圖,經過 2×2 最大池化后變成 50×50,尺寸減半,但關鍵特征(如貓的眼睛位置)依然存在。
3. 全連接層(Fully Connected Layer):“綜合判斷” 的 “大腦”
- 作用:把前面提取的所有特征整合起來,判斷圖片屬于哪個類別(比如 “貓”“狗”“汽車”)。
- 類比:
前面的卷積和池化層找到了 “胡須”“耳朵”“黃色毛” 等特征,全連接層就像大腦,根據這些特征組合判斷:“有胡須 + 尖耳朵 + 黃色毛 = 貓!” - 工作方式:
把所有特征圖 “拍扁” 成一維向量,然后通過多層神經網絡計算概率,輸出分類結果(比如 “貓” 的概率 90%,“狗” 10%)。
三、CNN 的 “思考” 過程:用貓圖舉個完整例子
- 輸入圖片:一張彩色貓的照片。
- 卷積層處理:
- 用多個過濾器(比如邊緣、顏色、形狀過濾器)掃描圖片,生成多張特征圖,分別記錄 “哪里有邊緣”“哪里有黃色”“哪里有圓形” 等。
- 池化層壓縮:
- 縮小特征圖尺寸,比如從 100×100→50×50,保留關鍵特征的位置和強度。
- 重復卷積 + 池化(深層網絡):
- 淺層網絡提取簡單特征(邊緣、顏色),深層網絡組合簡單特征成復雜特征(比如 “邊緣 + 圓形 = 眼睛”“眼睛 + 胡須 = 貓臉”)。
- 全連接層分類:
- 把所有特征整合,計算屬于 “貓” 的概率,輸出結果。
代碼實戰
一、環境準備與設備配置
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as pltdevice = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用設備: {device}")if torch.cuda.is_available():print(f"GPU名稱: {torch.cuda.get_device_name(0)}")print(f"GPU內存: {torch.cuda.get_device_properties(0).total_memory / 1024 / 1024} MB")
核心功能解析:
導入庫說明:
torch
:PyTorch 核心庫,提供張量計算和自動微分功能torch.nn
:包含神經網絡層定義(如卷積層、全連接層)torch.optim
:優化器庫(如 Adam、SGD)torchvision
:提供 MNIST 數據集和圖像預處理工具matplotlib
:用于可視化圖像和結果
設備配置原理:
torch.device
會自動檢測 GPU 是否可用(CUDA 是 NVIDIA 的 GPU 計算平臺)- 將模型和數據放在 GPU 上可加速計算(矩陣運算并行化)
- 若沒有 GPU,默認使用 CPU(計算速度較慢但可運行)
二、數據預處理與加載
transform = transforms.Compose([transforms.Resize((32, 32)), # LeNet-5輸入尺寸為32x32transforms.ToTensor(),transforms.Normalize((0.1307,), (0.3081,))
])train_dataset = datasets.MNIST('data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST('data', train=False, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=1000)
數據處理全解析:
預處理流程拆解:
Resize((32, 32))
:將 MNIST 原始 28x28 圖像放大到 32x32,因為 LeNet-5 設計輸入尺寸為 32x32,更大尺寸便于提取邊緣特征ToTensor()
:將圖像轉換為 PyTorch 張量,并將像素值從 [0,255] 縮放到 [0,1]Normalize
:標準化處理,公式為(x-mean)/std
,MNIST 數據集的全局均值 0.1307 和標準差 0.3081,可讓數據分布更穩定,加速訓練
數據集與數據加載器:
datasets.MNIST
:自動下載 MNIST 數據集(6 萬訓練圖 + 1 萬測試圖),transform
參數應用預處理流程DataLoader
:批量加載數據的工具:batch_size=64
:每次訓練使用 64 張圖像組成一個批次(Batch)shuffle=True
:每個訓練周期打亂數據順序,避免模型按固定模式學習- 測試集
batch_size=1000
:批量更大,減少測試次數
三、LeNet-5 模型定義(核心架構)
class LeNet5(nn.Module):def __init__(self):super().__init__()self.conv1 = nn.Conv2d(1, 6, kernel_size=5) # C1層:6個5x5卷積核self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2) # S2層:2x2池化self.conv2 = nn.Conv2d(6, 16, kernel_size=5) # C3層:16個5x5卷積核self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2) # S4層:2x2池化self.fc1 = nn.Linear(16 * 5 * 5, 120) # C5層:全連接self.fc2 = nn.Linear(120, 84) # F6層:全連接self.fc3 = nn.Linear(84, 10) # 輸出層self.relu = nn.ReLU()def forward(self, x):x = self.pool1(self.relu(self.conv1(x)))x = self.pool2(self.relu(self.conv2(x)))x = x.view(-1, 16 * 5 * 5) # 展平x = self.relu(self.fc1(x))x = self.relu(self.fc2(x))x = self.fc3(x)return x
LeNet-5 完整參數流動(關鍵!)
層名 | 操作 / 代碼 | 輸入尺寸 | 輸出尺寸 | 參數數量 | 計算過程(關鍵!) |
---|---|---|---|---|---|
輸入 | - | [1, 32, 32] | - | - | 32×32 像素的灰度圖像(單通道) |
Conv1 | nn.Conv2d(1, 6, kernel_size=5) | [1, 32, 32] | [6, 28, 28] | 156 | 輸出尺寸:(32-5+1)×(32-5+1)=28×28 參數:(5×5×1+1)×6=156 |
ReLU1 | self.relu(conv1(x)) | [6, 28, 28] | [6, 28, 28] | 0 | 對每個元素應用 max (0,x) |
Pool1 | nn.MaxPool2d(kernel_size=2) | [6, 28, 28] | [6, 14, 14] | 0 | 輸出尺寸:28÷2=14×14 |
Conv2 | nn.Conv2d(6, 16, kernel_size=5) | [6, 14, 14] | [16, 10, 10] | 2,416 | 輸出尺寸:(14-5+1)×(14-5+1)=10×10 參數:(5×5×6+1)×16=2416 |
ReLU2 | self.relu(conv2(x)) | [16, 10, 10] | [16, 10, 10] | 0 | 對每個元素應用 max (0,x) |
Pool2 | nn.MaxPool2d(kernel_size=2) | [16, 10, 10] | [16, 5, 5] | 0 | 輸出尺寸:10÷2=5×5 |
Flatten | x.view(-1, 16*5*5) | [16, 5, 5] | [400] | 0 | 將張量展平為一維向量:16×5×5=400 |
FC1 | nn.Linear(400, 120) | [400] | [120] | 48,120 | 參數:400×120+120=48120 |
ReLU3 | self.relu(fc1(x)) | [120] | [120] | 0 | 對每個元素應用 max (0,x) |
FC2 | nn.Linear(120, 84) | [120] | [84] | 10,164 | 參數:120×84+84=10164 |
ReLU4 | self.relu(fc2(x)) | [84] | [84] | 0 | 對每個元素應用 max (0,x) |
FC3 | nn.Linear(84, 10) | [84] | [10] | 850 | 參數:84×10+10=850 |
輸出 | - | [10] | - | - | 10 個類別得分(對應 0-9 數字) |
模型架構逐行解析:
繼承 nn.Module:所有 PyTorch 模型需繼承
nn.Module
基類,重寫__init__
和forward
方法卷積層(特征提取):
nn.Conv2d(1, 6, kernel_size=5)
:- 輸入通道 1(MNIST 是灰度圖),輸出通道 6(生成 6 個特征圖)
- 卷積核大小 5x5(局部感知野,每次看 5x5 的圖像區域)
- 作用:提取 6 種不同的邊緣特征(如橫線、豎線、斜線)
池化層(特征壓縮):
nn.MaxPool2d(kernel_size=2, stride=2)
:- 2x2 窗口,步長 2(窗口不重疊)
- 取窗口內最大值(保留最強特征,忽略位置偏移)
- 作用:將特征圖尺寸減半,減少計算量,增強抗變形能力
全連接層(特征分類):
nn.Linear(16*5*5, 120)
:- 輸入維度 1655=400(來自第二層池化后的特征圖尺寸:16 個 5x5 特征圖)
- 輸出維度 120(將 400 維特征映射到 120 維)
- 作用:前兩層全連接層用于組合特征,最后一層
fc3
輸出 10 維(對應 0-9 數字分類)
ReLU 激活函數:
nn.ReLU()
:公式f(x)=max(0, x)
- 作用:引入非線性,讓模型能學習復雜特征(如數字的曲線形狀),解決線性模型表達能力不足的問題
forward 前向傳播:
- 數據流向:輸入→卷積 1→ReLU→池化 1→卷積 2→ReLU→池化 2→展平→全連接 1→ReLU→全連接 2→ReLU→全連接 3→輸出
x.view(-1, 16*5*5)
:將多維特征圖展平為一維向量(-1 表示自動計算批量大小)
四、模型訓練流程
model = LeNet5().to(device)
loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)model.train()
epochs = 5
for epoch in range(epochs):running_loss = 0.0for batch_idx, (data, target) in enumerate(train_loader):optimizer.zero_grad()data, target = data.to(device), target.to(device)output = model(data)loss = loss_function(output, target)loss.backward()optimizer.step()running_loss += loss.item()if batch_idx % 100 == 0:print(f"Epoch {epoch + 1}, Batch {batch_idx}, Loss {loss.item():.4f}")print(f"Epoch {epoch + 1}, Average Loss {running_loss / len(train_loader):.4f}")
訓練過程深度解析:
初始化三要素:
model.to(device)
:將模型參數移至 GPU/CPUnn.CrossEntropyLoss()
:交叉熵損失函數,適用于多分類問題(自動包含 Softmax)optim.Adam(model.parameters(), lr=0.001)
:Adam 優化器,自動調整學習率,參數lr=0.001
是初始學習率
訓練循環邏輯:
model.train()
:設置模型為訓練模式(啟用 Dropout、BatchNorm 等,此處 LeNet-5 未使用)epochs=5
:完整遍歷訓練集 5 次batch_idx, (data, target)
:從數據加載器中獲取一個批次的數據(data 是圖像,target 是真實標簽)
單批次訓練步驟:
optimizer.zero_grad()
:清零梯度(PyTorch 默認累積梯度,每次迭代前需清零)data.to(device)
:將數據移至計算設備(GPU/CPU)output = model(data)
:前向傳播,計算模型預測結果loss = loss_function(output, target)
:計算預測與真實標簽的差異(損失)loss.backward()
:反向傳播,計算損失對所有參數的梯度optimizer.step()
:根據梯度更新模型參數(如卷積核權重、全連接層權重)
損失監控:
running_loss += loss.item()
:累加每個批次的損失loss.item():.4f
:打印當前批次損失(保留 4 位小數)Average Loss
:每個周期的平均損失,用于判斷模型是否在學習(理想情況是逐漸下降)
五、模型評估與可視化
# 評估模型
model.eval()
correct = 0
total = 0
with torch.no_grad():for data, target in test_loader:data, target = data.to(device), target.to(device)output = model(data)_, predicted = torch.max(output.data, 1)total += target.size(0)correct += (predicted == target).sum().item()print(f"Test Accuracy: {100 * correct / total}%")# 可視化預測結果
model.eval()
num_samples = 5
fig, axes = plt.subplots(1, num_samples, figsize=(15, 3))with torch.no_grad():for i, (data, target) in enumerate(test_loader):data, target = data.to(device), target.to(device)output = model(data)_, predicted = torch.max(output, 1)for j in range(num_samples):img = data[j].view(32, 32).cpu().numpy()axes[j].imshow(img, cmap='gray')axes[j].set_title(f'Pred: {predicted[j]}, True: {target[j]}')axes[j].axis('off')break
plt.tight_layout()
plt.show()
評估與可視化解析:
模型評估流程:
model.eval()
:設置模型為評估模式(關閉訓練特有的操作,如 Dropout)with torch.no_grad()
:禁用梯度計算(節省內存,加速計算)torch.max(output, 1)
:獲取每個樣本預測概率最大的類別(索引 0-9)- 準確率計算:正確預測數 ÷ 總樣本數 ×100%
可視化原理:
fig, axes = plt.subplots(1, 5)
:創建 1 行 5 列的子圖data[j].view(32, 32)
:將張量從 [1,32,32] 重塑為 [32,32](去掉通道維度)cpu().numpy()
:將 GPU 數據移至 CPU 并轉換為 numpy 數組(matplotlib 只能顯示 numpy 數組)set_title
:顯示預測類別和真實類別,驗證模型預測是否正確
六、LeNet-5 核心參數流動圖解
輸入尺寸變化:
- 輸入:[batch_size, 1, 32, 32](批量大小,通道數,高度,寬度)
conv1
:6 個 5x5 卷積核 → 輸出 [batch_size, 6, 28, 28](尺寸計算:32-5+1=28)pool1
:2x2 最大池化 → 輸出 [batch_size, 6, 14, 14](尺寸減半)conv2
:16 個 5x5 卷積核 → 輸出 [batch_size, 16, 10, 10](14-5+1=10)pool2
:2x2 最大池化 → 輸出 [batch_size, 16, 5, 5]view
展平:[batch_size, 1655=400]fc1
:400→120 → 輸出 [batch_size, 120]fc2
:120→84 → 輸出 [batch_size, 84]fc3
:84→10 → 輸出 [batch_size, 10](10 個類別概率)
參數數量計算:
- 卷積層 1:(1×5×5+1)×6=156 參數(每個卷積核 5×5×1 權重 + 1 偏置,共 6 個)
- 卷積層 2:(6×5×5+1)×16=2416 參數
- 全連接層 1:400×120+120=48120 參數
- 全連接層 2:120×84+84=10164 參數
- 全連接層 3:84×10+10=850 參數
- 總參數:156+2416+48120+10164+850=61706(約 6 萬參數,遠少于全連接網絡)
七、代碼中隱藏的 CNN 核心概念映射
代碼片段 | 對應 CNN 概念 | 通俗解釋 |
---|---|---|
nn.Conv2d | 卷積層 | 用濾鏡提取圖像局部特征(如邊緣、形狀) |
nn.MaxPool2d | 池化層 | 縮小特征圖,保留關鍵特征,忽略位置偏移 |
nn.ReLU() | 激活函數 | 只保留強特征,過濾弱特征,讓模型能學習復雜模式 |
CrossEntropyLoss | 損失函數 | 衡量模型預測與真實標簽的差異,指導模型優化 |
optimizer.step() | 反向傳播與參數更新 | 根據損失調整模型參數(濾鏡權重、全連接層權重),讓下次預測更準 |
DataLoader(batch_size=64) | 批量訓練(Batch Training) | 一次訓練多個樣本,加速收斂,減少噪聲影響 |