導入
機械学習モデルは学習データの範囲内では高精度でも、範囲外に外挿した瞬間に挙動が変わることがあります。本記事の目的は、木ベースのアンサンブルである LightGBM と線形回帰を比較し、「内挿に強く外挿に弱い」という性質を、温度と反応速度の最小実験で視覚的に確認することです。短いコードで再現できるため、手元で動かしながら直感と理解を一致させていきます。
TL;DR
-
20–40℃の狭い範囲で学習し、0–100℃で検証する実験を作成しました。
-
LightGBM は内挿では高精度ですが、外挿では値が伸びず飽和しやすくなります。
-
線形回帰は直線で外挿できる一方、真の関数が高温側で超線形に増えるため過小予測になります。
-
図と誤差で、内挿に強く外挿に弱いがはっきり確認できます(ノートブックと画像を公開)。
実験設計(データとモデル)
本実験では、横軸に 0–100℃の温度、縦軸に約 0–100 に正規化した反応速度を取り、真の関数には「直線+弱い指数項」を用いて高温ほど加速する形を与えます。学習データは 20–40℃の範囲に限定し、実測らしさを出すために小さなガウスノイズを付与します。これに対して、特徴量は温度のみとし、線形回帰とLightGBM(GBDT)の2モデルを学習します。評価は 0–100℃全域で行い、学習域(内挿)と学習域外(外挿)に分けて比較します。
図1:真の関数と学習データ
下図は真の関数と、20–40℃のノイズ付き学習データです。高温側で真の関数が線形を超えて増加する一方、学習データは狭い帯域に集中しています。ここで意図したのは、学習域では整合的だが、外挿ではモデル仮定の違いが露呈するという状況をつくることです。
図2:内挿 vs 外挿(予測曲線)
次に 0–100℃全域での予測を比較します。内挿域の 20–40℃では両モデルとも真の関数に沿い、学習がうまくいっていることがわかります。一方で外挿に出ると挙動が分かれます。LightGBM は葉ノードの定数出力の合成であるため、学習範囲の外側では値が張り付いて一定になりやすく、伸びが止まりがちです。対して線形回帰は直線仮定により外挿できますが、真の関数が高温側で加速するため、過小予測になります。
図3:絶対誤差の比較
誤差の観点からも性質は明瞭です。内挿域では両者の誤差は小さいのに対し、外挿では LightGBM の誤差が急増します。線形回帰は一様に増える傾向を示し、直線外挿の限界が観察できます。これらは「学習域内では強いが、学習域外の予測はモデル仮定に強く依存する」という教訓を裏付けます。
どうして LightGBM は外挿が苦手なのか
LightGBM は多数の浅い決定木を勾配ブースティングで足し合わせる手法です。各木の出力は区分定数であり、特徴量空間は分割と葉によって段差状に表現されます。このため、学習データが存在しない領域では、新たな分割が作られず、最も近い境界の葉値に張り付く傾向が生じます。結果として、未知領域で連続的・単調に値が伸びるような外挿は、原理的に苦手になりやすいのです。
実務での対策と示唆
実務では、学習域外の予測が必要な場面を避けられないことがあります。その場合は、まず特徴量設計や前処理でドメイン知識を導入します。たとえば反応速度なら Arrhenius 型の変換(温度の逆数や対数)を検討すると、関係が線形化され外挿が安定することがあります。また、評価設計として学習域外テストを必ず用意し、外挿誤差を可視化します。さらに、木モデルにこだわらず、仮定が明確なパラメトリックモデルやスプライン・ガウス過程などの選択肢も比較対象に入れると意思決定が堅牢になります。
最小実行コード
# !pip -q install lightgbm
import numpy as np, matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from lightgbm import LGBMRegressor
from sklearn.metrics import mean_squared_error
import os; os.makedirs("fig", exist_ok=True)
def build_true_function(a=8.0, b=0.03):
xg = np.linspace(0, 100, 1001)
yr = xg + a*(np.exp(b*(xg-60))-1)
mn, mx = yr.min(), yr.max()
return lambda x: 100*( (np.asarray(x)+a*(np.exp(b*(np.asarray(x)-60))-1)) - mn )/(mx-mn)
f = build_true_function()
x_val = np.linspace(0,100,501); y_true = f(x_val)
n=80; x_tr = np.random.uniform(20,40,n); y_tr = f(x_tr)+np.random.normal(0,1,n)
lin = LinearRegression().fit(x_tr.reshape(-1,1), y_tr)
lgb = LGBMRegressor(n_estimators=500, learning_rate=0.05, random_state=42).fit(x_tr.reshape(-1,1), y_tr)
y_lin = lin.predict(x_val.reshape(-1,1)); y_lgb = lgb.predict(x_val.reshape(-1,1))
# fig1
plt.figure(figsize=(7,4.5))
plt.plot(x_val,y_true,label="True function",lw=2)
plt.scatter(x_tr,y_tr,label="Train (noisy)",alpha=0.7)
plt.axvspan(20,40,alpha=0.12,label="Train domain [20, 40]")
plt.title("Ground truth and training data"); plt.xlabel("Temperature (C)"); plt.ylabel("Reaction rate (~0-100)")
plt.legend(); plt.grid(True); plt.savefig("fig/fig1_ground_truth.png",dpi=150,bbox_inches="tight"); plt.show()
# fig2
plt.figure(figsize=(7,4.5))
plt.plot(x_val,y_true,label="True function",lw=2)
plt.plot(x_val,y_lin,label="Linear Regression",ls="--")
plt.plot(x_val,y_lgb,label="LightGBM",ls=":")
plt.axvspan(20,40,alpha=0.12,label="Train domain [20, 40]")
plt.title("Interpolation vs Extrapolation"); plt.xlabel("Temperature (C)"); plt.ylabel("Predicted reaction rate")
plt.legend(); plt.grid(True); plt.savefig("fig/fig2_interp_vs_extrap.png",dpi=150,bbox_inches="tight"); plt.show()
# fig3
ae_lin = np.abs(y_lin - y_true); ae_lgb = np.abs(y_lgb - y_true)
plt.figure(figsize=(7,4.5))
plt.plot(x_val,ae_lin,label="Abs error: Linear Regression",ls="--")
plt.plot(x_val,ae_lgb,label="Abs error: LightGBM",ls=":")
plt.axvspan(20,40,alpha=0.12,label="Train domain [20, 40]")
plt.title("Absolute error across temperature"); plt.xlabel("Temperature (C)"); plt.ylabel("Absolute error")
plt.legend(); plt.grid(True); plt.savefig("fig/fig3_abs_error.png",dpi=150,bbox_inches="tight"); plt.show()
print("MSE(in, out): lin=", mean_squared_error(y_true[(x_val>=20)&(x_val<=40)], y_lin[(x_val>=20)&(x_val<=40)]),
mean_squared_error(y_true[(x_val<20)|(x_val>40)], y_lin[(x_val<20)|(x_val>40)]),
" lgb=", mean_squared_error(y_true[(x_val>=20)&(x_val<=40)], y_lgb[(x_val>=20)&(x_val<=40)]),
mean_squared_error(y_true[(x_val<20)|(x_val>40)], y_lgb[(x_val<20)|(x_val>40)]))
再現方法
本記事のすべてのコードと図を GitHub リポジトリにまとめています。requirements.txt を pip install -r で導入し、interp_vs_extrap_lightgbm.ipynb を開いて Run all すれば同じ図が再現できます。実験パラメータ(指数項の強さ、ノイズ量、学習点数)を変えれば、外挿の弱さがどの程度表に出るかを体感的に確認できます。
まとめ
本記事では、温度と反応速度の単純な回帰タスクを通じて、木ベースの LightGBM は内挿に強いが外挿に弱いこと、そして線形回帰は外挿できるが真の関数が非線形なら過小予測に陥ることを図と誤差で確かめました。実務ではドメイン知識を取り入れた特徴量や評価設計が不可欠であり、用途に応じてモデルの仮定を見直すことが重要です。読者の皆さんが、自分のデータにおける「内挿と外挿」を意識しながら、より信頼できる予測を設計する一助になれば幸いです。