はじめに

前回は、ニューラルネットワークの基礎と簡単にpytorchの使い方について紹介しました。(まだご覧でない方はこちら)
今回は、実際にテーブルデータを使って、ニューラルネットワークを学習したいと思います。
しかし、ここで、数値データ以外のデータ(性別、飛行機の便、etc)をどのようにして扱うかが問題になってきます。
よって、今回は、そのようなデータを「埋め込み(embedding)」を使うことによって処理する方法を紹介します。(例えば、他に有名な処理ですと、one-hotエンコーディングがあります。)

目標

  • カテゴリ変数を扱えるニューラルネットワークをpytorchで実装する
  • モデルの学習
    • Dataset, DataLoaderの作成

こちらのタイタニックのデータセットを使います。

※ 本ページの目標は、あくまで上記ですので、欠損値の補完や標準化については、一番簡単な処理を行います。

※ 数値データではないカラムをカテゴリ変数として処理しています。ご了承ください。

※ 詳しいコードは、こちらに記載しています。

ライブラリ

import random
import os
import pandas as pd
import numpy as np
from tqdm.notebook import tqdm
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from sklearn.preprocessing import StandardScaler
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

config

SEED = 0
TRAIN_FILE = './dataset/train.csv'
TEST_FILE = './dataset/test.csv'
SUB_FILE = './dataset/gender_submission.csv'
MODELS_DIR = "./models/"
CATEGORICAL = ['Sex', 'Cabin', 'Embarked']
NUMERICAL =  ['Pclass', 'Age', 'SibSp', 'Parch', 'Fare']
TARGET = 'Survived'
USE = CATEGORICAL + NUMERICAL
DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
EPOCHS = 300

データの確認

今回は以下のカラムを使用します。
['Sex', 'Cabin', 'Embarked', 'Pclass', 'Age', 'SibSp', 'Parch', 'Fare']

df_train = pd.read_csv(TRAIN_FILE)[USE+[TARGET]]
df_test = pd.read_csv(TEST_FILE)[USE]
df_train.head()
Sex Cabin Embarked Pclass Age SibSp Parch Fare Survived
0 male NaN S 3 22.0 1 0 7.2500 0
1 female C85 C 1 38.0 1 0 71.2833 1
2 female NaN S 3 26.0 0 0 7.9250 1
3 female C123 S 1 35.0 1 0 53.1000 1
4 male NaN S 3 35.0 0 0 8.0500 0

前処理

カテゴリとして扱う変数にcategoryと定義することが重要です。

# ラベルエンコーダ
for col in df.columns:
    if col in cat_cols:
        df[col] = LabelEncoder().fit_transform(df[col])
        df[col]= df[col].astype('category')

以下では、欠損値埋めと標準化、ラベルエンコードをしています。

※ 訓練データとテストデータをまとめて、標準化をしています。(訓練データのみでfitさせる方法もあります。)

def preprocessing(df_train, df_test, cat_cols=CATEGORICAL, num_cols=NUMERICAL, target=TARGET):
    df = pd.concat([df_train.drop(columns=target), df_test])
    y = df_train[target]
    train_len = len(df_train)
    # 欠損埋め
    df[cat_cols] = df[cat_cols].fillna('None')
    df[num_cols] = df[num_cols].fillna(0)
    # 標準化
    scaler = StandardScaler()
    scaler.fit(df[num_cols])
    df[num_cols] = scaler.transform(df[num_cols])
    # ラベルエンコーダ
    for col in df.columns:
        if col in cat_cols:
            df[col] = LabelEncoder().fit_transform(df[col])
            df[col]= df[col].astype('category')
    return pd.concat([df.iloc[:train_len], y], axis=1), df.iloc[train_len:]
df_train, df_test = preprocessing(df_train, df_test)
df_train
Sex Cabin Embarked Pclass Age SibSp Parch Fare Survived
0 1 185 3 0.841916 -0.106773 0.481288 -0.445000 -0.503023 0
1 0 106 0 -1.546098 0.803138 0.481288 -0.445000 0.734878 1
2 0 185 3 0.841916 0.120704 -0.479087 -0.445000 -0.489974 1
3 0 70 3 -1.546098 0.632530 0.481288 -0.445000 0.383356 1
4 1 185 3 0.841916 0.632530 -0.479087 -0.445000 -0.487558 0
... ... ... ... ... ... ... ... ... ...
886 1 185 3 -0.352091 0.177574 -0.479087 -0.445000 -0.391864 0
887 0 40 3 -1.546098 -0.277382 -0.479087 -0.445000 -0.063217 1
888 0 185 3 0.841916 -1.357902 0.481288 1.866526 -0.189843 0
889 1 77 0 -1.546098 0.120704 -0.479087 -0.445000 -0.063217 1
890 1 185 2 0.841916 0.461921 -0.479087 -0.445000 -0.493357 0

891 rows × 9 columns

各列の情報を確認してみます。

df_train.info()

Int64Index: 891 entries, 0 to 890
Data columns (total 9 columns):
 #   Column    Non-Null Count  Dtype
---  ------    --------------  -----
 0   Sex       891 non-null    category
 1   Cabin     891 non-null    category
 2   Embarked  891 non-null    category
 3   Pclass    891 non-null    float64
 4   Age       891 non-null    float64
 5   SibSp     891 non-null    float64
 6   Parch     891 non-null    float64
 7   Fare      891 non-null    float64
 8   Survived  891 non-null    int64
dtypes: category(3), float64(5), int64(1)
memory usage: 58.9 KB

学習データと検証データに分けます。今回は、簡単のためにhold-out形式です。

X_train, X_val, y_train, y_val = train_test_split(df_train.drop(columns=TARGET), df_train[TARGET], test_size=0.20, random_state=SEED, shuffle=True)
X_train.shape, X_val.shape, y_train.shape, y_val.shape
((712, 8), (179, 8), (712,), (179,))

カテゴリ変数を何次元に圧縮するかをemb_szsに設定しています。
例えば、果物というカラムに、「りんご」、「梨」、「ぶどう」が入っていたとすると、三種類ですので、3//2->1次元に圧縮します。
また、カラムのカテゴリ数が多い場合は、上限として50を設定しています。

cat_szs = [len(df_train[col].cat.categories) for col in CATEGORICAL]
emb_szs = [(size, min(50, (size+1)//2)) for size in cat_szs]
emb_szs
[(2, 1), (187, 50), (4, 2)]

Pytorch Dataset, DataLoaderの作成

DatasetとDataLoaderを作成します。
毎回のiterで、[カテゴリカラムのデータ, 数値カラムのデータ, 教師データ]が取り出されます。

class ClassificationColumnarDataset(Dataset):
    def __init__(self, df, target, cat_cols=CATEGORICAL,):
        self.df_cat = df[cat_cols]
        self.df_num = df.drop(cat_cols, axis=1)
        self.X_cats = self.df_cat.values.astype(np.int64)
        self.X_nums = self.df_num.values.astype(np.float32)
        self.target = target.values.astype(np.int64)
    def __len__(self):
        return len(self.target)
    def __getitem__(self, idx):
        return [self.X_cats[idx], self.X_nums[idx], self.target[idx]]
train_dataset = ClassificationColumnarDataset(X_train, y_train)
val_dataset = ClassificationColumnarDataset(X_val, y_val)
test_dataset = ClassificationColumnarDataset(df_test, pd.Series(np.zeros(len(df_test)).astype(np.int64)))
seed_set(SEED)
train_dataloader = DataLoader(train_dataset, batch_size=256, shuffle=True)
val_dataloader = DataLoader(val_dataset, batch_size=256, shuffle=False)
test_dataloader = DataLoader(test_dataset, batch_size=256, shuffle=False)

ニューラルネットワーク(カテゴリ変数の埋め込み)

モデルは以下のように実装します。カテゴリデータと数値データを別々に処理し、あとで結合させています。

class TabularModel(nn.Module):
    def __init__(self, embedding_sizes, n_num):
        super().__init__()
        self.embeddings = nn.ModuleList([nn.Embedding(categories, size) for categories, size in embedding_sizes])
        n_emb = sum(e.embedding_dim for e in self.embeddings)
        self.n_emb, self.n_num = n_emb, n_num
        self.lin1 = nn.Linear(self.n_emb + self.n_num, 100)
        self.lin2 = nn.Linear(100, 70)
        self.lin3 = nn.Linear(70, 2)
        self.bn1 = nn.BatchNorm1d(self.n_num)
        self.bn2 = nn.BatchNorm1d(100)
        self.bn3 = nn.BatchNorm1d(70)
        self.emb_drop = nn.Dropout(0.6)
        self.drops = nn.Dropout(0.3)
    def forward(self, x_cat, x_num):
        x = [e(x_cat[:, i]) for i, e in enumerate(self.embeddings)]
        x = torch.cat(x, dim=1)
        x = self.emb_drop(x)
        x2 = self.bn1(x_num)
        x = torch.cat([x, x2], dim=1)
        x = F.relu(self.lin1(x))
        x = self.drops(x)
        x = self.bn2(x)
        x = F.relu(self.lin2(x))
        x = self.drops(x)
        x = self.bn3(x)
        x = self.lin3(x)
        return x
model = TabularModel(emb_szs, len(NUMERICAL)).to(DEVICE)
compute_loss = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(),
                            lr=0.001,
                            betas=(0.9, 0.999),
                            amsgrad=True)

学習

学習のためには以下で十分ですが、より詳しいコードは、こちらをご覧ください。

for epoch in range(EPOCHS):
    # 学習
    model.train()
    for batch_idx, (cat_data, num_data, target) in enumerate(train_dataloader):
        cat_data, num_data, target = cat_data.to(DEVICE), num_data.to(DEVICE), target.to(DEVICE)
        optimizer.zero_grad()
        output = model(cat_data, num_data)
        loss = compute_loss(output, target)
        loss.backward()
        optimizer.step()

参考

以下では、より詳細な説明がありますので、興味のある方はご覧ください。