アメダス気象データ分析チャレンジ!(Python版)の2日目にようこそ.ここでは,実践編(予測的分析)に入る前に,教師あり学習の一種であるニューラルネットワークの基本について理解しましょう.Jupyter Notebookを使って,ニューラルネットワークによる機械学習のための基礎知識を習得しましょう.Pythonの標準ライブラリscikit-learn(sklearn)
を用いることで簡単にニューラルネットワークを実装することができます.
今回利用する ニューラルネットワーク は,教師あり学習 の一種です.教師あり学習では,教師データ(正解データ) による学習(訓練)が必要となります.例えば,スパムメールフィルタでは,事前にそのメールであるかどうかの正解を教えてあげる必要があります.教師あり学習には 「分類(classification)問題」 と 「回帰(regression)問題」 があります.「分類問題」とは,例えば「気温(24℃)」という入力データから「アイスが売れる(1)」か「アイスが売れない(0)」といった2クラスや「今日売れそうな商品(ビール,コーラ,コーヒー,お茶・・・)」といった多クラスを分類するような問題を指します.また,「回帰問題」とは,例えば「気温(24℃)」という入力データから「アイスが何個売れる(100個)」という出力値を予測するような問題を指します.
前回の診断的分析の中でも気温と電力消費量との間の散布図を作成し,線形回帰モデル(ライブラリscikit-learn(sklearn)
)により回帰直線を引きました.これも回帰問題です.線形回帰モデルでは説明変数と目的変数との間に線形の関係があることを仮定されていますが,非線形な関係であってもその関係を表現できることができるのがニューラルネットワーク(非線形回帰モデル) です.
そもそもモデルとは,入力値(説明変数)$x$(例えば,気温)を与えることで,出力値(目的変数)$y=f(x)$(例えば,電力消費量)を得るような関数$f$ を指します.
図:モデルとは?
ニューラルネットワークは,今流行の人工知能(AI)の基本的な技術となっていますので,まずはニューラルネットワークの基本的な仕組みから理解していきましょう.
ニューラルネットワークの歴史を簡単に振り返ると,まず,1943年に 「ニューロン」 という人間の脳を模した数理モデルが提唱され,1957年にデータによる学習が可能な 「パーセプトロン」 というニューラルネットワークが開発されます.パーセプトロンは1960年代に 第1次ブーム を引き起こしますが,XOR問題という弱点が指摘され 冬の時代 を迎えます.その後,1986年に誤差逆伝播法が提案されると,再び 第2次ブーム を迎えますが,コンピュータの能力が十分でなかったり,学習に必要なデータがなかったり,勾配消失などの問題があったりと,再び 冬の時代 となります.その後,2006年になり,トロント大学のヒントン教授によるランプ関数ReLU(Rectified Linear Unit)と呼ばれる活性化関数とドロップアウト正則化などの技術革新に加えて,コンピュータ性能の爆発的な向上により大規模なニューラルネットワークと大量のデータを処理できるようになり現在の 第3次ブーム を迎えることになります.現在も続く第3次ブームで,多層構造をもち「畳み込みネットワーク」を有するニューラルネットワークが広く普及するようになり,ディープラーニング(深層学習) と呼ばれるようになりました.ディープラーニングは音声認識や画像診断の分野で広く利用されるようになり産業応用分野でも急速に利用が広がっています.
図:ニューラルネットワークの歴史
ニューラルネットワークでは,人間の脳内にある 神経細胞(ニューロン) とそのつながり,つまり神経回路網を人工ニューロンという数式的なモデルで表現しています.ニューロンが他のニューロンと繋がっている状態を シナプス結合 といい,電気信号が伝わることによって情報的なつながり ネットワーク を形成しています.シナプスとシナプスの間の情報伝達効率は,前後のニューロンの活動状態によって決まり保持されます.このようなシナプスの情報伝達効率が生物の記憶や学習において重要な役割を果たしています.人間の脳では,1個のニューロンにおよそ1000個のニューロンが結合し,全部で1000億個のニューロンが存在していると言われており,人間の複雑な知性を司っていると考えられています.
図:ニューロンの構造
神経細胞(ニューロン)をコンピュータ上でモデル化したものを人工ニューロンやパーセプトロンともいいます.まず,中央のニューロン(破線の円)に着目すると,まず,入力側($u$)では$m$個のニューロンから $u = b + w_1 \times x_1 + w_2 \times x_2 + \cdots + w_m \times x_m = b + \sum_{i=1}^{m} w_m x_m $ という値を受け取っています.ここで,$x_1、x_2、x_3$ が前のニューロンから伝わってくる電気信号に当たり,$w_1、w_2、w_3$がニューロン間の情報伝達効率に当たります.多数のニューロンから伝達される入力$x$ と 重み$w$の線形結合に バイアス$b$ と呼ばれる定数を足すことで,ニューロンに入る電気信号の強さが決まります.また,中央のニューロンの出力側($o$)に注目すると,ニューロンが蓄積した電気量$y=f(u)$がある一定の閾値aを超えるかどうかによって,1または0どちらかの値が出力されています.1が出力される場合はニューロンの発火を,0が出力される場合にはニューロンの非発火を意味します.ここで,$f$は活性化関数と呼ばれる関数であり,蓄積した電気量をもとに発火(1)するか非発火(0)かを判断する関数です.
図:パーセプトロンの構造
このような単純な計算をしているニューロンが複雑に組み合わさりネットワークを組むことで,次々と電気信号が下流側へと伝達され,高度な「分類」や「回帰」が可能となります.これが ニューラルネットワーク です.ニューラルネットワークを理解する上で重要なのは,「入力層」「中間層(隠れ層)」「出力層」 です.そして,ニューロン同士をつなぐ 「重み」 です.ここで,「重み」 は,情報と情報のつながりの強さを表すパラメータです.この「重み」の大きさは,「入力層」と「出力層」に教師データを入力して 学習 することによって設定されます.情報(説明変数) が入力されるニューロンは 入力層 といいます.複数の説明変数があれば層は複数ニューロンで構成されます.入力層から情報を受け取った 「中間層(隠れ層)」 のニューロンは受け取った情報を自分なりに評価して,伝言ゲームのようにして,その結果を次の層のニューロンに伝達します.それを繰り返した末に,最終的に,情報(目的変数) が 「出力層」 に伝わります.このような,入力層から出力層へと一方向的に情報が伝わるニューラルネットワークのことを,「フィードフォワード(順伝播型)ニューラルネットワーク」 という.
下のフィードフォワードニューラルネットワークの例では,傘の画像の1つ1つのピクセルの輝度が配置された入力層から入っていきます.出力層には,「傘」「ゴルフクラブ」「杖」のニューロンが配置されています.入力層から入った情報は,中間層を伝って,最終的には,出力層の中の「傘」のニューロンに大きな電気信号(情報)が伝わり,画像が80%の確率で「傘」であると認識できたわけです(分類問題の場合).
図:フィードフォワードニューラルネットワークの構造
さて,精度の高いニューラルネットワークを作る上で,ニューロン同士をつなぐ多数の重み$w$の 学習 が重要となってきます(正確にはバイアス$b$も学習の対象となりますが,重み$w$の一種として考えることとしましょう).重み$w$の学習とは,様々な画像に対応する出力値と正解値との間の 誤差が最小 となるに重み$w$をチューニングしていく過程のことを指します.順伝播によって得られた出力値と予め用意された正解値との間で評価された誤差$E$(下の例では2乗和誤差)から,層を逆方向に遡るようにして重み$w$の値を少しずつ 更新 していきます.伝言ゲームの答え合わせをするようなイメージです.たくさんの教師データによりこのような更新の手続きを繰り返すことで次第にニューラルネットワークの誤差が最小になるように 最適化 されていきます.
図:ニューラルネットワークの学習
さて,このような誤差$E$が最小になるように重み$w$をチューニングする方法について考えてみましょう.一般的に,誤差$E$を重み$w$の関数$E(w)$として考え,この誤差関数$E(w)$が最小 となるような$w$の近似値を求めるというアプローチをとります.この近似値を求める際に使われる手法が,「勾配降下法」 と 「誤差逆伝播法」 です.ここで,勾配降下法 とは,重み$w$の初期値における誤差関数の勾配ベクトル(傾斜の向きと大きさ)を求め,その勾配ベクトルの成分が正の場合には重み$w$を小さくするように更新し,勾配ベクトルの成分が負の場合には重み$w$を大きくするように値の更新を行います.次に,1回目の更新後の重み$w$における勾配ベクトルを求めて,同様に勾配の正負に応じてさらに重み$w$の更新を行います.この作業を,重み$w$の更新ができなくなるまで,すなわち,誤差$E$が最小となるまで繰り返していくのです.
下の例では,2つの重み$w_1$と$w_2$の勾配降下法による更新のプロセスを模式化しています.傾斜のある地面に沿ってボールを転がしていくようなイメージで,誤差$E$(傾斜のある地面)がより低くなるような重み$w$の場所(ボールが止まる場所)を探すのです.
勾配降下法による 最適化アルゴリズム には,SGD法,Momentum法,AdaGrad法,Adam法など様々なものがあります.
図:勾配降下法の仕組み.
「勾配降下法」によって重み$w$の最適化を行うためには,上の図のように重み$w$の勾配に関する情報が必要となります.この勾配を効率的に計算する手法が 「誤差逆伝播法」(Backpropagation) と呼ばれるものです.出力側に近い勾配の値から順番に遡って入力側の勾配を求めていくことから,順伝播(フィードフォワード)の対義語として 「逆伝播」 と呼んでいます.
誤差関数$E$は多数の重み$w$で表現される合成関数であるため,多変数関数の連鎖律(チェーンルール)により,構成する各関数の導関数(勾配)の積によって表現することができます.そのため,層の深いネットワークの場合,入力層に近いところの重み$w$の勾配$\frac{\partial E}{\partial w}$を直接求めようとすると,多数の勾配の積を求めなくてはならず計算量が膨大となります.しかしながら,「誤差逆伝播法」では,出力層から入力層に遡るように,重み$w$の勾配$\frac{\partial E}{\partial w}$や入力$u$の勾配$\frac{\partial E}{\partial u}$をメモリに格納しながら効率的に計算することができます.
ただし,層の深いネットワークの場合,誤差逆伝播法によって重み$w$の勾配の値が0になってしまう 勾配消失 という問題があり,ニューラルネットワークの冬の時代をもたらす原因となっていましたが,前述した2006年のヒントン教授による技術革新(活性化関数ReLUとドロップアウト正則化)で解決され,今日のディープラーニングのブームのきっかけとなっています.
図:誤差逆伝播法の仕組み.
ニューラルネットワークにおける活性化関数(Activation function)は,ニューロンを興奮させるための関数であり,ニューロンの興奮状態を発火(1)か非発火(0)を表す信号に変換します.この活性化関数は,非線形変換 である必要があり,これによりニューラルネットワークの複雑な表現力を可能にします(線形変換は何回重ねてもても線形の変化しか表現できない).
使われる活性化関数は時代とともに変化しています.初期には,「ステップ関数」 という活性化関数が用いられていましたが,「誤差逆伝播法」が登場してからは 「シグモイド関数(ロジスティック関数)」 や 「tanh関数」 が使われるようになりました.また,最近のディープニューラルネットワークでは勾配消失の問題を解決させた 「ReLU」 がよく使われるようになりました.
また,出力層で使われる活性化関数は,二クラスの分類問題 の場合は 「シグモイド関数(ロジスティック関数)」,多クラスの分類問題 の場合は 「ソフトマックス関数」,回帰問題の場合は 「恒等関数」が挙げられます.
図:様々な活性化関数.
以下,ニューラルネットワークによく使われるシグモイド関数,tanh関数,ReLU関数,恒等関数のグラフをPythonのプログラミングによって示しています.恒等関数を除くいずれも$z=0$付近で 発火 している様子が見て取れます.
# シグモイド関数
import numpy as np
import matplotlib.pyplot as plt
def logistic_function(x):
return 1 / (1 + np.exp(-x))
X = np.arange(-5.0, 5.0, 0.1)
Y = logistic_function(X)
plt.plot(X, Y)
plt.ylim(-0.1, 1.1)
plt.xlabel("z")
plt.ylabel("f(z)")
plt.show()
# tanh関数
def tanh_function(x):
return np.tanh(x)
X = np.arange(-5.0, 5.0, 0.1)
Y = tanh_function(X)
plt.plot(X, Y)
plt.ylim(-1.1, 1.1)
plt.xlabel("z")
plt.ylabel("f(z)")
plt.show()
# ReLU関数
def relu_function(x):
return np.maximum(0, x)
x = np.arange(-5.0, 5.0, 0.1)
y = relu_function(x)
plt.plot(x, y)
plt.ylim(-1.0, 5.5)
plt.xlabel("z")
plt.ylabel("f(z)")
plt.show()
# identity関数(恒等関数)
def identity_function(x):
return x
x = np.arange(-5.0, 5.0, 0.1)
y = identity_function(x)
plt.plot(x, y)
plt.ylim(-5.5, 5.5)
plt.xlabel("z")
plt.ylabel("f(z)")
plt.show()
ここでは機械学習ライブラリsklearn
(scikit-learn)の中の統計モデルを用いて予測モデルを構築してみましょう.ライブラリsklearn
(scikit-learn)は,基本的な機械学習のアルゴリズムやデータセットを備えており,単純で定型的なコードで実装できるため手軽に機械学習を始めることができます.sklearn
(scikit-learn)には機械学習に関する6つのパッケージ:1) 回帰問題,2) 分類問題,3) クラスタリング,4) 次元削減,5) モデル評価,6) データ前処理があります.ここでは,「回帰問題」 のための ニューラルネットワークモデルのクラスneural_network.MLPRegressor
を統計モデルとして導入します.また, sklearn
には,「分類問題」 に対しても,ニューラルネットワークを使うこともできます.その場合は,クラスneural_network.MLPClassifier
を導入します.sklearn
(scikit-learn)のニューラルネットワークの詳細については,scikit-learnの公式ホームページ https://scikit-learn.org/stable/modules/neural_networks_supervised.html を参照してください.
neural_network.MLPRegressor()
クラスでは,多数の引数を設定し,試行錯誤でユーザーがハイパーパラメータの最適値を見つけて設定する必要があります.以下の引数の数値は,デフォルト値を示します.
class sklearn.neural_network.MLPRegressor(hidden_layer_sizes=(100, ), activation='relu', *, solver='adam', alpha=0.0001, batch_size='auto', learning_rate='constant', learning_rate_init=0.001, power_t=0.5, max_iter=200, shuffle=True, random_state=None, tol=0.0001, verbose=False, warm_start=False, momentum=0.9, nesterovs_momentum=True, early_stopping=False, validation_fraction=0.1, beta_1=0.9, beta_2=0.999, epsilon=1e-08, n_iter_no_change=10, max_fun=15000)
以下,主なハイパーパラメータについて説明します.
●solver
は,学習の最適化アルゴリズムを選択します.ここでの最適化アルゴリズムに基づいて予測誤差が最小になるように重み付けを決定します.ここの選択を誤ると学習速度が遅くなったり,最終的な学習結果が最適な場所(最小値)に行き着かない可能性があります.solver=lbfgs
は,準ニュートン法を省メモリにて実現した手法です.1000以下の小さいデータセットの場合に高速で学習できます.solver=sgd
は,確率的勾配降下法(Stochastic Gradient Descent)であり,訓練データをランダムにシャッフルしてミニバッチとして抽出し,重みを更新していきます.この手法は確率的に局所解にはまりにくくなるという長所やオンライン学習で使いやすいという長所を持っていますが,短所として学習率の設定が難しいことが挙げられます.solver=adam
は,同じく確率的勾配降下法をベースとする手法(ADAptive Momentum estimation)で,「AdaGard法」(過去の学習によって更新されてこなかったパラメータを優先的に更新する)と「Momentum法」(更新量の急変を抑え滑らかに更新する)を組み合わせた方法で高い性能で最適化できる手法です.このsolver=adam
がデフォルト設定となっています.
●activate
は,活性化関数を選択します.activate=identity
は,特に活性化関数を何も設定しません.つまり,$h(x)=x$であり,入力された値をそのまま出力として返します.activate=logistic
は,ロジスティック関数(シグモイド関数)です.式では,$h(x)=\frac{1}{1+e^{-x}}$となります.activate=tanh
は,ハイパボリックタンジェントを活性化関数として使います.tanh
はロジスティック関数と同じ特徴を持ちますが,tanh
では出力範囲が-1から1に変換されています.$h(x)= \tanh x = \frac{1-e^{-2x}}{1+e^{-2x}} $となります.activate=relu
は,ランプ関数ReLU(Rectified Linear Unit)と呼ばれます.式で表すと,$h(x)=\max(0,x)$となります.ReLU関数の導関数はは$x>0$で常に1となるので,ニューラルネットワークの学習で長年問題となっていた「勾配消失」という問題が生じないという利点があります.このactivate=relu
がデフォルト設定となっています.
●hidden_layer_sizes
は,中間層の数とニューロンの数を指定します.例えば,中間層が2層10ニューロンずつ配置する場合は(10,10,)のように指定します.中間層のニューロンの数を入力層の次元よりも小さくすると次元圧縮のように機能し,主成分分析のようなことが可能になります.逆に入力層の次元よりも大きくするとスパースなデータ表現を得ることができます.中間層の数を増やし,ニューロンの数を増やすことで,複雑な非線形回帰問題を解くことができます.ただし,ニューロンの数が多く,訓練データが少なすぎると,この少ない既知データに最適化されすぎてしまい,未知のデータへの対応ができなくなってしまう 「過学習」 という問題が生じてしまいます.過学習とは,学習データによる予測精度は高く,検証用データによる予測データは低い状態を指します.過学習が生じた場合には,より多くの訓練データを用意したり,次の 「L2正則化」 と呼ばれるテクニックを使います.
●alpha
は,L2正則化のペナルティ項の係数 を表しています.正則化とは,過学習を防ぐために,誤差関数に重みを付けた制約(重み$w$を小さくするような成約)を与える項(ペナルティ項)を加えます.alpha
は,一般的に0.01~0.00001の間の値を指定して,この値を大きくするほど,学習時により小さな重みが重要視されるようになります.過学習が生じているときにはこのパラメータを上げて試してみましょう.
●batch_size='auto'
は,最適化ソルバー'sgd'や'adam'のためのミニバッチ学習のサイズを設定します.ソルバーが'lbfgs'の場合,分類器はミニバッチを使用しません.auto "に設定されている場合,batch_size=min(200, n_samples)となります(サンプル数と200の小さい方の数字という意味).
●max_iter
は,学習の反復の最大回数を設定します.常に最大回数学習まで計算するわけではなく,途中で学習が完了したと判断された場合はこれよりも早く終了します.
●tol
は,学習の収束値を設定します.10エポック(学習回数のこと)連続でこの値よりもlossやscoreが向上しなかった場合には,学習が収束したと判断して,学習を終了します.大きくしすぎると学習途中で終了してしまいます.
●n_iter_no_change
は,このエポック数で連続して学習が改善しない場合には計算が収束したものとして計算を終了します.デフォルト値は10です.
●random_state
は,乱数発生器の番号をしてします.重み付けやバイアスの初期値やバッチサンプリング(ソルバーsgd
とadam
のみ)で乱数を発生させる場合に,指定した番号の乱数を発生させます.これをNone(デフォルト)とすることで,毎回異なる結果となりますが,乱数発生器の番号を指定することで再現性のある同じ結果が得られます.
●verbose
は,進行状況のメッセージを標準出力として出力するかしないかを指定します.結果がおかしい場合には,True
としてください.デフォルトは,False
です.
その他のハイパーパラメータの設定方法については,scikit-learnの公式ホームページ https://scikit-learn.org/stable/modules/generated/sklearn.neural_network.MLPRegressor.html を参照してください.
前置きが長くなりました.兎にも角にも,sklearn
(scikit-learn)で機械学習を体験しましょう.まずは,実行に必要なライブラリをインポートしましょう.
2行目で数値計算ライブラリnumpy
をインポートします.import numpy as np
とすることで,プログラム中ではnpと省略してインポートします.作図のための,matplotlib
とseaborn
も4~8行目でインポートします.10行目のニューラルネットワーク(neural_network
)を作成するのに必要なsklearn
(scikit-learn)もインポートします.12行目では,モデルの平均二乗誤差を評価するために必要なmean_squared_error
をインポートしています.
Jupyter Notebookの中だけで有効なマジックコマンド%precison 3
(小数点3桁まで)と%matplotlib inline
(インラインで画像を表示)も忘れずにつけましょう.
# numpyをnpという別名でインポートします.
import numpy as np
# matplotlibをpltという別名でインポートします.
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.cm as cm
# Seabornをインポートします.
import seaborn as sns
# sklearn(scikit-learn)のニューラルネットワークのライブラリをインポートします
from sklearn import neural_network
# sklearn(scikit-learn)の平均二乗誤差のライブラリをインポートします
from sklearn.metrics import mean_squared_error
%precision 3
%matplotlib inline
まず,最も簡単なニューラルネットワークを作ってみましょう.以下に示すプログラムのように,sklearn
(scikit-learn)を使えば実質10行のプログラムで機械学習ができてしまいます.
ここでは,まず,「1」という入力を1つ与えると(説明変数は1つ),その値を2倍して「2」と出力してくれるモデルを作成してみましょう.
図:説明変数が1つの場合のニューラルネットワーク.
プログラムでは,ニューラルネットワークの学習と検証のために,まずは訓練データ(モデルの学習に用いる)と検証データ(モデルが正しいかどうかの検証に用いる)を用意します.
まずは,ニューラルネットワークの訓練データを作成します.2行目のx_train=np.array([[1]], dtype=np.float)
で,訓練データの説明変数x_train
を設定します.numpy配列で 「1」 という 説明変数$x$ を 1つ設定したことになります.また,4行目のy_train=np.array([[2]], dtype=np.float)
では,訓練データの目的変数y_train
を設定します.numpy配列で 「2」 という 目的変数$y$ を 1つ設定 したことになります.
次に,作られたニューラルネットワークモデルの精度検証のための検証データを作成します.まずは,6行目のx_test=np.array([[1]], dtype=np.float)
では,検証データの説明変数x_test
を設定します.numpy配列で同様に 「1」 という 説明変数$x$(入力値) を 1つ設定 したことになります.また,8行目のy_test=np.array([[2]], dtype=np.float)
では,検証データの目的変数y_train
を設定します.numpy配列で同様に 「2」 という 目的変数$y$(正解値) を 1つ設定 したことになります.
11行目で neural_network.MLPRegressor
クラス から model
インスタンス を作成します.ハイパーパラメータの設定については,前の節を確認してください.ここでは,入力は1次元,中間層1層1ニューロン,出力は1次元 のもっとも 単純なネットワーク となっています.最適化計算には,solver="sgd"
として 確率的勾配降下法 を設定しています.また,ここでは,活性化関数は恒等関数solver="identity"
としています.中間層で重み$w^{(1)}$とバイアス$b^{(1)}$により$w^{(1)}x+b^{(1)}$が計算され,出力層で重み$w^{(2)}$とバイアス$b^{(2)}$により$w^{(2)}(w^{(1)}x+b^{(1)})+b^{(2)}$が計算され出力層へと情報が流れます.また,一般的にニューラルネットワークでは初期値化やミニバッチ学習の際に乱数が用いられるため,実行するたびに結果が変わりますが,乱数発生器random_state
を固定して,誰が計算しても同じ結果となるようにしています.ここでは,random_state=5
と設定しています(興味がある人は,ここの数値を変更して計算してみてください).
13行目のmodel.fit(x_train, y_train)
で,model
インスタンスからfit()
メソッドを呼び出しています.ここで,訓練データの 説明変数x_train
と 目的変数y_train
から 学習 を行い,ニューラルネットワークモデルの重み$w$やバイアス$b$を 確率的勾配降下法により最適化します.sgd
による最適化(学習)に時間がかかることがあるので気長に待ちましょう.
15行目のy_test_predict=model.predict(x_test)
で,model
インスタンスからpredict()
メソッドを呼び出しています.検証データの 説明変数x_test
を入力して,モデルによる 予測結果y_test_predict
を計算します.16行目では,正解値y_test
と予測値y_test_predict
の比較を表示します.
18行目のmse=mean_squared_error(y_test, y_test_predict)
では,ライブラリmean_squared_error
を使っています.正解値y_test
と 予測値y_test_predict
との間の平均二乗誤差MSE(mean squared error) を計算することで,検証データに対する予測精度を評価しています.19行目では,計算されたMSEを表示しています.ここで,平均二乗誤差は,
$MSE=\displaystyle{\frac{1}{n}\sum_{i=1}^{n}} (y_i - \hat{y}_i)^2$
のように定義されます.ここで,$y_i$は目的変数の正解値,$\hat{y}_i$は目的変数の予測値です.
さて,早速以下のプログラムを動かしてみましょう.
# 訓練データの説明変数(入力)の設定
x_train=np.array([[1]], dtype=np.float)
# 訓練データの目的変数(出力)の設定
y_train=np.array([[2]], dtype=np.float)
# 検証データの説明変数(入力)の設定
x_test=np.array([[1]], dtype=np.float)
# 検証データの目的変数(正解)の設定
y_test=np.array([[2]], dtype=np.float)
# neural_networkクラスからmodelインスタンスを作成
model = neural_network.MLPRegressor(solver="sgd",activation="identity", hidden_layer_sizes=(1),max_iter=10000,tol=1e-6,n_iter_no_change=50,random_state=5,verbose=False)
# 予測モデルの最適化
model.fit(x_train, y_train.ravel())
# モデルによる予測データの作成(検証用データ)
y_test_predict=model.predict(x_test)
print('y_test=',y_test.ravel(),'y_test_predict=',y_test_predict)
# 平均二乗誤差の評価
mse=mean_squared_error(y_test, y_test_predict)
print('Mean squared error=',mse)
確かに,検証データの入力値x_test
に対して「1」とモデルに入力することで,予測値y_test_predict
は「2」と出力できるモデルを構築できています.平均二乗誤差MSEも小さく,学習がうまくいったと言えるでしょう.
さて,このモデルは,「1」以外の数字を入力しても2倍した数値を出力してくれるのでしょうか?
以下のプログラムで試してみましょう.すでに,訓練データにより予測モデルの最適化(学習)は済んでいますので,ここでは,検証データだけ作り直して,モデルによる予測をし直して,再度,精度検証をしてみましょう.
ここでは,2行目のx_test=np.array([[2],[3],[4]], dtype=np.float)
では,検証データとして,入力する 説明変数「2」「3」「4」と設定 します.そして,4行目のy_test=np.array([[4],[6],[8]], dtype=np.float)
では,目的変数(正解値)を説明変数を2倍した「4」「6」「8」と設定 します.
そして,7行目に先のプログラムで作られたモデルにより再度予測を行い,10行目で平均二乗誤差MSEを再度計算します.
さて,以下のプログラムを動かしてみましょう.
# 検証データの説明変数(入力)の設定
x_test=np.array([[2],[3],[4]], dtype=np.float)
# 検証データの目的変数(正解)の設定
y_test=np.array([[4],[6],[8]], dtype=np.float)
# モデルによる予測データの作成(検証用データ)
y_test_predict=model.predict(x_test)
print('y_test=',y_test.ravel(),'y_test_predict=',y_test_predict)
# 平均二乗誤差の評価
mse=mean_squared_error(y_test, y_test_predict)
print('Mean square error=',mse)
予測された結果を見ると,説明変数「2」「3」「4」 に対して,いずれの 予測値も「3.236」「4.473」「5.71」 となっています.説明変数よりは大きい傾向は見られますが,正解値「4」「6」「8」 に比べて 全体に小さめ であるようです.平均二乗誤差も2.72とかなり大きく,誤差の大きなモデルと言えます.「1」を入力すると「2」と出力するモデルとなりましたが,その他の入力値も2倍にしてくれるようなこちらが想定したモデルとはなっていないようです.
そもそも,「1」を2倍して「2」にするという見方もできますし,「1」に1を足して「2」にするという見方もできます.学習データが不十分であるためこちら(分析者)が想定している適切なモデルが完成していない状態と言えるでしょう.少ない訓練データに過剰に適合してしまっている状態であり.未知のデータに対しては汎化できていない状態なのです.
このような問題に対応するためには,訓練データをもっと増やして学習する必要があるのです.
次に,訓練データを1つから3つに増やすことで再度,学習をしてみましょう.
以下のプログラムでは,前のプログラムに比べて訓練データが増えている点に違いがあります.
2行目のx_train=np.array([[1],[2],[3]], dtype=np.float)
では,訓練データの 説明変数$x$ を 「1」「2」「3」と3つ設定 しています.それに対応する 目的変数$y$ は,4行目のy_train=np.array([[2],[4],[6]], dtype=np.float)
で,「2」「4」「6」と3つ設定 しています.
6行目のx_test=np.array([[4],[5],[6]], dtype=np.float)
では,検証データの 説明変数$x$を,未知の「4」「5」「6」を3つ設定 しています.そして,対応する 目的変数$y$つまり正解値 は,8行目のy_test=np.array([[8],[10],[12]], dtype=np.float)
で,説明変数を2倍した「8」「10」「12」と3つ設定しています.
これらのデータを用いて,11行目と13行目でmodel1
インスタンスを作成し, 確率的勾配降下法sgd
による最適化計算を行っています.
15行目のy_test_predict=model1.predict(x_test)
で予測された結果が,どの程度正しいのかを18行目のmse=mean_squared_error(y_test, y_test_predict)
で平均二乗誤差を計算することで判定します.
早速,以下のプログラムを動かしてみましょう.今度は,入力値を2倍にするモデルができているでしょうか??
# 訓練データの説明変数の設定
x_train=np.array([[1],[2],[3]], dtype=np.float)
# 訓練データの目的変数の設定
y_train=np.array([[2],[4],[6]], dtype=np.float)
# 検証データの説明変数の設定
x_test=np.array([[4],[5],[6]], dtype=np.float)
# 検証データの目的変数(正解)の設定
y_test=np.array([[8],[10],[12]], dtype=np.float)
# neural_networkクラスからmodel1インスタンスを作成
model1 = neural_network.MLPRegressor(solver="sgd",activation="identity", hidden_layer_sizes=(1),max_iter=10000,tol=1e-6,n_iter_no_change=50,random_state=5,verbose=False)
# 予測モデルの最適化
model1.fit(x_train, y_train.ravel())
# モデルによる予測データの作成(検証用データ)
y_test_predict=model1.predict(x_test)
print('y_test=',y_test.ravel(),'y_test_predict=',y_test_predict)
# 平均二乗誤差の評価
mse=mean_squared_error(y_test, y_test_predict)
print('Mean square error=',mse)
予測された結果を見ると,説明変数「4」「5」「6」 に対して,いずれの 予測値も「7.983」「9.972」「11.962」 となっています.今回は,正解値「8」「10」「12」とほぼ一致する結果が得られています.平均二乗誤差MSEも先程に比べて大幅に減少しています.
以上の計算から,ニューラルネットワークにより精度の高いモデルを作るためには,十分な数の訓練データを与えてあげる必要がある ことが理解できました.
次にもう少し複雑なニューラルネットワークモデルを作ってみましょう.
前節のプログラムでは,説明変数は1つしかありませんでしたが,説明変数が2つ以上ある場合を考えてみます.つまり,前節のプログラムでは入力層が1つの 単回帰問題 であったのに対して,ここでは入力層が2つ以上 存在する 重回帰問題 となります.
ここで,以下の図のように, 横軸$x_1$と縦軸$x_2$の平面上の4つの点 の座標を考えます. A(0,0),B(1,0),C(0,1),D(1,1)の4点 です.ここで$x_1$と$x_2$の座標(位置)が2つの説明変数の値を表しています.4つの点で 赤(1) か 青(0) に色分け(ニューラルネットワーク自体は色を直接扱えないので1か0の数字で判断する)がされていて,それをニューラルネットワークにより学習することで,赤か青かを正しく分類できるかどうかを確かめてみましょう.下の図のように,平面上に 直線 $a x_1 + b x_2 + c = 0$(黒破線)を引くことで,4つの点を赤(1)と青(0)の2つのグループに分けることができるかを調べるイメージです.
図:点A,B,C,Dにおける青(0)と赤(1)の配置パターン
まず,ニューラルネットワークのプログラミングの前に,訓練データ の 説明変数(x_train
) と 目的変数(y_train
) を上のように散布図としてプロットする関数scatter_plot
を定義します.この関数では,同時にモデルの検証として,説明変数(x_train
) と 予測結果(y_train_predict
) の散布図も並べて表示できます.これによって,ニューラルネットワークの学習がうまくいったかどうかを確認できます.
まずは以下のセルを実行して,関数scatter_plot
を定義しましょう(関数を定義しただけですので,ここでは何も表示されません).
def scatter_plot(x_train,y_train,y_train_predict):
# 散布図の大きさを指定
plt.figure(figsize=(20, 8))
# 1行2列の1つ目に作図
plt.subplot(1,2,1)
# x軸の範囲
plt.xlim(-1, 2)
# y軸の範囲
plt.ylim(-1, 2)
# trainデータのプロット
colormap=plt.get_cmap('coolwarm')
cm=colormap(y_train)
for j in range(len(y_train)):
plt.text(x_train[j,0]+0.2,x_train[j,1]+0.2, '{:.3f}'.format(y_train[j]), fontsize=12)
plt.plot(x_train[j,0],x_train[j,1],color=cm[j], marker='o', markersize=50)
img=plt.imshow(np.array([[0,1]]), cmap='coolwarm')
img.set_visible(False)
plt.colorbar()
# タイトル
plt.title('y_train')
# x軸のラベル
plt.xlabel('X1')
# y軸のラベル
plt.ylabel('X2')
# 1行2列の2つ目に作図
plt.subplot(1,2,2)
# x軸の範囲
plt.xlim(-1, 2)
# y軸の範囲
plt.ylim(-1, 2)
# 予測データのプロット
colormap=plt.get_cmap('coolwarm')
cm=colormap(y_train_predict)
for j in range(len(y_train_predict)):
plt.text(x_train[j,0]+0.2,x_train[j,1]+0.2,'{:.3f}'.format(y_train_predict[j]),fontsize=12)
plt.plot(x_train[j,0],x_train[j,1],color=cm[j], marker='o', markersize=50)
img=plt.imshow(np.array([[0,1]]), cmap='coolwarm')
img.set_visible(False)
plt.colorbar()
# タイトル
plt.title('y_train_predict')
# x軸のラベル
plt.xlabel('X1')
# y軸のラベル
plt.ylabel('X2')
#
return
次に,前節と同じように,ニューラルネットワークのプログラムを組みます.ここでは,検証データ(test)は作成せずに,訓練データ(train)のみを扱い特にニューラルネットワークにより学習が適切にできているかどうかできるかを確認します.
まずは,訓練データを設定します.説明変数x_train
は,2行目のように点A(0,0),B(1,0),C(0,1),D(1,1)の座標を順にx_train=np.array([[0,0],[1,0],[0,1],[1,1]], dtype=np.float)
のように設定します.目的変数y_train
は,4行目のように点A,B,C,Dの順に青(0),赤(1),赤(1),赤(1)としてy_train=np.array([[0],[1],[1],[1]], dtype=np.float)
と設定します.このような演算を論理和(OR)演算といいます.
7行目でニューラルネットワークのmodel2
インスタンスを作成しています.ここでは,以下の図のように,入力層は2つ,中間層が1層1ニューロン,出力層は1つのネットワークとなっています.
図:説明変数が2つの場合のニューラルネットワーク
9行目では,model2.fit(x_train, y_train.ravel())
により最適化計算を行っています.
14行目ではモデルによる予測値(ただし,訓練データ)の平均二乗誤差MSEを評価して,最後の17行目で先に定義した関数scatter_plot
により作図しています.
以下のプログラムを実行してみてください.
# 訓練データの説明変数の設定
x_train=np.array([[0,0],[1,0],[0,1],[1,1]], dtype=np.float)
# 訓練データの目的変数の設定
y_train=np.array([[0],[1],[1],[1]], dtype=np.float)
# neural_networkクラスからmodel1インスタンスを作成
model2 = neural_network.MLPRegressor(solver="sgd",activation="identity", hidden_layer_sizes=(1),max_iter=10000,tol=1e-6,n_iter_no_change=50,random_state=5,verbose=False)
# モデルの最適化
model2.fit(x_train, y_train.ravel())
# モデルによる予測
y_train_predict=model2.predict(x_train)
print('y_train=',y_train.ravel(),'y_train_predict=',y_train_predict)
# 平均二乗誤差の評価
mse=mean_squared_error(y_train, y_train_predict)
print('Mean square error=',mse)
# 散布図にプロットします.
scatter_plot(x_train,y_train.ravel(),y_train_predict.ravel())
さて,結果を見ると,左側の目的変数の正解値(y_train
)と右側の予測値(y_train_predict
)の色はよく一致していることが分かります.値は,点A,B,C,Dの順番に,「0.259」,「0.716」,「0.786」,「1.243」となりました.ピタリではありませんが,点Aは「0」に近く,それ以外の点は「1」に近い結果となりました.そして,確かに,以下の図のような赤青の線引きが可能であることが分かります.
図:赤と青の線形分離
上のプログラムの4行目の4つの目的変数(0か?1か?)をいろいろと変えて,どのような場合にはニューラルネットワークによる分類の精度が高く(誤差が小さく),どのようば場合に分類の精度が低い(誤差が大きい)かを確認してみましょう.
ここで,ニューラルネットワークの分類の精度の低い(誤差の大きい)パターンの1つを見てみましょう.
以下のプログラムのように,4行目の目的変数の設定において,点A(0,0),B(1,0),C(0,1),D(1,1)の順に,「青(0)」「赤(1)」「赤(1)」「青(0)」としてy_train=np.array([[0],[1],[1],[0]], dtype=np.float)
と設定して,実行してみてください.このような演算を排他的論理和(XOR)演算といいます.
# 訓練データの説明変数の設定
x_train=np.array([[0,0],[1,0],[0,1],[1,1]], dtype=np.float)
# 訓練データの目的変数の設定
y_train=np.array([[0],[1],[1],[0]], dtype=np.float)
# neural_networkクラスからmodel1インスタンスを作成
model2 = neural_network.MLPRegressor(solver="sgd",activation="identity", hidden_layer_sizes=(1),max_iter=10000,tol=1e-6,n_iter_no_change=50,random_state=5,verbose=False)
# モデルの最適化
model2.fit(x_train, y_train.ravel())
# モデルによる予測
y_train_predict=model2.predict(x_train)
print('y_train=',y_train.ravel(),'y_train_predict=',y_train_predict)
# 平均二乗誤差の評価
mse=mean_squared_error(y_train, y_train_predict)
print('Mean square error=',mse)
# 散布図にプロットします.
scatter_plot(x_train,y_train.ravel(),y_train_predict.ravel())
さて,結果を見ると,左側の目的変数の正解値(y_train
)と右側の予測値(y_train_predict
)の色を比較すると大きく異なっていることが分かります.予測値は点A,B,C,Dの順番に「0.478」「0.461」「0.539」「0.521」となり,赤 (1)とも青 (0)ともつかずニューラルネットワークは分類に困っている様子です.
このような分類の場合,1本の直線で赤と青を分離することができません.つまり,より精度の高い分類のためには,下の図のように曲線を使って分類してあげる必要があるのです.
図:青と赤の非線形分離
ニューラルネットワークにおいて,このような非線形な分類を行うためには,次の2つの変更が必要となります.
1つは,活性化関数を非線形の表現を可能とするものに変更することです.現在のプログラムでは,活性化関数$h(x)$をactivation="identity"
としていて,ニューロンで重み付け計算された結果がそのまま出力層へと流れていっています.つまり,活性化関数は$h(x)=x$であり 線形 となっています.これでは, 複雑なモデル表現はできません. そこで,活性化関数として,ロジスティック関数activation="logistic"
($h(x)=\frac{1}{1+e^{-x}}$),tanh関数activation="tanh"
($h(x)= \tanh x = \frac{1-e^{-2x}}{1+e^{-2x}} $), ランプ関数activation="relu"
($h(x)=\max(0,x)$)といった 非線形の活性化関数 を導入してみましょう.
また,もう1つは,ニューラルネットワークの中間層の層数やニューロン数を増やすことです.現在のプログラムでは,中間層の数は,hidden_layer_sizes=(1)
と設定されていて,1層1ニューロンとなっています.つまり,hidden_layer_sizes=(4,4)
といった形で2層4ニューロンにすることでより複雑な表現ができるようになりますとなります.
以下のプログラムでは,上記のプログラムから活性化関数をランプ関数activation="relu"
,隠れ層を2層4ニューロン(hidden_layer_sizes=(4,4)
)に変更したものです.次のようにより複雑なネットワークとなります.
図:多層ニューラルネットワークの構造
どのような結果になるでしょうか?早速実行してみましょう.
# 訓練データの説明変数の設定
x_train=np.array([[0,0],[1,0],[0,1],[1,1]], dtype=np.float)
# 訓練データの目的変数の設定
y_train=np.array([[0],[1],[1],[0]], dtype=np.float)
# neural_networkクラスからmodel1インスタンスを作成
model2 = neural_network.MLPRegressor(solver="sgd",activation="relu", hidden_layer_sizes=(4,4),max_iter=10000,tol=1e-6,n_iter_no_change=50,random_state=5,verbose=False)
# モデルの最適化
model2.fit(x_train, y_train.ravel())
# モデルによる予測
y_train_predict=model2.predict(x_train)
print('y_train=',y_train.ravel(),'y_train_predict=',y_train_predict)
# 平均二乗誤差の評価
mse=mean_squared_error(y_train, y_train_predict)
print('Mean square error=',mse)
# 散布図にプロットします.
scatter_plot(x_train,y_train.ravel(),y_train_predict.ravel())
さて,結果を見ると,左側の目的変数の正解値(y_train
)と右側の予測値(y_train_predict
)を比較するとよく一致していて,青赤をきれいに分類できていることが分かります.予測値は点A,B,C,Dの順番に「0.023」「1.009」「0.977」「-0.013」となり,先程に比べてより正確に青(0)と赤(1)を分類することができています.平均二乗誤差MSEも極めて小さく(0.00033),精度の高いモデルを作ることができたと言えるでしょう.
つまり,ニューラルネットワークモデルの表現力を高めてより精度の高いものにするためには,1) 非線形の活性化関数の選択や2) 中間層の層数やニューロン数の設定が重要となります.
そして,大量の入力データとディープなニューラルネットワークの組み合わせにより,人間の分類能力を超えるような機械学習が可能となってくるのです.
● ニューラルネットワークは,「入力層」「中間層(隠れ層)」「出力層」から構成される.
● 情報(説明変数)を入力するためのニューロンの配置を「入力層」といいます.
● 「入力層」からの情報(説明変数)を受け取った「隠れ層(中間層)」のニューロンは,受け取った情報をの発火か非発火かを評価して,その結果を次の層のニューロンに伝達し,それを繰り返した末に,最終的に「出力層」に情報が伝達します.
● 「出力層」で得られた予測が目的変数として出力されます.
● ライブラリsklearn
(scikit-learn)のneural_network.MLPRegressor
クラスを使うと簡単にニューラルネットワークによる統計回帰モデルを構築できます.
● ニューラルネットワークに入力する訓練データが少ない場合には,「過学習」が発生する可能性があります.過学習を抑えるためには,訓練データを増やすことで解決することがあります.
● ニューラルネットワークで非線形な回帰問題を解く場合には,非線形の活性化関数を導入したり,中間層の層数やニューロン数を増やしたりすることで,表現力の高いモデルを用いる必要があります.
さて,次の「アメダス気象データ分析チャレンジ!(Python版) 2日目 実践編」では,このsklearn
(scikit-learn)のニューラルネットワークモデルを用いることで,気象データを説明変数とする電力消費量予測モデルを作ってみましょう.