Kaggleのタイタニックの実装例をご紹介します。

【Kaggle入門】まずはタイタニックに挑戦(PyTorch)

ずっとKaggleが気になっていたのですが、「難しそうだなぁ」と敬遠していました。しかし、実際にアカウントをつくってみると、コンペティションに参加するのは意外と簡単でした。(良いスコアをとるのは簡単ではありません…)

Kaggleには入門として、Titanic - Machine Learning from Disasterというコンペティションがあります。このコンペティションは賞金がなく、チュートリアルのようなものになっています。

私もこのチュートリアルに挑戦してみたので、この記事に記録を残します。

実行環境

Kaggle Notebookで実装して実行しました。

Kaggle Notebookはブラウザ上で完結するので使いやすいです。Kaggleのアカウントをつくるだけで利用できるので、気軽に試すことができます。

課題の確認

このコンペティションの題材は、1912年に沈没したタイタニック号です。各乗客の情報をもとに、その乗客が生存したか否かを推測して、その推測精度を競います。

用意されているデータセットに含まれるデータの種類は以下のとおりです。

データ名 説明 データ型
PassengerId 乗客のID int64
Survived 生存したか否か int64(0 = No, 1 = Yes)
Pclass チケットの等級 int64(1 = 1st, 2 = 2nd, 3 = 3rd)
Name 乗客の名前 object
Sex 性別 object(male / female)
Age 年齢 float64
SibSp 同乗した兄弟姉妹や配偶者の数 int64
Parch 同乗した親や子の数 int64
Ticket チケット番号 object
Fare 運賃 float64
Cabin キャビン番号 object
Embarked 乗船港 object(C = Cherbourg, Q = Queenstown, S = Southampton)

実装

方針

簡単なDNN(Deep Neural Network)を使って推測していきます。

ネットワーク構造は、単純な3層の全結合層です。乗客の情報を入力して、その乗客の生存情報を出力します。

実装にはPyTorchを使います。

オススメの機械学習フレームワークはPyTorch【2020】

データセットの確認

import pandas as pd

df = pd.read_csv("../input/titanic/train.csv")
print(df.info())
df

出力:

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 PassengerId 891 non-null int64
1 Survived 891 non-null int64
2 Pclass 891 non-null int64
3 Name 891 non-null object
4 Sex 891 non-null object
5 Age 714 non-null float64
6 SibSp 891 non-null int64
7 Parch 891 non-null int64
8 Ticket 891 non-null object
9 Fare 891 non-null float64
10 Cabin 204 non-null object
11 Embarked 889 non-null object
dtypes: float64(2), int64(5), object(5)
memory usage: 83.7+ KB
None
1_dataset

クラス/関数の準備

データリスト関数

import pandas as pd

def makeDataList(csv_path):
    datalist = pd.read_csv(csv_path)
    datalist = datalist.drop(["Name", "Ticket", "Cabin"], axis=1)
    datalist = pd.get_dummies(datalist)
    datalist = datalist.fillna(-1)
    return datalist

テスト:

datalist = makeDataList("../input/titanic/train.csv")
print("datalist.values[0] =", datalist.values[0])
datalist

出力:

datalist.values[0] = [ 1. 0. 3. 22. 1. 0. 7.25 0. 1. 0. 0. 1. ]
2_datalist

データリストの分割の確認

from sklearn.model_selection import train_test_split

train_datalist, val_datalist = train_test_split(datalist, test_size=0.1, random_state=1234, shuffle=True)
print("len(train_datalist) =", len(train_datalist))
print("len(val_datalist) =", len(val_datalist))

出力:

len(train_datalist) = 801
len(val_datalist) = 90

データセットクラス

import numpy as np

import torch.utils.data as data

class DatasetMaker(data.Dataset):
    def __init__(self, datalist):
        self.input_datalist = datalist.drop(["PassengerId", "Survived"], axis=1).values.astype(np.float32)
        self.label_datalist = datalist["Survived"].values.astype(np.long)

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

    def __getitem__(self, index):
        inputs = self.input_datalist[index]
        labels = self.label_datalist[index]
        return inputs, labels

テスト:

dataset = DatasetMaker(datalist)
print("dataset.__len__() =", dataset.__len__())
print("dataset.__getitem__(index=0)[0] =", dataset.__getitem__(index=0)[0])
print("dataset.__getitem__(index=0)[1] =", dataset.__getitem__(index=0)[1])

出力:

dataset.__len__() = 891
dataset.__getitem__(index=0)[0] = [ 3. 22. 1. 0. 7.25 0. 1. 0. 0. 1. ]
dataset.__getitem__(index=0)[1] = 0

データローダの確認

import torch

batch_size = 5
dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True, drop_last=True)
batch_itr = iter(dataloader)
inputs, labels = next(batch_itr)

print("inputs =\n", inputs)
print("inputs.size() =", inputs.size())
print("labels =", labels)
print("labels.size() =", labels.size())

出力:

inputs =
 tensor([[ 3.0000, 35.0000, 1.0000, 1.0000, 20.2500, 1.0000, 0.0000, 0.0000, 0.0000, 1.0000],
  [ 2.0000, 21.0000, 2.0000, 0.0000, 73.5000, 0.0000, 1.0000, 0.0000, 0.0000, 1.0000],
  [ 3.0000, -1.0000, 0.0000, 0.0000, 7.7500, 1.0000, 0.0000, 0.0000, 1.0000, 0.0000],
  [ 1.0000, 37.0000, 1.0000, 1.0000, 52.5542, 0.0000, 1.0000, 0.0000, 0.0000, 1.0000],
  [ 3.0000, 19.0000, 0.0000, 0.0000, 8.1583, 0.0000, 1.0000, 0.0000, 0.0000, 1.0000]])
inputs.size() = torch.Size([5, 10])
labels = tensor([1, 0, 1, 1, 0])
labels.size() = torch.Size([5])

ネットワーククラス

from torch import nn

class Network(nn.Module):
    def __init__(self, dim_inputs, dim_mid, dim_outputs, dropout_rate):
        super().__init__()

        self.fc = nn.Sequential(
            nn.Linear(dim_inputs, dim_mid),
            nn.ReLU(),
            nn.Dropout(p=dropout_rate),
            
            nn.Linear(dim_mid, dim_mid),
            nn.ReLU(),
            nn.Dropout(p=dropout_rate),
            
            nn.Linear(dim_mid, dim_outputs)
        )

    def forward(self, x):
        x = self.fc(x)
        return x

テスト:

net = Network(len(dataset.__getitem__(index=0)[0]), 64, 2, dropout_rate=0.1)
print(net)
outputs = net(inputs)
print("outputs = \n", outputs)
print("outputs.size() =", outputs.size())

出力:

Network(
 (fc): Sequential(
  (0): Linear(in_features=10, out_features=64, bias=True)
  (1): ReLU()
  (2): Dropout(p=0.1, inplace=False)
  (3): Linear(in_features=64, out_features=64, bias=True)
  (4): ReLU()
  (5): Dropout(p=0.1, inplace=False)
  (6): Linear(in_features=64, out_features=2, bias=True)
 )
)
outputs =
 tensor([[-0.2765, 0.3128],
  [-1.0995, 2.4285],
  [ 0.0444, 0.6987],
  [-0.9588, 0.6936],
  [ 0.8020, 0.0032]], grad_fn=)
outputs.size() = torch.Size([5, 2])

訓練

上記で用意したクラス/関数を利用して、ネットワークを訓練するコードを実装します。

import time
import matplotlib.pyplot as plt

from sklearn.model_selection import train_test_split

import torch
from torch import nn
import torch.optim as optim

class Trainer:
    def __init__(self, csv_path, num_epochs, batch_size, lr, save_weights_path):
        self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
        print("self.device =", self.device)

        self.num_epochs = num_epochs
        self.save_weights_path = save_weights_path

        datalist = makeDataList(csv_path)
        train_datalist, val_datalist = train_test_split(datalist, test_size=0.1, random_state=1234, shuffle=True)
        train_dataset = DatasetMaker(train_datalist)
        val_dataset = DatasetMaker(val_datalist)
        train_dataloader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True, drop_last=True)
        val_dataloader = torch.utils.data.DataLoader(val_dataset, batch_size=batch_size, shuffle=False, drop_last=False)
        
        self.dataloaders_dict = {"train": train_dataloader, "val": val_dataloader}
        self.net = Network(
            dim_inputs = len(train_dataset.__getitem__(index=0)[0]),
            dim_mid = 64,
            dim_outputs = 2,
            dropout_rate=0.1
        )
        self.net.to(self.device)
        print(self.net)
        self.criterion = nn.CrossEntropyLoss()
        self.optimizer = optim.Adam(self.net.parameters(), lr=lr)

    def train(self):
        ## time
        start_clock = time.time()
        ## record
        record_loss_dict = {"train": [], "val": []}
        min_loss_epoch = 0.0
        ## loop
        for epoch in range(self.num_epochs):
            if epoch == 0 or not (epoch+1) % (num_epochs // 10):
                print("----------")
                print("Epoch {}/{}".format(epoch+1, self.num_epochs))
            ## phase
            for phase in ["train", "val"]:
                ## setting
                if phase == "train":
                    self.net.train()
                else:
                    self.net.eval()
                ## buffer
                loss_epoch = 0.0
                num_inputs = 0
                ## mini-batch
                for inputs, labels in self.dataloaders_dict[phase]:
                    inputs = inputs.to(self.device)
                    labels = labels.to(self.device)
                    ## reset gradient
                    self.optimizer.zero_grad()
                    ## compute gradient only in training
                    with torch.set_grad_enabled(phase == "train"):
                        ## forward
                        outputs = self.net(inputs)
                        loss = self.criterion(outputs, labels)
                        ## backward
                        if phase == "train":
                            loss.backward()
                            self.optimizer.step()
                    ## add
                    loss_epoch += loss.item() * inputs.size(0)
                    num_inputs += inputs.size(0)
                ## average loss
                loss_epoch = loss_epoch / num_inputs
                record_loss_dict[phase].append(loss_epoch)
                if epoch == 0 or not (epoch+1) % (num_epochs // 10):
                    print("{} Loss: {:.4f}".format(phase, loss_epoch))
            ## save
            if epoch == 0 or record_loss_dict["val"][-1] < min_loss_epoch:
                min_loss_epoch = record_loss_dict["val"][-1]
                torch.save(self.net.state_dict(), self.save_weights_path)
        ## time
        mins = (time.time() - start_clock) // 60
        secs = (time.time() - start_clock) % 60
        print ("training time: ", mins, " [min] ", secs, " [sec]")
        ## graph
        self.showGraph(record_loss_dict)

    def showGraph(self, record_loss_dict):
        graph = plt.figure()
        plt.plot(range(len(record_loss_dict["train"])), record_loss_dict["train"], label="Training")
        plt.plot(range(len(record_loss_dict["val"])), record_loss_dict["val"], label="Validation")
        plt.legend()
        plt.xlabel("Epoch")
        plt.ylabel("Loss")
        plt.title("last loss: train=" + str(record_loss_dict["train"][-1]) + ", val=" + str(record_loss_dict["val"][-1]))
        plt.show()

        
if __name__ == '__main__':
    csv_path = "../input/titanic/train.csv"
    num_epochs = 2000
    batch_size = 80
    lr = 0.0001
    save_weights_path = "./weights.pth"

    trainer = Trainer(csv_path, num_epochs, batch_size, lr, save_weights_path)
    trainer.train()

出力:

self.device = cpu
Network(
 (fc): Sequential(
  (0): Linear(in_features=10, out_features=64, bias=True)
  (1): ReLU()
  (2): Dropout(p=0.1, inplace=False)
  (3): Linear(in_features=64, out_features=64, bias=True)
  (4): ReLU()
  (5): Dropout(p=0.1, inplace=False)
  (6): Linear(in_features=64, out_features=2, bias=True)
 )
)
----------
Epoch 1/2000
train Loss: 1.0380
val Loss: 0.8441
----------
Epoch 200/2000
train Loss: 0.5351
val Loss: 0.4802
----------
Epoch 400/2000
train Loss: 0.4718
val Loss: 0.4156
----------
Epoch 600/2000
train Loss: 0.4515
val Loss: 0.3967
----------
Epoch 800/2000
train Loss: 0.4370
val Loss: 0.3860
----------
Epoch 1000/2000
train Loss: 0.4145
val Loss: 0.3808
----------
Epoch 1200/2000
train Loss: 0.4167
val Loss: 0.3863
----------
Epoch 1400/2000
train Loss: 0.4003
val Loss: 0.3837
----------
Epoch 1600/2000
train Loss: 0.3905
val Loss: 0.3757
----------
Epoch 1800/2000
train Loss: 0.3829
val Loss: 0.3774
----------
Epoch 2000/2000
train Loss: 0.3865
val Loss: 0.3762
training time: 0.0 [min] 31.610609769821167 [sec]
3_loss

推論

上記で訓練したネットワークを利用して、テストデータに対する推論を実装します。

import time
import pandas as pd

import torch

class Evaluator:
    def __init__(self, csv_path, weights_path, save_csv_path):
        self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
        print("self.device = ", self.device)

        self.save_csv_path = save_csv_path
        self.datalist = makeDataList(csv_path)
        self.net = Network(
            dim_inputs = self.datalist.drop("PassengerId", axis=1).values.shape[1],
            dim_mid = 64,
            dim_outputs = 2,
            dropout_rate=0.1
        )
        self.net.to(self.device)
        if torch.cuda.is_available():
            loaded_weights = torch.load(weights_path)
        else:
            loaded_weights = torch.load(weights_path, map_location={"cuda:0": "cpu"})
        self.net.load_state_dict(loaded_weights)
        print("Weights have been loaded:", weights_path)
        print(self.net)

    def evaluate(self):
        ## time
        start_clock = time.time()
        ## setting
        self.net.eval()
        ## ndarray -> tensor
        inputs = torch.from_numpy(self.datalist.drop("PassengerId", axis=1).values.astype(np.float32))
        inputs = inputs.to(self.device)
        ## forward
        with torch.no_grad():
            outputs = self.net(inputs)
            outputs = torch.max(outputs, dim=1).indices
        ## save
        self.writeCSV(outputs)
        ## time
        mins = (time.time() - start_clock) // 60
        secs = (time.time() - start_clock) % 60
        print ("evaluation time: ", mins, " [min] ", secs, " [sec]")

    def writeCSV(self, outputs):
        result_df = pd.DataFrame({"PassengerId": self.datalist["PassengerId"].values, "Survived": outputs.cpu().detach().numpy()})
        result_df.to_csv(self.save_csv_path, index=False)
        print(result_df)


if __name__ == '__main__':
    csv_path = "../input/titanic/test.csv"
    weights_path = "./weights.pth"
    save_csv_path = "./submission.csv"

    evaluator = Evaluator(csv_path, weights_path, save_csv_path)
    evaluator.evaluate()

出力:

self.device = cpu
Weights have been loaded: ./weights.pth
Network(
 (fc): Sequential(
  (0): Linear(in_features=10, out_features=64, bias=True)
  (1): ReLU()
  (2): Dropout(p=0.1, inplace=False)
  (3): Linear(in_features=64, out_features=64, bias=True)
  (4): ReLU()
  (5): Dropout(p=0.1, inplace=False)
  (6): Linear(in_features=64, out_features=2, bias=True)
 )
)
PassengerId Survived
0 892 0
1 893 0
2 894 0
3 895 0
4 896 1
.. ... ...
413 1305 0
414 1306 1
415 1307 0
416 1308 0
417 1309 1

[418 rows x 2 columns]
evaluation time: 0.0 [min] 0.004124641418457031 [sec]

結果&反省

上記の実装で推論した結果、スコアは0.76076でした。低いスコアですね。

Discussionsを見てみると、決定木を用いた手法(e.g. Random forest)が多い印象です。また、アンサンブル学習(=複数の手法で多数決をとるテクニック)もよく使われているようです。

Kaggleでは、Discussionsや他人のコードを見て学ぶことも醍醐味です。

タイタニックの次は?

私の場合、タイタニック号コンペティションの次は、以下に挙げているような「Getting Started」に分類されるコンペティションに挑戦しました。そのなかでも、参加者が多く、Discussionsなどが豊富なコンペティションが、初心者にとっては良いと思います。

kaggle以外でも勉強したい場合は、以下の私の体験談も参考になるかもしれません。

AI初心者が学会発表するまでの道のり【AI勉強法】
機械学習用にゲーミングPCを購入した(選び方、購入理由を紹介)

さいごに

Kaggleのタイタニック号コンペティションに挑戦してみました。

もしこの記事を読んでKaggleに興味をもったら、まずはKaggleのアカウントをつくってみてはいかがでしょうか。


以上です。

Ad.