主要參考學習資料:
《動手學深度學習》阿斯頓·張 等 著
【動手學深度學習 PyTorch版】嗶哩嗶哩@跟李牧學AI
概述
- 為了實現更復雜的網絡,我們需要研究比層更高一級的單元塊,在編程中由類表示。通過自定義層和塊,我們能更靈活地搭建網絡。
- 我們有多種方法可以訪問、初始化和綁定模型參數。
- 為了加載和保存權重向量和整個模型,我們需要進行文件讀寫。
- 將計算設備指定為GPU而不是CPU,可以更高效地完成深度學習的計算任務。
目錄
- 4.1 層和塊
- 4.1.1 自定義塊
- 4.1.2 順序塊
- 4.2 參數管理
- 4.2.1 參數訪問
- 1.目標參數
- 2.一次性訪問所有參數
- 3.從嵌套塊收集參數
- 4.2.2 參數初始化
- 1.內置初始化
- 2.自定義初始化
- 4.2.3 參數綁定
- 4.3 自定義層
- 4.3.1 不帶參數的層
- 4.3.2 帶參數的層
- 4.4 讀寫文件
- 4.4.1 加載和保存張量
- 4.4.2 加載和保存模型參數
- 4.5 GPU
- 4.5.1 計算設備
- 4.5.2 張量與GPU
- 4.5.3 神經網絡與GPU
4.1 層和塊
事實證明,研究討論“比單個層大”但“比整個模型小”的組件更有價值。計算機視覺中廣泛流行的ResNet-152架構有數百層,這些層是由層組的重復模式組成。為了實現更復雜的網絡,我們引入神經網絡塊的概念。
塊可以描述單個層、由多個層組成的組件或整個模型本身。使用塊的好處是可以將一些塊組成更大的組件,這一過程通常是遞歸的。
在編程中,塊由類表示,類的任何子類都必須定義一個將其輸入轉換為輸出的前向傳播函數,并且必須存儲任何必需的參數。為了計算梯度,塊必須具有反向傳播函數。在自定義塊時,由于自動微分提供了一些后端實現,我們只需要考慮前向傳播函數和必須的參數。
4.1.1 自定義塊
塊必須提供的基本功能:
- 將輸入數據作為其前向傳播函數的參數。
- 通過前向傳播函數來生成輸出。
- 計算其輸出關于輸入的梯度,可通過其反向傳播函數進行訪問,通常是自動完成的。
- 存儲和訪問前向傳播計算所需的參數。
- 根據需要初始化模型參數。
import torch
from torch import nn
#functional模塊提供一些函數化的神經網絡操作,可直接應用于張量
from torch.nn import functional as F #以Module為父類定義MLP類
class MLP(nn.Module): def __init__(self): #初始化繼承自Module的數據屬性super().__init__() #實例化兩個全連接層self.hidden = nn.Linear(20, 256) self.out = nn.Linear(256, 10) #定義模型的前向傳播,根據輸入X返回模型輸出def forward(self, X): return self.out(F.relu(self.hidden(X)))
下面這個例子展示了自定義塊的靈活性:
class FixedHiddenMLP(nn.Module)def __init__(self):super().__init__()#設置不隨梯度更新的固定的權重,即常量參數self.rand_weight = torch.rand((20, 20), requires_grad=False)self.linear = nn.Linear(20, 20)#在前向傳播函數中執行自定義代碼def forward(self, X):X = self.linear(X)X = F.relu(torch.mm(X, self.rand_weight) + 1)#復用全連接層相當于這兩個全連接層共享參數X = self.linear(X)#控制流while X.abs().sum() > 1:X /= 2return X.sum()
4.1.2 順序塊
Sequential類的設計目的是將其他模塊串起來。構建自己的簡化的MySequential只需定義如下函數:
- 將塊逐個追加到列表中的函數。
- 前向傳播函數,用于將輸入按追加塊的順序傳遞給塊組成的“鏈條”。
class MySequential(nn.Module): #args傳入任意數量的模塊def __init__(self, *args): super().__init__() #遍歷模塊并為每個模塊分配一個字符串鍵存入_modules#_modules的變量類型是OrderedDictfor idx, module in enumerate(args): self._modules[str(idx)] = module def forward(self, X): #OrderedDict保證按照成員添加的順序遍歷它們for block in self._modules.values(): X = block(X) return X
4.2 參數管理
4.2.1 參數訪問
import torch
from torch import nnnet = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
X = torch.rand(size=(2, 4))
通過索引訪問模型的任意層,state_dict返回所訪問層的參數:
print(net[2].state_dict())
OrderedDict([('weight', tensor([[ 0.2889, -0.2738, -0.3096, -0.3151, 0.2594, 0.1743, -0.2288, -0.1173]])), ('bias', tensor([0.0921]))])
1.目標參數
每個參數都表示為參數類的一個實例:
print(type(net[2].bias))
<class 'torch.nn.parameter.Parameter'>
參數是復合的對象,如下訪問參數實例,并進一步訪問該參數的值(張量類型)和梯度:
print(net[2].bias)
print(net[2].bias.data)
net[2].weight.grad == True
Parameter containing:
tensor([0.0921], requires_grad=True)
tensor([0.0921])
False
另一種訪問網絡參數的方式:
net.state_dict()['2.bias'].data
tensor([0.0921])
2.一次性訪問所有參數
訪問一個全連接層的參數:
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
('weight', torch.Size([8, 4])) ('bias', torch.Size([8]))
訪問所有層的參數:
print(*[(name, param.shape) for name, param in net.named_parameters()])
('0.weight', torch.Size([8, 4])) ('0.bias', torch.Size([8])) ('2.weight', torch.Size([1, 8])) ('2.bias', torch.Size([1]))
3.從嵌套塊收集參數
定義生成塊的函數:
def block1():return nn.Sequential(nn.Linear(4, 8), nn.ReLU(),nn.Linear(8, 4), nn.ReLU())def block2():net = nn.Sequential()for i in range(4):net.add_module(f'block {i}', block1())return net
設計一個嵌套塊的網絡,并觀察其架構:
rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
print(rgnet)
Sequential((0): Sequential((block 0): Sequential((0): Linear(in_features=4, out_features=8, bias=True)(1): ReLU()(2): Linear(in_features=8, out_features=4, bias=True)(3): ReLU())(block 1): Sequential((0): Linear(in_features=4, out_features=8, bias=True)(1): ReLU()(2): Linear(in_features=8, out_features=4, bias=True)(3): ReLU())(block 2): Sequential((0): Linear(in_features=4, out_features=8, bias=True)(1): ReLU()(2): Linear(in_features=8, out_features=4, bias=True)(3): ReLU())(block 3): Sequential((0): Linear(in_features=4, out_features=8, bias=True)(1): ReLU()(2): Linear(in_features=8, out_features=4, bias=True)(3): ReLU()))(1): Linear(in_features=4, out_features=1, bias=True)
)
因為層是分層嵌套的,因此可以像通過嵌套列表索引一樣訪問層。下面訪問第一個主要的塊中第二個子塊的第一層的偏置:
rgnet[0][1][0].bias.data
tensor([ 0.1969, -0.0747, -0.1520, 0.0474, -0.2245, -0.2320, -0.2794, 0.3429])
4.2.2 參數初始化
1.內置初始化
normal_將權重參數初始化為標準差為0.01的高斯隨機變量,zeros_將偏置參數設置為0:
def init_normal(m):if type(m) == nn.Linear:nn.init.normal_(m.weight, mean=0, std=0.01)nn.init.zeros_(m.bias)
constant_將所有參數初始化為給定的常量:
def init_constant(m):if type(m) == nn.Linear:nn.init.constant_(m.weight, 1)nn.init.zeros_(m.bias)
xavier_uniform_為上一章中介紹的Xavier均勻分布初始化:
def init_xavier(m):if type(m) == nn.Linear:nn.init.xavier_uniform_(m.weight)
2.自定義初始化
以如下分布為例為任意權重參數 w w w定義初始化方法:
w ~ { U ( 5 , 10 ) , 可能性為 1 4 0 , 可能性為 1 2 U ( ? 10 , ? 5 ) , 可能性為 1 4 w\sim\left\{\begin{matrix}U(5,10),&\displaystyle可能性為\frac14\\0,&\displaystyle可能性為\frac12\\U(-10,-5),&\displaystyle可能性為\frac14\end{matrix}\right. w~? ? ??U(5,10),0,U(?10,?5),?可能性為41?可能性為21?可能性為41??
def my_init(m):if type(m) == nn.Linear:#以(-10, 10)為邊界的均勻分布nn.init.uniform_(m.weight, -10, 10)#將絕對值小于5的權重清零m.weight.data *= m.weight.data.abs() >= 5
4.2.3 參數綁定
定義一個層,并使用這個層的參數來設置另一個層的參數可以實現參數共享:
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),shared, nn.ReLU,shared, nn.ReLU,nn.Linear(8, 1))
改變shared的參數也會同步改變net中兩個對應層的參數。
4.3 自定義層
4.3.1 不帶參數的層
構建一個從其輸入中減去均值的層:
import torch
import torch.nn.functional as F
from torch import nnclass CenteredLayer(nn.Module):def __init__(self):super().__init__()def forward(self, X):return X - X.mean()
將自定義層作為組件合并到更復雜的模型中:
net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())
該自定義層沒有指定輸入維度,但框架會在數據第一次通過模型傳遞時動態地推斷出每個層的大小,即延遲初始化。
4.3.2 帶參數的層
下面是自定義版本的全連接層:
class MyLinear(nn.Module):#傳入輸入數和輸出數作為參數def __init__(self, in_units, units):super().__init__()#通過Parameter實例創建參數self.weight = nn.Parameter(torch.randn(in_units, units))self.bias = nn.Parameter(torch.randn(units,))def forward(self, X):linear = torch.matmul(X, self.weight.data) + self.bias.datareturn F.relu(linear)
4.4 讀寫文件
4.4.1 加載和保存張量
張量通過load和save函數保存到內存并分別讀寫它們。
import torch
from torch import nn
from torch.nn import functional as F#單個張量
x = torch.arange(4)
torch.save(x, 'x-file')
x2 = torch.load('x-file')#張量列表
y = torch.zeros(4)
torch.save([x, y], 'x-files')
x2, y2 = torch.load('x-files')#從字符串映射到張量的字典
mydict = {'x': x, 'y': y}
torch.save(mydict, 'mydict')
mydict2 = torch.load('mydict')
4.4.2 加載和保存模型參數
深度學習框架提供了內置函數來保存和加載整個網絡,但這只保存模型的參數而不是模型,因此架構需要自己生成。先定義一個多層感知機模型:
class MLP(nn.Module):def __init__(self):super().__init__()self.hidden = nn.Linear(20, 256)self.output = nn.Linear(256, 10)def forward(self, X):return self.output(F.relu(self.hidden(X)))net = MLP()
將模型參數存儲在文件mlp.params中:
torch.save(net.state_dict(), 'mlp.params')
實例化原多層感知機模型的一個備份,并通過直接讀取文件中存儲的參數來恢復模型:
clone = MLP()
clone.load_state_dict(torch.load('mlp.params'))
4.5 GPU
GPU具有強大的并行處理能力、浮點運算速度和內存帶寬,在處理人工智能這樣的復雜計算任務上相較于CPU有無可比擬的優勢。首先確保至少安裝了一個NVIDIA GPU,然后下載NVIDIA驅動和CUDA,最后使用nvidia-smi命令查看顯卡信息:
!nvidia-smi
在pytorch中,每個數組都有一個設備,我們通常稱其為環境。默認情況下,所有變量和相關的計算都分配給CPU,有時環境可能是GPU。
4.5.1 計算設備
我們可以指定用于存儲和計算的設備。在pytorch中,CPU和GPU可以使用torch.device(‘cpu’)和torch.device(‘cuda’))表示。如果有多個GPU,我們使用torch.device(f’cuda:{i}')來表示第i塊GPU(i從0開始),以及cuda:0和cuda等價。
import torch
from torch import nntorch.device('cpu'), torch.device('cuda'), torch.device('cuda:0')
(device(type='cpu'), device(type='cuda'), device(type='cuda', index=0))
查詢可用GPU的數量:
torch.cuda.device_count()
1
定義兩個函數以允許在不存在所需GPU的情況下執行代碼:
def try_gpu(i=0):"""如果存在,返回第i個GPU,否則返回CPU"""if torch.cuda.device_count() >= i + 1:return torch.device(f'cuda:{i}')return torch.device('cpu')def try_all_gpus():"""返回所有可用的GPU,如果沒有GPU則返回CPU"""devices = [torch.device(f'cuda:{i}') for i in range(torch.cuda.device_count())]return devices if devices else [torch.device('cpu')]try_gpu(), try_gpu(10), try_all_gpus()
(device(type='cuda', index=0),device(type='cpu'),[device(type='cuda', index=0)])
4.5.2 張量與GPU
默認情況下,張量是在內存中創建并使用CPU進行計算的。
x = torch.tensor([1, 2, 3])
x.device
device(type='cpu')
在創建張量時指定存儲設備來在GPU上存儲張量:
X = torch.ones(2, 3, device=try_gpu())
X
tensor([[1., 1., 1.],[1., 1., 1.]], device='cuda:0')
假設我們有兩個GPU(博主沒有所以移用示例代碼),如下在第二個GPU上創建一個隨機張量:
Y = torch.rand(2, 3, device=try_gpu(1))
Y
tensor([[0.2506, 0.4556, 0.9745],[0.2359, 0.6094, 0.0410]] device='cuda:1')
如果要計算X+Y,我們需要決定在哪里執行這個操作。張量的計算只能在同一設備上運行,因此要將X復制到第二個GPU才能執行:
Z = X.cuda(1)
print(X)
print(Z)
tensor([[1., 1., 1.],[1., 1., 1.]], device='cuda:0')
tensor([[1., 1., 1.],[1., 1., 1.]], device='cuda:1')
現在可以在同一個GPU上將其相加:
Y + Z
tensor([[1.2506, 1.4556, 1.9745],[1.2359, 1.6094, 1.0410]] device='cuda:1')
4.5.3 神經網絡與GPU
類似地,神經網絡模型可以指定設備,將模型參數放在GPU上:
net = nn.Sequential(nn.Linear(3, 1))
net = net.to(device=try_gpu())