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