2023-01-17 02:28 由於文章發佈的時候,附檔還未通過審核,後續會再補上
2023-01-17 13:17 已補上下載鏈接
本文將針對李宏毅教授的課程 NTU ML2022 Spring 作業 1 ,助教提供的代碼,以段落的方式進行分析,且每個小節的代碼是連續的如果要複製粘貼別忘了前面的縮排 (tab)。
我個人的一些修改(會加上註解 # By CLC
),最後會分享我的預測資料上傳到 Kaggle 驗證的結果。
由於助教的代碼有兩個版本,有興趣的朋友可以在文末自行下載。另外 Traning Data 的網址也有點變動,為了方便起見也會一並上傳供大家下載。
如果你的電腦已經設定好環境,助教提供的 Sample Code 是能夠直接運行的。
如果沒有的話,可以參考我寫的這篇文章環境建立筆記(macOS)先建立環境。
以下會以我個人根據附檔中 Sample Code 1 修改後的代碼進行講解,若有引用到一些相關的文章,也會加入連結在其中。
這部分匯入需要的模組,其中包含數學、PyTorch等相關的模組
# Numerical Operations
import math
import numpy as np
# Reading/Writing Data
import pandas as pd
import os
import csv
# For Progress Bar
from tqdm import tqdm
# Pytorch
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader, random_split
# For plotting learning curve
from torch.utils.tensorboard import SummaryWriter
下面 Plot 模組是我參考Sample Code 2 加入的模組,用來圖形化顯示訓練過程。
Time 模組則是最後輸出預測結果時,在檔名上加入時間戳記用。
# By CLC
import matplotlib.pyplot as plt
from matplotlib.pyplot import figure
# By CLC
import time
下面功能是為了確保隨機種子所執行的模型訓練,能夠再次復現而做的設定。
def same_seed(seed):
'''Fixes random number generator seeds for reproducibility.'''
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
這部分則是將數據分成驗證用、訓練用兩部分。
valid_ratio
分別計算驗證用 valid_set_size
、訓練用 train_set_size
的數據量。[train_set_size, valid_set_size]
決定 train_set
的大小 valid_set
。def train_valid_split(data_set, valid_ratio, seed):
'''Split provided training data into training set and validation set'''
valid_set_size = int(valid_ratio * len(data_set))
train_set_size = len(data_set) - valid_set_size
train_set, valid_set = random_split(data_set, [train_set_size, valid_set_size], generator=torch.Generator().manual_seed(seed))
return np.array(train_set), np.array(valid_set)
Predict
是用來執行我們的模型以及進行預測數據的保存
首先把模型切換到 evaluation 模式停止模型的訓練。接著透過 for 迴圈執行 test_loader 中的數據,並且每次的預測結果會接在上一次的結果之後,形成一個一維的數據,最後輸出。
def predict(test_loader, model, device):
model.eval() # Set your model to evaluation mode.
preds = []
for x in tqdm(test_loader):
x = x.to(device)
with torch.no_grad():
pred = model(x)
preds.append(pred.detach().cpu())
preds = torch.cat(preds, dim=0).numpy()
return preds
這部分是根據 Sample Code 2 加入的繪圖功能,用來顯示訓練過程的 Loss。
def plot_learning_curve(loss_record, title=''):
''' Plot learning curve of your DNN (train & dev loss) '''
total_steps = len(loss_record['train'])
x_1 = range(total_steps)
x_2 = x_1[::len(loss_record['train']) // len(loss_record['dev'])]
figure(figsize=(6, 4))
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.)
plt.xlabel('Training steps')
plt.ylabel('MSE loss')
plt.title('Learning curve of {}'.format(title))
plt.legend()
plt.show()
這部分是定義數據及的類,用到了 __init__
、__getitem__
以及 __len__
,可以參考我寫的這篇說明。
#Python_Magic Methods: getitem, setitem, len 的說明與範例
class COVID19Dataset(Dataset):
'''
x: Features.
y: Targets, if none, do prediction.
'''
def __init__(self, x, y=None):
if y is None:
self.y = y
else:
self.y = torch.FloatTensor(y)
self.x = torch.FloatTensor(x)
def __getitem__(self, idx):
if self.y is None:
return self.x[idx]
else:
return self.x[idx], self.y[idx]
def __len__(self):
return len(self.x)
這部分是定義神經網路模型,也是作業內容之一,需要自行去決定網路的深度、寬度,以及激勵函數 (activation function)。
參考李宏毅教授上課的投影片,可以更清楚這部分在做的事情。
class My_Model(nn.Module):
def __init__(self, input_dim):
super(My_Model, self).__init__()
# TODO: modify model's structure, be aware of dimensions.
self.layers = nn.Sequential(
# By CLC
nn.Linear(input_dim, 36),
nn.ReLU(),
nn.Linear(36, 25),
nn.ReLU(),
nn.Linear(25, 15),
nn.ReLU(),
nn.Linear(15, 1)
)
def forward(self, x):
x = self.layers(x)
x = x.squeeze(1) # (B, 1) -> (B)
return x
這部分也是作業內容之一,需要去選擇適合模型訓練的數據,Sample Code 1 & 2 都是將整個數據輸入到模型中訓練,會使模型訓練成本變高之外,也會導致模型預測的失準。
因此在這部分我定義了新的 feature 範圍。
至於訓練資料中的欄位代表哪些意思,可以參考這篇文章:
2021李宏毅机器学习课程作业一
def select_feat(train_data, valid_data, test_data, select_all):
'''Selects useful features to perform regression'''
y_train, y_valid = train_data[:,-1], valid_data[:,-1]
raw_x_train, raw_x_valid, raw_x_test = train_data[:,:-1], valid_data[:,:-1], test_data
if select_all:
feat_idx = list(range(raw_x_train.shape[1]))
else:
# By CLC
feat_idx = np.r_[1:37,38:41,53,54:57,69,70:73,85,86:89,101]
return raw_x_train[:,feat_idx], raw_x_valid[:,feat_idx], raw_x_test[:,feat_idx], y_train, y_valid
這個小節由於有許多的 Hyperparameters 要調整,於是透過下面小節的字典來輸入參數,以方便後續的調試。
此作業的 Loss 函數是以 Mean Square Error 為計算,公式如下:
M E S = 1 n ∑ i = 1 n ( y i − y I ^ ) 2 MES=\frac{1}{n}\sum_{i=1}^{n}(y_{i}-\hat{y_{I}})^2 MES=n1i=1∑n(yi−yI^)2
optimizer 的部分需要去嘗試 PyTorch 不同的優化器以及優化器的參數,這部分也是作業內容之一,當然如果是用 TenserFlow 的話也可以,這邊還是以 PyTorch 為主要說明。這部分我採用的是 ADAM 進行優化,並且加入 L2 Penalty。
正則化我們最常使用的就是 L1 Regularization & L2 Regularization,這兩種方式其實就是在 Loss Function 中加上對應的 L1 及 L2 penalty (懲罰項)1。
Plot_Data
則是我用來保存每次的 Loss 用來最後圖形化顯示的集合,
# Training Loop
def trainer(train_loader, valid_loader, model, config, device):
criterion = nn.MSELoss(reduction='mean') # Define your loss function, do not modify this.
# Define your optimization algorithm.
# TODO: Please check https://pytorch.org/docs/stable/optim.html to get more available algorithms.
# TODO: L2 regularization (optimizer(weight decay...) or implement by your self).
# By CLC
optimizer = torch.optim.Adam(model.parameters(), lr=config['learning_rate'], betas=(0.9, 0.999), eps=1e-08, weight_decay=config['weight_decay'], amsgrad=False)
writer = SummaryWriter() # Writer of tensoboard.
if not os.path.isdir('./models'):
os.mkdir('./models') # Create directory of saving models.
n_epochs, best_loss, step, early_stop_count = config['n_epochs'], math.inf, 0, 0
# By CLC
Plot_Data = {'train': [], 'dev': []}
下面是開始執行訓練,首先會把模型設定為 train 訓練模式,做的就是不斷的從每個 Batch 計算 Loss,更新 (update) 參數,歷遍整個 Dataset (一個 epoch),直到
代碼是接續上一段,因此別漏掉 for 迴圈前的縮排
代碼是接續上一段,因此別漏掉 for 迴圈前的縮排
代碼是接續上一段,因此別漏掉 for 迴圈前的縮排
for epoch in range(n_epochs):
# Inheritance: nn.Module.train
model.train() # Set your model to train mode.
loss_record = []
# tqdm is a package to visualize your training progress.
train_pbar = tqdm(train_loader, position=0, leave=True)
for x, y in train_pbar:
optimizer.zero_grad() # Set gradient to zero.
x, y = x.to(device), y.to(device) # Move your data to device.
pred = model(x) # forward pass (compute output)
loss = criterion(pred, y) # coupute loss
loss.backward() # Compute gradient(backpropagation).
optimizer.step() # Update parameters.
step += 1
loss_record.append(loss.detach().item())
#Plot_Data['train'].append(loss.detach().item())
# Display current epoch number and loss on tqdm progress bar.
train_pbar.set_description(f'Epoch [{epoch+1}/{n_epochs}]')
train_pbar.set_postfix({'loss': loss.detach().item()})
mean_train_loss = sum(loss_record)/len(loss_record)
writer.add_scalar('Loss/train', mean_train_loss, step)
model.eval() # Set your model to evaluation mode.
loss_record = []
for x, y in valid_loader:
x, y = x.to(device), y.to(device)
with torch.no_grad():
pred = model(x)
loss = criterion(pred, y)
loss_record.append(loss.item())
mean_valid_loss = sum(loss_record)/len(loss_record)
# By CLC
Plot_Data['dev'].append(mean_valid_loss)
# By CLC
Plot_Data['train'].append(mean_train_loss)
print(f'Epoch [{epoch+1}/{n_epochs}]: Train loss: {mean_train_loss:.4f}, Valid loss: {mean_valid_loss:.4f}')
writer.add_scalar('Loss/valid', mean_valid_loss, step)
if mean_valid_loss < best_loss:
best_loss = mean_valid_loss
torch.save(model.state_dict(), config['save_path']) # Save your best model
print('Saving model with loss {:.3f}...'.format(best_loss))
early_stop_count = 0
else:
early_stop_count += 1
if early_stop_count >= config['early_stop']:
print('\nModel is not improving, so we halt the training session.')
break
return Plot_Data
這部分是為了方便模型調試用的字典,大部分需要自己去調整,其中 batch_size
即為作業內容的提示項目之一。
由於這部分參數幾乎都修改過,如果想知道原始設定的話可以參考附檔中的 Sample Code。
# Configurations
# config contains hyper-parameters for training and the path to save your model.
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print('Device: ' + device)
config = {
'seed': 655123, # Your seed number, you can pick your lucky number. :)
'select_all': False, # Whether to use all features.
'valid_ratio': 0.2, # validation_size = train_size * valid_ratio
'n_epochs': 3000, # Number of epochs.
'batch_size': 96,
'learning_rate': 1e-5, #1e-5
'early_stop': 400, # If model has not improved for this many consecutive epochs, stop training.
'save_path': './models/model.ckpt', # Your model will be saved here.
'weight_decay': 1e-6, # 1e-6
'momentum': 0.9 # 0.9
}
以上都是在匯入、建立、設定等等的操作,下面開始為執行運算。
首先設定隨機種子。
# Dataloader
# Read data from files and set up training, validation, and testing sets. You do not need to modify this part.
# Set seed for reproducibility
same_seed(config['seed'])
讀取訓練以及測試數據,並且打亂驗證數據為訓練以及驗證用兩部分。
# train_data size: 2699 x 118 (id + 37 states + 16 features x 5 days)
# test_data size: 1078 x 117 (without last day's positive rate)
train_data, test_data = pd.read_csv('./covid.train.csv').values, pd.read_csv('./covid.test.csv').values
train_data, valid_data = train_valid_split(train_data, config['valid_ratio'], config['seed'])
顯示資料大小,用來確認是否沒問題。
# Print out the data size.
print(f"""train_data size: {train_data.shape}
valid_data size: {valid_data.shape}
test_data size: {test_data.shape}""")
選擇訓練模型的 Features。
# Select features
x_train, x_valid, x_test, y_train, y_valid = select_feat(train_data, valid_data, test_data, config['select_all'])
# Print out the number of features.
print(f'number of features: {x_train.shape[1]}')
以 COVID19Dataset
定義的集合建立 DataLoader。
shuffle=True
則是會將數據先打亂後,再取固定大小的 batch 出來進行 Loss 、 update 的計算。由於 test_loader 是用來預測我們模型的結果,因此不需要打亂。
train_dataset, valid_dataset, test_dataset = COVID19Dataset(x_train, y_train), \
COVID19Dataset(x_valid, y_valid), \
COVID19Dataset(x_test)
# Pytorch data loader loads pytorch dataset into batches.
train_loader = DataLoader(train_dataset, batch_size=config['batch_size'], shuffle=True, pin_memory=True)
valid_loader = DataLoader(valid_dataset, batch_size=config['batch_size'], shuffle=True, pin_memory=True)
test_loader = DataLoader(test_dataset, batch_size=config['batch_size'], shuffle=False, pin_memory=True)
開始訓練,並且把 Loss 數據寫入到 model_loss_record
。
將訓練完成的模型進行預測,並將預測結果保存為 csv 檔。為了方便數據對照,因此對每次訓練完成後的預測結果加上時間戳記後再保存。
最後將訓練過程的 Loss 繪製出來。
# Start training!
model = My_Model(input_dim=x_train.shape[1]).to(device) # put your model and data on the same computation device.
# By CLC
model_loss_record = trainer(train_loader, valid_loader, model, config, device)
def save_pred(preds, file):
''' Save predictions to specified file '''
with open(file, 'w') as fp:
writer = csv.writer(fp)
writer.writerow(['id', 'tested_positive'])
for i, p in enumerate(preds):
writer.writerow([i, p])
# Predict
model = My_Model(input_dim=x_train.shape[1]).to(device)
model.load_state_dict(torch.load(config['save_path']))
preds = predict(test_loader, model, device)
# By CLC
date = time.strftime("%Y"+'_'+"%m"+'_'+"%d"+"_%H%M")
# By CLC
date = date + '_pred.csv'
# By CLC
print('Output file name: ' + date)
# By CLC
save_pred(preds, date)
if model_loss_record != None:
# print(len(model_loss_record['dev']))
# print(len(model_loss_record['train']))
plot_learning_curve(model_loss_record, title='deep model')
以下內容為訓過程終端回傳的內容以及圖片結果分享。
由於我的 Mackbook Pro 還是 Intel 時代,所以並不支援 GPU 運算,因此回傳我的 Device 為 CPU。接著是數據的大小與我們這個訓練用到 Features 的數量。
Device: cpu
train_data size: (2160, 118)
valid_data size: (539, 118)
test_data size: (1078, 117)
number of features: 52
由於訓練經過設定的 config['early_stop']
次數後 Loss 都沒有再降低,於是在第 1809 個 Epoch 停下訓練。
Epoch [2833/3000]: Train loss: 1.1228, Valid loss: 1.0400
Epoch [2834/3000]: 100%|█████████████████████████████████████████████████████████████████████████████████████| 23/23 [00:00<00:00, 594.13it/s, loss=1.39]
Epoch [2834/3000]: Train loss: 1.1354, Valid loss: 1.0482
Epoch [2835/3000]: 100%|████████████████████████████████████████████████████████████████████████████████████| 23/23 [00:00<00:00, 550.89it/s, loss=0.889]
Epoch [2835/3000]: Train loss: 1.1231, Valid loss: 1.0684
Epoch [2836/3000]: 100%|█████████████████████████████████████████████████████████████████████████████████████| 23/23 [00:00<00:00, 518.88it/s, loss=1.38]
Epoch [2836/3000]: Train loss: 1.1335, Valid loss: 1.0656
Epoch [2837/3000]: 100%|█████████████████████████████████████████████████████████████████████████████████████| 23/23 [00:00<00:00, 516.19it/s, loss=0.84]
Epoch [2837/3000]: Train loss: 1.1220, Valid loss: 1.0412
Epoch [2838/3000]: 100%|█████████████████████████████████████████████████████████████████████████████████████| 23/23 [00:00<00:00, 481.32it/s, loss=1.46]
Epoch [2838/3000]: Train loss: 1.1363, Valid loss: 1.0635
Epoch [2839/3000]: 100%|████████████████████████████████████████████████████████████████████████████████████| 23/23 [00:00<00:00, 476.27it/s, loss=0.768]
Epoch [2839/3000]: Train loss: 1.1207, Valid loss: 1.0433
Epoch [2840/3000]: 100%|█████████████████████████████████████████████████████████████████████████████████████| 23/23 [00:00<00:00, 467.36it/s, loss=1.01]
Epoch [2840/3000]: Train loss: 1.1258, Valid loss: 1.0651
Epoch [2841/3000]: 100%|████████████████████████████████████████████████████████████████████████████████████| 23/23 [00:00<00:00, 538.49it/s, loss=0.965]
Epoch [2841/3000]: Train loss: 1.1245, Valid loss: 1.0634
Epoch [2842/3000]: 100%|████████████████████████████████████████████████████████████████████████████████████| 23/23 [00:00<00:00, 516.86it/s, loss=0.893]
Epoch [2842/3000]: Train loss: 1.1236, Valid loss: 1.0554
Epoch [2843/3000]: 100%|█████████████████████████████████████████████████████████████████████████████████████| 23/23 [00:00<00:00, 448.05it/s, loss=1.24]
Epoch [2843/3000]: Train loss: 1.1306, Valid loss: 1.0993
Epoch [2844/3000]: 100%|████████████████████████████████████████████████████████████████████████████████████| 23/23 [00:00<00:00, 239.29it/s, loss=0.793]
Epoch [2844/3000]: Train loss: 1.1211, Valid loss: 1.0461
Epoch [2845/3000]: 100%|█████████████████████████████████████████████████████████████████████████████████████| 23/23 [00:00<00:00, 445.83it/s, loss=1.33]
Epoch [2845/3000]: Train loss: 1.1326, Valid loss: 1.0572
Epoch [2846/3000]: 100%|████████████████████████████████████████████████████████████████████████████████████| 23/23 [00:00<00:00, 546.63it/s, loss=0.607]
Epoch [2846/3000]: Train loss: 1.1168, Valid loss: 1.0767
Epoch [2847/3000]: 100%|██████████████████████████████████████████████████████████████████████████████████████| 23/23 [00:00<00:00, 469.06it/s, loss=1.2]
Epoch [2847/3000]: Train loss: 1.1297, Valid loss: 1.0574
Epoch [2848/3000]: 100%|██████████████████████████████████████████████████████████████████████████████████████| 23/23 [00:00<00:00, 469.59it/s, loss=1.5]
Epoch [2848/3000]: Train loss: 1.1360, Valid loss: 1.0703
Model is not improving, so we halt the training session.
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 2242.14it/s]
Output file name: 2023_01_17_0216_pred.csv
下圖是以我此文章的代碼、參數,運行過程中的 Loss 紀錄。
由於這項作業的截止時間已經超過,因此 Kaggle 只能提供 Public Score 以及 Private Score 兩個數據。下面是我上傳 2023_01_17_0216_pred.csv
預測的分數,以其個人最好的分數。
希望上完整個學期的課程後再回頭重新訓練這模型,能把分數更上一層!
壓縮包中包含:
下載鏈接
L1 , L2 Regularization 到底正則化了什麼 ? ↩︎