Kaggle House Price 深層学習 実践

# この本について
**本書では、Pytorchを使って、KaggleのHouse Priceを解いていきます。**
**最終的な精度は0.18367です。**

この本を読むことによって
**データの正規化**
**回帰の仕方**
**早期終了**
**深層学習での回帰の仕方**
が学べます。

# 実行環境
OS:macOS 10.15.7
開発環境:Nova(Python対応テキストエディタならOK)
パッケージのinstall:Anaconda 4.9.2
Python:3.8.3

# 必要なパッケージ
**本書では、以下のライブラリがインストールされているとして話を進めていきます。**

```python
torch==1.6.0
numpy==1.19.2
pandas==1.1.5
scikit_learn==0.24.1
```

# お問い合わせ
内容に関する質問は

mailto:deepblack.inc@gmail.com

までお願いします。

# データの読み込み
まずは、Pandasを用いて、タイタニックのCSVを読み込みます。

```Python
import pandas as pd

data = pd.read_csv("data/train.csv")
```

# 用いる特徴量を決める、教師を確定する
次に、用いる特徴量と教師を確定します。
今回は、数値が最初から入っている、特徴量を用いました。
他の特徴量を用いて見たい場合は、この本が終わった後に自分で試してみてください。

```Python
X = data[["MSSubClass", "LotFrontage", "LotArea", "OverallQual", "OverallCond", "YearBuilt", "YearRemodAdd", "MasVnrArea", "BsmtFinSF1", "BsmtFinSF2", "BsmtUnfSF", "TotalBsmtSF", "1stFlrSF", "2ndFlrSF", "LowQualFinSF", "GrLivArea", "BsmtFullBath", "BsmtHalfBath", "FullBath", "HalfBath", "BedroomAbvGr", "KitchenAbvGr", "TotRmsAbvGrd", "Fireplaces", "GarageYrBlt", "GarageCars", "GarageArea", "WoodDeckSF", "OpenPorchSF", "EnclosedPorch", "3SsnPorch", "ScreenPorch", "PoolArea", "MiscVal", "MoSold", "YrSold"]].fillna(data.mean()).values
y = data["SalePrice"].values
```

# MinMaxScalerによる正規化とは
以下の式による 0 ~ 1 の範囲への変換

# scikit-learnによる正規化
以下の式で簡単に正規化が可能となる。
実に、簡単だ。

```Python
# データの正規化
mm = preprocessing.MinMaxScaler()
X = mm.fit_transform(X)
```

# 訓練とEarlyStoppng用のデータに分ける

```Python
from sklearn.model_selection import train_test_split

X_train, X_valid, y_train, y_valid = train_test_split(X, target, test_size=0.33, random_state=42)
```

# モデル
Deep Learningのモデルは以下のように書けます。
自分自身で、層を増やしたり、減らしたりして試してみてください。

```Python
import torch
from torch import nn
import torch.nn.functional as F

class Model(nn.Module):
	def __init__(self):
		super(Model, self).__init__()

		self.linear1 = nn.Linear(36, 10)
		self.linear2 = nn.Linear(10, 10)
		self.linear3 = nn.Linear(10, 1)

	def forward(self, x):
		batch_size = x.shape[0]

		x = self.linear1(x)
		x = F.relu(x)
		x = self.linear2(x)
		x = F.relu(x)
		out = self.linear3(x)
		
		return out
```

# PytorchのDatasetとDataLoader

この章では、PytorchのDatasetとDataLoaderについて解説していきます。
この章は**https://gotutiyan.hatenablog.com/entry/2020/04/21/182937** を参考に記述されています。
Pytorchでは、DatasetとDataLoaderを用いることで、簡単にミニバッチ化をすることができます。

# Datasetの実装
DataSetを実装する際には、クラスのメンバ関数として__len__()と__getitem__()を必ず作ります。

__len__()は、len()を使ったときに呼ばれる関数です。
__getitem__()は、array[i]のように[ ]を使って要素を参照するときに呼ばれる関数です。これが呼ばれる際には、必ず何かしらのindexが指定されているので、引数にindexの情報を取ります。また、入出力のペアを返すように設計します。

以上を踏まえて、Datasetを作成してみましょう。

```Python
class DataSet:
    def __init__(self):
        self.X = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] # 入力
        self.t = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1] # 出力

    def __len__(self):
        return len(self.X) # データ数(10)を返す

    def __getitem__(self, index):
        # index番目の入出力ペアを返す
        return self.X[index], self.t[index]
```
さて、実際にこのDataSetがどのような振る舞いをするか試してみましょう。

```Python
dataset = DataSet()
print('全データ数:',len(dataset))  # 全データ数: 10
print('3番目のデータ:',dataset[3]) # 3番目のデータ: (3, 1)
print('5~6番目のデータ:',dataset[5:7]) # 5~6番目のデータ: ([5, 6], [1, 0])
```

# DataLoaderの実装
バッチサイズを2、訓練時のデータのシャッフルをFalseとした実装は以下のようになります。

```Python
# さっき作ったDataSetクラスのインスタンスを作成
dataset = DataSet()
# datasetをDataLoaderの引数とすることでミニバッチを作成．
dataloader = torch.utils.data.DataLoader(dataset, batch_size=2, shuffle=False)
```
これでミニバッチ学習をする準備が整いました。
ミニバッチ用のデータはfor文で取り出すことができます。

```Python
for data in dataloader:
    print(data)

'''
出力:
[tensor([0, 1]), tensor([0, 1])]
[tensor([2, 3]), tensor([0, 1])]
[tensor([4, 5]), tensor([0, 1])]
[tensor([6, 7]), tensor([0, 1])]
[tensor([8, 9]), tensor([0, 1])]
'''
```

上記のdataloaderを用いて、10epoch学習をする場合には以下のように書けます。

```Python
epoch = 10
model = #何かしらのモデル
for _ in range(epoch):
    for data in dataloader:
        X = data[0]
        t = data[1]
        y = model(X)
        # lossの計算とか
```

PytorchのDatasetとDataloaderについての説明は、以上になります。

# House PriceのDataset

```Python
import torch

class DataSet(torch.utils.data.Dataset):
	def __init__(self, data, reg):

		self.data = data
		self.reg = reg
		self.length = len(data)

	def __len__(self):

		return self.length

	def __getitem__(self, index):

		data = self.data[index]
		reg = self.reg[index]

		return data, reg
```

# Early Stopping

この章は**深層学習 (アスキードワンゴ)** と **https://github.com/Bjarten/early-stopping-pytorch** を参考に書かれています。

十分な表現要領を持つ大きなモデルを訓練して、あるタスクに対して学習するとき訓練誤差は減少するが、検証誤差が再び増加し始めることがあります。
そこで、検証誤差が改善されるたびに、モデルを保存することにします。
一定のエポック数検証誤差が改善されない場合、学習は終了します。

![](https://storage.googleapis.com/zenn-user-upload/lldiobqg2j14tyozrsixdpyics2v)


コードは以下のようになります。

以下のコードをearlystopping.pyとして保存してください。

```Python
import numpy as np
import torch

class EarlyStopping:

    def __init__(self, patience=7, verbose=False):
        
        self.patience = patience
        self.verbose = verbose
        self.counter = 0
        self.best_score = None
        self.early_stop = False
        self.val_loss_min = np.Inf
        self.force_cancel = False

    def __call__(self, val_loss, model):
        
        score = -val_loss

        if self.best_score is None:
            self.best_score = score
            self.save_checkpoint(val_loss, model)
        elif score < self.best_score:
            self.counter += 1
            print(
                f'EarlyStopping counter: {self.counter} out of {self.patience}')
            if self.counter >= self.patience:
                self.early_stop = True
        else:
            self.best_score = score
            self.save_checkpoint(val_loss, model)
            self.counter = 0

    def save_checkpoint(self, val_loss, model):
        
        if self.verbose:
            print(
                f'Validation loss decreased ({self.val_loss_min:.6f} --> {val_loss:.6f}).  Saving model ...')
        torch.save(model.state_dict(), 'models/model.pth')
        self.val_loss_min = val_loss
```

# Deep Learningモデルの訓練

Deep Learningの訓練は以下のようになります。

```Python
for epoch in range(EPOCHS):

	train_loss = 0
	total = 0
	model.train()
	
	for data in trainloader:

		optimizer.zero_grad()

		output = model(data[0].float())

		target = data[1].float()

		loss = criterion(output.squeeze(), target)

		train_loss += loss.item()

		total += data[1].size(0)

		loss.backward()
		optimizer.step()

	train_loss = train_loss / total
	
	model.eval()
	vali_total = 0
	vali_loss = 0

	for data in validloader:

		with torch.no_grad():
			out = model.forward(data[0].float())

		target = data[1].float()

		loss = criterion(out.squeeze(), data[1])

		vali_loss += loss.item()
		vali_total += data[1].size(0)

	vali_loss = vali_loss / vali_total
	
	earlystopping(vali_loss, model)
	if earlystopping.early_stop:
		print("Early stopping")
		break

```

# 訓練のCode

```Python
import pandas as pd
from sklearn.preprocessing import MinMaxScaler
import pickle
import torch
from torch import nn
from sklearn import preprocessing
from sklearn.model_selection import train_test_split

from model import Model
from earlystopping import EarlyStopping
from data_loader import *

# HyperParameter
LEARNING_RATE = 0.0001
TRAIN_BATCH_SIZE = 10
VALID_BATCH_SIZE = 5
EPOCHS = 1000000
PATIENCE = 100

data = pd.read_csv("data/train.csv")

X = data[["MSSubClass", "LotFrontage", "LotArea", "OverallQual", "OverallCond", "YearBuilt", "YearRemodAdd", "MasVnrArea", "BsmtFinSF1", "BsmtFinSF2", "BsmtUnfSF", "TotalBsmtSF", "1stFlrSF", "2ndFlrSF", "LowQualFinSF", "GrLivArea", "BsmtFullBath", "BsmtHalfBath", "FullBath", "HalfBath", "BedroomAbvGr", "KitchenAbvGr", "TotRmsAbvGrd", "Fireplaces", "GarageYrBlt", "GarageCars", "GarageArea", "WoodDeckSF", "OpenPorchSF", "EnclosedPorch", "3SsnPorch", "ScreenPorch", "PoolArea", "MiscVal", "MoSold", "YrSold"]].fillna(data.mean()).values
target = data["SalePrice"].values

scaler = MinMaxScaler()

X = scaler.fit_transform(X)

X_train, X_valid, y_train, y_valid = train_test_split(X, target, test_size=0.33, random_state=42)

model = Model()

earlystopping = EarlyStopping(PATIENCE)

criterion = nn.MSELoss()

optimizer = torch.optim.AdamW(model.parameters(), lr=LEARNING_RATE)

trainset = DataSet(X_train, y_train)
trainloader = torch.utils.data.DataLoader(
	trainset, batch_size=TRAIN_BATCH_SIZE, shuffle=True)
validset = DataSet(X_valid, y_valid)
validloader = torch.utils.data.DataLoader(
	validset, batch_size=VALID_BATCH_SIZE, shuffle=True)

for epoch in range(EPOCHS):

	train_loss = 0
	total = 0
	model.train()
	
	for data in trainloader:

		optimizer.zero_grad()

		output = model(data[0].float())

		target = data[1].float()

		loss = criterion(output.squeeze(), target)

		train_loss += loss.item()

		total += data[1].size(0)

		loss.backward()
		optimizer.step()

	train_loss = train_loss / total
	
	model.eval()
	vali_total = 0
	vali_loss = 0

	for data in validloader:

		with torch.no_grad():
			out = model.forward(data[0].float())

		target = data[1].float()

		loss = criterion(out.squeeze(), data[1])

		vali_loss += loss.item()
		vali_total += data[1].size(0)

	vali_loss = vali_loss / vali_total
	
	earlystopping(vali_loss, model)
	if earlystopping.early_stop:
		print("Early stopping")
		break

with open('models/scaler.pkl', 'wb') as f:
	pickle.dump(scaler, f)
```

# テストのCode

```Python
import pandas as pd
from sklearn.preprocessing import MinMaxScaler
import pickle
import torch
from torch import nn
from sklearn import preprocessing

from model import Model
from data_loader import *

with open('models/scaler.pkl', 'rb') as f:
	scaler = pickle.load(f)

data = pd.read_csv("data/test.csv")

X = data[["MSSubClass", "LotFrontage", "LotArea", "OverallQual", "OverallCond", "YearBuilt", "YearRemodAdd", "MasVnrArea", "BsmtFinSF1", "BsmtFinSF2", "BsmtUnfSF", "TotalBsmtSF", "1stFlrSF", "2ndFlrSF", "LowQualFinSF", "GrLivArea", "BsmtFullBath", "BsmtHalfBath", "FullBath", "HalfBath", "BedroomAbvGr", "KitchenAbvGr", "TotRmsAbvGrd", "Fireplaces", "GarageYrBlt", "GarageCars", "GarageArea", "WoodDeckSF", "OpenPorchSF", "EnclosedPorch", "3SsnPorch", "ScreenPorch", "PoolArea", "MiscVal", "MoSold", "YrSold"]].fillna(data.mean()).values

ids_lst = list(data["Id"].values)

X = scaler.transform(X)

model = Model()
model.load_state_dict(torch.load("models/model.pth"))

res = model(torch.tensor(X).float()).squeeze().detach().numpy()

for i in range(0, len(ids_lst)):
	
	ids_lst[i] = int(ids_lst[i])

sub_df = pd.DataFrame([ids_lst, res]).T

sub_df = sub_df.rename(columns={0:"Id", 1:"SalePrice"})

sub_df["Id"] = sub_df["Id"].round().astype(int)

sub_df.to_csv("sub.csv", index=False)
```