「機械学習のモデルって、なぜその答えを出したのか分からなくてモヤモヤする…」「正解率は高いのに、本番で使うと全然当たらない」

機械学習を触り始めると、こういう悩みにぶつかりますよね。

そこでおすすめなのが決定木(けっていぎ)です。決定木は「どんな質問をして、どう判定したか」を図やテキストでそのまま見られる、数少ないモデルのひとつです。

この記事では、scikit-learn に入っているワインのデータを使って、

  • 決定木で品種を当てるモデルを作る
  • 判定ルールを図(plot_tree)とテキスト(export_text)で見える化する
  • 「学習データだけ100点」の過学習を見抜き、交差検証で木の深さを選ぶ

までを、実際の実行結果つきで一緒に見ていきましょう!

難易度:初級〜中級(Pythonの基本文法が分かり、scikit-learn は初めて〜少し触ったことがある方向け)。

決定木とは?「はい・いいえ」で絞り込むフローチャート

decision tree diagram
decision tree diagram / Photo by Ann H via Pexels

イメージとしては、「はい・いいえ」で答える質問を繰り返して答えを絞り込む、20の質問ゲームです。

たとえば「プロリン(ワインの成分)は765以下?」→「はい」→「フラバノイドは1.4以下?」→「はい」→「品種C!」という具合に、質問の分かれ道をたどって答えにたどり着きます。

つまり決定木は、データから「どの成分を、どの値で区切って質問すれば一番うまく分けられるか」を自動で見つけてくれる仕組みです。ポイントをまとめるとこんな感じです👇

  • 判定の理由を人が読める(上司やお客さんに説明しやすい)
  • 数値の大きさをそろえる標準化が基本的にいらない(「○○以下か?」と比べるだけなので)
  • そのかわり、深く育てすぎると学習データを丸暗記してしまう(過学習)

最後の弱点が、この記事の後半のテーマです。

準備:ライブラリのインストール

scikit-learn と、図を描くための matplotlib を入れておきましょう。

pip install scikit-learn matplotlib

今回の動作確認は Python 3.13・scikit-learn 1.9.0・matplotlib 3.11.0(Windows)で行いました。

使うデータは scikit-learn に最初から入っているワインのデータセット(load_wine)です。イタリアの同じ地域で造られた3つの品種のワイン178本について、アルコール度数やプロリンなど13種類の成分を測ったデータで、ダウンロードなしですぐ使えます。

ステップ1:決定木で品種を当ててみる

まずは何も設定せずに決定木を作り、正解率を見てみます。ここが重要です👇

  • train_test_split で学習用70%・テスト用30%に分ける(テスト用は「本番の未知のデータ」の代わり)
  • stratify=y で、学習用とテスト用の品種の割合をそろえる
  • random_state=0 を付けると、何度実行しても同じ結果になる
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier

# ワインのデータ(178本・13種類の成分・3つの品種)を読み込む
wine = load_wine()
X, y = wine.data, wine.target
print("データの形:", X.shape)
print("品種の名前:", wine.target_names.tolist())

# 学習用70%・テスト用30%に分ける(品種の割合はそろえる)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=0, stratify=y
)

# 決定木を作って学習(深さの制限なし)
model = DecisionTreeClassifier(random_state=0)
model.fit(X_train, y_train)

print("木の深さ:", model.get_depth(), "/ 葉の数:", model.get_n_leaves())
print(f"学習データの正解率: {model.score(X_train, y_train):.3f}")
print(f"テストデータの正解率: {model.score(X_test, y_test):.3f}")
実行結果:深さ制限なしの決定木の正解率
実行結果:深さ制限なしの決定木の正解率

実行結果はこうなりました。

学習データは1.000(100点満点)、テストデータは0.944です。テストの54本中51本を当てているので、なかなかの成績ですよね😊

でも、ちょっと待ってください。学習データで100点というのは、裏を返すと「学習データの細かいクセまで全部覚えた」ということでもあります。これが本当に良いことなのかは、ステップ4で確かめます。

ステップ2:判定ルールを図にする(plot_tree)

決定木のいちばんの魅力、「中身を見る」をやってみましょう。深さ制限なしの木は図が大きくなるので、ここではmax_depth=2(質問は最大2回)に制限した木を描きます。

import matplotlib.pyplot as plt
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, plot_tree

plt.rcParams["font.family"] = "Meiryo"  # Windowsで日本語を表示する(Macなら "Hiragino Sans")

wine = load_wine()
X_train, X_test, y_train, y_test = train_test_split(
    wine.data, wine.target, test_size=0.3, random_state=0, stratify=wine.target
)

# 図を読みやすくするため、深さを2に制限した木を作る
model = DecisionTreeClassifier(max_depth=2, random_state=0)
model.fit(X_train, y_train)

plt.figure(figsize=(12, 6))
plot_tree(
    model,
    feature_names=wine.feature_names,    # 質問に使う成分の名前
    class_names=["品種A", "品種B", "品種C"],  # class_0〜2 に日本語名をつける
    filled=True,                         # 多数派の品種で色分け
    rounded=True,
    fontsize=11,
)
plt.title("ワインの品種を当てる決定木(深さ2)")
plt.show()
実行結果:plot_treeで描いた深さ2の決定木
実行結果:plot_treeで描いた深さ2の決定木

実行すると、次のような図が表示されます。

図の読み方はこんな感じです👇

  • 1行目のproline <= 765.0が質問。当てはまれば左(True)、当てはまらなければ右(False)へ進む
  • samples:その箱にたどり着いた学習データの本数(最初は124本)
  • value:その中の品種ごとの本数。[41, 50, 33]なら品種A・B・Cの順
  • class:その箱での多数派=この箱に来たら出す答え
  • gini:品種の「混ざり具合」。0に近いほど1つの品種にそろっている

右下の箱を見ると value = [39, 1, 0] で、40本中39本が品種Aです。つまり「プロリンが765より多くて、フラバノイドが2.165より多ければ、ほぼ品種A」というルールを、モデルが自分で見つけたわけです。

一方で、右から2番目の箱は samples = 3 とたった3本しかありません。「3本だけのために作られた分かれ道」は、たまたまのクセを覚えている可能性があります。このあたりが過学習の入り口です⚠️

ステップ3:ルールをテキストで出す・重要度と確率を見る

図はきれいですが、ルールをメモに残したり、Excelの条件式に書き写したりしたいときはテキストのほうが便利です。あわせて、「どの成分が効いているか」と「予測の自信の度合い」も見てみましょう。

from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, export_text

wine = load_wine()
X_train, X_test, y_train, y_test = train_test_split(
    wine.data, wine.target, test_size=0.3, random_state=0, stratify=wine.target
)
model = DecisionTreeClassifier(max_depth=2, random_state=0)
model.fit(X_train, y_train)
names = ["品種A", "品種B", "品種C"]

# ① 判定ルールをテキストで表示する
print(export_text(model, feature_names=wine.feature_names, class_names=names))

# ② どの成分が判定に効いているか(特徴量の重要度)
print("--- 重要度(0より大きいものだけ)---")
for name, imp in sorted(zip(wine.feature_names, model.feature_importances_),
                        key=lambda t: t[1], reverse=True):
    if imp > 0:
        print(f"{name:12} {imp:.3f}")

# ③ テストデータの先頭1本を予測して、確率も見る
sample = X_test[:1]
pred = model.predict(sample)[0]
proba = model.predict_proba(sample)[0]
print("--- 予測 ---")
print(f"proline={sample[0][12]:.0f}, flavanoids={sample[0][6]:.2f}")
print("予測:", names[pred], "/ 正解:", names[y_test[0]])
print("確率:", {n: round(float(p), 3) for n, p in zip(names, proba)})
実行結果:判定ルール・重要度・予測確率
実行結果:判定ルール・重要度・予測確率

実行結果はこちらです。

ポイントをまとめるとこんな感じです👇

  • export_text:図と同じルールが、インデント付きのテキストで出る。しきい値は小数第2位までに丸められるので、図の 2.165 が 2.17 と表示されています
  • feature_importances_:13成分のうち、この木が使ったのはproline と flavanoids の2つだけ。重要度はほぼ半々でした
  • predict_proba:たどり着いた箱の品種の割合がそのまま「確率」になる。今回は [39, 1, 0] の箱なので、39÷40=0.975

これ、実行せずに答えられますか?「proline=1375・flavanoids=2.79 のワインはどの品種?」……図をたどれば、1375は765より大きいので右へ、2.79は2.165より大きいので右へ、で品種A。モデルの答えと同じ道筋を、人間もたどれるのが決定木の強みです。

⚠️ ひとつ注意として、重要度が0の成分は「役に立たない成分」という意味ではありません。「この木では質問に使われなかった」だけです。似た情報を持つ別の成分が先に選ばれると、もう片方は0になることがあります。

ステップ4:過学習を見抜いて、木の深さを決める

いよいよ本題です。ステップ1の「学習データ100点」は良いことなのでしょうか?

過学習とは、イメージとしては「問題集の答えを丸暗記して、問題集では100点なのに、本番の初見問題には弱い」状態です。これを確かめるには、解いたことのない問題での成績と比べるのが一番です。

そこで使うのが交差検証(クロスバリデーション)です。学習データを5つに分け、「4つで学習→残り1つでテスト」を担当を替えながら5回繰り返して、その平均を取ります。cross_val_score を使えば1行で書けます。

import matplotlib.pyplot as plt
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.tree import DecisionTreeClassifier

plt.rcParams["font.family"] = "Meiryo"

wine = load_wine()
X_train, X_test, y_train, y_test = train_test_split(
    wine.data, wine.target, test_size=0.3, random_state=0, stratify=wine.target
)

depths = range(1, 7)
train_scores, cv_scores = [], []
print("深さ  学習データ  交差検証(5分割)")
for d in depths:
    model = DecisionTreeClassifier(max_depth=d, random_state=0)
    # 学習データだけを5つに分けて「解いたことのない問題」の正解率を測る
    cv = cross_val_score(model, X_train, y_train, cv=5).mean()
    model.fit(X_train, y_train)
    train_scores.append(model.score(X_train, y_train))
    cv_scores.append(cv)
    print(f"{d:>3}   {train_scores[-1]:.3f}       {cv:.3f}")

best = depths[cv_scores.index(max(cv_scores))]
print("交差検証で一番よい深さ:", best)

# 選んだ深さで作り直し、最後に1回だけテストデータで確認する
final = DecisionTreeClassifier(max_depth=best, random_state=0).fit(X_train, y_train)
print(f"テストデータの正解率: {final.score(X_test, y_test):.3f}")

plt.plot(depths, train_scores, marker="o", label="学習データ")
plt.plot(depths, cv_scores, marker="s", label="交差検証")
plt.xlabel("木の深さ(max_depth)")
plt.ylabel("正解率")
plt.title("深くするほど学習データには強くなるが…")
plt.legend()
plt.grid(alpha=0.3)
plt.show()
実行結果:木の深さごとの学習データと交差検証の正解率
実行結果:木の深さごとの学習データと交差検証の正解率

実行結果(表の部分)はこうなりました。

深さ 学習データ 交差検証(5分割)
1 0.702 0.686
2 0.927 0.831
3 0.984 0.863
4 1.000 0.872
5 1.000 0.872
6 1.000 0.872

そのあとに「交差検証で一番よい深さ: 4」「テストデータの正解率: 0.944」と表示されます。グラフにするとこうです。

ここから読み取れることは3つです👇

  • 学習データと交差検証の差が大きい:深さ4以上では学習データ1.000に対して交差検証0.872。この約0.13の差が「丸暗記ぶん」で、過学習のサインです
  • 深さ4から先は変わらない:このデータでは深さ4の時点で学習データを全部当て切ってしまい、それ以上質問を増やす必要がないので、木がそれ以上育ちません(ステップ1の「木の深さ: 4」と同じ理由です)
  • 深さ3と4の差はわずか:交差検証で0.863と0.872、差は0.01ほど。葉の数は深さ3で8個、深さ4で9個です。説明のしやすさを優先して、あえて浅い深さ3を選ぶのも十分ありです

つまずきポイント:テストデータで深さを選ばない

実は、テストデータの正解率を深さごとに測ると、深さ3が0.963、深さ4が0.944で、交差検証とは逆に深さ3のほうが高くなります。

「じゃあテストデータで一番よい深さを選べばいいのでは?」と思いますよね。でもこれは避けたほうが安全です。テストデータは54本しかないので、1本当たるかどうかで約0.02も動きます。そこで一番よい値を選ぶと、「たまたまこの54本と相性がよかった設定」を選んでしまい、本番の成績を正しく見積もれなくなります。

だからこのコードでは、

  1. 深さ選びは学習データの中の交差検証だけで行う
  2. 決めた設定で作り直したモデルを、最後に1回だけテストデータで確認する

という順番にしています。この「テストデータは最後まで取っておく」習慣は、決定木に限らずどの機械学習モデルでも大事です。

深さ以外の「育ちすぎ防止」パラメータ

木の育ちすぎを抑える方法は max_depth だけではありません。よく使うものをまとめておきます。

パラメータ 意味 イメージ
max_depth 質問の回数の上限 分かれ道は最大○段まで
min_samples_leaf 1つの葉に残すデータの最低数 「3本だけの箱」を作らせない
min_samples_split 分割してよい箱のデータの最低数 少なすぎる箱はもう分けない
ccp_alpha 育てた木を後から刈り込む強さ 効果の薄い枝を剪定する

まずは max_depth を交差検証で選ぶ、という今回の方法から始めるのがおすすめです。慣れてきたら GridSearchCV を使うと、複数のパラメータの組み合わせをまとめて交差検証で比べられます。

詳しい引数は公式ドキュメント(scikit-learn: Decision Trees)にまとまっています。

まとめ

今回は scikit-learn の決定木で、ワインの品種を当てるモデルを作りながら、中身の見える化と過学習の見抜き方を見てきました。ポイントをおさらいしましょう。

  • 決定木は「はい・いいえ」の質問で答えを絞り込むモデル。判定の理由を人が読める
  • plot_tree で図、export_text でテキストとしてルールを出せる。predict_proba の確率は「たどり着いた箱の品種の割合」
  • 学習データで100点でも安心しない。学習データと交差検証の差が大きければ過学習のサイン
  • 深さなどの設定は交差検証で選び、テストデータは最後に1回だけ使う

「なぜその答えになったのか」が見えると、機械学習がぐっと身近に感じられるはずです。ワインのデータで流れがつかめたら、手元のCSVでもぜひ試してみてください🚀

📚 関連商品・おすすめ書籍

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

もしも

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

初心者に定番のPython入門書

Amazonで見る
実践Claude Code入門―現場で活用するためのAIコーディングの思考法

もしも

実践Claude Code入門―現場で活用するためのAIコーディングの思考法

AIコーディングの現場活用法を学ぶ一冊

Amazonで見る
Python Web開発実践入門 ―― FastAPIによるWebAPI開発と非同期処理

もしも

Python Web開発実践入門 ―― FastAPIによるWebAPI開発と非同期処理

FastAPIでWebAPI開発を実践的に学ぶ

Amazonで見る

※本記事にはアフィリエイトリンクが含まれます。

ABOUT ME
やまちゃん
これまで学生と社会人を合わせて5000人以上にプログラミング学習を指導。 ゼロからイチをわかりやすく解説する専門家として活動しており、本業ではArduinoを用いたIoT開発とロボットプログラミングが専門。 Pythonを用いたアプリ開発、ウェブアプリケーションの開発で業務の効率化をサポートしています。