【動手學深度學習】LeNet:卷積神經網絡的開山之作
- 1,LeNet卷積神經網絡簡介
- 2,Fashion-MNIST圖像分類數據集
- 3,LeNet總體架構
- 4,LeNet代碼實現
- 4.1,定義LeNet模型
- 4.2,定義模型評估函數
- 4.3,定義訓練函數進行訓練
1,LeNet卷積神經網絡簡介
LeNet 是一種經典的卷積神經網絡,是現代卷積神經網絡的起源之一。它是早期成功的神經網絡;LeNet先使用卷積層來學習圖片空間信息,使用池化層降低圖片敏感度,然后使用全連接層來轉換到類別空間。 其思想被廣泛應用于圖像分類、目標檢測、圖像分割等多個計算機視覺領域
,為這些領域的研究和發展提供了新的思路和方法。例如,在安防領域用于面部識別和監控系統,在自動駕駛領域用于實時視頻分析和對象跟蹤等。
1989年,Yann LeCun等人在貝爾實驗室工作期間提出了LeNet-1。這個網絡主要用于手寫數字識別,引入了卷積操作和權值共享的概念,簡化了網絡結構,減少了參數數量,提高了模型的泛化能力和訓練速度。此后經過多年的迭代改進,1998年,LeCun等人正式發表了LeNet-5。LeNet-5在LeNet-1的基礎上進一步優化了網絡結構,增加了網絡的深度和復雜度,使其在手寫數字識別任務上取得了更好的性能。LeNet-5的成功應用證明了CNN在圖像識別領域的巨大潛力,為后續CNN的發展奠定了堅實的基礎。
2,Fashion-MNIST圖像分類數據集
Fashion-MNIST數據集是一個廣泛使用的圖像分類數據集。
Fashion-MNIST中包含的10個類別
,分別為t-shirt(T恤)、trouser(褲子)、pullover(套衫)、dress(連衣裙)、coat(外套)、sandal(涼鞋)、shirt(襯衫)、sneaker(運動鞋)、bag(包)和ankle boot(短靴)。
之前,已經學習過Fashion-MNIST數據集。 【動手學深度學習】Fashion-MNIST圖片分類數據集,其基本情況如下:
- 訓練集:包含60,000張圖像,用于模型訓練;
- 測試集:包含10,000張圖像,用于評估模型性能;
- 數據集由灰度圖像組成,其通道數為1;
- 每個圖像的高度和寬度均為28像素;
- 調用load_data_fashion_mnist()函數加載數據集;
具體定義如下:
"""
下載Fashion-MNIST數據集,然后將其加載到內存中
參數resize表示調整圖片大小
"""
def load_data_fashion_mnist(batch_size, resize=None): # trans是一個用于轉換的 *列表*trans = [transforms.ToTensor()]if resize: # resize不為空,表示需要調整圖片大小trans.insert(0, transforms.Resize(resize))trans = transforms.Compose(trans)mnist_train = torchvision.datasets.FashionMNIST(root="../data", train=True, transform=trans, download=True)mnist_test = torchvision.datasets.FashionMNIST(root="../data", train=False, transform=trans, download=True)return (data.DataLoader(mnist_train, batch_size, shuffle=True,num_workers=get_dataloader_workers()),data.DataLoader(mnist_test, batch_size, shuffle=False,num_workers=get_dataloader_workers()))
3,LeNet總體架構
總體來看,LeNet(LeNet-5)由兩個部分組成:
- 卷積編碼器:由兩個卷積層組成;
- 全連接層密集塊:由三個全連接層組成;
每個卷積塊中的基本單元是一個卷積層、一個sigmoid激活函數和平均匯聚層。(實際上使用ReLU激活函數和最大匯聚層更有效,但當時還沒有發現):
-
Fashion-MNIST數據集的圖像通道為1,大小為28×28,內部
經過卷積層填充
之后得到的實際輸入數據是32×32的圖像數據; -
第一卷積層有
6
個輸出通道,而第二個卷積層有16
個輸出通道; -
對應輸出通道的數量,第一個卷積層有6個5×5的卷積核,第二個卷積層有16個5×5的卷積核;
-
每個卷積核應用于輸入數據時會產生一個特征圖(feature map),也就是一個輸出通道;
-
每個卷積層都使用不同數量的5×5的卷積核和一個sigmoid激活函數。這些層將輸入映射到多個二維特征輸出,通常同時增加通道的數量;
-
卷積操作后,通過
2×2的池化操作
(默認步幅為2
和池化窗口大小保持一致)將原特征圖的各維度減半。比如原來是28×28,池化后變為14×14;
4,LeNet代碼實現
接下來使用深度學習框架實現LeNet模型,并進行訓練和測試。
4.1,定義LeNet模型
LeNet模型總共七層: 兩層卷積層、兩層池化層、三層全連接層; 其中每層都使用sigmod作為激活函數,它將卷積層的輸出壓縮到0和1之間,有助于非線性變換。
import torch
from torch import nn
from d2l import torch as d2l
""" 默認情況下,深度學習框架中的步幅與匯聚窗口的大小相同(窗口沒有重疊)"""# nn.Sequential 是一個容器,可按順序包裝一系列子模塊(如層、激活函數)。使得模型的構建變得更加簡潔
net = nn.Sequential(# 第一個二維卷積層,輸入通道是1(灰度圖像),輸出通道是6,卷積核大小5×5,圖像周圍加入兩層0填充# 使用sigmod激活函數nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),# 第一個平均池化層:用2x2的池化窗口,步長為2。經此池化操作后得6個14×14的特征圖nn.AvgPool2d(kernel_size=2, stride=2),# 這是第二個二維卷積層,輸入通道數為6(與第一個卷積層的輸出通道數相匹配),輸出通道數為16。卷積核的大小為5x5,沒有使用padding填充# 使用sigmod激活函數nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),# 第二個平均池化層:配置與第一層平均池化層相同。nn.AvgPool2d(kernel_size=2, stride=2),# 在將數據傳遞給全連接層之前,需要將多維的卷積和池化輸出展平為一維向量。以便傳給全連接層nn.Flatten(),# 經過前面的卷積和池化操作后,輸出16個5×5的特征圖# 全連接層,輸入特征的數量是16 * 5 * 5。輸出特征的數量是120。nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),# 全連接層,輸入特征數量120,輸出84nn.Linear(120, 84), nn.Sigmoid(),# 全連接層,輸入特征數量84,輸出10,對應Fashion-MNIST數據集的10個類別nn.Linear(84, 10))
下面,我們將一個大小為 28 × 28 28 \times 28 28×28的單通道(黑白)圖像通過LeNet。通過在每一層打印輸出的形狀,我們可以檢查模型,以確保其操作與我們期望的一致。
# 打印調試信息,檢查模型
# size=(1, 1, 28, 28):批次大小1,通道數1,形狀28*28
X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)# 遍歷了神經網絡 net 中的每一層
for layer in net:X = layer(X)# 打印該層的類型(Conv2d、AvgPool2d、Flatten、Linear)以及輸出張量的形狀print(layer.__class__.__name__,'output shape: \t',X.shape)# torch.Size([1, 6, 28, 28])中的1代表批次大小,6表示通道數
運行結果如下:
4.2,定義模型評估函數
我們已經實現了LeNet,接下來讓我們看看LeNet在Fashion-MNIST數據集上的表現。
加載Fashion-MNIST圖片分類數據集
batch_size = 256 # 批量大小
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
定義評估函數計算預測準確率
def evaluate_accuracy_gpu(net, data_iter, device=None):"""使用GPU計算模型在數據集上的精度"""if isinstance(net, nn.Module):net.eval() # 設置為評估模式if not device: # 若沒有指定device,則通過獲取模型參數的第一個元素的設備來確定應該使用的設備# net.parameters()返回模型的所有可學習參數(如權重和偏置)# next() 函數從迭代器中獲取第一個元素。通常是第一個層的權重或偏置# .device 是 PyTorch 張量(torch.Tensor)的一個屬性,表示該張量所在的備(如 GPU 或 CPU)# 例如,模型在 GPU 上運行,.device 的值可能是 device(type='cuda', index=0)device = next(iter(net.parameters())).device# 累加器記錄正確預測的數量和總預測的數量metric = d2l.Accumulator(2)with torch.no_grad(): # 評估模型時,不需要計算梯度for X, y in data_iter: # 每次迭代獲取一個數據批次X和對應的標簽yif isinstance(X, list): # x為list,每個元素都挪到對應的設備X = [x.to(device) for x in X]else: # x是tensor,只需要挪一次X = X.to(device)y = y.to(device)# accuracy可以計算出預測正確的樣本數量# y.numel()計算出樣本總數metric.add(d2l.accuracy(net(X), y), y.numel())return metric[0] / metric[1]
4.3,定義訓練函數進行訓練
定義可以使用GPU訓練的訓練函數。
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):"""用GPU訓練模型"""def init_weights(m): # 初始化權重# 如果是全連接層或卷積層使用Xavier均勻初始化方法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)# 使用隨機梯度下降(SGD)優化器optimizer = torch.optim.SGD(net.parameters(), lr=lr)# 使用交叉熵損失函數(nn.CrossEntropyLoss),適用于分類任務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)# 將模型設置為訓練模式,這會啟用Dropout等訓練時特有的操作net.train()for i, (X, y) in enumerate(train_iter): # 遍歷訓練數據集timer.start()optimizer.zero_grad() # 梯度清零X, y = X.to(device), y.to(device) # 將輸入數據X和標簽y移動到指定的設備# 前向傳播,得到預測結果 y_haty_hat = net(X) """在 PyTorch 中,nn.CrossEntropyLoss 默認會對每個樣本的損失值進行平均,返回的是批次中所有樣本損失的平均值。"""l = loss(y_hat, y) # 計算損失# 進行反向傳播,計算梯度。l.backward()# 使用優化器更新模型參數。optimizer.step()with torch.no_grad(): # 禁用梯度計算# l * X.shape[0]是當前批次的總損失。樣本平均損失乘當前批次樣本數# d2l.accuracy(y_hat, y) 計算當前批次正確預測的樣本數# X.shape[0]代表當前批次的樣本數# 最終累加器累積了整個訓練集的總損失,預測正確的樣本總數和總樣本數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]# 更新動畫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}, 'f'test acc {test_acc:.3f}')# 打印訓練過程中每秒處理的樣本數(即訓練效率),以及訓練所使用的設備。print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec 'f'on {str(device)}')
調用函數進行訓練
lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
運行結果如下: