zuminote

個人的な勉強記録

PythonとKerasによるディープラーニング 第3章(3)回帰

Boston Housingデータセットを用いた住宅価格予測を行う。

ここでの学び

  • データの正規化(入力データ行列の列ごとに平均を引く&標準偏差で割る→中心0,標準偏差1)
  • 小さいネットワークを使ったほうが、過学習を抑制できる。訓練データが少ない=過学習に陥りやすいので、ネットワークはムダに大きくしない。
  • スカラー回帰(連続値を1つだけ予測する回帰)は、最終層ユニットは1,活性化関数は使わない。sigmoidなどの活性化関数を使うと値が0〜1に制限されてしまうので不適切。
  • クロスバリデーションを使う場合のワークフロー(データ数少ないときにやる。CVは平均をとって精度を確認するためのもの。そのあと、trainデータ全部で本番の学習をして、testで検証する。)

github.com

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)