Pytorch-手寫MNIST數(shù)據(jù)集模型訓(xùn)練過(guò)程

1、MNIST數(shù)據(jù)集簡(jiǎn)介

  • 60000張圖片,包括10個(gè)類別,每個(gè)類別均為6000張,60000 = 10 * 6000;
  • 訓(xùn)練集50000張,測(cè)試集10000張;
  • 圖片大小均為 28 * 28,單通道灰度圖像(0~255);
from torchvision.datasets import mnist
import torchvision.transforms as transforms
import matplotlib.pyplot as plt

transforms = transforms.Compose([transforms.ToTensor(), transforms.Normalize([0.5], [0.5])])
train_dataset = mnist.MNIST('./data', train=True, transform=transforms, download=True)

print("Length of train_dataset: ", len(train_dataset))
print("Data shape of train_dataset: ", train_dataset.data.shape)
print("Target shape of train_dataset: ", train_dataset.targets.shape)

data, targets = train_dataset.data[:6], train_dataset.targets[:6]
fig = plt.figure(figsize=(5, 5))
for i in range(6):
    plt.subplot(2, 3, i + 1)  # i + 1是因?yàn)?,子圖從1開始繪制
    plt.tight_layout()  # 顯示看起來(lái)更緊湊,可以注釋掉該行再看效果
    plt.imshow(data[i], cmap="gray")  # 以灰度格式顯示,將cmap參數(shù)去掉,可能會(huì)看到彩色圖
    plt.title("target: {}".format(i))  # 顯示圖片上的字樣
    plt.xticks([])  # 去掉x坐標(biāo)軸
    plt.yticks([])  # 去掉y坐標(biāo)軸
Length of train_dataset:  60000
Data shape of train_dataset:  torch.Size([60000, 28, 28])
Target shape of train_dataset:  torch.Size([60000])

第1行,從torchvision.datasets中導(dǎo)入mnist模塊,torchvision.datasets模塊中還有CIFAR10數(shù)據(jù)集、COCO數(shù)據(jù)集等常用的數(shù)據(jù)集,都可以依據(jù)此方式導(dǎo)入。
第2行,導(dǎo)入torchvision.transforms模塊,該模塊用于數(shù)據(jù)集圖片的預(yù)處理或者數(shù)據(jù)增強(qiáng),例如減均值、圖片翻轉(zhuǎn)、隨機(jī)裁剪等操作。
第3行,導(dǎo)入matplotlib模塊,用于繪圖顯示。

第5行,transforms.Compose方法將多個(gè)變換組合到一起,存在列表中。transforms.ToTensor()方法把一個(gè)取值范圍是[0,255]的PIL.Image轉(zhuǎn)換成Tensor。形狀為(H,W,C)的Numpy.ndarray轉(zhuǎn)換成形狀為[C,H,W],取值范圍是[0,1.0]的torch.FloatTensor。transforms.Normalize()方法是圖片標(biāo)準(zhǔn)化,即減均值并除以標(biāo)準(zhǔn)差,將圖片的值規(guī)范化在[-1, 1]。
第6行,下載數(shù)據(jù)集,會(huì)將數(shù)據(jù)集下載在當(dāng)前文件夾下的data文件夾中。
第8-10行,顯示train_dataset的一些信息,包含60000張圖片,圖片大小28*28.
第12行,取數(shù)據(jù)集中的前6張圖片和對(duì)應(yīng)的標(biāo)簽。
第13-20行,繪制圖片。

2、加載數(shù)據(jù)集并制作可迭代對(duì)象

from torchvision.datasets import mnist
import torchvision.transforms as transforms
from torch.utils.data import DataLoader

# 超參數(shù)
train_batch_size = 128
test_batch_size = 64

# 數(shù)據(jù)集加載
transforms = transforms.Compose([transforms.ToTensor(), transforms.Normalize([0.5], [0.5])])
train_dataset = mnist.MNIST('./data', train=True, transform=transforms, download=True)
test_dataset = mnist.MNIST('./data', train=False, transform=transforms)

# 創(chuàng)建數(shù)據(jù)集迭代對(duì)象,但不是迭代器,可以使用for循環(huán),不能使用next
train_loader = DataLoader(train_dataset, batch_size=train_batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=test_batch_size, shuffle=False)

3、構(gòu)建網(wǎng)絡(luò)

from torch import nn
from torch.nn import functional as F

class Net(nn.Module):
    
    def __init__(self, in_dim, n_hidden1, n_hidden2, out_dim):
        super(Net, self).__init__()
        
        self.layer1 = nn.Sequential(nn.Linear(in_dim, n_hidden1), nn.BatchNorm1d(n_hidden1))
        self.layer2 = nn.Sequential(nn.Linear(n_hidden1, n_hidden2), nn.BatchNorm1d(n_hidden2))
        self.layer3 = nn.Sequential(nn.Linear(n_hidden2, out_dim))
    
    def forward(self, x):
        x = F.relu(self.layer1(x))
        x = F.relu(self.layer2(x))
        x = self.layer3(x)
        return x

model = Net(28 * 28, 300, 100, 10)
print(model)

在torch的神經(jīng)網(wǎng)絡(luò)nn工具箱中,有兩類搭建模型的工具,一種是基于類的,例如nn.Linear、nn.BatchNorm1d、nn.Conv2d等;一類是基于函數(shù)的,例如F.linear、F.batch_norm、F.conv2d。這兩大類在功能上相同的,但也有區(qū)別:

1)基于類的函數(shù)名首字母大寫;基于函數(shù)的全小寫或者加下劃線;
2)基于類的不需要專門初始化權(quán)重等,但基于函數(shù)的需要;
3)一般具體使用時(shí),具有學(xué)習(xí)參數(shù)的層(例如卷積層、全鏈接層等)使用基于類的方式(例如nn.Conv2d);沒有學(xué)習(xí)參數(shù)的層(例如激活層、池化層等)使用基于函數(shù)的方式(例如F.Relu())。

第1-2行,導(dǎo)入torch的nn模塊,以及nn.functional模塊;
第4行,在創(chuàng)建模型時(shí),一般都要繼承nn.Module模塊,這樣就可以接下來(lái)重寫第13行的forward函數(shù);
第7行,因?yàn)镹et繼承自nn.Module,所以需要增加該行;
第9行,使用nn.Sequential()函數(shù)來(lái)壘積木,包含了全連接層(nn.Linear)和BN層(nn.BatchNorm1d),這樣在14行使用self.layer1(x)時(shí),x就會(huì)先經(jīng)過(guò)nn.Linear層,nn.Linear層的輸出直接進(jìn)入nn.BatchNorm1d層中,最后該層的輸出作為self.layer1(x)的輸出值。nn.Sequential()可以連續(xù)這樣累積,前面層的輸出直接傳入后面緊鄰的層,需要注意的是只能添加基于類的層(nn.Conv2d),不能添加基于函數(shù)的層(F.conv2d);
第10-11行,與第9行同理‘
第14-15行,F(xiàn).relu是ReLU激活函數(shù);
第19行,創(chuàng)建模型,傳入必要參數(shù),即各層神經(jīng)元的個(gè)數(shù);
第20行,打印出的model如下:

Net(
  (layer1): Sequential(
    (0): Linear(in_features=784, out_features=300, bias=True)
    (1): BatchNorm1d(300, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  )
  (layer2): Sequential(
    (0): Linear(in_features=300, out_features=100, bias=True)
    (1): BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  )
  (layer3): Sequential(
    (0): Linear(in_features=100, out_features=10, bias=True)
  )
)

4、定義損失函數(shù)和優(yōu)化器

from torch import nn
import torch.optim as optim

# 超參數(shù)
learning_rate = 0.01
momentum = 0.5

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=momentum)

5、訓(xùn)練模型

# 超參數(shù)
num_epoches = 10

# 定義計(jì)算資源
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model.to(device)

# 訓(xùn)練過(guò)程
train_losses = []
train_accs = []

for epoch in range(num_epoches):
    train_loss = 0
    train_acc = 0
    for img, label in train_loader:
        img = img.to(device)    
        label = label.to(device)
        
        img = img.view(img.size(0), -1)
        
        # 前向傳播
        out = model(img)
        loss = criterion(out, label)
        # 反向傳播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        # 計(jì)算損失
        train_loss += loss.item()
        # 計(jì)算準(zhǔn)確率
        pred = out.argmax(dim=1)
        train_acc += (pred == label).sum().item() / img.size(0)
    train_loss = train_loss / len(train_loader)
    train_acc = train_acc / len(train_loader)
    train_losses.append(train_loss)
    train_accs.append(train_accs)   
    # 日志輸出
    print("Epoch: {}, Train loss: {}, Train acc: {}".format(epoch, train_loss, train_acc))

第5行,選擇計(jì)算資源,如果存在GPU資源,則將device選定為"cuda:0",否則為"cpu";
第6行、16行、17行,將模型、訓(xùn)練圖片和標(biāo)注全部放置到device上參與計(jì)算;
第9-10行,存儲(chǔ)每一個(gè)epoch的損失值和準(zhǔn)確率值;
第13-14行,在一個(gè)epoch中,累加所有batch的損失值和準(zhǔn)確率值,因?yàn)槲覀兦懊嬖O(shè)置train_batch_size=128,那么一個(gè)epoch中train_loader大概生成60000 / 128 = 469個(gè)batch,將這些batch的損失值和準(zhǔn)確率值累加;
第19行,將img變形,變形成model()可接受的參數(shù)形狀,img的原始形狀為(128, 1, 28, 28),通過(guò)view()變形成(128, 12828);
第22行,通過(guò)model計(jì)算出預(yù)測(cè)值,注意out的形狀為(128, 10),128表示128張圖片,10表示針對(duì)每一張圖片是10個(gè)類中各個(gè)類的預(yù)測(cè)值,例如取其中一個(gè)圖片的在10個(gè)類別上的預(yù)測(cè)值:

tensor([-3.2453,  1.4134, -2.7710, -3.4826,  7.9144,  0.1818, -0.3955, -2.4984,
        -0.2138,  0.1812], device='cuda:0', grad_fn=<SelectBackward>)

當(dāng)然你會(huì)發(fā)現(xiàn),這里不是0~1范圍內(nèi)的概率值,確實(shí)是這樣,實(shí)際上out中的這些值需要經(jīng)過(guò)softmax函數(shù)才會(huì)變成0~1范圍內(nèi)的概率值;而softmax函數(shù)以及之后的log函數(shù)集成到了后續(xù)的CrossEntropyLoss()函數(shù)(即criterion())中,希望這樣解釋,您不會(huì)感到困惑;
第23行,通過(guò)criterion函數(shù)計(jì)算損失,需要注意的是這一個(gè)batch中128個(gè)圖片只會(huì)得到一個(gè)損失值(torch中的一個(gè)標(biāo)量值),實(shí)際上是對(duì)128張圖片各自的損失求和并平均后的值;
第25-27行,反向傳播過(guò)程都是這么三步。首先優(yōu)化之前,先將梯度清零,因?yàn)閠orch中梯度會(huì)累加,所以每次更新權(quán)重前先將梯度清零;然后loss.backward()進(jìn)行反向傳播獲得各個(gè)權(quán)重值的梯度值;最后optimizer.step()將各個(gè)權(quán)重值進(jìn)行更新。
第30行,將所有的batch的損失值進(jìn)行累加,其中item()可以將torch中的標(biāo)量值變?yōu)槠胀ǖ臄?shù)字。
第32行,找出batch中各個(gè)圖片在10個(gè)類別中最匹配的類別,也就是說(shuō)模型將該圖片預(yù)測(cè)成該類別,argmax()可以找到最大值的索引,dim=1表示沿著列的方向找最大值。
第33行,計(jì)算準(zhǔn)確率,統(tǒng)計(jì)這128張圖片(一個(gè)batch大?。┲蓄A(yù)測(cè)對(duì)的個(gè)數(shù),然后除以128,這就是這個(gè)batch的準(zhǔn)確率。pred和label的形狀都是(128, 1),pred==label中,如果二者對(duì)應(yīng)的值相等則為True,否則為False,sum()可以統(tǒng)計(jì)出128個(gè)值中有多少個(gè)True,item()變成數(shù)字。img.size(0)是128,因?yàn)閕mg的size是[128, 1, 28, 28]。
第34-35行,len(train_loader)等于一個(gè)epoch中batch的個(gè)數(shù)。

6、完整代碼

from torchvision.datasets import mnist
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
from torch import nn
from torch.nn import functional as F
import torch
import torch.optim as optim

# 超參數(shù)
train_batch_size = 128
test_batch_size = 64
learning_rate = 0.01
momentum = 0.5
num_epoches = 10

# 數(shù)據(jù)集加載
transforms = transforms.Compose([transforms.ToTensor(), transforms.Normalize([0.5], [0.5])])
train_dataset = mnist.MNIST('./data', train=True, transform=transforms, download=True)
test_dataset = mnist.MNIST('./data', train=False, transform=transforms)

# 創(chuàng)建數(shù)據(jù)集迭代對(duì)象,但不是迭代器,可以使用for循環(huán),不能使用next
train_loader = DataLoader(train_dataset, batch_size=train_batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=test_batch_size, shuffle=False)

# 構(gòu)建模型
class Net(nn.Module):
    
    def __init__(self, in_dim, n_hidden1, n_hidden2, out_dim):
        super(Net, self).__init__()
        
        self.layer1 = nn.Sequential(nn.Linear(in_dim, n_hidden1), nn.BatchNorm1d(n_hidden1))
        self.layer2 = nn.Sequential(nn.Linear(n_hidden1, n_hidden2), nn.BatchNorm1d(n_hidden2))
        self.layer3 = nn.Sequential(nn.Linear(n_hidden2, out_dim))
    
    def forward(self, x):
        x = F.relu(self.layer1(x))
        x = F.relu(self.layer2(x))
        x = self.layer3(x)
        return x

# 實(shí)例化模型
model = Net(28 * 28, 300, 100, 10)
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model.to(device)

# 定義損失函數(shù)和優(yōu)化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=momentum)

# 訓(xùn)練過(guò)程
train_losses = []
train_accs = []

for epoch in range(num_epoches):
    train_loss = 0
    train_acc = 0
    for img, label in train_loader:
        img = img.to(device)    
        label = label.to(device)
        
        img = img.view(img.size(0), -1)
        
        # 前向傳播
        out = model(img)
        loss = criterion(out, label)
        # 反向傳播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        # 計(jì)算損失
        train_loss += loss.item()
        # 計(jì)算準(zhǔn)確率
        pred = out.argmax(dim=1)
        train_acc += (pred == label).sum().item() / img.size(0)
    train_loss = train_loss / len(train_loader)
    train_acc = train_acc / len(train_loader)
    train_losses.append(train_loss)
    train_accs.append(train_accs)   
    # 日志輸出
    print("Epoch: {}, Train loss: {:.4f}, Train acc: {:.4f}".format(epoch, train_loss, train_acc))

輸出結(jié)果

Epoch: 0, Train loss: 0.4735, Train acc: 0.8938
Epoch: 1, Train loss: 0.1742, Train acc: 0.9557
Epoch: 2, Train loss: 0.1194, Train acc: 0.9698
Epoch: 3, Train loss: 0.0893, Train acc: 0.9775
Epoch: 4, Train loss: 0.0714, Train acc: 0.9819
Epoch: 5, Train loss: 0.0578, Train acc: 0.9862
Epoch: 6, Train loss: 0.0477, Train acc: 0.9890
Epoch: 7, Train loss: 0.0399, Train acc: 0.9911
Epoch: 8, Train loss: 0.0337, Train acc: 0.9928
Epoch: 9, Train loss: 0.0280, Train acc: 0.9948
?著作權(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)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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