K_DM Book
用語集
対象者 標準
対象者別の入口 →

対象者の設定はこのブラウザの中にだけ保存されます。

言語 JA

勾配ブースティングの可視化

中級

02.4.7

勾配ブースティングの可視化

最終更新 2020-04-08 読了時間 4 分
まとめ
このページでは、勾配ブースティングの最終予測だけでなく「木が 1 本足されるたびの予測」を図にします。GradientBoostingRegressor を木 10 本・学習率 0.5 で学習し、reg.estimators_[i][0].predict(X) に学習率を掛けた値を、平均値の初期モデルの上に棒グラフとして積み上げると、各木がどの区間の残差を埋めたかが見えます。1 本目は大きなうねりを、2〜3 本目は細かいずれを直し、5 本目で sin 曲線にかなり近づきます。読み終えると、n_estimators と learning_rate が「何本の棒を、どれだけの高さで積むか」の指定だと図で説明できるようになります。

疑似残差と加法モデルの式は 勾配ブースティング で説明しています。このページはその式 \(F_m = F_{m-1} + \nu h_m\) の各項を 1 本ずつ図に描くので、式を先に見ておくと棒グラフの意味がすぐ分かります。

直感 #

貯金の残高グラフを思い浮かべてください。最初に初期残高の線があり、毎月の入金が棒グラフとして上に積み上がり、棒の天井をつないだ線が現在の残高です。勾配ブースティングの予測もこれと同じで、初期残高が全データの平均値 \(F_0\)、毎月の入金が「\(i\) 本目の木の出力 × 学習率 0.5」、残高の線が最終予測 \(F_{10}\) です。違うのは、入金額が \(x\) の位置ごとに異なり、マイナスの入金(予測を下げる)もあることです。このページでは reg.estimators_ から 10 本の木を 1 本ずつ取り出し、その「入金」を棒グラフで積み上げて、赤い最終予測線がどう組み立てられたかを目で確かめます。

何が起きているか(ステップで見る) #

コードを読む前に、この手法が図の上で何をしているかを追ってみましょう。「再生」を押すか、スライダーを動かすと 1 手順ずつ進みます。

見るポイント:灰色の線が前の段階。1 段階で直されるのは残差(縦線)が大きい場所から。

式で見ると棒グラフの 1 本は何か #

加法モデル #

勾配ブースティングは加法モデルとして定式化されます。$M$ 本の木を逐次追加して予測関数を構築し、下のコードでは $M = 10$、$\nu = 0.5$ です:

$$F_M(x) = F_0(x) + \sum_{m=1}^{M} \nu \cdot h_m(x)$$

ここで $F_0(x) = \arg\min_c \sum_i L(y_i, c)$(二乗誤差の回帰なら訓練データの平均値)、$\nu$ は学習率です。後の図では $F_0$ が灰色の破線、和の各項 $\nu \cdot h_m(x)$ が 1 本ずつの棒に対応します。

残差の更新 #

各ステージ $m$ で、前段までの予測 $F_{m-1}(x)$ に対する擬似残差を計算します:

$$r_i^{(m)} = -\left[\frac{\partial L(y_i, F(x_i))}{\partial F(x_i)}\right]_{F = F_{m-1}}$$

二乗損失 $L = \frac{1}{2}(y - F)^2$ の場合、擬似残差は単純な残差になります:

$$r_i^{(m)} = y_i - F_{m-1}(x_i)$$

更新ステップ #

新しい木 $h_m$ を擬似残差 ${r_i^{(m)}}$ に対してフィットし、モデルを更新します:

$$F_m(x) = F_{m-1}(x) + \nu \cdot h_m(x)$$

学習率 $\nu \in (0, 1]$ は各木の寄与を縮小し、過学習を抑制します。$\nu = 0.5$ なら木が「残差をすべて埋める」出力を返しても、実際に足されるのは半分です。$\nu$ が小さいほど多くの木が必要ですが、汎化性能は向上する傾向があります。


Python で確かめる #

以下では、まず sin 波に 10 本の木を当てて最終予測を描き、次に estimators_ から木を 1 本ずつ取り出して寄与を棒グラフで積み上げ、最後に 1 本目から 5 本目まで「途中の予測」を順に並べます。

1
2
3
import numpy as np
import matplotlib.pyplot as plt
from sklearn.ensemble import GradientBoostingRegressor

1. 学習と最終予測の確認 #

まず $y = 10 \sin x + 10$ に 0〜10 の一様ノイズを足した 500 点に、木 10 本・学習率 0.5 の勾配ブースティング回帰を学習し、最終的な予測曲線と初期モデル(平均値)の線を重ねます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
n_samples = 500
X = np.linspace(-10, 10, n_samples)[:, np.newaxis]
noise = np.random.rand(X.shape[0]) * 10
y = (np.sin(X).ravel()) * 10 + 10 + noise

# モデル作成
n_estimators = 10
learning_rate = 0.5
reg = GradientBoostingRegressor(
    n_estimators=n_estimators,
    learning_rate=learning_rate,
)
reg.fit(X, y)

# 予測
y_pred = reg.predict(X)

# 可視化
plt.figure(figsize=(20, 10))
plt.scatter(X, y, c="k", marker="x", label="訓練データ")
plt.plot(X, y_pred, c="r", label="最終予測", linewidth=1)
plt.axhline(y=np.mean(y), color="gray", linestyle=":", label="初期モデル(平均値)")
plt.xlabel("x"); plt.ylabel("y")
plt.title("勾配ブースティングの最終予測")
plt.legend(); plt.show()

まずは簡単なデータに対して勾配ブースティング回帰を学習し、最終的な予測曲線を確認しますの図

灰色の破線が「初期モデル」で、$y$ の平均値だけを返す予測です。赤い線が 10 本の木を足し合わせた最終予測で、平均値から出発して木を 1 本追加するごとに sin 波へ近づいた結果です。次の節でその 10 回の追加を 1 本ずつ分解します。


2. 木ごとの寄与を積み上げて表示 #

次に「各木がどれだけ予測を修正したか」を棒グラフで積み上げます。reg.estimators_[i][0] が $i$ 本目の木で、その predict(X) に学習率 0.5 を掛けた値が式の $\nu \cdot h_m(x)$ です。平均値 $F_0$ の上にこれを 10 段積むと、棒の天井が赤い最終予測と一致するはずです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fig, ax = plt.subplots(figsize=(20, 10))
temp = np.zeros(n_samples) + np.mean(y)  # 初期モデルは平均値

for i in range(n_estimators):
    # i本目の木の予測値 × learning_rate が寄与部分
    res = reg.estimators_[i][0].predict(X) * learning_rate
    ax.bar(X.flatten(), res, bottom=temp, label=f"{i+1} 本目の木", alpha=0.05)
    temp += res  # 累積して次へ

# データと最終予測を重ねる
plt.scatter(X.flatten(), y, c="k", marker="x", label="訓練データ")
plt.plot(X, y_pred, c="r", label="最終予測", linewidth=1)
plt.xlabel("x"); plt.ylabel("y")
plt.title("木ごとの寄与を積み上げた可視化")
plt.legend(); plt.show()

次に「各木がどれだけ予測を修正したか」を棒グラフで積み上げますの図

薄い棒 1 段が「1 本の木がどれだけ予測を修正したか」で、山の区間では上向き、谷の区間では下向きに伸びています。10 段を累積すると棒の天井が赤い予測曲線と一致します。学習率 0.5 を掛けているので、各木は残差の半分だけを埋め、残りは次の木に持ち越されています。


3. 途中までの積み上げ(段階的な改善) #

さらに「木を 1 本、2 本、……、5 本まで足した時点」の予測を 5 枚の図で順に描きます。1 枚目の赤線が $F_1$、5 枚目が $F_5$ です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
for i in range(5):
    fig, ax = plt.subplots(figsize=(20, 10))
    plt.title(f"{i+1} 本目までの寄与で作られた予測")
    temp = np.zeros(n_samples) + np.mean(y)

    for j in range(i + 1):
        res = reg.estimators_[j][0].predict(X) * learning_rate
        ax.bar(X.flatten(), res, bottom=temp, label=f"{j+1} 本目", alpha=0.05)
        temp += res

    # データと予測を描画
    plt.scatter(X.flatten(), y, c="k", marker="x", label="訓練データ")
    plt.plot(X, temp, c="r", linewidth=1.2, label="途中の予測")
    plt.xlabel("x"); plt.ylabel("y")
    plt.legend(); plt.show()

さらに「木を5本まで足した時点」での予測を順に可視化しますの図

さらに「木を5本まで足した時点」での予測を順に可視化しますの図

さらに「木を5本まで足した時点」での予測を順に可視化しますの図

さらに「木を5本まで足した時点」での予測を順に可視化しますの図

さらに「木を5本まで足した時点」での予測を順に可視化しますの図

さらに「木を5本まで足した時点」での予測を順に可視化しますの図

1 本目の木は平均値からの大きなずれ、つまり sin 波の山と谷のおおまかな位置を 8 段の階段で補正します。2〜3 本目は 1 本目が取り残した細かいずれを埋め、5 本目の時点で赤線は sin 曲線にかなり近づきます。残り 5 本を足すと前節の赤い最終予測になります。学習率が 0.5 なので、1 本目の階段の高さは残差のちょうど半分で、これが「一歩ずつ慎重に直す」の実体です。

まとめ #

勾配ブースティングの最終予測は、平均値 $F_0$ の上に「$i$ 本目の木の出力 × 学習率」を $M$ 段積んだ和 $F_M = F_0 + \sum_m \nu h_m$ で、reg.estimators_[i][0].predict(X) * learning_rate を bottom=temp の棒グラフとして積むとその式を図にできます。木 10 本・学習率 0.5 の例では、1 本目が sin 波の大まかな形を、2〜5 本目が細かいずれを埋め、棒の天井が reg.predict(X) の赤線と一致しました。n_estimators は棒の段数、learning_rate は各段の高さの倍率と読めるので、学習率を 0.1 にするなら段数を 5 倍の 50 本前後にする、という調整の意味が図から説明できます。


推定器数と予測 #

スライダーで n_estimators を 1 本から 200 本まで動かすと、このページで 5 本まで追った「途中の予測」がさらに先まで続き、train_mse が下がり続ける一方で test_mse がある本数から上がり始める様子を確認できます。