9
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[chainer+optuna]ニューラルネットワークのハイパーパラメータを最適化して回帰モデルを実装

Last updated at Posted at 2019-02-04

1.はじめに

 初めまして。ijykと申します。
G検定2018#2に合格しましたが、実践的なことは何もできていないので、勉強もかねてpythonで色々と遊んでいます。

 ニューラルネットワーク(NN)を使った回帰(予測)した事例はいくつも見かけますが、
・chainerとoptunaを使った回帰モデルとハイパーパラメータのチューニング
・ハイパーパラメータ最適化後の回帰モデル実装、可視化までの流れ
はあまり見かけなかったので、今回、勉強も兼ねて実装したものをまとめました。
すみませんが、用語の説明などは省かせて頂きます。

2.やりたいこと

 1)ニューラルネットワークの回帰(予測)モデルを作りたい
 2)ハイパーパラメータを最適化したい
 3)最適化したハイパーパラメータで予測したい
 4)予測結果(精度とか誤差とか)を可視化したい

・・・てんこ盛り!

3.さあやってみよう

1)回帰(予測)モデルの作成

 今回は、シンプルにボストンの住宅価格のデータセットを使用します。
他のデータセットで実装したい場合は、X,Yのデータを変えれば使えます。

from sklearn.datasets import load_boston
from sklearn.preprocessing import StandardScaler #標準化に使います
import pandas as pd
import numpy as np

#boston住宅価格を読み込む
boston=load_boston()
X=boston['data']
Y=boston['target'].reshape(len(X),1)
x_col=boston['feature_names']

#データを標準化
x_scalar=StandardScaler()
X=x_scalar.fit_transform(X)
y_scalar=StandardScaler()
Y=y_scalar.fit_transform(Y)

#pandasに格納
X=pd.DataFrame(X)
Y=pd.DataFrame(Y)
Y.columns=['target']
X.columns=x_col
data_std=pd.concat([X,Y],axis=1)

#chainer用にfloat32型に変換
x=np.array(data_std[x_col]).astype('float32')
y=np.array(data_std['target']).astype('float32')
y_n=y.ndim

#目的変数の次元が1つだとreshapeが必要
y=y.reshape(len(y),y_n)

次に、chainerに放り込むデータセットをいじります。

import chainer
import chainer.functions as F
import chainer.links as L
from chainer import report 
from chainer.datasets import split_dataset_random

#chainerに用いるデータセットをタプルに格納
dataset=chainer.datasets.TupleDataset(x,y)

#教師データとテストデータに分割
n_train=int(len(dataset)*0.7)
n_train_examples = n_train
n_test_examples = len(dataset) - n_train

#データセットの作成
rng = np.random.RandomState(0)
train, test = split_dataset_random(dataset, n_train, seed=0)
train = chainer.datasets.SubDataset(
    train, 0, n_train_examples, order=rng.permutation(len(train)))
test = chainer.datasets.SubDataset(
    test, 0, n_test_examples, order=rng.permutation(len(test)))

#データセット内の説明変数と目的変数を分ける
X_train, Y_train = map(list, zip(*train))
X_test, Y_test = map(list, zip(*test))

chainerを使ってモデルを作っていきますが、scikit-learnのMLPRegressorとは違い、少しコードが複雑なので素人の私には理解するのに時間がかかりました。defは未だに分からん...
今回は、optunaを使ってハイパーパラメータを最適化するので、最適化したいパラメータは変数化します。
〇〇=trial.suggest_△△(◇◇) とすればOK。
最適化するパラメータは
 ・隠れ層の数
 ・各層のニューロン数
 ・バッチ数
 ・活性化関数
 ・エポック数
 ・最適化関数
 ・重み減衰
です。少なくとも上3つは寄与が大きいのでしたほうが良いそうです。とりあえず色々と変数化してみました。

#ニューラルネットワークの構造
def MLP(trial):
#活性化関数と隠れ層の数をoptuna用に変数化
    activation=trial.suggest_categorical('activation', ['relu', 'tanh','sigmoid'])
    n_fc_layers = trial.suggest_int('n_fc_layers', 1, 5)
    if activation=='relu':
        act=F.relu
    elif activation=='tanh':
        act=F.tanh
    else:
        act=F.sigmoid
#設定した隠れ層の数に基づきネットワークを作成
    layers = []
    for i in range(n_fc_layers):
#ニューロン数も変数化
        n_units = int(trial.suggest_int('n_units_l{}'.format(i), 4, 128))
        layers.append(L.Linear(None, n_units))
        layers.append(act)
    layers.append(L.Linear(None, y_n))#y_n・・・目的変数の次元
    return chainer.Sequential(*layers)

分類はL.Classifierですが、回帰は別の関数がありますのでそちらを使います。
さらに、最適化関数も選べるように設定します。

class MyRegressor(chainer.Chain):

    def __init__(self, predictor):
        super(MyRegressor, self).__init__(predictor=predictor)

    def __call__(self, x, y):
#予測するときは〇〇.predictor(x)使用
#平均絶対誤差(MAE)と、平均二乗誤差(MSE)を返す
        pred = self.predictor(x)
        abs_error = F.sum(abs(pred - y)) / len(x.data)
        loss = F.mean_squared_error(pred, y)

#平均絶対誤差(MAE)と平均二乗誤差(MSE)を記録
        report({'abs_error': abs_error, 'squared_error': loss}, self)
        return loss

#最適化関数を決める関数
def create_optimizer(trial, model):
# 最適化関数の選択
    optimizer_name = trial.suggest_categorical('optimizer', ['Adam', 'MomentumSGD'])
    if optimizer_name == 'Adam':
        adam_alpha = trial.suggest_loguniform('adam_alpha', 1e-5, 1e-1)
        optimizer = chainer.optimizers.Adam(alpha=adam_alpha)
    else:
        momentum_sgd_lr = trial.suggest_loguniform('momentum_sgd_lr', 1e-5, 1e-1)
        optimizer = chainer.optimizers.MomentumSGD(lr=momentum_sgd_lr)

    weight_decay = trial.suggest_loguniform('weight_decay', 1e-10, 1e-3)
    optimizer.setup(model)
    optimizer.add_hook(chainer.optimizer.WeightDecay(weight_decay))
    return optimizer

ハイパーパラメータを最適化するときはobjectiveという関数を作ります。基本的にはここに最適化したいパラメータや、出力する誤差、学習実行のコードを組み込んでおきます。

def objective(trial):
# モデルのインスタンス化
    model = MyRegressor(MLP(trial))
    optimizer = create_optimizer(trial, model) # モデルとoptimizerを紐付ける
    batchsize=trial.suggest_int('batchsize',10,len(y))
    epoch=trial.suggest_int('epoch',10,50)
# Iteratorの設定

    train_iter = chainer.iterators.SerialIterator(train, batchsize)
    test_iter = chainer.iterators.SerialIterator(test, batchsize, repeat=False, shuffle=False)
# Trainerの設定
    updater = chainer.training.StandardUpdater(train_iter, optimizer)
    trainer = chainer.training.Trainer(updater, (epoch, 'epoch'))
    trainer.extend(chainer.training.extensions.Evaluator(test_iter, model))
    log_report_extension = chainer.training.extensions.LogReport(log_name=None)
    trainer.extend(chainer.training.extensions.PrintReport(
        ['epoch', 'main/squared_error', 'validation/main/squared_error',
         'main/abs_error', 'validation/main/abs_error', 'elapsed_time']))
    trainer.extend(log_report_extension)

# 学習の実行
    trainer.run()

# 学習結果の保存
    log_last = log_report_extension.log[-1]
    for key, value in log_last.items():
        trial.set_user_attr(key, value)

# 最終的なバリデーションの値を返す
    val_err = log_report_extension.log[-1]['validation/main/squared_error']
    return val_err

2)ハイパーパラメータ最適化

ハイパーパラメータの最適化手法はグリッドサーチやランダムサーチなどがありますが、今回はoptunaというベイズ最適化ライブラリを使って最適化します。ベイズ最適化をすごくざっくり要約すると、誤差が小さくなりそうなパラメータを人のカンコツのように「こっちのほうが良くなりそうだ」といった挙動で探してくれる優れものです。厳密にいうと、確率論的にパラメータを選択していくわけですが、ここでは割愛します(詳しく知りたい方は最下部参考⑩などを読んでみてください)。
 ハイパーパラメータ最適化は学習を都度繰り返すので、途中で精度がサチュレートしたときに打ち切りたいときがあります。optunaではそれが可能なので必要に応じて使用します。

import optuna

#最適化
study = optuna.create_study()

#枝刈りを行う場合は以下のコメントアウトを外して使用
#study = optuna.create_study(pruner=optuna.pruners.MedianPruner(n_warmup_steps=5))

study.optimize(objective, n_trials=50)   #n_trials・・・探索回数

#最適化されたパラメータの出力
print('Number of finished trials: ', len(study.trials))

print('Best trial: ', )
trial = study.best_trial

#ハイパーパラメータの出力と同時に呼び出せるように格納
print('Params: ')
param_n=[]
param_v=[]
for key, value in trial.params.items():
    print('{}:{}'.format(key, value))
    param_n.append(key)
    param_v.append(value)
    
print('User attrs: ')
attrs_n=[]
attrs_v=[]
for key, value in trial.user_attrs.items():
    print('{}:{}'.format(key, value))
    attrs_n.append(key)
    attrs_v.append(value)

変数化したものはすべて出力されます。下は計算結果です。毎回結果が変わり、同じハイパーパラメータは得られないのであしからず。

Params 値 
activation relu
n_fc_layers 3
n_units_l0 57
n_units_l1 127
n_units_l2 54
optimizer Adam
adam_alpha 1.634e-2
weight_decay 9.576e-8
batchsize 52
epoch 39
User attrs 値 
main/abs_error 1.086e-1
main/squared_error 2.251e-2
validation/main/abs_error 1.907e-1
validation/main/squared_error 6.801e-2
epoch 39
iteration 266
elapsed_time 3.085

3)最適化したハイパーパラメータで予測

最適化したハイパーパラメータが得られたので、NNで予測してみます。sklearnとchainerで実装しました。

(1)sklearn(MLPRegressor)での実装

from sklearn.neural_network import MLPRegressor
from sklearn.model_selection import train_test_split

#NN回帰モデル
model=MLPRegressor()
#sklearnは隠れ層が複数の場合タプルにしないといけないので
#タプルに出力させる
n_units=()
for i in range(param_v[1]):
    n_units=n_units+(param_v[i+2],)#「,」がないとエラーが出る
#最適化関数別にパラメータを設定
if param_v[param_v[1]+2]=='Adam':
    model.set_params(activation=param_v[0],alpha=param_v[param_v[1]+3],
                     batch_size=int(param_v[param_v[1]+5]),hidden_layer_sizes=n_units,
                     max_iter=int(attrs_v[5]),solver='adam')
else:
    model.set_params(activation=param_v[0],learning_rate_init=param_v[param_v[1]+3],
                     batch_size=int(param_v[param_v[1]+5]),hidden_layer_sizes=n_units,
                     max_iter=int(attrs_v[5]),solver='sgd')
#モデルの学習
model.fit(X_train,Y_train)

#予測データ、元データの標準化を戻す。絶対誤差も計算。
Y_test_in=y_scalar.inverse_transform(Y_test).transpose()
X_test_pre=y_scalar.inverse_transform(model.predict(X_test))
error_test=Y_test_in-X_test_pre
Y_train_in=y_scalar.inverse_transform(Y_train).transpose()
X_train_pre=y_scalar.inverse_transform(model.predict(X_train))
error_train=Y_train_in-X_train_pre


#予測結果
print('予測精度(テスト):{}'.format(model.score(X_test,Y_test)))
print('予測精度(教師):{}'.format(model.score(X_train,Y_train)))

予測精度(テスト):0.864
予測精度(教師):0.977

まずまずといったところでしょうか。
過学習気味なので、エポック数などを調整するなどの工夫が必要そうです。

(2)chainerでの予測

#ハイパーパラメータを呼び出す
activation=param_v[0]
batchsize=int(param_v[param_v[1]+5])
epoch=int(attrs_v[4])
optimizer_name=param_v[param_v[1]+2]
weight_decay=param_v[param_v[1]+4]
n_fc_layers=param_v[1]

#最適化関数別にパラメータを設定
if param_v[param_v[1]+2]=='Adam':
    adam_alpha=param_v[param_v[1]+3]
else:
    momentum_sgd_lr=param_v[param_v[1]+3]

ここから先は最適化時のコードをいじっただけです。
詳細は省きます。

def MLP():
    if activation=='relu':
        act=F.relu
    elif activation=='tanh':
        act=F.tanh
    layers = []
    for i in range(n_fc_layers):
        n_units = param_v[i+2]
       #dropout_ratio= trial.suggest_loguniform('dropout_r{}'.format(i), 0.5, 1)
        layers.append(L.Linear(None, n_units))
        layers.append(act)
        #layers.append(F.dropout(act,ratio=dropout_ratio))
    layers.append(L.Linear(None, y_n))
    return chainer.Sequential(*layers)

class MyRegressor(chainer.Chain):

    def __init__(self, predictor):
        super(MyRegressor, self).__init__(predictor=predictor)

    def __call__(self, x, y):
        pred = self.predictor(x)
        abs_error = F.sum(abs(pred - y)) / len(x.data)
        loss = F.mean_squared_error(pred, y)
        report({'abs_error': abs_error, 'squared_error': loss}, self)
        return loss

def create_optimizer( model):
    if optimizer_name == 'Adam':
        optimizer = chainer.optimizers.Adam(alpha=adam_alpha)
    else:
        optimizer = chainer.optimizers.MomentumSGD(lr=momentum_sgd_lr)
    optimizer.setup(model)
    optimizer.add_hook(chainer.optimizer.WeightDecay(weight_decay))
    return optimizer

model = MyRegressor(MLP())
optimizer = create_optimizer(model) 
train_iter = chainer.iterators.SerialIterator(train, batchsize)
test_iter = chainer.iterators.SerialIterator(test, batchsize, repeat=False, shuffle=False)

updater = chainer.training.StandardUpdater(train_iter, optimizer)
trainer = chainer.training.Trainer(updater, (epoch, 'epoch'))
trainer.extend(chainer.training.extensions.Evaluator(test_iter, model))
log_report_extension = chainer.training.extensions.LogReport(log_name=None)
trainer.extend(chainer.training.extensions.PrintReport(
    ['epoch', 'main/squared_error', 'validation/main/squared_error',
    'main/abs_error', 'validation/main/abs_error', 'elapsed_time']))
trainer.extend(log_report_extension)

#学習の実行
trainer.run()

#予測データ、元データの標準化を戻します。絶対誤差も計算します。
#chainerの予測データはVariable形で出力されるので、「.array」でnumpyに変換しています
test_pre=y_scalar.inverse_transform(model.predictor(np.array(X_test).astype('float32')).array)
test_Y=y_scalar.inverse_transform(np.array(Y_test).astype('float32'))
train_pre=y_scalar.inverse_transform(model.predictor(np.array(X_train).astype('float32')).array)
train_Y=y_scalar.inverse_transform(np.array(Y_train).astype('float32'))
error_te=test_pre-test_Y
error_tr=train_pre-train_Y

#予測精度
test_sc=F.r2_score(test_pre,test_Y)
train_sc=F.r2_score(train_pre,train_Y)
print('予測精度(テスト):{}'.format(train_sc.array))
print('予測精度(教師):{}'.format(test_sc.array))

予測精度(テスト):0.955
予測精度(教師):0.955

chainerはかなり良い結果になりました。エポック数や重み減衰のパラメータが設定できる分精度がよくなっているのでしょうか。

4)予測結果の可視化

 matplotlibを使って可視化します。可視化するとどこが外れ値とか、本当に精度よく予測できてるかよくわかります。

(1)sklearn

#予測結果の可視化
import matplotlib.pyplot as plt
plt.scatter(Y_train_in,X_train_pre)
plt.scatter(Y_test_in,X_test_pre)
plt.xlabel("target")
plt.ylabel("predict")
plt.show()

#誤差の可視化
plt.scatter(Y_train_in,error_train)
plt.scatter(Y_test_in,error_test)
plt.ylim(-20,20)
plt.xlabel("target")
plt.ylabel("error")
plt.show()

青色が教師データ、オレンジ色がテストデータです。

元データvs予測データ

あ.png

元データvs絶対誤差

い.png

いくつか外れ値が見られます。右端の値については大きく外れていますね。やや過学習気味にも見えます。。。

(2)chainer

plt.scatter(train_Y,train_pre)
plt.scatter(test_Y,test_pre)
plt.xlabel("target")
plt.ylabel("predict")
plt.show()

plt.scatter(train_Y,error_tr)
plt.scatter(test_Y,error_te)
plt.ylim(-20,20)
plt.xlabel("target")
plt.ylabel("error")
plt.show()

同じく青色が教師データ、オレンジ色がテストデータです。

元データvs予測データ

image.png

元データvs絶対誤差

う.png
かなりリニアになりましたが右端のデータがばらついています。それでもまずまずの精度はでてそうです。

4.まとめ

 ディープラーニングを行う上では、一番基本となる教師あり学習について記載しました。ちなみに、データの前処理に標準化を行っていますが、標準化しなかった場合予測精度はR2=0.5以下とかなり悪かったです。今回は、ハイパーパラメータの最適化に注力しましたが、データセットの前処理(クレンジング)も必要不可欠ということですね。今後はそのあたりも含めて検討したいです。
 大真面目に色々書きましたが、データセットなどユニークなものでも検討してみたいですね。

以下は参考にさせて頂いたページとライブラリのURLです。

①chainerのTrainerやらUpdaterやらの仕組みを理解したかった
 https://qiita.com/sumsum88/items/a62b6950533e8dbb7e02
②ChainerとOptunaでハイパーパラメータ探索
 https://qiita.com/yoshiyoshi0505/items/2e39ffdab305dba95164
③chainer
 https://chainer.org/
④chainerのMyRegressorについて
 https://docs.chainer.org/en/stable/reference/util/generated/chainer.report.html
⑤optuna
 https://optuna.org/
⑥numpy
 http://www.numpy.org/
⑦pandas
 https://pandas.pydata.org/
⑧scikit-learn
 https://scikit-learn.org/stable/
⑨matplotlib
 https://matplotlib.org/
⑩ベイズ最適化入門
 https://qiita.com/masasora/items/cc2f10cb79f8c0a6bbaa

投稿 '19/2/5
改訂 '22/4/12

9
14
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?