はじめに
前回は、ニューラルネットワークの基礎と簡単に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()
参考
以下では、より詳細な説明がありますので、興味のある方はご覧ください。