# Chapter 6 :Builders’ Guide :::success [回深度學習目錄](https://hackmd.io/UiomJi7URTKHv3S3hJmlYg#Chapter-6-:Builders%E2%80%99-Guide) 報告:讀書會 2024/04/11 編輯:2024/04/08 ::: :::info 對應中文版第5章 ::: [TOC] ## 6.1 Layers and Modules 介紹神經網絡模(Modules)的概念。一個模塊可以描述單個層(layer)、由多個層組成的組件,或者整個模型本身. ![image](https://hackmd.io/_uploads/By9Qf5geA.png) 一個Modules 可以視為一個class,一個Modeule須包含: - 接收輸入數據作為其前向傳播方法的參數。 - 通過前向傳播方法返回一個值來生成輸出。 - 計算其輸出相對於其輸入的梯度,可以通過其反向傳播方法訪問。通常情況下,這是自動完成的。 - 存儲並提供執行前向傳播計算所需的參數。 - 根據需要初始化模型參數。 為了計算梯度,一個Module需包含定義Backward propogation function 的定義,不過在因為有**自動微分**的關係所以只需要定義Forwad和參數即可. ```python= import torch from torch import nn from torch.nn import functional as F net = nn.Sequential(nn.LazyLinear(256), nn.ReLU(), nn.LazyLinear(10)) X = torch.rand(2, 20) net(X).shape ``` ``` torch.Size([2, 10]) ``` 此段程式碼中*net*包含一個隱藏層,接受一個任意維度的輸入,輸出成256維,經過Relu函數後,到下一個輸出層,並且輸出成10維. ### 6.1.1 A Custom Module ```python=+ class MLP(nn.Module): def __init__(self): # Call the constructor of the parent class nn.Module to perform # the necessary initialization super().__init__() self.hidden = nn.LazyLinear(256) self.out = nn.LazyLinear(10) # Define the forward propagation of the model, that is, how to return the # required model output based on the input X def forward(self, X): return self.out(F.relu(self.hidden(X))) net = MLP() net(X).shape ``` ``` torch.Size([2, 10]) ``` 此code透過自定義Class的方式完成與上面的Code相同的內容,``super().__init__()``中進行``nn.Module``父類別的初始化.並且定義**forward**函數,描述怎麼產生輸出. 接著定義如何把多個Module串在一起的**Sequential Module** ```python=+ class MySequential(nn.Module): def __init__(self, *args): super().__init__() for idx, module in enumerate(args): self.add_module(str(idx), module) def forward(self, X): for module in self.children(): X = module(X) return X net = MySequential(nn.LazyLinear(256), nn.ReLU(), nn.LazyLinear(10)) net(X).shape ``` ``` torch.Size([2, 10]) ``` 有時我們不只是要再層間傳遞內容,我們需要做額外的數學運算,例如加入一個不隨模型訓練更新的常數$c$,計算$f(\mathbf{x},\mathbf{w}) = c \cdot \mathbf{w}^\top \mathbf{x}$,這裡$w$是隨訓練改變的權重. ```python=+ class FixedHiddenMLP(nn.Module): def __init__(self): super().__init__() # Random weight parameters that will not compute gradients and # therefore keep constant during training self.rand_weight = torch.rand((20, 20)) #中文版有加上 requires_grad=False 不過我查預設就是False了 self.linear = nn.LazyLinear(20) def forward(self, X): X = self.linear(X) X = F.relu(X @ self.rand_weight + 1) # Reuse the fully connected layer. This is equivalent to sharing # parameters with two fully connected layers X = self.linear(X) # Control flow while X.abs().sum() > 1: X /= 2 return X.sum() net = FixedHiddenMLP() net(X) ``` ``` tensor(-0.3836, grad_fn=<SumBackward0>) ``` 這邊做這個操作只是用來表示可以把每個Modules設計的很彈性,並沒有提及訓練上的意義.最後用一個程式碼把此章節的東西全部串起來. ```python=+ class NestMLP(nn.Module): def __init__(self): super().__init__() self.net = nn.Sequential(nn.LazyLinear(64), nn.ReLU(), nn.LazyLinear(32), nn.ReLU()) self.linear = nn.LazyLinear(16) def forward(self, X): return self.linear(self.net(X)) chimera = nn.Sequential(NestMLP(), nn.LazyLinear(20), FixedHiddenMLP()) chimera(X) ``` ``` tensor(0.0679, grad_fn=<SumBackward0>) ``` ## 6.2 Parameter Management 每次我們選定一組超參數之後,就會進行以最小化損失函數為目標的訓練,而透過訓練會得到參數,因此需要能儲存並提取參數的功能. 本章介紹 - 用於調試、診斷和可視化的參數訪問。 - 在不同的模型組件之間共享參數。 ```python=+ import torch from torch import nn net = nn.Sequential(nn.LazyLinear(8), nn.ReLU(), nn.LazyLinear(1)) X = torch.rand(size=(2, 4)) net(X).shape ``` ``` torch.Size([2, 1]) ``` 上述程式碼包含了一個隱藏層輸出為8維,經過Relu後到一個輸出為1維的輸出層. ```python=+ net[2].state_dict() ``` ``` OrderedDict([('weight', tensor([[-0.1649, 0.0605, 0.1694, -0.2524, 0.3526, -0.3414, -0.2322, 0.0822]])), ('bias', tensor([0.0709]))]) ``` 透過將輸出層叫出來可以發現,他有8個權重分別對應到上一層的8個輸出,並且加上一項常數項. ```python=+ type(net[2].bias), net[2].bias.data ``` ``` (torch.nn.parameter.Parameter, tensor([0.0709])) ``` 可以各別查詢所需的參數 ```python=+ net[2].weight.grad == None ``` ``` True ``` 因為還沒錯backward Propogation的緣故,因此還沒計算gradiant,故gradiant仍不存在. ```python=+ [(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]))] ``` 透過for迴圈一次查看所有參數的種類及維度. ```python=+ # We need to give the shared layer a name so that we can refer to its # parameters shared = nn.LazyLinear(8) net = nn.Sequential(nn.LazyLinear(8), nn.ReLU(), shared, nn.ReLU(), shared, nn.ReLU(), nn.LazyLinear(1)) net(X) # Check whether the parameters are the same print(net[2].weight.data[0] == net[4].weight.data[0]) net[2].weight.data[0, 0] = 100 # Make sure that they are actually the same object rather than just having the # same value print(net[2].weight.data[0] == net[4].weight.data[0]) ``` ``` tensor([True, True, True, True, True, True, True, True]) tensor([True, True, True, True, True, True, True, True]) ``` 若我們希望在不同層間共用參數,可以透過上述的程式碼實現. ## 6.3 Parameter Initialization 此子章節介紹將參數進行初始化的方式.之前都是使用預設的方式進行初始化,這裡介紹幾個客製化的方式. ```python=+ import torch from torch import nn net = nn.Sequential(nn.LazyLinear(8), nn.ReLU(), nn.LazyLinear(1)) X = torch.rand(size=(2, 4)) net(X).shape ``` ``` torch.Size([2, 1]) ``` 上述程式碼包含了一個隱藏層輸出為8維,經過Relu後到一個輸出為1維的輸出層. ```python=+ def init_normal(module): if type(module) == nn.Linear: nn.init.normal_(module.weight, mean=0, std=0.01) nn.init.zeros_(module.bias) net.apply(init_normal) net[0].weight.data[0], net[0].bias.data[0] ``` ``` (tensor([-0.0129, -0.0007, -0.0033, 0.0276]), tensor(0.)) ``` 此段程式碼定義了一個使用$N(0,0.01)$初始化權重並加上0作為bias的函數. ```python=+ def init_constant(module): if type(module) == nn.Linear: nn.init.constant_(module.weight, 1) nn.init.zeros_(module.bias) net.apply(init_constant) net[0].weight.data[0], net[0].bias.data[0] ``` ``` (tensor([1., 1., 1., 1.]), tensor(0.)) ``` 此程式碼則使用常數1進行初始化. ```python=+ def init_xavier(module): if type(module) == nn.Linear: nn.init.xavier_uniform_(module.weight) def init_42(module): if type(module) == nn.Linear: nn.init.constant_(module.weight, 42) net[0].apply(init_xavier) net[2].apply(init_42) print(net[0].weight.data[0]) print(net[2].weight.data) ``` ``` tensor([-0.0974, 0.1707, 0.5840, -0.5032]) tensor([[42., 42., 42., 42., 42., 42., 42., 42.]]) ``` 此程式碼展示分層初始化,第一種是類似Uniform然後可以Normlized,第二個用常數42去初始化. ![image](https://hackmd.io/_uploads/rJo-rolxA.png) ```python=+ def my_init(module): if type(module) == nn.Linear: print("Init", *[(name, param.shape) for name, param in module.named_parameters()][0]) nn.init.uniform_(module.weight, -10, 10) module.weight.data *= module.weight.data.abs() >= 5 net.apply(my_init) net[0].weight[:2] ``` ``` Init weight torch.Size([8, 4]) Init weight torch.Size([1, 8]) tensor([[ 0.0000, -7.6364, -0.0000, -6.1206], [ 9.3516, -0.0000, 5.1208, -8.4003]], grad_fn=<SliceBackward0>) ``` 上面使用 $$ \begin{aligned} w \sim \begin{cases} U(5, 10) & \textrm{ with probability } \frac{1}{4} \\ 0 & \textrm{ with probability } \frac{1}{2} \\ U(-10, -5) & \textrm{ with probability } \frac{1}{4} \end{cases} \end{aligned} $$ 進行初始化 ```python=+ net[0].weight.data[:] += 1 net[0].weight.data[0, 0] = 42 net[0].weight.data[0] ``` ``` tensor([42.0000, -6.6364, 1.0000, -5.1206]) ``` 示範如何操縱參數. ## 6.4 Lazy Initialization 在此之前**6.1,6.2**中都是採用不指定輸入維度的方式,且一開始也不對參數進行初始化. 深度學習框架有時不能知道網絡的輸入維度。因此進行推遲初始化,等到我們第一次通過模型傳遞數據時,才即時推斷每個層的大小。 稍後,在處理卷積神經網絡時,這個技巧將變得更加方便,因為輸入維度(例如,圖像的分辨率)將影響每個後續層的維度。因此,在編寫代碼時能夠設置參數,而不需要在編寫代碼時知道維度的值,可以極大地簡化指定和後續修改模型的任務。接下來,我們將深入探討初始化的機制。 ```python=+ import torch from torch import nn from d2l import torch as d2l net = nn.Sequential(nn.LazyLinear(256), nn.ReLU(), nn.LazyLinear(10)) net[0].weight ``` ``` <UninitializedParameter> ``` 可以看到使用``LazyLinear(256)``的方式只需要宣告輸出維度,不需要宣告輸入維度.且不會對參數進行初始化. ![image](https://hackmd.io/_uploads/HkKnOjggC.png) ![image](https://hackmd.io/_uploads/ryIROigl0.png) ```python=+ X = torch.rand(2, 20) net(X) net[0].weight.shape ``` ``` torch.Size([256, 20]) ``` 而透過第一次的參數輸入,初始化了參數.