# 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)** 的人數比例。作業的目標即是利用其他資訊來預測這條橘色線。

* **輸入與輸出**:
* 會提供美國某特定州**過去三天**的調查資料。
* 需要利用這些資料來**預測第三天**新檢測出的陽性案例百分比。
* **例如**:提供第一天的調查資料與陽性比例、第二天的資料與陽性比例,以及第三天的調查資料 (但不包含陽性比例),學生需預測第三天的陽性比例。

* **重要規定**:
* **嚴禁使用任何我們提供資料以外的額外資料或預訓練模型 (pre-trained models)。**
---
## Data (資料說明)
### Delphi's COVID-19 Surveys (Delphi 研究小組的 COVID-19 調查)
* **調查方式**:針對美國各州,每日透過 Facebook 隨機抽樣數百人進行問卷調查。
* **調查內容**:包含是否有 COVID-19 類似症狀、流感症狀、是否進行檢測、是否居家隔離、是否戴口罩、是否跨州移動、心理健康狀況 (焦慮、憂鬱) 等。
* **資料處理流程**:從特定州的總人口中抽樣部分人進行問卷調查,再根據這些樣本的結果去估計整個州的情況。我們使用的就是這個估計後的資料。

### 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。

### Data Structure (資料結構)
* 每一列 (row) 代表一個樣本 (sample)。
* **Training Data (訓練資料)** (`covid.train.csv`):
* 共 2700 筆樣本。
* 每筆樣本維度:
* **前 40 維**:州別的 One-hot encoding。
* **接下來 18 維**:第一天的特徵 (包含上述症狀、行為、心理健康指標,以及當日的 `tested_positive` 比例)。
* **再接下來 18 維**:第二天的特徵 (同上)。
* **最後 18 維**:第三天的特徵 (同上,最後一欄為 `tested_positive`,即預測目標)。

* **Testing Data (測試資料)** (`covid.test.csv`):
* 共 893 筆樣本。
* 與訓練資料相比,第三天的特徵少了最後一個 `tested_positive` 欄位,因為這是需要預測的目標。因此第三天只有 17 維特徵。

---
## 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 可能沒有分數。

---
## 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 顯示**:

* 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

* **Validation**:

* **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

* **Validation**:

* **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

* **Validation**:

* **Testing**:
* Public Score:0.92540
* Private Score:0.96652
> 未通過 Strong Baseline,差的遠了。已放棄。
---
回[主目錄](https://hackmd.io/@Jaychao2099/aitothemoon)