---
# System prepended metadata

title: Pytorch 基本教學
tags: [PyTorch, 機器學習]

---

# Pytorch 基本教學

* **教學影片**：
    * ==[ML2021 Pytorch tutorial part 1](https://www.youtube.com/watch?v=8DaeP2vSu90)==
    * ==[ML2021 Pytorch Tutorial part 2](https://www.youtube.com/watch?v=CyV3zbcuufs)==
* **程式碼範例**：
    * colab：[Pytorch Tutorial](https://colab.research.google.com/github/ga642381/ML2021-Spring/blob/main/Pytorch/Pytorch_Tutorial.ipynb)

---
## 先備知識

需具備以下基礎：

*   **Python 3**:
    *   熟悉 `if-else` 條件判斷、迴圈 (`loop`)、函式 (`function`) 定義、檔案輸入輸出 (file IO)、類別 (`class`) 等基本語法與概念。
    *   參考資料：[Python 官方教學](https://docs.python.org/3/tutorial/), [Google's Python Class](https://developers.google.com/edu/python/), [Learn Python the Hard Way](https://learnpythonthehardway.org/python3/)
*   **NumPy**:
    *   了解 NumPy 的陣列 (`array`) 操作。
    *   參考資料：[NumPy 官方教學](https://numpy.org/doc/stable/user/quickstart.html)

---
## 什麼是 PyTorch？

PyTorch 是一個開源的機器學習 (machine learning) 框架。它主要提供以下兩個高級功能：

*   **Tensor (張量) 計算**：類似 NumPy 的陣列運算，但具有強大的 GPU 加速能力。Tensor 可以在圖形處理單元 (Graphics Processing Unit, GPU) 上運行以加速計算。
*   **基於 TAPE 的自動微分 (Autograd) 系統**：用於建構與訓練深度神經網路，能夠自動計算梯度 (gradient)，這是訓練 DNN 最重要的部分之一。
![image](https://hackmd.io/_uploads/BJRXnV7Zle.png)

---
## PyTorch v.s. TensorFlow

PyTorch 和 TensorFlow 是目前最主流的兩個深度學習框架。它們的主要差異如下：

| 特性        | PyTorch                                   | TensorFlow                                       |
| :---------- | :---------------------------------------- | :----------------------------------------------- |
| **開發者**  | Facebook AI                               | Google Brain                                     |
| **介面**    | Python & C++                              | Python, C++, JavaScript, Swift                   |
| **除錯**    | 相對容易 (動態圖)                         | 舊版較難，2.0 版後改善 (預設 Eager Execution)      |
| **應用**    | **研究領域**較受歡迎 (快速開發、易於除錯)       | 工業界**產品部署**較多                               |

TensorFlow 可以在更多平台上運行，例如 JavaScript 或 Swift。PyTorch 因為其易用性和除錯的便利性，在學術研究中更受青睞。本課程將主要使用 PyTorch。

---
## DNN 訓練流程概覽

訓練一個深度神經網路通常包含以下步驟，PyTorch 提供了相應的模組來支持這些步驟：

1.  **載入資料 (Load Data)**:
    *   這是訓練模型的第一步，你需要先有資料才能進行後續操作。
    *   PyTorch 提供 `torch.utils.data.Dataset` 和 `torch.utils.data.DataLoader` 來處理資料載入。
2.  **定義神經網路 (Define Neural Network)**:
    *   建構你的模型架構。
    *   使用 `torch.nn` 模組。
3.  **定義損失函數 (Loss Function)**:
    *   衡量模型預測結果與真實答案之間的差距。
    *   同樣在 `torch.nn` 中定義。
4.  **定義優化器 (Optimizer)**:
    *   根據損失函數計算出來的梯度來更新模型的權重，執行梯度下降 (gradient descent) 等優化算法。
    *   使用 `torch.optim` 模組。
5.  **訓練 (Training)**:
    *   將資料輸入模型，計算損失，反向傳播計算梯度，並使用優化器更新模型參數。
6.  **驗證 (Validation)**:
    *   在訓練過程中，使用驗證集評估模型性能，檢查模型是否有進步，並用於調整超參數或判斷是否發生過擬合。
7.  **測試 (Testing)**:
    *   訓練完成後，使用獨立的測試集評估模型的最終性能。

訓練和驗證步驟通常會重複多次 (多個 epochs)。
![image](https://hackmd.io/_uploads/BJ3nhEQWgg.png)

---
## Tensor (張量)

Tensor 是 PyTorch 中的核心資料結構，可以理解為一個高維度的矩陣 (matrix) 或陣列 (array)。

*   一維 Tensor：向量 (vector)
*   二維 Tensor：矩陣 (matrix)
*   三維 Tensor：可以想像成一個立方體
![image](https://hackmd.io/_uploads/Byylp47Zge.png)

### Tensor 資料型態 (Data Type)

Tensor 中最常儲存的是浮點數 (floating point) 和整數 (integer)。

| 資料型態描述         | `dtype`       | Tensor 建立函式       |
| :------------------- | :------------ | :-------------------- |
| 32-位元 ﬂoating point| `torch.float` | `torch.FloatTensor`   |
| 64-位元 integer (signed) | `torch.long`  | `torch.LongTensor`    |

> 更多資料型態可參考：[PyTorch Tensors 文件](https://pytorch.org/docs/stable/tensors.html)

### Tensor 的形狀 (Shape)

Tensor 的形狀描述了其在各個維度上的大小。

*   **一維 Tensor**：例如 `[1, 2, 3, 4, 5]`，其 shape 為 `(5,)`。
*   **二維 Tensor (矩陣)**：例如一個 3 個 row、5 個 column 的矩陣，其 shape 為 `(3, 5)`。
*   **三維 Tensor**：例如一個 4x5x3 的立方體，其 shape 為 `(4, 5, 3)`。

維度 (Dimension) 的索引從 0 開始。PyTorch 中的 `dim` 與 NumPy 中的 `axis` 是相同的概念。
![image](https://hackmd.io/_uploads/H1bXC4Qbgl.png)

### Tensor 建構器 (Constructor)

有幾種常見的方式可以創建 Tensor：

1.  **從 Python list 或 NumPy array 轉換**:
    ```python=
    # From list
    x = torch.tensor([[1, -1], [-1, 1]])
    
    # From NumPy array
    import numpy as np
    arr = np.array([[1, -1], [-1, 1]])
    x = torch.from_numpy(arr)
    ```
    轉換後的 `x` 會是：
    ```
    tensor([[ 1., -1.],
            [-1.,  1.]])
    ```
2.  **創建全零 Tensor**：使用 `torch.zeros()`，需要指定 shape。
    ```python=
    x = torch.zeros([2, 2])
    ```
    結果：
    ```
    tensor([[0., 0.],
            [0., 0.]])
    ```
3.  **創建全一 Tensor**：使用 `torch.ones()`，需要指定 shape。
    ```python=
    x = torch.ones([1, 2, 5])
    ```
    結果：
    ```
    tensor([[[1., 1., 1., 1., 1.],
             [1., 1., 1., 1., 1.]]])
    ```

### Tensor 常用運算 (Operators)

PyTorch 提供了豐富的 Tensor 運算。

*   **`squeeze(dim=None)`**：移除 Tensor 中所有大小為 1 的維度。如果指定 `dim`，則只移除指定維度 (若其大小為 1)。
    *   例如：一個 Tensor 的 shape 為 `(1, 2, 3)`，`x.squeeze(0)` 後，shape 變為 `(2, 3)`。
![image](https://hackmd.io/_uploads/Hks0A4Xbgg.png)
*   **`unsqueeze(dim)`**：在指定的維度 `dim` 上增加一個大小為 1 的新維度。
    *   例如：一個 Tensor 的 shape 為 `(2, 3)`，`x.unsqueeze(1)` 後，shape 變為 `(2, 1, 3)`。
![image](https://hackmd.io/_uploads/HJJVJrQWel.png)
*   **`transpose(dim0, dim1)`**：交換指定的兩個維度 `dim0` 和 `dim1`。
    *   例如：一個 Tensor 的 shape 為 `(2, 3)`，`x.transpose(0, 1)` 後，shape 變為 `(3, 2)`。
![image](https://hackmd.io/_uploads/HyvUySQblg.png)
*   **`cat(tensors, dim=0)`**：將多個 Tensor 沿著指定的維度 `dim` 串接 (concatenate) 在一起。除了串接的維度外，其他維度的 shape 必須相同。
    *   例如：有三個 Tensor x, y, z，shape 分別為 `(2, 1, 3)`, `(2, 3, 3)`, `(2, 2, 3)`。
        `w = torch.cat([x, y, z], dim=1)`
        `w` 的 shape 會是 `(2, 1+3+2, 3)` 即 `(2, 6, 3)`。
![image](https://hackmd.io/_uploads/rko5kr7bex.png)
*   **算術運算**:
    *   加法：`z = x + y`
    *   減法：`z = x - y`
    *   次方：`y = x.pow(2)` 或 `y = x ** 2`
*   **聚合運算**:
    *   加總：`y = x.sum()`
    *   平均：`y = x.mean()`

> 更多運算子請參考：[PyTorch Tensor Operators](https://pytorch.org/docs/stable/tensors.html)

### Tensor - PyTorch v.s. NumPy

PyTorch 的 Tensor 操作在很多方面與 NumPy 相似。

*   **屬性 (Attributes)**:
    | PyTorch  | NumPy    |
    | :------- | :------- |
    | `x.shape`| `x.shape`|
    | `x.dtype`| `x.dtype`|

*   **形狀操作 (Shape manipulation)**:
    | PyTorch                 | NumPy                     |
    | :---------------------- | :------------------------ |
    | `x.reshape()` / `x.view()` | `x.reshape()`            |
    | `x.squeeze()`           | `x.squeeze()`             |
    | `x.unsqueeze(dim)`      | `np.expand_dims(x, axis)` |

> 參考：[PyTorch for NumPy users](https://github.com/wkentaro/pytorch-for-numpy-users)

### Tensor - 運算裝置 (Device)

預設情況下，Tensor 和模組會在 CPU 上進行運算。PyTorch 允許將運算轉移到 GPU 上以獲得加速。

*   **CPU**：`x = x.to('cpu')`
*   **GPU**：`x = x.to('cuda')` (如果 CUDA 可用)

要使用 GPU，你需要有一張支援 CUDA 的 NVIDIA 顯示卡。

*   **檢查 GPU 是否可用**:
    ```python
    torch.cuda.is_available()
    ```
*   **指定 GPU**：如果有多張 GPU，可以指定使用哪一張，例如 `cuda:0` (第一張), `cuda:1` (第二張) 等。
    ```python
    device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
    x = x.to(device)
    ```
*   **為什麼使用 GPU?**
    GPU 擁有大量的核心，非常適合進行平行運算。矩陣運算通常可以分解為許多獨立的小運算，這些小運算可以在 GPU 的多個核心上同時執行，從而大幅提升運算速度。
    > 參考：[What is a GPU and do you need one in Deep Learning?](https://www.quora.com/What-is-a-GPU-and-do-you-need-one-in-deep-learning-3)

---
## 如何計算梯度 (Gradient)？

PyTorch 的 `autograd` 套件提供了自動微分功能，用於計算 Tensor 的梯度。

假設我們有一個輸入 Tensor $X$，我們要計算一個輸出 $Z$，其計算方式為 $X$ 中每個元素的平方和：$$Z = \sum_i \sum_j X_{i,j}^2$$

$Z$ 對 $X$ 中任一元素 $X_{k,l}$ 的偏微分 (partial derivative) 為：$$\frac{\partial Z}{\partial X_{k,l}} = 2X_{k,l}$$

梯度 $\nabla_X Z$ 就是所有這些偏微分組成的矩陣。

在 PyTorch 中計算梯度的步驟如下：

1.  **創建 Tensor 並設定 `requires_grad=True`**：告知 PyTorch 需要追蹤此 Tensor 的所有運算，以便計算梯度。
    ```python
    x = torch.tensor([[1., 0.], [-1., 1.]], requires_grad=True)
    ```
2.  **進行運算得到輸出 Tensor**:
    ```python
    z = x.pow(2).sum() # 2次方後加總
    ```
3.  **呼叫 `backward()` 方法**：從輸出 Tensor `z` 開始**反向傳播**計算梯度。
    ```python
    z.backward()
    ```
4.  **獲取梯度**：梯度會儲存在原始 Tensor 的 `.grad` 屬性中。
    ```python
    print(x.grad)
    ```
    結果會是：
    ```
    tensor([[ 2.,  0.],
            [-2.,  2.]])
    ```
    這與我們手動計算的 $2X$ 是一致的。
![image](https://hackmd.io/_uploads/S1hSGBX-ll.png)

---
## Dataset 與 DataLoader

為了有效地載入和處理資料，PyTorch 提供了 `Dataset` 和 `DataLoader` 兩個類別。
```python
from torch.utils.data import Dataset, DataLoader
```

### Dataset

`Dataset` 是一個抽象類別，用於表示資料集。你需要繼承 `Dataset` 並覆寫以下兩個方法來創建自訂的資料集：

*   **`__init__(self, ...)`**：初始化函式。通常在這裡讀取資料、進行預處理 (例如 feature extraction 特徵提取)。
*   **`__getitem__(self, index)`**：根據索引 `index` 返回一個樣本 (一筆資料)。`DataLoader` 會使用這個方法來獲取資料。
*   **`__len__(self)`**：返回資料集的總大小 (樣本數量)。
```python=
class MyDataset(Dataset):
    def __init__(self, file_path):
        # 例如：讀取 CSV 檔案或圖片路徑列表
        self.data = ... # 讀取資料並進行預處理
        
    def __getitem__(self, index):
        # 返回第 index 筆資料及其對應的標籤 (label)
        sample = self.data[index]
        # label = self.labels[index]
        return sample #, label
        
    def __len__(self):
        return len(self.data)
```

### DataLoader

`DataLoader` 是一個迭代器，它將 `Dataset` 包裝起來，並提供批次 (batch) 讀取、資料打亂 (shuffle) 等功能。

* **創建 `DataLoader` 時的主要參數**：
    *   `dataset`：你創建的 `Dataset` 物件。
    *   `batch_size`：每次迭代返回的樣本數量 (一個 mini-batch 的大小)。
    *   `shuffle`：是否在每個 epoch 開始時打亂資料順序。通常在訓練時設為 `True`，在驗證和測試時設為 `False` (確保每次評估順序一致，避免結果變異)。
    *   `num_workers`：使用多少個子行程來載入資料。可以加速資料讀取。

```python=
my_dataset = MyDataset(file_path='path/to/your/data')
my_dataloader = DataLoader(dataset=my_dataset, batch_size=32, shuffle=True)

# 使用 DataLoader 範例
for epoch in range(num_epochs):
    for data_batch in my_dataloader:
        inputs, labels = data_batch
        # ... 進行訓練 ...
```

* **`DataLoader()` 內部**：從 `Dataset` 中透過 `__getitem__` 獲取多筆資料 (數量等於 `batch_size`)，然後將它們組合成一個 mini-batch 返回。
![image](https://hackmd.io/_uploads/H1G19UmWgx.png)

---
## torch.nn - 神經網路模組

`torch.nn` 模組包含了建構神經網路所需的各種工具，如網路層 (layers)、激活函數 (activation functions)、損失函數 (loss functions) 等。

### 神經網路層 (Neural Network Layers)

*   **`nn.Linear(in_features, out_features)`**：全連接層 (fully-connected layer)。
    *   `in_features`：輸入特徵的維度。
    *   `out_features`：輸出特徵的維度。
    *   輸入 Tensor 的 shape 可以是 `(*, in_features)`，其中 `*` 表示任意數量的其他維度 (通常是 batch size)。最後一個維度必須是 `in_features`。
    *   輸出 Tensor 的 shape 會是 `(*, out_features)`。
        *   例如：`layer = nn.Linear(32, 64)`。如果輸入 shape 為 `(N, 32)`，輸出 shape 為 `(N, 64)`。
![image](https://hackmd.io/_uploads/H1a0jU7-xg.png)
* 一個線性層的運算可以表示為：$y = Wx + b$
    *   $x$：輸入向量 (shape：`in_features`)
    *   $W$：權重矩陣 (shape：`out_features * in_features`)
    *   $b$：偏差向量 (bias) (shape：`out_features`)
    *   $y$：輸出向量 (shape：`out_features`)
![image](https://hackmd.io/_uploads/BksrhLQZex.png)

    可以通過 `layer.weight` 和 `layer.bias` 來存取線性層的 weight 和 bias。
    *   `layer.weight.shape` 會是 `(out_features, in_features)`。(上圖為 `([64, 32])`)
    *   `layer.bias.shape` 會是 `(out_features,)`。(上圖為 `([64])`)

### 激活函數 (Activation Functions)

`torch.nn` 提供了多種激活函數，例如：

*   **`nn.Sigmoid()`**：[Sigmoid 激活函數](https://hackmd.io/@Jaychao2099/imrobot1#%E7%A1%ACS%E5%9E%8B%E5%87%BD%E5%BC%8F-Hard-Sigmoid%EF%BC%9A)$$f(x) = \frac{1}{1 + e^{-x}}$$
*   **`nn.ReLU()`**：[ReLU (Rectified Linear Unit) 激活函數](https://hackmd.io/@Jaychao2099/imrobot1#%E5%8F%A6%E4%B8%80%E6%BF%80%E6%B4%BB%E5%87%BD%E5%BC%8F%EF%BC%9AReLU-Rectified-Linear-Unit)$$f(x) = \max(0, x)$$
![image](https://hackmd.io/_uploads/HJYmd57-ge.png)
*   其他還有 `nn.Tanh()`, [`nn.Softmax()`](https://hackmd.io/@Jaychao2099/imrobot3#%E7%B6%B2%E8%B7%AF%E8%BC%B8%E5%87%BA%E8%88%87-Softmax-%E5%87%BD%E6%95%B8) 等工具。

### 損失函數 (Loss Functions)

`torch.nn` 中也定義了多種常用的損失函數：

*   **`nn.MSELoss()`**：[均方誤差損失 (Mean Squared Error)](https://hackmd.io/@Jaychao2099/imrobot3#%E5%9D%87%E6%96%B9%E8%AA%A4%E5%B7%AE-Mean-Squared-Error-MSE)，常用於**迴歸 (regression) 問題**。$$L(y, \hat{y}) = \frac{1}{N} \sum (y_i - \hat{y}_i)^2$$
*   **`nn.CrossEntropyLoss()`**：[交叉熵損失 (Cross-Entropy Loss)](https://hackmd.io/@Jaychao2099/imrobot3#%E4%BA%A4%E5%8F%89%E7%86%B5-Cross-entropy)，常用於**分類 (classification) 問題**。這個函數內部通常會結合 `LogSoftmax` 和 `NLLLoss`。$$L(y, \hat{y}) = - \frac{1}{N}\sum_{i} \hat{y}_i \ln(y'_i)$$
*   其他還有 `nn.L1Loss()`, `nn.BCELoss()` (二元交叉熵) 等。

### 建構自訂神經網路

要建構自己的神經網路，你需要創建一個繼承自 `nn.Module` 的類別。

*   在 `__init__(self)` 方法中：
    *   呼叫 `super().__init__()`。
    *   定義網路中需要用到的層 (layers)。這些層本身也是 `nn.Module` 的子類。
    *   可以使用 `nn.Sequential` 來快速串聯多個層。`nn.Sequential` 是一個容器，它會按照層被加入的順序依次執行這些層。
*   在 `forward(self, x)` 方法中：
    *   定義資料 `x` 如何在網路中前向傳播 (forward pass) 並得到輸出。

```python=
import torch.nn as nn

class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        # 定義網路層
        self.net = nn.Sequential(
            nn.Linear(10, 32),      # 輸入 10 維，輸出 32 維
            nn.Sigmoid(),           # Sigmoid 激活
            nn.Linear(32, 1)        # 輸入 32 維，輸出 1 維
        )

    def forward(self, x):
        # 定義前向傳播路徑
        return self.net(x)

# 使用模型
model = MyModel()
input_tensor = torch.randn(5, 10) # 假設 batch_size=5, input_features=10
output_tensor = model(input_tensor)
```
上述模型的結構：Input (10維) -> Linear(10, 32) -> Sigmoid -> Linear(32, 1) -> Output (1維)。

![image](https://hackmd.io/_uploads/ryz9kPXZlx.png)

---

## torch.optim - 優化器模組

`torch.optim` 模組實現了多種優化算法，用於更新神經網路的參數 (weight 和 bias)。

*   **`optim.SGD(params, lr, momentum=0, ...)`**：**隨機**梯度下降 (Stochastic Gradient Descent, SGD)。
    *   `params`：模型中需要被優化的參數。通常是 `model.parameters()`。
    *   `lr`：[學習率 (learning rate)](https://hackmd.io/@Jaychao2099/imrobot3#%E4%B8%89-%E6%B7%B1%E5%BA%A6%E5%AD%B8%E7%BF%92%E8%A8%93%E7%B7%B4%E6%8A%80%E5%B7%A7%EF%BC%9A%E8%87%AA%E5%8B%95%E8%AA%BF%E6%95%B4%E5%AD%B8%E7%BF%92%E9%80%9F%E7%8E%87-Adaptive-Learning-Rate)，控制每次參數更新的步長。
    *   `momentum`：[動量 (momentum)](https://hackmd.io/@Jaychao2099/imrobot3#%E5%8B%95%E9%87%8F-Momentum)，SGD 的一種改進，有助於加速收斂並減少震盪。
```python=
# 假設 model 和 criterion (損失函數) 已定義
# model = MyModel()
# criterion = nn.MSELoss()

optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
```
*   其他[常用優化器](https://hackmd.io/@Jaychao2099/imrobot3#%E5%B8%B8%E8%A6%8B%E7%9A%84-sigma_it-%E8%A8%88%E7%AE%97%E6%96%B9%E6%B3%95)：`optim.Adam()`, `optim.RMSprop()`, `optim.Adagrad()` 等。

---
## 神經網路訓練與評估

### 訓練準備 (Setup)

在開始訓練迴圈之前，需要完成以下設置：

1.  **準備 dataset 和 data loader**:
    ```python=
    dataset = MyDataset(file_path_train)
    train_loader = DataLoader(dataset, batch_size=64, shuffle=True)

    val_dataset = MyDataset(file_path_val)
    val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)
    ```
2.  **實例化模型並移至特定裝置**:
    ```python=6
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model = MyModel().to(device)
    ```
3.  **定義損失函數**:
    ```python=8
    criterion = nn.MSELoss()
    ```
4.  **定義優化器**:
    ```python=9
    optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
    ```

### 訓練迴圈 (Training Loop)

訓練通常在一個迴圈中進行，每個迴圈迭代稱為一個 epoch (完整遍歷一次訓練資料集)。

```python=10
num_epochs = 10
for epoch in range(num_epochs):
    model.train()    # 將模型設置為訓練模式

    running_loss = 0.0
    for inputs, labels in train_loader:    # 假設 train_loader 已定義
        # 1. 將資料移至指定裝置
        inputs, labels = inputs.to(device), labels.to(device)

        # 2. 清除舊的梯度 (非常重要！)
        optimizer.zero_grad()

        # 3. 前向傳播: 計算模型預測
        outputs = model(inputs)

        # 4. 計算損失
        loss = criterion(outputs, labels)

        # 5. 反向傳播: 計算梯度
        loss.backward()

        # 6. 更新參數
        optimizer.step()

        running_loss += loss.item()
    
    print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {running_loss/len(train_loader):.4f}")

    # (可選) 每個 epoch 後進行驗證
    # ... validation code ...
```

**關鍵步驟解釋**:

*   `model.train()`：告訴模型現在是訓練模式。這對某些層 (如 `Dropout`, `BatchNorm`) 很重要，它們**在訓練和評估時的行為不同**。
*   `optimizer.zero_grad()`：在計算新的梯度之前，必須**清除上一步殘留的梯度**。否則梯度會累加。
*   `loss.backward()`：計算損失相對於模型參數的梯度。
*   `optimizer.step()`：根據 `loss.backward()` 計算得到的梯度來更新模型的參數。

### 驗證 (Validation)

在每個 epoch 結束後，通常會在驗證集上評估模型性能。

```python=40
    model.eval()    # 將模型設置為評估模式
    total_val_loss = 0
    with torch.no_grad():    # 在評估時不需要計算梯度，可以節省記憶體和計算
        for inputs, labels in val_loader:    # 假設 val_loader 已定義
            inputs, labels = inputs.to(device), labels.to(device)
            outputs = model(inputs)
            loss = criterion(outputs, labels)
            total_val_loss += loss.item()
    avg_val_loss = total_val_loss / len(val_loader)
    print(f"Validation Loss: {avg_val_loss:.4f}")

    # 根據 avg_val_loss 決定是否保存模型
```

**關鍵步驟解釋**:

*   `model.eval()`：告訴模型現在是評估模式。
*   `with torch.no_grad()`：在這個區塊內的運算**不會被追蹤梯度**，這在驗證和測試時是必要的，可以節省計算資源並防止意外的梯度計算。

### 測試 (Testing)

訓練完成後，在測試集上評估模型的最終性能。測試集上的資料模型之前從未見過。

```python=
model.eval()    # 設置為評估模式
predictions = []
with torch.no_grad():
    for test_inputs in test_loader:    # 測試集通常只有輸入，沒有標籤
        test_inputs = test_inputs.to(device)
        outputs = model(test_inputs)
        
        # 將預測結果移回 CPU 並轉換為 NumPy array (如果需要)
        predictions.extend(outputs.cpu().numpy()) 

# 將 predictions 整理成提交格式，例如 CSV 檔案
```
* `.cpu()` 方法是將 Tensor 從 GPU 移回 CPU。

---
## 儲存與載入神經網路

訓練好的模型參數需要儲存起來，以便後續使用或繼續訓練。

*   **儲存模型 (Save)**：通常儲存模型的 `state_dict` (狀態字典)，它包含了模型的所有參數 (wieght 和 bias)。
    ```python=
    torch.save(model.state_dict(), 'model_weights.pth')
    ```
*   **載入模型 (Load)**:
    1.  先創建一個和儲存時相同結構的模型實例。
    2.  然後載入 `state_dict`。
    ```python=
    model_loaded = MyModel()    # 創建模型實例
    model_loaded.load_state_dict(torch.load('model_weights.pth'))
    model_loaded.to(device)    # 不要忘記將模型移到適當的裝置
    model_loaded.eval()        # 如果是載入來做預測，設為評估模式
    ```

---
## 更多關於 PyTorch

* PyTorch 生態系中有許多其他實用的套件：
    *   **`torchaudio`**：用於處理**語音**和**音訊**資料。
    *   **`torchtext`**：用於處理**自然語言處理 (NLP) 任務**的資料。
    *   **`torchvision`**：包含了流行的**電腦視覺**資料集、模型架構和圖像轉換工具。
    *   **`skorch`**：一個將 PyTorch 與 scikit-learn 結合的套件，使得 PyTorch 模型可以像 scikit-learn 模型一樣使用。

* 一些有用的 GitHub repositories (使用 PyTorch)：
    *   [Hugging Face Transformers](https://github.com/huggingface/transformers)：提供了大量預訓練的 Transformer 模型 (如 BERT, GPT)。
    *   [Fairseq](https://github.com/facebookresearch/fairseq)：用於 sequence-to-sequence 建模，適用於 NLP 和語音。
    *   [ESPnet](https://github.com/espnet/espnet)：用於 end-to-end 語音處理，包括語音辨識、翻譯、合成等。

---

## PyTorch 文件查詢

[PyTorch 官方文件](https://pytorch.org/docs/stable/)是學習和解決問題的重要資源。
*   神經網路相關函式：`torch.nn`
*   優化演算法相關：`torch.optim`
*   Dataset 和 DataLoader：`torch.utils.data`

### 文件查詢範例：`torch.max`

當查詢一個函式 (例如 `torch.max`) 時，注意以下幾點：

1.  **函式簽名 (Function Signature)**:
    *   第一行通常會顯示函式的輸入參數和輸出。
    *   例如 `torch.max(input) → Tensor` 表示輸入 `input`，輸出一個 `Tensor`。
![image](https://hackmd.io/_uploads/HJl5mtX-el.png)
2.  **參數說明 (Parameters & Keyword Arguments)**:
    *   會詳細列出每個參數的名稱、型態和意義。
    *   **位置參數 (Positional Arguments)**：不需要指定參數名稱，按順序傳入即可。
    *   **關鍵字參數 (Keyword Arguments)**：必須指定參數名稱才能傳入值。通常在函式簽名中**以 `*` 分隔**位置參數和關鍵字參數。
    *   **預設值 (Default Value)**：某些參數有預設值，如果沒有傳入這些參數，函式會使用預設值。
![image](https://hackmd.io/_uploads/rkgMNYQZex.png)
3.  **不同用法**:
    一個函式可能有多種用法，根據輸入參數的不同，行為也會不同。以下為 **`torch.max` 的三種主要用法**：

    1.  **`torch.max(input) → Tensor`**:
        *   返回整個 `input` Tensor 中的最大值。
        *   例如 `x = torch.tensor([[1, 2, 3], [5, 6, 4]])`，`torch.max(x)` 返回 `tensor(6)`。![image](https://hackmd.io/_uploads/SklY79mWlx.png)
    2.  **`torch.max(input, dim, keepdim=False, *, out=None) → (Tensor, LongTensor)`**:
        *   沿著指定的 `dim` 維度尋找最大值。
        *   返回一個 tuple，包含兩個 Tensor：
            *   `values`：每個子陣列在 `dim` 維度上的**最大值**。
            *   `indices`：這些最大值在 `dim` 維度上的**索引**。
        *   `keepdim=True` 會保持原始維度數量，最大值所在的維度大小變為 1。
        *   例如 `x = torch.tensor([[1, 2, 7], [5, 6, 4]])`
            `values, indices = torch.max(x, dim=1)`
            `values` 為 `tensor([7, 6])`，`indices` 為 `tensor([2, 1])`。![](https://res.cloudinary.com/dkfys4c5d/image/upload/v1747381703/torchmax-2-c_t9tfcc.gif)
    3.  **`torch.max(input, other, *, out=None) → Tensor`**:
        *   比較兩個 Tensor `input` 和 `other` (shape 需相同或可廣播)，**逐元素取最大值**。
        *   返回一個與 `input` (或廣播後) shape 相同的 Tensor。
        *   例如 `x = torch.tensor([[1, 2, 3], [5, 6, 4]])`, `y = torch.tensor([[2, 4, 0], [1, 3, 5]])`
            `torch.max(x, y)` 返回 `tensor([[2, 4, 3], [5, 6, 5]])`。![](https://res.cloudinary.com/dkfys4c5d/image/upload/v1747381182/torchmax-3_uc7pdq.gif)

#### 程式碼範例:

```python=
import torch

x = torch.randn(4,5)
y = torch.randn(4,5)

# 1. max of entire tensor
m = torch.max(x)
print(m)

# 2. max along a dimension
m_val, idx = torch.max(x, dim=0)    # 沿著第0維 (column-wise)
print(m_val)
print(idx)

# 錯誤範例：
m, idx = torch.max(x,0,False,p)    # 'out' 是關鍵字參數，不能當作位置參數 p 傳入
m, idx = torch.max(x,True)         # 未指定 dim，True 會被誤認為 dim
                                   # (且 True 不能被視為 1)
# 3. max of two tensors
t = torch.max(x,y)
print(t)
```

---
## 常見錯誤 (Common Errors)

1.  **Tensor 與 Model 在不同裝置 (Device Mismatch)**:
    *   **錯誤訊息類似**：
        * `RuntimeError：Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!`
        * `Tensor for * is on CPU, but expected them to be on GPU`。
    *   **原因**：模型在 GPU 上，但輸入 Tensor 在 CPU 上 (或反之)。
    *   **解決**：確保模型和所有輸入 Tensor 都在同一個裝置上。
        ```python=
        model = torch.nn.Linear(5,1).to("cuda:0")
        x_cpu = torch.Tensor([1,2,3,4,5]).to("cpu")
        y = model(x_cpu)    # << 會報錯

        # 修正
        x_gpu = x_cpu.to("cuda:0")
        y = model(x_gpu)
        ```
2.  **維度不匹配 (Mismatched Dimensions)**:
    *   **錯誤訊息類似**：`RuntimeError：The size of tensor a (X) must match the size of tensor b (Y) at non-singleton dimension Z`。
    *   **原因**：進行運算的兩個 Tensor 在某個維度上的大小不一致，且無法進行廣播 (broadcasting)。
    *   **解決**：使用 `transpose()`, `squeeze()`, `unsqueeze()`, `view()`, `reshape()` 等操作來調整 Tensor 的 shape。
        ```python=
        x = torch.randn(4,5)
        y = torch.randn(5,4)
        z = x + y    # << 會報錯

        # 修正
        y_transposed = y.transpose(0,1)    # y_transposed shape is (4,5)
        z = x + y_transposed
        ```
        這種錯誤可能發生在使用預訓練模型時，輸入資料的 batch size 或特徵維度與模型期望的不符。
3.  **CUDA 記憶體不足 (CUDA Out-of-Memory)**:
    *   **錯誤訊息**：`RuntimeError：CUDA out of memory. Tried to allocate X MiB ...`。
    *   **原因**：一次性載入過多資料到 GPU，導致 GPU 記憶體耗盡。
    *   **解決**：
        *   減小 `batch_size`。
        *   如果不是使用 `DataLoader`，而是手動將整個資料集放入 GPU，請改用迭代方式或使用 `DataLoader`。
        ```python=
        resnet18 = models.resnet18().to("cuda:0")
        data = torch.randn(2048,3,244,244)    # 假設是 2048 張 244x244 的 RGB 圖片
        out = resnet18(data.to("cuda:0"))     # << 可能 OOM

        # 修正 (迭代處理)
        for d_single_image in data:
            # d_single_image shape：(3, 244, 244)
            # resnet18 預期輸入 (N, C, H, W), 所以要 unsqueeze
            out = resnet18(d_single_image.to("cuda:0").unsqueeze(0))
        ```
4.  **Tensor 型態不匹配 (Mismatched Tensor Type)**:
    *   **錯誤訊息類似**：`RuntimeError：Expected scalar type Long but found Float`。
    *   **原因**：函式期望特定資料型態的 Tensor，但傳入的是其他型態。
    *   常見於使用 `nn.CrossEntropyLoss` 時，其 `target` (標籤) 參數期望是 `torch.long` 型態的 Tensor，但可能不小心傳入了 `torch.float`。
    *   **解決**：將 Tensor 轉換為正確的型態，例如使用 `.long()` 或 `.float()`。
        ```python=
        L = nn.CrossEntropyLoss()
        outs = torch.randn(5,5)        # 模型的輸出 (logits)
        labels_float = torch.Tensor([1,2,3,4,0])    # 錯誤的型態
        lossval = L(outs, labels_float)             # << 會報錯

        # 修正
        labels_long = labels_float.long()
        lossval = L(outs, labels_long)
        ```
        注意 `torch.Tensor()` 預設創建的是 `torch.FloatTensor`。

---
## 補充：Dataset 和 DataLoader 的更多細節

`torch.utils.data.Dataset` 和 `torch.utils.data.DataLoader` 是 PyTorch 中處理資料的標準方式。

*   **`Dataset` 的核心**:
    *   `__len__(self)`：告訴 `DataLoader` 資料集有多大。
    *   `__getitem__(self, idx)`：根據 `DataLoader` 提供的索引 `idx` (可能經過 shuffle)，返回對應的資料點。
*   **`DataLoader` 的作用**:
    *   根據 `Dataset` 的 `__len__` 和 `batch_size` 產生索引。
    *   如果 `shuffle=True`，會打亂索引順序。
    *   將一批索引傳給 `Dataset` 的 `__getitem__` 來獲取一批資料。
    *   將這批資料組合成一個 mini-batch。

### 範例：資料增強 (Data Augmentation) 的簡單實現
可以在 `Dataset` 的 `__getitem__` 中實現簡單的資料增強。例如，如果我們想讓資料集長度加倍，一半是原始小寫字母，另一半是大寫字母：

```python=
import torch.utils.data

class ExampleDataset(torch.utils.data.Dataset):
    def __init__(self):
        self.data = "abcdefghijklmnopqrstuvwxyz"
        self.original_len = len(self.data)

    def __getitem__(self, idx):
        if idx >= self.original_len:    # 索引超出原始長度，返回大寫
            actual_idx = idx % self.original_len
            return self.data[actual_idx].upper()
        else:    # 索引在原始長度內，返回小寫
            return self.data[idx]

    def __len__(self):
        return 2 * self.original_len # 資料集長度變為兩倍

dataset1 = ExampleDataset()
dataloader = torch.utils.data.DataLoader(dataset=dataset1, shuffle=True, batch_size=1)
for datapoint in dataloader:
    print(datapoint)
```

這個範例展示了如何在 `Dataset` 內部控制資料的返回方式和資料集的有效大小。

---

回[主目錄](https://hackmd.io/@Jaychao2099/aitothemoon)