# 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/)