Loss が悪化したら学習を自動で打ち切る(Early Stopping)
Loss(モデルの誤差)が悪化したら学習を自動 Abort し、バックエンドの GPU も止める。
「Early Stopping(早期停止)」は、学習の指標が改善しなくなったり悪化し始めたら、最後まで回さず途中で打ち切るパターンです。Arkor に Early Stopping は組み込まれていませんが、TypeScript 数行で組み立てるための材料はすべて揃っています。
このレシピは 3 つのプリミティブを組み合わせます:
- バックエンドからストリームされる loss を見る
onLog。 - そのシグナルをトレーナーに配線した
AbortController。 - ローカル Abort 後にバックエンドの学習も止める
trainer.cancel()。
パターン
// src/arkor/trainer.ts
import { createTrainer } from "arkor";
const LOSS_CEILING = 5.0; // データセットに合わせて調整
export function makeTrainer() {
const controller = new AbortController();
const trainer = createTrainer({
name: "support-bot-v1",
model: "unsloth/gemma-4-E4B-it",
dataset: { type: "huggingface", name: "arkorlab/triage-demo" },
lora: { r: 16, alpha: 16 },
maxSteps: 100,
abortSignal: controller.signal,
callbacks: {
onLog: ({ step, loss }) => {
if (loss === null) return;
if (!Number.isFinite(loss) || loss > LOSS_CEILING) {
console.warn(`step=${step} loss=${loss} exceeds ceiling, aborting`);
controller.abort();
}
},
},
});
return { trainer, controller };
}その後、Abort に反応できるよう自分で学習を実行します:
// src/arkor/index.ts
import { createArkor } from "arkor";
import { makeTrainer } from "./trainer";
const { trainer, controller } = makeTrainer();
export const arkor = createArkor({ trainer });
// CLI 外で学習を走らせたい場合:
async function main() {
try {
await trainer.wait();
} catch (err) {
if (controller.signal.aborted) {
// ローカル abort で wait() が reject。続けてバックエンドの学習を止め、
// GPU を回し続けないようにする。
try {
await trainer.cancel();
} catch {
// ベストエフォート。ジョブが既に終わっていれば cancel は reject し得る
}
return;
}
throw err;
}
}arkor start(Studio の "Run training" や CLI)を使い続けるなら上のエクスポートだけで十分です: controller は依然として wait() を Abort しますが、CLI の await を自前の try / catch で囲むことはできません。バックエンドのキャンセルを保証したいなら上のパターンが安全策です。
なぜ abortSignal と cancel の両方?
abortSignal と cancel は別物です。混同すると GPU 料金が無駄になるので、ドキュメントもここを明示しています。
abortSignalはローカルのwait()ループを止めます(SDK § トレーナー制御)。cancelを呼びませんし、バックエンドにも何も送らず、ジョブはマネージド GPU で走り続けます。trainer.cancel()はバックエンドにジョブの停止を依頼します。ベストエフォート: ジョブが既に終端状態(completed、failed、cancelled)にあるとリクエストは reject されることがあります。投機的に呼ぶならtry / catchで囲んでください。
「もう待ちたくない」だけなら abortSignal で十分。「もう課金させたくない」なら Abort の後に cancel も呼ぶこと。
バリエーション
スムージングしたしきい値。 1 ステップだけの悪い値はノイズかもしれません。クロージャー内でローリングウィンドウを保持:
const recent: number[] = [];
const WINDOW = 10;
onLog: ({ step, loss }) => {
if (loss === null || !Number.isFinite(loss)) return;
recent.push(loss);
if (recent.length > WINDOW) recent.shift();
const avg = recent.reduce((a, b) => a + b, 0) / recent.length;
if (recent.length === WINDOW && avg > LOSS_CEILING) {
controller.abort();
}
}改善が止まったら止める。 これまでで一番低かった Loss を覚えておき、それを更新できないステップが一定数続いたら打ち切ります(機械学習では「patience(忍耐)」と呼ばれるパターンです):
let best = Infinity;
let stalled = 0;
const PATIENCE = 30;
onLog: ({ step, loss }) => {
if (loss === null || !Number.isFinite(loss)) return;
if (loss < best) {
best = loss;
stalled = 0;
} else {
stalled++;
if (stalled >= PATIENCE) {
console.warn(`no improvement for ${PATIENCE} steps, aborting at ${step}`);
controller.abort();
}
}
}外部トリガー。 同じ controller はトレーナーファイルの外からも動きます。Next.js API ルート、SIGINT ハンドラー、親プロセスから controller.abort() を呼べばオンデマンドに学習を止められます。
心に留めておくこと
- コールバック内で throw しない。 throw は即座に
wait()を reject します。SSE 再接続ループには回されず、リトライもされません(このループはトランスポート障害のみを扱います)。そのため throw で学習を止めようとしても、きれいに止まるのではなくwait()がエラーで突然 abort するだけです(SDK § ライフサイクルコールバック 参照)。controller を使うこと。これが決定的に動かす唯一の方法です。 abortSignalはバックエンドをキャンセルしない。 一番よくある落とし穴です。コストが問題ならcontroller.abort()とtrainer.cancel()を必ずペアで。lossフィールドはnumber | null。 バックエンドはメトリックスステップでしか loss を埋めません。非メトリックスフレームはnullを運びます。Number.isFiniteチェックはNaNも弾き、実用上はこちらの方が発散シグナルとしてよく出てきます。