# PyTorch 教學 ###### tags: `Self_Learning` ## 安裝 關鍵字可以打:pytorch get started 網址是:[https://pytorch.org/get-started/locally/](https://pytorch.org/get-started/locally/) ## 匯入 在Python中當然要先import才能用囉! ```python import torch ``` --- ## 變數介紹 不論是在TensorFlow還是PyTorch,都會聽到一個詞叫做張量(Tensor),先不管他是什麼,只要想成是這些AI工具的變數型態就好! 在PyTorch中,Tensor的型態是```torch.Tensor``` ```python= import torch a = torch.tensor([1.0]) print(type(a)) # 輸出為:<class 'torch.Tensor'> ``` ### tensor資料型態 在Python內建的資料型態有整數(int)、32位元浮點數(float)、64位元浮點數(double)。在PyTorch中,在宣告新變數時可以用: * ```torch.tensor()``` * 最泛用的,甚至可以創造非n維矩陣的資料。範例: :::danger 只有這個會有可能產生出整數型態的tensor,可能會導致運算上的錯誤 ::: ```python int_x = torch.tensor(1) # 整數型態tensor float_x = torch.tensor(1.0) # 浮點數型態tensor ``` * ```torch.Tensor()``` * 與上一個相似,差別在於參數必須是n維矩陣。範例: ```python x = torch.Tensor([1.0, 2.0]) ``` * ```torch.FloatTensor()``` * 與```torch.Tensor```相似,用法相同,變數資料型態為32位元浮點數 * ```torch.DoubleTensor()``` * 與```torch.Tensor```相似,用法相同,變數資料型態為64位元浮點數 ### Tensor資料型態轉型 轉換成32位元浮點數用```.float()``` 轉換成64位元浮點數用```.double()``` ```python a = torch.tensor([1.0]) float_a = a.float() double_a = a.double() ``` 不同位元無法相互運算,發生錯誤時轉型即可。 ### 四則運算、矩陣乘法 加(```+```)減(```-```)乘(```*```)除(```/```)及取餘數(```%```)這些基本算數運算元皆為element-wise,也就是對應元素相互運算。 矩陣乘法為使用```torch.mm()```,兩者需相同資料型態,且須滿足矩陣乘法之規則: * 範例: ```python a = torch.randn(3, 2) b = torch.randn(2, 5) ab = torch.mm(a, b) ``` ### 與numpy互動 要將tensor轉成numpy的資料型態非常簡單,只需要加上```.numpy()```即可: ```python a = torch.tensor([1.0]) np_a = a.numpy() # 這樣就變numpy了 ``` 如果是要將numpy的變數轉成tensor,則使用```torch.from_numpy()```: ```python a = np.array([1.0]) tensor_a = torch.from_numpy(a) ``` ### 矩陣、向量產生工具(ones, zeros, rand, randn, linspace, arange) 這些都跟numpy用法相似,差別在於除了使用tuple和list外,資料的shape也可以用多參數的方式設定: * ones:產生全部都是1的矩陣 * zeros:產生全部都是0的矩陣 * rand:產生全部都是隨機數值的矩陣(0~1之間) * randn:產生標準常態分佈之隨機數值的矩陣(平均0、標準差1) 範例: ```python arr_ones = torch.ones(3, 2) # 多參數設定 arr_zeros = torch.zeros((5, 4, 3)) # 用tuple設定 arr_rand = torch.rand([2, 3, 4, 2]) # 用list設定 arr_randn = torch.randn(arr_rand.size()) # 用別的變數的size來設定 ``` * ones_like、zeros_like: 參考目標產生相同size的全一或全零矩陣 ```python arr = torch.randn(5, 4, 3) arr_ones = torch.ones_like(arr) arr_zeros = torch.zeros_like(arr) ``` * normal:常態分佈隨機 ```torch.normal(均值, 變異數, 資料shape)``` 範例: ```python arr_normal = torch.normal(0, 1, (3, 2)) ``` * linspace:依照點數等距切割 ```torch.linspace(下限, 上限, 切割點數)``` 範例: ```python arr_linspace = torch.linspace(0, 1, 6) # 這樣就是:[0.0, 0.2, 0.4, 0.6, 0.8, 1.0] ``` * arange:依照間格等距切割 ```torch.arange(起點, 終點, 間格)``` :::danger 這裡跟Python內建的range及numpy的arange一樣,採用的是從起點開始一直加間隔,直到超出終點(**不含終點**) ::: 範例: ```python arr_arange = torch.arange(0, 1, 0.2) # 這樣就是:[0.0, 0.2, 0.4, 0.6, 0.8] arr_arange2 = torch.arange(1, 0, -0.2) # 這樣就是:[1.0, 0.8, 0.6, 0.4, 0.2] ``` --- ## 倒傳遞演算法(自動梯度追蹤功能) :::danger 設定追蹤梯度的變數無法直接使用```.numpy()```,必須先```.data```或```.detach()```才可以(範例:```w.data.numpy()```與```w.detach().numpy()```)。 ::: 在人工神經網路(Artificial Neural Network, ANN)中,所有梯度數值大多是依靠倒傳遞(Back Propagation)演算法求得,而倒傳遞的原理就是微積分的連鎖率。 在這些AI工具中都會有其特殊的方式來協助開發者求梯度值,PyTorch中使用以下方式: 1. 首先,我們建立一個tensor ```python w = torch.tensor(2.0) ``` 2. 使用```.requires_grad_(True)```告訴PyTorch你現在要讓```w```在接下來的運算中追蹤梯度 :::danger 要追蹤梯度的必須是32位元或是64位元的浮點數,且必須是n維矩陣形式,此範例雖然使用2.0而非\[2.0\],但其實它在設定追蹤梯度後會自動轉換,為避免意外請使用n維矩陣。 ::: ```python w.requires_grad_(True) ``` > ```.requires_grad_```是return自己本身(```self```),所以可以合併縮寫為: > ```w = torch.tensor(2.0).requires_grad_(True)``` 3. 將他乘上3,並定義一個新變數叫```y``` ```python y = w * 3 ``` 4. 對```y```執行倒傳遞```.backward()``` ```python y.backward() ``` 5. 用```.grad```取得```w```本身的梯度值(即為$\partial y/\partial w$) :::danger 在執行```.backward()```之前```.grad```都是```None``` ::: ```python print(w.grad) # 輸出為:tensor(3.) ``` #### 程式總覽 ```python= import torch w = torch.tensor(2.0) w.requires_grad_(True) y = w * 3 y.backward() print(w.grad) # 輸出為:tensor(3.) ``` 是不是非常簡單易懂呢? --- ## 優化器(Optimizer) [官方文件](https://pytorch.org/docs/stable/optim.html) 在神經網路中,優化器是用來協助最小化優化問題的工具,採用如隨機梯度下降(Stochastic Gradient Descent, SGD)、Momentum、RMSProp、Adam等梯度相關演算法。 在PyTorch中,優化器都放在```torch.optim```底下,例如: * ```torch.optim.SGD``` * ```torch.optim.RMSProp``` * ```torch.optim.Adam``` ### 優化器參數 所有優化器都需要兩個參數: * 訓練對象(params): * 若非神經網路的參數則需使用list包裝(不建議使用tuple): ```python params=[x, y] ``` * 若有需要能使用雜湊陣列dict對個別參數使用不同學習率: ```python params=[ {'params': [x, y], 'lr': 0.01}, {'params': [z], 'lr': 0.1} ] ``` * 學習率(lr): * 在PyTorch中每個優化器其實都有預設學習率,更改lr數值來設定: ```python lr=1e-2 ``` #### 設定範例 ```python opt = torch.optim.SGD(params=[w], lr=1e-2) ``` ### 執行優化器 執行剛剛已經建議好的優化器的```.step()```方法即可,為了避免上一次的前向傳播與這一次疊加,會先使用```.zero_grad()```來歸零梯度,故每次訓練通常會有這一系列的步驟: :::success 疊加是什麼意思? 假設我的前向傳播是$f(x)$,在沒有歸零情況下會發生下一次的前向傳播變成$f\left(f(x)\right)$。 也就是說,程式並不知道何時為起點與終點,必須將梯度歸零以避免疊加多次前向傳播。 ::: ```python opt.zero_grad() # 梯度歸零 loss.backward() # 從loss這個變數開始進行倒傳遞 opt.step() # 執行優化器 ``` ### 自訂優化器流程 有時候我們需要做的不是梯度下降,而是梯度上升,或是只是需要計算梯度值,再用於其他較為不同的演算法,這時就需要把優化器內部所發生的事情重現一次。 :::info 雖然以公式來說只需要將學習率定為負值就能實現梯度上升,但PyTorch內建的優化器限制使用者無法設定為負值。 ::: 以下範例為找出$y(x)=-x^2$為極大值時的$x$為何,即$x=\underset{x}{\arg\max}\hspace{2pt}\{-x^2\}$ 匯入PyTorch,初始化```x```並追蹤梯度: ```python= import torch x = torch.randn(1).requires_grad_(True) ``` 疊代20次,將```x```代入$y(x)=-x^2$: ```python=5 for epoch in range(20): y = - x**2 ``` 從```y```進行倒傳遞: ```python=7 y.backward() ``` 執行梯度上升,學習率定為```0.1```: :::danger $x=x+1$這行為在數學上是不可能的,也就是說無法計算梯度,所以PyTorch禁止追蹤梯度的變數直接賦值,必須使用```.data```變更。 ::: ```python=8 x.data += 0.1 * x.grad ``` **\[非常重要\]** 將梯度歸零: ```python=9 x.grad.data.zero_() ``` 總覽: ```python= import torch x = torch.randn(1).requires_grad_(True) for epoch in range(20): y = - x**2 y.backward() x.data += 0.1 * x.grad x.grad.data.zero_() # print當下結果 print('x = {}, y = {}'.format(x.data.numpy(), -x.data.numpy()**2)) ``` --- ## 損失函數 要評估訓練成果需要一個指標,最常用的是均方誤差(Mean Square Error, MSE): $$ MSE(\textbf{y}_{pred},\textbf{y}_{real})=\frac{1}{N}\sum^{N-1}_{i=0}{\left(y_{i,pred}-y_{i,real}\right)^2} $$ ```python loss_func = torch.nn.MSELoss() # 宣告使用的損失函數 loss = loss_func(y_pred, y_real) # 計算誤差 ``` 其餘還有很多種,詳情可以去查閱官方文件。 ## 訓練範例1:一元線性方程式 至此,已經可以建立一個簡易的訓練流程,以下為範例: 最一開始一定要import: ```python= import torch ``` 先設定一個假的數據: ```python=3 x = torch.linspace(-5, 5, 30) ``` 我希望它能找出$y=x\times 3+2$這條直線,先給一個實際值: ```python=4 real_y = x * 3 + 2 ``` 我想找到的是$x$的權重3和偏差值2,在這邊我宣告兩個初始為隨機值的變數,並設定追蹤梯度: ```python=5 weight = torch.randn(1).requires_grad_(True) bias = torch.randn(1).requires_grad_(True) ``` 接著設定優化器,採用隨機梯度下降SGD,學習率用0.1: ```python=8 opt = torch.optim.SGD(params=[weight, bias], lr=1e-1) ``` 再來設定損失函數,採用MSE: ```python=9 loss_func = torch.nn.MSELoss() ``` 使用for迴圈進行20次疊代: ```python=11 for epoch in range(100): pred_y = x * weight + bias loss = loss_func(pred_y, real_y) opt.zero_grad() loss.backward() opt.step() print('Epoch {}: loss = {}, weight = {}, bias = {}')\ .format(epoch, loss.data.numpy(),\ weight.data.numpy(), bias.data.numpy()) ``` ### 範例總覽 ```python= import torch x = torch.linspace(-5, 5, 30) real_y = x * 3 + 2 weight = torch.randn(1).requires_grad_(True) bias = torch.randn(1).requires_grad_(True) opt = torch.optim.SGD(params=[weight, bias], lr=1e-1) loss_func = torch.nn.MSELoss() for epoch in range(100): pred_y = x * weight + bias loss = loss_func(pred_y, real_y) opt.zero_grad() loss.backward() opt.step() print('Epoch {}: loss = {}, weight = {}, bias = {}')\ .format(epoch, loss.data.numpy(),\ weight.data.numpy(), bias.data.numpy()) ``` --- ## 建立神經網路 :::success 此處僅使用全連接神經網路做說明,若需進階的網路可自行查閱網路資源。 ::: 在建立神經網路時需要繼承```torch.nn.Module```,並於類別初始化時執行父類別的初始化,且須覆寫```forward```方法定義正向傳播。 先以一個範例說明: ```python= import torch class FCNet(torch.nn.Module): # 繼承torch.nn.Module def __init__(self): super(FCNet, self).__init__() # 執行父類別的初始化 self.layer_1 = torch.nn.Linear(2, 2) self.layer_2 = torch.nn.Linear(2, 1) def forward(self, x): # 定義正向傳播 x = self.layer_1(x) x = self.layer_2(x) return x ``` 以上就是一個簡易的全連接神經網路,接著來逐行解釋: ```python= import torch class FCNet(torch.nn.Module): ``` 這一段應該不用說明,就只是宣告一個類別叫```FCNet```,並繼承```torch.nn.Module```。 ```python=4 def __init__(self): super(FCNet, self).__init__() # 執行父類別的初始化 self.layer_1 = torch.nn.Linear(2, 2) self.layer_2 = torch.nn.Linear(2, 1) ``` Python中類別裡的```__init__```方法會在新增該類別之物件時執行,即為初始化。 ```torch.nn.Linear```為線性全連接,參數依序為輸入和輸出的size,其內部會進行矩陣乘法,故須注意乘法的規則。 若不希望有bias,可設定為```torch.nn.Linear(2, 2, bias=False)``` ```python=9 def forward(self, x): # 定義正向傳播 x = self.layer_1(x) x = self.layer_2(x) return x ``` 此處為定義正向傳播,```x = self.layer_1(x)```等同$\textbf{x}\leftarrow \textbf{x}\textbf{w}+\textbf{b}$ ### 取出參數 在優化器中需要設定訓練對象,在新增類別物件後使用```.parameters()```即可取出。 ```python net = FCNet() ``` 特定層的參數也可以透過內部放於```self```中的來取出: ```python net.layer_1.parameters() ``` 該層的權重(weight)、偏差值(bias)的取出方式也很直觀: ```python net.layer_1.weight net.layer_1.bias ``` 且因為這些都是需要更新的,因此也看得到他的梯度: ```python net.layer_1.weight.grad ``` ### 正向傳播 要讓資料流過這個網路非常簡單,直接輸入就好! ```python net = FCNet() output = net(input_data) # input在python是保留字,不要用來當變數名稱喔 ``` ## 準備數據集 如果數據量不大沒有要做批次處理的話,直接輸入進模型就可以了。 ### 批次處理 在PyTorch裡有提供非常好用的工具:```TensorDataset```和```DataLoader```,能提供批次處理、隨機打散的功能。 首先,將需要依照batch size切割的數據集放入```torch.utils.data.TensorDataset```中: > 因為```torch.utils.data```有點太長了,所以我會習慣用```from```和```import```的方式先匯入```TensorDataset```和```DataLoader``` ```python= import torch from torch.utils.data import TensorDataset, DataLoader dataset_x = torch.randn(500, 2) dataset_y = torch.randn(500, 1) tensor_dataset = TensorDataset(dataset_x, dataset_y) ``` :::success TensorDataset裡面要放幾個參數都可以,因為他採用```*tensors```作為參數 ::: 接著使用DataLoader: ```python= loader = DataLoader( dataset=tensor_dataset, batch_size=5, shuffle=True, # 是否打散 num_workers=2 # 要用幾個執行序來協助批次分割 ) ``` ### 取出批次數據 直接使用for迴圈即可: ```python= for batch_x, batch_y in loader: ``` ## 激勵函數(Activation Function) PyTorch中的激勵函數都放在```torch.nn.functional```內,如: * ```torch.nn.functional.relu``` * ```torch.nn.functional.sigmoid``` 為了方便多數人會將```torch.nn.functional```重新命名: ```python import torch.nn.functional as F ``` 使用方式與```torch.nn.Linear```一樣,直接把輸入丟進去就好: ```python x = F.relu(x) ``` ## 訓練範例2:擬合非線性凸函數 本範例結構為:輸入層(1個神經元)$\rightarrow$隱藏層(10個神經元)$\rightarrow$relu函數$\rightarrow$輸出層(1個神經元) 最一開始先匯入: ```python= import torch from torch.utils.data import TensorDataset, DataLoader import torch.nn.functional as F ``` 先建立模擬數據,此範例預計讓模型擬合$y(x)=x^2$: ```python=5 x = torch.linspace(-5, 5, 500).view(-1, 1) y = x ** 2 y += torch.normal(0, 0.01, x.size()) # 補上高斯雜訊 ``` 設定DataLoader協助批次處理: ```python=9 dataset = TensorDataset(x, y) loader = DataLoader(dataset=dataset, batch_size=100,\ shuffle=True, num_workers=2) ``` 建立一個神經網路模型: ```python=13 class Net(torch.nn.Module): def __init__(self, n_in, n_hidden, n_out): super(Net, self).__init__() self.layer_1 = torch.nn.Linear(n_in, n_hidden) self.layer_2 = torch.nn.Linear(n_hidden, n_out) def forward(self, x): x = self.layer_1(x) x = F.relu(x) x = self.layer_2(x) return x ``` 接著建立```Net```型態的物件(此範例的內部結構採用初始化時設定): ```python=26 net = Net(n_in=1, n_hidden=10, n_out=1) ``` 設定優化器和損失函數: ```python=27 opt = torch.optim.SGD(net.parameters, lr=1e-1) loss_func = torch.nn.MSELoss() ``` 開始疊代: ```python=30 for epoch in range(100): for step, (batch_x, batch_y) in enumerate(loader): output = net(batch_x) loss = loss_func(output, batch_y) opt.zero_grad() loss.backward() opt.step() print('Epoch {}, step {}: loss = {}'\ .format(epoch, step, loss.data.numpy())) ``` ### 範例總覽 > 為了順便展示DataLoader,這個範例的擬合結果不會太好看,範例參考來自[莫凡的PyTorch教學](https://morvanzhou.github.io/tutorials/machine-learning/torch/3-01-regression/) ```python= import torch from torch.utils.data import TensorDataset, DataLoader import torch.nn.functional as F x = torch.linspace(-5, 5, 500).view(-1, 1) y = x ** 2 y += torch.normal(0, 0.01, x.size()) # 補上高斯雜訊 dataset = TensorDataset(x, y) loader = DataLoader(dataset=dataset, batch_size=100,\ shuffle=True, num_workers=2) class Net(torch.nn.Module): def __init__(self, n_in, n_hidden, n_out): super(Net, self).__init__() self.layer_1 = torch.nn.Linear(n_in, n_hidden) self.layer_2 = torch.nn.Linear(n_hidden, n_out) def forward(self, x): x = self.layer_1(x) x = F.relu(x) x = self.layer_2(x) return x net = Net(n_in=1, n_hidden=10, n_out=1) opt = torch.optim.SGD(net.parameters(), lr=1e-1) loss_func = torch.nn.MSELoss() for epoch in range(100): for step, (batch_x, batch_y) in enumerate(loader): output = net(batch_x) loss = loss_func(output, batch_y) opt.zero_grad() loss.backward() opt.step() print('Epoch {}, step {}: loss = {}'\ .format(epoch, step, loss.data.numpy())) ``` :::danger 如果執行時看到這個錯誤的話: ```RuntimeError: DataLoader worker (pid(s) ####, ####) exited unexpectedly``` 只要將主要執行的部分(包含DataLoader的)用function包裝就好了,所以一般來說我喜歡整理成以下**修改範例** ::: ### 修改範例 ```python= import torch from torch.utils.data import TensorDataset, DataLoader import torch.nn.functional as F x = torch.linspace(-5, 5, 500).view(-1, 1) y = x ** 2 y += torch.normal(0, 0.01, x.size()) # 補上高斯雜訊 class Net(torch.nn.Module): def __init__(self, n_in, n_hidden, n_out): super(Net, self).__init__() self.layer_1 = torch.nn.Linear(n_in, n_hidden) self.layer_2 = torch.nn.Linear(n_hidden, n_out) def forward(self, x): x = self.layer_1(x) x = F.relu(x) x = self.layer_2(x) return x def main(): dataset = TensorDataset(x, y) loader = DataLoader(dataset=dataset, batch_size=100,\ shuffle=True, num_workers=2) net = Net(n_in=1, n_hidden=10, n_out=1) opt = torch.optim.SGD(net.parameters(), lr=1e-1) loss_func = torch.nn.MSELoss() for epoch in range(100): for step, (batch_x, batch_y) in enumerate(loader): output = net(batch_x) loss = loss_func(output, batch_y) opt.zero_grad() loss.backward() opt.step() print('Epoch {}, step {}: loss = {}'\ .format(epoch, step, loss.data.numpy())) if __name__ == '__main__': main() ``` ## 補充 ### tensor特殊操作 * size與shape:```<tensor>.shape```或```<tensor>.size()``` 用來查看該tensor的shape ```<tensor>.size()```可以用加入參數查看指定維度的數量 範例: ```a.size()```是```(2, 3)``` ```a.size(0)```是```2``` ```a.size(1)```是```3``` * view:```<tensor>.view(<*new_shape>)``` 更改shape,可以在其中一個維度使用-1讓他自動計算 範例: ```python a = torch.ones(30).view(-1, 10) print(a.size()) # (3, 10) ``` * cat:```torch.cat([<tensor>, <tensor>, ...], dim=<dim>)``` 類似numpy的append,在指定維度上疊加,可用-1指定最後一維度。 可疊加多個tensor。 範例: ```a.size()```是```(2, 3)``` ```torch.cat((a, a), dim=0).size()```是```(4, 3)``` ```torch.cat((a, a), dim=1).size()```是```(2, 6)``` * stack:```torch.stack([<tensor>, <tensor>, ...], dim=<dim>)``` 與cat相似,但是是在指定維度新增一個維度並疊加(因此這個維度數字可以比原本多1),可用-1指定最後一維度(原維度+1)。 可疊加多個tensor。 範例: ```a.size()```是```(2, 3)``` ```torch.stack((a, a), dim=0).size()```是```(2, 2, 3)``` ```torch.stack((a, a), dim=-1).size()```是```(2, 3, 2)``` :::danger cat和stack有可能會操作失誤,建議可以開一個terminal(如IDLE)測試後再使用 ::: ### 變數複製、移除梯度追蹤 在物件導向程式設計中,有一種東西是必須被注意的,那就是**副作用(side-effect)**。 也就是說當一個變數作為參數輸入給一個方法,若非刻意不要讓這個函數變更這個變數。 這樣說很模糊,用一個簡短範例說明: 假設現在有一個變數: ```python= x = 5 ``` 將```x```作為參數輸入```f```這個方法: ```python=2 f(x) # 不管裡面發生什麼事 ``` 這時若我print出```x```,而他的數值發生改變,那就是發生了副作用,也就是這個方法的參數採用了傳址(pass by reference)而不是傳值(pass by value)。 最經典的是list的副作用,範例如下: ```python= def f(x): x.append(1) return x x = [1] y = f(x) print(x) # [1, 1] print(y) # [1, 1] ``` 這時我們一般來說會靠複製,避免影響到參數本身,如使用```copy```和```deepcopy```來複製list。 ```python= import copy def f(x): x = copy.copy(x) x.append(1) return x x = [1] y = f(x) print(x) # [1] print(y) # [1, 1] ``` 在PyTorch中,有些時候會有不可預期的副作用,這時可以使用```.clone()```來複製變數,而若需要複製追蹤梯度的變數而不保留追蹤梯度的行為,則先使用```.detach()```移除梯度追蹤。 範例:```x_clone = x.clone()```或```x_dc = x.detach().clone()``` ### 製作數據集 如果這個數據不是靠讀取現有的數據取得,而是由程式運算後取得的模擬數據,可利用stack將結果組合起來。 此範例為將30個shape為(10, 2)的隨機數據組合成數據集: ```python= dataset = list() for _ in range(30): dataset.append(torch.randn(10, 2)) dataset = torch.stack(dataset, dim=0) ``` 這樣```dataset```的size就是```(30, 10, 2)``` ## 外部資源 * [PyTorch 官網](https://pytorch.org/) * [Pytorch 教程系列| 莫烦Python](https://morvanzhou.github.io/tutorials/machine-learning/torch/)