[Kaggle]Spaceship titanicをNNで解く①
はじめに
こんにちは、あんどうです。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