pytorch實戰1:手把手教你基于pytorch實作LeNet5
前言
最近在看經典的卷積網絡架構,打算自己嘗試複現一下,在此系列文章中,會參考很多文章,有些已經忘記了出處,是以就不貼連結了,希望大家了解。
完整的代碼在最後。
本系列必須的基礎
python基礎知識、CNN原理知識、pytorch基礎知識
本系列的目的
一是幫助自己鞏固知識點;
二是自己實作一次,可以發現很多之前的不足;
三是希望可以給大家一個參考。
目錄結構
文章目錄
-
- pytorch實戰1:手把手教你基于pytorch實作LeNet5
-
- 1. 資料集介紹與下載下傳:
-
- 1.1 介紹:
- 1.2 下載下傳:
- 2. LeNet5模型建立:
-
- 2.1 架構介紹:
- 2.2 模型類建立:
- 3. 模型訓練和評估:
-
- 3.1 資料加載:
- 3.2 模型執行個體化和放入GPU中:
- 3.3 定義損失函數、優化器:
- 3.4 訓練:
- 3.5 評估:
- 3.6 探究:
- 4. 總結:
1. 資料集介紹與下載下傳:
1.1 介紹:
LeNet5是1998年提出的,主要用來當時的手寫數字識别,是以使用的資料集是MNIST資料集。
MNIST是一個經典的手寫數字資料,也是一個公開的小型資料。 MNIST中的圖像每個都是28*28=784的大小,并且為灰階圖,值為0-255。
1.2 下載下傳:
資料集可以通過官網進行下載下傳
http://yann.lecun.com/exdb/mnist/
,(建議)也可以通過
pytorch代碼擷取
:
# 導包
from torchvision.datasets import MNIST
import torchvision.transforms as transforms
# 下載下傳資料集或者加載資料集
train_dataset = MNIST(root='../data/',train=True,transform=transforms.ToTensor(),download=True)
test_dataset = MNIST(root='../data/',train=False,transform=transforms.ToTensor())
注意,上面下載下傳代碼中root參數指明了下載下傳後儲存的位址,需要根據自己的檔案夾路徑進行修改。
2. LeNet5模型建立:
2.1 架構介紹:
LeNet5模型架構如下圖所示:
其中,值得注意的地方是:
- 輸入大小為32*32,而資料集的圖像大小為28*28,是以需要做出處理,方法是為原圖像進行填充,即padding=2(這樣大小為:28+2+2=32)。
- 上圖中全連接配接層寫的不是特别清晰,比如FC1層120個神經元是指的是輸出的神經元個數,輸入神經元個數應該是5*5*16。
2.2 模型類建立:
下面我們來建立這個模型類,首先根據pytorch建立模型的基本結構,寫出:
# 建立模型
class LeNet(nn.Module):
def __init__(self):
super(LeNet,self).__init__()
# 定義模型
pass
def forward(self,x):
pass
接着,我們來定義我們的模型,這裡我們采取的思路是:先定義前面的卷積、池化層,再定義全連接配接層。
好的,先定義卷積層(看注釋):
self.features = nn.Sequential(
# C1層,輸入通道數為1是因為為灰階圖,不是彩色圖。其餘的就是根據架構填寫的參數,除去padding=2,
# padding=2是為了讓28*28的圖檔變為32*32
nn.Conv2d(in_channels=1,out_channels=6,kernel_size=(5,5),stride=1,padding=2),
nn.ReLU(),
# Pool 1 層
nn.MaxPool2d(kernel_size=2,stride=2),
# C2層,輸入通道數是上一層的輸出通道數,其餘同上
nn.Conv2d(in_channels=6,out_channels=16,kernel_size=(5,5),stride=1),
nn.ReLU(),
# Pool 2 層
nn.MaxPool2d(kernel_size=2,stride=2),
)
這裡,我把本來使用的sigmoid函數改為了relu函數,大家可以寫的時候可以改回來。
然後,定義全連接配接層(這裡注意,我自己寫的時候開始還有點懵,後來才了解):
self.classifier = nn.Sequential(
# FC1層,輸入為5*5*16=400,輸出數為指定的參數值
nn.Linear(in_features=400, out_features=120),
nn.ReLU(),
# FC2 層
nn.Linear(in_features=120, out_features=84),
nn.ReLU(),
# FC3 層
nn.Linear(in_features=84, out_features=10)
)
最開始寫的時候,沒有想到還有400這個值,是以很糾結,後來才想起來:卷積層到全連接配接層的時候,需要把多元的資料拉平,是以顯然此時的輸入為卷積層輸出的各個次元相乘,即6*6*25。
最後,來定義前向算法,這個非常簡單,隻是注意要多一步把資料拉平(變為1維)的操作:
def forward(self,x):
# 定義前向算法
x = self.features(x)
x = torch.flatten(x,1)
result = self.classifier(x)
return result
這裡,也許你會問:**為什麼flatten參數要寫一個1?這是因為,我們這裡會采取批量訓練,是以傳入的資料是一個類似于[batch,32,32,5]的思維資料,其中batch指的是每批的個數,後面分别書圖像大小(32-32)和卷積核個數。是以,我們拉平的時候,需要從第二位開始拉平,使之變為[batch,32*32*5]**的形式,是以需要指定為1。
3. 模型訓練和評估:
3.1 資料加載:
使用pytorch加載資料,很簡單:
# 下載下傳資料集或者加載資料集
train_dataset = MNIST(root='../data/',train=True,transform=transforms.ToTensor(),download=True)
test_dataset = MNIST(root='../data/',train=False,transform=transforms.ToTensor())
# 加載資料: 分批次,每批32個資料
batch_size = 32
train_loader = DataLoader(dataset=train_dataset,batch_size=batch_size,shuffle=True)
test_loader = DataLoader(dataset=test_dataset,batch_size=batch_size,shuffle=False)
**基本上(也有需要自己定義Dataset的情況)**加載已有的資料都是這樣的格式。
3.2 模型執行個體化和放入GPU中:
這段很簡單,另外調用GPU的方法也是固定的,就不多說:
# 建立模型
model = LeNet()
# 模型放入GPU中
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model = model.to(device)
3.3 定義損失函數、優化器:
損失函數我們使用分類任務中常用的交叉熵損失函數,而優化器采用SGD優化器,學習率設定維常用的0.001,Momentum設定為0.9。
# 定義損失函數
loss_func = nn.CrossEntropyLoss()
loss_list = [] # 用來存儲損失值
# 定義優化器:第一個參數是模型的參數
SGD = optim.SGD(params=model.parameters(),lr=0.001,momentum=0.9)
3.4 訓練:
首先,指定訓練次數:
# 訓練指定次數,這裡寫為了3
for i in range(3):
xx
然後,疊代讀取資料:
for i in range(3):
loss_temp = 0 # 定義一個損失值,用來列印檢視
# 其中j是疊代次數,data和label都是批量的,每批32個
for j,(batch_data,batch_label) in enumerate(train_loader):
xxx
接着将資料放入GPU中:
for i in range(3):
loss_temp = 0
for j,(batch_data,batch_label) in enumerate(train_loader):
# 啟用GPU
batch_data,batch_label = batch_data.cuda(),batch_label.cuda()
然後,就是常見的操作:清空梯度、計算模型、計算損失、反向傳播、更新梯度
for i in range(3):
loss_temp = 0 # 定義一個損失值,用來列印檢視
# 其中j是疊代次數,data和label都是批量的,每批32個
for j,(batch_data,batch_label) in enumerate(train_loader):
# 啟用GPU
batch_data,batch_label = batch_data.cuda(),batch_label.cuda()
# 清空梯度
SGD.zero_grad()
# 模型訓練
prediction = model(batch_data)
# 計算損失
loss = loss_func(prediction,batch_label)
loss_temp += loss
# BP算法
loss.backward()
# 更新梯度
SGD.step()
最後,我們可以列印一下損失值來檢視模型訓練的怎麼樣了,這裡我們每隔兩百小批次就列印一次損失值:
# 訓練指定次數
for i in range(3):
loss_temp = 0 # 定義一個損失值,用來列印檢視
# 其中j是疊代次數,data和label都是批量的,每批32個
for j,(batch_data,batch_label) in enumerate(train_loader):
# 啟用GPU
batch_data,batch_label = batch_data.cuda(),batch_label.cuda()
# 清空梯度
SGD.zero_grad()
# 模型訓練
prediction = model(batch_data)
# 計算損失
loss = loss_func(prediction,batch_label)
loss_temp += loss
# BP算法
loss.backward()
# 更新梯度
SGD.step()
if (j + 1) % 200 == 0:
print('第%d次訓練,第%d批次,損失值: %.3f' % (i + 1, j + 1, loss_temp / 200))
loss_temp = 0
運作上面的代碼,顯示的結果為:
第1次訓練,第200批次,損失值: 2.298
第1次訓練,第400批次,損失值: 2.285
第1次訓練,第600批次,損失值: 2.241
第1次訓練,第800批次,損失值: 1.812
第1次訓練,第1000批次,損失值: 0.754
第1次訓練,第1200批次,損失值: 0.521
第1次訓練,第1400批次,損失值: 0.423
第1次訓練,第1600批次,損失值: 0.365
第1次訓練,第1800批次,損失值: 0.334
第2次訓練,第200批次,損失值: 0.284
第2次訓練,第400批次,損失值: 0.245
第2次訓練,第600批次,損失值: 0.235
第2次訓練,第800批次,損失值: 0.211
第2次訓練,第1000批次,損失值: 0.202
第2次訓練,第1200批次,損失值: 0.179
第2次訓練,第1400批次,損失值: 0.180
第2次訓練,第1600批次,損失值: 0.165
第2次訓練,第1800批次,損失值: 0.148
第3次訓練,第200批次,損失值: 0.147
第3次訓練,第400批次,損失值: 0.143
第3次訓練,第600批次,損失值: 0.136
第3次訓練,第800批次,損失值: 0.130
第3次訓練,第1000批次,損失值: 0.115
第3次訓練,第1200批次,損失值: 0.114
第3次訓練,第1400批次,損失值: 0.114
第3次訓練,第1600批次,損失值: 0.093
第3次訓練,第1800批次,損失值: 0.122
3.5 評估:
我們按照上面訓練的思路,可以輕松寫出測試的代碼:
correct = 0
for batch_data,batch_label in test_loader:
batch_data, batch_label = batch_data.cuda(), batch_label.cuda()
prediction = model(batch_data)
predicted = torch.max(prediction.data, 1)[1]
correct += (predicted == batch_label).sum()
print('準确率: %.2f %%' % (100 * correct / 10000)) # 因為總共10000個測試資料
當然上面的代碼第一次接觸還是有點疑問,主要的一點是
predicted = torch.max(prediction.data, 1)[1]
這段代碼在幹什麼。
首先,我們一個圖像傳入模型,輸出的是一個向量,這個向量10個值,代表數字0-9的概念值。而torch.max(x,1)表示按行(1:行,0:列)取出最大值,它傳回的是一個特殊格式的資料,第一個元素是各個最大值,第二個元素是其索引(在這裡就等價于0-9數字),是以使用[1]取出。
運作結果:
準确率: 91.25 %
# 這個是隻訓練一次的結果
3.6 探究:
下面将模型訓練1、2、3、4次的準确率結果:
# 1次 : 準确率: 91.25 %
# 2次 : 準确率: 93.44 %
# 3次 : 準确率: 97.19 %
# 4次 : 準确率: 97.58 %
另外,測試一下使用GPU與不使用GPU的時間差:
# 為了測試,我訓練10次,并且僅僅記錄訓練花費時間
# 使用GPU: 訓練花了: 124 s
# 不适用GPU:訓練花了: 160 s
上面GPU測試不嚴謹,因為首次調用GPU是需要花費時間的,但是從兩者差别看出,調用GPU确實效率很好。
4. 總結:
這次算是把整個流程從頭到尾跑了一遍,并簡單探究了訓練次數和調用GPU對訓練的影響。會了本篇文章的代碼,至少後面再實作CNN的圖像分類架構,應該還是比較簡單了。
完整代碼
# author: baiCai
# 導包
import time
import torch
from torch import nn
from torch import optim
from torch.utils.data import DataLoader
from torchvision.datasets import MNIST
import torchvision.transforms as transforms
from torchvision.models import AlexNet
# 建立模型
class LeNet(nn.Module):
def __init__(self):
super(LeNet,self).__init__()
# 定義模型
self.features = nn.Sequential(
nn.Conv2d(in_channels=1,out_channels=6,kernel_size=(5,5),stride=1,padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2,stride=2),
nn.Conv2d(in_channels=6,out_channels=16,kernel_size=(5,5),stride=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2,stride=2),
)
self.classifier = nn.Sequential(
nn.Linear(in_features=400, out_features=120),
nn.ReLU(),
nn.Linear(in_features=120, out_features=84),
nn.ReLU(),
nn.Linear(in_features=84, out_features=10)
)
def forward(self,x):
# 定義前向算法
x = self.features(x)
# print(x.shape)
x = torch.flatten(x,1)
# print(x.shape)
result = self.classifier(x)
return result
# 下載下傳資料集或者加載資料集
train_dataset = MNIST(root='../data/',train=True,transform=transforms.ToTensor(),download=True)
test_dataset = MNIST(root='../data/',train=False,transform=transforms.ToTensor())
# 加載資料: 分批次,每批256個資料
batch_size = 32
train_loader = DataLoader(dataset=train_dataset,batch_size=batch_size,shuffle=True)
test_loader = DataLoader(dataset=test_dataset,batch_size=batch_size,shuffle=False)
# start time
start_time = time.time()
# 建立模型
model = LeNet()
# 模型放入GPU中
# device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# model = model.to(device)
# 定義損失函數
loss_func = nn.CrossEntropyLoss()
loss_list = [] # 用來存儲損失值
# 定義優化器
SGD = optim.SGD(params=model.parameters(),lr=0.001,momentum=0.9)
# 訓練指定次數
for i in range(10):
loss_temp = 0 # 定義一個損失值,用來列印檢視
# 其中j是疊代次數,data和label都是批量的,每批32個
for j,(batch_data,batch_label) in enumerate(train_loader):
# 啟用GPU
# batch_data,batch_label = batch_data.cuda(),batch_label.cuda()
# 清空梯度
SGD.zero_grad()
# 模型訓練
prediction = model(batch_data)
# 計算損失
loss = loss_func(prediction,batch_label)
loss_temp += loss
# BP算法
loss.backward()
# 更新梯度
SGD.step()
if (j + 1) % 200 == 0:
print('第%d次訓練,第%d批次,損失值: %.3f' % (i + 1, j + 1, loss_temp / 200))
loss_temp = 0
# end_time
end_time = time.time()
print('訓練花了: %d s' % int((end_time-start_time)))
# 使用GPU: 訓練花了: 124 s
# 不适用GPU:訓練花了: 160 s
# 測試
correct = 0
for batch_data,batch_label in test_loader:
batch_data, batch_label = batch_data.cuda(), batch_label.cuda()
prediction = model(batch_data)
predicted = torch.max(prediction.data, 1)[1]
correct += (predicted == batch_label).sum()
print('準确率: %.2f %%' % (100 * correct / 10000)) # 因為總共10000個測試資料