かえるのプログラミングブログ

プログラミングでつまずいたところとその解決策などを書いていきます。

PyTorch NN を用いて Titanic コンペに挑戦する。

こんばんは、かえるるる(@kaeru_nantoka)です。今回は、テーブルデータに Neural Network (以下 NN) を応用してみる Tutorial として PyTorch で Titanic 問題に取り組んでみたので、そのソースコードと説明をご紹介できればと思います。

今回のソースコードは、kaggle kernel に公開設定で置いております( https://www.kaggle.com/kaerunantoka/titanic-pytorch-nn-tutorial?scriptVersionId=12211278 ) ので、質問やアドバイスなど大歓迎です。


目次

1. 動機

2. 参考 kernel 集

3. ソースコードの説明

4. 感想など

5. Todo


1 : 動機

https://twitter.com/nyanp/status/1111242580765769728

こちらの Nomi さんのスライドにも言及されているように、昨今の kaggle で上位入賞を果たしている方(チーム)はほとんど木系のモデルのみではなく、NN でもモデリングをしてアンサンブルすることでスコアを伸ばしています。

そんな重要な知見であるテーブルデータにおける NN ですが、ネットで調べてみてもほとんど出てこず、自分で勉強していく中で何か参考になる簡単な問題に対して適用されたコードがあればいいなと思いましたので、復習としてまとめたものを output しようと思った次第です。


2: 参考kernel集

今回の私のコードを書くにあたって参考にしたカーネルです。ネットで調べてもほとんど出てこず〜と上では書きましたが、PyTorch の docs で勉強し、うまい書き方ないかなと、過去コンペのコードを漁っていた傍、参考になりそうなものはありました。こちらにまとめておきます。

i : Quora コンペより

( https://www.kaggle.com/hengzheng/pytorch-starter )

model.fit() の引数で ループ数を回す keras と異なり、PyTorch では、 dataloader というデータと正解ラベルを n個づつ返す iterater のループを、さらに epoch数のループで回すような見慣れない書き方をします。

それプラス Kfold のループを回すような書き方を探していた時に見つけたカーネルです。 Quora コンペも Titanic コンペと同じ 2値分類タスクなので、評価指標の f1-score の部分を acc に書き換える実装をして他の部分はほとんどそのまま使っています。

i i : TwoSigma コンペより

( https://www.kaggle.com/christofhenkel/market-data-nn-baseline )

NN 自体は keras での実装ですが、NN に特徴量を投入する前の前処理部分を参考にしました。カテゴリカルデータ(文字などの数値で与えられていない特徴量や一応数値では与えられているが数値としての意味を持たないもの(男性、女性を 0, 1 として表して与えられているもの))の encoding や numerical データ(数値がそのままの意味を持っているデータ。金額や人数など)を正則化するなど最低限の前処理がなされております。


3: ソースコードの説明

[2] PyTorch の利点である 「seed を簡単に固定できる」 を実現してくれる部分です。

    def seed_everything(seed=1234):
        random.seed(seed)
        os.environ['PYTHONHASHSEED'] = str(seed)
        tf.set_random_seed(seed)
        np.random.seed(seed)
        torch.manual_seed(seed)
        torch.cuda.manual_seed(seed)
        torch.backends.cudnn.deterministic = True
    kaeru_seed = 1337
    seed_everything(seed=kaeru_seed)

[5] categorical columns と numerical columns を指定します。 cat_cols は今回は数値でないものを適当に指定しています。

num_cols はテーブルによっては非常に多くなることもあるので、list(set()) を使用すると簡単に取得できます。

cat_cols = ['Cabin','Embarked','Name','Sex','Ticket',]

num_cols = list(set(train.columns) - set(cat_cols) - set(["Survived"]))

[6] [7] categorical columns と numerical columns それぞれの前処理の部分です。

def encode(encoder, x):
    len_encoder = len(encoder)
    try:
        id = encoder[x]
    except KeyError:
        id = len_encoder
    return id

encoders = [{} for cat in cat_cols]


for i, cat in enumerate(cat_cols):
    print('encoding %s ...' % cat, end=' ')
    encoders[i] = {l: id for id, l in enumerate(train.loc[:, cat].astype(str).unique())}
    train[cat] = train[cat].astype(str).apply(lambda x: encode(encoders[i], x))
    print('Done')

embed_sizes = [len(encoder) for encoder in encoders]

# =====

from sklearn.preprocessing import StandardScaler
 
train[num_cols] = train[num_cols].fillna(0)
print('scaling numerical columns')

scaler = StandardScaler()
train[num_cols] = scaler.fit_transform(train[num_cols])

[8] [9] 今回は私の練習として、自作の層を定義してそれを利用するような NN にしてます。

nn.Sequential() は keras の sequential model と同じように使えます。

自作レイヤーを使わずに net を定義し直すと、

net = nn.Sequential( nn.Linear(12, 32) nn.ReLU(), nn.Dropout(0.5), nn.Linear(32, 1) )

というように書けます。

class CustomLinear(nn.Module):
    def __init__(self, in_features,
                 out_features,
                 bias=True, p=0.5):
        super().__init__()
        self.linear = nn.Linear(in_features,
                               out_features,
                               bias)
        self.relu = nn.ReLU()
        self.drop = nn.Dropout(p)
        
    def forward(self, x):
        x = self.linear(x)
        x = self.relu(x)
        x = self.drop(x)
        return x

net = nn.Sequential(CustomLinear(12, 32),
                    nn.Linear(32, 1))

入力は 12次元 (targetを除いたもの), 出力は 1次元です。 のちに、BCEWithLogitsLoss を最小化するように計算して sigmoid 関数で 0,1 の2値分類を解く形になっています。 (誤りがあったら教えてくださいm( )m)

# Kfold のループ部分
for i, (train_idx, valid_idx) in enumerate(splits):

    # X_train, y_train, X_val, y_val をテンソル化(PyTorch で扱える形に変換)し、 .cuda() (GPUで計算するために特徴量を GPU に渡す処理)をする。
    x_train_fold = torch.tensor(X_train[train_idx], dtype=torch.float32).cuda()
    y_train_fold = torch.tensor(y_train[train_idx, np.newaxis], dtype=torch.float32).cuda()
    x_val_fold = torch.tensor(X_train[valid_idx], dtype=torch.float32).cuda()
    y_val_fold = torch.tensor(y_train[valid_idx, np.newaxis], dtype=torch.float32).cuda()
    
    # model を呼び出して、
    model = net
    # model も GPU に渡す。
    model.cuda()
    
    # loss 関数を呼び出す。BCELoss() よりも好まれるらしい。。
    loss_fn = torch.nn.BCEWithLogitsLoss(reduction="sum")
    optimizer = torch.optim.Adam(model.parameters())
    
    # dataloader で扱える形( = Dataset )にする
    train = torch.utils.data.TensorDataset(x_train_fold, y_train_fold)
    valid = torch.utils.data.TensorDataset(x_val_fold, y_val_fold)
    
    # x_train_fold batch_size個, y_train_fold batch_size個ずつを各ループで返す iterater の定義
    train_loader = torch.utils.data.DataLoader(train, batch_size=batch_size, shuffle=True)
    # x_valid_fold batch_size個, y_valid_fold batch_size個ずつを各ループで返す iterater の定義
    valid_loader = torch.utils.data.DataLoader(valid, batch_size=batch_size, shuffle=False)
    
    print(f'Fold {i + 1}')
    
    # epoch 分のループを回す
    for epoch in range(train_epochs):
        start_time = time.time()
        
        # model を train mode にする
        model.train()
        avg_loss = 0.

        # x_train_fold と y_train_fold を batch_size 個ずつ渡すループ
        for x_batch, y_batch in tqdm(train_loader, disable=True):
            # predict
            y_pred = model(x_batch)
            # loss の計算
            loss = loss_fn(y_pred, y_batch)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            avg_loss += loss.item() / len(train_loader)
        
        model.eval()
        valid_preds_fold = np.zeros((x_val_fold.size(0)))
        test_preds_fold = np.zeros(len(X_test))
        avg_val_loss = 0.
        for i, (x_batch, y_batch) in enumerate(valid_loader):
            y_pred = model(x_batch).detach()
            avg_val_loss += loss_fn(y_pred, y_batch).item() / len(valid_loader)
            valid_preds_fold[i * batch_size:(i+1) * batch_size] = sigmoid(y_pred.cpu().numpy())[:, 0]
        
        elapsed_time = time.time() - start_time 
        print('Epoch {}/{} \t loss={:.4f} \t val_loss={:.4f} \t time={:.2f}s'.format(
            epoch + 1, train_epochs, avg_loss, avg_val_loss, elapsed_time))

    # X_test_fold を batch_size ずつ渡すループ    
    for i, (x_batch,) in enumerate(test_loader):
        y_pred = model(x_batch).detach()

        # batch_size のリストのリストになっているのを単一階層のリストに変換して、cpuに値を渡し、テンソルから numpy.array()に変換したものを sigmoid 関数に渡す
        test_preds_fold[i * batch_size:(i+1) * batch_size] = sigmoid(y_pred.cpu().numpy())[:, 0]

    train_preds[valid_idx] = valid_preds_fold

    # 予測値の kfold数で割った値を加える
    test_preds += test_preds_fold / len(splits)

[15] 最後に予測値に対して閾値を決めて 予測値 を 0, 1 の形に変換する。

from sklearn.metrics import accuracy_score

def threshold_search(y_true, y_proba):
    best_threshold = 0
    best_score = 0
    for threshold in tqdm([i * 0.01 for i in range(100)]):
        score = accuracy_score(y_true=y_true, y_pred=y_proba > threshold)
        if score > best_score:
            best_threshold = threshold
            best_score = score
    search_result = {'threshold': best_threshold, 'accuracy_score': best_score}
    return search_result

この submission は、LB 0.66985 でした。

遠い昔、簡単に前処理と特徴量エンジニアリングして決定木にかけた時の最高スコアが、0.71770 だったので初めてにしては悪すぎずといったところでしょうか :)


4 : 感想

今回の目標である、テーブルデータに対して PyTorch の NN を適用することはできました。今後はこのコードを使いまわして、過去コンペの NN 縛り LateSubmission チャレンジをやってみようかなと思ってます。


5 : Todo

・出力1, BCELogitsLoss --> 出力2, CrossEntropyLoss でやってみる

・LGBM とアンサンブルしてどれくらいのスコアの上がり幅になるか見る。


以上です。ありがとうございました