0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

XGBoostはなぜ予測できるのか ― 決定木からGradient BoostingまでをPythonで学ぶ【Part 1 / Lesson 01】

0
Posted at

XGBoostはなぜ予測できるのか ― 決定木からGradient BoostingまでをPythonで学ぶ【Part 1 / Lesson 01】

はじめに

XGBoostは、表形式データを扱う機械学習でよく使われる手法です。実際にプログラムを書いてみると、短いコードで分類モデルや回帰モデルを作れます。ただし、学習がどのように進んでいるのかを知らないままパラメータを変えても、結果の理由を説明するのは難しいものです。

このLessonでは、XGBoostの土台にあるBoostingとGradient Boostingを確認します。単一の決定木を出発点にして、小さな決定木を順番に増やすと予測がどのように変わるかをPythonで確かめます。

今回は難しい最適化の式を追いかけません。XGBoost特有の正則化や二階微分は次のLessonで取り上げます。まずは「今の予測を改善するために、次の木を追加する」という流れをつかむことが目標です。

この連載の全体構想は「Pythonで学ぶXGBoost ― 全体構想」にまとめています。

1. 今回の学習目標

このLessonを終えると、次のことができるようになります。

  1. 1本の決定木と、複数の木を組み合わせるBoostingの違いを説明する。
  2. 二乗誤差の回帰問題で、決定木を追加するたびに予測が更新される仕組みを追う。
  3. 決定木、AdaBoost、Gradient Boosting、XGBoostを同じデータで実行して評価する。
  4. Accuracy、ROC-AUC、Loglossの役割を大まかに区別する。

2. 決定木とは何か

決定木は、説明変数の値を条件にしてデータを分けながら予測するモデルです。例えば、単純化した設備点検の分類であれば、「温度が一定値より高いか」「振動が一定値を超えているか」といった条件で枝分かれします。

dtree_example_flow.png

※これは仕組みを説明するための模式図であり、設備の実際の判定基準ではありません。

条件の組み合わせを学習できるため、決定木は非線形な関係も表現できます。一方で、1本の木だけではデータを十分に表現できなかったり、深くしすぎると学習データに合わせすぎたりします。

そこで、1本のモデルにすべてを任せるのではなく、複数のモデルを組み合わせる方法を考えます。これをアンサンブル学習と呼びます。

3. Boostingとは何か

アンサンブル学習には、複数のモデルを独立に学習して結果をまとめる方法と、順番にモデルを学習する方法があります。Boostingは後者の代表例です。

Boostingでは、前に学習したモデルがうまく扱えなかった部分を、後から学習するモデルで改善していきます。「弱い学習器」と呼ばれる小さなモデルを複数組み合わせるのが基本的な考え方です。

AdaBoostは、間違えたデータを次の学習で重視するようにサンプルの重みを変更する方法です。一方、Gradient Boostingは、損失関数を小さくする方向に新しいモデルを追加していきます。どちらもBoostingですが、学習の進め方は同じではありません。

手法 次の学習で重視するもの このLessonでの役割
単一決定木 1本の木を作る 比較の基準
AdaBoost 主に誤分類されたデータの重みを調整 Boostingのもう一つの考え方
Gradient Boosting 損失関数の負の勾配を利用 XGBoostを理解する土台
XGBoost 勾配・二階微分・正則化などを使うBoosting これから学ぶ中心の手法

XGBoostで扱うGradient Boostingの流れを図にすると、次のようになります。

Gradient Boostingの学習イメージ

ここで大切なのは、前の木を作り直すのではなく、新しい木の出力を足し込むことです。

4. Gradient Boostingを数式で表す

Gradient Boostingでモデルを1回更新する考え方は、次の式で表せます。

F_m(x) = F_{m-1}(x) + \eta f_m(x)
  • $F_{m-1}(x)$:これまでに作ったモデルの予測値
  • $f_m(x)$:今回追加する小さな決定木が出す補正値
  • $\eta$:学習率。通常は小さな正の値を使います。
  • $F_m(x)$:新しい木を追加した後の予測値

いまの予測が「10」で、今回の木が「2」を補正値として出し、学習率が0.3なら、新しい予測は10 + 0.3 × 2 = 10.6です。

ただし、「どんな問題でも観測値と予測値の差をそのまま学習すればよい」わけではありません。二乗誤差を使う回帰問題では、残差を学習する考え方が勾配ブースティングと対応します。分類や別の損失関数では、損失の勾配を使う必要があります。この区別は覚えておきましょう。

5. 実習①:決定木を1本ずつ追加する

最初の実習では、回帰問題を使ってGradient Boostingを手作業に近い形で追います。学習にはscikit-learnのDecisionTreeRegressorを使いますが、木を追加する繰り返しの部分は自分で書きます。

使用するデータは160件の合成データです。波のように変化する値へ、小さなノイズを加えます。このデータは学習の仕組みを見るためだけのもので、実際の観測データではありません。

5.1 初期予測を作る

二乗誤差の回帰では、最初は目的変数の平均値を一律に予測するところから始められます。

# yは観測値を格納したNumPy配列です。
# np.full()で、すべての行に平均値を入れた予測配列を作ります。
current = np.full(len(y), float(np.mean(y)))

最初から波の形を当てることはできません。そのため、観測値との間に残差が残ります。

5.2 残差を決定木に学習させる

# 現在の予測が、各データに対してどれだけずれているかを計算します。
residual = y - current

# 残差の特徴を学ぶ、深さ2の小さな決定木を作ります。
tree = DecisionTreeRegressor(max_depth=2, random_state=42)
tree.fit(X, residual)

# 学習率を掛けて、決定木の出力を現在の予測に追加します。
current = current + 0.45 * tree.predict(X)

この一連の処理を何度か繰り返せば、前の予測を補正する決定木が増えていきます。

5.3 予測の変化をグラフで見る

決定木を追加したときの予測の変化

散布図が合成データの観測値で、折れ線が決定木を追加した段階の予測値です。初期予測はほぼ水平ですが、木が追加されると、データの形に近づく様子が分かります。

実際に計算した学習データ上の平均二乗誤差(MSE)は次のとおりです。

追加した木の数 学習MSE
0 0.18915
1 0.07920
2 0.03818
3 0.02163
4 0.01294

注意:この数値は学習データ上の誤差です。 木を増やすほど学習誤差が小さくなっても、初めて見るデータの予測精度まで改善するとは限りません。学習しすぎによる問題(過学習)は、後のLessonで扱います。

実習①の全Pythonコードを表示する
"""Lesson 01: 二乗誤差の勾配ブースティングを小さな決定木で追跡する。

学習の更新手順を理解するための教育用コードです。
XGBoost本体の実装を再現したものではありません。二乗誤差の回帰に限定して、
「残差を決定木で学習する → 学習率を掛けて予測に加える」操作を示します。
"""

from __future__ import annotations

import csv
from pathlib import Path

import matplotlib

matplotlib.use("Agg")  # 画面を持たないLinux・CI上でもPNGを書き出せるようにします。
import matplotlib.pyplot as plt
import numpy as np
from sklearn.metrics import mean_squared_error
from sklearn.tree import DecisionTreeRegressor

ROOT = Path(__file__).resolve().parents[1]
OUTPUT = ROOT / "figures"
RESULTS = ROOT / "results"


def create_sample_data(seed: int = 42) -> tuple[np.ndarray, np.ndarray]:
    """再現可能な1変数の回帰データを作成する。

    Parameters
    ----------
    seed:
        観測ノイズの乱数シード。同じ値なら同じ実習データになります。

    Returns
    -------
    (X, y):
        Xは(n_samples, 1)の説明変数、yは連続値の目的変数です。
    """
    rng = np.random.default_rng(seed)
    x = np.linspace(0.0, 1.0, 160)
    # 分かりやすい波形に少量の観測ノイズを加えます。実測データではありません。
    y = 0.35 + 0.70 * np.sin(2.0 * np.pi * x) + 0.35 * x
    y = y + rng.normal(loc=0.0, scale=0.09, size=len(x))
    return x.reshape(-1, 1), y


def fit_manual_boosting(
    X: np.ndarray,
    y: np.ndarray,
    *,
    n_rounds: int = 4,
    learning_rate: float = 0.45,
    max_depth: int = 2,
    seed: int = 42,
) -> tuple[list[np.ndarray], list[float]]:
    """残差を学習する小さな決定木を逐次的に加え、各段階を記録する。

    この教材では二乗誤差の回帰のみを扱います。一般の損失関数では、
    残差そのものではなく損失の負の勾配に基づく更新を考えます。

    Parameters
    ----------
    X:
        学習用の説明変数。
    y:
        学習用の目的変数。
    n_rounds:
        追加する決定木の数。0以上を指定します。
    learning_rate:
        1本の木が加える補正の割合。(0, 1]の範囲とします。
    max_depth:
        各決定木の最大深さ。
    seed:
        決定木の乱数シード。

    Returns
    -------
    predictions:
        初期予測と各反復終了時の予測のコピー(計n_rounds+1個)。
    mse_history:
        各段階の学習データ上の平均二乗誤差。
    """
    if n_rounds < 0:
        raise ValueError("n_roundsは0以上にしてください。")
    if not 0.0 < learning_rate <= 1.0:
        raise ValueError("learning_rateは0より大きく1以下にしてください。")
    if len(X) != len(y):
        raise ValueError("Xとyの行数が一致していません。")

    # 二乗誤差を最小化する定数の初期予測は、目的変数の平均です。
    # np.fullは同じ値の配列を作ります。毎反復で配列を更新していきます。
    current = np.full(shape=len(y), fill_value=float(np.mean(y)), dtype=float)
    predictions = [current.copy()]
    mse_history = [float(mean_squared_error(y, current))]

    for step in range(n_rounds):
        # 二乗誤差では「観測値 - 現在の予測」が改善方向を表します。
        # 残差へ小さな決定木を当て、どこで補正が必要かを学びます。
        residual = y - current
        tree = DecisionTreeRegressor(max_depth=max_depth, random_state=seed + step)
        tree.fit(X, residual)

        # 学習率を掛けることで、一度に大きく変更しすぎないようにします。
        # 以前の木の学習結果を変更するのではなく、新しい木の出力を加算します。
        current = current + learning_rate * tree.predict(X)
        predictions.append(current.copy())
        mse_history.append(float(mean_squared_error(y, current)))

    return predictions, mse_history


def save_figure(X: np.ndarray, y: np.ndarray, predictions: list[np.ndarray]) -> Path:
    """初期状態と木を追加した後の予測を1枚のPNGに保存する。"""
    OUTPUT.mkdir(parents=True, exist_ok=True)
    plt.rcParams["font.family"] = "Noto Sans CJK JP"
    fig, ax = plt.subplots(figsize=(10.5, 5.5), dpi=160)
    ax.scatter(X[:, 0], y, s=11, alpha=0.45, color="#708494", label="観測値(合成)")
    selected = [0, 1, 2, len(predictions) - 1]
    colors = ["#9A9FA5", "#4B8CA6", "#D79F48", "#A44C57"]
    labels = ["初期予測(平均)", "決定木を1本追加", "決定木を2本追加", f"決定木を{len(predictions)-1}本追加"]
    for index, color, label in zip(selected, colors, labels):
        ax.plot(X[:, 0], predictions[index], linewidth=2.0, label=label, color=color)
    ax.set(xlabel="説明変数 x", ylabel="予測値 / 観測値", title="決定木を順番に追加すると予測が変わる")
    ax.legend(loc="best", ncol=2)
    ax.grid(alpha=0.2)
    fig.tight_layout()
    filepath = OUTPUT / "lesson01_manual_boosting.png"
    fig.savefig(filepath, bbox_inches="tight")
    plt.close(fig)
    return filepath


def save_history(mse_history: list[float]) -> Path:
    """反復番号と学習MSEをCSVに保存し、数値の再確認に使えるようにする。"""
    RESULTS.mkdir(parents=True, exist_ok=True)
    filepath = RESULTS / "lesson01_manual_mse.csv"
    with filepath.open("w", newline="", encoding="utf-8") as stream:
        writer = csv.writer(stream)
        writer.writerow(["round", "train_mse"])
        for index, value in enumerate(mse_history):
            writer.writerow([index, f"{value:.8f}"])
    return filepath


def main() -> None:
    """手作業の勾配ブースティング例を実行し、学習過程を図とCSVへ保存する。"""
    X, y = create_sample_data()
    predictions, mse_history = fit_manual_boosting(X, y)
    image_path = save_figure(X, y, predictions)
    csv_path = save_history(mse_history)
    for index, mse in enumerate(mse_history):
        print(f"反復 {index}: 学習MSE = {mse:.5f}")
    print(f"保存した図: {image_path}")
    print(f"保存したCSV: {csv_path}")


if __name__ == "__main__":
    main()

5.4 条件を変えてみる

lesson01_manual_boosting.pyのfit_manual_boosting()でlearning_rateを0.15、0.45、0.8へ変更し、同じ4本の決定木でどれだけ予測が変わるかを比べてみましょう。

同じデータ・同じ木の最大深さで比較することが重要です。学習率と木の深さを一度に変えてしまうと、結果が変わった理由を説明しにくくなります。

6. 実習②:四つのモデルを比較する

次は二値分類です。scikit-learnの合成データ生成機能を使って、2,000件・12特徴量のデータを用意します。正解ラベルは0と1の二種類です。

比較するモデルは、単一決定木、AdaBoost、Gradient Boosting、XGBoostの四つです。

6.1 データを三つに分ける

data_ml_flow.png

ここではtrain_test_split()を2回使い、分類ラベルの比率をなるべく維持する層化抽出を行います。

  • Train:決定木を学習するデータ
  • Validation:学習の途中経過を見るためのデータ
  • Test:最終的に予測性能を確かめる、学習に使っていないデータ

今回のモデルの条件はあらかじめ固定し、Testの結果を見ながらチューニングすることはしません。

6.2 実行するモデル

models = {
    "Decision Tree": DecisionTreeClassifier(max_depth=3, random_state=42),
    "AdaBoost": AdaBoostClassifier(
        estimator=DecisionTreeClassifier(max_depth=1, random_state=42),
        n_estimators=100, learning_rate=0.1, random_state=42,
    ),
    "Gradient Boosting": GradientBoostingClassifier(
        n_estimators=100, max_depth=3, learning_rate=0.1, random_state=42,
    ),
    "XGBoost": XGBClassifier(
        n_estimators=100, max_depth=3, learning_rate=0.1,
        tree_method="hist", eval_metric="logloss", n_jobs=2,
        random_state=42,
    ),
}

n_estimators=100は木の数に関わる設定ですが、各手法で内部の学習方法が異なるため、同じ値にしてもモデルの複雑さが完全に等しくなるわけではありません。まずは使い方を比べるための固定条件だと考えてください。

6.3 比較する評価指標

今回はAccuracyとROC-AUCを表示します。

**Accuracy(正解率)**は、予測した0と1がどれだけ正解したかを見る指標です。

\mathrm{Accuracy}=\frac{\text{正しく分類した件数}}{\text{全件数}}

ROC-AUCは、モデルが陽性クラスと陰性クラスをどれだけ区別して順位付けできるかを測る指標です。Accuracyと異なり、特定の分類しきい値だけに依存しません。詳細な読み方はLesson 05で扱います。

6.4 実行結果

モデル Accuracy ROC-AUC
Decision Tree 0.6725 0.7153
AdaBoost 0.6675 0.7431
Gradient Boosting 0.7725 0.8802
XGBoost 0.7775 0.8801

四つのモデルのROC-AUC比較

この条件では、Gradient BoostingとXGBoostのROC-AUCがほぼ同じ水準になりました。一方、単一決定木とAdaBoostでは異なる結果になっています。

AdaBoostよりGradient Boostingのほうが常に高精度ということではありません。データの性質、基本となる木の深さ、木の数、学習率によって結果は変わります。比較実験では、固定した条件のもとで何が起こったのかを読むことが重要です。

実習②の全Pythonコードを表示する
"""Lesson 01: 単一決定木・AdaBoost・GBDT・XGBoostを同じデータで比較する。

各モデルの条件をそろえ、評価データの扱いを明示するための実習です。
この1回の結果を使って「手法の一般的な優劣」を決めないようにします。
"""

from __future__ import annotations

import csv
import json
import platform
from pathlib import Path
from time import perf_counter

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import sklearn
import xgboost
from sklearn.datasets import make_classification
from sklearn.ensemble import AdaBoostClassifier, GradientBoostingClassifier
from sklearn.metrics import accuracy_score, log_loss, roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from xgboost import XGBClassifier

ROOT = Path(__file__).resolve().parents[1]
OUTPUT = ROOT / "figures"
RESULTS = ROOT / "results"
SEED = 42


def create_classification_data() -> tuple[np.ndarray, np.ndarray]:
    """外部ダウンロード不要の二値分類データを作る。

    説明変数は12個、サンプルは2,000件とします。実測値ではなく、
    分類器の動作を安全に比較するための合成データです。
    """
    return make_classification(
        n_samples=2000,
        n_features=12,
        n_informative=8,
        n_redundant=2,
        n_clusters_per_class=2,
        weights=[0.62, 0.38],
        class_sep=0.85,
        flip_y=0.03,
        random_state=SEED,
    )


def split_three_ways(
    X: np.ndarray, y: np.ndarray
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
    """層化抽出でTrain 60%・Validation 20%・Test 20%に分割する。

    Testは最後の性能確認にのみ使用し、学習曲線の計算には渡しません。
    分割後のインデックスが重ならないようにtrain_test_splitを2回使います。
    """
    X_dev, X_test, y_dev, y_test = train_test_split(
        X, y, test_size=0.20, stratify=y, random_state=SEED
    )
    X_train, X_valid, y_train, y_valid = train_test_split(
        X_dev, y_dev, test_size=0.25, stratify=y_dev, random_state=SEED
    )
    return X_train, X_valid, X_test, y_train, y_valid, y_test


def build_models() -> dict[str, object]:
    """比較に使う4種類の分類器を固定条件で作成する。

    同じデータで比較しますが、手法ごとに推奨パラメータが異なるため、
    n_estimators=100などを揃えても完全に同じ複雑さにはなりません。
    ここでは「比較の練習」を目的とします。
    """
    return {
        "Decision Tree": DecisionTreeClassifier(max_depth=3, random_state=SEED),
        "AdaBoost": AdaBoostClassifier(
            estimator=DecisionTreeClassifier(max_depth=1, random_state=SEED),
            n_estimators=100,
            learning_rate=0.1,
            random_state=SEED,
        ),
        "Gradient Boosting": GradientBoostingClassifier(
            n_estimators=100,
            max_depth=3,
            learning_rate=0.1,
            random_state=SEED,
        ),
        "XGBoost": XGBClassifier(
            n_estimators=100,
            max_depth=3,
            learning_rate=0.1,
            objective="binary:logistic",
            eval_metric="logloss",
            tree_method="hist",
            n_jobs=2,
            random_state=SEED,
        ),
    }


def evaluate_models() -> tuple[pd.DataFrame, dict[str, list[float]]]:
    """4手法のテスト性能とXGBoostの学習曲線を計算して返す。

    XGBoostの学習経過はTrainとValidationにのみ記録します。
    モデルの選択・パラメータ調整にはTestを一切使いません。
    """
    X, y = create_classification_data()
    X_train, X_valid, X_test, y_train, y_valid, y_test = split_three_ways(X, y)
    print(f"サンプル数: Train={len(y_train)}, Validation={len(y_valid)}, Test={len(y_test)}")
    rows: list[dict[str, float | str]] = []
    xgb_history: dict[str, list[float]] = {}

    for name, model in build_models().items():
        started = perf_counter()
        if name == "XGBoost":
            # Testをeval_setに渡すと、学習の判断材料にTestを使ってしまいます。
            # そのため、追跡対象はTrainとValidationのみに限定します。
            model.fit(
                X_train,
                y_train,
                eval_set=[(X_train, y_train), (X_valid, y_valid)],
                verbose=False,
            )
            history = model.evals_result()
            xgb_history = {
                "train": [float(v) for v in history["validation_0"]["logloss"]],
                "valid": [float(v) for v in history["validation_1"]["logloss"]],
            }
        else:
            model.fit(X_train, y_train)
        elapsed = perf_counter() - started

        # 最終Test評価: 正解率は閾値0.5で作られる予測クラスを測定。
        # ROC-AUCは閾値に依存しない順位性能を見るため、確率を使います。
        labels = model.predict(X_test)
        positive_prob = model.predict_proba(X_test)[:, 1]
        rows.append(
            {
                "model": name,
                "accuracy": float(accuracy_score(y_test, labels)),
                "roc_auc": float(roc_auc_score(y_test, positive_prob)),
                "fit_seconds": float(elapsed),
            }
        )
    return pd.DataFrame(rows), xgb_history


def save_results(metrics: pd.DataFrame, history: dict[str, list[float]]) -> None:
    """表示用PNG・数値CSV・実行バージョン情報を保存する。"""
    RESULTS.mkdir(parents=True, exist_ok=True)
    OUTPUT.mkdir(parents=True, exist_ok=True)
    plt.rcParams["font.family"] = "Noto Sans CJK JP"
    metrics.to_csv(RESULTS / "lesson01_model_metrics.csv", index=False, encoding="utf-8")
    with (RESULTS / "lesson01_xgboost_loss.csv").open("w", encoding="utf-8", newline="") as stream:
        writer = csv.writer(stream)
        writer.writerow(["round", "train_logloss", "validation_logloss"])
        for i, (train_loss, valid_loss) in enumerate(zip(history["train"], history["valid"])):
            writer.writerow([i + 1, f"{train_loss:.8f}", f"{valid_loss:.8f}"])

    # 2つの性能指標は尺度の意味が異なるため、別々の図で比較します。
    fig, ax = plt.subplots(figsize=(9.4, 4.8), dpi=160)
    y_pos = np.arange(len(metrics))
    bars = ax.barh(y_pos, metrics["roc_auc"], color=["#97A8B1", "#6D92A7", "#446A83", "#3F7F74"])
    ax.set_yticks(y_pos, metrics["model"])
    ax.set_xlim(0.0, 1.05)
    ax.set(xlabel="ROC-AUC(Test)", title="4種類の分類モデルを同じ合成データで比較")
    ax.invert_yaxis()
    ax.grid(axis="x", alpha=0.2)
    for bar, value in zip(bars, metrics["roc_auc"]):
        ax.text(float(value) + 0.01, bar.get_y() + bar.get_height() / 2, f"{value:.3f}", va="center", fontsize=9)
    fig.tight_layout()
    fig.savefig(OUTPUT / "lesson01_model_comparison.png", bbox_inches="tight")
    plt.close(fig)

    fig, ax = plt.subplots(figsize=(9.4, 4.8), dpi=160)
    rounds = np.arange(1, len(history["train"]) + 1)
    ax.plot(rounds, history["train"], linewidth=2, color="#446A83", label="Train")
    ax.plot(rounds, history["valid"], linewidth=2, color="#B06D4B", label="Validation")
    ax.set(xlabel="追加した木の数(Boosting rounds)", ylabel="Logloss", title="XGBoostの学習曲線(テストデータは不使用)")
    ax.grid(alpha=0.2)
    ax.legend()
    fig.tight_layout()
    fig.savefig(OUTPUT / "lesson01_xgboost_loss.png", bbox_inches="tight")
    plt.close(fig)

    manifest = {
        "python": platform.python_version(),
        "numpy": np.__version__,
        "pandas": pd.__version__,
        "scikit_learn": sklearn.__version__,
        "xgboost": xgboost.__version__,
        "matplotlib": matplotlib.__version__,
        "seed": SEED,
        "data": "make_classification; n_samples=2000; train:valid:test=60:20:20",
        "note": "合成データの1回の結果であり、業務データへの汎化を保証しない。",
    }
    (RESULTS / "lesson01_environment.json").write_text(
        json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
    )


def main() -> None:
    """実習の4モデルを学習し、図・数値・環境記録を書き出す。"""
    metrics, history = evaluate_models()
    save_results(metrics, history)
    print(metrics.to_string(index=False, float_format=lambda x: f"{x:.4f}"))
    print(f"XGBoost:最終検証Logloss={history['valid'][-1]:.4f}")
    print(f"結果を保存しました: {RESULTS}")


if __name__ == "__main__":
    main()

6.5 XGBoostの学習曲線

XGBoostについては、木を追加する途中のLoglossも記録しました。

XGBoostの学習曲線

Loglossは予測確率が正解ラベルからどれだけ外れているかを評価する損失関数で、小さいほど良い指標です。特に自信を持って外した予測に大きなペナルティを与えます。

グラフにはTrainとValidationの二つを表示しています。Trainだけを見ていると、学習を増やすことが必ずよいように見える場合があります。Validationの線も確認することで、学習データだけに過度に合わせていないかを考えられます。

このLessonではまだ学習停止のタイミングを自動で決めていません。Early StoppingはPart 2のLesson 09で実習します。

7. 手を動かして確認する課題

課題1:決定木の深さを変える

単一決定木のmax_depth=3を1または6へ変え、同じ分割データでROC-AUCを計算してください。木の深さと予測性能の関係を考えてみましょう。

課題2:XGBoostの木の数を変える

n_estimators=100を20と200へ変更して学習曲線を観察してください。この課題ではValidationの曲線を使って比較し、Testの値を見ながら条件を決めないようにします。最終テストは条件が決まってから実行します。

課題3:学習率を変える

learning_rate=0.1を0.03に変更し、木の数をそのままにして検証Loglossがどう変化するかを確認してください。学習率を小さくしても、必ず結果が良くなるとは限りません。

8. 確認問題

問1: BoostingとRandom Forestでは、決定木の学習の進め方にどのような違いがあるでしょうか。

解答を見る

Boostingでは、前までのモデルを改善するために決定木を順番に追加します。Random Forestでは、複数の決定木を主に独立に学習して予測を集約します。

問2: 二乗誤差を使う回帰問題で、最初に目的変数の平均値を予測するのはなぜでしょうか。

解答を見る

すべてのデータに同じ定数を予測する場合、二乗誤差を最小にする定数が目的変数の平均値だからです。この初期予測から残差を計算し、決定木による補正を始めます。

問3: Accuracyの高いモデルが、必ず実務でよいモデルになるでしょうか。

解答を見る

必ずしもそうなりません。クラス比率が大きく偏る場合は、多数派だけを当てても高いAccuracyになることがあります。Precision、Recall、PR-AUCなども必要に応じて確認し、誤判定の影響を考えて評価指標を選びます。

9. Docker+JupyterLabでの実行方法

Part 1では、Windows 11 → WSL2 Ubuntu 24.04 → Docker Engine / Compose → JupyterLabを標準環境にしました。Docker DesktopとGPUは不要です。環境の詳細はPart 1共通実行環境ガイドを参照してください。

参考のために、ZIPファイルを置いておきますので、ダウンロードして活用してください。

項目 設定
ベースイメージ Python 3.13(python:3.13-slim-bookworm)
XGBoost 3.1.3(requirements.txtで固定)
scikit-learn 1.8.0
JupyterLab 4.5.3
Notebook notebooks/lesson01_boosting.ipynb
共有ソース src/lesson01_manual_boosting.py、src/lesson01_model_comparison.py
保存先 figures/にPNG、results/にCSV/JSON

9.1 Docker Composeで起動する

# WSL2 Ubuntu側のLinuxファイルシステムに展開します。
mkdir -p ~/projects
cd ~/projects
unzip xgboost_elearning_part1_docker_jupyter_r002.zip
cd xgboost_elearning_part1_docker_jupyter_r002

# 初回起動。.envの認証トークン生成とDockerビルドを行います。
./scripts/start.sh

Windowsのブラウザで http://localhost:8888/lab にアクセスし、.env内のJUPYTER_TOKENを使ってログインします。KernelにはPython (XGBoost Part 1)を選びます。

最初にnotebooks/00_environment.ipynbでCPU上の動作を確認します。その後、**notebooks/lesson01_boosting.ipynb**を開き、上から順番に実行してください。

9.2 Notebookから生成する結果

lesson01_boosting.ipynbはsrc/の関数を使って計算します。生成されるファイルは次のとおりです。

ファイル 内容
figures/lesson01_manual_boosting.png 決定木追加による予測の変化
figures/lesson01_model_comparison.png 4種類のモデルの比較
figures/lesson01_xgboost_loss.png Train/ValidationのLogloss
results/lesson01_manual_mse.csv 追加した木の数と学習MSE
results/lesson01_model_metrics.csv 4種類のAccuracyとROC-AUC
results/lesson01_xgboost_loss.csv 学習反復ごとのLogloss

これらはコンテナ内だけでなくWSL2側のフォルダにも残ります。Notebookの実習セルを変更して、学習率と木の数を比較してください。

9.3 テストと終了

# 全LessonのpytestおよびNotebookを実行します。
./scripts/check.sh

# 途中で止まったときのログ確認
 docker compose logs --tail=100 jupyter

# JupyterLabを停止します。
./scripts/stop.sh

Notebookの一括検証では、元のNotebookを上書きせず、実行済みのコピーをoutputs/executed_notebooks/に保存します。

10. まとめと次回予告

今回の実習では、小さな決定木を一つずつ追加することで、予測が徐々に変化することを確認しました。また、同じ二値分類データに単一決定木・AdaBoost・Gradient Boosting・XGBoostを適用し、結果を比較しました。

これで、XGBoostが決定木を組み合わせるモデルだということは見えてきたと思います。次に知りたいのは、「なぜXGBoostは損失関数の勾配だけでなく二階微分まで使うのか」「複雑すぎる木をどう抑えるのか」という点です。

次回の**Lesson 02「XGBoostの学習アルゴリズム」**では、この二つを具体的な例と数式で追っていきます。

参考資料


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?