【Kaggle学習日記】第一回　Titanicコンペ　

# はじめに
Kaggleを通してPyTorchを学んでいきます。

先にTensorflow/Kerasで勉強していたので、PyTorchとの違いについても意識しながら進んでいきます。

基本的にカーネルを読んで内容を理解しながら勉強していきます。
# コンペ概要
タイタニックコンペは、Kaggleを始めて最初に取り組む最も基本的なコンペです。参加者も15,891 teamsと一番多いのではないでしょうか。

乗客の性別や年齢、客室のグレードや乗船港といった10個程度の特徴量から、その乗客が生存したかどうかを予測します。
データセットはtrain.csvによって与えられ、891行あります。

# データセットの読み込み
まずはデータセットを読み込みます。

```python
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
```

![Sex-Survived.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494482/ef8f352d-e399-3016-a5e9-b0c1248de0af.png)


# 1. 特徴量エンジニアリング

特徴量エンジニアリングについては分量が多いのと、PyTorchの勉強という点ではあまり重要ではないため、説明を省きます。

特徴的なのは、Ageの欠損値を単に最頻値や平均値で埋めるのではなく、他の特徴量から年齢を推測して埋めています。

```python
# -------- 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に分割します。たくさんある特徴量の中から、学習に重要そうなものをピックアップしています。

```python
# -------- 前処理 --------
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|

>・[Pandas の get_dummies() 関数でカテゴリ変数をダミー変数に変換する](https://kakakakakku.hatenablog.com/entry/2021/05/06/085943)

# 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__()`を繰り返します。

```python
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)をする必要はありません。)

```python
dataset = DATASET(X_train, y_train)

print('全データ数:',len(dataset))  
print('0番目のデータ:',dataset[0])
```
実行結果
```python
全データ数: 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とバッチサイズを引数にして、バッチサイズ分のデータを返します。

```python
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
```
実行結果
```python
[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()`で順伝播させます。

```python
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`の初期化関数を起動しているらしいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494482/d52ae039-ab31-ca87-5983-d753bdb4336e.png)


# 5. モデルの学習
Tensorflowとは違い、PyTorchでは学習をforで回していく必要があります。
```python
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を定義します。自作のものを定義する方法は今度記事にまとめて学習します。

```python
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を使ってミニバッチ分で学習します。

1. `batch_y_pred = model(batch_x)`で順伝播させた出力から、
2. `loss = loss_func(batch_y_pred, batch_y.unsqueeze(dim=1))`で損失を求めます。
3. `optimizer.zero_grad()`は、以前のバッチで計算された勾配が蓄積されることを防ぐために勾配を0にセットします。
4. `loss.backward() `を呼び出して、損失に対する勾配を計算します。
45. 最後に`optimizer.step()`で計算された勾配を使用してパラメータを更新します。
これで学習が1エポック終わりました。

次にモデルを評価モードにし、評価を行っていきます。

1. `train_pred = model(train_dataset.X)`で予測値を求めます。(train_pred.shape=(712, 1))
2. 要素が0.6より大きい場合に生存（1）とします。`.detach()`は計算グラフから予測値を切り離すらしいですがよくわかりません。
3. train_predをnumpy型にしてflattenで1次元ベクトル(712,)にします。ターゲットと一致した要素数を求め、`LEN_TRAIN`で割ることで正解率を求めることができます。valも同様です。

20epochごとに正解率を表示し、val_accが0.81を超えたら学習を終了させています。

# 6. 提出ファイルの作成
作成したモデルから、実際に提出するファイルを作ります。
```python
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 書き方 まとめ](https://qiita.com/shizuma/items/8616bbe3ebe8ab0b6ca1)
[Kaggleのタイタニックに挑戦してみた（その１）](https://qiita.com/pocokhc/items/f20b1518fb36f4ad0d00#%E3%81%82%E3%81%A8%E3%81%8C%E3%81%8D)
[【Pytorch】ミニバッチ学習に便利なDataSet・DataLoaderの使い方](https://gotutiyan.hatenablog.com/entry/2020/04/21/182937)