訓練・検証・テスト|機械学習の問題設定と評価分割

このページはプロモーション(広告)を含みます。

機械学習の問題設定と評価分割(中級・読む30分+演習40分)

この授業のゴール

仕事でどう使うか

モデルの評価値は「本番で新しいデータに使ったときの性能」の見積もりです。見積もりが甘いと、導入後に「検証では良かったのに」という事態になります。3つの集合を役割どおりに使うことは、この見積もりを正直にするための約束事です。

路線 この授業の内容が使われる成果物・意思決定
DS 予測評価の報告書。「検証で選び、テストで1回確認した値」と、比較したベースラインを並べて書く
ML 実験管理。どの集合で何を選んだか(特徴量・ハイパーパラメータ・区切り)を記録し、テストの使用回数を管理する
DX 受け入れ検査。ベンダーにテストデータを渡さず、発注側が最後に評価する体制をつくる
RS 比較実験。手法の調整に使ったデータと、最終比較に使うデータを分け、論文に書く

この授業では、ベースライン の期待利益をそのまま使い、「検証で区切りを選び、テストで1回だけ確かめる」流れを実行します。

前提

次の言葉と操作を使います。

この授業で使う架空の表は、5人の利用者(u1〜u5)について3月と4月の2行ずつ、計10行です。実務には小さすぎる、役割を確かめるための演習用の表です。

身近な状況:「同じ人」が練習と本番の両方にいる

ある会社で、利用者ごとに毎月1行の記録を作り、翌月に解約したか(y)を予測することになりました。

user_id month usage_hours support_calls y
u1 2025-03 20.0 0 0
u1 2025-04 18.5 1 0
u2 2025-03 3.0 3 0
u2 2025-04 2.5 4 1
u3 2025-03 12.0 1 0
u3 2025-04 11.0 1 0
u4 2025-03 30.0 0 0
u4 2025-04 31.5 0 0
u5 2025-03 6.0 2 0
u5 2025-04 1.0 5 1

担当者は10行を無作為に7:3で分けました。すると、u1の3月が訓練、u1の4月がテストに入りました。u1の2行は利用時間が20.0と18.5で、ほとんど同じです。モデルは訓練で「u1のような人は継続」と覚え、テストで同じu1を当てます。これは新しい利用者に使う力を測っていません。

この予測を「来月入会する新しい利用者」に使うなら、評価も訓練で見たことのない人で行う必要があります。この授業では、そのための分け方と、3つの集合の正しい使い方を学びます。

直感的な説明:練習問題・模試・本番試験

受験勉強にたとえると、3つの集合の役割がわかりやすくなります。

集合 たとえ 何をするか 何回見てよいか
訓練(train) 問題集 解いて、解説を読んで覚える(モデルや前処理を学習する) 何度でも
検証(valid) 模擬試験 勉強法(モデルの種類・設定・区切り)を比べて選ぶ 何度でも。ただし見るほど模試に慣れる
テスト(test) 本番試験 選び終えた後に、実力を1回だけ測る 原則1回

本番試験の問題を事前に何度も見て勉強法を変えたら、本番の点数は実力を表しません。同じように、テストを見て設定を直すと、テストは「選択に使ったデータ」になり、新しいデータでの性能の見積もりとして使えなくなります。

もう1つ大事なのは、問題集と本番に同じ問題を入れないことです。同じ人の似た行が訓練とテストに分かれると、同じ問題が出た試験と同じになります。

正確な定義:3つの集合と、分ける単位

分け方は、次の3つの条件を満たすように決めます。

  1. 利用時点:未来の予測に使うなら、評価は訓練より後の期間で行う(時点・個体・群の分離 で詳しく扱う)。
  2. 個体:新しい人に使うなら、同じ人の行はすべて同じ集合に入れる。
  3. 件数:検証・テストの評価値のばらつきが、判断に使える程度に小さい。

3について数値で確かめます。正解率 $p$ を $n$ 件で評価したときの標準誤差は $\sqrt{p(1-p)/n}$ です(「標準誤差」(有料版))。$p=0.8$ のとき、$n=100$ なら $\sqrt{0.8\times0.2/100}=0.04$、$n=600$ なら約0.016です。100件のテストでは、2つのモデルの差が4ポイント程度あっても偶然と区別しにくいということです。「7:2:1」などの比率は目安にすぎず、評価に必要な件数と陽性の件数から逆算します。陽性が少ないときは、層化(各集合の陽性割合をそろえる)も併用します。

件数が少なくて検証データを別に取れないときは、訓練データの中で交差検証(訓練をk個に分け、1つずつ検証役にする)を使って選びます(「指標と交差検証」(有料版))。この場合もテストは別に残します。

段階付き例題:人単位で分け、検証で選び、テストで1回確かめる

ステップ1:人単位で割り当てる(10行の表)。新しい人への予測なので、訓練 u1〜u3、検証 u4、テスト u5 とします。行ではなく user_id に集合を割り当て、map で各行に広げます。

import pandas as pd
rows = pd.DataFrame({
    'user_id':       ['u1', 'u1', 'u2', 'u2', 'u3', 'u3', 'u4', 'u4', 'u5', 'u5'],
    'month':         ['2025-03', '2025-04'] * 5,
    'usage_hours':   [20.0, 18.5, 3.0, 2.5, 12.0, 11.0, 30.0, 31.5, 6.0, 1.0],
    'support_calls': [0, 1, 3, 4, 1, 1, 0, 0, 2, 5],
    'y':             [0, 0, 0, 1, 0, 0, 0, 0, 0, 1],
})
assign = {'u1': 'train', 'u2': 'train', 'u3': 'train', 'u4': 'valid', 'u5': 'test'}
rows['set'] = rows['user_id'].map(assign)          # 人単位で割り当てる

print(rows.groupby('set').agg(行数=('user_id', 'size'), 人数=('user_id', 'nunique')))
users = {s: set(g['user_id']) for s, g in rows.groupby('set')}
print('train∩valid:', users['train'] & users['valid'],
      ' train∩test:', users['train'] & users['test'],
      ' valid∩test:', users['valid'] & users['test'])

出力:

       行数  人数
set
test    2   1
train   6   3
valid   2   1
train∩valid: set()  train∩test: set()  valid∩test: set()

人数(3・1・1)と行数(6・2・2)、すべての組み合わせで重なりが空集合(set())であることを確認しました。分割を作ったら、毎回この3つを表示する習慣をつけます。

ステップ2:実データで「訓練→検証→テスト」を通す。churn.csv(1人1行、split 列で分割済み)を使います。make_pipeline(StandardScaler(), LogisticRegression()) は標準化とモデルをつないだ Pipeline で、fit を呼ぶと標準化の平均・標準偏差も訓練データだけから計算されます。profit は前の授業の期待利益(円)を計算する関数で、round で円単位に丸めています。

import pandas as pd
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

df = pd.read_csv('/data/churn.csv')
cols = ['tenure_months', 'monthly_fee', 'support_calls', 'usage_hours']
tr, va, te = (df[df['split'] == s] for s in ['train', 'valid', 'test'])

def profit(y, p, thr, V=12000, s=0.3, c=500):      # 前の授業の期待利益(円)
    call = p >= thr
    return round(((call) & (y == 1)).sum() * s * V - call.sum() * c)

# (1) 訓練で fit(標準化の平均・標準偏差もここで学ぶ)
model = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))
model.fit(tr[cols], tr['churned'])

# (2) 検証で区切りを選ぶ(ここは何度見てもよい)
p_va = model.predict_proba(va[cols])[:, 1]
for thr in [0.1, 0.15, 0.2, 0.3, 0.5]:
    print(f'検証 区切り{thr:<4} 期待利益 {profit(va["churned"].values, p_va, thr):>8,} 円')
best = max([0.1, 0.15, 0.2, 0.3, 0.5], key=lambda t: profit(va['churned'].values, p_va, t))
print('選んだ区切り:', best)

# (3) テストで1回だけ確認する
p_te = model.predict_proba(te[cols])[:, 1]
print('テスト 期待利益:', f'{profit(te["churned"].values, p_te, best):,}', '円(600人)')
print('テスト 全員に電話:', f'{profit(te["churned"].values, p_te, 0.0):,}', '円')

出力:

検証 区切り0.1  期待利益  217,200 円
検証 区切り0.15 期待利益  206,900 円
検証 区切り0.2  期待利益  181,300 円
検証 区切り0.3  期待利益  127,400 円
検証 区切り0.5  期待利益   55,000 円
選んだ区切り: 0.1
テスト 期待利益: 157,600 円(600人)
テスト 全員に電話: 110,400 円

結果の意味:

  1. 区切りは検証データで選びました(0.1)。検証の217,200円は「選んだ結果」なので、少し楽観的な値です。
  2. テストで1回だけ確認すると157,600円でした。検証より低いのは、選択による楽観と、テストの解約率(19.0%)が検証(21.0%)より低いことの両方が考えられます。
  3. 同じテストで基準「全員に電話」は110,400円です。報告すべきはテストの157,600円と、基準との差47,200円です。検証の217,200円を最終成績として報告してはいけません。

操作して理解する:テストを何度も見るとどうなるか

中身はただのコイン投げ(0か1をでたらめに出す)の「モデル」を100個作り、200件のテストで一番成績の良いものを選びます。本当の正解率はどれも50%のはずです。

import numpy as np
rng = np.random.default_rng(0)
n_test, n_models = 200, 100
y_test = rng.integers(0, 2, n_test)         # テストの正解(0/1が半々)
y_new = rng.integers(0, 2, n_test)          # 本番で出会う新しいデータ

# 中身はただのコイン投げの「モデル」を100個作り、テストの成績が一番良いものを選ぶ
pred_test = rng.integers(0, 2, (n_models, n_test))
acc = (pred_test == y_test).mean(axis=1)
best = int(np.argmax(acc))
print('テストで選んだモデルの正解率:', acc[best])
# 同じモデル(コイン投げ)を新しいデータに使うと
pred_new = rng.integers(0, 2, n_test)
print('新しいデータでの正解率     :', (pred_new == y_new).mean())
print('100個のテスト正解率の平均  :', acc.mean().round(3))

出力:

テストで選んだモデルの正解率: 0.58
新しいデータでの正解率     : 0.46
100個のテスト正解率の平均  : 0.5

コイン投げでも、100回試して一番良いものを選ぶと、テストでは58%に見えます。新しいデータでは46%で、本当の実力(50%)に戻ります。選ぶ回数が多いほど、選んだものの成績は偶然の分だけ良く見えます。テストを見て特徴量や設定を何度も直すと、これと同じことが起きます。

次のように書き換えて試してください。

  1. n_models = 100 を 1000 にすると? → 偶然の最大値はさらに大きくなり、テストの正解率はもっと良く見えます。
  2. n_test = 200 を 2000 にすると? → 1回ごとのばらつきが小さくなるので、選んだモデルの「水増し」は小さくなります(ただしゼロにはなりません)。

実務での対策は、①選択はすべて検証(または訓練内の交差検証)で行う、②テストを見た回数を記録する、③テストの結果が悪くても隠さず報告し、改善するなら新しいテストデータ(次の期間のデータなど)を用意する、の3つです。

発展:テストの使い方の細かい規則と限界

よくある失敗

失敗1:テストの結果を見て区切りを何度も調整する

例題で、テストの期待利益を見ながら区切りを0.1、0.12、0.08…と何度も変え、一番良い値を報告したとします。これは「テストで選んだ」ことになり、操作して理解する節のコイン投げと同じ水増しが起きます。

手順 区切りを選んだデータ 報告する値の意味
誤り テスト 選択に使った値。新しいデータでは下がる可能性が高い
正しい 検証 テストの値は選択に使っていないので、新しいデータでの見積もりになる

直し方:区切り・設定・特徴量は検証で決め、テストは最後に1回だけ評価する。テストの結果を見て直したくなったら、それは新しい実験として、別のテストデータを用意する。

失敗2:全データの平均で欠損を補完してから分割する

欠損値を平均で埋める前処理を、分割の前に全データで行うと、テストの値が訓練の前処理に影響します。

import numpy as np
x = np.array([10.0, 12.0, np.nan, 11.0, 50.0, np.nan])  # 前4件=訓練、後2件=テスト
train, test = x[:4], x[4:]
print('全データの平均で補完:', round(np.nanmean(x), 2))
print('訓練だけの平均で補完:', round(np.nanmean(train), 2))

出力:

全データの平均で補完: 20.75
訓練だけの平均で補完: 11.0

テストにある外れ値50.0が全体の平均を引き上げ、訓練の欠損が20.75で埋められます。本番では未来のデータの平均は使えないので、学習時と本番で前処理が変わります。直し方:分割してから、補完の平均は訓練だけで fit する。Pipeline に補完器(SimpleImputer)を入れて fit を訓練に対して呼べば、この順序が自動で守られます。

失敗3:同じ人の行を別々の集合に入れる

冒頭の表を行単位で無作為に分けると、u1の3月が訓練、4月がテストのように、同じ人が両側に入ります。同じ人の行は利用時間や問い合わせ回数が似ているので、モデルは「この人はこう」と覚えるだけでテストを当てられます。

誤り:train = {u1(3月), u2, u3, ...}、test = {u1(4月), u5}  → 重なり {'u1'}
正しい:train = {u1, u2, u3}、valid = {u4}、test = {u5}      → 重なり なし

直し方:新しい人に使う予測なら、user_id 単位で集合を割り当て、重なりが空であることをコードで確認する(GroupShuffleSplit などの道具は 時点・個体・群の分離 で扱います)。なお、「すでにいる人の来月」を予測する問題なら、同じ人が訓練とテストにいてもよい場合があります。その場合は人ではなく時点で分けます。目的によって正しい分け方が変わります。

まとめ

用語

訓練データ(training set)

モデルと前処理のパラメータを fit で学習するためのデータ。

検証データ(validation set)

モデルの種類・ハイパーパラメータ・特徴量・区切りなどを選ぶためのデータ。

テストデータ(test set)

選択を終えた後に、最終的な性能を1回だけ見積もるために残しておくデータ。

ハイパーパラメータ(hyperparameter)

学習の前に人が決める設定(木の深さ、正則化の強さなど)。検証データで選ぶ。

分割ID(split assignment)

各個体(利用者など)をどの集合に割り当てたかの記録。再現と監査のために保存する。

よくある質問

7:2:1 や 8:1:1 の比率は必須ですか?

必須ではありません。件数と目的で決めます。評価に必要な件数(標準誤差が判断に使える程度に小さいか)と、陽性の件数をまず確保し、残りを訓練に回すのが基本です。10万件あればテスト1万件でも十分なことが多く、1000件なら交差検証の併用を考えます。

件数が少ないときはどうすればよいですか?

テストだけは別に残し、訓練の中で交差検証を使って選びます。さらに少ない研究では、入れ子の交差検証で選択と評価の両方を行います。どの場合も、同じ人や未来の行が両側に入らないように、人単位・時点単位の分け方を交差検証にも適用します。

乱数の種(seed)を固定すれば、分割は再現できますか?

データが同じなら再現できますが、行の追加や並び順の変更で分割が変わります。実務では、分割ごとの利用者IDの一覧(分割ID)とデータの版(抽出日など)を保存し、seed とあわせて記録します。

検証とテストの違いが曖昧になるのは、どんなときですか?

「テストで最終確認したら少し悪かったので、設定を1つだけ変えてもう一度テストした」というときです。1回でもテストを見て選び直したら、そのテストは検証の役割になっています。変更履歴とテストの使用回数を実験記録に残すと、曖昧さに気づけます。

仕事で、ベンダーからテストの成績を報告されたときに確認することは?

①テストを誰が持っていたか(ベンダーが自由に見られたか)、②何回評価したか、③どの単位(人・期間)で分けたか、④ベースラインの成績も同じテストで出しているか、の4つです。可能なら、発注側がテストを保管し、最終モデルを受け取ってから自分で評価します。

成績が高いほど良いモデルだと考えてよいですか?

分割と評価が正しいことが前提です。同じ人や未来の情報が混ざっていると、成績は高く出ても本番では再現しません。また、正解率が高くても、費用を考えた期待利益が基準より低いこともあります(ベースライン)。漏洩の確認と事業上の指標の両方を見て判断します。

確認問題

問1 次のうち、検証データで行うべき作業はどれか。

  1. ① ロジスティック回帰の係数を学習する
  2. ② 標準化の平均と標準偏差を計算する
  3. ③ 木の深さや分類の区切りを候補の中から選ぶ
  4. ④ 選び終えたモデルの最終成績を報告する
解答と解説

正解:③

検証データは候補の中から選ぶために使います。係数や前処理のパラメータは訓練で fit し、最終成績はテストで1回測ります。

問2 テストの結果を見て特徴量を5回入れ替え、一番良かったテスト正解率を報告した。この値の性質として正しいものはどれか。

  1. ① 新しいデータでの性能の正直な見積もりである
  2. ② 選択にテストを使ったので楽観的で、新しいデータでは下がる可能性が高い
  3. ③ 5回試したので、むしろ控えめな値である
  4. ④ テストの件数が多ければ、何回選んでも影響はない
解答と解説

正解:②

テストで選ぶと、偶然良かった分だけ成績が水増しされます。件数が多いと水増しは小さくなりますが、ゼロにはなりません。

問3 欠損値を平均で補完する。正しい手順はどれか。

  1. ① 全データの平均で補完してから分割する
  2. ② 分割してから、訓練データの平均で訓練・検証・テストのすべてを補完する
  3. ③ 分割してから、各集合をそれぞれの集合の平均で補完する
  4. ④ テストデータの平均で全体を補完する
解答と解説

正解:②

補完の値も学習するパラメータなので訓練だけで fit し、その値を他の集合に transform で当てはめます。

問4 新しく入会する利用者の解約を予測したい。1人につき毎月1行の表がある。分割の方法として最も適切なものはどれか。

  1. ① 行を無作為に7:2:1で分ける
  2. ② 利用者IDごとに集合を割り当て、同じ人の行はすべて同じ集合に入れる
  3. ③ 利用時間の多い人を訓練、少ない人をテストにする
  4. ④ すべての行を訓練に使い、訓練の成績を報告する
解答と解説

正解:②

新しい人に使うので、評価も訓練で見ていない人で行います。人単位で分けます。

問5 正解率0.8程度のモデルを100件のテストで評価した。標準誤差の大きさとして最も近いものはどれか。

  1. ① 0.004
  2. ② 0.04
  3. ③ 0.16
  4. ④ 0.4
解答と解説

正解:②

$\sqrt{0.8\times0.2/100}=\sqrt{0.0016}=0.04$ です。4ポイント程度の差は偶然と区別しにくい大きさです。

問6 テストで評価したら、検証よりかなり悪かった。適切な対応はどれか。

  1. ① テストの結果を伏せて、検証の結果を報告する
  2. ② テストデータを入れ替えて、良い結果が出るまで評価し直す
  3. ③ テストの結果をそのまま報告し、改善する場合は新しい評価計画(別の期間のデータなど)を立てる
  4. ④ テストデータを訓練に加えて、もう一度同じテストで評価する
解答と解説

正解:③

テストの結果は隠さず報告します。改善後の評価には、選択に使っていない新しいデータが必要です。

次に学ぶこと

次は 時点・個体・群の分離 で、時点・個体・群(店舗や地域)で分ける具体的な方法と道具(GroupShuffleSplit、時系列の分割)を学びます。その後 漏洩の検出 で、漏洩を見つける方法を扱います。

fit / transform の違いに不安があれば 「fit・transform・predict」(有料版)、Pipeline は 「Pipeline」(有料版)、交差検証は 「指標と交差検証」(有料版) へ戻ってください。

キャリア学習:データサイエンティスト・機械学習エンジニア・DX推進・研究者を目指す人の学習アプリ。164モジュール・984授業を、ブラウザで動くSQLとPythonの演習で。基礎と各分野の最初のモジュールは無料。全部は2,980円の買い切り。

アプリを無料で開く有料版と無料版の違い