神經網路中的Heloo,World,基於MINST資料集的LeNet

TrueDZ發表於2022-03-21

前言

最近剛開始接觸機器學習,記錄下目前的一些理解,以及看到的一些好文章mark一下

1.MINST資料集

MNIST 資料集來自美國國家標準與技術研究所, National Institute of Standards and Technology (NIST). 訓練集 (training set) 由來自 250 個人手寫的數字構成, 其中 50% 是高中學生, 50% 來自人口普查局 (the Census Bureau) 的工作人員. 測試集(test set) 也是同樣比例的手寫數字資料.

MINST手寫字型識別,大概相當於機器學習領域的"Hello World"的存在

1.1資料集的下載即載入

1.2可以去官網下載MNIST handwritten digit database, Yann LeCun, Corinna Cortes and Chris Burges

2.1.通過pytorch的dataloader直接下載

# 設定訓練資料集
data_train = torchvision.datasets.MNIST(
    root='./data',
    train=True,
    transform=torchvision.transforms.ToTensor(),
    download=True)
#將訓練資料集載入DataLoader
train_loader = torch.utils.data.DataLoader(data_train, batch_size=100, shuffle=True)
2.2.1torchvision.datasets

torchvision.datasets是pytorch中整合的可以載入多個資料集的工具,包含圖片分類,語義分割,視訊等資料集,詳情見

[Datasets — Torchvision 0.12 documentation (pytorch.org)](https://pytorch.org/vision/stable/datasets.html?highlight=utils data dataloader)進入該頁面我們找到MINST就可以檢視到使用MINST所需要的引數

也可以在pycharm中定位到這個程式碼,按Ctrl+P檢視需要的引數,我們重點關注這幾個

1.root指定資料集的路徑,2.train設定該資料是否用來訓練,3.download引數用來設定是否下載該資料集預設False

4.transform在pytorch封裝了許多影像處理的方法,如填充(Pad),Resize,CenterCrop(中心裁剪),Grayscale(灰度圖),更多方法見:Illustration of transforms — Torchvision 0.12 documentation (pytorch.org)

而這裡的ToTensor是常用的步驟,可以實現將PIL格式或者numpy.ndarray格式的輸入轉化為tensor資料型別

2.3data.DataLoader

DataLoader的意義就是做資料打包,引數含義重點關注這幾個

首先的1. dataset用來指定傳入的資料集,2. batch_size用來指定多少個資料為一組(如=64代表一次傳入64張圖片),太大可能爆視訊記憶體,3. shuffle用來設定是否隨機取樣(可以理解為圖片順序打亂?),4. num_workers代表任務數量(理解為執行緒數),5. drop_last設定是否丟棄最後的圖片(當最後幾張圖片不滿一個batch_size時丟棄)

3.MINST的內容的展示

(22條訊息) MNIST資料集轉為圖片形式輸出_zzZ_CMing的部落格-CSDN部落格_mnist圖片 這篇部落格有寫怎麼將MINST的資料儲存到本地

顯示幾個資料

#enumerate() 函式用於將一個可遍歷的資料物件(如列表、元組或字串)組合為一個索引序列,同時列出資料和資料下標
examples = enumerate(train_loader)
#next() 返回迭代器的下一個專案。
batch_idx, (example_data, example_targets) = next(examples)
# 繪製檢視資料集內容
fig = plt.figure()
for i in range(16):
  plt.subplot(4,4,i+1)  #設定影像顯示為2行3列
  plt.tight_layout()  #tight_layout會自動調整子圖引數,使之填充整個影像區域。
  plt.imshow(example_data[i][0], cmap='gray')
  plt.title("Truth Number: {}".format(example_targets[i]))
  plt.xticks([])    #不顯示橫縱座標
  plt.yticks([])
plt.show()

debug一下,發現影像的尺寸為[1000,1,28,28],分別代表batch_size為1000,chanel(通道)數為1(灰度圖),影像尺寸為28x28

2. MINST的訓練和測試方法

2.1定義訓練方法

def train(net, epoch=1):
    net.train()#設定training mode
    run_loss = 0#損失
    for num_epoch in range(epoch):	#設定訓練的輪數為epoch輪
        print(f'epoch {num_epoch}')
        for i, data in enumerate(train_loader):#i為索引,data為資料
#            x, y = data[0], data[1]	#也可以這麼寫
            x,y = data	#x,y分別代表資料和標註
            outputs = net(x) #將資料放入神經網路中
            loss = criterion(outputs, y) #計算損失
            optimizer.zero_grad()	#梯度清零
            loss.backward()	#反向傳播
            optimizer.step() #優化器更新引數
            run_loss += loss.item()
            if i % 100 == 99:	#每訓練100輪列印一次
                print(f'[{(i + 1) * 100} / 60000] loss={run_loss / 100}')
                run_loss = 0
                test(net)

這段程式碼初學者肯定比較懵逼,比如說我,下面簡單說幾個

net.train()的作用如下

可以看到,其實設不設定影響都不大,僅當你的模型中有使用Dropout()層或BatchNorm層時會有影響

為什麼要梯度清零

由於pytorch的動態計算圖,當我們使用loss.backward()和opimizer.step()進行梯度下降更新引數的時候,梯度並不會自動清零。並且這兩個操作是獨立操作。如果梯度不清零,pytorch中會將上次計算的梯度和本次計算的梯度累加。

有興趣可以看這裡(60 封私信 / 84 條訊息) PyTorch中在反向傳播前為什麼要手動將梯度清零? - 知乎 (zhihu.com)

損失函式

損失函式使用主要是在模型的訓練階段,每個批次的訓練資料送入模型後,通過前向傳播輸出預測值,然後損失函式會計算出預測值和真實值之間的差異值,也就是損失值。得到損失值之後,模型通過反向傳播去更新各個引數,來降低真實值與預測值之間的損失,使得模型生成的預測值往真實值方向靠攏,從而達到學習的目的。我們接下來使用的是nn.CrossEntropyLoss()

優化器

優化器或者優化演算法,是通過訓練優化引數,來最小化(最大化)損失函式。為了使模型輸出逼近或達到最優值,我們需要用各種優化策略和演算法,來更新和計算影響模型訓練和模型輸出的網路引數。接下來的我們使用的是Adam(自適應學習率優化演算法)。關於各種優化器的對比詳情見深度學習之——優化器 - 簡書 (jianshu.com)

這幾天看別人程式碼,感覺還是SGD(隨機梯度下降)用的比較多

2.2定義測試方法

def test(net):
    net.eval()#和上面的net.train()一樣
    test_loader = torch.utils.data.DataLoader(data_train, batch_size=10000, shuffle=False)
    test_data = next(iter(test_loader))
    with torch.no_grad():
#        x, y = test_data[0], test_data[1]
	    x,y = data
        outputs = net(x)
        pred = torch.max(outputs, 1)[1]
        print(f'test acc: {sum(pred == y) / outputs.shape[0]}')#計算出損失(每個之和除以數量)

3.定義神經網路

3.1 神經網路

什麼是卷積神經網路(CNN),具體可以看這篇文章:一文看懂卷積神經網路-CNN(基本原理+獨特價值+實際應用)- 產品經理的人工智慧學習庫 (easyai.tech)

人工神經網路(Artificial Neural Networks,ANN)是一種模擬生物神經系統的結構和行為,進行分散式並行資訊處理的演算法數學模型。ANN通過調整內部神經元與神經元之間的權重關係,從而達到處理資訊的目的。而卷積神經網路(Convolutional Neural Network,CNN)是一種前饋神經網路,它由若干卷積層和池化層組成,尤其在影像處理方面CNN的表現十分出色。

一般的卷積神經網路結果如下

3.1.1. 卷積層

在pytorch中卷積被封裝在nn.Conv2d()中,引數如下,後幾個我沒用過

關於卷積的輸出尺寸怎麼計算官網也有給出

光看這個直接給人看傻了,我們看點直觀的例子,如下左圖所示,下方灰色的3x3kernel是我們定義的卷積核,下方是我們輸入的影像,四周白色的方框代表我們的填充(padding=1),卷積核不斷在輸入影像上移動並計算,最終得到上方綠色的輸出影像

在這裡插入圖片描述

再舉個例子來求影像的輸出尺寸,如下有一個28x28的輸入,我們假設padding=2,步長為1,卷積核為5x5

則輸出的影像尺寸就應該為(28 - 2 * 2 - 1 * (5-1)-1/1)+1 = 28,所有輸出的尺寸也應該為28x28,即不變

3.1.2. 池化層

池化層分最大池化層和平均池化層,對應程式碼中的MaxPool2d,和AvgPool2d,池化層能很好的壓縮圖片資訊,而且保留一部分特徵

最大池化層的作用過程如下,輸入影像為5x5,我們用一個3x3的核對它做最大池化,每次都求出3x3中的最大值來得到結果,ceil_model為False是用來設定把不滿足3x3的結果都丟棄掉(類似之前談的drop_last)

平均池化也同理,只不過不是求出最大的而是求一個平均

3.1.3. 激勵層

啟用函式的作用

神經網路中使用啟用函式來加入非線性因素,提高模型的表達能力。

如果不用激勵函式(其實相當於激勵函式是f(x) = x),在這種情況下你每一層節點的輸入都是上層輸出的線性函式,很容易驗證,無論你神經網路有多少層,輸出都是輸入的線性組合,與沒有隱藏層效果相當,這種情況就是最原始的感知機(Perceptron)了,那麼網路的逼近能力就相當有限。正因為上面的原因,需要引入非線性函式作為激勵函式,這樣深層神經網路表達能力就更加強大(不再是輸入的線性組合,而是幾乎可以逼近任意函式)。

常用的非線性啟用函式有nn.ReLu()和nn.Sigmoid()他們的影像如下,可以看到ReLu在大於0時為x,小於0時為0


3.2 LeNet

關於LeNet的幾個版本介紹LeNet:第一個卷積神經網路 (ruanx.net)

 LeNet 是幾種神經網路的統稱,它們是 Yann LeCun 等人在 1990 年代開發的。一般認為,它們是最早的卷積神經網路(Convolutional Neural Networks, CNNs)。模型接收灰度影像,並輸出其中包含的手寫數字。LeNet 包含了以下三個模型:

  • LeNet-1:5 層模型,一個簡單的 CNN。
  • LeNet-4:6 層模型,是 LeNet-1 的改進版本。
  • LeNet-5:7 層模型,最著名的版本。

這裡我們使用LeNet5這個經典的神經網路,網路模型如下總共有7層

可以發現輸入影像是32x32的,但MINST資料集的影像尺寸為28x28,可以得出padding=2,第一個卷積層輸入chanel=1,輸出chanel=6,第一個池化層從28x28到14x14,推斷出kernel為2x2,按上圖模型編寫程式碼

class LeNet5(nn.Module):
    def __init__(self):
        super().__init__()
        #這個註釋的比較詳細
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, padding=2)
        # self.pool1 = nn.AvgPool2d(2) #本來LeNet使用的平均池化不如最大池化
        self.pool1 = nn.MaxPool2d(kernel_size=2)
        self.pool1 = nn.MaxPool2d(2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        # self.pool2 = nn.AvgPool2d(2)
        self.pool2 = nn.MaxPool2d(2)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)
#前向傳播
    def forward(self, x):
        # x = torch.tanh(self.conv1(x)) #tanh在這裡不如relu
        x = torch.relu(self.conv1(x))
        x = self.pool1(x)
        # x = torch.tanh(self.conv2(x))
        x = torch.relu(self.conv2(x))
        x = self.pool2(x)

        x = x.view(-1, 16 * 5 * 5)	#改變矩陣維度
        x = self.fc1(x)
        x = self.fc2(x)
        x = self.fc3(x)
        return x

在LeNet上訓練20輪後的acc最高可以達到99.86%可以說相當可以了,

在測試一下,在MINST資料集下的正確率確實很高了

但我自己手寫了幾個,發現正確率不怎麼樣,原因還不清楚是為什麼,難道是我字太醜了?。。。還是說這個模型過擬合了?。。。

還有個坑說一下,一定要轉灰度圖單通道,不然會莫名其妙報很多錯

4.完整程式碼

是cpu版本的就用cpu訓練,有gpu版本的就用gpu訓練,要快很多,但是要注意,你用的什麼裝置訓練出來的模型儲存再載入的時候要指明裝置。意思就是你不能用cpu訓練了一個模型,然後你再載入的時候又用cuda來加速

import torch
import torchvision
import torch.nn as nn
import torchvision.transforms as transforms
from torch.nn import ReLU
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt
from torch.optim import optimizer

from PIL import Image
# 用來記錄最佳的acc
best_acc = 0

trans_to_tensor = transforms.Compose([
    transforms.ToTensor()
])

data_train = torchvision.datasets.MNIST(
    root='./data',
    train=True,
    transform=torchvision.transforms.ToTensor(),
    download=True)

data_test = torchvision.datasets.MNIST(
    './data',
    train=False,
    transform=trans_to_tensor,
    download=True)

train_loader = torch.utils.data.DataLoader(data_train, batch_size=100, shuffle=True)

x, y = next(iter(train_loader))


#MINST的通用訓練方法
def test(net):
    global best_acc
    net.eval()
    test_loader = torch.utils.data.DataLoader(data_train, batch_size=10000, shuffle=False)
    test_data = next(iter(test_loader))
    with torch.no_grad():
        # x, y = test_data[0], test_data[1]
        # x = x.cuda()
        # y = y.cuda()
        x, y = test_data
        outputs = net(x)
        pred = torch.max(outputs, 1)[1]
        acc = sum(pred == y) / outputs.shape[0]
        if acc >  best_acc:
            best_acc = acc
        print(f'test acc: {acc}')
    # net.train()

def train(net, epoch=1):
    net.train()
    run_loss = 0
    for num_epoch in range(epoch):
        print(f'epoch {num_epoch}')
        for i, data in enumerate(train_loader):
            # x, y = data[0], data[1]
            x,y = data
            # print(data)
            outputs = net(x)
            loss = criterion(outputs, y)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            run_loss += loss.item()
            if i % 100 == 99:
                print(f'[{(i + 1) * 100} / 60000] loss={run_loss / 100}')
                run_loss = 0
                test(net)

# LeNet5網路模型
class LeNet5(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, padding=2)
        # self.pool1 = nn.AvgPool2d(2)
        self.pool1 = nn.MaxPool2d(kernel_size=2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        # self.pool2 = nn.AvgPool2d(2)
        self.pool2 = nn.MaxPool2d(2)
        self.fc1 = nn.Linear(in_features=16 * 5 * 5, out_features=120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        # x = torch.tanh(self.conv1(x))
        x = torch.relu(self.conv1(x))
        x = self.pool1(x)
        # x = torch.tanh(self.conv2(x))
        x = torch.relu(self.conv2(x))
        x = self.pool2(x)

        x = x.view(-1, 16 * 5 * 5)
        x = self.fc1(x)
        x = self.fc2(x)
        x = self.fc3(x)
        return x

if __name__ == '__main__':
    # res = []
    # classes = ['0','1','2','3','4','5','6','7','8','9']

    net_5 = LeNet5().cuda()
    criterion = nn.CrossEntropyLoss().cuda()
    optimizer = torch.optim.Adam(net_5.parameters())
    train(net_5, epoch=20)
    print(best_acc)
    '''
    @以下內容用來儲存自己的模型並做驗證
    
    torch.save(net_5.state_dict(),'LeNet5_par.pt')#儲存模型引數
    torch.save(net_5,'LeNet5_all.pt')#儲存整個模型
    
    model = torch.load('./LeNet5_all_cpu.pt',map_location=torch.device("cpu"))
    print(model)

    for i in range(0,7):
        image_path = f'./data/imgs/my_nums/{i}.jpg'
        img = Image.open(image_path).convert('L')
        transform = torchvision.transforms.Compose([torchvision.transforms.Resize([28,28]),
                                                    torchvision.transforms.ToTensor()])

        image = transform(img)
        image = torch.reshape(image, (-1, 1, 28, 28))
        model.eval()
        with torch.no_grad():
            output = model(image)
            prediction = torch.softmax(output, dim=1)
            prediction = np.argmax(prediction)
            plt.imshow(img)
            plt.show()
            res.append(prediction.item())
            print(prediction.item())
            print('----------')

    print(res)
    
    
    '''
    

相關文章