Titanic Dataset - 使用 Pytorch 搭建神經網路 + 測試 overfitting
Reference
前言
最近選了一堂AI課程,有一個作業是我們寫出一個Nerual Network,並且使用Titanic Dataset來訓練,並且透過增加 hidden layer 跟 neurons 的方式實現overfitting,並透過 dropout 或其他方法來消除 overfitting 的影響。
在此紀錄ㄧ下作業撰寫的過程。
環境設置與作業要求
環境設置:
- Python 3.10.9
- Pytorch 2.0.1
作業要求
- Write a custom dataset class for the titanic data (see the data folder on GitHub). Use only the features: “Pclass”, “Age”, “SibSp”, “Parch”, „Fare“, „Sex“, „Embarked“. Preprocess the features accordingly in that class (scaling, one-hot-encoding, etc) and split the data into train and validation data (80% and 20%). The constructor of that class should look like this:
1
2titanic_train = TitanicDataSet('titanic.csv', train=True)
titanic_val = TitanicDataSet('titanic.csv', train=False) - Build a neural network with one hidden layer of size 3 that predicts the survival of the passengers. Use a BCE loss (Hint: you need a sigmoid activation in the output layer). Use a data loader to train in batches of size 16 and shuffle the data.
- Evaluate the performance of the model on the validation data using accuracy as metric.
- Create the following plot that was introduced in the lecture.
- Increase the complexity of the network by adding more layers and neurons and see if you can overfit on the training data.
- Try to remove overfitting by introducing a dropout layer.
簡單來說
簡單來說,我們會從以下四個步驟中滿足上述要求:
-
資料前處理
Task 1
: 建制 class 並且把 Titanic 的資料導入Task 1
: 只選取特定欄位作為訓練特徵Task 1
: 資料前處理 (scaling, one-hot-encoding, etc),把性別或是Embaded的這種object型態,非數字的欄位轉換成數字Task 1
: 資料切分成 train data 跟 validation data (80% and 20%)Task 1
: 建立一個 class 並且把資料導入
-
建置 Neural Network
Task 2
: 建立一個 three layer 的 network (1 input layer + 1 hidden layer + 1 output layer)。Task 2
: 第一層 hidden layer 的 neurons size 為 3Task 2
: 使用 BCE loss 作為 loss FunctionTask 2
: 使用 sigmoid activation 作為 output layer 的 activation function
-
模型訓練
Task 3
: 開始訓練模型,並且記錄每次的 accuracy
-
產出結果
Task 4
: 產出結果,並且畫出圖表
-
製造 Overfitting
Task 5
: 增加 hidden layer 跟 neurons 的方式實現overfitting
-
使用 Dropout
Task 6
: 透過 dropout 或其他方法來消除 overfitting 的影響
Step1. 資料前處理
那我們就先來開始做資料前處理:
Task 1
: 建制 class 並且把 Titanic 的資料導入Task 1
: 只選取特定欄位作為訓練特徵Task 1
: 資料前處理 (scaling, one-hot-encoding, etc),把性別或是Embaded的這種object型態,非數字的欄位轉換成數字Task 1
: 資料切分成 train data 跟 validation data (80% and 20%)Task 1
: 建立一個 class 並且把資料導入
1.1 資料前處理
-
我們先匯入目前所需要的所有套件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25import os
# data process
import numpy as np
import pandas as pd
from skimage import io, transform
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms, utils
# plot
import matplotlib.pyplot as plt
# neural network
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
# preprocessing
from sklearn.preprocessing import MinMaxScaler,OneHotEncoder
from sklearn.model_selection import train_test_split
import warnings
warnings.filterwarnings("ignore")
plt.ion() # interactive mode -
在開始之前,我想要先把所有需要會使用的參數都放在最上面比較好更改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18# share variables
D_in, D_out = 10, 1
num_epochs = 250
log_interval = 100
# batch_size:每次訓練的資料量
batch_size = 30
# learning rate:因為我會建立兩種不同的 network,因此我們分別設定兩種不同的 learning rate
learning_rate = 0.001
multi_learning_rate = 0.001
# hidden layers
multi_num_layers = 6
# hidden neurons:因為我會建立兩種不同的 network,因此我們分別設定兩種不同的 hidden neurons
neurons = 3
multi_neurons = 1024 -
接著,我們根據要求建制 class 並且把 Titanic 的資料導入,並回傳features
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66class TitanicDataset(Dataset):
# 初始化函數,用於載入和預處理數據
def __init__(self, root_dir, train=True, transform=None):
# train 參數用於指示是訓練數據還是測試數據
self.train = train
# transform 參數用於定義一個轉換函數,如果需要對數據進行轉換的話
# 創建 MinMaxScaler 和 OneHotEncoder 來進行數據預處理
minmax_scaler = MinMaxScaler()
onehot_enc = OneHotEncoder()
# 讀取 CSV 文件中的鐵達尼號數據
titanic = pd.read_csv(root_dir)
# 從數據中選取特定的列
titanic = titanic[["Pclass", "Age", "SibSp", "Parch", "Fare", "Sex", "Embarked", "Survived"]]
# 將 "Age" 列中的缺失值用平均值填充,並刪除包含缺失值的行
titanic["Age"] = titanic["Age"].fillna(titanic["Age"].mean())
titanic = titanic.dropna()
titanic = titanic.reset_index(drop=True)
# 將數據分為類別特徵、數值特徵和標籤
categorical_features = titanic[titanic.select_dtypes(include=['object']).columns.tolist()]
numerical_features = titanic[titanic.select_dtypes(exclude=['object']).columns].drop('Survived', axis=1)
label_features = titanic['Survived']
# 對數值特徵進行歸一化(MinMax 歸一化)
numerical_features_arr = minmax_scaler.fit_transform(numerical_features)
# 對類別特徵進行獨熱編碼
categorical_features_arr = onehot_enc.fit_transform(categorical_features).toarray()
# 將歸一化的數值特徵和獨熱編碼後的類別特徵合併成一個數據集
combined_features = pd.DataFrame(data=numerical_features_arr, columns=numerical_features.columns)
combined_features = pd.concat([combined_features, pd.DataFrame(data=categorical_features_arr)], axis=1)
combined_features = pd.concat([combined_features, label_features], axis=1).reset_index(drop=True)
# 將數據集分為訓練集和測試集
train_data, test_data = train_test_split(combined_features, test_size=0.2, random_state=42)
# 根據訓練或測試模式選擇要使用的數據
if train:
self.data = train_data
else:
self.data = test_data
# 返回數據集的長度
def __len__(self):
return len(self.data)
# 用於訓練神經網絡的函數,返回特徵和標籤
def __getitem__(self, idx):
# 獲取在 self.data DataFrame 中的第 idx 行的數據
sample = self.data.iloc[idx]
# 將一個數據結構轉換為 PyTorch 張量 並指定這個张量的數據類型為浮點數(float)
features = torch.FloatTensor(sample[:-1])
label = torch.FloatTensor([sample['Survived']])
if self.transform:
features = self.transform(features)
return features, label
# 返回整個數據集的 DataFrame
def getData(self):
return self.data -
寫好 function 後,就可以開始使用了,我們可以使用以下指令來測試一下:
1
2
3
4
5
6
7
8
9
10
11titanic_train = TitanicDataset('./data/titanic.csv', train=True)
titanic_val = TitanicDataset('./data/titanic.csv', train=False)
print('train_dataset len:', len(titanic_train))
print('val_dataset len:', len(titanic_val))
print('total_dataset len:', len(titanic_train) + len(titanic_val))
# 最後會印出如下:
'''
train_dataset len: 711
val_dataset len: 178
total_dataset len: 889
''' -
可以透過以下程式,列印出以下結果:
1
titanic_val.getData()
Step2. 建制 Neural Network
- 接下來我們建制以下 Neural Network,主要做以下事情:
__init__
: 建立一個 three layer 的 network (1 input layer + 1 hidden layer + 1 output layer)。D_in
: input layer 的 neurons sizeH
: hidden layer 的 neurons sizeD_out
: output layer 的 neurons size
forward
: 進行 forward pass 的地方,主要做第一層的 linear transformation,並且使用relu
作為 activation function,第二層的 linear transformation,並且使用sigmoid
作為 activation function,最後回傳預測的結果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class TwoLayerNet(nn.Module):
def __init__(self, D_in, H, D_out):
"""
In the constructor we instantiate two nn.Linear modules and assign them as member variables.
"""
super(TwoLayerNet, self).__init__()
# the weight and bias of linear1 will be initialized
# you can access them by self.linear1.weight and self.linear1.bias
self.linear1 = nn.Linear(D_in, H) # this will create weight, bias for linear1
self.linear2 = nn.Linear(H, D_out) # this will create weight, bias for linear2
self.sigmoid = nn.Sigmoid() # Sigmoid activation for binary classification
def forward(self, x):
"""
In the forward function we accept a Tensor of input data and we must return a Tensor of output data.
We can use Modules defined in the constructor as well as arbitrary operators on Tensors.
"""
h_relu = F.relu(self.linear1(x))
y_pred = self.sigmoid(self.linear2(h_relu))
return y_pred - 在訓練模型之前,我們要先把模型建立起來。下面程式碼的意思就是,我們設定 batch_size = 16,每次以 16 個單位進行一次訓練,然後把所有 889 筆資料以 16單位全部訓練完作為一次epoch,input layer 的 neurons size = 10,hidden layer 的 neurons size = 3,output layer 的 neurons size = 1,learning rate = 0.001,總共訓練 500 次。
- 我們建立了 network 把網路神經建立起來
- 使用 Adam 作為 optimizer,進行 Gradient Descent 的更新
- 使用 Binary Cross-Entropy Loss 作為 loss function
1
2
3
4
5
6
7
8N, D_in, H, D_out = 16, 10, 3, 1
lr = 0.001
n_epochs = 50
log_interval = 100 # Print the training status every log_interval epoch
network = TwoLayerNet(D_in, H, D_out) # H=3 for one hidden layer with 3 neurons
optimizer = optim.Adam(network.parameters(), lr)
criterion = nn.BCELoss() # Define the loss function as Binary Cross-Entropy Loss
Step3. 模型訓練
- 可以先建立好所需的 list 清單來記住每次的 loss 跟 accuracy
1
2
3
4train_losses = [] # Save the loss value of each training loop (epoch) of the neural network model during the training process
train_counter = [] # Save the number of images for training so far
test_losses = [] # Save the loss value of each test loop (epoch) of the neural network model during the training process
test_counter = [i*len(titanic_train) for i in range(n_epochs+1)] # how many data for training so far - 建制 train function,主要目的是把 model train 好,使用 train dataset 來進行模型訓練。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37def train(epoch): # 目前跑了第幾個 epoch
network.train() # 把上一步驟建立好的 network 拿進來使用
correct = 0 # 紀錄目前正確的次數
cur_count = 0 # 紀錄目前已經訓練了多少筆資料
for batch_idx, (data, target) in enumerate(train_dataloader):
optimizer.zero_grad() # 先把目前的 gradient 清空,因為每次訓練完一個 batch 就會更新一次 gradient
# forward propagation
output = network(data) # 把資料餵入 network 進行 forward propagation
loss = criterion(output, target) # 計算 loss
# Accuracy
pred = (output >= 0.5).float() # 因為答案不是0就是1,因此我們需要設定 threshold,大於等於 0.5 就是 1,小於 0.5 就是 0
correct += (pred == target).sum().item() # 紀錄目前正確的次數,如果與 target 一樣就 +1
cur_count += len(data) # 紀錄目前已經訓練了多少筆資料
# backword propagation
loss.backward() # 計算 loss 的 gradient
optimizer.step() # 更新 gradient
if batch_idx % log_interval == 0: # 每 log_interval 印出一次訓練狀態
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}\t Accuracy: {}/{} ({:.0f}%)'.format(
epoch,
cur_count,
len(train_dataloader.dataset),
100. * cur_count / len(train_dataloader.dataset),
loss.item(),
correct, len(train_dataloader.dataset),
100. * correct / len(train_dataloader.dataset))
)
train_losses.append(loss.item())
train_counter.append((batch_idx*16) + ((epoch-1)*len(train_dataloader.dataset)))
# 回傳目前的 accuracy
return correct / len(train_dataloader.dataset) - 建制 test function,主要目的是把 train 好的 model 透過 validation dataset 進行測試,看這個模型訓練在檢測未知資料時,準確率如何。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21def test():
network.eval() # 把上一步驟建立好的 network 拿進來使用,告知目前要進行 evaluation 的狀態
test_loss = 0 # 紀錄目前的 loss
correct = 0 # 紀錄目前正確的次數
with torch.no_grad(): # 因為不需要計算 gradient,因此可以使用 torch.no_grad() 來加速
for data, target in test_dataloader: # 透過 test_dataloader 來取得資料
# forward propagation
output = network(data) # 把資料餵入 train 好的 network 進行 forward propagation
test_loss += criterion(output, target).item() # 計算 loss
# Accuracy
pred = (output >= 0.5).float() # 0.5 is the threshold
correct += (pred == target).sum().item() # 紀錄目前正確的次數,如果與 target 一樣就 +1
test_loss /= len(test_dataloader.dataset) # 計算平均的 loss
test_losses.append(test_loss) # 把目前的 loss 加入 list 中
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss,
correct,
len(test_dataloader.dataset),
100. * correct / len(test_dataloader.dataset))
)
return correct / len(test_dataloader.dataset) # 回傳目前的 accuracy - 最後我們就可以根據 epoch 數量來進行模型的訓練,並做完每次 epoch 時,就透過
test()
來檢驗一下訓練狀況。1
2
3
4
5
6test()
train_accuracy_list = []
test_accuracy_list = []
for epoch in range(1, n_epochs + 1): # 進行 n_epochs 次的訓練
train_accuracy_list.append(train(epoch)) # 訓練完後,把目前的 accuracy 加入 list 中
test_accuracy_list.append(test()) # 訓練完後,把目前的 accuracy 加入 list 中 - 出來應該會長這樣:
Step4. 產出結果
最後可以透過以下指令來產出結果:
1 | import matplotlib.pyplot as plt |
Step5. 製造 Overfitting
製造 Overfitting 主要可以有幾種方式:
- epoch 調大,就會有一點 overfitting 的現象
- 或是把 hidden layer 數量提高或是把 neurons size 調大,也會有一點 overfitting 的現象
因為題目要求把 hidden layer 數量提高獲釋把 neurons size 調大,因此我們就來試試看吧!
最簡單的方式就是把,hidden layer調高一點,neurons size 調大一點,並且 epoch 調大一點,就可以看到 overfitting 的現象了。
- 建立一個 MultiLayerNet,並且把 hidden layer 跟 neurons size 調高一點
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class MultiLayerNet(nn.Module):
def __init__(self, D_in, H, D_out, num_layers):
super(MultiLayerNet, self).__init__()
neurons = 128
self.input = nn.Linear(D_in, H)
self.linear1 = nn.Linear(H, 128)
self.linear2 = nn.Linear(128, 64)
self.linear3 = nn.Linear(64, 32)
self.linear4 = nn.Linear(32, 16)
self.output = nn.Linear(16, D_out)
self.sigmoid = nn.Sigmoid() # Sigmoid activation for binary classification
def forward(self, x):
y_relu = F.relu(self.input(x))
y_relu = F.relu(self.linear1(y_relu))
y_relu = F.relu(self.linear2(y_relu))
y_relu = F.relu(self.linear3(y_relu))
y_relu = F.relu(self.linear4(y_relu))
y_pred = self.sigmoid(self.output(y_relu))
return y_pred
踩雷筆記:如果你純粹增加 layer 並不會有太多學習的效果,你會總是看到一水平的條線…,然後準確率就沒再上升了!後來同學發現,neurons要從多慢慢遞減,才會有學習的效果,因此我們可以把 neurons 設定成 128, 64, 32, 16!
「 同學說:這就像沙漏一樣」,這樣會慢慢一步步的過濾掉不重要的資訊,最後留下重要的資訊!」
-
針對 multi network 建立新的 multi_train() 跟 test_multi() function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58def train_multi(epoch):
multi_network.train() # 把上一步驟建立好的 network 拿進來使用
correct = 0
cur_count = 0
for batch_idx, (data, target) in enumerate(train_dataloader):
multi_optimizer.zero_grad()
# forward propagation
output = multi_network(data) # 你會發現這裡使用 multi_network 來進行 forward propagation
loss = multi_criterion(output, target) # 你會發現這裡使用 multi_criterion 來計算 loss
# Accuracy
pred = (output >= 0.5).float() # survival_rate is the threshold
correct += (pred == target).sum().item()
cur_count += len(data)
# backword propagation
loss.backward()
multi_optimizer.step()
if batch_idx % log_interval == 0:
print('Muti Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}\t Accuracy: {}/{} ({:.0f}%)'.format(
epoch,
cur_count,
len(train_dataloader.dataset),
100. * cur_count / len(train_dataloader.dataset),
loss.item(),
correct, len(train_dataloader.dataset),
100. * correct / len(train_dataloader.dataset))
)
train_losses.append(loss.item())
train_counter.append((batch_idx*16) + ((epoch-1)*len(train_dataloader.dataset)))
return correct / len(train_dataloader.dataset)
def test_multi():
multi_network.eval()
test_loss = 0
correct = 0
with torch.no_grad():
for data, target in test_dataloader:
# forward propagation
output = multi_network(data)
test_loss += multi_criterion(output, target).item()
# Accuracy
pred = (output >= 0.5).float() # 0.5 is the threshold
correct += (pred == target).sum().item()
test_loss /= len(test_dataloader.dataset)
test_losses.append(test_loss)
print('\nMulti Test set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss,
correct,
len(test_dataloader.dataset),
100. * correct / len(test_dataloader.dataset))
)
return correct / len(test_dataloader.dataset) -
重新 train model
1
2
3
4
5
6
7
8test_multi()
multi_train_accuracy_list = []
multi_test_accuracy_list = []
for epoch in range(1, n_epochs + 1):
multi_train_accuracy_list.append(train_multi(epoch))
multi_test_accuracy_list.append(test_multi()) -
重新畫圖: 你可以嘗試把 epoch 條到 500 次,就會看到 overfitting 的現象了!
1
2
3
4
5
6
7import matplotlib.pyplot as plt
plt.plot(multi_train_accuracy_list, color='orange')
plt.plot(multi_test_accuracy_list, color='green')
plt.ylim(0.5, 0.9)
plt.legend(['Train Accuracy', 'Test Accuracy', 'Mutli Train Accuracy', 'Mutli Test Accuracy'], loc='upper right')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
進階版
如果希望可以更加動態的調整neurons跟hidden layer的數量,可以使用以下方式:
neurons
: 一開始設定的 neurons 數量,如果設定 1024,就會從 1024 開始遞減至 16,每次遞減就除以 2,直到 neurons 數量小於 16 為止num_layers
: hidden layer 數量
1 | neurons = 1024 |
Step6. 使用 Dropout
這邊我們可以使用 Dropout 來避免 overfitting 的現象,主要是在 forward propagation 的時候,隨機把一些 neurons 給關掉,這樣就可以避免 overfitting 的現象。
1 | import torch.nn.functional as F |
這時候你會發現,overfitting 的現象就沒那麼嚴重了!以下是 epoch 數量設定為 200 時的結果。
Without Dropout
With Dropout
進階版
進階版的差異就是,dropout layer 的數量跟 hidden layer 的數量是一樣的,並且 dropout layer 的數量是隨著 hidden layer 的數量遞減的。
1 | class MultiLayerNet(nn.Module): |