目錄
LeNet 的設計背景與目標
?LeNet 的網絡結構(經典 LeNet-5)
局部感受野詳解
一、局部感受野和全連接網絡的區別
1.?傳統全連接網絡的問題
2.?局部感受野的解決方案
二、局部感受野的優勢
1. 參數大幅減少
2. 提取局部特征
3. 平移不變性
參數共享(權值共享)詳解
一、定義與核心思想
二、為什么需要參數共享?
1.?減少參數數量
2.?平移不變性
3.?強制特征學習的普適性
三、參數共享與局部感受野的協同作用
?池化詳解
一、池化的核心作用
二、常見的池化方式
1.?最大池化(Max Pooling)
2.?平均池化(Average Pooling)
3.?其他變體
三、池化的工作流程
四、池化與卷積的區別
五、池化的參數設置
?為什么每個 epoch 都需要新的累加器?
代碼執行流程驗證
?完整代碼
實驗結果
LeNet 的設計背景與目標
在 LeNet 出現之前,圖像識別主要依賴全連接神經網絡,但存在兩個關鍵問題:
?
- 參數爆炸:全連接網絡將圖像展平為向量,若輸入為 32×32 的圖像,僅第一層就需要約 1000 個神經元(參數近百萬),計算成本極高。
- 缺乏平移不變性:圖像中目標的微小位移會導致全連接網絡的輸入向量大幅變化,識別魯棒性差。
LeNet 通過局部感受野、參數共享和池化三大核心思想解決了這些問題,專為圖像識別設計。
?LeNet 的網絡結構(經典 LeNet-5)
LeNet-5 的結構簡潔且層次分明,可分為卷積層塊(特征提取)和全連接層塊(分類決策)兩部分。整體結構如下:
層類型 名稱 核心參數 輸入維度 輸出維度 作用 輸入層 - 手寫數字圖像(灰度圖) 32×32×1 32×32×1 原始像素輸入 卷積層 C1 6 個 5×5 卷積核,步幅 = 1,無填充(padding=0) 32×32×1 28×28×6 提取局部特征(如邊緣、拐角),輸出 6 個特征圖 平均池化層 S2 2×2 窗口,步幅 = 2,無填充 28×28×6 14×14×6 降低維度(寬高減半),增強平移不變性 卷積層 C3 16 個 5×5 卷積核,步幅 = 1,無填充 14×14×6 10×10×16 提取更復雜的組合特征(如紋理、局部形狀) 平均池化層 S4 2×2 窗口,步幅 = 2,無填充 10×10×16 5×5×16 進一步降維,寬高再減半 卷積層(特殊) C5 120 個 5×5 卷積核,步幅 = 1,無填充(等價于全連接,因輸入 5×5 剛好被卷積核覆蓋) 5×5×16 1×1×120 提取高層抽象特征,輸出 120 個特征 全連接層 F6 84 個神經元 120 84 整合特征,為分類做準備 輸出層 F7 10 個神經元(對應數字 0-9),使用 softmax 激活 84 10 輸出
局部感受野詳解
一、局部感受野和全連接網絡的區別
1.?傳統全連接網絡的問題
在處理圖像時,若使用全連接網絡:
- 輸入層神經元與圖像像素一一對應(如 32×32 圖像需 1024 個神經元)。
- 隱藏層每個神經元連接所有輸入神經元,導致參數極多(如 1000 個隱藏神經元需 1024×1000≈100 萬參數)。
- 這種 “全局連接” 忽略了圖像的局部相關性(相鄰像素關聯緊密,遠距離像素關聯弱)。
2.?局部感受野的解決方案
在 CNN 中,卷積層的每個神經元僅連接輸入圖像的局部區域,而非全部像素:
- 局部區域大小:由卷積核(濾波器)的尺寸決定。例如,5×5 的卷積核對應 5×5 的局部感受野。
- 滑動窗口機制:卷積核在輸入圖像上滑動,每個位置生成一個輸出值,形成特征圖。
示例(輸入圖像 32×32,卷積核 5×5):
- 第一個卷積神經元僅連接輸入圖像左上角 5×5 的區域(感受野)。
- 第二個神經元連接向右滑動 1 步后的 5×5 區域(假設步幅 = 1)。
- 依此類推,直到覆蓋整個圖像。
二、局部感受野的優勢
1. 參數大幅減少
- 每個卷積核的參數固定(如 5×5 卷積核僅 25 個參數),無論輸入圖像多大。
- 相比全連接網絡,參數減少幾個數量級,緩解過擬合,降低計算成本。
2. 提取局部特征
- 卷積核能自動學習圖像的局部特征(如邊緣、紋理、角點)。
- 多層 CNN 通過堆疊卷積層,可從低級特征(邊緣)逐步構建高級特征(物體部件、整體)。
3. 平移不變性
- 無論特征出現在圖像的哪個位置,卷積核都能檢測到(因參數共享,見下文)。
- 例如,識別數字 “7” 時,無論 “7” 出現在圖像左上角還是右下角,同一卷積核都能響應。
參數共享(權值共享)詳解
參數共享(Parameter Sharing)?是卷積神經網絡(CNN)的核心機制之一,它與局部感受野共同構成了 CNN 的效率基石。通過參數共享,CNN 能夠在大幅減少模型參數的同時,保持強大的特征提取能力。
一、定義與核心思想
參數共享是指在神經網絡中,一組參數(權重)被應用于多個不同的輸入位置。在 CNN 中,這一思想體現在以下方面:
卷積核的參數共享
每個卷積核(濾波器)的參數在整個輸入圖像上保持不變。例如,一個 5×5 的卷積核在掃描輸入圖像的所有位置時,使用的是同一組權重。跨通道的獨立性
不同卷積核的參數是獨立的,但同一卷積核在不同位置的參數共享。例如,32 個卷積核會生成 32 個特征圖,每個特征圖使用各自獨立的參數。二、為什么需要參數共享?
1.?減少參數數量
傳統全連接網絡處理圖像時,參數數量與圖像尺寸的平方成正比(如 32×32 圖像需約 100 萬參數)。而 CNN 通過參數共享,將參數數量降低到與圖像尺寸無關:
?
- 一個 5×5 的卷積核僅需 25 個參數(加偏置 26 個)。
- 若有 32 個卷積核,總參數僅 32×26=832 個,遠少于全連接網絡。
2.?平移不變性
參數共享使 CNN 對圖像中的平移變換具有魯棒性:
- 無論特征出現在圖像的哪個位置,同一卷積核都能檢測到它。
- 例如,一個識別 “貓耳朵” 的卷積核,在圖像左上角和右下角都能發揮作用。
3.?強制特征學習的普適性
通過參數共享,網絡被迫學習在圖像所有位置都有效的特征,而非特定位置的特征,從而增強泛化能力。
三、參數共享與局部感受野的協同作用
參數共享與局部感受野是 CNN 的兩大核心思想,二者相互配合:
- 局部感受野:每個神經元僅連接輸入的局部區域,提取局部特征。
- 參數共享:同一卷積核在所有局部區域使用相同參數,檢測相同特征。
示例:
- 一個垂直邊緣檢測器(卷積核)通過參數共享,在圖像的所有位置檢測垂直邊緣。
- 若沒有參數共享,網絡需為每個位置學習獨立的邊緣檢測器,參數數量爆炸式增長。
?池化詳解
在卷積神經網絡(CNN)中,池化(Pooling)?是一種重要的下采樣操作,主要用于減少特征圖的空間維度(高度和寬度),同時保留關鍵特征。它通常緊跟在卷積層之后,是 CNN 中控制模型復雜度、提升計算效率和增強平移不變性的核心手段之一。
一、池化的核心作用
- 降低維度:通過對特征圖進行聚合操作,減少輸出特征圖的尺寸(例如將 2×2 區域壓縮為 1 個值),從而降低后續層的計算量和參數數量,避免過擬合。
- 增強平移不變性:對局部區域的特征進行聚合(如取最大值或平均值),使得模型對輸入數據的微小位移不敏感(例如圖像中物體的輕微移動不影響特征提取結果)。
- 保留關鍵特征:通過選擇局部區域的顯著特征(如最大值)或統計特征(如平均值),過濾冗余信息,突出重要模式。
二、常見的池化方式
池化操作通過一個固定大小的 “池化窗口” 在特征圖上滑動(類似卷積窗口),對窗口內的元素進行聚合計算。常見的池化方式有:
1.?最大池化(Max Pooling)
- 操作:取池化窗口內所有元素的最大值作為輸出。
- 特點:保留局部區域內的最顯著特征(如邊緣、紋理的強度),對噪聲更魯棒,是實際應用中最常用的池化方式。
- 示例:對窗口
[[1, 3], [2, 4]]
進行最大池化,結果為4
。2.?平均池化(Average Pooling)
- 操作:取池化窗口內所有元素的平均值作為輸出。
- 特點:保留局部區域的整體強度信息,對特征的平滑性更好,但可能會弱化顯著特征。
- 示例:對窗口
[[1, 3], [2, 4]]
進行平均池化,結果為(1+3+2+4)/4 = 2.5
。3.?其他變體
- 最小池化:取窗口內最小值(較少用,適用于檢測暗區域特征)。
- L2 池化:取窗口內元素平方和的平方根,兼顧數值大小和穩定性。
三、池化的工作流程
以2×2 最大池化為例,步驟如下:
- 設定池化窗口大小(如 2×2)和滑動步長(通常與窗口大小相同,如步長 = 2)。
- 窗口從特征圖左上角開始,依次在水平和垂直方向滑動。
- 對每個窗口內的元素執行池化操作(如取最大值),生成輸出特征圖的一個元素。
- 重復滑動,直到覆蓋整個特征圖。
示例:
?
輸入特征圖為 3×3 矩陣:?[[0, 1, 2],[3, 4, 5],[6, 7, 8]]
使用 2×2 最大池化(步長 = 2),輸出為 2×2 矩陣:
?[[4, 5], # 窗口(0-1行, 0-1列)最大值=4;窗口(0-1行, 1-2列)最大值=5[7, 8]] # 窗口(1-2行, 0-1列)最大值=7;窗口(1-2行, 1-2列)最大值=8
四、池化與卷積的區別
維度 卷積操作 池化操作 核心目的 提取局部特征(如邊緣、紋理) 降低維度,保留關鍵特征 是否有參數 有卷積核參數(需要學習) 無參數(僅執行固定聚合規則) 操作對象 輸入與卷積核的加權求和 窗口內元素的聚合(max/avg 等) 輸出維度變化 可通過 padding 和 stride 控制 通常尺寸減小(下采樣) 五、池化的參數設置
與卷積類似,池化操作也需要指定:
- 池化窗口大小(kernel_size):如 2×2、3×3(常用奇數,便于對稱滑動)。
- 步長(stride):窗口滑動的步幅,默認與窗口大小相同(如 2×2 窗口對應步長 = 2)。
- 填充(padding):在特征圖邊緣補 0,用于保持輸出尺寸(較少用,因池化通常目的是降維)。
例如,在 PyTorch 中定義一個 3×3 最大池化層:
?import torch.nn as nn pool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) # 窗口3×3,步長2,邊緣補1層0
?為什么每個 epoch 都需要新的累加器?
累加器(
d2l.Accumulator
)的作用是統計當前 epoch 內的訓練指標(訓練損失總和、正確預測數、總樣本數)。每個 epoch 是一個獨立的訓練周期,需要單獨統計該周期的指標,因此必須在每個 epoch 開始時重置累加器。具體來說:
metric
?的三個累加項:
metric[0]
:當前 epoch 所有樣本的損失總和(l * X.shape[0]
)。metric[1]
:當前 epoch 所有樣本中正確預測的數量(d2l.accuracy(y_hat, y)
)。metric[2]
:當前 epoch 處理的總樣本數(X.shape[0]
,即批次大小)。每個 epoch 獨立統計:
- 第 1 個 epoch 結束后,
metric
?中存儲的是第 1 個 epoch 的指標,用于計算該 epoch 的平均損失和準確率。- 第 2 個 epoch 開始時,必須創建新的?
metric
,否則會與第 1 個 epoch 的指標混淆,導致計算錯誤。代碼執行流程驗證
假設?
?num_epochs=2
(訓練 2 個 epoch),流程如下:?# 第1個epoch開始 epoch=0:metric = d2l.Accumulator(3) # 新累加器,初始值[0,0,0]遍歷所有批次:metric.add(當前批次損失, 當前批次正確數, 當前批次樣本數)計算第1個epoch的指標:train_l = metric[0]/metric[2] # 第1個epoch的平均損失train_acc = metric[1]/metric[2] # 第1個epoch的準確率# 第2個epoch開始 epoch=1:metric = d2l.Accumulator(3) # 新累加器,重置為[0,0,0]遍歷所有批次:metric.add(當前批次損失, 當前批次正確數, 當前批次樣本數)計算第2個epoch的指標:train_l = metric[0]/metric[2] # 第2個epoch的平均損失(獨立于第1個epoch)
如果不重置累加器,第 2 個 epoch 的指標會疊加到第 1 個 epoch 上,導致結果錯誤。
?完整代碼
"""
文件名: 6.6 卷積神經網絡(LeNet)
作者: 墨塵
日期: 2025/7/13
項目名: dl_env
備注:
"""
import torch
from torch import nn
from d2l import torch as d2l
# 手動顯示圖像(關鍵)
import matplotlib.pyplot as plt
import matplotlib.text as text # 新增:用于修改文本繪制# -------------------------- 核心解決方案:替換減號 --------------------------
# 定義替換函數:將Unicode減號U+2212替換為普通減號-
def replace_minus(s):if isinstance(s, str):return s.replace('\u2212', '-')return s# 安全重寫Text類的set_text方法,避免super()錯誤
original_set_text = text.Text.set_text # 保存原始方法
def new_set_text(self, s):s = replace_minus(s) # 替換減號return original_set_text(self, s) # 調用原始方法
text.Text.set_text = new_set_text # 應用新方法
# -------------------------------------------------------------------------# -------------------------- 字體配置(關鍵修改)--------------------------
# 解決中文顯示和 Unicode 減號(U+2212)顯示問題
plt.rcParams["font.family"] = ["SimHei"]
plt.rcParams["text.usetex"] = True # 使用Latex渲染
plt.rcParams["axes.unicode_minus"] = True # 正確顯示負號
plt.rcParams["mathtext.fontset"] = "cm" # 確保數學符號(如減號)正常顯示
d2l.plt.rcParams.update(plt.rcParams) # 讓 d2l 繪圖工具繼承字體配置
# -------------------------------------------------------------------------"""激活函數的核心作用是引入非線性,因此:
隱藏層必須使用激活函數,否則深層網絡失去意義;
輸出層根據任務選擇是否使用,取決于是否需要對輸出進行范圍約束或概率化。"""# 評估模型在數據集上的準確率(使用GPU)
def evaluate_accuracy_gpu(net, data_iter, device=None): #@save"""使用GPU計算模型在數據集上的精度"""if isinstance(net, nn.Module):net.eval() # 設置為評估模式(關閉Dropout、BatchNorm等)if not device:device = next(iter(net.parameters())).device # 獲取模型參數所在的設備# 累加器:用于統計正確預測數和總樣本數metric = d2l.Accumulator(2) # 創建一個包含2個累加項的累加器with torch.no_grad(): # 關閉梯度計算,節省內存和計算資源for X, y in data_iter:# 將數據移至GPUif isinstance(X, list):X = [x.to(device) for x in X] # 處理特殊輸入(如BERT)else:X = X.to(device)y = y.to(device)# 計算預測正確的樣本數,并累加到metric中metric.add(d2l.accuracy(net(X), y), y.numel()) # y.numel()返回y中元素的總數return metric[0] / metric[1] # 返回準確率# 訓練模型(使用GPU)自定義GPU訓練模型
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):"""用GPU訓練模型(在第六章定義)"""# 權重初始化函數:對線性層和卷積層使用Xavier均勻初始化def init_weights(m):if type(m) == nn.Linear or type(m) == nn.Conv2d:nn.init.xavier_uniform_(m.weight)net.apply(init_weights) # 應用權重初始化print('training on', device)net.to(device) # 將模型移至GPU# 定義優化器和損失函數optimizer = torch.optim.SGD(net.parameters(), lr=lr) # 使用隨機梯度下降loss = nn.CrossEntropyLoss() # 交叉熵損失函數(用于多分類)# 創建動畫繪制器,用于可視化訓練過程animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],legend=['train loss', 'train acc', 'test acc'])timer, num_batches = d2l.Timer(), len(train_iter) # 計時器和批次數量# 訓練主循環for epoch in range(num_epochs):# 訓練損失之和,訓練準確率之和,樣本數metric = d2l.Accumulator(3) # 創建包含3個累加項的累加器net.train() # 設置為訓練模式# 遍歷每個批次的數據for i, (X, y) in enumerate(train_iter):timer.start() # 開始計時# 梯度清零,前向傳播,計算損失,反向傳播,更新參數optimizer.zero_grad()X, y = X.to(device), y.to(device) # 將數據移至GPUy_hat = net(X) # 前向傳播l = loss(y_hat, y) # 計算損失l.backward() # 反向傳播optimizer.step() # 更新參數# 統計訓練損失和準確率with torch.no_grad():metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])timer.stop() # 停止計時# 計算當前批次的訓練損失和準確率train_l = metric[0] / metric[2]train_acc = metric[1] / metric[2]# 每5個批次或最后一個批次時,更新動畫if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:animator.add(epoch + (i + 1) / num_batches,(train_l, train_acc, None))# 每個epoch結束后,在測試集上評估模型test_acc = evaluate_accuracy_gpu(net, test_iter)animator.add(epoch + 1, (None, None, test_acc))# 打印最終結果print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, test acc {test_acc:.3f}')print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on {str(device)}')if __name__ == '__main__':# ------------------------------# 步驟1: 定義LeNet-5網絡# ------------------------------# 定義LeNet-5網絡(針對MNIST數據集優化版本)net = nn.Sequential(# ===== 第一卷積塊:提取低級特征 =====nn.Conv2d(1, 6, kernel_size=5, padding=2), # 輸入1通道,輸出6通道,保持尺寸28x28nn.Sigmoid(), # 引入非線性nn.AvgPool2d(kernel_size=2, stride=2), # 池化,尺寸減半至14x14# ===== 第二卷積塊:提取中級特征 =====nn.Conv2d(6, 16, kernel_size=5), # 輸入6通道,輸出16通道,尺寸減小至10x10nn.Sigmoid(), # 引入非線性nn.AvgPool2d(kernel_size=2, stride=2), # 池化,尺寸減半至5x5# ===== 展平層:將多維特征圖轉為一維向量 =====nn.Flatten(), # 展平為400維向量# ===== 全連接層塊:分類器 =====nn.Linear(16 * 5 * 5, 120), # 400→120nn.Sigmoid(), # 引入非線性nn.Linear(120, 84), # 120→84nn.Sigmoid(), # 引入非線性nn.Linear(84, 10) # 84→10(輸出10類,對應0-9數字))# -----------------------------------------------# 步驟2: 檢查LeNet-5網絡的可用性# ------------------------------------------------# 創建一個隨機輸入張量,模擬MNIST數據X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)# 逐層打印輸出形狀,驗證網絡結構for layer in net:X = layer(X)print(layer.__class__.__name__, 'output shape: \t', X.shape)# -----------------------------------------------# 步驟3: 初始化數據集# ------------------------------------------------# 加載Fashion-MNIST數據集(28x28灰度圖像,10個類別)batch_size = 256train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)# -----------------------------------------------# 步驟4: 訓練# ------------------------------------------------lr, num_epochs = 0.9, 10 # 學習率和訓練輪數# 使用GPU訓練模型(如果可用)train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())# 顯示圖像plt.show(block=True) # block=True 確保窗口阻塞,直到手動關閉