はじめに
予測値だけでなく「その予測にどれくらい自信がないか」を出したいことがあります。分位点回帰では予測区間(ばらつきの幅)を出しましたが、CatBoostはそのばらつきを2種類に分けて推定できます。
- データ由来(aleatoric):データそのもののノイズ。同じ
xでもyがばらつく領域で大きく、データを増やしても減らない - 知識由来(epistemic):モデルの経験不足。訓練データが少ない領域で大きく、データを増やせば減る
この記事では人工データでこの2つを実際に出し、それぞれが何に反応するかを見ます。catboost==1.2.10を使います。
人工データ
y = sin(x) に観測ノイズを乗せたデータを作ります。狙いは3つの領域を用意することです。
- 左側(
x < -1)はノイズ小、右側(x > 1)はノイズ大(ばらつきが場所で変わる=ヘテロ) -1 < x < 1は訓練データを置かない穴|x| > 4は訓練範囲の外(外挿)
import numpy as np
from catboost import CatBoostRegressor
rng = np.random.RandomState(0)
xl = rng.uniform(-4, -1, 150) # 左クラスタ
xr = rng.uniform(1, 4, 150) # 右クラスタ(間の (-1,1) は穴)
X = np.concatenate([xl, xr])[:, None]
sd = np.where(X[:, 0] < 0, 0.05, 0.35) # 左は低ノイズ、右は高ノイズ
y = np.sin(X[:, 0]) + rng.normal(0, sd)
作ったデータを散布図にすると次のようになります。左のクラスタはばらつきが小さく、右は大きく、中央(-1〜1)にはデータがありません。
データ由来の不確実性
loss_function='RMSEWithUncertainty'を指定すると、各点について平均と分散の2つを予測します。predictの戻り値が(n, 2)になり、1列目が平均、2列目がデータ由来の分散です。
model = CatBoostRegressor(loss_function="RMSEWithUncertainty", posterior_sampling=True,
iterations=1000, learning_rate=0.03, depth=4,
random_seed=0, verbose=0)
model.fit(X, y)
学習したら、代表4点 x = -3(低ノイズ域), 0(穴), 3(高ノイズ域), 5.5(外挿)で予測してみます。predictの戻り値は(n, 2)で、1列目が平均、2列目がデータ由来の分散です。
X_test = np.array([[-3.0], [0.0], [3.0], [5.5]])
pred = model.predict(X_test) # shape (n, 2)
mean, data_var = pred[:, 0], pred[:, 1]
print(pred)
[[-0.168 0.002]
[-0.226 0.297]
[ 0.12 0.078]
[-0.793 0.103]]
分散(2列目)を見ると、低ノイズ域(0.002)に比べて高ノイズ域(0.078)や穴(0.297)で大きくなっています。平均 ± 1.96×√分散 を帯にすると、ノイズの大きさに応じて帯の幅が変わります。
左(低ノイズ)では帯が細く、右(高ノイズ)では太くなっています。データ由来の不確実性(標準偏差に直した値)を領域ごとに平均すると、仕込んだノイズ(左0.05・右0.35)をおおよそ復元できていました(シード10通りの平均)。
領域 data由来(std)
low-noise (x<-1) 0.068
high-noise (x>1) 0.293
知識由来の不確実性
知識由来は、posterior_sampling=Trueで学習したモデルから仮想アンサンブルを作って推定します。1つのモデルの木を複数のグループに分け、グループ間で予測がどれだけばらつくかを見る方法です。virtual_ensembles_predictにprediction_type='TotalUncertainty'を渡すと、(n, 3)=平均・知識由来・データ由来が返ります。
ve = model.virtual_ensembles_predict(X_test, prediction_type="TotalUncertainty",
virtual_ensembles_count=20)
mean, know_var, data_var = ve[:, 0], ve[:, 1], ve[:, 2]
print(ve)
[[-0.171 0. 0.002]
[-0.108 0.004 0.349]
[ 0.116 0. 0.088]
[-0.777 0. 0.101]]
左から平均・知識由来・データ由来です。2列目(知識由来)は穴の行(x=0の0.004)だけ大きく、他はほぼ0です。データ由来(3列目)が穴と高ノイズ域の両方で大きいのとは対照的で、知識由来は「データの有無」にだけ反応しています。この知識由来(標準偏差)をxに沿って描くと、訓練データの穴(-1〜1)ではっきり盛り上がります。データの無い領域でアンサンブルの意見が割れるからです。
領域ごとの平均でも、穴の知識由来は密な領域の5倍ほどでした。
領域 know由来(std)
low-noise (x<-1) 0.0064
high-noise (x>1) 0.0094
gap (-1..1) 0.0320 ← 穴で上昇
ノイズの大きい右側(高ノイズ)でも知識由来は小さいままです。知識由来は「ノイズの大小」ではなく「データの有無」に反応する、という切り分けができています。
外挿では効かない(木モデルの限界)
一方、訓練範囲の外(外挿)では知識由来が増えません。上の図でも|x| > 4の外挿域は、境界の値のまま横ばいになっています。
これは決定木の性質によるものです。木は範囲外を最も端の葉の値(定数)で外挿するため、どれだけ遠くへ行っても予測が変わりません。仮想アンサンブルの各グループも同じ定数を返すので意見が割れず、知識由来は境界の値で凍りついたまま増えないのです。データ由来も同様に境界の値のまま一定になります。
先ほどの生出力でも、外挿のx=5.5の行([-0.777, 0., 0.101])は知識由来が0で、範囲内のx=3の行([0.116, 0., 0.088])と変わりません。図でも外挿域は完全に横ばいになっています。
ガウス過程やニューラルネットなら外挿で不確実性が発散しますが、木ベースの手法では**「訓練データから遠い」ことをこの不確実性では検知できない**点に注意が必要です。外挿の検出をしたい場合は、入力が訓練分布から外れていないかを別途チェックする(例:特徴量ごとの学習時の値域を持っておく)などの対策が要ります。
注意点
predictが(n, 2)を返すのはloss_function='RMSEWithUncertainty'のときだけです。通常のRMSEでは1列(平均のみ)です。- 知識由来(仮想アンサンブル)を使うには、学習時に
posterior_sampling=Trueが必要です。 - 知識由来が反応するのは主に訓練範囲の内側の穴です。外挿域では境界値で頭打ちになり、増えません。
- 知識由来の絶対値はデータ由来よりかなり小さく出ます。両者を足し引きするより、「データ由来=ノイズ」「知識由来=データの薄い内側領域」と役割で見るのがわかりやすいです。
まとめ
- CatBoostは
RMSEWithUncertaintyで予測の不確実性を出せ、predictが平均とデータ由来分散を返す posterior_sampling=True+virtual_ensembles_predictで、データ由来と知識由来に分解できる- データ由来はノイズの大小をきれいに捉えた(仕込んだ0.05/0.35をほぼ復元)
- 知識由来は訓練データの穴で上昇するが、外挿では境界値で頭打ちになり増えない(木は範囲外を定数で外挿するため)。外挿の検出は別手段が必要