ニューラルネットワークの実装(回帰)

本章では、前章と同様に PyTorch Lightning を使用し、回帰を下記の流れで実装していきます。復習になりますが、分類はカテゴリを予測し、回帰では数値(連続値)を予測します。本章の問題設定では、家賃の中央値を予測するような問題になっています。

本章の構成

  • データセットの準備
  • PyTorch Lightning によるモデルと学習手順の定義
  • モデルの学習

データセットの準備

今回はこちらから housing.csv を使用して回帰の実装を練習してみましょう。どのような形式でデータを準備しておくと良いのかといった参考になるため、格納されているデータセットの中身も確認しておきます。

# ライブラリのインストール
!pip install pytorch_lightning
      
import numpy as np
import pandas as pd
      
from google.colab import files
uploaded = files.upload()
      
# データの読み込み(df: DataFrame)
df = pd.read_csv('housing.csv')
      
# データの表示
df.head()
      
x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12 x13 y
0 0.00632 18.0 2.31 0 0.538 6.575 65.2 4.0900 1 296 15.3 396.90 4.98 24.0
1 0.02731 0.0 7.07 0 0.469 6.421 78.9 4.9671 2 242 17.8 396.90 9.14 21.6
2 0.02729 0.0 7.07 0 0.469 7.185 61.1 4.9671 2 242 17.8 392.83 4.03 34.7
3 0.03237 0.0 2.18 0 0.458 6.998 45.8 6.0622 3 222 18.7 394.63 2.94 33.4
4 0.06905 0.0 2.18 0 0.458 7.147 54.2 6.0622 3 222 18.7 396.90 5.33 36.2
df.shape
      
(506, 14)

目的変数 y を除くと、x は 506 行 13 列であることが確認できます。これはサンプル数が 506、入力変数が 13 を意味しています。変数の説明は下記になりますので、気になる方はチェックしてください。

列名 説明
CRIM 人口 1 人当たりの犯罪発生数
ZN 25,000 平方フィート以上の住居区画の占める割合
INDUS 小売業以外の商業が占める面積の割合
CHAS チャールズ川によるダミー変数 (1: 川の周辺, 0: それ以外)
NOX NOx の濃度
RM 住居の平均部屋数
AGE 1940 年より前に建てられた物件の割合
DIS 5 つのボストン市の雇用施設からの距離 (重み付け済)
RAD 環状高速道路へのアクセスしやすさ
TAX $10,000 ドルあたりの不動産税率の総計
PTRATIO 町毎の児童と教師の比率
B 町毎の黒人 (Bk) の比率を次の式で表したもの。
LSTAT 給与の低い職業に従事する人口の割合 (%)

データの切り分け

t = df['y']
x = df.drop('y', axis=1)
      

こちらのように、正しく切り分けられているかデータの中身を表示して確認しておきましょう。

# 表示して確認
x.head()
      
x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12 x13
0 0.00632 18.0 2.31 0 0.538 6.575 65.2 4.0900 1 296 15.3 396.90 4.98
1 0.02731 0.0 7.07 0 0.469 6.421 78.9 4.9671 2 242 17.8 396.90 9.14
2 0.02729 0.0 7.07 0 0.469 7.185 61.1 4.9671 2 242 17.8 392.83 4.03
3 0.03237 0.0 2.18 0 0.458 6.998 45.8 6.0622 3 222 18.7 394.63 2.94
4 0.06905 0.0 2.18 0 0.458 7.147 54.2 6.0622 3 222 18.7 396.90 5.33
x.shape, t.shape
      
((506, 13), (506,))
type(x), type(t)
      
(pandas.core.frame.DataFrame, pandas.core.series.Series)

tensor に変換

import torch
      
# PyTorch で学習に使用できる形式へ変換
x = torch.tensor(x.values, dtype=torch.float32)
t = torch.tensor(t.values, dtype=torch.float32)
      

dataset にまとめる

TensorDataset を使用し、dataset にまとめましょう。

import torch.utils.data
      
# 入力変数と目的変数をまとめて、ひとつのオブジェクト dataset に変換
dataset = torch.utils.data.TensorDataset(x, t)
dataset
      
<torch.utils.data.dataset.TensorDataset at 0x128256b38>
# (入力変数, 目的変数) のようにタプルで格納されている
dataset[0]
      
(tensor([6.3200e-03, 1.8000e+01, 2.3100e+00, 0.0000e+00, 5.3800e-01, 6.5750e+00, 6.5200e+01, 4.0900e+00, 1.0000e+00, 2.9600e+02, 1.5300e+01, 3.9690e+02, 4.9800e+00]), tensor(24.))

学習用データ、検証用データ、テスト用データに分割

DataLoader の設定は PyTorch Lightning 側で用意されているので、必要ありませんでした。

# 各データセットのサンプル数を決定
# train : val : test = 60% : 20% : 20%
n_train = int(len(dataset) * 0.6)
n_val = int(len(dataset) * 0.2)
n_test = len(dataset) - n_train - n_val
      
# サンプル数の確認
n_train, n_val, n_test
      
(303, 101, 102)
# ランダムに分割を行うため、シードを固定して再現性を確保
torch.manual_seed(0)

# データセットの分割
train, val, test = torch.utils.data.random_split(dataset, [n_train, n_val, n_test])
      

PyTorch Lightning によるモデルと学習手順の定義

前章の最後に紹介した、学習データ、検証データ、テストデータのそれぞれに対する処理を TrainNetValidationNetTestNet のクラスにそれぞれ記述し、それらを継承した Net に変化のある部分を記述していく形式で定義していきます。

import torch.nn as nn
import torch.nn.functional as F
import pytorch_lightning as pl
      
# 学習データに対する処理
class TrainNet(pl.LightningModule):
    
    @pl.data_loader 
    def train_dataloader(self):
        return torch.utils.data.DataLoader(train, self.batch_size, shuffle=True)
    
    def training_step(self, batch, batch_nb):
        x, t = batch
        y = self.forward(x)
        loss = self.lossfun(y, t)
        results = {'loss': loss}
        return results
      
# 検証データに対する処理
class ValidationNet(pl.LightningModule):

    @pl.data_loader
    def val_dataloader(self):
        return torch.utils.data.DataLoader(val, self.batch_size)

    def validation_step(self, batch, batch_nb):
        x, t = batch
        y = self.forward(x)
        loss = self.lossfun(y, t)
        results = {'val_loss': loss}
        return results

    def validation_end(self, outputs):
        avg_loss = torch.stack([x['val_loss'] for x in outputs]).mean()
        results = {'val_loss': avg_loss}
        return results
      
# テストデータに対する処理
class TestNet(pl.LightningModule):

    @pl.data_loader
    def test_dataloader(self):
        return torch.utils.data.DataLoader(test, self.batch_size)

    def test_step(self, batch, batch_nb):
        x, t = batch
        y = self.forward(x)
        loss = self.lossfun(y, t)
        results = {'test_loss': loss}
        return results

    def test_end(self, outputs):
        avg_loss = torch.stack([x['test_loss'] for x in outputs]).mean()
        results = {'test_loss': avg_loss}
        return results
      

それでは、3 つのクラスを継承した Net クラスを記述していきます。前章の分類とは 1 点変更があり、目的関数 lossfunF.mse_loss になっています。回帰には平均ニ乗誤差(MSE:Mean Squared Error) を採用することが多く、以下のような式になっています。

L=1Nn=1N(tnyn)2\begin{array}{c} L = \frac{1}{N} \sum_{n=1}^{N} (t_n - y_n )^2 \end{array}

データ全体のサンプル数が NN で、目標値が tnt_n 、予測値が yny_n となっています。モデルの性能がどれだけ悪いかを定量評価してくれる指標であり、MSE を小さくすることを目指します。

# 学習データ、検証データ、テストデータへの処理を継承したクラス
class Net(TrainNet, ValidationNet, TestNet):
    
    def __init__(self, input_size=13, hidden_size=5, output_size=1, batch_size=10):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.fc2 = nn.Linear(hidden_size, output_size)
        self.batch_size = batch_size

    def forward(self, x):
        x = self.fc1(x)
        x = F.relu(x)
        x = self.fc2(x)
        return x
    
    # New: 平均ニ乗誤差
    def lossfun(self, y, t):
        return F.mse_loss(y, t)

    def configure_optimizers(self):
        return torch.optim.SGD(self.parameters(), lr=0.1)
      

モデルの学習

それでは、モデルの定義ができたので学習を実行していきましょう。

from pytorch_lightning import Trainer
      
# 再現性の確保
torch.manual_seed(0)

# インスタンス化
net = Net()
trainer = Trainer()

# 学習の実行
trainer.fit(net)
      

検証データとテストデータに対する精度の確認

trainer.test()
      
trainer.callback_metrics
      
{'loss': 225.140869140625, 'val_loss': 95.52346801757812, 'epoch': 999, 'test_loss': 79.75951385498047}
# 元のスケールに調整
torch.sqrt(torch.tensor(trainer.callback_metrics['test_loss']))
      
tensor(8.7748)

練習課題 本章のまとめ

上記の結果のように、平均二乗誤差がテストデータにおいて 79.76 となっており、オリジナルのスケールだと平均で 8.77 の誤差が生じていることになり、予測誤差が大きいことがわかります。今のモデルでは、まだまだ家賃予測の現場で使うことが難しいです。この原因を考え、対策をうち、平均二乗誤差を小さくできるようなモデルを考えてみましょう。

ヒント

  • Batch Normalization を入れてスケールを統一してみよう
  • さらに全結合層を追加し、モデルを複雑にしてみよう
  • エポック数を増やしてみよう
  • データを可視化して、関係性をイメージで捉えよう

ぜひ、取り組んでみてください。

shareアイコン