Kaggle實戰:泰坦尼克倖存者預測 - 上

(文章同步更新於個人博客@dai98.github.io

源代碼: Github Kaggle

泰坦尼克倖存者預測是Kaggle上數據競賽的入門級別的比賽,我曾經在一年前作爲作業參加過這個比賽,我想要再次從這個比賽開始,嘗試不同的模型,來當作在Kaggle比賽的起點。

關於此次競賽,我想分成兩個部分,第一個部分基於PyTorch建立神經網絡,第二個部分使用sklearn做多個分類器投票。

使用的編程環境及依賴包版本:

import matplotlib
from matplotlib import pyplot as plt
import seaborn as sns
import IPython
import numpy as np
import pandas as pd
from collections import Counter
from sklearn import preprocessing
from sklearn.model_selection import train_test_split
import torch
import torch.nn as nn
import torch.nn.functional as F
import warnings
import sys
import os
warnings.filterwarnings("ignore")
os.environ['CUDA_LAUNCH_BLOCKING'] = "1"

在這裏插入圖片描述

一、數據預處理

首先我們看看數據中有多少空缺值:

我們可以看到,AgeCabinEmbarkedFare有空缺值。Cabin有78%的空缺值,而且其餘值也沒有明顯的規律,我們在之後可以直接刪除改列。對於其他值,我們可以對Age和Fare補充中位數,把Embarked補充頻率最高的值。

我們可以把訓練數據train與測試數據test拼接在一起,一起來進行處理,可以省去各自處理的麻煩。

data = [train,test]

train_backup = train.copy()
test_backup = test.copy()
for dataset in data:
    dataset["Age"].fillna(dataset["Age"].median(),inplace = True)
    dataset["Fare"].fillna(dataset["Fare"].median(),inplace = True)
    dataset["Embarked"].fillna(dataset["Embarked"].mode()[0],inplace = True)

這裏有一點需要提醒,如果我們使用for循環來對兩個數據集依次進行處理,需要注意Python在對列表值的遍歷的時候提供的值是列表值中的淺拷貝,也就是說datasetdata[0]中的值對應的是一個指針,然而是兩個不同的對象。所以對dataset做重賦值的情況,並不會真正改變traintest兩個數據集。Pandas中有些函數(drop,apply等)可以對數據集進行操作來改變數據集,但記得要把inplace參數設置爲True。如果想要在循環中修改數據集的值,一定要記得通過索引來訪問列表!

現在沒有了空缺值,我們來處理一下其他的數據。我們從Name開始,雖然一個人叫什麼不會影響他/她是否倖存,但如果你自己觀察,每個人的名字中間帶有他們的稱謂,或者前綴(Mr, Mrs),能從一定程度上反映出他/她的性別和社會地位,也會一定程度的影響倖存與否。

threshhold = 10

for dataset in data:
    
    dataset["Prefix"] = dataset["Name"].apply(lambda x:x.split(" ")[1])
    freq = (dataset["Prefix"].value_counts() < threshhold)
    dataset["Prefix"] = dataset["Prefix"].apply(lambda x: "others" if freq.loc[x] else x)

這裏我發現稱謂的種類實在太多了,有許多隻出現了一兩次,所以我把所有出現低於10次的稱謂都重新賦爲"others"。

我們來繼續處理其他變量。我看到SibSpParch代表船上兄弟姐妹、配偶、父母、孩子的數量,可以把它們相加來代表家人的數量,並再添加一個二元變量,代表該人當時在船上是否只有一個人。注意,我再相加的時候多加了1,代表整個家庭的人數,你也可以不加1,代表他/她的家人的數量。

for dataset in data:
    # The size of the whole family
    dataset["Family"] = dataset["Parch"] + dataset["SibSp"] + 1 
    dataset["IsAlone"] = dataset["Family"].apply(lambda x: 1 if x == 1 else 0)
    
    dataset["FareBin"] = pd.qcut(dataset["Fare"],4)
    dataset["AgeBin"] = pd.cut(dataset["Age"],4)
    dataset["FamilyBin"] = pd.cut(dataset["Family"],3)

我又根據數值的大小,將FareAgeFamily進行分段重新歸類。這樣的離散化處理使得我們將連續性變量轉換爲類別型的變量。注意cut函數和qcut的區別,cut函數是將數據分割成等長區間,每個區間的觀測值數量不等;qcut是將數據分割成自適應區間,雖然區間的長度是不固定的,但是每個區間的觀測值數量是相等的。

然而,如果此時你查看我們的數據,你會發現數據的值變成了區間。爲了再之後做獨熱碼的時候變量名更加清晰,我們將變量的值重新賦爲數字類別:

label = preprocessing.LabelEncoder()
label_columns = ["FareBin","AgeBin","FamilyBin"]

for dataset in data:
    
    for label_column in label_columns:
        dataset[label_column] = label.fit_transform(dataset[label_column])

最後一步,因爲數字類別的特徵會導致類別本身數值大小也成爲特徵,因此我們把數字類別轉換爲獨熱碼。例如,類別3轉換爲[0 0 1],這樣數字本身的大小便不會影響模型了。除此之外,我們再將沒有用處的列刪除掉。

drop_columns = ["PassengerId","Name","Age","SibSp","Parch",
				"Ticket","Fare","Cabin","Family"]
dummy_columns = ["Pclass","Sex","Embarked","Prefix","FareBin","AgeBin","FamilyBin"]

for i,dataset in enumerate(data):
    
    data[i].drop(drop_columns,axis = 1,inplace = True)
    
    for dummy in dummy_columns:
        dummy_df = pd.get_dummies(dataset[dummy],prefix = dummy)
        data[i] = pd.concat([data[i],dummy_df],axis = 1)
        data[i].drop(dummy,axis = 1,inplace = True)

最後數據的特徵:

我們再將數據重新分割成訓練集和測試集:

train = data[0]
test = data[1]

train_data = train.drop("Survived",axis = 1,inplace = False)
train_target = train["Survived"]

二、變量選擇

我們通過計算每個變量之間的相關係數,來判斷每個變量和目標變量(Survived)之間的相關係數,並通過每個變量之間的相關係數來判斷多重共線性是否存在。

correlation = train.corr()

_, ax = plt.subplots(figsize = (10,10))
colormap = sns.diverging_palette(220,10,as_cmap = True)
_ = sns.heatmap(correlation, 
            cmap = colormap,
            square = True,
            cbar_kws = {"shrink":.6},
            ax = ax,
            linewidths = 0.1, vmax = 1.0, linecolor = "white",
            annot_kws = {"fontsize":12}
)
plt.title("Correlation Heatmap \n")

我們發現相關係數比較高的變量是SexPrefix,也就是如果一個人的性別是男的,他就不可能是女的,並且很大概率被稱作"Mr."。對我們的迴歸沒有很大的影響,我們無需刪除。

三、模型搭建

神經網絡的搭建基於PyTorch,首先我們把數據從PandasDataFrame轉換爲Numpyarray,再通過torchfrom_numpy函數來生成Tensor

train_data = np.array(train_data)
train_target = np.array(train_target)
test = np.array(test)

data_tensor = torch.from_numpy(train_data).type(torch.FloatTensor)
target_tensor = torch.from_numpy(train_target).type(torch.LongTensor)
test_tensor = torch.from_numpy(test).type(torch.FloatTensor)

注意,雖然我們的數據集只有0和1兩種值,但是必須要設置爲FloatTensor,因爲稍後要計算交叉熵損失函數;而目標張量target_tensor設置成LongTensor即可。

現在我們來設置一下之後會用到的模型的超參數:

config = {
    "USE_CUDA":torch.cuda.is_available(),
    "N":train_data.shape[0],
    "D_in":train_data.shape[1],
    "H":train_data.shape[1]+1,
    "D_out":2,
    "learning_rate":0.02,
    "epoch":10000
}

下面我們就可以開始創建模型了,在這裏我用了三層的神經網絡,前兩層使用了ReLU激活函數,最後一層使用Sigmoid激活函數來進行二分類,並在中間添加了一層Dropout,來防止過擬合。

class Model(nn.Module):
    
    def __init__(self,D_in,H,D_out):
        super(Model,self).__init__()
        self.linear1 = nn.Linear(D_in,H)
        self.linear2 = nn.Linear(H,H)
        self.linear3 = nn.Linear(H,D_out)
        self.dropout = nn.Dropout(0.3)
        
    def forward(self,x):
        layer1 = F.relu(self.linear1(x))
        layer2 = F.relu(layer1)
        layer2 = self.dropout(layer2)
        layer3 = F.sigmoid(self.linear3(layer2))
        return layer3
             
    def predict(self,x):
        pred = self.forward(x)
        ans = []
        for t in pred:
            if t[0]>t[1]:
                ans.append(0)
            else:
                ans.append(1)
        return torch.tensor(ans)

下面我們就可以創建模型對象,以及損失函數、優化器和Scheduler:

model = Model(config["D_in"],config["H"],config["D_out"])
loss_func = nn.CrossEntropyLoss()  
optimizer = torch.optim.Adam(model.parameters(), lr=config["learning_rate"])
scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma = 0.5)

Scheduler是幫助我們的Learning rate衰退用的,在下面會詳細解釋到。

如果安裝了CUDA和Cudnn,可以使用GPU加速計算:

if config["USE_CUDA"]:
    model = model.cuda()
    data_tensor = data_tensor.cuda()
    target_tensor = target_tensor.cuda()
    test_tensor = test_tensor.cuda()

現在我們開始訓練過程:

losses = []

for epoch in range(config["epoch"]+1):

    pred_tensor = model(data_tensor)
    loss = loss_func(pred_tensor,target_tensor)
    if epoch % 500 == 0:
        print("Epoch",epoch," loss",loss.item())
    if epoch % 1000 == 0:
        loss_value = loss.item()
        if len(losses) == 0 or loss_value < min(losses):
            print("Minimum Loss Updated")
            torch.save(model.state_dict(),"model.pth")
        else:
            print("Learning rate decay")
            scheduler.step()
        losses.append(loss_value)
    loss.backward()
    optimizer.step()
    optimizer.zero_grad()

在這裏,我設置了每500輪顯示一次損失函數的值;每1000輪存儲一次損失函數,如果損失函數的值是當前最小的值,說明當前是模型最優的時候,我們將其保存,如果稍後有更優的值,將覆蓋此次值;如果損失函數的值沒有增大,那我們就將Learning rate減小一半。

爲什麼要減小呢?實際上模型訓練的過程和下山的過程很像,Learning Rate就是我們步子的大小。我們訓練的過程實際上就是從當前位置走到最低點。在最開始的時候我們步子很大,所以下降的很快,過了一會兒,我們發現因爲我們步子太長,一直在一個坑的兩側邁來邁去,進不到坑裏面。那麼怎麼辦呢?只要步子小一點就行了!這也是Scheduler的作用,在訓練受到阻礙的時候幫助我們把Learning Rate減小。

現在訓練完成,我們只需要新建一個模型,讀取剛纔模型最優時候的狀態,再用最優模型來預測測試集數據即可:

best_model = Model(config["D_in"],config["H"],config["D_out"])
if config["USE_CUDA"]:
    best_model = best_model.cuda()
best_model.load_state_dict(torch.load("model.pth"))
test_target = best_model.predict(test_tensor)
test_target = test_target.cpu().numpy()

注意,我們在使用GPU訓練之後,要將Tensor從GPU上推回CPU。
最後我們把結果保存成csv文件即可。

res = {
    "PassengerId":test_backup["PassengerId"],
    "Survived":test_target
}
res_dataframe = pd.DataFrame(res)
res_dataframe.to_csv("result.csv", index = False)

可以看到,我們的準確率爲77.51%,下一篇文章使用的多分類器投票將進一步提升正確率。

四、參考資料

[1]. A Data Science Framework: To Achieve 99% Accuracy
[2]. Python進行泰坦尼克生存預測
[3]. PyTorch實現二分類器
[4]. Kaggle Titanic生死率預測

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章