十二、pytorch的基礎知識

鹤比纷恆红發表於2024-10-10

1、快捷命令

  十二、pytorch的基礎知識

2、Tensor

  Tensor是PyTorch中重要的資料結構,可認為是一個高維陣列。它可以是一個數(標量)、一維陣列(向量)、二維陣列(矩陣)以及更高維的陣列。Tensor和Numpy的ndarrays類似,但Tensor可以使用GPU進行加速。

  Tensor的基本使用:

from __future__ import print_function
import torch as t

(1)建立Tensor
   構建 5x3 矩陣,只是分配了空間,未初始化

x = t.Tensor(5, 3)
x = t.Tensor([[1,2],[3,4]])
##tensor([[1., 2.],
##        [3., 4.]])

(2)使用[0,1]均勻分佈隨機初始化二維陣列 

x = t.rand(5, 3)  
x
    ##tensor([[0.1595, 0.0289, 0.6098],
    ##        [0.3763, 0.2346, 0.9171],
    ##        [0.9731, 0.4014, 0.6734],
    ##        [0.2359, 0.8480, 0.5956],
    ##        [0.1340, 0.5178, 0.5605]])

(3)torch.Size 是tuple物件(元組)的子類,因此它支援tuple的所有操作,如x.size()[0]

print(x.size()) # 檢視x的形狀
x.size()[1], x.size(1) # 檢視列的個數, 兩種寫法等價
    ## torch.Size([5, 3])  (3,3)

(4)Tensor的運算 

y = t.rand(5, 3)
# 加法的第一種寫法
x + y
# 加法的第二種寫法
t.add(x, y)
# 加法的第三種寫法:指定加法結果的輸出目標為result
result = t.Tensor(5, 3) # 預先分配空間
t.add(x, y, out=result) # 輸入到result
result
    • 注意,函式名後面帶下劃線**_** 的函式會修改Tensor本身。例如,x.add_(y)和x.t_()會改變 x,但x.add(y)和x.t()返回一個新的Tensor, 而x不變。

(5)Tensor的選取操作與Numpy類似,Tensor和Numpy的陣列之間的互操作非常容易且快速。對於Tensor不支援的操作,可以先轉為Numpy陣列處理,之後再轉回Tensor

import numpy as np
a = np.ones(5)
b = t.from_numpy(a) # Numpy->Tensor
b.add_(1) # 以`_`結尾的函式會修改自身
print(a)
print(b) # Tensor和Numpy共享記憶體

# [2. 2. 2. 2. 2.]
# tensor([2., 2., 2., 2., 2.], dtype=torch.float64)
    • Tensor和numpy物件共享記憶體,所以他們之間的轉換很快,而且幾乎不會消耗什麼資源。但這也意味著,如果其中一個變了,另外一個也會隨之改變。

(6)如果你想獲取某一個元素的值,可以使用`scalar.item`。 直接`tensor[idx]`得到的還是一個tensor: 一個0-dim(零維,表示單個數值) 的tensor,一般稱為scalar.

import torch

# 建立一個 1D Tensor
tensor = torch.tensor([10, 20, 30, 40, 50])

# 使用索引獲取元素
index = 2
scalar_tensor = tensor[index]  # 這裡返回的是一個 0 維的 Tensor

print("獲取的標量 Tensor:", scalar_tensor)  # 輸出: tensor(30)

# 使用 item() 方法獲取 Python 原生數值
value = scalar_tensor.item()
print("獲取的具體值:", value)  # 輸出: 30

(7)需要注意的是,t.tensor()或者tensor.clone()總是會進行資料複製,新tensor和原來的資料不再共享記憶體。所以如果你想共享記憶體的話,建議使用torch.from_numpy()或者tensor.detach()來新建一個tensor, 二者共享記憶體。

tensor = t.tensor([3,4]) # 新建一個包含 3,4 兩個元素的tensor
scalar = t.tensor(3)

old_tensor = tensor
new_tensor = old_tensor.clone()
new_tensor[0] = 1111
old_tensor, new_tensor
# (tensor([3, 4]), tensor([1111,    4]))

new_tensor = old_tensor.detach()
new_tensor[0] = 1111
old_tensor, new_tensor
# (tensor([1111,    4]), tensor([1111,    4]))

(8)Tensor可透過.cuda 方法轉為GPU的Tensor,從而享受GPU帶來的加速運算。

# 在不支援CUDA的機器下,下一步還是在CPU上執行
device = t.device("cuda:0" if t.cuda.is_available() else "cpu")
x = x.to(device)
y = y.to(x.device) # 確保x和y在同一裝置上
z = x+y

3、autograd:自動微分

  深度學習的演算法本質上是透過反向傳播求導數,而PyTorch的**autograd**模組則實現了此功能。在Tensor上的所有操作,autograd都能為它們自動提供微分,避免了手動計算導數的複雜過程。要想使得Tensor使用autograd功能,只需要設定tensor.requries_grad=True.

# 為tensor設定 requires_grad 標識,代表著需要求導數
# pytorch 會自動呼叫autograd 記錄操作
x = t.ones(2, 2, requires_grad=True)

# 上一步等價於
# x = t.ones(2,2)
# x.requires_grad = True

y = x.sum() # tensor(4., grad_fn=<SumBackward0>)
y.grad_fn  # <SumBackward0 at 0x7f63e55b7810> 檢視 y 的梯度函式,可以看出 y 是透過 SumBackward0 計算得到的。
y.backward() # 反向傳播,計算梯度,由於 y 是透過 x 計算得出的,PyTorch 會自動計算出 x 的梯度。
# y = x.sum() = (x[0][0] + x[0][1] + x[1][0] + x[1][1])
# 因為 y 是透過對 x 的所有元素求和得到的,因此每個 x 元素對 y 的貢獻都是 1,梯度值為 1。
x.grad 
# tensor([[1., 1.],
#        [1., 1.]])

y.backward()
x.grad
# tensor([[2., 2.],
#         [2., 2.]])
## 注意:grad在反向傳播過程中是累加的(accumulated),這意味著每一次執行反向傳播,梯度都會累加之前的梯度,所以反向傳播之前需把梯度清零。

# 以下劃線結束的函式是inplace操作,會修改自身的值,就像add_
x.grad.data.zero_()
# tensor([[0., 0.],
#         [0., 0.]])
y.backward()
x.grad
# tensor([[1., 1.],
#         [1., 1.]])

4、神經網路

  Autograd實現了反向傳播功能,但是直接用來寫深度學習的程式碼在很多情況下還是稍顯複雜,torch.nn是專門為神經網路設計的模組化介面。nn構建於 Autograd之上,可用來定義和執行神經網路。nn.Module是nn中最重要的類,可把它看成是一個網路的封裝,包含網路各層定義以及forward方法,呼叫forward(input)方法,可返回前向傳播的結果。

  下面以LeNet為例,來看看如何用nn.Module實現。LeNet 這個網路雖然很小,但是它包含了深度學習的基本模組:卷積層,池化層,全連結層。是其他深度學習模型的基礎,

  (1)定義網路

  定義網路時,需要繼承nn.Module,並實現它的forward方法,把網路中具有可學習引數的層放在建構函式__init__中。如果某一層(如ReLU)不具有可學習的引數,則既可以放在建構函式中,也可以不放,但建議不放在其中,而在forward中使用nn.functional代替。

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

class Net(nn.Module):
    def __init__(self):
        # nn.Module子類的函式必須在建構函式中執行父類的建構函式
        # 下式等價於nn.Module.__init__(self)
        super(Net, self).__init__()
        
        # 卷積層 '1'表示輸入圖片為單通道, '6'表示輸出通道數,'5'表示卷積核為5*5
        self.conv1 = nn.Conv2d(1, 6, 5) 
        # 卷積層
        self.conv2 = nn.Conv2d(6, 16, 5) 
        # 仿射層/全連線層,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): 
        # 池化(啟用(卷積))
        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        x = F.max_pool2d(F.relu(self.conv2(x)), 2) 
        # reshape,‘-1’表示自適應
        x = x.view(x.size()[0], -1)  # 將多維張量展平為一維
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)        
        return x

net = Net()
print(net)

# Net(
#   (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)
# )

  只要在nn.Module的子類中定義了forward函式backward函式就會自動被實現(利用autograd)。在forward 函式中可使用任何tensor支援的函式,還可以使用if、for迴圈、print、log等Python語法,寫法和標準的Python寫法一致。

  網路的可學習引數透過net.parameters()返回,net.named_parameters可同時返回可學習的引數及名稱。

for name,parameters in net.named_parameters():
    print(name,':',parameters.size())
    
# conv1.weight : torch.Size([6, 1, 5, 5])
# conv1.bias : torch.Size([6])
# conv2.weight : torch.Size([16, 6, 5, 5])
# conv2.bias : torch.Size([16])
# fc1.weight : torch.Size([120, 400])
# fc1.bias : torch.Size([120])
# fc2.weight : torch.Size([84, 120])
# fc2.bias : torch.Size([84])
# fc3.weight : torch.Size([10, 84])
# fc3.bias : torch.Size([10])
# forward函式的輸入和輸出都是Tensor。
# 生成一個隨機張量(input),其尺寸為 (樣本數,通道數,高度,寬度)
input = t.randn(1, 1, 32, 32) 
# 將輸入資料 input 傳入網路 net,執行網路的前向傳播,得到輸出 out。
out = net(input)
# 檢視輸出尺寸
out.size() # torch.Size([1, 10])

net.zero_grad() # 所有引數的梯度清零
out.backward(t.ones(1,10)) # 反向傳播

  需要注意的是,torch.nn只支援mini-batches,不支援一次只輸入一個樣本,即一次必須是一個batch。但如果只想輸入一個樣本,則用 input.unsqueeze(0)將batch_size設為1。例如 nn.Conv2d 輸入必須是4維的,形如nSamples x nChannels x Height x Width.可將nSample設為1,即1 x nChannels x Height x Width.

(2)損失函式

  nn實現了神經網路中大多數的損失函式,例如nn.MSELoss用來計算均方誤差,nn.CrossEntropyLoss用來計算交叉熵損失

output = net(input)
# t.arange(0,10)建立一個從 0 到 9 的一維張量,.view(1, 10) 將一維張量的形狀變為 (1, 10)的二維張量,表示一個樣本的十個目標值。
target = t.arange(0,10).view(1,10).float() 
criterion = nn.MSELoss()
loss = criterion(output, target)
loss # loss是個標量scalar

# tensor(28.6152, grad_fn=<MseLossBackward>)

  如果對loss進行反向傳播溯源(使用gradfn屬性),可看到它的計算圖如下:

  十二、pytorch的基礎知識

  當呼叫loss.backward()時,該圖會動態生成並自動微分,也即會自動計算圖中引數(Parameter)的導數。

# 執行.backward,觀察呼叫之前和呼叫之後的grad
net.zero_grad() # 把net中所有可學習引數的梯度清零
print('反向傳播之前 conv1.bias的梯度')
print(net.conv1.bias.grad)
loss.backward() # PyTorch 會根據損失函式和模型的輸出,利用鏈式法則自動計算每個引數的梯度。
print('反向傳播之後 conv1.bias的梯度')
print(net.conv1.bias.grad)

# 反向傳播之前 conv1.bias的梯度
# tensor([0., 0., 0., 0., 0., 0.])
# 反向傳播之後 conv1.bias的梯度
# tensor([ 0.1366,  0.0885, -0.0036,  0.1410,  0.0144,  0.0562])

(3)最佳化器

  在反向傳播計算完所有引數的梯度後,還需要使用最佳化方法來更新網路的權重和引數,例如隨機梯度下降法(SGD)的更新策略如下:

  十二、pytorch的基礎知識

  torch.optim中實現了深度學習中絕大多數的最佳化方法,例如RMSProp、Adam、SGD等,更便於使用,因此大多數時候並不需要手動寫上述程式碼。

import torch.optim as optim
#新建一個最佳化器,指定要調整的引數和學習率
optimizer = optim.SGD(net.parameters(), lr = 0.01)

# 在訓練過程中
# 先梯度清零(與net.zero_grad()效果一樣)
optimizer.zero_grad() 

# 計算損失
output = net(input)
loss = criterion(output, target)

#反向傳播
loss.backward()

#更新引數
optimizer.step()

(4)資料載入與預處理

  在深度學習中資料載入及預處理是非常複雜繁瑣的,但PyTorch提供了一些可極大簡化和加快資料處理流程的工具。同時,對於常用的資料集,PyTorch也提供了封裝好的介面供使用者快速呼叫,這些資料集主要儲存在torchvison中。

  torchvision實現了常用的影像資料載入功能,例如Imagenet、CIFAR10、MNIST等,以及常用的資料轉換操作,這極大地方便了資料載入,並且程式碼具有可重用性。

5、CIFAR-10分類

  下面我們來嘗試實現對CIFAR-10資料集的分類,步驟如下:

1. 使用torchvision載入並預處理CIFAR-10資料集
2. 定義網路
3. 定義損失函式和最佳化器
4. 訓練網路並更新網路引數
5. 測試網路

(1)CIFAR-10資料載入及預處理

  CIFAR-10^3是一個常用的彩色圖片資料集,它有10個類別: 'airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck'。每張圖片都是3×32×32,也即3-通道彩色圖片,解析度為32×32。

import torchvision as tv
import torchvision.transforms as transforms
from torchvision.transforms import ToPILImage
show = ToPILImage() # 可以把Tensor轉成Image,方便視覺化

# 第一次執行程式torchvision會自動下載CIFAR-10資料集,
# 如果已經下載有CIFAR-10,可透過root引數指定

# 定義對資料的預處理
transform = transforms.Compose([
        transforms.ToTensor(), # 轉為Tensor
        transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), # 歸一化
                             ])

# 訓練集
trainset = tv.datasets.CIFAR10(
                    root='/home/cy/tmp/data/', 
                    train=True, 
                    download=True,
                    transform=transform)

trainloader = t.utils.data.DataLoader(
                    trainset, 
                    batch_size=4,
                    shuffle=True, 
                    num_workers=2)

# 測試集
testset = tv.datasets.CIFAR10(
                    '/home/cy/tmp/data/',
                    train=False, 
                    download=True, 
                    transform=transform)

testloader = t.utils.data.DataLoader(
                    testset,
                    batch_size=4, 
                    shuffle=False,
                    num_workers=2)

classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

  其中Dataset物件是一個資料集,可以按下標訪問,返回形如(data, label)的資料。

(data, label) = trainset[100]
print(classes[label])

# (data + 1) / 2是為了還原被歸一化的資料
show((data + 1) / 2).resize((100, 100))

  Dataloader是一個可迭代的物件,它將dataset返回的每一條資料拼接成一個batch,並提供多執行緒加速最佳化和資料打亂等操作。當程式對dataset的所有資料遍歷完一遍之後,相應的對Dataloader也完成了一次迭代。

dataiter = iter(trainloader) # 建立了一個迭代器 dataiter,可以用來逐步獲取資料。
images, labels = dataiter.next() # 返回4張圖片及標籤
print(' '.join('%11s'%classes[labels[j]] for j in range(4)))
show(tv.utils.make_grid((images+1)/2)).resize((400,100))

(2)定義網路

  複製上面的LeNet網路,修改self.conv1第一個引數為3通道,因CIFAR-10是3通道彩圖。

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

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5) 
        self.conv2 = nn.Conv2d(6, 16, 5)  
        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 = F.max_pool2d(F.relu(self.conv1(x)), (2, 2)) 
        x = F.max_pool2d(F.relu(self.conv2(x)), 2) 
        x = x.view(x.size()[0], -1) 
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)        
        return x

(3)定義損失函式和最佳化器

from torch import optim
criterion = nn.CrossEntropyLoss() # 交叉熵損失函式
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

(4)訓練網路

  所有網路的訓練流程都是類似的,不斷地執行如下流程:

    • 輸入資料
    • 前向傳播和反向傳播
    • 更新引數
t.set_num_threads(8) # 設定了執行緒數量為8,這對於多執行緒處理資料和計算是有益的
for epoch in range(2):  
    
    running_loss = 0.0
    for i, data in enumerate(trainloader, 0): # 使用enumerate函式遍歷trainloader中的每個mini-batch。
        
        # 輸入資料
        inputs, labels = data
        
        # 梯度清零
        optimizer.zero_grad()
        
        # forward + backward 
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()   
        
        # 更新引數 
        optimizer.step()
        
        # 列印log資訊
        # loss 是一個scalar,需要使用loss.item()來獲取數值,不能使用loss[0]
        running_loss += loss.item()
        if i % 2000 == 1999: # 每2000個batch列印一下訓練狀態
            print('[%d, %5d] loss: %.3f' \
                  % (epoch+1, i+1, running_loss / 2000))
            running_loss = 0.0
print('Finished Training')
correct = 0 # 預測正確的圖片數
total = 0 # 總共的圖片數


# 由於測試的時候不需要求導,可以暫時關閉autograd,提高速度,節約記憶體
with t.no_grad():
    for data in testloader:
        images, labels = data
        outputs = net(images)
        # 將神經網路輸出的每個樣本中最大值的索引儲存在 predicted 變數中
        _, predicted = t.max(outputs, 1) # _ 是一個佔位符,通常用於接收不需要的返回值(在這裡是最大值本身)。
        total += labels.size(0)
        correct += (predicted == labels).sum()

print('10000張測試集中的準確率為: %d %%' % (100 * correct / total))

(5)在GPU訓練

 就像之前把Tensor從CPU轉到GPU一樣,模型也可以類似地從CPU轉到GPU。

device = t.device("cuda:0" if t.cuda.is_available() else "cpu")

net.to(device)
images = images.to(device)
labels = labels.to(device)
output = net(images)
loss= criterion(output,labels)

loss