Skip to content

第 8 章 演習解答

第 8 章「木構造とアンサンブル学習」の演習問題の解答例です。問題は 第 8 章 演習問題 を参照してください。

演習 8-1: ジニ不純度の手計算

問題を見る

ジニ不純度の定義 \(G = 1 - \sum_{k} p_k^2\) に各クラスの割合を代入します。

  1. クラスA 80%、クラスB 20%

    \[G = 1 - (0.8^2 + 0.2^2) = 1 - (0.64 + 0.04) = 0.32\]
  2. クラスA 50%、クラスB 50%

    \[G = 1 - (0.5^2 + 0.5^2) = 1 - 0.5 = 0.5\]
  3. クラスA 100%(純粋なノード)

    \[G = 1 - 1^2 = 0\]

考察:2クラスのジニ不純度は、半々のとき最大の0.5になり、片方に偏るほど小さくなります。1クラスだけになると0です。決定木は、分割後のジニ不純度が小さくなる(ノードが純粋になる)分割を選びます。

演習 8-2: 決定木による分類と過学習

問題を見る

import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report

# 1. データの読み込みと欠損値の削除
penguins = sns.load_dataset("penguins").dropna()

# 2. 特徴量 X とラベル y の設定
X = penguins[["bill_length_mm", "bill_depth_mm", "body_mass_g"]]
y = penguins["species"]

# 3. 訓練・テストに分割(8:2, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# 4. 決定木モデル(max_depth=3)の学習
model = DecisionTreeClassifier(max_depth=3, random_state=42)
model.fit(X_train, y_train)

# 5. テストデータで予測し、正解率と classification_report を表示
y_pred = model.predict(X_test)
print(f"正解率: {accuracy_score(y_test, y_pred):.4f}")
print(classification_report(y_test, y_pred))

# 6. plot_tree で木構造を可視化
plt.figure(figsize=(14, 8))
plot_tree(model, feature_names=X.columns, class_names=model.classes_,
          filled=True, fontsize=9)
plt.show()

# 7. max_depth を 1〜10 で変化させ、訓練精度とテスト精度をプロット
depths = range(1, 11)
train_accs, test_accs = [], []
for depth in depths:
    clf = DecisionTreeClassifier(max_depth=depth, random_state=42)
    clf.fit(X_train, y_train)
    train_accs.append(accuracy_score(y_train, clf.predict(X_train)))
    test_accs.append(accuracy_score(y_test, clf.predict(X_test)))
    print(f"max_depth={depth:2d}: 訓練 {train_accs[-1]:.4f}, "
          f"テスト {test_accs[-1]:.4f}")

plt.figure(figsize=(8, 5))
plt.plot(depths, train_accs, marker="o", label="訓練精度")
plt.plot(depths, test_accs, marker="s", label="テスト精度")
plt.xlabel("max_depth")
plt.ylabel("正解率")
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
正解率: 0.9851
              precision    recall  f1-score   support

      Adelie       0.97      1.00      0.98        31
   Chinstrap       1.00      0.92      0.96        13
      Gentoo       1.00      1.00      1.00        23

    accuracy                           0.99        67
   macro avg       0.99      0.97      0.98        67
weighted avg       0.99      0.99      0.98        67

max_depth= 1: 訓練 0.7669, テスト 0.7015
max_depth= 2: 訓練 0.9511, テスト 0.9254
max_depth= 3: 訓練 0.9699, テスト 0.9851
max_depth= 4: 訓練 0.9850, テスト 1.0000
max_depth= 5: 訓練 0.9925, テスト 0.9701
max_depth= 6: 訓練 0.9962, テスト 0.9701
max_depth= 7: 訓練 1.0000, テスト 0.9701
max_depth= 8: 訓練 1.0000, テスト 0.9701
max_depth= 9: 訓練 1.0000, テスト 0.9701
max_depth=10: 訓練 1.0000, テスト 0.9701

実行すると、まず深さ3の木構造が表示されます。根ノードでは bill_depth_mm による分割が選ばれ、くちばしが薄く体重の重いグループとして Gentoo が早い段階で分離されます。続いて、max_depth に対する訓練精度・テスト精度の折れ線グラフが表示されます。

考察:深さ1では訓練・テストとも7割程度で、未学習です。深さ2〜3で精度が大きく上がり、テスト精度は深さ4で1.0000になります。深さ5以降は訓練精度が1.0に近づく一方、テスト精度は0.9701に下がっており、過学習です。このデータでは深さ3〜4が適切です。

演習 8-3: ランダムフォレストと特徴量の重要度

問題を見る

import seaborn as sns
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# 1. データの準備(演習8-2と同じ)
penguins = sns.load_dataset("penguins").dropna()
X = penguins[["bill_length_mm", "bill_depth_mm", "body_mass_g"]]
y = penguins["species"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# 2. ランダムフォレストの学習と正解率の表示
model = RandomForestClassifier(n_estimators=100, max_depth=3,
                               random_state=42)
model.fit(X_train, y_train)
print(f"正解率: {accuracy_score(y_test, model.predict(X_test)):.4f}")

# 3. 特徴量の重要度を DataFrame に整理して表示
importance_df = pd.DataFrame({
    "特徴量": X.columns,
    "重要度": model.feature_importances_
}).sort_values("重要度", ascending=False)
print(importance_df)

# 4. 棒グラフで可視化
plt.figure(figsize=(8, 4))
plt.barh(importance_df["特徴量"], importance_df["重要度"])
plt.xlabel("重要度")
plt.gca().invert_yaxis()
plt.show()

# 5. max_depth を変えたときのテスト精度を比較
for depth in [1, 2, 3, 5, None]:
    clf = RandomForestClassifier(n_estimators=100, max_depth=depth,
                                 random_state=42)
    clf.fit(X_train, y_train)
    acc = accuracy_score(y_test, clf.predict(X_test))
    print(f"max_depth={str(depth):>4s}: テスト精度 {acc:.4f}")
正解率: 1.0000
              特徴量       重要度
0  bill_length_mm  0.381794
1   bill_depth_mm  0.365298
2     body_mass_g  0.252908
max_depth=   1: テスト精度 0.7910
max_depth=   2: テスト精度 0.9851
max_depth=   3: テスト精度 1.0000
max_depth=   5: テスト精度 1.0000
max_depth=None: テスト精度 1.0000

実行すると、重要度の降順に並んだ横向きの棒グラフも表示されます。

考察:テスト精度は決定木(演習8-2)の0.9851から1.0000に上がりました。重要度は3つの特徴量に大きな偏りなく分かれています。ランダムフォレストでは木ごとに使う特徴量の組み合わせが変わるためです。max_depth の比較では、深さ1は各木が1回しか分岐できず0.7910にとどまりますが、深さ3以降は max_depth=None(制限なし)でも1.0000のままです。個々の木が過学習しても多数決で平均されるため、単一の決定木ほど深さを気にしなくてよくなります。

演習 8-4: 学習率と木の本数の関係

問題を見る

import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import accuracy_score

# ペンギンデータセット(演習8-2と同じ準備)
penguins = sns.load_dataset("penguins").dropna()
X = penguins[["bill_length_mm", "bill_depth_mm", "body_mass_g"]]
y = penguins["species"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

for n in [10, 50, 100, 500, 1000]:
    clf = GradientBoostingClassifier(
        n_estimators=n, learning_rate=0.01,
        max_depth=3, random_state=42
    )
    clf.fit(X_train, y_train)
    acc = accuracy_score(y_test, clf.predict(X_test))
    print(f"n_estimators={n:4d}: テスト精度 {acc:.4f}")
n_estimators=  10: テスト精度 0.8060
n_estimators=  50: テスト精度 1.0000
n_estimators= 100: テスト精度 1.0000
n_estimators= 500: テスト精度 1.0000
n_estimators=1000: テスト精度 0.9851

考察:学習率0.01では1本の木が誤差を少ししか修正しないため、木が10本では精度が約81%にとどまります。50本以上で修正が積み重なり、100%に達します。学習率を小さくするなら木の本数を増やす必要があり、そのぶん学習時間は長くなります。1000本ではテスト精度が0.9851にわずかに下がっており、木を増やしすぎると過学習することがあります。

発展課題: 特徴量重要度の比較

問題を見る

import seaborn as sns
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import (RandomForestClassifier,
                              GradientBoostingClassifier)

# irisデータセット
df = sns.load_dataset("iris")
X = df.drop("species", axis=1)
y = df["species"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42
)

# 3つのモデルを学習し、特徴量重要度を集める
models = {
    "決定木": DecisionTreeClassifier(max_depth=3, random_state=42),
    "ランダムフォレスト": RandomForestClassifier(
        n_estimators=100, random_state=42),
    "勾配ブースティング": GradientBoostingClassifier(
        n_estimators=100, learning_rate=0.1,
        max_depth=3, random_state=42),
}
importance_df = pd.DataFrame(index=X.columns)
for name, model in models.items():
    model.fit(X_train, y_train)
    importance_df[name] = model.feature_importances_

print(importance_df.round(4))

# 棒グラフで比較
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
for ax, name in zip(axes, models):
    importances = importance_df[name].values
    indices = np.argsort(importances)
    ax.barh(range(len(indices)), importances[indices])
    ax.set_yticks(range(len(indices)))
    ax.set_yticklabels(importance_df.index[indices])
    ax.set_xlabel("重要度")
    ax.set_title(name)
plt.tight_layout()
plt.show()
                 決定木  ランダムフォレスト  勾配ブースティング
sepal_length  0.0000     0.1041     0.0034
sepal_width   0.0000     0.0446     0.0178
petal_length  0.9251     0.4173     0.5952
petal_width   0.0749     0.4340     0.3836

実行すると、3つのモデルの特徴量重要度を横並びで比較する棒グラフが表示されます。

考察:irisでは、どのモデルでも花弁(petal)の長さ・幅に重要度が集まり、がく片(sepal)はあまり使われません。花弁の大きさだけで3品種をほぼ分けられるためです。決定木は petal_length に偏り(0.9251)、ランダムフォレストは petal_length と petal_width にほぼ半々です。演習8-3のペンギンデータでは3つの特徴量がまんべんなく使われており、重要度の分かれ方はデータによって変わります。