主要是使用LeNet進行手寫數字識別。
一、介紹LeNet網絡
- 結構圖
- 詳細展開
它的輸入尺寸是32×32。
- 重點介紹一下卷積層、池化層和全連接層
卷積層:通過卷積運算,可以使原信號特徵增強,並且降低噪音。
# ------------------------------------------------------------------
解析代碼:nn.Conv2d(1, 6, 5, stride=1, padding=2)
第一個參數:輸入圖片的維數(灰度爲1,彩色圖像爲3)
第二個參數:使用5*5大小的過濾器6個
第三個參數:卷積核(過濾器)大小爲5*5
第四個參數:stride爲每次移動卷積核的步長爲1
第五個參數:padding爲填充
如上代碼, 對於mnist內的圖片大小爲28*28,而LeNet網絡的輸入大小爲32*32
根據padding的大小,將mnist填充爲32*32
經過卷積層(32 - 5 + stride)輸出的特徵圖大小爲28*28*6
# ----------------------------------------------------------------
池化層(下采樣層):利用圖像局部相關性的原理,對圖像進行子抽樣,可以1.減少數據處理量同時保留有用信息,2.降低網絡訓練參數及模型的過擬合程度
# ------------------------------------------------------------------
解析代碼:nn.ReLU(True)
nn.MaxPool2d(2, 2)
激活後池化,使用2*2大小的過濾器,步長爲2,padding=0.
如上代碼,輸入28*28*6大小的特徵圖,池化後,輸出14*14*6大小的特徵圖
# ----------------------------------------------------------------
全連接層:
# ------------------------------------------------------------------
解析代碼:nn.Linear(400, 120)
每個單元與上一次的全部5*5*16(400)個單元直接進行全連接。120*(400+1)=48120個可訓練參數。如同經典神經網絡,全連接層層計算輸入向量和權重向量之間的點積,再加上一個偏置。
# ----------------------------------------------------------------
輸出層:
# ------------------------------------------------------------------
輸出層由歐式徑向基函數(Euclidean Radial Basis Function)單元組成,每類一個單元,每個有84個輸入。
換句話說,每個輸出RBF單元計算輸入向量和參數向量之間的歐式距離。
輸入離參數向量越遠,RBF輸出的越大。
用概率術語來說,RBF輸出可以被理解爲F6層配置空間的高斯分佈的負log-likelihood。
給定一個輸式,損失函數應能使得F6的配置與RBF參數向量(即模式的期望分類)足夠接近。
# ----------------------------------------------------------------
- LeNet網絡的特點
1)、每個卷積層包含三部分:卷積、池化和非線性激活函數
2)、使用卷積提取空間特徵
3)、降採樣(Subsample)的平均池化層
4)、雙曲正切(tanh)和S型(Sigmoid)的激活函數,MLP作爲最後的分類器
5)、層與層之間的稀疏連接減少計算複雜度
二、加載數據集
在這之前先看一下數據預處理:
直接看https://pytorch.org/docs/stable/torchvision/transforms.html#
from torch.utils.data import DataLoader
from torchvision import transforms
from torchvision import datasets
# 下載訓練集 MNIST 手寫數字訓練集
train_dataset = datasets.MNIST(
root='./mnist', train=True, transform=transforms.ToTensor(), download=True)
test_dataset = datasets.MNIST(
root='./mnist', train=False, transform=transforms.ToTensor())
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
# -------------------------------------------------------------------------------
torchvision.datasets.MNIST(
root,
train = True,
transform = None,
target_transform = None,
download = False )
root(string):保存數據集的根目錄
train(bool):如果爲true,則從training.pt中創建數據集,否則從test.pt中創建數據集
transform(可調用的):一個方法或者transform,它接受一個PIL圖像並返回transform,例如
transforms.RandomCrop
download(bool):如果true,則從網上下載數據集並放在根目錄下,如果已經下載了就不會再次下載。
target_transform :接受目標並對其進行轉換的函數/轉換
# -----------------------------------------------------------------------------------
DataLoader(
dataset, # 數據集
batch_size=1, # int,每個批次要加載的樣本數,默認爲1
shuffle=False, # bool,設置爲true,則每個epoch,reshuffle數據,默認爲false
sampler=None, # sample,定義數據集中提取樣本的策略,如果指定,則shuffle必須爲false
batch_sampler=None, # 類似sampler,但是一次返回一批索引。互斥有batch_size,shuffle,
# sampler,和drop_last。
num_workers=0, # int,用於數據加載的子進程數。0表示將在主進程中加載數據,默認值爲0
collate_fn=None, # 合併樣本列表以形成張量的小批量
pin_memory=False,
drop_last=False,
timeout=0,
worker_init_fn=None)
# --------------------------------------------------------------------------------------
三、定義LeNet網絡模型
# 定義LeNet網絡
class Cnn(nn.Module):
# nn.Moudle子類的函數必須在構造函數中執行父類的構造函數
# 下式等價與nn.Moudle.__init__(self)
def __init__(self, in_dim, n_class):
super(Cnn, self).__init__()
# super用法:Cnn繼承父類nn.Model的屬性,並用父類的方法初始化這些屬性
self.conv = nn.Sequential(
# padding=2保證輸入輸出尺寸相同(參數依次是:輸入維度,輸出深度,ksize,步長,填充)
# 卷積層‘1’表示輸入圖片爲單通道,‘6’表示輸出通道數
# ‘5’表示卷積核爲5*5
nn.Conv2d(in_dim, 6, 5, stride=1, padding=2),
nn.ReLU(True),
nn.MaxPool2d(2, 2),
nn.Conv2d(6, 16, 5, stride=1, padding=0),
nn.ReLU(True),
nn.MaxPool2d(2, 2))
# 全連接層
self.fc = nn.Sequential(
nn.Linear(400, 120),
nn.Linear(120, 84),
nn.Linear(84, n_class))
# 前向傳播
def forward(self, x):
out = self.conv(x)
# print(out.size()):torch.Size([128, 16, 5, 5])
# 而經過卷積池化後的特徵圖大小爲5*5*16
# 但是全連接層的輸入爲400,所有要將out的大小改爲400
# 使用view
out = out.view(out.size(0), -1)
# 此時out的size爲400,由於批次爲128,所有128*400
out = self.fc(out)
# 返回out的size爲128*10
return out
# 實例化LeNet網絡
model = Cnn(1, 10) # 圖片大小爲28*28,圖片維度爲1,最終輸出的是10類
四、定義損失函數和優化器
criterion = nn.CrossEntropyLoss() # 交叉熵損失
# 隨機梯度下降法SGD,指定要調整的參數和學習率
# learning_rate = 1e-2 # 學習率
optimizer = optim.SGD(model.parameters(), lr=learning_rate)
五、訓練階段
num_epoches = 20 # 遍歷訓練集的次數
for epoch in range(num_epoches):
running_loss = 0.0 # 運行損失
running_acc = 0.0 # 精度
# ----------------------------------------------
# 對enumerate(iterator, start)的解釋:
# numerate()用於將可迭代、可遍歷的數據對象組合爲一個索引序列,同時列出數據和數據下標。
# e2 = enumerate(list, 4)
# for i in e2:
# print(i)
# 輸出結果:
# (4, 'A')
# (5, 'B')
# (6, 'C')
# (7, 'D')
# ----------------------------------------------
# 遍歷訓練集的每一張圖片
for i, data in enumerate(train_loader, 1):
img, label = data
#-------------------------------
# cuda
#if use_gpu:
#img = img.cuda()
#label = label.cuda()
# ------------------------------
# 將img和label轉換爲Variable
img = Variable(img)
label = Variable(label)
# 前向傳播
out = model(img)
# 計算損失
loss = criterion(out, label) # 計算交叉熵損失
running_loss += loss.item() * label.size(0) #
_, pred = torch.max(out, 1) # 預測最大值所在位置的標籤,即預測的數字。維度設爲1
num_correct = (pred == label).sum() # 預測正確的數目
accuracy = (pred == label).float().mean()
running_acc += num_correct.item() # 統計預測正確的總數
# 反向傳播
# grad在反向傳播過程中是累加的,這意味着每次運行反向傳播,梯度都會累加之前的梯度,
# 所以反向傳播之前需把梯度清零。
optimizer.zero_grad()
loss.backward()
optimizer.step() # 更新參數
print('Finish {} epoch, Loss: {:.6f}, Acc: {:.6f}'.format(
epoch + 1, running_loss / (len(train_dataset)), running_acc / (len(train_dataset))))
六、測試階段
# model.train() :啓用 BatchNormalization 和 Dropout
# model.eval() :不啓用 BatchNormalization 和 Dropout
# 訓練階段需要啓用,而測試階段不需要。
model.eval() # 模型評估
eval_loss = 0 # 評估損失
eval_acc = 0 # 評估精度
for data in test_loader:
img, label = data
# -------------------------------------------------
# if use_gpu:
# img = Variable(img, volatile=True).cuda()
# label = Variable(label, volatile=True).cuda()
# else:
# img = Variable(img, volatile=True)
# label = Variable(label, volatile=True)
# --------------------------------------------------
# 開始測試圖片
out = model(img)
loss = criterion(out, label)
eval_loss += loss.item() * label.size(0)
_, pred = torch.max(out, 1) # 預測最大值所在位置的標籤,即預測的數字。維度設爲1
num_correct = (pred == label).sum() # 預測正確的數目
eval_acc += num_correct.item() # 統計預測正確的數目
print('Test Loss: {:.6f}, Acc: {:.6f}'.format(eval_loss / (len(
test_dataset)), eval_acc / (len(test_dataset))))
七、保存和加載模型
- 保存模型
保存整個模型的結構和參數,保存對象爲model
torch.save(model,’./model.pth’)
保存對象的參數,保存的對象是模型的狀態model.state_dict()
torch.save(model.state_dict(),’./model_state.pth)
# 保存模型
# 將狀態爲state_dict()的模型保存在當前目錄下,命名爲cnn.pth
torch.save(model.state_dict(), './cnn.pth')
- 加載模型
加載模型結構和參數
load_model=torch.load('model.pth')
加載模型參數信息,需要先導入模型結構,然後通過model.load_state_dic(torch.load(‘model_state.pth’))導入
# 加載模型
load_model = torch.load('cnn.pth')
八、總結
- 在處理一個數據集時,首先要查看該數據集的結構,每張圖片的大小。
- 設置好超參數,比如學習率、訓練次數等。
- 瞭解採用網絡的整體框架,知道每一步的流程。
- 要知道訓練模型與測試模型之間的區別。
- 測試模型時,不需要網絡的後面兩層。(視情況而定)
- 在後向傳播之前要記得梯度清零。
- 熟悉所採用的損失函數,爲什麼要採用損失函數。