PythonとKerasによるディープラーニング 第3章(3)回帰
Boston Housingデータセットを用いた住宅価格予測を行う。
ここでの学び
- データの正規化(入力データ行列の列ごとに平均を引く&標準偏差で割る→中心0,標準偏差1)
- 小さいネットワークを使ったほうが、過学習を抑制できる。訓練データが少ない=過学習に陥りやすいので、ネットワークはムダに大きくしない。
- スカラー回帰(連続値を1つだけ予測する回帰)は、最終層ユニットは1,活性化関数は使わない。sigmoidなどの活性化関数を使うと値が0〜1に制限されてしまうので不適切。
- クロスバリデーションを使う場合のワークフロー(データ数少ないときにやる。CVは平均をとって精度を確認するためのもの。そのあと、trainデータ全部で本番の学習をして、testで検証する。)
Boston Housingデータセットは、1970年代中頃のボストン近郊での住宅価格に関するデータセットである。
説明変数をみると、trainの形状は (404, 13)、testの形状は(102, 13)となっており、13種類の特徴量がある。(犯罪発生率、1戸あたりの平均部屋数、幹線道路へのアクセス指数など)
また、これまでのIMDb, ロイターと比較してデータ数が少ない。
目的変数をみると、floatで数値が1つずつ格納されているndarrayになっている。この数値が、住宅価格を表す。
データの正規化
mean = train_data.mean(axis=0) train_data -= mean std = train_data.std(axis=0) train_data /= std test_data -= mean test_data /= std
特徴量ごとにデータの範囲がバラバラなのでそろえる。
trainの列ごとの平均を引く&列ごとの標準偏差で割ることで、特徴量の中心が0、標準偏差が1になる。
testも、trainで算出したmean, stdを使って同様の処理をする。ここで一瞬納得できなかったのだが、機械学習のワークフローにおいては、テストデータを使って計算した値は一切使ってはダメなので、このようにしている。
ネットワーク
ここで、今回はデータ数が少ないことに留意する。
小さいネットワークを使ったほうが、過学習を抑制できる。訓練データが少ない=過学習に陥りやすいときは、ネットワークはムダに大きくしない。
def build_model(): model = keras.Sequential([ layers.Dense(64, activation="relu"), layers.Dense(64, activation="relu"), layers.Dense(1) ]) model.compile(optimizer="rmsprop", loss="mse", metrics=["mae"]) return model
また、今回はスカラー回帰(連続値を1つだけ予測する回帰)なので、最終層ユニットは1,活性化関数は使わない。
なぜ活性化関数を使わないかと言うと、sigmoidなどの活性化関数を使うと値が0〜1に制限されてしまうので不適切だからである。
回帰なので損失関数はMSE(平均二乗誤差)。訓練時には、MAE(平均絶対誤差)も監視している。MAEは、予測値と目的値の差の絶対値。
K分割交差検証
これも、学習データが少なく、過学習に陥りやすいときに行う手法である。
クロスバリデーションをする意義は、train, validの分割の仕方によるブレが生じることが予想されるので、何パターンかやって全平均をとった結果を見て、モデルがちゃんと汎化性能を出せているか確認する、ということである。
以下、サンプルコード。
CVの分割数ぶんforをまわして、各回でtrain/val分割→モデル初期化・学習(fit)→MAEをhistoryからリストに格納、という流れをおこなっている。
最後に、各エポックにおけるCV間平均をとっている。これを可視化してモデルの汎化性能を確認する。
num_epochs = 500 all_mae_histories = [] for i in range(k): print(f"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() history = model.fit(partial_train_data, partial_train_targets, validation_data=(val_data, val_targets), epochs=num_epochs, batch_size=16, verbose=0) # 今回のCVにおけるMAEをhistoryからとってきて、リストに格納(次のCVで上書きされてしまうから) mae_history = history.history["val_mae"] # all_mae_historiesは最終的に、len500のリストが4つ格納されたリストになる。 all_mae_histories.append(mae_history) # 全CVにおける同じエポック数での平均をとる average_mae_history = [ np.mean([x[i] for x in all_mae_histories]) for i in range(num_epochs)]
なんだか未だにCVの意義については一瞬混乱しそうになるのだが、CVの結果を可視化して、だいたいどのあたりから過学習気味になっていくかを確認して、本番のモデル学習を行う。 今度はtrainを分割せずに全部使って、評価をtestデータで行う。
model = build_model() model.fit(train_data, train_targets, epochs=130, batch_size=16, verbose=0) test_mse_score, test_mae_score = model.evaluate(test_data, test_targets)