見たことのあるテスト
学習、検証、テストは、例を三つに分けた山です。モデルは一つ目から学び、二つ目で調整し、三つ目で良し悪しを判断します。
テストのケースが学習の山にもあると、モデルはそれを覚えているだけかもしれません。点数は良く見えても、何も意味しません。
このレッスンのゴール
メモ
テストのケースを、学習データの外に置ける。
キーワード
| ことば | いみ |
|---|---|
| 学習用 | モデルが学ぶ例 |
| 検証用 | 選び方を調整するための例 |
| テスト用 | 結果の判断のために取っておく例 |
| 漏れ | テストのケースが学習にもあること |
| 重複 | 同じ例が二度あること |
学生が入力するセルは、このプレビューでは空白です。
三つの山と、三つの役目
学習の前に、確かめたファイルを分けます。ほとんどの例は学習用に入れます。小さい検証用の山は、進めながら選び方を比べるために使います。テスト用は、最後に一度だけ使います。
分け方は、位置のような決まった規則にします。そうすれば、毎回同じ行が同じ山に入ります。
例: 十行を三つの山に
| 山 | 行 | 使い道 |
|---|---|---|
| 学習用 | 1 から 6 | 学ぶ |
| 検証用 | 7 と 8 | 版の選び分け |
| テスト用 | 9 と 10 | 最後の判断を一回 |
学生が入力するセルは、このプレビューでは空白です。
近い写しも、漏れ
1 行目は「荷物はどこですか」で、7 行目は「荷物はどこですか?」です。言葉は同じなので、モデルはもう答えを見ています。
こうした写しを見つけるには、まず二つの文を簡単な形にします。小文字にして、句読点も空白も捨て、文字と数字だけを残します。そのうえで比べます。
よくある間違い
注意
「ファイルをでたらめに混ぜれば、テスト用は安全だ」と考えがちです。混ぜると、同じ例の写しが別々の山に散るだけです。先に重複を取り除いてから、分けます。
モデル用の補助を書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// 応答に含まれるトークン数。なければ null。' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
モデル用の補助を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"status \" + res.status);' ' const d = await res.json();' ' return { text: d.choices[0].message.content, inTokens: count(d, \"prompt_tokens\"), outTokens: count(d, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"記録済みサンプル: モデルが答えませんでした (\" + error.message + \")。\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
漏れを探すスクリプトを書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const rows = [' ' \"荷物はどこですか\", \"二重に請求されました\", \"注文をキャンセルしてください\",' ' \"箱がつぶれて届きました\", \"請求書の金額が違います\", \"住所を変えられますか\",' ' \"荷物はどこですか?\", \"いいお店ですね\", \"返金がまだ届きません\", \"日曜日も店は開いていますか\",' '];' > adapt3.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
漏れを探すスクリプトを書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function normal(text) {' ' let out = \"\";' ' for (const ch of text.toLowerCase()) {' ' if (/[\p{L}\p{N}]/u.test(ch)) out = out + ch;' ' }' ' return out;' '}' 'const train = rows.slice(0, 6);' 'const validation = rows.slice(6, 8);' 'const test = rows.slice(8);' >> adapt3.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
漏れを探すスクリプトを書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function leaks(from, into) {' ' const seen = from.map(normal);' ' return into.filter(function (t) { return seen.includes(normal(t)); });' '}' 'async function main() {' ' console.log(\"学習 \" + train.length + \"、検証 \" + validation.length + \"、テスト \" + test.length);' ' console.log(\"検証への漏れ: \" + leaks(train, validation).join(\" | \"));' ' console.log(\"テストへの漏れ: \" + leaks(train, test).length);' ' const q = \"テスト用のケースを学習データに入れてはいけないのは、なぜですか。短い一文で答えてください。\";' ' const sample = \"モデルは見た例をそのまま繰り返せるので、漏れたケースがあると点数が実際より良く見えるからです。\";' ' const reply = await ask([{ role: \"user\", content: q }], sample, 60);' ' console.log(\"返事: \" + reply.text);' '}' 'main();' >> adapt3.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
漏れを探す
docker exec -w /home/node/project agent node adapt3.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
探した結果
Run は、架空の十文を位置で分け、それぞれを簡単な形にして比べます。漏れは、検証用の山に一つ見つかり、テスト用の山にはありません。
最後の行は、短いモデルの呼び出しか、「記録済みサンプル」です。