1、快捷命令
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
屬性),可看到它的計算圖如下:
當呼叫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)的更新策略如下:
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