---
# System prepended metadata

title: "\n    1. 實驗步驟與觀察\n"

---

<h1>
    1. 實驗步驟與觀察
</h1>

<h2>
    code explain
</h2>

首先題目給的資料有兩份，分別是 train.csv 和 test.csv，目標是的到一個model，藉由輸入x1, x2預測最終的y值。

由於input的維度是1，所以主要使用nn.Linear()來架構model，其次因為因為目標是預測y值，所以model的output只有一個值。

我選擇使用pytorch來完成作業，由於之前就已安裝過，不再多做贅述。

<h3>
    import
</h3>

```python
import os
from pathlib import Path
import numpy as np
import pandas as pd

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset
from torch.utils.data import DataLoader, random_split
from tqdm.notebook import tqdm as tqdm
from sklearn.preprocessing import StandardScaler
from pathlib import Path
from datetime import datetime
from matplotlib import pyplot as plt
from sklearn.model_selection import KFold
```

<h3>
    setting device to cuda if available
</h3>

```python
if torch.cuda.is_available():
    device = torch.device('cuda')
    torch.backends.cudnn.benchmark = True
else:
    device = torch.device('cpu')

print(device)
```

<h3>
    setting dir
</h3>

```python
data_dir = str(Path(fr"./data/train.csv"))
testdata_dir = str(Path(fr"./data/test.csv"))
save_dir = str(Path(fr"./runs/exp"))
```

<h3>
    setting hyperparameter
</h3>

```python
# number of subprocesses to use for data loading
num_workers = 0
# how many samples per batch to load
batch_size = 32

epoch_num = 500

dtype = torch.float32

# Learning Rate
LR = 1e-3
EPS = 1e-7
```

<h3>
    define CustomDataset class
</h3>

資料處理部分，首先要將csv檔讀進來，並轉成pytorch框架能接受的datatype，這裡選擇numpy，雖然目前沒有加入transform的打算，但還是有準備，這裡設計getitem時會直接將型別轉為tensor，至於dtype則是作為一個參數。
```python
class CustomDataset(Dataset):
    def __init__(self, data_dir, dtype=torch.float, transform=None):
        data = pd.read_csv(data_dir)
        self.inputs = data[['x1', 'x2']].to_numpy()
        self.outputs = data['y'].to_numpy()
        self.dtype = dtype
        self.transform = transform

    def __len__(self):
        return len(self.inputs)

    def __getitem__(self, idx):
        input_data = torch.tensor(self.inputs[idx], dtype=dtype)
        output_data = torch.tensor(self.outputs[idx], dtype=dtype)

        if self.transform:
            input_data = self.transform(input_data)

        return input_data, output_data
```

<h3>
    Data Preprocessing
</h3>

1. 首先利用上一個 block 定義的 class 來讀取資料，建立 dataset 實例
2. 計算 dataset 大小，並切成 train set 跟 validation set
3. 藉由DataLoader class 將兩份資料依照定義的 batch size 做成 dataloader 實例，其中我有將 train set 打亂
```python
dataset = CustomDataset(data_dir)

train_size = int(0.8 * len(dataset))
val_size = len(dataset) - train_size
train_dataset, valid_dataset = random_split(dataset, [train_size, val_size])

dataloaders = {x: DataLoader(y, batch_size=batch_size, shuffle=z) 
               for x, y, z in zip(['train', 'valid'], [train_dataset, valid_dataset], [True, False])}
```

<h3>
    define model structure
</h3>

activation function 使用現代model常用的 ReLU，並且加上Batch Normalization，希望以此來減緩梯度消失的問題。

```python
class DeepModelWithBN(nn.Module):
    def __init__(
            self, 
            layer_sizes):
        super().__init__()

        self.layers = nn.ModuleList()

        for i in range(len(layer_sizes) - 1):
            layer = nn.Linear(layer_sizes[i], layer_sizes[i + 1]).to(dtype)
            self.layers.append(layer)
            
            if i != len(layer_sizes) - 2:
                bn = nn.BatchNorm1d(layer_sizes[i + 1]).to(dtype)
                self.layers.append(bn)

        self.activation = nn.ReLU()

    def forward(self, x):
        for i, layer in enumerate(self.layers[:-1]):
            x = layer(x)
            if isinstance(layer, nn.Linear):
                x = self.activation(x)
        
        x = self.layers[-1](x)
        return x
```

<h3>
    define model, optimizer, and criterion
</h3>

定義完 model 架構後，簡單的依照2的指數，創建了一個model的實例。此時選用 adam 作為 optimizer，提高 model 訓練時參數更新時的穩定性，收斂的速度也較快。criterion 則是依照題目定義的分數檢測方式，期望能最小化 MSE Loss。

```python
model_name = 'DeepModelWithBN'
model_args = [[2, 100, 1]]

model = DeepModelWithBN(*model_args)

optimizer = torch.optim.Adam(model.parameters(), lr=LR, eps=EPS)
criterion = nn.MSELoss()
```

<h3>
    training
</h3>


為了避免在訓練期間因為其他因素造成沒有存到檔的情形，會在每一個 epoch 時儲存，另外還會儲存目前最佳的 model。
會依照 epoch 數量是否為零判斷是否要沿用目前的儲存路徑，或是避免覆蓋上一個 model 的結果。

```python
def train(ckpt, 
          save_dir, 
          model, 
          dataloaders, 
          optimizer, 
          num_epochs=300,
          dtype=torch.float32, 
          device='cpu'):

    save_dir = Path(save_dir)
    save_dir = increment_path(
        Path(save_dir), exist_ok=(False if ckpt['epoch'] == 0 else True), mkdir=True)
    
    model = model.to(dtype=dtype).to(device)

    # initial
    model.load_state_dict(ckpt['model_state_dict'])
    optimizer.load_state_dict(ckpt['optimizer_state_dict'])
    criterion = ckpt['criterion']

    for epoch in range(1, num_epochs + 1):

        # keep track of training and validation loss
        train_loss = 0.0
        valid_loss = 0.0
        print(f"running epoch: {ckpt['epoch'] + 1}")

        # Training loop
        model.train()
        for inputs, outputs in dataloaders['train']:
            optimizer.zero_grad()
            
            inputs = inputs.to(dtype).to(device)
            outputs = outputs.to(dtype).to(device)

            predictions = model(inputs)
            loss = criterion(predictions.squeeze(1), outputs)
            loss.backward()
            optimizer.step()

            # update training loss
            train_loss += loss.item() * inputs.size(0)

        # Validation loop
        model.eval()
        with torch.no_grad():
            for inputs, outputs in dataloaders['valid']:

                inputs = inputs.to(dtype).to(device)
                outputs = outputs.to(dtype).to(device)

                predictions = model(inputs)
                loss = criterion(predictions.squeeze(1), outputs)
                valid_loss += loss.item() * inputs.size(0)
        
        train_loss = train_loss / len(dataloaders['train'].dataset)
        valid_loss = valid_loss / len(dataloaders['valid'].dataset)
        

        ckpt['history']['train_loss'].append(train_loss)
        ckpt['history']['valid_loss'].append(valid_loss)

        print(
            f'Train loss     -> {train_loss:.6f}      \
                Validation loss     -> {valid_loss:.6f}')
        
        # update and save check point information
        ckpt['epoch'] += 1
        ckpt['model_state_dict'] = model.state_dict()
        ckpt['optimizer_state_dict'] = optimizer.state_dict()
        ckpt['date'] = datetime.now().isoformat()

        # save model if validation loss has decreased
        if valid_loss <= ckpt['history']['valid_loss_min']:
            print(
                f"Validation loss decreased ({ckpt['history']['valid_loss_min']:.6f} \
                    --> {valid_loss:.6f}).  Saving model ...")
            ckpt['history']['valid_loss_min'] = valid_loss
            torch.save(ckpt, str(Path(save_dir) / Path('valid_best.pth')))

        torch.save(ckpt, str(Path(save_dir) / Path('last.pth')))

    return ckpt
```

<h3>
    初始化 ckpt 
</h3>
                             
```python
ckpt = {
    'epoch': 0,
    'model_name': model_name,
    'model_args': model_args,
    'model_state_dict': model.state_dict(),
    'optimizer_state_dict': optimizer.state_dict(),
    'criterion': criterion,
    'history': {
        'train_loss': [], 
        'valid_loss': [],
        'valid_loss_min': np.Inf,
        'best_acc': 0.0,
    },
    'date': datetime.now().isoformat(),
}
```

<h3>
    預測 test data 的 y
</h3>

```python
model.to(device)
model = model.to(dtype=dtype)
data = pd.read_csv(testdata_dir)
x = data[['x1', 'x2']].to_numpy()
x = torch.tensor(x, dtype=dtype)
model.eval()
with torch.no_grad():
    y = model(x.to(device))

y = y.squeeze().cpu().numpy()
ids = np.arange(1, 2001)
df = pd.DataFrame({'id': ids, 'y': y})
df.to_csv('output_last.csv', index=False)
```


<h3>
    可視化結果
</h3>

利用matplotlib 來繪圖，為了更好的觀察 model 是否過擬合，以及 loss 收斂的數值，為了可以看得更清楚，這裡直接將 y 座標限制在 0.1 內。

```python
# loss 
plt.plot(ckpt['history']['train_loss'],label='train')
plt.plot(ckpt['history']['valid_loss'],label='valid')
plt.legend()
plt.ylim(0,0.1)
plt.title('loss')
plt.xlabel('epoch')
plt.ylabel('loss')
```

為了更了解資料的特性，我將訓練集以matplotlib畫成3d可視圖。經過觀察，我將資料想成一個三維空間中的曲面。我將 x1, x2 想成三維空間的 x, z，y 則對應空間中的 y。
    
```python
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider
from mpl_toolkits.mplot3d import Axes3D

data = pd.read_csv(data_dir)

# 創建一個3D圖形
fig = plt.figure()
fig = plt.figure(figsize=(12, 8))
ax = fig.add_subplot(111, projection='3d')

# 繪製散點圖
ax.scatter(data['x1'].to_numpy(), 
           data['x2'].to_numpy(), 
           data['y'].to_numpy(), 
           c=data['y'].to_numpy(), 
           cmap='viridis', 
           marker='o')

# 設置標籤
ax.set_xlabel('X1')
ax.set_ylabel('X2')
ax.set_zlabel('Y')

# 顯示圖形
plt.show()
```

![](https://i.imgur.com/jLN8Dwr.png)

為了可以用另一種角度來查看 model 是否過擬合，完不完美，我利用 numpy 在 x1, x2 為 -1 ~ 1 的範圍生成10000個資料點，並丟入 model 預測。

```python
import numpy as np
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider
from mpl_toolkits.mplot3d import Axes3D


# 設定網格點的數量
grid_size = 100

# 使用linspace在-1和1之間創建均勻間隔的值
x_values = np.linspace(-1, 1, grid_size)
y_values = np.linspace(-1, 1, grid_size)

# 使用meshgrid創建網格座標
x_grid, y_grid = np.meshgrid(x_values, y_values)

# 將網格座標陣列整形為2D點陣列
x = np.column_stack((x_grid.flatten(), y_grid.flatten()))

x_t = torch.tensor(x, dtype=dtype)
model = model.to(device)
model.eval()
with torch.no_grad():
    y = model(x_t.to(device))
y = y.squeeze().cpu().numpy()

data = pd.DataFrame({'x1': x[:, 0], 'x2': x[:, 1], 'y': y})

# 創建一個3D圖形
fig = plt.figure()
fig = plt.figure(figsize=(12, 8))
ax = fig.add_subplot(111, projection='3d')

# 繪製散點圖
ax.scatter(data['x1'].to_numpy(), data['x2'].to_numpy(), data['y'].to_numpy(), c=data['y'].to_numpy(), cmap='viridis', marker='o')

# 設置標籤
ax.set_xlabel('X1')
ax.set_ylabel('X2')
ax.set_zlabel('Y')

# 顯示圖形
plt.show()
```

![](https://i.imgur.com/ybyWms0.png)
![](https://i.imgur.com/LTefgph.png)

<h3>
    Bug
</h3>

在實驗前期，我有發現 loss 的結果有些問題，後來發現兩個 bug 。
第一個是 model 的 output shape 為 (B, 1) 而 output shape 則是 (B, )，一個為二維向量，一個是一維向量，雖然還是可以算出loss值，但結果有些不同。
第二個是在計算loss的總和，並印出來時，將 len(dataloader.dataset) 寫成了 len(dataloader) 一個是回傳有多少個batch，一個是回傳有多少筆資料。

```python
k = iter(dataloaders['train'])
x, y = next(k)

predictions = model(x)
print(predictions.shape)
print(y.shape)
print(y.reshape(batch_size, 1).shape)
print(predictions.squeeze(1).shape)
print(criterion(predictions.squeeze(1), y))
print(criterion(predictions, y))
print(criterion(predictions, y.reshape(batch_size, 1)))
```

執行結果
torch.Size([64, 1])
torch.Size([64])
torch.Size([64, 1])
torch.Size([64])
tensor(1.2346, dtype=torch.float64, grad_fn=<MseLossBackward0>)
tensor(1.2805, dtype=torch.float64, grad_fn=<MseLossBackward0>)
tensor(1.2346, dtype=torch.float64, grad_fn=<MseLossBackward0>)


    
<h2>
    Exp 1
</h2>
    
到此處成功 train 了幾個 model，可以看出 valid loss 的數值較train震盪的更嚴重。我在想是否有可能是batch過小，model 參數更新時受極端值影響較大。為了避免過度跳動，我嘗試加大batch size，我比較32, 64, 128, 256，發現當 batch size 較大時，跳動的問題有得到緩解，而且訓練時間也有效的減短了，但缺點是 loss 有點降不下去。


| **32** | **64** |
| -------- | -------- |
| ![](https://i.imgur.com/puCbKBG.png) | ![](https://i.imgur.com/5Nq5Nxn.png) |  
| **128** | **256** |
| ![](https://i.imgur.com/nrGJsyT.png) |  ![](https://i.imgur.com/4QfD3Tc.png) |


<h2>
    Exp 2
</h2>
    
進一步對資料可視化後，發現雜訊不少，所以嘗試加深加廣網路。深度增加後，loss 值下降了不少，而且 model 的收斂更快了，loss 跳動的問題也更不明顯了。此時也發現不管再怎麼加深、加寬網路，loss 值大致上只能降至 0.025，此時我也開始將結果繳交至 kaggle 上。

                             
| **model_args = [[2, 100, 100, 1]]** | **model_args = [[2, 100, 100, 100, 100, 1]]** |
| -------- | -------- |
| ![](https://i.imgur.com/sZkZ7x6.png) | ![](https://i.imgur.com/8tOAEmP.png) |  
| **model_args = [[2, 64, 128, 128, 64, 1]]** | **model_args = [[2, 64, 128, 256, 256, 128, 64, 1]]** |
| ![](https://i.imgur.com/x9ORQzL.png) | ![](https://i.imgur.com/L3D7Wrs.png) |



<h2>
    Exp 3
</h2>

繳交後發現結果不是很理想，再自己的資料集上，原本可以降到0.03以下的，上傳後全部都是高於0.05。再此問題中，我做的嘗試有:
1. 換一個dataloader，我怕某次資料集分割剛好訓練效果不好
2. 嘗試將 activation fuction 改為 LeakyReLU、ELU、tanh
3. 使用不同的 optimizer 像是 SGD、Adadelta、AdaGrad、RMSprop、AdamW

經過多次嘗試的結果，只有使用tanh比較成功，雖然從訓練的loss變化來看，沒什麼變化，但上傳後的結果竟然比較好，這時我的最佳結果來到0.045。其他的更動都沒有明顯差異，有些 optimizer 反而會使 model 更難收斂，所以後面主要還是使用 Adam。
  
    
    
<h2>
    Exp 4
</h2>

簡單觀察不同的 result 後，我發現每個 model 就算最終 loss 值不相上下，其中 y 值都沒有甚麼規律，所以我想到將多個 model 的 result 取平均，是否會更好。
結果是有些資料有變好，但有些資料可能會變更差，後來就放棄直接從不同的 output 找規律了，且就算有進步，也是少個 0.001。

```python

files_list = ["runs/exp61/output_best.csv",
              "runs/exp60/output_best.csv", 
              ]

# 初始化一個空的DataFrame用於存儲結果
merged_data = pd.DataFrame()

for index, file in enumerate(files_list):
    # 讀取CSV檔案
    data = pd.read_csv(file)
    
    # 使用檔案名稱或索引作為新列名
    new_column_name = f'y_{index}' 
    
    # 將y值重命名為新列名
    data = data.rename(columns={'y': new_column_name})
    
    # 合併檔案
    if merged_data.empty:
        merged_data = data
    else:
        merged_data = merged_data.merge(data, on='id', how='outer')

# 計算y值的平均
merged_data['y'] = merged_data.iloc[:, 1:].mean(axis=1)

# 將結果輸出到CSV檔案
merged_data.to_csv('merged_data.csv', index=False)
```    
<h2>
    Exp 5
</h2>

後面決定改 model 架構，當時想到的改法有幾個:
1. 是否加入 BatchNorm1d，我想說為了防止梯度消失，所以還是有留。
2. 使用不同的 weight initial 方式。
3. 加入 Dropout，但是我又看了一下我們 model 的收斂，我認為這只會讓 model 的能力下降而以，所以沒採用。
4. activation function，我只知道 tanh 效果會好一些，但我堆這些函數是否適合用在此處感到疑惑，所以我決定將一個layer的 output 套用不同的 activation function後再concate 在一起。
5. 為了能進一步的加大加深我的 model，我想使用類似ResNet、DenseNet的技術，在不同層之間加入 shortcut，我最後決定將網路設計的像 DenseNet 一樣。

_Block 會將 input 先送進一個全連階層，其 output 經過 batchnorm 之後複製 N 份進入 N 個 activation function 中，最後將這些不同的 features 再 concate 在一起。
![](https://i.imgur.com/uAErhKm.png)
    
DeepMultiAFwithBNmodel 則是以 _Block 為基本模塊，將每一層 layer 的 input 跟 output concate 在一起，就會達到如容下面圖示的效果。
![](https://i.imgur.com/MNm5oF3.png)

```python
class _Block(nn.Module):
    def __init__(
            self, 
            num_input_features, 
            num_output_features, 
            activation_list,
            drop_rate: float
    ) -> None:
        super().__init__()
        self.add_module("linear", nn.Linear(num_input_features, num_output_features))
        self.add_module("norm", nn.BatchNorm1d(num_output_features))
        self.activation_list = activation_list
        self.drop_rate = float(drop_rate)

    def forward(self, input):

        if isinstance(input, torch.Tensor):
            prev_features = [input]
        else:
            prev_features = input
        
        x = torch.cat(prev_features, 1)

        x = self.linear(x)
        x = self.norm(x)
        features = []
        for acivate in self.activation_list:
            features.append(acivate(x))
        return torch.cat(features, 1)

class DeepMultiAFwithBNmodel(nn.Module):
    def __init__(
            self, 
            num_init_features: int,
            num_classes,
            block_config, 
            activation_list,
            drop_rate: float = 0
    ):
        super().__init__()

        self.features = nn.ModuleDict()

        num_features = num_init_features
        for i, num_layers in enumerate(block_config):
            block = _Block(num_features, num_layers, activation_list, drop_rate)
            self.features.add_module(f"block{i}", block)
            num_features = num_features + num_layers * len(activation_list)
        self.classifier = nn.Linear(num_features, num_classes)

        # 参数初始化
        for m in self.modules():
            if isinstance(m, nn.Linear):
                nn.init.xavier_uniform_(m.weight)  # 使用Xavier初始化
                nn.init.constant_(m.bias, 0)
            elif isinstance(m, nn.BatchNorm1d):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)

    def forward(self, init_features):
        features = [init_features]
        for name, layer in self.features.items():
            new_features = layer(features)
            features.append(new_features)
        
        x = torch.cat(features, 1)
        x = self.classifier(x)
        return x
```
    
經過簡單的嘗試幾個參數，train 出來的結果如下，收斂的速度稍微變慢，且 loss 的浮動有點大，有點不穩定，但最終的收斂數字約莫是0.22，有進步。
參數部分可以發現上面那個的浮動較大，下面的較小，我實驗得到的結果是，activation function 不怎麼影響結果，而是神經網路太寬時就會浮動特別大。

model_args = [2, 1, [16, 64, 128, 512, 1024, 512, 128, 64, 16], [nn.Tanh(), nn.ReLU(), nn.LeakyReLU()]]    
![](https://i.imgur.com/MQOt5ZH.png)


model_args = [2, 1, [32, 32, 64, 64, 64, 128, 64, 64, 64, 32, 32], [nn.Tanh(), nn.ReLU(), nn.ELU()]]    
![](https://i.imgur.com/2jea62B.png)


<h2>
    Exp 6
</h2>

我嘗試著將 LR 調整為 1e-4 ，但結果是 model 收斂太慢，所以我嘗試加入 lr scheduler，這裡測試兩種方式:
1. StepLR，定義是每過step_size個epoch之後，LR = LR * LR_GAMMA。
2. ReduceLROnPlateau，不是依照epoch num，而是看 loss 有沒有持續往下降，或是 acc 有沒有往上升，會有一個 patience 值，如果連續 patience 次還是沒有打破目前最好的，就會更新一次，還可以設定 cooldown 值。


```python
LR_STEP =  100
LR_GAMMA = 0.5
lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=LR_STEP, gamma=LR_GAMMA
lr_scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, 
                                                          mode='min', 
                                                          factor=0.5, 
                                                          patience=30, 
                                                          verbose=True, 
                                                          threshold=0.0001, 
                                                          threshold_mode='rel', 
                                                          cooldown=50, 
                                                          min_lr=0, 
                                                          eps=EPS)
```    

  
經過幾次測試後，效果都不錯，但是 ReduceLROnPlateau 對參數較不敏感，而 StepLR 要針對不同的 model 參數給出 LR_STEP 和 LR_GAMMA，所以後面選用前者。
上傳到 kaggle 後，最好結果為 0.035。
    
![](https://i.imgur.com/2UWr1qp.png)
![](https://i.imgur.com/5Wx218A.png)

<h2>
    Exp 7
</h2>    

後來我有問其他同學討論了下結果，他們只有單純使用 Linear，然後深度超過十層，所以我又寫了一個 model。

```python
class DeepModel(nn.Module):
    def __init__(
            self, 
            num_init_features: int,
            num_classes,
            block_config, 
            # activation_list,
            drop_rate: float = 0
    ):
        super().__init__()

        self.features = nn.Sequential()

        num_features = num_init_features
        for i, num_layers in enumerate(block_config):
            layer = nn.Linear(num_features, num_layers)
            self.features.add_module(f"linear{i}", layer)
            if i % 2:
                self.features.add_module(f"ELU{i}", nn.ELU())
            else:
                self.features.add_module(f"Tanh{i}", nn.Tanh())
            num_features = num_layers
        self.classifier = nn.Linear(num_features, num_classes)

        for m in self.modules():
            if isinstance(m, nn.Linear):
                nn.init.xavier_uniform_(m.weight)
                nn.init.constant_(m.bias, 0)

    def forward(self, x):
        features = self.features(x)
        out = self.classifier(features)

        return out
```

經過幾次測試，調整後，大概可以收斂至 train loss 跟 valid loss 可以收斂至 0.022，有些甚至能收斂至0.02，上傳後結果都小於0.03。

model_args = [2, 1, [8, 16, 32, 64, 128, 64, 32, 16, 8]]
![](https://i.imgur.com/kPzvhbt.png)

model_args = [2, 1, [8, 16, 16, 32, 32, 64, 64, 128, 128, 64, 64, 32, 32, 16, 16, 8]]
![](https://i.imgur.com/MmoZE59.png)


<h2>
    Exp 8
</h2>  

目前為止都是只有使用到部分資料，有一部分作為 valid，我認為trainset 更大，可能會有更好的效果，但是此時我們會無法評估是否過擬和，雖然經過簡單的測試可以發現運行1000個epoch也不會使 model 過擬和，但這裡還是使用 K-Fold 來觀察，經由觀察來決定執行的 epoch 數。
經過多次嘗試，最後達到 0.01804 的成績。
    
```python
def plot_k_res(list):
    # loss 
    for i in list:
        plt.plot(i['history']['train_loss'],label='train')
        plt.plot(i['history']['valid_loss'],label='valid')
        plt.legend()
        plt.ylim(0,0.1)
        plt.title('loss')
        plt.xlabel('epoch')
        plt.ylabel('loss')
        plt.show()
```
```python
k_folds = 5
kfold = KFold(n_splits=k_folds, shuffle=True)

train_losses_k = []
valid_losses_k = []
ckpt_list = []

for fold, (train_indices, val_indices) in enumerate(kfold.split(dataset)):
    print(f"Fold {fold + 1}")

    train_subset = torch.utils.data.Subset(dataset, train_indices)
    val_subset = torch.utils.data.Subset(dataset, val_indices)

    dataloaders = {x: DataLoader(y, batch_size=batch_size, shuffle=z) 
            for x, y, z in zip(['train', 'valid'], [train_subset, val_subset], [True, False])}
    
    model_name = 'DeepModel'
    model_args = [2, 1, [8, 16, 32, 64, 128, 64, 32, 16, 8]]

    model = DeepModel(*model_args)
    optimizer = torch.optim.Adam(model.parameters(), lr=LR, eps=EPS)
    lr_scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, 
                                                            mode='min', 
                                                            factor=0.5, 
                                                            patience=30, 
                                                            verbose=True, 
                                                            threshold=0.0001, 
                                                            threshold_mode='rel', 
                                                            cooldown=50, 
                                                            min_lr=0, 
                                                            eps=EPS)

    criterion = nn.MSELoss()

    ckpt = {
        'epoch': 0,
        'model_name': model_name,
        'model_args': model_args,
        'model_state_dict': model.state_dict(),
        'optimizer_state_dict': optimizer.state_dict(),
        'lr_scheduler_state_dict': lr_scheduler.state_dict(),
        'criterion': criterion,
        'history': {
            'train_loss': [], 
            'valid_loss': [],
            'valid_loss_min': np.Inf,
            'train_loss_min': np.Inf,
            'best_acc': 0.0,
        },
        'readme': "",
        'date': datetime.now().isoformat(),
    }

    ckpt = train(ckpt, save_dir, model, dataloaders, optimizer, lr_scheduler=lr_scheduler, num_epochs=500, device=device)

    train_losses_k.append(ckpt['history']['train_loss_min'])
    valid_losses_k.append(ckpt['history']['valid_loss_min'])
    ckpt_list.append(ckpt)


print(f"Average Train Loss: {np.mean(train_losses_k):.4f}, Average Val Loss: {np.mean(valid_losses_k):.4f}")
plot_k_res(ckpt_list)
```
    
<h1>
    2. Discussions on the results
</h1>

大致上可以分為三種 model 架構，DeepModel、DeepModelWithBN、DeepMultiAFwithBNmodel。
1. DeepModel:
    最晚嘗試的一個，我完全沒有料到 model 這麼單純竟然結果是最好的。內部只有Linear layer 跟 activation function，這裡設計使用 RLU、tanh 兩種穿插使用，最後得到最好的結果，至少我的實驗結果是如此，而且訓練十收斂的 loss 也跟 test result 的成績差不多。
2. DeepModelWithBN:
    第一個嘗試的，相對第一個加了 batchnorm，只有用一種 activation function，雖然在 train set、 valid set 中的收斂效果不錯，但不知為何test的result上船成績不理想，差異很大。
3. DeepMultiAFwithBNmodel:
    我自己大改的 model 架構，嘗試將 CNN model DenseNet 中的部分技術在全連階層中重現，整體效果不錯，但不是最好的。

經過比較，activation function 使用tanh的效果不錯。
然後 model 參數上，使用十層以上，但寬度最寬只有 128 的會比寬度到 1024 的效果更好。
最後是將多個 model 的結果去取平均，由於是預測，所以我想多個model 之間可能一個預測太高，一個預測太低，所以多個去平均會有較好的結果，且概念向是將多個分離的model output，在接一層全連階層，整體來說是可行的，但是只能說效果不是很顯著，其進步量也有限，且很吃運氣。
    
<h1>
    3. Conclution
</h1>

之前大部分都是去網路上下載別人的 model、別人的資料處理 ，這次所有的東西都是自己打的，所以對一些細節有更深的理解。再來是 K-Fold，之前有聽說過，但藉由這次作業才理解到底在幹麼，並實作出來。最後是操參數的調整，以前我都直接用 Adam 作為 optimizer，趁這次機會多學了好幾個 optimizer 了解其差異、優缺點，還有以前想說 Adam 本身好像就會去調整LR了，那使用 lr scheduler 還有用嗎?由於兩者調整的理論不同，所以 lr scheduler 還是有用。在 activation function 方面，也多認識了好幾個，我之前都無腦用ReLU。