😎

[Kaggle]Spaceship titanicをNNで解く①

2023/04/27に公開

はじめに

こんにちは、あんどうです。Kaggleなどでテーブルデータのコンペでは一般にGBDT系のモデル(LightGBMとか)がよく使われますが、深層学習モデルではどのくらいの精度を出すことができるんだろう?と気になったので今回はシンプルなNNを用いてKaggleのSpaceship Titanic(初心者用の常設コンペ)を解いていこうと思います。

データ確認

今回使っていくデータを確認します。

BASE = "path_to_dir"

train = pd.read_csv(BASE+"train.csv")
test = pd.read_csv(BASE+"test.csv")
sub = pd.read_csv(BASE+"sample_submission.csv")

train.head(3)
PassengerId HomePlanet CryoSleep Cabin Destination Age VIP RoomService FoodCourt ShoppingMall Spa VRDeck Name Transported
0 0001_01 Europa False B/0/P TRAPPIST-1e 39 False 0 0 0 0 0 Maham Ofracculy False
1 0002_01 Earth False F/0/S TRAPPIST-1e 24 False 109 9 25 549 44 Juanna Vines True
2 0003_01 Europa False A/0/S TRAPPIST-1e 58 True 43 3576 0 6715 49 Altark Susent False
test.head(3)
PassengerId HomePlanet CryoSleep Cabin Destination Age VIP RoomService FoodCourt ShoppingMall Spa VRDeck Name
0 0013_01 Earth True G/3/S TRAPPIST-1e 27 False 0 0 0 0 0 Nelly Carsoning
1 0018_01 Earth False F/4/S TRAPPIST-1e 19 False 0 9 0 2823 0 Lerome Peckers
2 0019_01 Europa True C/0/S 55 Cancri e 31 False 0 0 0 0 0 Sabih Unhearfus

特徴量エンジニアリング

文字列特徴量の分割

Cabin列が分割できそうなので
B/0/P → B, 0, P
のように3つの列に分割します

#Cabinを3つに分解
def split_cabin(df):
    """
    キャビン(Cabin)列をスラッシュで分割し、3つの新しい列に分割します。
    新しい列の名前は Cabin_0、Cabin_1、および Cabin_2 です。
    
    Parameters
    ----------
    df : pandas.DataFrame
        入力となるデータフレーム。Cabin列を含む必要があります。
    
    Returns
    -------
    pandas.DataFrame
        スラッシュで分割されたキャビン情報を含む新しいデータフレーム。
        元のデータフレームから Cabin 列が削除されています。
    """
    cabins = df["Cabin"].str.split("/")
    df["Cabin_0"] = cabins.str.get(0)
    df["Cabin_1"] = cabins.str.get(1).astype(float)
    df["Cabin_2"] = cabins.str.get(2)
    df = df.drop(columns="Cabin")
    return df

train = split_cabin(train)
test = split_cabin(test)

訓練データと検証データの分割

trainデータをtrain:valid = 8:2で訓練データと検証データに分割します

train, valid = train_test_split(train, test_size=0.2, random_state=2000, shuffle=True)

欠損値の確認

train.isnull().sum()
PassengerId       0
HomePlanet      201
CryoSleep       217
Destination     182
Age             179
VIP             203
RoomService     181
FoodCourt       183
ShoppingMall    208
Spa             183
VRDeck          188
Name            200
Transported       0
Cabin_0         199
Cabin_1         199
Cabin_2         199
dtype: int64

ほとんどの列に欠損値が存在しているようです。NNでは欠損値が入ったまま学習することができないので、補完を行います。今回はカテゴリ変数を最頻値で、数値の変数を平均値で補完します。ここでのポイントはtrainの値を用いてvalid, testも補完するということです。validデータやtestデータで欠損値置換を行うとリークを起こします。

# 欠損値を最頻値で補完します。
# カテゴリ変数は最頻値で、数値の変数は平均値で補完します。

cat_col = train.select_dtypes("object").columns
num_col = train.select_dtypes(["float", "int"]).columns

train[cat_col] = train[cat_col].fillna(train[cat_col].mode().iloc[0, :])
valid[cat_col] = valid[cat_col].fillna(train[cat_col].mode().iloc[0, :])
test[cat_col] = test[cat_col].fillna(train[cat_col].mode().iloc[0, :])

train[num_col] = train[num_col].fillna(train[num_col].mean())
valid[num_col] = valid[num_col].fillna(train[num_col].mean())
test[num_col] = test[num_col].fillna(train[num_col].mean())


print("TRAIN")
print(train.isnull().sum())
print("\nVALID")
print(valid.isnull().sum())
print("\nTEST")
print(test.isnull().sum())
# 補完結果は省略します

ダミー変数化

最後にカテゴリ変数をダミー変数化して、すべてのカラムを数値列に変換します。

#最後にカテゴリ変数をダミー変数化して、全てのカラムを数値に変換します
train = pd.get_dummies(train.drop(columns=["Name", "PassengerId"])).astype("float32")
valid = pd.get_dummies(valid.drop(columns=["Name", "PassengerId"])).astype("float32")
test = pd.get_dummies(test.drop(columns=["Name", "PassengerId"])).astype("float32")
train.head(3)
CryoSleep Age VIP RoomService FoodCourt ShoppingMall Spa VRDeck Transported Cabin_1 HomePlanet_Earth HomePlanet_Europa HomePlanet_Mars Destination_55 Cancri e Destination_PSO J318.5-22 Destination_TRAPPIST-1e Cabin_0_A Cabin_0_B Cabin_0_C Cabin_0_D Cabin_0_E Cabin_0_F Cabin_0_G Cabin_0_T Cabin_2_P Cabin_2_S
1046 0 31 0 0 0 0 0 0 0 79 0 0 1 0 0 1 0 0 0 0 1 0 0 0 0 1
5245 1 3 0 0 0 0 0 0 1 904 1 0 0 0 0 1 0 0 0 0 0 0 1 0 0 1
7478 0 7 0 0 0 0 0 0 1 1659 0 0 1 0 0 1 0 0 0 0 0 1 0 0 1 0

Pytorch学習準備

今回はPytorchという深層学習ライブラリを使って学習を行っていきます。Pytorchで学習を行う際にはざっくり言うとDataset, DataLoader, Modelの定義が必要なので、定義していきます。
Pytorchが初めての方は、Pytorchチュートリアルこちらの本で学習することをお勧めします。

Dataset&DataLoaderの作成

class TitanicDataset(Dataset):
    def __init__(self, df, is_train=True):
        self.data = df
        self.is_train = is_train
        if self.is_train:
            self.targets = df["Transported"].astype(int)

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

    def __getitem__(self, idx):
        if self.is_train:
            features = self.data.drop(columns="Transported").iloc[idx, :].values
            target = self.targets[idx]
            return features, target
        else:
            features = self.data.iloc[idx, :].values
            return features, -1


train_dataset = TitanicDataset(train.reset_index(drop=True), is_train=True)
valid_dataset = TitanicDataset(valid.reset_index(drop=True), is_train=True)
test_dataset = TitanicDataset(test, is_train=False)

train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
valid_loader = DataLoader(valid_dataset, batch_size=128, shuffle=False)
test_loader = DataLoader(test_dataset, batch_size=128, shuffle=False)

モデルの定義①

今回はシンプルな2層のニューラルネットワークを構築してモデルを学習させます。

class TitanicModel(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim):
        super(TitanicModel, self).__init__()
        
        # 重み行列をランダムに初期化
        self.fc1 = nn.Linear(input_dim, hidden_dim, dtype=torch.float32)
        self.fc2 = nn.Linear(hidden_dim, output_dim, dtype=torch.float32)
        
    def forward(self, x):
        x = self.fc1(x)
        x = nn.ReLU()(x)
        x = self.fc2(x)
        return x

Lets学習!!

ようやく学習していきます。ハイパーパラメータは適当に決めました。

# 評価結果のリストを準備
train_acc_l = []
val_acc_l = []

# ハイパーパラメータの定義
input_dim = 25 # 入力の次元
hidden_dim = 52 # 隠れ層の次元
output_dim = 2 # 出力の次元
learning_rate = 0.001 # 学習率
num_epochs = 10 # エポック数

# モデルの定義
model = TitanicModel(input_dim, hidden_dim, output_dim)

# 損失関数の定義
criterion = nn.CrossEntropyLoss()

# 最適化関数の定義
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

# トレーニングループ
for epoch in range(num_epochs):
    correct = 0
    total = 0

    # モデルをトレーニングモードに設定
    model.train()

    # データをイテレート
    for i, (inputs, labels) in enumerate(train_loader):
        # 入力をモデルに渡して予測を取得
        outputs = model(inputs)

        # 損失を計算
        loss = criterion(outputs, labels)

        # 勾配を初期化して逆伝播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        # 評価指標の計算
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
    
    train_acc = 100 * correct / total
    train_acc_l.append(train_acc)

    # 1エポックごとに損失を表示
    print(f"{w_}Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}")

    # モデルを評価モードに設定
    model.eval()

    # テストデータで評価
    with torch.no_grad():
        correct = 0
        total = 0
        for inputs, labels in valid_loader:
            outputs = model(inputs)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()

        val_acc = 100 * correct / total
        val_acc_l.append(val_acc)

        print(f"{b_}Train Accuracy: {train_acc:.2f}% {r_}Valid Accuracy: {val_acc:.2f}%")

学習ログ↓

trainとvalidでのAccuracyの推移↓

少しグラフがぐちゃってて不安定になってる?気がします。特に検証データでの評価も不安定です。

バッチ正規化層の追加

学習を安定させるべく、バッチ正規化(Batch Normalization)を行う層を追加していきます。バッチ正規化の仕組みについてはこちらがわかりやすいです。

モデル構築②

バッチ正規化層を追加したモデルを定義します。

class TitanicModel(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim):
        super(TitanicModel, self).__init__()
        
        # 重み行列をランダムに初期化
        self.fc1 = nn.Linear(input_dim, hidden_dim, dtype=torch.float32)
        self.bn1 = nn.BatchNorm1d(hidden_dim) # Batch Normalizationの層を追加
        self.fc2 = nn.Linear(hidden_dim, output_dim, dtype=torch.float32)
        
    def forward(self, x):
        x = self.fc1(x)
        x = self.bn1(x)
        x = nn.ReLU()(x)
        x = self.fc2(x)
        return x

Lets学習(2回目)!!

再度学習していきます。学習コードは同じなので割愛します。
学習ログ↓

trainとvalidでのAccuracyの推移↓

1回目に比べて、かなり安定して学習できてそうです。

Kaggleに提出

せっかくなので、Kaggleにも提出してスコアを見てみたいと思います。

提出ファイルの作成

pred_l = []

with torch.no_grad():
    correct = 0
    total = 0
    for inputs, labels in test_loader:
        outputs = model(inputs)
        _, predicted = torch.max(outputs.data, 1)
        pred_l.append(predicted.tolist())
	
sub["Transported"] = sum(pred_l, [])
sub["Transported"] = sub["Transported"].astype(bool)

sub.to_csv("sub001.csv", index=False)

提出結果


現時点(2023/04/27)で959位/2608チームでした。ハイパラ調整や交差検証なしでざっくり上位40%に入っていると考えると、NNでもそこそこいい精度が出ることがわかりました。

改善点

今回行った学習には、以下のようにいくつか改善点があるので、時間があったらまたブログを書きたいと思います。ご覧くださりありがとうございました。
・ハイパラ調整
・交差検証
・特徴量作り
・NNの構造探索
・early stoppingの実施

Discussion