---
# System prepended metadata

title: HW1：Regression (COVID-19 Cases Prediction)
tags: [機器學習]

---

# HW1：Regression (COVID-19 Cases Prediction)

* **教學影片**：
    * ==[ML2021 Hw1](https://www.youtube.com/watch?v=Q4yskCCixhw)==
* **程式碼範例**：
    * [ML2021Spring - HW1](https://colab.research.google.com/github/ga642381/ML2021-Spring/blob/main/HW01/HW01.ipynb#scrollTo=ZeZnPAiwDRWG)
* **測驗平台**：
    * [ML2021Spring-hw1](https://www.kaggle.com/c/ml2021spring-hw1)

---
## Objectives (學習目標)

本次作業主要有以下三個學習目標：
1.  學習使用深度神經網路 (Deep Neural Networks, DNN) 來解決迴歸 (Regression) 問題。
2.  理解訓練一個深度神經網路的基本技巧與方法，例如：
    *   **超參數調整 (Hyper-parameter tuning)**
    *   **特徵選取 (Feature selection)**
    *   **正則化 (Regularization)**
3.  熟悉 PyTorch 深度學習框架的使用。

---
## Task Description (任務說明)

*   **任務**：預測 COVID-19 新增確診案例的百分比。
*   **資料來源**：美國卡內基梅隆大學 (Carnegie Mellon University, CMU) Delphi 研究小組。他們自 2020 年 4 月起，每日透過 Facebook 進行調查。
    *   下圖中，橫軸為天數，縱軸為百分比。藍色線代表 **有類似 COVID-19 症狀 (COVID-like Illness)** 的人數比例，橘色線代表**檢測陽性 (Tested Positive)** 的人數比例。作業的目標即是利用其他資訊來預測這條橘色線。
![image](https://hackmd.io/_uploads/HyLkIzD-xg.png)
*   **輸入與輸出**：
    *   會提供美國某特定州**過去三天**的調查資料。
    *   需要利用這些資料來**預測第三天**新檢測出的陽性案例百分比。
    *   **例如**：提供第一天的調查資料與陽性比例、第二天的資料與陽性比例，以及第三天的調查資料 (但不包含陽性比例)，學生需預測第三天的陽性比例。
![image](https://hackmd.io/_uploads/ByjgUGPbge.png)
*   **重要規定**：
    *   **嚴禁使用任何我們提供資料以外的額外資料或預訓練模型 (pre-trained models)。**

---
## Data (資料說明)

### Delphi's COVID-19 Surveys (Delphi 研究小組的 COVID-19 調查)
*   **調查方式**：針對美國各州，每日透過 Facebook 隨機抽樣數百人進行問卷調查。
*   **調查內容**：包含是否有 COVID-19 類似症狀、流感症狀、是否進行檢測、是否居家隔離、是否戴口罩、是否跨州移動、心理健康狀況 (焦慮、憂鬱) 等。
*   **資料處理流程**：從特定州的總人口中抽樣部分人進行問卷調查，再根據這些樣本的結果去估計整個州的情況。我們使用的就是這個估計後的資料。
![image](https://hackmd.io/_uploads/Hyzs8MvZee.png)

### Provided Features (提供的特徵)
我們選取了美國 40 個資料較乾淨的州的數據，資料包含以下幾類，數值多以百分比表示：
1.  **States (州別)** (40 維)：
    *   使用 One-hot vectors 表示。例如：AL (阿拉巴馬州), AK (阿拉斯加州), AZ (亞利桑那州) 等。
2.  **COVID-like illness (類 COVID-19 症狀指標)** (4 維)：
    *   例如：cli (COVID-like illness), ili (influenza-like illness, 類流感症狀) 等。
3.  **Behavior Indicators (行為指標)** (8 維)：
    *   例如：wearing_mask (戴口罩), travel_outside_state (跨州移動) 等。
4.  **Mental Health Indicators (心理健康指標)** (5 維)：
    *   例如：anxious (焦慮), depressed (憂鬱) 等。
5.  **Tested Positive Cases (檢測陽性案例)** (1 維)：
    *   `tested_positive`：這是我們要預測的目標。

### One-hot Vector
*   **定義**：一個向量中，所有元素皆為 0，只有一個元素為 1。
*   **用途**：通常用於編碼 **離散型數值 (Discrete Values)**。
*   **範例**：若州別代碼為 AZ (Arizona)，其 One-hot encoding 可能表示為一個向量，其中對應 Arizona 的位置為 1，其餘位置為 0。
    *   例如 `[0, 0, 1, 0, ..., 0]`，假設第三個元素代表 Arizona。
![image](https://hackmd.io/_uploads/S1tewzvZel.png)

### Data Structure (資料結構)
*   每一列 (row) 代表一個樣本 (sample)。
*   **Training Data (訓練資料)** (`covid.train.csv`):
    *   共 2700 筆樣本。
    *   每筆樣本維度：
        *   **前 40 維**：州別的 One-hot encoding。
        *   **接下來 18 維**：第一天的特徵 (包含上述症狀、行為、心理健康指標，以及當日的 `tested_positive` 比例)。
        *   **再接下來 18 維**：第二天的特徵 (同上)。
        *   **最後 18 維**：第三天的特徵 (同上，最後一欄為 `tested_positive`，即預測目標)。
![未命名](https://hackmd.io/_uploads/Sy6UwzwZxe.jpg)
*   **Testing Data (測試資料)** (`covid.test.csv`):
    *   共 893 筆樣本。
    *   與訓練資料相比，第三天的特徵少了最後一個 `tested_positive` 欄位，因為這是需要預測的目標。因此第三天只有 17 維特徵。
![未命名](https://hackmd.io/_uploads/B1xpPGvWll.jpg)

---
## Evaluation Metric (評估指標)

*   我們使用 **[均方根誤差 (Root Mean Squared Error, RMSE)](https://hackmd.io/@Jaychao2099/imrobot3#%E5%9D%87%E6%96%B9%E8%AA%A4%E5%B7%AE-Mean-Squared-Error-MSE)** 來評估模型的好壞。
*   RMSE 越低，代表模型預測結果越接近真實值，表現越好。
*   公式：$$ RMSE = \sqrt{\frac{1}{N}\sum_{n=1}^{N}(f(\mathbf{x}^n) - \hat{y}^n)^2} $$其中：
    *   $N$：測試資料的總筆數。
    *   $f(\mathbf{x}^n)$：你的模型 $f$ 對於第 $n$ 筆輸入特徵 $\mathbf{x}^n$ (testing data) 的預測輸出。
    *   $\hat{y}^n$：第 $n$ 筆資料的真實答案 (ground truth label)，這部分不提供，將由 Kaggle 進行線上評測。

---
## Kaggle (Kaggle 競賽平台)

*   **連結**：[https://www.kaggle.com/c/ml2021spring-hw1](https://www.kaggle.com/c/ml2021spring-hw1)
*   **Displayed Name (顯示名稱)**：
    *   **修課學生**：`<學號>_<自訂名稱>` (例如：`b06901020_puipui`)。
    *   **旁聽學生**：請勿在名稱中包含任何學號資訊，以免混淆。
*   **Submission Format (上傳格式)**：
    *   一個 `.csv` 檔案。
    *   包含兩個欄位 (column)：
        *   `id` (第幾筆資料，從 0 開始)
        *   `tested_positive` (預測的浮點數值)。
    *   **範例**：
        ```csvpreview
        id,tested_positive
        0,0.0
        1,0.0
        2,0.0
        ...
        ```
*   **Submission Rules (上傳規則)**：
    *   每日最多可上傳 5 次結果。此處的「每日」是以 UTC 時間計算，台灣時間 (UTC+8) 的每日重置時間為早上 8 點。
    *   在競賽結束前，必須**自行選取 2 個**你認為最好的提交結果，用於 Private Leaderboard 的最終計分。若未勾選，Private Leaderboard 可能沒有分數。
![image](https://hackmd.io/_uploads/r1R8tMP-xx.png)

---
## Grading (評分方式)

總共 10 分，評分標準如下：
*   **Baselines (基準線)**：有三個等級的 Baseline：Simple, Medium, Strong。每個 Baseline 分為 Public Leaderboard 和 Private Leaderboard。
    *   通過 Simple Baseline (Public)：+1 分 (執行 Sample Code 即可達成)。
    *   通過 Simple Baseline (Private)：+1 分 (執行 Sample Code 即可達成)。
    *   通過 Medium Baseline (Public)：+1 分。
    *   通過 Medium Baseline (Private)：+1 分。
    *   通過 Strong Baseline (Public)：+1 分。
    *   通過 Strong Baseline (Private)：+1 分。
*   **Code Upload (程式碼上傳)**：
    *   將程式碼上傳至 NTU COOL：+4 分。**未上傳則此 4 分無法獲得。**
*   **Kaggle Baseline 顯示**：
![image](https://hackmd.io/_uploads/By6cYMDble.png)
    *   Strong Baseline 若過於困難，助教可能會進行調整。


## Code Submission (程式碼提交)
 外校，略過。
<!--

*   **平台**：NTU COOL (佔 4 分)。
*   **格式**：將你的程式碼 (以及符合條件的報告) 壓縮成一個 `.zip` 檔案。
*   **命名方式**：`<你的學號>_hw1.zip` (例如：`b06901020_hw1.zip`)。
*   **注意事項**：
    *   我們只會批改你最後一次上傳的檔案。
    *   **請勿上傳你的模型檔案 (`.pth`) 或我們提供的資料集 (`.csv`)**，以免檔案過大。
    *   若檢查發現程式碼無法理解、可讀性差或有作弊嫌疑，學期總成績將直接乘以 0.9。
    *   **必須註明程式碼來源**：若使用了他人 (如助教 Sample Code、GitHub) 的程式碼片段，需在程式碼底部或報告中加入 Reference 區塊，清楚標示來源。
        *   範例：
            ```
            Reference
            Source: Heng-Jui Chang @ NTUEE (https://github.com/ga642381/ML2021-Spring/blob/main/HW01/HW01.ipynb)
            ```
*   **.zip 檔案內容**：
    *   **Code (程式碼)**：`.py` 檔案或 `.ipynb` (Jupyter Notebook) 檔案。
    *   **Report (報告)**：`.pdf` 檔案 (僅限獲得 10 分並希望爭取額外加分的同學提交)。
*   **從 Google Colab 下載程式碼**：
    *   在 Colab 介面，點選左上角 "File" (檔案) -> "Download" (下載) -> "Download .ipynb"。
*   **壓縮檔案教學**：
    *   [Windows](https://support.microsoft.com/en-us/windows/zip-and-unzip-files-f6dde0a7-0fec-8294-e1d3-703ed85e7ebc)：在檔案或資料夾上按右鍵 -> "傳送到" -> "壓縮的 (zipped) 資料夾"。
    *   [Mac](https://support.apple.com/guide/mac-help/mchlp2528/mac)：選取檔案或資料夾後，按右鍵 (或 Control + 點擊) -> "壓縮..."。
    *   Command Line (指令列)：`zip -r <壓縮檔名>.zip <要壓縮的資料夾或檔案>` (例如：`zip -r b06901020_hw1.zip b06901020_hw1/`)
-->

---
## Hints (提示)

*   **Simple Baseline (簡單基準線)**：
    *   執行助教提供的 Sample Code 即可通過。
*   **Medium Baseline (中等基準線)**：
    *   **Feature Selection (特徵選取)**：僅使用 40 個州的 One-hot encoding 特徵以及 `tested_positive` 相關的特徵 (在 Sample Code 的 `COVID19Dataset` class 中，提示為 indices = 57 & 75，對應 `target_only=True` 時的選取邏輯)。
    *   在 Sample Code 中，修改 `target_only = True` 即可。
*   **Strong Baseline (進階基準線)**：
    *   **Feature Selection (特徵選取)**：思考除了州別和 `tested_positive` 外，還有哪些特徵可能有用？
    *   **DNN architecture (神經網路架構)**：調整網路層數、每層的神經元數量、活化函數 (activation function) 等。
    *   **Training (訓練過程)**：調整 mini-batch 大小 (batch size)、選用不同的 optimizer、調整學習率 (learning rate)。
    *   **L2 Regularization (L2 正則化)**：或其他正則化方法，幫助模型泛化 (generalize)、避免 overfitting。
    *   **Sample Code 的小細節**：助教提供的 Sample Code 雖然能過基本 Baseline，但可能存在一些可以調整或改進的小地方，找到並修正它們可能有助於達到 Strong Baseline。

---
## 重要規定

1.  作業必須獨立完成。
2.  不可手動修改模型預測輸出的 `.csv` 檔案。
3.  禁止與任何同學或他人分享你的程式碼或預測結果檔案。
4.  禁止使用任何不正當手段，使得每日 Kaggle 上傳次數超過 5 次。
5.  **嚴禁搜尋、使用任何額外的資料或預訓練模型。僅能使用提供的資料。**

---
## Useful Links

*   **李宏毅老師課程錄影**：
    *   Regression & Gradient Descent (Mandarin)：[[1]](https://www.youtube.com/watch?v=fegAeph9UaA) [[2]](https://www.youtube.com/watch?v=L_hIs0jIIxI) [[3]](https://www.youtube.com/watch?v=d1499b--h0M) [[4]](https://www.youtube.com/watch?v=kPxpAZbL7qs) [[5]](https://www.youtube.com/watch?v=f8NG022qg94) [[6]](https://www.youtube.com/watch?v=Dwp3L94tbeI)
    *   Tips for Training Deep Networks (Mandarin)：[[1]](https://www.youtube.com/watch?v=x0aokz9xKkM) [[2]](https://www.youtube.com/watch?v=Ta0c3N0tCQQ)
*   **Google Machine Learning Crash Course (English)**：
    *   [Regularization](https://developers.google.com/machine-learning/crash-course/regularization/video-lecture)
    *   [NN Training](https://developers.google.com/machine-learning/crash-course/introduction-to-neural-networks/video-lecture)
*   **[PyTorch Official Documentation](https://pytorch.org/docs/stable/index.html) (PyTorch 官方文件)**

---

# HW1 Sample Code 解析

## 下載資料 (Download Data)

下載 `covid.train.csv` (訓練資料) 和 `covid.test.csv` (測試資料) 到 Colab 的工作環境中。

```python=
tr_path = 'covid.train.csv'  # path to training data
tt_path = 'covid.test.csv'   # path to testing data

!gdown --id '19CCyCgJrUxtvgZF53vnctJiOJ23T5mqF' --output covid.train.csv
!gdown --id '1CE240jLm2npU-tdz81-oVKEF3T2yfT1O' --output covid.test.csv
```

---
## 匯入所需套件 (Import Some Packages)

```python=
# PyTorch
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader

# For data preprocess
import numpy as np
import csv    # 讀寫 CSV 檔案的模組
import os     # 作業系統相關功能的模組，如此處用於建立資料夾

# For plotting
import matplotlib.pyplot as plt        # 用於視覺化結果
from matplotlib.pyplot import figure   # 設定圖片大小

myseed = 42069  # 設定一個固定的隨機種子
torch.backends.cudnn.deterministic = True # 確保在使用 cuDNN 時，每次運行的結果一致 (可能會犧牲一點效能)
torch.backends.cudnn.benchmark = False    # 如果網路架構固定，設為 `True` 通常能加速，但為了重現性此處設為 `False`
np.random.seed(myseed)        # 設定 NumPy 的隨機種子
torch.manual_seed(myseed)     # 設定 PyTorch 在 CPU 上的隨機種子
if torch.cuda.is_available():            # 如果 CUDA (GPU) 可用
    torch.cuda.manual_seed_all(myseed)   # 則設定 PyTorch 在所有 GPU 上的隨機種子
```
*   **設定隨機種子**的目的是為了確保實驗的可重現性 (reproducibility)，即每次執行程式碼時，隨機過程 (如權重初始化、資料打亂) **都會產生相同的結果**。

---
## 工具函數 (Some Utilities)

這部分包含一些輔助函數，不需要修改。

```python=
# 彈性地在有 GPU 或只有 CPU 的環境下執行
def get_device():
    return 'cuda' if torch.cuda.is_available() else 'cpu'

# 顯示訓練過程中的訓練損失 (train loss) 和驗證損失 (dev loss)
# loss_record = 一個字典，應包含 `'train'` 和 `'dev'`兩個 key，其值為 Loss 的列表
def plot_learning_curve(loss_record, title=''):
    total_steps = len(loss_record['train'])    # 訓練的總迭代次數
    
    # x_1: 訓練損失的 x 軸座標 (每個 step)。
    # x_2: 驗證損失的 x 軸座標。通常不是每個 step 都驗證，所以這裡做了取樣，使其點數與 `loss_record['dev']` 長度一致。
    x_1 = range(total_steps)
    x_2 = x_1[::len(loss_record['train']) // len(loss_record['dev'])]
    
    figure(figsize=(12, 6))    # 設定圖片大小
    plt.plot(x_1, loss_record['train'], c='tab:red', label='train')
    plt.plot(x_2, loss_record['dev'], c='tab:cyan', label='dev')
    plt.ylim(0.0, 5.)        # 設定 y 軸的範圍
    
    # 設定圖表的標籤、標題和圖例，並顯示圖表
    plt.xlabel('Training steps')
    plt.ylabel('MSE loss')
    plt.title('Learning curve of {}'.format(title))
    plt.legend()
    plt.show()

# 繪製模型的預測值與真實值的散佈圖
# dv_set = 驗證集的 DataLoader
# model = 訓練好的模型
# device = 運算裝置 ('cpu' 或 'cuda')
# lim = 圖表 x, y 軸的上限
# preds, @targets: (可選參數)
#                   如果提供了預先計算好的預測值和目標值，則直接使用；
#                   否則，函數會遍歷 `dv_set` 來計算。
def plot_pred(dv_set, model, device, lim=35., preds=None, targets=None):
    if preds is None or targets is None:
        model.eval()    # 將模型設定為評估模式
                        # 會關閉 Dropout 和 Batch Normalization 的更新
        preds, targets = [], []
        for x, y in dv_set:    # 遍歷驗證集，獲取預測值 `pred` 和真實值 `y`
            x, y = x.to(device), y.to(device)
            with torch.no_grad():    # 不計算梯度，節省記憶體並加速推論
                pred = model(x)
                
                # 將 tensor 從計算圖中分離出來 (detach)，並移至 CPU
                # 方便後續轉換為 NumPy 陣列
                preds.append(pred.detach().cpu())
                targets.append(y.detach().cpu())
        preds = torch.cat(preds, dim=0).numpy()
        targets = torch.cat(targets, dim=0).numpy()

    figure(figsize=(8, 8))
    plt.scatter(targets, preds, c='r', alpha=0.5)    # 繪製紅色散佈圖
    plt.plot([-0.2, lim], [-0.2, lim], c='b')        # 繪製一條 y=x 的藍色對角線
    
    # 設定圖表的標籤、標題和圖例，並顯示圖表
    plt.xlim(-0.2, lim)
    plt.ylim(-0.2, lim)
    plt.xlabel('ground truth value')
    plt.ylabel('predicted value')
    plt.title('Ground Truth v.s. Prediction')
    plt.show()
```

---
## 資料預處理 (Preprocess)

### `COVID19Dataset` 類別

```python=
class COVID19Dataset(Dataset):
    # 初始化函數
    def __init__(self,
                 path,
                 mode='train',
                 target_only=False):
        self.mode = mode

        # 讀取資料，轉換成 numpy arrays
        with open(path, 'r') as fp:
            data = list(csv.reader(fp))
            data = np.array(data[1:])[:, 1:].astype(float)    # 跳過第一 row, col
        
        # 特徵選取
        if not target_only:
            feats = list(range(93)) # 選擇原始資料中的所有 93 個特徵
        else:
            ''' 達成 Medium Baseline 的關鍵!!!!!!!!!!!!!'''
            # Using 40 states & 2 tested_positive features (indices = 57 & 75)
            pass

        # 模式判斷與資料處理
        if mode == 'test':        # data: 893筆 x 93維
            data = data[:, feats]    # 根據 `feats` 選取特徵
            self.data = torch.FloatTensor(data)
        else:                     # data: 2700筆 x 94維 (train/dev sets)
            target = data[:, -1]     # 取出最後一欄作為目標值 (target)
            data = data[:, feats]    # 根據 `feats` 選取特徵 (無第94維)

            if mode == 'train':
                # 每 10 筆資料取 9 筆作為 訓練集
                indices = [i for i in range(len(data)) if i % 10 != 0]
            elif mode == 'dev':
                # 每 10 筆資料取 1 筆作為 驗證集
                indices = [i for i in range(len(data)) if i % 10 == 0]

            # 特徵資料、目標值 轉換成 PyTorch tensors
            self.data = torch.FloatTensor(data[indices])
            self.target = torch.FloatTensor(target[indices])

        # 特徵正則化 (可移除做對照實驗)
        # 只對州別之後的特徵進行正則化
        # Z-score 正則化公式
        self.data[:, 40:] = \
            (self.data[:, 40:] - self.data[:, 40:].mean(dim=0, keepdim=True)) \
            / self.data[:, 40:].std(dim=0, keepdim=True)

        self.dim = self.data.shape[1]    # 儲存最終特徵的維度

        # 印出資料集讀取完成的資訊
        print('Finished reading the {} set of COVID19 Dataset ({} samples found, each dim = {})'
              .format(mode, len(self.data), self.dim))

    # 根據索引 `index` 返回一筆資料
    def __getitem__(self, index):
        if self.mode in ['train', 'dev']:
            # 返回 特徵 和 target
            return self.data[index], self.target[index]
        else:
            # 只返回 特徵
            return self.data[index]

    # 取得資料集大小
    def __len__(self):
        return len(self.data)    # 返回資料集的總筆數
```
*   這個類別繼承自 `torch.utils.data.Dataset`，是 PyTorch 中用來自訂資料集的標準方式。
*   **`__init__`**：
    > [!Tip]可實作特定的「特徵選取」以獲得更好的結果。
    > **Simple**：(無)
    > **Medium**：**修改`feats`**：陽性率。
    > ```python=19
    > feats = list(range(40)) + [57, 75]
    > ```
    > **Strong**：
    > 1. **進階修改`feats`**：類流感症狀 (CLI)、家人有類流感症狀、社區有類流感症狀、陽性率。
    > ```python=19
    > feats = list(range(40)) + [75, 57, 42, 60, 78, 43, 61, 79, 40, 58, 76]
    > ```
    > 2. **修改 Z-score `feats` 正則化的參數 (均值、標準差)**：改為僅從`training_set`計算，然後應用到所有 set 上。
    > 使用 `sklearn.preprocessing.StandardScaler`，在訓練數據上 `fit_transform`，然後在驗證和測試數據上只 `transform`。
    > ```python=
    > from sklearn.preprocessing import StandardScaler
    > scaler = None    # 全域 scaler 變數
    > ```
    > ```python=22
    > # 模式判斷與資料處理
    > global scaler  # 使用全域 scaler
    > if mode == 'test':        # data: 893筆 x 93維
    >     data = data[:, feats]    # 根據 `feats` 選取特徵
    >     # --- 使用訓練集 fit 好的 scaler 進行 transform ---
    >     if scaler is not None:
    >         data[:, 40:] = scaler.transform(data[:, 40:])
    >     else:
    >         raise RuntimeError('scaler 尚未 fit，請先建立訓練集 Dataset！')
    >     self.data = torch.FloatTensor(data)
    > else:                     # data: 2700筆 x 94維 (train/dev sets)
    >     target = data[:, -1]     # 取出最後一欄作為目標值 (target)
    >     data = data[:, feats]    # 根據 `feats` 選取特徵 (無第94維)
    > ```
    > ```python=41
    > # 特徵正則化
    > # 特徵資料、目標值 轉換成 numpy
    > data_split = data[indices]
    > # --- 訓練集：fit_transform，驗證集：transform ---
    > if mode == 'train':
    >     scaler = StandardScaler()  # 建立 scaler
    >     data_split[:, 40:] = scaler.fit_transform(data_split[:, 40:])  # 只 fit_transform 州別之後的特徵
    > else:
    >     if scaler is not None:
    >         data_split[:, 40:] = scaler.transform(data_split[:, 40:])
    >     else:
    >         raise RuntimeError('scaler 尚未 fit，請先建立訓練集 Dataset！')
    > self.data = torch.FloatTensor(data_split)
    > self.target = torch.FloatTensor(target[indices])
    > ```
*   **`__getitem__(self, index)` (取得單筆資料)**：是 Dataset 類別必須實作的方法。
*   **`__len__(self)` (取得資料集大小)**：是 Dataset 類別必須實作的方法。

### `prep_dataloader` 函數
建立一個 `COVID19Dataset` 物件，然後將它包裝成一個 `DataLoader` 物件。
```python=
# path = 資料檔案路徑。
# mode = 'train', 'dev', 或 'test'。
# batch_size = 每個批次的大小。
# n_jobs = 用於資料載入的子行程數量 (0 表示在主行程中載入)。
# target_only = 傳遞給 COVID19Dataset 的參數，用於特徵選取。
def prep_dataloader(path, mode, batch_size, n_jobs=0, target_only=False):
    dataset = COVID19Dataset(path, mode=mode, target_only=target_only)
    dataloader = DataLoader(
        dataset, batch_size,
        shuffle=(mode == 'train'), # 只有在訓練模式時才打亂資料順序
        drop_last=False,           # 若最後一批資料數量不足 `batch_size`，是否丟棄？
        num_workers=n_jobs,        # 使用多少個子行程來預先載入資料，可以加速訓練
        pin_memory=True)    # 將 Tensors 複製到 CUDA 固定記憶體 (pinned memory) 中
                            # 可以加速從 CPU 到 GPU 的資料傳輸
    return dataloader
```

---
## 深度神經網路 (Deep Neural Network)

### `NeuralNet` 類別

```python=
class NeuralNet(nn.Module):
    # 初始化函數 (定義神經網路)
    def __init__(self, input_dim):
        super(NeuralNet, self).__init__()   # 呼叫父類 nn.Module 的初始化函數

        ''' 定義神經網路!!!!!!!!!!!!!!!!!! '''
        # 如何修改此模型以獲得更好效能？
        # 增加層數、改變神經元數量、使用不同活化函數...
        self.net = nn.Sequential(     # 宣告一個容器，將多個 Layer 按順序組合起來
            nn.Linear(input_dim, 64), # 全連接層，輸入 `input_dim`維、輸出 64 維
            nn.ReLU(),                # ReLU
            nn.Linear(64, 1)          # 另一個全連接層，輸出 1 維 = 迴歸問題的預測值
        )

        # 定義損失函數 MSE
        self.criterion = nn.MSELoss(reduction='mean')

    # 前向傳播函數
    def forward(self, x):       # x 的大小應為 (batch_size, input_dim)
        return self.net(x).squeeze(1)   # 將 x 傳遞給 self.net 中定義的網路結構
                                        # .squeeze(1) 移除編號為 1 的維度
                                        # 匹配 target 的形狀 (batch_size)

    # 計算損失函數
    def cal_loss(self, pred, target):
        # 計算預測值 pred 和目標值 target 之間的損失
        # 可以在此處實作 L1/L2 正則化 (regularization)
        return self.criterion(pred, target) # 直接用 MSE 損失函數計算
```
*   這個類別繼承自 `torch.nn.Module`，是 PyTorch 中建立所有神經網路模型的基礎類別。
*   **`__init__(self, input_dim)` (初始化/定義神經網路函數)**：
    > [!Tip] 可以修改此模型以獲得更好效能。
    > (例如增加層數、改變神經元數量、使用不同活化函數等)。
    > **Simple**：(無)
    > **Medium**：(無)
    > **Strong**：引入 **批次標準化**、**Dropout**、**~~多層 ReLU~~**(效果不好)
    > ```python=9
    > self.net = nn.Sequential(     # 宣告一個容器，將多個 Layer 按順序組合起來
    >     nn.Linear(input_dim, 32), # 全連接層，輸入 `input_dim`維、輸出 32 維
    >     nn.BatchNorm1d(32),       # 批次標準化，批次要大一點
    >     nn.LeakyReLU(),           # LeakyReLU
    >     nn.Dropout(p=0.1),
    > 
    >     nn.Linear(32, 1)          # 另一個全連接層，輸出 1 維 = 迴歸問題的預測值
    > )
    > ```
*   **`forward(self, x)` (前向傳播函數)**：是 `nn.Module` 必須實作的方法。
*   **`cal_loss(self, pred, target)` (計算損失函數)**：自訂的輔助函數。
    > [!Tip] 可以在此處實作 L1/L2 正則化 (regularization)。
    > **Simple**：(無)
    > **Medium**：(無)
    > **Strong**：引入 ~~**L1 正則化**~~(效果不好，改回手動特徵選取) 和 **L2 正則化** (在`Optimizer`的參數引入`weight_decay`)

---
## 訓練/驗證/測試 函數 (Train/Dev/Test Functions)

### `train` 函數 (訓練模型)
執行神經網路的訓練過程。
```python=
# tr_set = 訓練集的 DataLoader
# dv_set = 驗證集的 DataLoader
# model = 要訓練的模型
# config = 包含"訓練超參數"的配置
# device = 運算裝置
def train(tr_set, dv_set, model, config, device):
    n_epochs = config['n_epochs']  # 最大訓練週期數

    # 設定 optimizer
    # 從模組動態獲取 config 中指定的優化器，如 SGD, Adam
    optimizer = getattr(torch.optim, config['optimizer'])(
        model.parameters(),         # 傳入模型所有可訓練的參數
        **config['optim_hparas'])   # 傳入 config 中，該 optimizer 的超參數 (如lr, momentum)

    # 宣告變數
    min_mse = 1000.     # 記錄最低的驗證集 MSE，初始設為一個較大的值
    loss_record = {'train': [], 'dev': []}  # 紀錄 loss
    early_stop_cnt = 0                      # early stopping 計數器
    epoch = 0                               # 目前的 epoch 計數
    while epoch < n_epochs:
        model.train()                           # training mode

        # 批次迴圈
        for x, y in tr_set:
            optimizer.zero_grad()               # 清除前一輪的梯度
            x, y = x.to(device), y.to(device)   # 資料移至指定的運算裝置(cpu/cuda)
            pred = model(x)                     # forwarding，得到預測值
            mse_loss = model.cal_loss(pred, y)  # 計算 loss
            mse_loss.backward()                 # backpropagation，計算梯度
            optimizer.step()                    # 更新模型的權重
            loss_record['train'].append(mse_loss.detach().cpu().item())

        # 驗證與模型儲存
        dev_mse = dev(dv_set, model, device)    # 評估模型，得到驗證集 MSE
        if dev_mse < min_mse:       # if 進步了
            min_mse = dev_mse
            print('Saving model (epoch = {:4d}, loss = {:.4f})'
                .format(epoch + 1, min_mse))
            torch.save(model.state_dict(), config['save_path'])  # 儲存模型的狀態配置
            early_stop_cnt = 0      # 重置早停計數器，因為模型有進步
        else:
            early_stop_cnt += 1     # 早停計數器加一

        epoch += 1
        loss_record['dev'].append(dev_mse)

        # 早停檢查
        if early_stop_cnt > config['early_stop']:
            break       # 太久都沒進步，提前終止訓練

    print('Finished training after {} epochs'.format(epoch))
    return min_mse, loss_record
```

### `dev` 函數 (驗證模型)
在驗證集上評估模型的表現 (計算平均 MSE)。
```python=
def dev(dv_set, model, device):
    model.eval()    # 評估模式 (關閉 Dropout 和 Batch Normalization 的更新，確保評估結果的一致性)
    total_loss = 0
    for x, y in dv_set: # 驗證集的 DataLoader
        x, y = x.to(device), y.to(device)
        with torch.no_grad():                   # 不需更新權重
            pred = model(x)                     # forwarding，得到預測值
            mse_loss = model.cal_loss(pred, y)  # 計算 loss
        total_loss += mse_loss.detach().cpu().item() * len(x)  # 累加各批次的總 loss
    total_loss = total_loss / len(dv_set.dataset)        # 計算整個驗證集的平均 MSE

    return total_loss
```

### `test` 函數 (測試模型並產生預測)
使用訓練好的模型在測試集上進行預測。
```python=
def test(tt_set, model, device):
    model.eval()    # 評估模式 (關閉 Dropout 和 Batch Normalization 的更新，確保評估結果的一致性)
    preds = []
    for x in tt_set:    # 測試集的 DataLoader (注意：沒有 y)
        x = x.to(device)
        with torch.no_grad():                   # 不需更新權重
            pred = model(x)                     # forwarding，得到預測值
            preds.append(pred.detach().cpu())   #  將預測結果加入列表
    preds = torch.cat(preds, dim=0).numpy()     # 將列表中所有批次的預測結果，沿第 0 維串接起來
                                                # 形成一個大的 tensor
                                                # 再轉換為 NumPy 陣列
    return preds
```

---
## 設定超參數 (Setup Hyper-parameters)

```python=
device = get_device()
os.makedirs('models', exist_ok=True)  # 建立目錄 /models/ 來儲存訓練好的模型
target_only = False                   # 控制特徵選取

'''如何調整超參數以提升模型效能??????????????????????'''
config = {
    'n_epochs': 3000,                # 最大 epochs 數
    'batch_size': 270,               # DataLoader 的批次大小
    'optimizer': 'SGD',              # optimizer 的名稱，如 'SGD', 'Adam' (in torch.optim)
    'optim_hparas': {                # optimizer 的超參數
        'lr': 0.001,                 # learning rate of SGD
        'momentum': 0.9              # momentum for SGD
    },
    'early_stop': 200,               # 早停的容忍週期數
    'save_path': 'models/model.pth'  # 儲存最終模型的路徑
}
```
> [!Tip] 如何調整這些超參數以提升模型效能？
> **Simple**：(無)
> **Medium**：啟用特徵選取。
> ```python=3
> target_only = True
> ```
> **Strong**：
> 1. `optimizer`改用 [Adam](https://hackmd.io/@Jaychao2099/imrobot3#Adam) + L2 正則化 (減少 overfitting) = **AdamW**。
> 2. **加大`lr`**，交給`optimizer`自動處理；配合`lr`，**加大`weight_decay`**。
> ```python=3
> target_only = True
> ```
> ```python=6
> config = {
>     'n_epochs': 10000,               # 反正會早停
>     'batch_size': 128,               # 多次嘗試得出
>     'optimizer': 'AdamW',
>     'optim_hparas': {
>         'lr': 0.05,                # learning rate
>         'weight_decay': 0.1,       # L2 regularization
>     },
>     'early_stop': 200,
>     'save_path': 'models/model.pth',
> }
> ```

---
## 載入資料與模型 (Load data and model)

```python=
# 準備 DataLoader
tr_set = prep_dataloader(tr_path, 'train', config['batch_size'], target_only=target_only)
dv_set = prep_dataloader(tr_path, 'dev', config['batch_size'], target_only=target_only)
tt_set = prep_dataloader(tt_path, 'test', config['batch_size'], target_only=target_only)

# 建立初始模型
model = NeuralNet(tr_set.dataset.dim).to(device)
```

---
## 開始訓練 (Start Training!)

```python=
# model_loss 會是 最佳的驗證集 的 MSE
# model_loss_record 會是 包含訓練和驗證損失歷史 的 字典
model_loss, model_loss_record = train(tr_set, dv_set, model, config, device)
```

---
## 繪製訓練結果 (Plotting Results)

```python=
# 繪製學習曲線
plot_learning_curve(model_loss_record, title='deep model')

del model       # 刪除當前的 model 物件 (可選，為了確保載入的是全新的模型狀態)

model = NeuralNet(tr_set.dataset.dim).to(device)    # 重新建立一個和訓練時相同結構的模型
ckpt = torch.load(config['save_path'], map_location='cpu')  # 載入最佳的模型配置
model.load_state_dict(ckpt)       # 載入的配置應用到新建立的模型上，恢復訓練好的權重
plot_pred(dv_set, model, device)  # 繪製模型的預測值與真實值的散佈圖
```

---
## 測試模型並儲存預測結果 (Testing)

```python=
# 將預測結果儲存為 Kaggle 要求的 CSV 格式
def save_pred(preds, file):
    print('Saving results to {}'.format(file))
    with open(file, 'w') as fp:     #  開啟指定的檔案進行寫入
        writer = csv.writer(fp)     # 建立 CSV 寫入器
        writer.writerow(['id', 'tested_positive'])  # 寫入表頭
        for i, p in enumerate(preds):
            writer.writerow([i, p]) # 逐行將 索引i、預測值p 寫入 CSV

preds = test(tt_set, model, device)  # 用模型預測 COVID-19 陽性數量
save_pred(preds, 'pred.csv')         # 將預測結果儲存到 pred.csv
```

---
## 結果

### 1. Simple Baseline：
:::info
直接執行範例程式碼。
:::
*   **Training**： 1524 epoch, loss = 0.7593
![image](https://hackmd.io/_uploads/SJTZRAKbxe.png)
*   **Validation**：
![image](https://hackmd.io/_uploads/ByVXACY-xg.png)
*   **Testing**：
    * Public Score：1.36584
    * Private Score：1.45539

### 2. Medium Baseline：
:::info
關鍵在於特徵選取，只使用 40 個州的 one-hot 特徵和 2 個 `tested_positive` 相關的特徵 (注意：超參數 `target_only` 記得設為  `True`)。
:::
*   **Training**： 519 epoch, loss = 0.9614
![image](https://hackmd.io/_uploads/ByG3aAKbel.png)
*   **Validation**：
![image](https://hackmd.io/_uploads/BkXRTCFbgl.png)
*   **Testing**：
    * Public Score：1.05926
    * Private Score：1.06473

### 3. Strong Baseline：
:::info
*   更進階的特徵選取。
*   調整 DNN 架構 (維度、活化函數、批次標準化、Dropout)。
*   調整訓練參數 (mini-batch 大小、Optimizer、學習率)。
*   加入 L2 正則化。
*   修改 Features Z-score 正則化的方式。
:::
*   **Training**： 623 epoch, loss = 0.8781
![image](https://hackmd.io/_uploads/rJNpg8ibex.png)
*   **Validation**：
![image](https://hackmd.io/_uploads/S1EUZLibxx.png)
*   **Testing**：
    * Public Score：0.92540
    * Private Score：0.96652

> 未通過 Strong Baseline，差的遠了。已放棄。

---

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