線形基底関数モデル④ ホールドアウト検証

線形基底関数モデル④ ホールドアウト検証

オーバーフィッティング

 次の図は、前回記事 のランダム・ガウス基底のコードで、試行回数NT (Number of Trials) を 1000 回に設定して描いた近似曲線です。

オーバーフィッティング問題 NT=1000

 標準偏差 SD は 2.874 となり、NT = 10 のときよりも大幅に下がっています。しかし、波打つような曲線になっていることには違和感を覚えます。直感的には NT = 10 のモデルのほうがよさそうに思えます。

 これはデータのばらつきを曲線で表そうと頑張り過ぎた結果です。回帰のアルゴリズムは標準偏差(あるいは平均2乗誤差)を減らす方向へパラメータを調整し続けます。つまり、すべての点を近似曲線に乗せるように調整を繰り返すので、最終的には湾曲部の多い不自然な曲線になってしまいます。このような現象を 過学習(オーバーフィッティング : over-fitting)といいます。オーバーフィッティング現象は単純に基底関数を増やした場合には、より顕著な形で現れます。
 

独学プログラマー Python言語の基本から仕事のやり方まで

中古価格
¥1,537から
(2019/7/30 20:25時点)

ホールドアウト検証

 オーバーフィッティングの問題を解消する方法の1つがデータの分割です。以下のコードでは、データの 4 分の 3 を 訓練データ、残りの 4 分の 1 を テストデータ として分け、訓練データのみを使って NT = 10 でパラメータを最適化した結果を表示します。こちらの記事に掲載している Fit_func クラス を実装して、以下のコードを試してみてください。

# https://python.atelierkobato.com/over-fit/

# 線型基底関数モデル リストM3-B-5

# 入力データベクトル
x = np.array([35, 16, 22, 43, 5,
              66, 20, 13, 52, 1,
              39, 62, 45, 33, 8,
              28, 71, 24, 18, 3])

# 目標データベクトル
y = np.array([85.19, 58.93, 64.27, 68.91, 21.27,
              68.88, 60.07, 55.18, 88.08, 8.89,
              82.31, 81.18, 80.76, 78.98, 37.55,
              75.9, 72.39, 69.51, 62.04, 12.47])

# テストデータ
x_test = x[:int(len(x)/4)]
y_test = y[:int(len(y)/4)]

# 訓練データ
x_train = x[int(len(x)/4):]
y_train = y[int(len(y)/4):]

# ガウス関数を定義
def gauss(x, mu = 0, sigma = 1):
    return np.exp(-(x - mu)**2 / (2 * sigma**2))

# 標準偏差sdの初期値
sd = 10.0

# 乱数シードを固定
np.random.seed(10)

# 試行回数(Number of trials)を設定
nt = 10

# 乱数を使った基底関数の調整
for k in range(nt):
    rdmu = 20 * np.random.rand(5) - 10
    rdsg = 10 * np.random.rand(5) - 5
    
    def phi_0(x):
        return gauss(x, rdmu[0], 10 + rdsg[0])
    
    def phi_1(x):
        return gauss(x, 20 + rdmu[1], 10 + rdsg[1])
    
    def phi_2(x):
        return gauss(x, 40 + rdmu[2], 10 + rdsg[2])

    def phi_3(x):
        return gauss(x, 60 + rdmu[3], 10 + rdsg[3])
    
    def phi_4(x):
        return gauss(x, 80 + rdmu[4], 10 + rdsg[4])
    
    def phi_5(x):
        return 1

    # 基底関数のリスト
    func_list = [phi_0, phi_1, phi_2, phi_3, phi_4, phi_5]
    
    # Fit_funcオブジェクトを作成
    w = Fit_func(x_train, y_train, func_list)

    # 標準偏差と基底関数リストの更新
    if w.sd() < sd:
        sd = w.sd()
        bf_list = func_list

# 標準偏差の値を丸める
sd = np.round(sd, 3)

# 採用された基底関数でFit_funcオブジェクトを作成
z = Fit_func(x_train, y_train, bf_list)

# 平均2乗誤差と標準偏差
mse_test = np.mean((z.line(x_test)-y_test)**2)
sd_test = np.sqrt(mse_test)
sd_test = np.round(sd_test, 3)

# params()メソッドで最適化されたパラメータを取得
prm = np.round(z.params(), 3)

# Figureを作成
fig = plt.figure(figsize = (8, 6))

# FigureにAxesを1つ追加
ax = fig.add_subplot(111)

# Axesのタイトルを設定
ax.set_title("Random Gaussian base", fontsize = 16)

# テキストボックス書式辞書を作成
boxdic = {"facecolor" : "white", "edgecolor" : "gray",}

# テキストボックスを表示
ax.text(48, 10, "NT = {}\nSD_train = {}\nSD_test = {}"
        .format(nt,sd,sd_test),
        size = 14, linespacing = 1.5, bbox = boxdic)

# 目盛線を表示
ax.grid()

# 軸範囲を設定
ax.set_xlim([0, 70])
ax.set_ylim([0, 100])

# 軸ラベルを設定
ax.set_xlabel("Age", fontsize = 14)
ax.set_ylabel("Weight [kg]", fontsize = 14)

# 身長と体重データの散布図
ax.scatter(x_train, y_train, color = "blue", label = "training_data")
ax.scatter(x_test, y_test, color = "darkorange", label = "test_data")

# line()メソッドを用いて近似曲線を描画
x2 = np.linspace(0, 70, 100)
y2 = z.line(x2)
ax.plot(x2, y2, color = "red")

# 凡例を左上に表示
ax.legend(loc = "upper left")

plt.show()

Python ホールドアウト検証(訂正図)NT=10

 青色の点が訓練データ、オレンジ色の点がテストデータです。
 右下に表示されている SD_train が近似曲線と訓練データで計算した標準偏差、SD_test は近似曲線とテストデータで計算した標準偏差です。上のコードを NT = 1000 に変えて実行すると次のようになります。

ホールドアウト検証(訂正図)NT=1000

 試行回数が増えているので、SD_train は大きく下がっていますが、SD_test は逆に開いてしまっています。つまり、NT = 10 のほうが、NT = 1000 よりも優れたモデルであると推測できます。そしておそらく、NT = 11 から NT = 999 の中に、さらに SD_test が小さいモデルが存在していたはずです。

 以上の考察から、「訓練データでパラメータベクトル p を最適化した近似関数を作り、テストデータを使って標準偏差 SD_test を計算する」試行を繰り返し、その中からSD_test が最小になる基底関数の組合わせを採用すれば、試行回数を増やすほど予測精度の高いモデルを構築できることになります。

ホールドアウト検証の概念図

 このように、訓練に用いなかった未知のデータを使って予測精度を評価する手法を ホールドアウト検証 とよびます。

 次のコードサンプルを実行すると、NT = 6000 で交差検証を行って近似曲線を表示します。

# https://python.atelierkobato.com/over-fit/

# 線型基底関数モデル リストM3-B-6

# 入力データ
x = np.array([35, 16, 22, 43, 5,
              66, 20, 13, 52, 1,
              39, 62, 45, 33, 8,
              28, 71, 24, 18, 3])

# 目標データ
y = np.array([85.19, 58.93, 64.27, 68.91, 21.27,
              68.88, 60.07, 55.18, 88.08, 8.89,
              82.31, 81.18, 80.76, 78.98, 37.55,
              75.9, 72.39, 69.51, 62.04, 12.47])

# テストデータ
x_test = x[:int(len(x)/4)]
y_test = y[:int(len(y)/4)]

# 訓練データ
x_train = x[int(len(x)/4):]
y_train = y[int(len(y)/4):]

# ガウス関数を定義
def gauss(x, mu = 0, sigma = 1):
    return np.exp(-(x - mu)**2 / (2 * sigma**2))

# 標準偏差sd_testの初期値
sd_test = 10.0

# 乱数シードを固定
np.random.seed(10)

# 試行回数(Number of trials)を設定
nt = 6000

# 乱数を使った基底関数の調整
for k in range(nt):
    rdmu = 20 * np.random.rand(5) - 10
    rdsg = 10 * np.random.rand(5) - 5
    
    def phi_0(x):
        return gauss(x, rdmu[0], 10 + rdsg[0])
    
    def phi_1(x):
        return gauss(x, 20 + rdmu[1], 10 + rdsg[1])
    
    def phi_2(x):
        return gauss(x, 40 + rdmu[2], 10 + rdsg[2])

    def phi_3(x):
        return gauss(x, 60 + rdmu[3], 10 + rdsg[3])
    
    def phi_4(x):
        return gauss(x, 80 + rdmu[4], 10 + rdsg[4])
    
    def phi_5(x):
        return 1

    # 基底関数のリスト
    func_list = [phi_0, phi_1, phi_2, phi_3, phi_4, phi_5]
    
    # Fit_funcオブジェクトを作成
    w = Fit_func(x_train, y_train, func_list)
    
    # テストデータの平均2乗誤差
    m = np.mean((w.line(x_test) - y_test)**2)

    # テストデータの標準偏差
    s = np.sqrt(m)
    s = np.round(s, 3)

    # 標準偏差と基底関数リストの更新
    if s < sd_test:
        sd_test = s
        bf_list = func_list

# 採用された基底関数でFit_funcオブジェクトを作成
z = Fit_func(x, y, bf_list)

# Figureを作成
fig = plt.figure(figsize = (8, 6))

# 訓練データの標準偏差
sd = np.round(z.sd(), 3)

# FigureにAxesを1つ追加
ax = fig.add_subplot(111)

# Axesのタイトルを設定
ax.set_title("Random Gaussian base", fontsize = 16)

# テキストボックス書式辞書を作成
boxdic = {"facecolor" : "white", "edgecolor" : "gray",}

# テキストボックスを表示
ax.text(48, 10, "NT = {}\nSD = {}\nSD_test = {}"
        .format(nt, sd, sd_test),
        size = 14, linespacing = 1.5, bbox = boxdic)

# 目盛線を表示
ax.grid()

# 軸範囲を設定
ax.set_xlim([0, 70])
ax.set_ylim([0, 100])

# 軸ラベルを設定
ax.set_xlabel("Age", fontsize = 14)
ax.set_ylabel("Weight [kg]", fontsize = 14)

# 身長と体重データの散布図
ax.scatter(x_train, y_train, color = "blue")

# line()メソッドを用いて近似曲線を描画
x2 = np.linspace(0, 70, 100)
y2 = z.line(x2)
ax.plot(x2, y2, color = "red")

plt.show()

ホールドアウト検証 NT=6000

 ただし、これは1つの分割方法によって得られた結果なので、まだ完全に信用することはできません。次回記事では結果がデータの分割方法に依存しないようにプログラムを改良します。