訓練・検証・テスト|機械学習の問題設定と評価分割
このページはプロモーション(広告)を含みます。
機械学習の問題設定と評価分割(中級・読む30分+演習40分)
この授業のゴール
- 訓練・検証・テストの3つの集合の役割(学習・選択・最終確認)を説明し、各作業をどの集合で行うか判断できる
- 利用時点と個体(人)に沿った分割を作り、集合ごとの人数・行数と重なりがないことを示せる
- テストを繰り返し見て選ぶと評価が楽観的になることを、シミュレーションの結果で説明できる
- 前処理(補完・標準化)を訓練データだけで fit する理由を、数値例で説明できる
仕事でどう使うか
モデルの評価値は「本番で新しいデータに使ったときの性能」の見積もりです。見積もりが甘いと、導入後に「検証では良かったのに」という事態になります。3つの集合を役割どおりに使うことは、この見積もりを正直にするための約束事です。
| 路線 | この授業の内容が使われる成果物・意思決定 |
|---|---|
| DS | 予測評価の報告書。「検証で選び、テストで1回確認した値」と、比較したベースラインを並べて書く |
| ML | 実験管理。どの集合で何を選んだか(特徴量・ハイパーパラメータ・区切り)を記録し、テストの使用回数を管理する |
| DX | 受け入れ検査。ベンダーにテストデータを渡さず、発注側が最後に評価する体制をつくる |
| RS | 比較実験。手法の調整に使ったデータと、最終比較に使うデータを分け、論文に書く |
この授業では、ベースライン の期待利益をそのまま使い、「検証で区切りを選び、テストで1回だけ確かめる」流れを実行します。
前提
次の言葉と操作を使います。
- fit / transform / predict と、前処理を訓練で fit する理由 → 「fit・transform・predict」(有料版)
- train_test_split・層化分割 → 「学習と評価の分割」(有料版)
- Pipeline(前処理とモデルをつなぎ、fit を一度で行う仕組み)→ 「Pipeline」(有料版)
- ベースラインと期待利益 → ベースライン
- 予測時点・スナップショット(同じ人が複数の時点に出る)→ 目的変数と利用時点
この授業で使う架空の表は、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つの集合と、分ける単位
- 訓練データ:モデルのパラメータ(ロジスティック回帰の係数など)と、前処理のパラメータ(補完の平均値、標準化の平均・標準偏差、カテゴリの一覧)を
fitで学習するデータ。 - 検証データ:候補の中から選ぶためのデータ。選ぶ対象は、モデルの種類、ハイパーパラメータ(学習前に人が決める設定。木の深さ、正則化の強さなど)、特徴量の組み合わせ、分類の区切り(しきい値)など。
- テストデータ:すべての選択を終えた後に、最終的な性能を1回だけ見積もるデータ。結果を見て選び直さない。
分け方は、次の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 円
結果の意味:
- 区切りは検証データで選びました(0.1)。検証の217,200円は「選んだ結果」なので、少し楽観的な値です。
- テストで1回だけ確認すると157,600円でした。検証より低いのは、選択による楽観と、テストの解約率(19.0%)が検証(21.0%)より低いことの両方が考えられます。
- 同じテストで基準「全員に電話」は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%)に戻ります。選ぶ回数が多いほど、選んだものの成績は偶然の分だけ良く見えます。テストを見て特徴量や設定を何度も直すと、これと同じことが起きます。
次のように書き換えて試してください。
n_models = 100を1000にすると? → 偶然の最大値はさらに大きくなり、テストの正解率はもっと良く見えます。n_test = 200を2000にすると? → 1回ごとのばらつきが小さくなるので、選んだモデルの「水増し」は小さくなります(ただしゼロにはなりません)。
実務での対策は、①選択はすべて検証(または訓練内の交差検証)で行う、②テストを見た回数を記録する、③テストの結果が悪くても隠さず報告し、改善するなら新しいテストデータ(次の期間のデータなど)を用意する、の3つです。
発展:テストの使い方の細かい規則と限界
- テストは本当に1回だけか:実務では、最終候補が2〜3個残り、テストで比べることもあります。その場合は「テストで3候補を比べた」と報告に書き、選んだものの値は少し楽観的だと明記します。比べた回数が増えるほど、漏洩の検出 で扱う「テストへの過剰適合」が強くなります。
- 入れ子の交差検証:件数が少なく、テストを別に取れない研究では、外側の交差検証で性能を見積もり、内側の交差検証で設定を選ぶ方法を使います。計算は増えますが、選択の楽観を避けられます。
- Pipelineでも解決しないこと:Pipeline は「前処理を訓練で fit する」ことを守りますが、分割そのものが誤っている(同じ人が両側にいる、未来の行が訓練にある)問題は解決しません。分割の正しさは、人数・期間・重なりを表示して自分で確認します。
- テストが本番を代表しない:テストが過去のデータなら、本番の時期に利用者の構成が変わると性能は変わります。導入後も、予測と実際の結果を記録して定期的に評価し直します(モニタリング)。
- 再現のための記録:乱数の種(seed)だけでは、データが更新されると同じ分割になりません。分割ごとの
user_idの一覧(分割ID)とデータの版を保存します(「保存と再現する環境」(有料版))。
よくある失敗
失敗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 などの道具は 時点・個体・群の分離 で扱います)。なお、「すでにいる人の来月」を予測する問題なら、同じ人が訓練とテストにいてもよい場合があります。その場合は人ではなく時点で分けます。目的によって正しい分け方が変わります。
まとめ
- 訓練は学習(モデルと前処理の fit)、検証は選択、テストは選択後の最終確認に1回だけ使う。
- 分割は比率より先に、予測の対象(新しい人か)と利用時点に合わせて単位を決める。
- 分割を作ったら、集合ごとの人数・行数と重なりがないことを毎回表示する。
- テストを見て選び直すと、偶然の分だけ成績が水増しされる。悪い結果も隠さず報告する。
- 補完や標準化も訓練だけで fit する。Pipeline は前処理の順序を守るが、分割の誤りは直さない。
用語
- 訓練データ(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 次のうち、検証データで行うべき作業はどれか。
- ① ロジスティック回帰の係数を学習する
- ② 標準化の平均と標準偏差を計算する
- ③ 木の深さや分類の区切りを候補の中から選ぶ
- ④ 選び終えたモデルの最終成績を報告する
解答と解説
正解:③
検証データは候補の中から選ぶために使います。係数や前処理のパラメータは訓練で fit し、最終成績はテストで1回測ります。
問2 テストの結果を見て特徴量を5回入れ替え、一番良かったテスト正解率を報告した。この値の性質として正しいものはどれか。
- ① 新しいデータでの性能の正直な見積もりである
- ② 選択にテストを使ったので楽観的で、新しいデータでは下がる可能性が高い
- ③ 5回試したので、むしろ控えめな値である
- ④ テストの件数が多ければ、何回選んでも影響はない
解答と解説
正解:②
テストで選ぶと、偶然良かった分だけ成績が水増しされます。件数が多いと水増しは小さくなりますが、ゼロにはなりません。
問3 欠損値を平均で補完する。正しい手順はどれか。
- ① 全データの平均で補完してから分割する
- ② 分割してから、訓練データの平均で訓練・検証・テストのすべてを補完する
- ③ 分割してから、各集合をそれぞれの集合の平均で補完する
- ④ テストデータの平均で全体を補完する
解答と解説
正解:②
補完の値も学習するパラメータなので訓練だけで fit し、その値を他の集合に transform で当てはめます。
問4 新しく入会する利用者の解約を予測したい。1人につき毎月1行の表がある。分割の方法として最も適切なものはどれか。
- ① 行を無作為に7:2:1で分ける
- ② 利用者IDごとに集合を割り当て、同じ人の行はすべて同じ集合に入れる
- ③ 利用時間の多い人を訓練、少ない人をテストにする
- ④ すべての行を訓練に使い、訓練の成績を報告する
解答と解説
正解:②
新しい人に使うので、評価も訓練で見ていない人で行います。人単位で分けます。
問5 正解率0.8程度のモデルを100件のテストで評価した。標準誤差の大きさとして最も近いものはどれか。
- ① 0.004
- ② 0.04
- ③ 0.16
- ④ 0.4
解答と解説
正解:②
$\sqrt{0.8\times0.2/100}=\sqrt{0.0016}=0.04$ です。4ポイント程度の差は偶然と区別しにくい大きさです。
問6 テストで評価したら、検証よりかなり悪かった。適切な対応はどれか。
- ① テストの結果を伏せて、検証の結果を報告する
- ② テストデータを入れ替えて、良い結果が出るまで評価し直す
- ③ テストの結果をそのまま報告し、改善する場合は新しい評価計画(別の期間のデータなど)を立てる
- ④ テストデータを訓練に加えて、もう一度同じテストで評価する
解答と解説
正解:③
テストの結果は隠さず報告します。改善後の評価には、選択に使っていない新しいデータが必要です。
次に学ぶこと
次は 時点・個体・群の分離 で、時点・個体・群(店舗や地域)で分ける具体的な方法と道具(GroupShuffleSplit、時系列の分割)を学びます。その後 漏洩の検出 で、漏洩を見つける方法を扱います。
fit / transform の違いに不安があれば 「fit・transform・predict」(有料版)、Pipeline は 「Pipeline」(有料版)、交差検証は 「指標と交差検証」(有料版) へ戻ってください。
キャリア学習:データサイエンティスト・機械学習エンジニア・DX推進・研究者を目指す人の学習アプリ。164モジュール・984授業を、ブラウザで動くSQLとPythonの演習で。基礎と各分野の最初のモジュールは無料。全部は2,980円の買い切り。
アプリを無料で開く有料版と無料版の違い