はじめに
Kaggleを通してPyTorchを学んでいきます。
先にTensorflow/Kerasで勉強していたので、PyTorchとの違いについても意識しながら進んでいきます。
基本的にカーネルを読んで内容を理解しながら勉強していきます。
コンペ概要
タイタニックコンペは、Kaggleを始めて最初に取り組む最も基本的なコンペです。参加者も15,891 teamsと一番多いのではないでしょうか。
乗客の性別や年齢、客室のグレードや乗船港といった10個程度の特徴量から、その乗客が生存したかどうかを予測します。
データセットはtrain.csvによって与えられ、891行あります。
データセットの読み込み
まずはデータセットを読み込みます。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.data as tud
# -------- データセットの読み込み --------
train_data = pd.read_csv('./datasets/train.csv')
test_data = pd.read_csv('./datasets/test.csv')
test_data['Survived'] = np.nan
df = pd.concat([train_data, test_data], ignore_index=True, sort=False)
sns.barplot(x='Sex', y='Survived', data=df, palette='Set3')
plt.show()
print(df.info())
PandasでcsvファイルをDataFrameに読み込み、trainとtestを一緒に処理していくためにconcatで結合しています。ignore_indexは結合した後にindexを新しく振り直します。
また、df.info()でdfの欠損値やdtypeを確認し、seabornで性別と生存率の関係を可視化しています。
実行結果
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1309 entries, 0 to 417
Data columns (total 12 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 PassengerId 1309 non-null int64
1 Survived 891 non-null float64
2 Pclass 1309 non-null int64
3 Name 1309 non-null object
4 Sex 1309 non-null object
5 Age 1046 non-null float64
6 SibSp 1309 non-null int64
7 Parch 1309 non-null int64
8 Ticket 1309 non-null object
9 Fare 1308 non-null float64
10 Cabin 295 non-null object
11 Embarked 1307 non-null object
dtypes: float64(3), int64(4), object(5)
memory usage: 132.9+ KB
None
1. 特徴量エンジニアリング
特徴量エンジニアリングについては分量が多いのと、PyTorchの勉強という点ではあまり重要ではないため、説明を省きます。
特徴的なのは、Ageの欠損値を単に最頻値や平均値で埋めるのではなく、他の特徴量から年齢を推測して埋めています。
# -------- Age --------
from sklearn.ensemble import RandomForestRegressor
age_df = df[['Age', 'Pclass', 'Sex', 'Parch', 'SibSp']]
age_df = pd.get_dummies(age_df)
known_age = age_df[age_df.Age.notnull()].values
unknown_age = age_df[age_df.Age.isnull()].values
X = known_age[:, 1:]
y = known_age[:, 0]
rfr = RandomForestRegressor(n_estimators=100, n_jobs=-1)
rfr.fit(X, y)
predictedAge = rfr.predict(unknown_age[:, 1:])
df.loc[df['Age'].isnull(), 'Age'] = predictedAge
# -------- Name --------
df['Title'] = df['Name'].map(lambda x: x.split(', ')[1].split('. ')[0])
df['Title'].replace(['Capt', 'Col', 'Major', 'Dr', 'Rev'], 'Officer', inplace=True)
df['Title'].replace(['Don', 'Sir', 'the Countess', 'Lady', 'Dona'], 'Royalty', inplace=True)
df['Title'].replace(['Mme', 'Ms'], 'Mrs', inplace=True)
df['Title'].replace(['Mlle'], 'Miss', inplace=True)
df['Title'].replace(['Jonkheer'], 'Master', inplace=True)
sns.barplot(x='Title', y='Survived', data=df, palette='Set3')
# plt.show()
# ------------ Surname ------------
df['Surname'] = df['Name'].map(lambda name:name.split(',')[0].strip())
df['FamilyGroup'] = df['Surname'].map(df['Surname'].value_counts())
Female_Child_Group = df.loc[(df['FamilyGroup']>=2) & ((df['Age']<=16) | (df['Sex']=='female'))]
Female_Child_Group = Female_Child_Group.groupby('Surname')['Survived'].mean()
Male_Adult_Group = df.loc[(df['FamilyGroup']>=2) & ((df['Age']>16) & (df['Sex']=='male'))]
Male_Adult_Group = Male_Adult_Group.groupby('Surname')['Survived'].mean()
Dead_list = set(Female_Child_Group[Female_Child_Group.apply(lambda x:x==0)].index)
Survived_list = set(Male_Adult_Group[Male_Adult_Group.apply(lambda x:x==1)].index)
df.loc[(df['Survived'].isnull()) & (df['Surname'].apply(lambda x:x in Dead_list)),\
['Sex','Age','Title']] = ['male',28.0,'Mr']
df.loc[(df['Survived'].isnull()) & (df['Surname'].apply(lambda x:x in Survived_list)),\
['Sex','Age','Title']] = ['female',5.0,'Mrs']
# ------- Fare --------
fare = df.loc[(df['Embarked'] == 'S') & (df['Pclass'] == 3), 'Fare'].median()
df['Fare'] = df['Fare'].fillna(fare)
# -------- Family --------
df['Family'] = df['SibSp'] + df['Parch'] + 1
df.loc[(df['Family']>=2) & (df['Family']<=4), 'Family_label'] = 2
df.loc[(df['Family']>=5) & (df['Family']<=7) | (df['Family']==1), 'Family_label'] = 1
df.loc[(df['Family']>=8), 'Family_label'] = 0
sns.barplot(x='Family', y='Survived', data=df, palette='Set3')
# plt.show()
sns.barplot(x='Pclass', y='Survived', data=df, palette='Set3')
# plt.show()
# -------- Ticket --------
Ticket_Count = dict(df['Ticket'].value_counts())
df['TicketGroup'] = df['Ticket'].map(Ticket_Count)
df.loc[(df['TicketGroup']>=2) & (df['TicketGroup']<=4), 'Ticket_label'] = 2
df.loc[(df['TicketGroup']>=5) & (df['TicketGroup']<=8) | (df['TicketGroup']==1), 'Ticket_label'] = 1
df.loc[(df['TicketGroup']>=11), 'Ticket_label'] = 0
sns.barplot(x='Ticket_label', y='Survived', data=df, palette='Set3')
# plt.show()
# -------- Cabin --------
df['Cabin'] = df['Cabin'].fillna('Unknown')
df['Cabin_label'] = df['Cabin'].str.get(0)
sns.barplot(x='Cabin_label', y='Survived', data=df, palette='Set3')
#plt.show()
# -------- Embarked ---------
df['Embarked'] = df['Embarked'].fillna('S')
sns.barplot(x='Embarked', y='Survived', data=df, palette='Set3')
#plt.show()
2. データの前処理
dfをtrain, val, testに分割します。たくさんある特徴量の中から、学習に重要そうなものをピックアップしています。
# -------- 前処理 --------
df = df[['Survived', 'Pclass', 'Sex', 'Age', 'Fare', 'Embarked', 'Title', 'Family_label', 'Ticket_label', 'Cabin_label']]
df = pd.get_dummies(df)
train = df[df['Survived'].notnull()]
test = df[df['Survived'].isnull()].drop('Survived', axis=1)
X = train.values[:, 1:]
y = train.values[:, 0]
from sklearn.model_selection import train_test_split
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2)
test_X = test.values
get_dummies()はカテゴリ変数をダミー変数に変換する関数です。例えば 'Sex'コラムの要素にmaleとfemaleがあれば、新たに'Sex_male'と'Sex_female'の2つのコラムに置き換えて、要素を0と1で表します。
Sex | |
---|---|
1 | male |
2 | male |
3 | female |
↓ダミー化
Sex_male | Sex_female | |
---|---|---|
1 | 1 | 0 |
2 | 1 | 0 |
3 | 0 | 1 |
3. ミニバッチ学習の準備
やっとPyTorchの出番が来ました。
PyTorchではミニバッチ化する際に、model.fit(..., batch_size=64)のようなことはできません。そもそもfitがなく、for文のなかでlossやoptimizerをepoch回繰り返してあげる必要があります。
ミニバッチ化は、DataSetとDataLoaderという機能を使って実装します。
3.1 DataSet
DataSetクラスには、__init__()
, __len__()
, __getitem__()
が必要です。
__getitem__()
はデータを取り出すときに呼び出されます。indexを引数に持ち、指定されたindexを返します。
__len__()
は単にデータ数を返します。データ数分だけ__getitem__()
を繰り返します。
class DATASET(tud.Dataset):
def __init__(self, train, test):
self.X = torch.from_numpy(np.asarray(train)).float()
self.y= torch.from_numpy(np.asarray(test)).float()
def __getitem__(self, index):
return self.X[index], self.y[index]
def __len__(self):
return len(self.y)
中身自体は簡単です。
ここでnp.ndarray型のtrain, testをtorch.Tensor型のfloatに変換しています。(train, testは元々ndarray型なのでnp.asarray(train)をする必要はありません。)
dataset = DATASET(X_train, y_train)
print('全データ数:',len(dataset))
print('0番目のデータ:',dataset[0])
実行結果
全データ数: 712
0番目のデータ: (tensor([ 3.0000, 18.0000, 7.8542, 2.0000, 1.0000, 0.0000, 1.0000, 0.0000,
0.0000, 1.0000, 0.0000, 0.0000, 1.0000, 0.0000, 0.0000, 0.0000,
0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
1.0000]), tensor(0.))
0番目のtrainとtestがtupleで返されているのが確認できます。
3.2 DataLoader
DataLoaderには、import torch.utils.data.DataLoaderを使います。
DataSetとバッチサイズを引数にして、バッチサイズ分のデータを返します。
dataloader = tud.DataLoader(dataset, batch_size=5, shuffle=False, drop_last=False)
c = 0
for data in dataloader:
print(data)
c += 1
if c >= 3:
break
実行結果
[tensor([[ 1.0000, 48.0000, 26.5500, 1.0000, 1.0000, 0.0000, 1.0000, 0.0000,
0.0000, 1.0000, 0.0000, 0.0000, 1.0000, 0.0000, 0.0000, 0.0000,
0.0000, 0.0000, 0.0000, 0.0000, 1.0000, 0.0000, 0.0000, 0.0000,
0.0000],
[ 3.0000, 35.0000, 7.0500, 1.0000, 1.0000, 0.0000, 1.0000, 0.0000,
0.0000, 1.0000, 0.0000, 0.0000, 1.0000, 0.0000, 0.0000, 0.0000,
0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
1.0000],
[ 3.0000, 28.6062, 8.0500, 1.0000, 1.0000, 0.0000, 1.0000, 0.0000,
0.0000, 1.0000, 0.0000, 0.0000, 1.0000, 0.0000, 0.0000, 0.0000,
0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
1.0000],
[ 3.0000, 4.0000, 16.7000, 2.0000, 2.0000, 1.0000, 0.0000, 0.0000,
0.0000, 1.0000, 0.0000, 1.0000, 0.0000, 0.0000, 0.0000, 0.0000,
0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 1.0000, 0.0000,
0.0000]]), tensor([1., 0., 0., 1.])]
[tensor([[ 3.0000, 28.6062, 7.7500, 1.0000, 1.0000, 0.0000, 1.0000, 0.0000,
1.0000, 0.0000, 0.0000, 0.0000, 1.0000, 0.0000, 0.0000, 0.0000,
0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
1.0000],
[ 3.0000, 33.0000, 20.5250, 2.0000, 2.0000, 0.0000, 1.0000, 0.0000,
0.0000, 1.0000, 0.0000, 0.0000, 1.0000, 0.0000, 0.0000, 0.0000,
0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
1.0000],
[ 1.0000, 54.0000, 78.2667, 2.0000, 2.0000, 1.0000, 0.0000, 1.0000,
0.0000, 0.0000, 0.0000, 1.0000, 0.0000, 0.0000, 0.0000, 0.0000,
0.0000, 0.0000, 0.0000, 1.0000, 0.0000, 0.0000, 0.0000, 0.0000,
0.0000],
[ 3.0000, 45.0000, 27.9000, 1.0000, 1.0000, 1.0000, 0.0000, 0.0000,
0.0000, 1.0000, 0.0000, 0.0000, 0.0000, 1.0000, 0.0000, 0.0000,
0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
1.0000]]), tensor([0., 0., 1., 0.])]
[tensor([[ 3.0000, 28.6062, 7.2292, 1.0000, 1.0000, 0.0000, 1.0000, 1.0000,
0.0000, 0.0000, 0.0000, 0.0000, 1.0000, 0.0000, 0.0000, 0.0000,
0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
1.0000],
[ 2.0000, 7.0000, 26.2500, 2.0000, 2.0000, 1.0000, 0.0000, 0.0000,
0.0000, 1.0000, 0.0000, 1.0000, 0.0000, 0.0000, 0.0000, 0.0000,
0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
1.0000],
[ 2.0000, 18.0000, 11.5000, 1.0000, 1.0000, 0.0000, 1.0000, 0.0000,
0.0000, 1.0000, 0.0000, 0.0000, 1.0000, 0.0000, 0.0000, 0.0000,
0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
1.0000],
[ 3.0000, 22.0000, 8.0500, 1.0000, 1.0000, 0.0000, 1.0000, 0.0000,
0.0000, 1.0000, 0.0000, 0.0000, 1.0000, 0.0000, 0.0000, 0.0000,
0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
1.0000]]), tensor([0., 1., 0., 0.])]
バッチサイズ4でdataloaderからtrain, testのタプルが返されていることがわかります。
4. モデル構築
簡単なNNモデルを構築します。PyTorchのモデル構築では、__init__()
でレイヤーを初期化した後、forward()
で順伝播させます。
D_IN, H1, H2, H3, D_OUT = 25, 100, 100, 10, 1
DROP_PROB1, DROP_PROB2 = 0.5, 0.5
class Base_net(nn.Module):
def __init__(self):
super(Base_net, self).__init__()
self.linear1 = nn.Linear(D_IN, H1)
self.linear2 = nn.Linear(H1, H2)
self.linear3 = nn.Linear(H2, H3)
self.linear4 = nn.Linear(H3, D_OUT)
self.dp1 = nn.Dropout(DROP_PROB1)
self.dp2 = nn.Dropout(DROP_PROB2)
def forward(self, x):
x = F.relu(self.linear1(x))
x = F.leaky_relu(self.linear2(x))
x = self.dp1(x)
x = F.leaky_relu(self.linear3(x))
x = self.dp2(x)
x = F.leaky_relu(self.linear4(x))
x = torch.sigmoid(x)
return x
nn.Linear
は全結合層です。わかりにくいですが、D_IN (25)
は特徴量の個数でdataset[0][0].shape
です。Sequentialではなくクラスで構築することで、入力や出力が複数になったモデルも構築することができます。
super(Base_net, self).__init__()
で、継承したnn.Module
の初期化関数を起動しているらしいです。
5. モデルの学習
Tensorflowとは違い、PyTorchでは学習をforで回していく必要があります。
train_dataset = DATASET(X_train, y_train)
val_dataset = DATASET(X_val, y_val)
train_dataloador = tud.DataLoader(train_dataset, batch_size=64, shuffle=False, drop_last=False)
LEN_TRAIN = len(train_dataset.y)
LEN_VAL = len(y_val)
LEARNING_RATE = 1e-3
WEIGHT_DECAY = 2e-4
model = Base_net()
optimizer = torch.optim.Adam(model.parameters(), weight_decay=WEIGHT_DECAY, lr=LEARNING_RATE)
loss_func = nn.BCELoss()
trainとvalのデータセットを作り、trainはミニバッチ学習するためにDataLoaderも作成します。
LEN_TRAIN
とLEN_VAL
は正解率を求めるときに使います。
optimizerとlossを定義します。自作のものを定義する方法は今度記事にまとめて学習します。
for epoch in range(1000):
model.train()
for batch_x, batch_y in train_dataloador:
batch_y_pred = model(batch_x)
loss = loss_func(batch_y_pred, batch_y.unsqueeze(dim=1))
optimizer.zero_grad()
loss.backward()
optimizer.step()
model.eval()
#Train data acc
train_pred = model(train_dataset.X)
train_pred = train_pred.detach().apply_(lambda x : 1 if x > 0.6 else 0)
train_acc = np.sum(train_pred.numpy().flatten() == train_dataset.y.numpy().flatten())/ LEN_TRAIN
#Val data acc
val_pred = model(val_dataset.X)
val_pred = val_pred.detach().apply_(lambda x : 1 if x > 0.6 else 0)
val_acc = np.sum(val_pred.numpy().flatten() == val_dataset.y.numpy().flatten())/ LEN_VAL
if epoch % 20 == 0 :
print("epoch: {} | loss: {} | train acc: {} | answer acc: {}".format(epoch, loss.item(), train_acc, val_acc))
if val_acc > 0.81 :
print("epoch: {} | loss: {} | train acc: {} | answer acc: {}".format(epoch, loss.item(), train_acc, val_acc))
break
モデルを学習モードにし、DataLoaderを使ってミニバッチ分で学習します。
-
batch_y_pred = model(batch_x)
で順伝播させた出力から、 -
loss = loss_func(batch_y_pred, batch_y.unsqueeze(dim=1))
で損失を求めます。 -
optimizer.zero_grad()
は、以前のバッチで計算された勾配が蓄積されることを防ぐために勾配を0にセットします。 -
loss.backward()
を呼び出して、損失に対する勾配を計算します。 - 最後に
optimizer.step()
で計算された勾配を使用してパラメータを更新します。
これで学習が1エポック終わりました。
次にモデルを評価モードにし、評価を行っていきます。
-
train_pred = model(train_dataset.X)
で予測値を求めます。(train_pred.shape=(712, 1)) - 要素が0.6より大きい場合に生存(1)とします。
.detach()
は計算グラフから予測値を切り離すらしいですがよくわかりません。 - train_predをnumpy型にしてflattenで1次元ベクトル(712,)にします。ターゲットと一致した要素数を求め、
LEN_TRAIN
で割ることで正解率を求めることができます。valも同様です。
20epochごとに正解率を表示し、val_accが0.81を超えたら学習を終了させています。
6. 提出ファイルの作成
作成したモデルから、実際に提出するファイルを作ります。
test_X = torch.from_numpy(test_X.astype(np.float32))
prediction = model(test_X)
prediction = prediction.detach().apply_(lambda x : 1 if x > 0.6 else 0).flatten().int()
PassengerId = test_data['PassengerId'].values
submission = pd.DataFrame({"PassengerId": PassengerId, "Survived": prediction})
submission.to_csv('Submittion.csv', index=False)
7. あとがき
提出スコアは0.78468でした。XGBoostやLightGBMを使った場合でもスコアはあまり変わらなかったので、あとはアンサンブルすれば良いスコアが得られそうです。
今回の感想として、Tensorflow/Kerasと比べると学習と評価がかなり複雑になっていました。そのぶん、モデルがどのように学習しているかなど中身は見えやすくなっています。
今後学びたいことは、
- historyで中間結果を保存する方法
- lossの自作方法
- 画像データへの応用
です。
第十回目くらいでは終了済みでないコンペに参加していたいです。
参考
Qiita Markdown 書き方 まとめ
Kaggleのタイタニックに挑戦してみた(その1)
【Pytorch】ミニバッチ学習に便利なDataSet・DataLoaderの使い方