---
# System prepended metadata

title: 【Pytorch 深度學習筆記】模型驗證與過擬合以及 Autograd 的細節
tags: [python, 程式設計, DL, Python, 深度學習, 程式語言, AI]

---

# 【Pytorch 深度學習筆記】模型驗證與過擬合以及 Autograd 的細節

[TOC]

哈囉大家好我是 LukeTseng，感謝您點進本篇筆記，該篇筆記主要配合讀本 《Deep Learning with pytorch》 進行學習，另外透過網路資料作為輔助。本系列筆記是我本人奠基深度學習基礎知識的開始，若文章有誤煩請各位指正，謝謝！

本篇為 《Deep Learning with pytorch》 這本書 5.5.3 Training, validation, and overfitting 及 5.5.4 Autograd nits and switching it off 的相關筆記。

## 模型驗證（validation）

在訓練模型時，最重要的目標是讓模型不僅能記住所給定的訓練資料，還能對沒見過的資料泛化（Generalization）。而最壞的情況是遇到過擬合。

:::info
泛化（Generalization）指的是模型在訓練時從資料中學到「可重複使用的規律」，因此面對沒看過的新資料（同類型、但不是訓練集裡的樣本）也能做出合理的預測，而不是只有把訓練資料背起來而已。
:::

因此需要有兩套系統來分別對模型訓練及驗證：

- 訓練集（Training set）：用來讓模型擬合（fit）參數的資料。
- 驗證集（Validation set）：不參與訓練，僅用於評估模型在處理新資料時表現如何。

## 過擬合（overfitting）

什麼是過擬合（overfitting）？就是模型對於訓練資料擬合（fit）的太好了，在訓練集上都表現得非常好，那個曲線幾乎做到完全貼合、貼緊，但換到沒見過的新資料（如驗證集）時表現變差，因為模型記住了訓練資料的細節甚至雜訊，而不是學到可泛化的規律。

過擬合並非模型不夠強，而是模型太適應訓練集，結果對新資料預測不準，因此過擬合本質上是泛化能力不好，也就是離開訓練資料分佈後就失效。

所以如果發現模型在訓練集上的準確度極高，但在驗證集上的損失卻很高（或不降反升），就是過擬合的徵兆。

像是以下這兩張示例圖，表示過擬合的極端例子：

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

Source："Deep Learning with PyTorch" P.133 Figure 5.13

### 如何解決過擬合問題？

- 增加資料量：獲取更多資料以涵蓋更多變化。
- 簡化模型：減小模型的規模（減少參數數量）。雖會讓模型對訓練集的擬合不如複雜模型完美，但在資料點間的表現會更穩定。
- 正則化（Regularization）：在損失函數（Loss Function）中加入懲罰項，讓模型參數變化更平滑。
- 加入雜訊（Noise）：在輸入中人為加入擾動，強迫模型學習更本質的特徵。
- 平衡策略：先增加模型規模直到能擬合資料，再縮減規模直到停止過擬合。

### 評估損失的標準（也是判斷過擬合的標準）

在訓練迴圈（Training Loop）中，要觀察兩個數值：

1. 訓練損失（Training Loss）：模型是否能擬合訓練集。如果訓練損失不下降，通常表示模型太簡單、資料量不足。
2. 驗證損失（Validation Loss）：模型是否能泛化到新資料。如果跟訓練損失開始分岔，就表示模型正在過擬合。

### 拆分資料集（Spliting a dataset）

在這邊拆分是要拆成前面說的「訓練集（training set）」跟「驗證集（Validation set）」。

拆成這樣就可以去判斷一個模型是否遇到 overfitting 的問題。

使用 `torch.randperm` 來隨機排列索引，然後將 dataset 分為兩部分（[Code Source](https://github.com/deep-learning-with-pytorch/dlwpt-code/blob/master/p1ch5/3_optimizers.ipynb)）：

```python=
n_samples = t_u.shape[0]
n_val = int(0.2 * n_samples) # 拿 20% 做驗證

shuffled_indices = torch.randperm(n_samples)

train_indices = shuffled_indices[:-n_val]
val_indices = shuffled_indices[-n_val:]

train_indices, val_indices
```

Output：

```
(tensor([9, 6, 5, 8, 4, 7, 0, 1, 3]), tensor([ 2, 10]))
```

在獲得了 index 的 tensor 後，可利用這些 tensor 從資料的 tensor 來去建立 training set 跟 validation set。

```python=
train_t_u = t_u[train_indices]
train_t_c = t_c[train_indices]

val_t_u = t_u[val_indices]
val_t_c = t_c[val_indices]

train_t_un = 0.1 * train_t_u
val_t_un = 0.1 * val_t_u
```

接著修改 training_loop，在每個 Epoch 中，分別計算「訓練損失」（Training Loss）和「驗證損失」（Validation Loss），但要注意只在訓練損失（Training Loss）上呼叫 `.backward()` 來更新模型參數。

```python=
def training_loop(n_epochs, optimizer, params, train_t_u, val_t_u,
                  train_t_c, val_t_c):
    for epoch in range(1, n_epochs + 1):
        # 訓練用
        train_t_p = model(train_t_u, *params)
        train_loss = loss_fn(train_t_p, train_t_c)
        
        # 驗證用
        # 只拿來觀察用，不更新參數
        val_t_p = model(val_t_u, *params)
        val_loss = loss_fn(val_t_p, val_t_c)
        
        optimizer.zero_grad()
        train_loss.backward() # 只需要對訓練資料做 backward
        optimizer.step()

        if epoch <= 3 or epoch % 500 == 0:
            print(f"Epoch {epoch}, Training loss {train_loss.item():.4f},"
                  f" Validation loss {val_loss.item():.4f}")
            
    return params
```

採用 SGD，正規化參數再跑訓練：

```python=
params = torch.tensor([1.0, 0.0], requires_grad=True)
learning_rate = 1e-2
optimizer = optim.SGD([params], lr=learning_rate)

training_loop(
    n_epochs = 3000, 
    optimizer = optimizer,
    params = params,
    train_t_u = train_t_un, # 正規化
    val_t_u = val_t_un, # 正規化
    train_t_c = train_t_c,
    val_t_c = val_t_c)
```

Output：

```
Epoch 1, Training loss 66.5811, Validation loss 142.3890
Epoch 2, Training loss 38.8626, Validation loss 64.0434
Epoch 3, Training loss 33.3475, Validation loss 39.4590
Epoch 500, Training loss 7.1454, Validation loss 9.1252
Epoch 1000, Training loss 3.5940, Validation loss 5.3110
Epoch 1500, Training loss 3.0942, Validation loss 4.1611
Epoch 2000, Training loss 3.0238, Validation loss 3.7693
Epoch 2500, Training loss 3.0139, Validation loss 3.6279
Epoch 3000, Training loss 3.0125, Validation loss 3.5756
tensor([  5.1964, -16.7512], requires_grad=True)
```

從結果看來，模型在訓練集與驗證集上的 loss 都持續下降，代表是真的有在學習，而且目前看起來沒有明顯 overfitting 的跡象。

雖說 Validation loss 明顯比 Training loss 高，但也不至於說高出一個量級，是在可接受的範圍內。

作者也說這樣的評估其實並不公平，因為驗證集是很小的，對於評估驗證損失的意義只能到某種程度，而且模型預期會在 training set 表現更好，因為參數就是被 training set 形塑出來的。因此 validation loss 比 training loss 高本身不意外，重點是它是否一樣跟 training loss 一起往下走、且差距是否合理接近。

### 一些過擬合的情境

- A：訓練和驗證損失都沒有下降，表示模型沒學到東西。
- B：訓練損失下降，驗證損失卻上升，即為過擬合。
- C：兩者同步下降，是最理想的情況。
- D：兩者趨勢相似但數值有差距，這表示過擬合在可控範圍內。

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

Source："Deep Learning with PyTorch" P.136 Figure 5.14

### 小結

可以把訓練集（Training set）想像成是課本裡面的範例，而所謂驗證集（Validation set）就是實戰考題。

如果一個學生只是把練習題的答案背下來，在寫練習題時都全對（訓練損失極低），但一碰到沒看過的考題就 G 了（驗證損失極高），就表示這學生已經過擬合了。

## Autograd 的細節

### Autograd 會不會搞混？

在之前的訓練迴圈中，於同一個 Epoch 裡做了兩件事：

1. 把訓練資料 `train_t_u` 丟進模型，算出 `train_loss`。
2. 把驗證資料 `val_t_u` 丟進模型，算出 `val_loss`。

然後只對 `train_loss` 呼叫了 `.backward()`。

疑惑的點在於，模型被執行了兩次（一次訓練、一次驗證），是否會讓 Autograd 搞混？而在當呼叫 `.backward()` 時，他會不會不小心把驗證集的資料也算進去，影響到參數的更新？

答案是不會。

原因在於 PyTorch 的運作機制：獨立的計算圖（Computation Graph）。

- 當用 `train_t_u` 跑模型時，PyTorch 建立了一個計算圖，連接了 `train_t_u` -> `model` -> `train_loss`。
- 當用 `val_t_u` 跑模型時，PyTorch 又建立了另一個獨立的計算圖，連接了 `val_t_u` -> `model` -> `val_loss`。

這兩張圖唯一的交集是共享了模型的參數（parameters）。

當只對 `train_loss` 呼叫 `.backward()` 時，Autograd 只會沿著訓練的圖往回跑，計算參數相對於訓練損失的梯度，不會理驗證的圖。

但是有一個風險，書中作者提醒，如果不小心對 `val_loss` 也呼叫了 `.backward()`，那麼梯度就會累加，然後模型會變成整個資料集（訓練集 + 驗證集）在學習，這就會破壞驗證集要作為獨立測試的意義了。

以下的示例圖就是在說明上面這些東東：

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

Source："Deep Learning with PyTorch" P.137 Figure 5.15

### 為什麼要關閉 Autograd？

既然都不會在 `val_loss` 上呼叫 `.backward()`，那麼 PyTorch 為了「跟蹤計算過程」而建立驗證計算圖的行為，其實是在浪費資源。

如果試著去優化，建立計算圖還是會消耗額外的速度和記憶體，特別是當模型擁有數百萬個參數時。

解決方法就是用先前有用過的 `torch.no_grad`。

```python=
# 以下是原本的寫法，會建立不必要的圖
val_t_p = model(val_t_u, *params)
val_loss = loss_fn(model(val_t_u), val_t_c)

# 以下是改進後的寫法
with torch.no_grad():
    val_t_p = model(val_t_u, *params)
    val_loss = loss_fn(val_t_p, val_t_c)
    assert val_loss.requires_grad == False  # 驗證輸出的 tensor 不需要梯度
```

另一種方式是用 `torch.set_grad_enabled(is_train)`。

如果要根據 bool（像是是否處於訓練模式）來動態控制，可使用這個方法。

```python=
def calc_forward(t_u, t_c, is_train):
    with torch.set_grad_enabled(is_train): # 根據 is_train 開啟或關閉
        t_p = model(t_u, *params)
        loss = loss_fn(t_p, t_c)
    return loss
```

## 總整理

### 為什麼需要模型驗證（Validation）

模型訓練的真正目標不是把訓練資料「背起來」，而是學到可泛化的規律（Generalization），能正確預測沒看過的新資料。

因此，資料必須分成兩部分：

* 訓練集（Training set）：用來調整模型參數。
* 驗證集（Validation set）：不參與訓練，只用來檢查模型的泛化能力。

驗證集的存在，就是用來檢測過擬合。

### 什麼是過擬合（Overfitting）

過擬合指的是「模型在訓練集表現極好，但在驗證集或新資料上表現明顯變差的現象」。

原因不是模型不夠強，而是太貼合訓練資料的細節與雜訊，導致離開訓練資料分佈就失效。

典型徵兆是：

- 訓練損失持續下降
- 驗證損失卻停止下降甚至上升

表示模型的泛化能力不足。

### 常見的過擬合解法

- 增加資料量：獲取更多資料以涵蓋更多變化。
- 簡化模型：減小模型的規模（減少參數數量）。雖會讓模型對訓練集的擬合不如複雜模型完美，但在資料點間的表現會更穩定。
- 正則化（Regularization）：在損失函數（Loss Function）中加入懲罰項，讓模型參數變化更平滑。
- 加入雜訊（Noise）：在輸入中人為加入擾動，強迫模型學習更本質的特徵。
- 平衡策略：先增加模型規模直到能擬合資料，再縮減規模直到停止過擬合。

### 如何用 Loss 判斷模型狀態

在訓練過程中需同時觀察兩個數值：

1. Training Loss
    - 反映模型是否能學會訓練資料。
    - 不下降通常代表模型太簡單或資料不足。
2. Validation Loss
    - 反映模型的泛化能力。
    - 與 Training Loss 分岔是過擬合的警訊。

理想的情況是兩者同步下降，且差距合理。

### 資料集拆分與驗證流程

實務上會先隨機打亂資料，再切出一部分作為驗證集（如 80% / 20% 的比例做拆分）。

訓練迴圈中主要就分兩部分：

- 訓練資料：計算 loss → backward → 更新參數
- 驗證資料：只 forward、只觀察，不更新參數

只要 Validation loss 跟著 Training loss 一起下降，通常表示模型真的在學習，而不是死背資料。

### 幾種典型學習情境

* A：訓練、驗證 loss 都不降 → 模型沒學到東西
* B：訓練降、驗證升 → 明顯過擬合
* C：兩者同步下降 → 理想狀態
* D：趨勢相同但驗證略高 → 可接受的輕微過擬合

### Autograd 與驗證的關係

在同個 Epoch 中如何處理 Autograd 跟驗證：

- 訓練資料與驗證資料會各自建立獨立的計算圖（Computation Graph）
- 只對 Training loss 呼叫 `.backward()`
- Autograd 不會把驗證資料算進梯度

但若誤對 `val_loss.backward()`，梯度就會被污染，驗證集失去意義。

### 為什麼驗證時要關閉 Autograd

驗證階段不需要梯度，但 PyTorch 預設仍會建立計算圖，造成：

- 額外的記憶體消耗
- 不必要的計算成本

解法：

- 使用 `torch.no_grad()`
- 或用 `torch.set_grad_enabled(is_train)` 動態控制

能有效提升效能，特別是在大型模型中。

