PyTorch零基礎入門之構建模型基礎
一、神經網絡的構造
- PyTorch中神經網絡構造一般是基於
Module
類的模型來完成的,它讓模型構造更加靈活。Module
類是nn
模塊里提供的一個模型構造類,是所有神經網絡模塊的基類,我們可以繼承它來定義我們想要的模型。 - 下面繼承
Module
類構造多層感知機。這里定義的 MLP 類重載了Module
類的init
函數和forward
函數。它們分別用於創建模型參數和定義前向計算。前向計算也即正向傳播。
# -*- coding: utf-8 -*- """ Created on Sat Oct 16 09:43:21 2021 @author: 86493 """ import torch from torch import nn class MLP(nn.Module): # 聲明帶有模型參數的層,此處聲明瞭2個全連接層 def __init__(self, **kwargs): # 調用MLP父類Block的構造函數來進行必要的初始化 # 這樣在構造實例時還可以指定其他函數 super(MLP, self).__init__(**kwargs) self.hidden = nn.Linear(784, 256) self.act = nn.ReLU() self.output = nn.Linear(256, 10) # 定義模型的前向計算 # 即如何根據輸入x計算返回所需要的模型輸出 def forward(self, x): o = self.act(self.hidden(x)) return self.output(o) X = torch.rand(2, 784) net = MLP() print(net) print('-' * 60) print(net(X))
結果為:
MLP(
(hidden): Linear(in_features=784, out_features=256, bias=True)
(act): ReLU()
(output): Linear(in_features=256, out_features=10, bias=True)
)
————————————————————
tensor([[ 0.1836, 0.1946, 0.0924, -0.1163, -0.2914, -0.1103, -0.0839, -0.1274,
0.1618, -0.0601],
[ 0.0738, 0.2369, 0.0225, -0.1514, -0.3787, -0.0551, -0.0836, -0.0496,
0.1481, 0.0139]], grad_fn=<AddmmBackward>)
註意:
(1)上面的MLP類不需要定義反向傳播函數,系統將通過自動求梯度而自動生成反向傳播所需的backward
函數。
(2)將數據X傳入實例化MLP類後得到的net對象,會做一次前向計算,並且net(X)
會調用MLP
類繼承自父類Module
的call
函數——該函數調用我們定義的子類MLP
的forward
函數完成前向傳播計算。
(3)這裡沒將Module類命名為Layer(層)或者Model(模型)等,是因為該類是一個可供自由組建的部件, 它的子類既可以是一個層(如繼承父類nn
的子類線性層Linear
),也可以是一個模型(如此處的子類MLP
),也可以是模型的一部分。
二、神經網絡中常見的層
有全連接層、卷積層、池化層與循環層等,下面學習使用Module
定義層。
2.1 不含模型參數的層
下面構造的 MyLayer 類通過繼承 Module 類自定義了一個將輸入減掉均值後輸出的層,並將層的計算定義在了 forward 函數里。這個層里不含模型參數。
# -*- coding: utf-8 -*- """ Created on Sat Oct 16 10:19:59 2021 @author: 86493 """ import torch from torch import nn class MyLayer(nn.Module): def __init__(self, **kwargs): # 調用父類的方法 super(MyLayer, self).__init__(**kwargs) def forward(self, x): return x - x.mean() # 測試,實例化該層,做前向計算 layer = MyLayer() layer1 = layer(torch.tensor([1, 2, 3, 4, 5], dtype = torch.float)) print(layer1)
結果為:
tensor([-2., -1., 0., 1., 2.])
2.2 含模型參數的層
可以自定義含模型參數的自定義層。其中的模型參數可以通過訓練學出。
Parameter
類其實是 Tensor
的子類,如果一個 Tensor
是 Parameter
,那麼它會自動被添加到模型的參數列表里。所以在自定義含模型參數的層時,我們應該將參數定義成 Parameter
,除瞭直接定義成 Parameter
類外,還可以使用 ParameterList
和 ParameterDict
分別定義參數的列表和字典。
PS:下面出現torch.mm
是將兩個矩陣相乘,如
# -*- coding: utf-8 -*- """ Created on Sat Oct 16 10:56:03 2021 @author: 86493 """ import torch a = torch.randn(2, 3) b = torch.randn(3, 2) print(torch.mm(a, b)) # 效果相同 print(torch.matmul(a, b)) #tensor([[1.8368, 0.4065], # [2.7972, 2.3096]]) #tensor([[1.8368, 0.4065], # [2.7972, 2.3096]])
(1)代碼栗子1
# -*- coding: utf-8 -*- """ Created on Sat Oct 16 10:33:04 2021 @author: 86493 """ import torch from torch import nn class MyListDense(nn.Module): def __init__(self): super(MyListDense, self).__init__() # 3個randn的意思 self.params = nn.ParameterList([nn.Parameter(torch.randn(4, 4)) for i in range(3)]) self.params.append(nn.Parameter(torch.randn(4, 1))) def forward(self, x): for i in range(len(self.params)): # mm是指矩陣相乘 x = torch.mm(x, self.params[i]) return x net = MyListDense() print(net)
打印得:
MyListDense(
(params): ParameterList(
(0): Parameter containing: [torch.FloatTensor of size 4×4]
(1): Parameter containing: [torch.FloatTensor of size 4×4]
(2): Parameter containing: [torch.FloatTensor of size 4×4]
(3): Parameter containing: [torch.FloatTensor of size 4×1]
)
)
(2)代碼栗子2
這回用變量字典:
# -*- coding: utf-8 -*- """ Created on Sat Oct 16 11:03:29 2021 @author: 86493 """ import torch from torch import nn class MyDictDense(nn.Module): def __init__(self): super(MyDictDense, self).__init__() self.params = nn.ParameterDict({ 'linear1': nn.Parameter(torch.randn(4, 4)), 'linear2': nn.Parameter(torch.randn(4, 1)) }) # 新增 self.params.update({'linear3': nn.Parameter(torch.randn(4, 2))}) def forward(self, x, choice = 'linear1'): return torch.mm(x, self.params[choice]) net = MyDictDense() print(net)
打印得:
MyDictDense(
(params): ParameterDict(
(linear1): Parameter containing: [torch.FloatTensor of size 4×4]
(linear2): Parameter containing: [torch.FloatTensor of size 4×1]
(linear3): Parameter containing: [torch.FloatTensor of size 4×2]
)
)
2.3 二維卷積層
二維卷積層將輸入和卷積核做互相關運算,並加上一個標量偏差來得到輸出。卷積層的模型參數包括了卷積核和標量偏差。在訓練模型的時候,通常我們先對卷積核隨機初始化,然後不斷迭代卷積核和偏差。
卷積窗口形狀為 p × q p \times q p×q 的卷積層稱為 p × q p \times q p×q 卷積層。同樣, p × q p \times q p×q 卷積或 p × q p \times q p×q 卷積核說明卷積核的高和寬分別為 p p p 和 q q q。
(1)填充可以增加輸出的高和寬。這常用來使輸出與輸入具有相同的高和寬。
(2)步幅可以減小輸出的高和寬,例如輸出的高和寬僅為輸⼊入的高和寬的 ( 為大於1的整數)。
# -*- coding: utf-8 -*- """ Created on Sat Oct 16 11:20:57 2021 @author: 86493 """ import torch from torch import nn # 卷積運算(二維互相關) def corr2d(X, K): h, w = K.shape X, K = X.float(), K.float() Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1)) for i in range(Y.shape[0]): for j in range(Y.shape[1]): Y[i, j] = (x[i: i + h, j: j + w] * K).sum() return Y # 二維卷積層 class Conv2D(nn.Module): def __init__(self, kernel_size): super(Conv2D, self).__init__() self.weight = nn.Parameter(torch.randn(kernel_size)) self.bias = nn.Parameter(torch.randn(1)) def forward(self, x): return corr2d(x, self.weight) + self.bias conv2d = nn.Conv2d(in_channels = 1, out_channels = 1, kernel_size = 3, padding = 1) print(conv2d)
得:
Conv2d(1, 1, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
填充(padding)是指在輸入高和寬的兩側填充元素(通常是0元素)。
下個栗子:創建一個高和寬為3的二維卷積層,設輸入高和寬兩側的填充數分別為1。給定一高和寬都為8的input,輸出的高和寬會也是8。
# -*- coding: utf-8 -*- """ Created on Sat Oct 16 11:54:29 2021 @author: 86493 """ import torch from torch import nn # 定義一個函數計算卷積層 # 對輸入和輸出左對應的升維和降維 def comp_conv2d(conv2d, X): # (1, 1)代表批量大小和通道數 X = X.view((1, 1) + X.shape) Y = conv2d(X) # 排除不關心的前2維:批量和通道 return Y.view(Y.shape[2:]) # 註意這裡是兩側分別填充1行或列,所以在兩側共填充2行或列 conv2d = nn.Conv2d(in_channels = 1, out_channels = 1, kernel_size = 3, padding = 1) X = torch.rand(8, 8) endshape = comp_conv2d(conv2d, X).shape print(endshape) # 使用高為5,寬為3的卷積核,在高和寬兩側填充數為2和1 conv2d = nn.Conv2d(in_channels = 1, out_channels = 1, kernel_size = (5, 3), padding = (2, 1)) endshape2 = comp_conv2d(conv2d, X).shape print(endshape2)
結果為:
torch.Size([8, 8])
torch.Size([8, 8])
stride
在二維互相關運算中,卷積窗口從輸入數組的最左上方開始,按從左往右、從上往下 的順序,依次在輸⼊數組上滑動。我們將每次滑動的行數和列數稱為步幅(stride)。
# 步幅stride conv2d = nn.Conv2d(in_channels = 1, out_channels = 1, kernel_size = (3, 5), padding = (0, 1), stride = (3, 4)) endshape3 = comp_conv2d(conv2d, X).shape print(endshape3) # torch.Size([2, 2])
2.4 池化層
- 池化層每次對輸入數據的一個固定形狀窗口(又稱池化窗口)中的元素計算輸出。不同於卷積層里計算輸入和核的互相關性,池化層直接計算池化窗口內元素的最大值或者平均值。該運算也 分別叫做最大池化或平均池化。
- 在二維最大池化中,池化窗口從輸入數組的最左上方開始,按從左往右、從上往下的順序,依次在輸入數組上滑動。當池化窗口滑動到某⼀位置時,窗口中的輸入子數組的最大值即輸出數組中相應位置的元素。
下面把池化層的前向計算實現在pool2d
函數裡。
最大池化:
# -*- coding: utf-8 -*- """ Created on Sat Oct 16 18:49:27 2021 @author: 86493 """ import torch from torch import nn def pool2d(x, pool_size, mode = 'max'): p_h, p_w = pool_size Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1)) for i in range(Y.shape[0]): for j in range(Y.shape[1]): if mode == 'max': Y[i, j] = X[i: i + p_h, j: j + p_w].max() elif mode == 'avg': Y[i, j] = X[i: i + p_h, j: j + p_w].mean() return Y X = torch.Tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]]) end = pool2d(X, (2, 2)) # 默認是最大池化 # end = pool2d(X, (2, 2), mode = 'avg') print(end)
tensor([[4., 5.], [7., 8.]])
平均池化:
# -*- coding: utf-8 -*- """ Created on Sat Oct 16 18:49:27 2021 @author: 86493 """ import torch from torch import nn def pool2d(x, pool_size, mode = 'max'): p_h, p_w = pool_size Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1)) for i in range(Y.shape[0]): for j in range(Y.shape[1]): if mode == 'max': Y[i, j] = X[i: i + p_h, j: j + p_w].max() elif mode == 'avg': Y[i, j] = X[i: i + p_h, j: j + p_w].mean() return Y X = torch.FloatTensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]]) # end = pool2d(X, (2, 2)) # 默認是最大池化 end = pool2d(X, (2, 2), mode = 'avg') print(end)
結果如下,註意上面如果mode是avg模式(平均池化)時,不能寫X = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
,否則會報錯Can only calculate the mean of floating types. Got Long instead.
。把tensor
改成Tensor
或FloatTensor
後就可以瞭(Tensor是FloatTensor的縮寫)。
tensor([[2., 3.],
[5., 6.]])
三、LeNet模型栗子
一個神經網絡的典型訓練過程如下:
1 定義包含一些可學習參數(或者叫權重)的神經網絡
2. 在輸入數據集上迭代
3. 通過網絡處理輸入
4. 計算 loss (輸出和正確答案的距離)
5. 將梯度反向傳播給網絡的參數
6. 更新網絡的權重,一般使用一個簡單的規則:weight = weight - learning_rate * gradient
# -*- coding: utf-8 -*- """ Created on Sat Oct 16 19:21:19 2021 @author: 86493 """ import torch import torch.nn as nn import torch.nn.functional as F class LeNet(nn.Module): # 需要把網絡中具有可學習參數的層放在構造函數__init__ def __init__(self): super(LeNet, self).__init__() # 輸入圖像channel:1;輸出channel:6 # 5*5卷積核 self.conv1 = nn.Conv2d(1, 6, 5) self.conv2 = nn.Conv2d(6, 16, 5) # an affine operation:y = Wx + b self.fc1 = nn.Linear(16 * 5 * 5, 120) self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, 10) def forward(self, x): # 2 * 2 最大池化 x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2)) # 如果是方陣,則可以隻使用一個數字進行定義 x = F.max_pool2d(F.relu(self.conv2(x)), 2) # 做一次flatten x = x.view(-1, self.num_flat_features(x)) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x def num_flat_features(self, x): # 除去批處理維度,得到其他所有維度 size = x.size()[1:] num_features = 1 # 將剛才得到的維度之間相乘起來 for s in size: num_features *= s return num_features net = LeNet() print(net) # 一個模型的可學習參數可以通過`net.parameters()`返回 params = list(net.parameters()) print("params的len:", len(params)) # print("params:\n", params) print(params[0].size()) # conv1的權重 print('-' * 60) # 隨機一個32×32的input input = torch.randn(1, 1, 32, 32) out = net(input) print("網絡的output為:", out) print('-' * 60) # 隨機梯度的反向傳播 net.zero_grad() # 清零所有參數的梯度緩存 end = out.backward(torch.randn(1, 10)) print(end) # None
print的結果為:
LeNet(
(conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
(conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
(fc1): Linear(in_features=400, out_features=120, bias=True)
(fc2): Linear(in_features=120, out_features=84, bias=True)
(fc3): Linear(in_features=84, out_features=10, bias=True)
)
params的len: 10
torch.Size([6, 1, 5, 5])
————————————————————
網絡的output為: tensor([[ 0.0904, 0.0866, 0.0851, -0.0176, 0.0198, 0.0530, 0.0815, 0.0284,
-0.0216, -0.0425]], grad_fn=<AddmmBackward>)
————————————————————
None
三點提醒:
(1)隻需要定義 forward
函數,backward
函數會在使用autograd
時自動定義,backward
函數用來計算導數。我們可以在 forward
函數中使用任何針對張量的操作和計算。
(2)在backward
前最好net.zero_grad()
,即清零所有參數的梯度緩存。
(3)torch.nn
隻支持小批量處理 (mini-batches)。整個 torch.nn
包隻支持小批量樣本的輸入,不支持單個樣本的輸入。比如,nn.Conv2d
接受一個4維的張量,即nSamples x nChannels x Height x Width
如果是一個單獨的樣本,隻需要使用input.unsqueeze(0)
來添加一個“假的”批大小維度。
torch.Tensor
:一個多維數組,支持諸如backward()
等的自動求導操作,同時也保存瞭張量的梯度。nn.Module
:神經網絡模塊。是一種方便封裝參數的方式,具有將參數移動到GPU、導出、加載等功能。nn.Parameter
:張量的一種,當它作為一個屬性分配給一個Module
時,它會被自動註冊為一個參數。autograd.Function
:實現瞭自動求導前向和反向傳播的定義,每個Tensor
至少創建一個Function
節點,該節點連接到創建Tensor
的函數並對其歷史進行編碼。
四、AlexNet模型栗子
# -*- coding: utf-8 -*- """ Created on Sat Oct 16 21:00:39 2021 @author: 86493 """ import torch from torch import nn class AlexNet(nn.Module): def __init__(self): super(AlexNet, self).__init__() self.conv = nn.Sequential( # in_channels,out_channels,kernel_size,stride,padding nn.Conv2d(1, 96, 11, 4), nn.ReLU(), # kernel_size, stride nn.MaxPool2d(3, 2), # 見笑卷積窗口,但使用padding=2來使輸入和輸出的高寬相同 # 且增大輸出通道數 nn.Conv2d(96, 256, 5, 1, 2), nn.ReLU(), nn.MaxPool2d(3, 2), # 連續3個卷積層,且後面使用更小的卷積窗口 # 除瞭最後的卷積層外,進一步增大瞭輸出 # 註:前2個卷積層後不使用池化層來減少輸入的高和寬 nn.Conv2d(256, 384, 3, 1, 1), nn.ReLU(), nn.Conv2d(384, 383, 3, 1, 1), nn.ReLU(), nn.Conv2d(384, 256, 3, 1, 1), nn.ReLU(), nn.MaxPool2d(3, 2) ) # 這裡的全連接層的輸出個數比LeNet中的大數倍。 # 使用丟棄層來緩解過擬合 self.fc = nn.Sequential( nn.Linear(256 *5 * 5, 4096), nn.ReLU(), nn.Dropout(0.5), nn.Linear(4096, 4086), nn.ReLU(), nn.Dropout(0.5), # 輸出層,下次會用到Fash-MNIST,所以此處類別設為10, # 而非論文中的1000 nn.Linear(4096, 10), ) def forward(self, img): feature = self.conv(img) output = self.fc(feature.view(img.shape[0], -1)) return output net = AlexNet() print(net)
可以看到該網絡的結構:
AlexNet( (conv): Sequential( (0): Conv2d(1, 96, kernel_size=(11, 11), stride=(4, 4)) (1): ReLU() (2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False) (3): Conv2d(96, 256, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2)) (4): ReLU() (5): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False) (6): Conv2d(256, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (7): ReLU() (8): Conv2d(384, 383, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (9): ReLU() (10): Conv2d(384, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (11): ReLU() (12): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False) ) (fc): Sequential( (0): Linear(in_features=6400, out_features=4096, bias=True) (1): ReLU() (2): Dropout(p=0.5, inplace=False) (3): Linear(in_features=4096, out_features=4086, bias=True) (4): ReLU() (5): Dropout(p=0.5, inplace=False) (6): Linear(in_features=4096, out_features=10, bias=True) ) )
Reference
(1)官方文檔:https://pytorch.org/docs/stable/_modules/torch/nn/modules/conv.html#Conv2d
(2)datawhale notebook
到此這篇關於PyTorch零基礎入門之構建模型基礎的文章就介紹到這瞭,更多相關PyTorch 構建模型基礎內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Pytorch深度學習之實現病蟲害圖像分類
- 淺談Pytorch 定義的網絡結構層能否重復使用
- Python LeNet網絡詳解及pytorch實現
- pytorch查看網絡參數顯存占用量等操作
- Pytorch 統計模型參數量的操作 param.numel()