TensorFlow使ってみた~GoogleColab/Auto MPG~回帰問題の実装

Auto MPGデータセットを使用した回帰問題

www.tensorflow.org

チュートリアルより引用

回帰問題では、価格や確率といった連続的な値の出力を予測することが目的となります。
これは、分類問題の目的が、(例えば、写真にリンゴが写っているかオレンジが写っているかといった)離散的なラベルを予測することであるのとは対照的です。

MNIST手書き数字の認識ではインプットを0~9のクラスに分類したのに対して、予測した値を求めます。
Auto MPGモデルを使用した例では、車種の情報から、燃費性能を予測します。
AutoはAutoMobile、つまり自動車です。
MPGはMiles Per Gallon(1ガロンあたり何マイル走れるの?)です。
1マイルは約1.6km、1ガロンはイギリスでは約4.5リットル、アメリカでは約3.8リットルだそうです。
なんで国によって量が違うんですかね

実装

チュートリアルに従って実装を行っていきます。

インポートとか

# ペアプロットのためseabornを使用します
!pip install -q seaborn

from __future__ import absolute_import, division, print_function, unicode_literals

import pathlib

import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns

import tensorflow as tf

from tensorflow import keras
from tensorflow.keras import layers

print(tf.__version__)
2.2.0-rc4

データセット読み込み

Downloading data from https://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data
32768/30286 [================================] - 0s 4us/step

dataset.tail()
dataset.tail()

データクレンジング

# データクレンジング
# このデータには、いくつか欠損値があります。
dataset.isna().sum()
# この最初のチュートリアルでは簡単化のためこれらの行を削除します。
dataset = dataset.dropna()

# "Origin"の列は数値ではなくカテゴリーです。このため、ワンホットエンコーディングを行います。
origin = dataset.pop('Origin')

dataset['USA'] = (origin == 1)*1.0
dataset['Europe'] = (origin == 2)*1.0
dataset['Japan'] = (origin == 3)*1.0
dataset.tail()

dataset.tail()
dataset.tail()

データを訓練用セットとテスト用セットに分割

# データを訓練用セットとテスト用セットに分割
# データセットを訓練用セットとテスト用セットに分割しましょう。
# テスト用データセットは、作成したモデルの最終評価に使用します。
train_dataset = dataset.sample(frac=0.8,random_state=0)
test_dataset = dataset.drop(train_dataset.index)

データの観察

# データの観察
sns.pairplot(train_dataset[["MPG", "Cylinders", "Displacement", "Weight"]], diag_kind="kde")

# 全体の統計値
train_stats = train_dataset.describe()
train_stats.pop("MPG")
train_stats = train_stats.transpose()
train_stats

訓練用セットのいくつかの列の組み合わせの同時分布
訓練用セットのいくつかの列の組み合わせの同時分布

MPG:燃費(Mile/Gallon)
Cylinders:気筒数
Displacement:排気量
Weight:車体重量

全体の統計値
全体の統計値

ラベルと特徴量の分離

# ラベルと特徴量の分離
# ラベル、すなわち目的変数を特徴量から切り離しましょう。このラベルは、モデルに予測させたい数量です。
train_labels = train_dataset.pop('MPG')
test_labels = test_dataset.pop('MPG')

データの正規化

それぞれの特徴量の範囲がどれほど違っているかに注目してください。

スケールや値の範囲が異なる特徴量を正規化するのはよい習慣です。
特徴量の正規化なしでもモデルは収束するかもしれませんが、モデルの訓練はより難しいなり、結果として得られたモデルも入力で使われる単位に依存することになります。

def norm(x):
  return (x - train_stats['mean']) / train_stats['std']
normed_train_data = norm(train_dataset)
normed_test_data = norm(test_dataset)

モデルの構築

それではモデルを構築しましょう。 ここでは、2つの全結合の隠れ層と、1つの連続値を返す出力層からなる、Sequentialモデルを使います。

def build_model():
  model = keras.Sequential([
    layers.Dense(64, activation='relu', input_shape=[len(train_dataset.keys())]),
    layers.Dense(64, activation='relu'),
    layers.Dense(1)
  ])

  optimizer = tf.keras.optimizers.RMSprop(0.001)

  model.compile(loss='mse',
                optimizer=optimizer,
                metrics=['mae', 'mse'])
  return model

model = build_model()

モデルの検証

summaryメソッドを使って、モデルの簡単な説明を表示します。

model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                (None, 64)                640       
_________________________________________________________________
dense_1 (Dense)              (None, 64)                4160      
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 65        
=================================================================
Total params: 4,865
Trainable params: 4,865
Non-trainable params: 0
_________________________________________________________________

モデルを試す

モデルを試してみましょう。
訓練用データのうち10個のサンプルからなるバッチを取り出し、それを使ってメソッドを呼び出します。

example_batch = normed_train_data[:10]
example_result = model.predict(example_batch)
example_result
_________________________________________________________________
array([[ 0.04067548],
       [ 0.11909638],
       [ 0.04571575],
       [ 0.3901804 ],
       [-0.12568757],
       [ 0.23469958],
       [-0.10220642],
       [ 0.4401984 ],
       [ 0.12558347],
       [-0.3357187 ]], dtype=float32)

うまく動作しているようです。
予定どおりの型と形状の出力が得られています。

モデルの訓練

モデルを1000エポック訓練し、訓練と検証の正解率をhistoryオブジェクトに記録します。

# エポックが終わるごとにドットを一つ出力することで進捗を表示
class PrintDot(keras.callbacks.Callback):
  def on_epoch_end(self, epoch, logs):
    if epoch % 100 == 0: print('')
    print('.', end='')

EPOCHS = 1000

history = model.fit(
  normed_train_data, train_labels,
  epochs=EPOCHS, validation_split = 0.2, verbose=0,
  callbacks=[PrintDot()])
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................

モデルの訓練の様子を可視化

historyオブジェクトに保存された数値を使ってモデルの訓練の様子を可視化します。

hist = pd.DataFrame(history.history)
hist['epoch'] = history.epoch
hist.tail()

最後の5エポック
最後の5エポック

def plot_history(history):
  hist = pd.DataFrame(history.history)
  hist['epoch'] = history.epoch
  
  plt.figure()
  plt.xlabel('Epoch')
  plt.ylabel('Mean Abs Error [MPG]')
  plt.plot(hist['epoch'], hist['mae'],
           label='Train Error')
  plt.plot(hist['epoch'], hist['val_mae'],
           label = 'Val Error')
  plt.ylim([0,5])
  plt.legend()
  
  plt.figure()
  plt.xlabel('Epoch')
  plt.ylabel('Mean Square Error [$MPG^2$]')
  plt.plot(hist['epoch'], hist['mse'],
           label='Train Error')
  plt.plot(hist['epoch'], hist['val_mse'],
           label = 'Val Error')
  plt.ylim([0,20])
  plt.legend()
  plt.show()


plot_history(history)

平均絶対誤差(Mean Absolute Error)
平均絶対誤差(Mean Absolute Error)
平均二乗誤差(Mean Square Error)
平均二乗誤差(Mean Square Error)

このグラフを見ると、検証エラーは100エポックを過ぎたあたりで改善が見られなくなり、むしろ悪化しているようです。
検証スコアの改善が見られなくなったら自動的に訓練を停止するように、model.fitメソッド呼び出しを変更します。
ここでは、エポック毎に訓練状態をチェックするEarlyStoppingコールバックを使用します。
設定したエポック数の間に改善が見られない場合、訓練を自動的に停止します。

EarlyStoppingによる訓練自動停止

model = build_model()

# patience は改善が見られるかを監視するエポック数を表すパラメーター
early_stop = keras.callbacks.EarlyStopping(monitor='val_loss', patience=10)

history = model.fit(normed_train_data, train_labels, epochs=EPOCHS,
                    validation_split = 0.2, verbose=0, callbacks=[early_stop, PrintDot()])

plot_history(history)
...................................................................

平均絶対誤差2(Mean Absolute Error)
平均絶対誤差2(Mean Absolute Error)
平均二乗誤差2(Mean Square Error)
平均二乗誤差2(Mean Square Error)

検証用データセットでのグラフを見ると、平均誤差は+/- 2 MPG(マイル/ガロン)前後です。
これはよい精度でしょうか? その判断はおまかせします。

テストデータでの評価

モデルの訓練に使用していないテスト用データセットを使って、モデルがどれくらい汎化できているか見てみましょう。
これによって、モデルが実際の現場でどれくらい正確に予測できるかがわかります。

loss, mae, mse = model.evaluate(normed_test_data, test_labels, verbose=2)

print("Testing set Mean Abs Error: {:5.2f} MPG".format(mae))
3/3 - 0s - loss: 5.8512 - mae: 1.8385 - mse: 5.8512
Testing set Mean Abs Error:  1.84 MPG

モデルを使った予測

最後に、テストデータを使ってMPG値を予測します。

test_predictions = model.predict(normed_test_data).flatten()

plt.scatter(test_labels, test_predictions)
plt.xlabel('True Values [MPG]')
plt.ylabel('Predictions [MPG]')
plt.axis('equal')
plt.axis('square')
plt.xlim([0,plt.xlim()[1]])
plt.ylim([0,plt.ylim()[1]])
_ = plt.plot([-100, 100], [-100, 100])

予測値の散布図
予測値の散布図

そこそこよい予測ができているように見えます。誤差の分布を見てみましょう。

error = test_predictions - test_labels
plt.hist(error, bins = 25)
plt.xlabel("Prediction Error [MPG]")
_ = plt.ylabel("Count")

誤差の分布
誤差の分布

とても正規分布には見えませんが、サンプル数が非常に小さいからだと考えられます。

結論

このノートブックでは、回帰問題を扱うためのテクニックをいくつか紹介しました。
・平均二乗誤差(MSE:Mean Squared Error)は回帰問題に使われる一般的な損失関数です(分類問題には異なる損失関数が使われます)。
・同様に、回帰問題に使われる評価指標も分類問題とは異なります。回帰問題の一般的な評価指標は平均絶対誤差(MAE:Mean Absolute Error)です。
・入力数値特徴量の範囲が異なっている場合、特徴量ごとにおなじ範囲に正規化するべきです。
・訓練用データが多くない場合、過学習を避けるために少ない隠れ層をもつ小さいネットワークを使うというのがよい方策の1つです。
・Early Stoppingは過学習を防止するための便利な手法の一つです。

まとめ

というわけで回帰問題の実装をチュートリアルに従い行いました。
分類問題との大きな違いは、損失関数と最適化手法ですかね。
細かい点はもっとありますが・・・
いろいろなパターンを実装していきノウハウを積んでいきましょう。