【Pytorch+torchvision】MNIST手寫(xiě)數(shù)字識(shí)別(代碼附最詳細(xì)注釋?zhuān)?/h2>

我想很多人入門(mén)深度學(xué)習(xí)可能都是從這個(gè)項(xiàng)目開(kāi)始的,相當(dāng)于是機(jī)器學(xué)習(xí)的Hello World。但我第一個(gè)深度學(xué)習(xí)項(xiàng)目是一年前跑的吳恩達(dá)的手指數(shù)字識(shí)別課后作業(yè),感興趣的讀者也可以試著跑一下,寫(xiě)者認(rèn)為看著機(jī)器學(xué)習(xí)的過(guò)程也是非常有意思的。本文代碼具有詳細(xì)注釋?zhuān)阌诘谝淮稳腴T(mén)深度學(xué)習(xí)的讀者學(xué)習(xí)。


深度學(xué)習(xí)
手寫(xiě)數(shù)字

在本文中,我們將在PyTorch中構(gòu)建一個(gè)簡(jiǎn)單的卷積神經(jīng)網(wǎng)絡(luò),并使用MNIST數(shù)據(jù)集訓(xùn)練它識(shí)別手寫(xiě)數(shù)字。 MNIST包含70,000張手寫(xiě)數(shù)字圖像: 60,000張用于培訓(xùn),10,000張用于測(cè)試。圖像是灰度(即通道數(shù)為1)28x28像素,并且居中的,以減少預(yù)處理和加快運(yùn)行。


PyTorch是一個(gè)非常流行的深度學(xué)習(xí)框架。但是與其他框架不同的是,PyTorch具有動(dòng)態(tài)執(zhí)行圖,意味著計(jì)算圖是動(dòng)態(tài)創(chuàng)建的。


深度學(xué)習(xí)

下面附上詳細(xì)代碼(讀者可以注意看注釋,其中詳細(xì)介紹了每一步,絕對(duì)是最細(xì)的注釋?zhuān)憧炊^(guò)程)
import torch
import torchvision
from torch.utils.data import DataLoader
import torch.nn as nn #torch.nn層中包含可訓(xùn)練的參數(shù)
import torch.nn.functional as F
import torch.optim as optim
import matplotlib.pyplot as plt
#注意下面兩行在matplotlib使用上出錯(cuò)時(shí),加上可不出錯(cuò)
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'

n_epochs = 3 #epoch的數(shù)量定義了將循環(huán)整個(gè)訓(xùn)練數(shù)據(jù)集的次數(shù)
batch_size_train = 64 #每次投喂的樣本數(shù)量
batch_size_test = 1000
learning_rate = 0.01
momentum = 0.5 #優(yōu)化器的超參數(shù)
log_interval = 10
random_seed = 1
torch.manual_seed(random_seed) #對(duì)于可重復(fù)的實(shí)驗(yàn),須為任何使用隨機(jī)數(shù)產(chǎn)生的東西設(shè)置隨機(jī)種子
#訓(xùn)練集數(shù)據(jù)
train_loader = torch.utils.data.DataLoader(
  torchvision.datasets.MNIST('./data/', train=True, download=True, #加載該數(shù)據(jù)集(download=True)
                             transform=torchvision.transforms.Compose([
                               torchvision.transforms.ToTensor(),
                               torchvision.transforms.Normalize(
                                 (0.1307,), (0.3081,))
                             ])), #Normalize()轉(zhuǎn)換使用的值0.1307和0.3081是該數(shù)據(jù)集的全局平均值和標(biāo)準(zhǔn)偏差,這里將它們作為給定值
  batch_size=batch_size_train, shuffle=True)
#測(cè)試集數(shù)據(jù)
test_loader = torch.utils.data.DataLoader(
  torchvision.datasets.MNIST('./data/', train=False, download=True,
                             transform=torchvision.transforms.Compose([
                               torchvision.transforms.ToTensor(),
                               torchvision.transforms.Normalize(
                                 (0.1307,), (0.3081,))
                             ])),
  batch_size=batch_size_test, shuffle=True) #使用size=1000對(duì)這個(gè)數(shù)據(jù)集進(jìn)行測(cè)試
#查看一批測(cè)試數(shù)據(jù)由什么組成
examples = enumerate(test_loader) #enumerate指循環(huán),類(lèi)似for
batch_idx, (example_data, example_targets) = next(examples) #example_targets是圖片實(shí)際對(duì)應(yīng)的數(shù)字標(biāo)簽,example_data是指圖片本身數(shù)據(jù)
print(example_targets)
print(example_data.shape) #輸出torch.Size([1000, 1, 28, 28]),意味著我們有1000個(gè)例子的28x28像素的灰度(即沒(méi)有rgb通道)

#定義卷積神經(jīng)網(wǎng)絡(luò)
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # batch*1*28*28(每次會(huì)送入batch個(gè)樣本,輸入通道數(shù)1(黑白圖像),圖像分辨率是28x28)
        # 下面的卷積層Conv2d的第一個(gè)參數(shù)指輸入通道數(shù),第二個(gè)參數(shù)指輸出通道數(shù)(即用了幾個(gè)卷積核),第三個(gè)參數(shù)指卷積核的大小
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5) #因?yàn)閳D像為黑白的,所以輸入通道為1,此時(shí)輸出數(shù)據(jù)大小變?yōu)?8-5+1=24.所以batchx1x28x28 -> batchx10x24x24
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5) #第一個(gè)卷積層的輸出通道數(shù)等于第二個(gè)卷積層是輸入通道數(shù)。
        self.conv2_drop = nn.Dropout2d() #在前向傳播時(shí),讓某個(gè)神經(jīng)元的激活值以一定的概率p停止工作,可以使模型泛化性更強(qiáng),因?yàn)樗粫?huì)太依賴(lài)某些局部的特征
        self.fc1 = nn.Linear(320, 50) #由于下部分前向傳播處理后,輸出數(shù)據(jù)為20x4x4=320,傳遞給全連接層。# 輸入通道數(shù)是320,輸出通道數(shù)是50
        self.fc2 = nn.Linear(50, 10)#輸入通道數(shù)是50,輸出通道數(shù)是10,(即10分類(lèi)(數(shù)字1-9),最后結(jié)果需要分類(lèi)為幾個(gè)就是幾個(gè)輸出通道數(shù))。全連接層(Linear):y=x乘A的轉(zhuǎn)置+b
    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2)) # batch*10*24*24 -> batch*10*12*12(2*2的池化層會(huì)減半,步長(zhǎng)為2)(激活函數(shù)ReLU不改變形狀)
        x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2)) #此時(shí)輸出數(shù)據(jù)大小變?yōu)?2-5+1=8(卷積核大小為5)(2*2的池化層會(huì)減半)。所以 batchx10x12x12 -> batchx20x4x4。
        x = x.view(-1, 320) # batch*20*4*4 -> batch*320
        x = F.relu(self.fc1(x)) #進(jìn)入全連接層
        x = F.dropout(x, training=self.training) #減少遇到過(guò)擬合問(wèn)題,dropout層是一個(gè)很好的規(guī)范模型。
        x = self.fc2(x)
        #計(jì)算log(softmax(x))
        return F.log_softmax(x)
#初始化網(wǎng)絡(luò)和優(yōu)化器
#如果我們使用GPU進(jìn)行訓(xùn)練,應(yīng)使用例如network.cuda()將網(wǎng)絡(luò)參數(shù)發(fā)送給GPU。將網(wǎng)絡(luò)參數(shù)傳遞給優(yōu)化器之前,將它們傳輸?shù)竭m當(dāng)?shù)脑O(shè)備很重要,否則優(yōu)化器無(wú)法以正確的方式跟蹤它們。
network = Net()
optimizer = optim.SGD(network.parameters(), lr=learning_rate,
                      momentum=momentum)
train_losses = []
train_counter = []
test_losses = []
test_counter = [i*len(train_loader.dataset) for i in range(n_epochs + 1)]
#每個(gè)epoch對(duì)所有訓(xùn)練數(shù)據(jù)進(jìn)行一次迭代。加載單獨(dú)批次由DataLoader處理
#訓(xùn)練函數(shù)
def train(epoch):
    network.train() #在訓(xùn)練模型時(shí)會(huì)在前面加上
    for batch_idx, (data, target) in enumerate(train_loader):
        optimizer.zero_grad() #使用optimizer.zero_grad()手動(dòng)將梯度設(shè)置為零,因?yàn)镻yTorch在默認(rèn)情況下會(huì)累積梯度
        output = network(data) #生成網(wǎng)絡(luò)的輸出(前向傳遞)
        loss = F.nll_loss(output, target) #計(jì)算輸出(output)與真值標(biāo)簽(target)之間的負(fù)對(duì)數(shù)概率損失
        loss.backward() #對(duì)損失反向傳播
        optimizer.step() #收集一組新的梯度,并使用optimizer.step()將其傳播回每個(gè)網(wǎng)絡(luò)參數(shù)
        if batch_idx % log_interval == 0: #log_interval=10,每10次投喂后輸出一次
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                       100. * batch_idx / len(train_loader), loss.item()))
            train_losses.append(loss.item()) #添加進(jìn)訓(xùn)練損失列表中
            train_counter.append(
                (batch_idx * 64) + ((epoch - 1) * len(train_loader.dataset)))
            #神經(jīng)網(wǎng)絡(luò)模塊以及優(yōu)化器能夠使用.state_dict()保存和加載它們的內(nèi)部狀態(tài)。這樣,如果需要,我們就可以繼續(xù)從以前保存的狀態(tài)dict中進(jìn)行訓(xùn)練——只需調(diào)用.load_state_dict(state_dict)。
            torch.save(network.state_dict(), './model.pth')
            torch.save(optimizer.state_dict(), './optimizer.pth')


train(1)

#測(cè)試函數(shù)。總結(jié)測(cè)試損失,并跟蹤正確分類(lèi)的數(shù)字來(lái)計(jì)算網(wǎng)絡(luò)的精度。
def test():
    network.eval() #在測(cè)試模型時(shí)在前面使用
    test_loss = 0
    correct = 0
    with torch.no_grad(): #使用上下文管理器no_grad(),我們可以避免將生成網(wǎng)絡(luò)輸出的計(jì)算結(jié)果存儲(chǔ)在計(jì)算圖(計(jì)算過(guò)程的構(gòu)建,以便梯度反向傳播等操作)中。(with是使用的意思)
        for data, target in test_loader:
            output = network(data) #生成網(wǎng)絡(luò)的輸出(前向傳遞)
            # 將一批的損失相加
            test_loss += F.nll_loss(output, target, size_average=False).item() #NLLLoss 的輸入是一個(gè)對(duì)數(shù)概率向量和一個(gè)目標(biāo)標(biāo)簽
            pred = output.data.max(1, keepdim=True)[1] ## 找到概率最大的下標(biāo)
            correct += pred.eq(target.data.view_as(pred)).sum() #預(yù)測(cè)正確的數(shù)量相加
    test_loss /= len(test_loader.dataset)
    test_losses.append(test_loss)
    print('\nTest set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset),
        100. * correct / len(test_loader.dataset)))

test()

#我們將在循環(huán)遍歷n_epochs之前手動(dòng)添加test()調(diào)用,以使用隨機(jī)初始化的參數(shù)來(lái)評(píng)估我們的模型。
for epoch in range(1, n_epochs + 1):
  train(epoch)
  test()

#評(píng)估模型的性能,畫(huà)損失曲線(xiàn)
fig = plt.figure()
plt.plot(train_counter, train_losses, color='blue')
plt.scatter(test_counter, test_losses, color='red')
plt.legend(['Train Loss', 'Test Loss'], loc='upper right')
plt.xlabel('number of training examples seen')
plt.ylabel('negative log likelihood loss')
plt.show()

#輸出自己找的測(cè)試圖片,比較模型的輸出。
examples = enumerate(test_loader)
batch_idx, (example_data, example_targets) = next(examples)
with torch.no_grad():
  output = network(example_data)
fig1 = plt.figure()
for i in range(6):
  plt.subplot(2,3,i+1)
  plt.tight_layout()
  plt.imshow(example_data[i][0], cmap='gray', interpolation='none')
  plt.title("Prediction: {}".format(
    output.data.max(1, keepdim=True)[1][i].item()))
  plt.xticks([])
  plt.yticks([])
plt.show()

#繼續(xù)對(duì)網(wǎng)絡(luò)進(jìn)行訓(xùn)練,并看看如何從第一次培訓(xùn)運(yùn)行時(shí)保存的state_dicts中繼續(xù)進(jìn)行訓(xùn)練。我們將初始化一組新的網(wǎng)絡(luò)和優(yōu)化器。
continued_network = Net()
continued_optimizer = optim.SGD(network.parameters(), lr=learning_rate,
                                momentum=momentum)

network_state_dict = torch.load('model.pth') #見(jiàn)左側(cè)項(xiàng)目列表,有該文件
continued_network.load_state_dict(network_state_dict) #使用.load_state_dict(),我們現(xiàn)在可以加載網(wǎng)絡(luò)的內(nèi)部狀態(tài),并在最后一次保存它們時(shí)優(yōu)化它們。
optimizer_state_dict = torch.load('optimizer.pth') #見(jiàn)左側(cè)項(xiàng)目列表,有該文件
continued_optimizer.load_state_dict(optimizer_state_dict)
#同樣,運(yùn)行一個(gè)訓(xùn)練循環(huán)應(yīng)該立即恢復(fù)我們之前的訓(xùn)練。為了檢查這一點(diǎn),我們只需使用與前面相同的列表來(lái)跟蹤損失值
for i in range(4,9):
  test_counter.append(i*len(train_loader.dataset))
  train(i)
  test()
#我們?cè)俅慰吹綔y(cè)試集的準(zhǔn)確性從一個(gè)epoch到另一個(gè)epoch有了(運(yùn)行更慢的,慢的多了)提高。
#輸出自己找的測(cè)試圖片,比較模型的輸出。
examples = enumerate(test_loader)
batch_idx, (example_data, example_targets) = next(examples)
with torch.no_grad():
  output = network(example_data)
fig1 = plt.figure()
for i in range(6):
  plt.subplot(2,3,i+1)
  plt.tight_layout()
  plt.imshow(example_data[i][0], cmap='gray', interpolation='none')
  plt.title("Prediction: {}".format(
    output.data.max(1, keepdim=True)[1][i].item()))
  plt.xticks([])
  plt.yticks([])
plt.show()


如果在matplotlib使用上出錯(cuò)時(shí),可加上

import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'

會(huì)使錯(cuò)誤消失,具體我也不知道是為什么,但是百度得到的解決方案,好用就是了。


下面附上運(yùn)行結(jié)果:

(1)訓(xùn)練曲線(xiàn),可以看到測(cè)試的損失在一點(diǎn)一點(diǎn)變小

訓(xùn)練曲線(xiàn)

(2)這是在epoch=3時(shí)的結(jié)果,可以看到準(zhǔn)確率已經(jīng)達(dá)到97%,識(shí)別手寫(xiě)數(shù)字已經(jīng)幾乎沒(méi)有問(wèn)題

準(zhǔn)確率97%
識(shí)別手寫(xiě)數(shù)字

(3)這是在epoch=8時(shí)的結(jié)果,可以看到準(zhǔn)確率已經(jīng)達(dá)到98%,識(shí)別手寫(xiě)數(shù)字變得更準(zhǔn)確

準(zhǔn)確率98%
識(shí)別手寫(xiě)數(shù)字

如果讀者仍可以繼續(xù)訓(xùn)練,可以再試試更高的epoch,不過(guò)要注意過(guò)擬合的問(wèn)題,希望本文對(duì)你有所幫助。


越努力,越幸運(yùn)

end~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容