Pytorch實戰(1)-Mnist手寫數據集 (作爲實戰入門)

主要是使用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')

八、總結

  • 在處理一個數據集時,首先要查看該數據集的結構,每張圖片的大小。
  • 設置好超參數,比如學習率、訓練次數等。
  • 瞭解採用網絡的整體框架,知道每一步的流程。
  • 要知道訓練模型與測試模型之間的區別。
  • 測試模型時,不需要網絡的後面兩層。(視情況而定)
  • 在後向傳播之前要記得梯度清零。
  • 熟悉所採用的損失函數,爲什麼要採用損失函數。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章