「Deep Learning with Python」のサンプルプログラム3つ目。
データはBoston Housing Dataを用いる。1970年代のボストン郊外地域の不動産物件に関するデータで、ある地域の平均物件価格と部屋の数や築年数といった物件情報、犯罪率や黒人比率などの人口統計に関する属性が付属している。つまり、ある地域の不動産の属性を元にして、その地域の平均物件価格を予測する問題である。先の2つの分類問題とは異なり、これは回帰問題に属する。つまり、データを分類するものではなく、学習データからある1つの連続量を予測するものである。
どうでもいいけど、最近、核戦争が起きて荒廃した未来のボストンを舞台にしたゲームを遊んでいるので、ボストンという地名に反応してしまう (笑)
例によってkerasのインターフェイスからデータセットをロードする。
from keras.datasets import boston_housing (train_data, train_targets), (test_data, test_targets) = boston_housing.load_data()
目的変数 (住宅価格) は以下の通り。単位は千ドルで、少し安いと思うかもしれない。これは1970年代の国勢調査のデータで、インフレ調整していないことが原因。
>>> train_targets [ 15.2, 42.3, 50. ... 19.4, 19.4, 29.1]
予測の元となる説明変数は全部で13種類あり、それぞれの意味は以下の通り。
- CRIM・・・犯罪発生率(人口単位)
- ZN・・・25,000平方フィート以上の住宅区画の割合
- INDUS・・・非小売業の土地面積の割合(人口単位)
- CHAS・・・チャールズ川沿いかどうか(1:Yes、0:No)
- NOX・・・窒素酸化物の濃度(pphm単位)
- RM・・・1戸あたりの平均部屋数
- AGE・・・1940年よりも前に建てられた家屋の割合
- DIS・・・ボストンの主な5つの雇用圏までの重み付き距離
- RAD・・・幹線道路へのアクセス指数
- TAX・・・10,000ドルあたりの所得税率
- PTRATIO・・・教師あたりの生徒の数(人口単位)
- B・・・黒人居住者の割合(人口単位)
- LSTAT・・・低所得者の割合
ボストンデータセットは、以前に取り上げたMNIST、IMDBデータセットとは次の2点で少々異なる。
1点目は、データの数が506件と比較的少数であることで、検証用データをここから取り分けるため訓練に使用できるデータは更に減る。2点目は、それぞれの説明変数が異なるスケールを持っていることである。上記の通り、割合や人口比といったパーセントで表されるデータもあり、部屋数の平均のように数値で表されるデータもあり、川沿いか否かという0/1のデータもある。もちろんこのままニューラルネットに入力することもできるが、学習が難しくなる可能性がある。
上記2点のデータ特性に対応するため、ちょっとした操作が必要となる。
データの正規化
まずは2点目の特性に対応するため、データの正規化を行う。これは、スケールが揃っていない説明変数を、平均0、分散1となるように変換するもの。
Numpyを使ってこの処理は簡単に書ける。
# 前処理 # データを正規化(平均0、分散1に変換)する mean = train_data.mean(axis=0) train_data -= mean std = train_data.std(axis=0) train_data /= std test_data -= mean test_data /= std
ニューラルネットの定義
続いて、1点目の特性である「データセットの少なさ」に対する対応。一般に、データのサンプル数が少なく学習モデルのパラメータ数が多い場合、過学習が起こりやすい。今回のデータも比較的少数であるため、過学習を避けるために比較的小さなニューラルネットを用いる。ここでは、隠れ層が2層、それぞれ64ノードのモデルを用いる。
# モデル定義 #KFCV用に同じモデルを複数回作成するため、関数にする def build_model(): model = models.Sequential() model.add(layers.Dense(64, activation='relu', input_shape=(train_data.shape[1],))) model.add(layers.Dense(64, activation='relu')) model.add(layers.Dense(1)) model.compile(optimizer='rmsprop', loss='mse', metrics=['mae']) return model
ニューラルネットの出力層は、活性関数の無い単一ノードを用いる。(線形関数の層となる) ここでは、大きな値を取る可能性がある住宅価格を予測するものであるため、活性関数は使用していない。回帰問題の場合でも、出力の範囲を制限したい場合は何らかの活性関数を用いる場合がある。たとえば、確率値として0〜1の値を求めたい場合は、活性関数としてシグモイド関数を用いる。また、損失関数は、回帰問題でよく用いられるmse (mean squared error)を用いる。次に出てくるK分割交差検証のために、新しいモデルを何度も作る必要があるため、後で再利用できるように関数化している。
K分割交差検証
更に続いて、データセットの少なさに対する対応。訓練したニューラルネットの予測を検証する際に使うデータの選び方によって、検証結果の変動が大きくなる (高variance) 可能性がある。これを防ぐために、K分割交差検証 (K-fold cross validation; KFCV) という方法を用いる。
ただし、"Deep Learning with Python"のKFCVを説明した図3.11には大きな誤植がある。Fold 2とFold 3の1つ目の四角は、Trainingでなければならないが、Validationと書かれてしまっている。コードや説明文には誤りはないので丁寧に読んでいれば間違えることは無いと思うけど、混乱する…
(正しい)図を見ればだいたい理解できるはずだけど、KFCVの方法は次の通り。データセットをk個(今回はk=4)に均等に分割し、k番目のデータを検証用に取り分け、それ以外のk-1個のデータでニューラルネットを訓練する。これをk回繰り返して、最終的な検証スコアはk回の平均を取ったものになる。
上記の考え方を素直にコードに落としたものがこちら。
k = 4 num_val_samples = len(train_data) // k num_epochs = 100 all_scores = [] # K-fold cross validation (K-分割交差検証; KFCV) for i in range(k): print('processing fold #', i) val_data = train_data[i * num_val_samples: (i+1) * num_val_samples] val_targets = train_targets[i * num_val_samples: (i+1) * num_val_samples] partial_train_data = np.concatenate( [train_data[:i * num_val_samples], train_data[(i+1) * num_val_samples:]], axis=0) partial_train_targets = np.concatenate( [train_targets[:i * num_val_samples], train_targets[(i+1) * num_val_samples:]], axis=0) model = build_model() model.fit(partial_train_data, partial_train_targets, epochs=num_epochs, batch_size=1, verbose=0) val_mse, val_mae = model.evaluate(val_data, val_targets, verbose=0) all_scores.append(val_mae)
訓練中の検証スコアを記録してプロットする。グラフを見やすくするために、学習過程の最初の10回は無視して、また平滑化を行う。
だいたい80回前後くらいで過学習が始まって検証スコアが悪化を始めているようだ。
最終結果
上記までが学習回数の検討段階で、次で最終的なモデルの訓練と予測を実施する。学習エポック数を80回に固定して、モデルの学習を行う。
model = build_model() model.fit(train_data, train_targets, epochs=80, batch_size=16, verbose=0) test_mse_score, test_mae_score = model.evaluate(test_data, test_targets)
最終的な結果は、平均絶対誤差(Mean Absolute Error)の値で2550ドル程度…らしい。平均絶対誤差とは、予測の値と真の値の差を取り、差の絶対値を平均したもの。完璧に予測できていれば0となるので、これは小さければ小さいほど予測が正確であるということを意味する。
>>> test_mae_score
2.5532484335057877
これで3章の内容は終わりだけど、この結果だけでは予測過程がブラックボックスなので、次回はこの結果を可視化する方法を検討してみたい。