Kaggleの世界に足を踏み出すべく、Titanicコンペティションに参加してみました。今回はベースラインモデルの作成とユーティリティースクリプトの記述を中心に見てみます。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
こんにちは。Deep Insider編集部のかわさきです。Deep Insiderでは主としてPythonやVisual Studio Codeの記事を手がけています。たまには機械学習の記事も書いていますけど。そんなことから、もう1人の編集者である一色さんとは違って、もっとレベルの低いところから始める「機械学習初心者」といってもよいでしょう。そんな人間がKaggleで悪戦苦闘する様をお楽しみいただければ幸いです。
本連載は「Kaggle初心者のためのコンペガイド ― Titanicの先へ」という記事で始まっていますが、初心者らしくボクはTitanicから始めてみることにしました。
スタンスとしては「取りあえずDNN(Deep Neural Network)に突っ込んでみる」ところから始めてみようと思っています。また、たがいのスキルや知識、それから好みの差から、一色さんの記事とは違った感じの記事になることでしょう。
ディープラーニングを中心にKaggleに挑戦したり機械学習に実践したりすることに興味がある人に楽しめる記事になりそうですね。(一色)
TitanicとはKaggleにおけるHello Worldともいえる「Titanic - Machine Learning from Disaster」のこと。訓練データとして用意されている乗客のさまざまな属性(名前、性別、年齢、チケット種別、etc)と生死の情報から、他の乗客の生死を推測してみるというものです。このコンペティションに参加しながら、Kaggleや機械学習で必要なことを体感してみるのが今回から続く数回の主なテーマとなるはずです。が、コードを見直していて、既に違う方向で話したいことが出てきました(笑)。
まずは今回の記事を書くまで、どんなことをしてきたかをざっくりと表にまとめました。書いてみるとたいしたことはしていないような気がします。
バージョン | 説明 | パブリックスコア | 大ざっぱな順位 |
---|---|---|---|
0 | ベースライン | 0.73444 | 13200位 |
2 | k-fold交差検証を導入 | 0.74162 | 13000位 |
3 | エポック数を500から1000に | 0.72727 | 低下(一番ひどい……) |
4 | モデルを3種類に | 0.76794 | 9900位 |
今回やってきたこと |
ベースラインのモデルは3層の全結合型DNNです。これに訓練データを突っ込んで学習をさせているだけです。それだけですから、順位も低いに決まっています。
上の表を見ると、バージョン0があってバージョン1がない辺りから筆者がKaggle素人であることがよく分かります(なんでそうなっているのか、全く記憶にございません)。
バージョン1はないのか……あはは……。それはそうと、初心者のうちは、すごい人のノートブックを参考に最初から高いスコアと順位で始めるよりも、そのすごいノートブックに到達するまでの過程を自分で体験できるように、今回のように真っさらなところから始めてみるのもよいと思います。その方がスコアを上げやすくてモチベーションを保ちやすいと思うので。
次のバージョンでは、k-fold交差検証を行うために、指定した数のフォールドにPyTorchのデータセットを分割する関数(とそのヘルパー関数)を定義したり、PyTorchのネットワークモジュール(nn.Moduleクラス)を継承するDNNクラスを定義したり、学習と検証、推測を行うための関数を定義したりして、それらをKaggleの「ユーティリティースクリプト」として登録しました。スコアは少しだけ上昇しましたが、それよりも頻繁に使うであろうクラスや関数をPythonのスクリプトとして括り出したことで、ノートブックのコードがシンプルになったことが良かった点だと思っています。
バージョン3では、スコアが少し上がったことに気をよくして「もっと学習させればスコアがもっと上向くんじゃ?」とエポック数を増やしてみたのが逆に悪影響したのか、スコアが低下してしまいました。反省。
過学習ですかね。
過学習かなーと思いました。
バージョン2では(なぜそうしたのか筆者も覚えがないのですが)なぜか4層の全結合型のDNNを定義していました。そこでバージョン4は、これに加えて、中間層を持たない(線形回帰を行う)モデルと、3層の全結合型のDNNを作成して、それら3つのモデルを使ってk-fold交差検証を行うようにしています。この結果、スコアはそれなりに上昇して下から3分の1程度のラインまで到達しました。先はまだまだ長いでしょうが、以下ではここに至るまでの道のりを簡単にまとめます。
といっても詳しいコードの説明までしていると、いつまで経ってもこの原稿が終わりません。ノートブックを公開したので詳しくはそちらを参照してください。
なお、自分がこれまでにしてきたサブミッション(推測結果の提出)はミッションのページにある[My Submissions]をクリックすると参照できます。
既に述べた通り、「取りあえずDNNに突っ込んでみる」のがスタンスですから、ここでは全結合型のDNNをPyTorchで作成して、そこにデータを入力してみることにしました(出力は0〜1の範囲の値を1つとして、その値が0.5以上なら生存と、0.5未満なら亡くなったものと判断することにしました)。なお、以下でご覧いただくコードは上記ノートブックから関連する部分をまとめたもので、個別のセルの内容とは異なっています。
class Net(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super().__init__()
self.fc1 = nn.Linear(input_size, hidden_size)
self.fc2 = nn.Linear(hidden_size, output_size)
def forward(self, x):
y = self.fc1(x)
y = torch.sigmoid(y)
y = self.fc2(y)
y = torch.sigmoid(y)
return y
INPUT_SIZE = 6
HIDDEN_SIZE = 32
OUTPUT_SIZE = 1
net = Net(INPUT_SIZE, HIDDEN_SIZE, OUTPUT_SIZE)
また訓練データおよびテストデータの前処理はpandasを使い、不要と思われる列のドロップ、性別の数値化、欠損値の補填(チケット料金を表すFare列、年齢を表すAge列で欠損値をその列の平均値で埋めることにしました)などだけを行いました。
dataframe = pd.read_csv('../input/titanic/train.csv')
print(sum(dataframe['Survived']) / len(dataframe))
drop_rows = ['PassengerId', 'Name', 'Ticket', 'Cabin', 'Embarked']
dataframe = dataframe.drop(drop_rows, axis=1)
average_age = dataframe['Age'].mean()
print('average ages:', average_age)
dataframe['Age'] = dataframe['Age'].fillna(average_age)
dataframe = dataframe.replace('male', 0).replace('female', 1)
これらの処理を行った上で、PyTorchのDatasetクラスの派生クラスを定義して、データフレームを渡し、データセットオブジェクトを作成しました。
class TitanicDataset(IterableDataset):
def __init__(self, df):
super().__init__()
self.X = df.drop(['Survived'], axis=1)
self.y = df['Survived']
def __len__(self):
return len(self.X)
def __getitem__(self, idx):
X = self.X.iloc[idx, :].values
y = self.y.iloc[idx]
return X, y
dataset = TitanicDataset(dataframe)
train_dataset, val_dataset = torch.utils.data.random_split(dataset, [640, 251])
trainloader = DataLoader(train_dataset, batch_size=32, shuffle=True)
valloader = DataLoader(val_dataset, batch_size=32, drop_last=True)
覚えておいてほしいのは、torch.utils.data.random_split関数です。これは与えられたデータセットを、第2引数に指定した反復可能オブジェクトの個数(上のコードなら2つ)に分割するものです。そして、1つは640個の要素で、もう1つは251個の要素で構成されるようになります。
実際に学習を行うコードは以下のようなものです。PyTorchの典型的なコードなのであまり説明する箇所はありません。
criterion = nn.BCELoss()
optimizer = optim.Adam(net.parameters(), lr=0.01)
EPOCHS = 500
losses = []
for epoch in range(EPOCHS + 1):
for X, y in trainloader:
optimizer.zero_grad()
pred = net(X.float())
loss = criterion(pred.reshape(-1), y.float())
loss.backward()
optimizer.step()
losses.append(loss.detach().numpy())
if epoch % 50 == 0:
print(f'epoch: {epoch}, loss: {loss}')
この他にも検証を行うコードや推測を行うコードももちろん記述しています。テストデータを使って推測を行った結果(パブリックスコア)は、既に述べたように0.73444とあまりよくはないものでした。が、まずはここをスタート地点としてコードを洗練させていく準備ができたといえます。
バージョン0(バージョン1)ができたところで、本来は性別をone-hot表現にするとか、家族の有無を表す新たな特徴量を導入するとかに進むのが普通です(Webをいろいろと検索した限りは)。ですが、筆者としてはそれよりも「学習や検証を行うコードを関数化して再利用ができるようにした方が、コードが見やすくなるんじゃない?」とか、「k-fold交差検証をしてみたい」とか、そっちの欲求の方が高まってきたので、コードの整理をすることにしました。
一色さんとの(密ではないオンライン)ミーティングで「ユーティリティースクリプトって知ってる? 結構便利だよ」と教えてもらったのがきっかけです。ありがとうございました。
ユーティリティースクリプトは便利だよね。こういったTips的な内容はあまり情報がないから、何か便利機能を発見したら、この連載でも紹介していければうれしいです。
ユーティリティースクリプトは、KaggleのWeb UIにある[File]メニューから[New Notebook]を選択して、ノートブックを作成した後に、その[File]メニューから[Set as Utility Script]を選択し、さらに[File]メニューから[Editor Type]−[Script]を選択することで編集できます。
このようにして作成したユーティリティースクリプトには、先ほど見たようなTitanicDatasetクラスやDNNを表すクラス、各種の関数などを記述します。今回はデータセットを分割するためのコードをkfoldds.pyファイルに、学習や検証などで使用するクラスや関数をtitanic_nn.pyファイルに記述しました。
例として、短めなkfoldds.pyファイルを以下に示します(titanic_nn.pyファイルについてはこちらを参照してください)。
import random
from random import sample
from torch.utils.data import IterableDataset, Subset
def make_kfold_range(length, k):
random.seed(2)
quotient = length // k
remainder = length % k
trains = [] # kfolded train data. [[data0], [data1], ...]
vals = [] # kfolded val data. [[data0], [data1], ...]
indices = sample(range(length), length) # shuffle indices
if k <= 1:
return [(indices, [])]
for num in range(k):
start, end = 0, quotient
tmp = []
for cnt in range(k):
if num == cnt:
end += remainder
vals.append(indices[start:end])
else:
tmp.extend(range(start, end))
start, end = end, end + quotient
r: list[int] = [indices[idx] for idx in tmp]
trains.append(r)
return list(zip(trains, vals))
def get_kfold_datasets(dataset, k):
datasets = []
if not hasattr(dataset, '__len__'):
raise TypeError(f'{dataset} does not have a __len__ attr')
else:
ds_length = len(dataset) # ok since dataset has a __len__ attr
for trainidx, validx in make_kfold_range(ds_length, k):
datasets.append((Subset(dataset, trainidx), Subset(dataset, validx)))
return datasets
このファイルでは、2つの関数を定義しています。make_kfold_range関数はデータ数と分割数を与えると、range(データ数)をランダムに並べ替えて、それらを指定した数に分割した結果を返してくれます。この結果はデータセットに対するインデックスとして使用することを念頭に置いたものです。
そして、get_kfold_datasets関数はmake_kfold_range関数を使って、データセットを指定した数に分割するものです。
Webを「pytorch kfold」などとして検索すると、scikit-learnと組み合わせる例がたくさん見つかります。でも、PyTorchを使うんならPyTorchだけでやりたいじゃん? というのがこんなコードを書き始めた理由です。それ以上の意味はありません。そして、詳しくは後述しますが、実はこんなコードを書かなくても恐らくは……(泣)。
データ分割の部分にだけ、scikit-learnのたった1つの関数を呼び出す気持ち悪さは分かりますね……。まぁしかし、永続的にコードをメンテナンスするアプリ開発ならまだしも、Kaggleのように何度も作り直すのが前提のコードであれば、すっきり感よりも雑然としていても効率重視で、部分的に使えるライブラリ機能を所々で五月雨的に使って済ませてしまってもいいのかなと思います。そこを割り切れるかどうかは、実装者の性格によるところがあるかもしれません。
ユーティリティースクリプトをノートブックで使用するには、[File]メニューから[Add utility script]を選択します。
これによりダイアログが表示されるので、作成したユーティリティースクリプトを追加します。このとき、気を付けたいのは元のファイル名の末尾に「_py」というサフィックスが追加されることです(上の画像の右側を参照してください)。インポートを行うときには、そちらをモジュール名として使用する必要があります。
Copyright© Digital Advantage Corp. All Rights Reserved.