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
クラス/関数の準備
データリスト関数
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. ]
データリストの分割の確認
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]
推論
上記で訓練したネットワークを利用して、テストデータに対する推論を実装します。
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などが豊富なコンペティションが、初心者にとっては良いと思います。
- House Prices - Advanced Regression Techniques
- Digit Recognizer
- Store Sales - Time Series Forecasting
kaggle以外でも勉強したい場合は、以下の私の体験談も参考になるかもしれません。
▶AI初心者が学会発表するまでの道のり【AI勉強法】
▶機械学習用にゲーミングPCを購入した(選び方、購入理由を紹介)
さいごに
Kaggleのタイタニック号コンペティションに挑戦してみました。
もしこの記事を読んでKaggleに興味をもったら、まずはKaggleのアカウントをつくってみてはいかがでしょうか。
以上です。
コメント