#機器學習_台大李宏毅教授HW1範例代碼,含解析

2 篇文章 0 订阅
2 篇文章 0 订阅

2023-01-17 02:28 由於文章發佈的時候,附檔還未通過審核,後續會再補上
2023-01-17 13:17 已補上下載鏈接

A. 前言

本文將針對李宏毅教授的課程 NTU ML2022 Spring 作業 1 ,助教提供的代碼,以段落的方式進行分析,且每個小節的代碼是連續的如果要複製粘貼別忘了前面的縮排 (tab)。

我個人的一些修改(會加上註解 # By CLC),最後會分享我的預測資料上傳到 Kaggle 驗證的結果。

由於助教的代碼有兩個版本,有興趣的朋友可以在文末自行下載。另外 Traning Data 的網址也有點變動,為了方便起見也會一並上傳供大家下載。

如果你的電腦已經設定好環境,助教提供的 Sample Code 是能夠直接運行的。

如果沒有的話,可以參考我寫的這篇文章環境建立筆記(macOS)先建立環境。


B. 代碼分析

以下會以我個人根據附檔中 Sample Code 1 修改後的代碼進行講解,若有引用到一些相關的文章,也會加入連結在其中。

B.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

B.2 方便應用的函式庫

下面功能是為了確保隨機種子所執行的模型訓練,能夠再次復現而做的設定。

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)

這部分則是將數據分成驗證用、訓練用兩部分。

  1. 根據輸入的 valid_ratio 分別計算驗證用 valid_set_size、訓練用 train_set_size 的數據量。
  2. 透過 PyTorch 的 ramdon_split 模組,在數據中隨機挑選不重複的數據,並且根據輸入的 [train_set_size, valid_set_size] 決定 train_set 的大小 valid_set
  3. 最後以數組的型態回傳數據。
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()

B.3 數據集

這部分是定義數據及的類,用到了 __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)

B.4 訓練模型

這部分是定義神經網路模型,也是作業內容之一,需要自行去決定網路的深度、寬度,以及激勵函數 (activation function)。
參考李宏毅教授上課的投影片,可以更清楚這部分在做的事情。
NNW

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

B.5 特徵選擇

這部分也是作業內容之一,需要去選擇適合模型訓練的數據,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

B.6 訓練模型

這個小節由於有許多的 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=1n(yiyI^)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),直到

  1. Loss 經過好幾次 update 沒有再降低。
  2. 達到我們設定的 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

B.7 參數字典

這部分是為了方便模型調試用的字典,大部分需要自己去調整,其中 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
}

B.8 運行

以上都是在匯入、建立、設定等等的操作,下面開始為執行運算。
首先設定隨機種子。

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

C. 訓練過程與上傳 Kaggle 結果

以下內容為訓過程終端回傳的內容以及圖片結果分享。

C.1 開始執行

由於我的 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

C.2 訓練過程

由於訓練經過設定的 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

C.2 Loss 圖

下圖是以我此文章的代碼、參數,運行過程中的 Loss 紀錄。
Loss

C.3 Kaggle 驗證結果

由於這項作業的截止時間已經超過,因此 Kaggle 只能提供 Public Score 以及 Private Score 兩個數據。下面是我上傳 2023_01_17_0216_pred.csv 預測的分數,以其個人最好的分數。

希望上完整個學期的課程後再回頭重新訓練這模型,能把分數更上一層!
在这里插入图片描述
在这里插入图片描述


D. 課程資源

壓縮包中包含:

  • 兩個助教的 Sample Code
  • 訓練用的數據
  • 預測用的數據

下載鏈接


  1. L1 , L2 Regularization 到底正則化了什麼 ? ↩︎

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值