# ヘッドを訓練する

ヘッドはあなたの例で訓練する小さな層です。エンコーダーの上に載り、埋め込みをラベルの選択に変えます。訓練したヘッドはゼロショットよりはるかに正確で（下の 2 つのインテントのベンチマークで 66-73% に対して 90-94%）、確信度は較正されています。

## 例を集める {#collect-examples}

例とは、テキストとそれに付くべきラベルの組です。

```ts
const examples = [
  { text: "my card still hasn't come", label: "card_arrival" },
  { text: "can I get a refund for this", label: "refund" },
  // ...
];
```

モデルを使う場所から集めてください。過去のサポートチケット、実際の検索クエリ、レビューなどです。アプリに入力する人たちが書いたテキストは、自分で作ったテキストに勝ります。その人たちの綴り、長さ、癖があるからです。

- テキストはユーザーの書き方で書いてください。メッセージが短くて小文字なら、例もそうします。
- ラベルごとに変化を付けてください。1 つの文の言い換え 5 つより、同じことを頼む 5 通りの言い方のほうが多くを教えます。
- ラベルは区別できるようにしてください。テキストだけで 2 つのラベルを見分けられないなら、ヘッドにもできません。統合してください。
- テキストの重複はかまいません。エンコードは 1 回だけです。
- テキストは段落 1 つ程度まで読みます。それより長いものはエンコーダーが切ります（[FAQ](/ja/decide/docs/faq#can-it-read-long-texts) を参照）。

## ラベルごとにいくつ必要か {#how-many-per-label}

各データセットのテスト分割での精度です。3 シードの平均で、デフォルトのモデル（q8 の bge-small-en-v1.5）を使いました。

| ラベルごとの例 | 5 | 10 | 25 | すべて |
|---|---|---|---|---|
| BANKING77（77 ラベル） | 83.7% | 87.5% | 90.2% | 93.1% |
| CLINC150（150 ラベル） | 90.2% | 92.8% | 94.3% | 96.3% |
| 較正誤差（ECE）、BANKING77 | 0.050 | 0.015 | 0.013 | 0.013 |
| 較正誤差（ECE）、CLINC150 | 0.020 | 0.015 | 0.016 | 0.008 |

出典：`eval/results-h14.json`、バージョン 0.2、ラベル名あり。0.2 より前（すべての規模で SGD、ラベル名なし、`eval/results-bge-small.json`）：

| ラベルごとの例 | 5 | 10 | 25 | すべて |
|---|---|---|---|---|
| BANKING77（77 ラベル） | 82.3% | 83.8% | 88.7% | 90.7% |
| CLINC150（150 ラベル） | 89.6% | 92.0% | 93.4% | 94.4% |
| 較正誤差（ECE）、BANKING77 | 0.071 | 0.029 | 0.011 | 0.017 |
| 較正誤差（ECE）、CLINC150 | 0.028 | 0.010 | 0.015 | 0.008 |

出典：`eval/results-bge-small.json`（ラベル名なしで計測）。リリース前のデフォルトの all-MiniLM-L6-v2 では次のとおりです（`eval/results.json`）。

| ラベルごとの例 | 5 | 10 | 25 | すべて |
|---|---|---|---|---|
| BANKING77（77 ラベル） | 78.7% | 83.0% | 87.4% | 90.6% |
| CLINC150（150 ラベル） | 87.3% | 89.8% | 92.8% | 94.0% |
| 較正誤差（ECE）、BANKING77 | 0.065 | 0.048 | 0.025 | 0.019 |
| 較正誤差（ECE）、CLINC150 | 0.029 | 0.013 | 0.012 | 0.012 |

BANKING77 のラベルは互いに近い（カードと送金のインテントが多い）ので、150 のインテントが 10 のドメインに広がる CLINC150 より多くの例が必要です。

小さなタスクならもっと少なくて済みます。CLINC150 のラベルを 2〜10 個抜き出したタスクをサイズごとに 60 個作り、その検証クエリで採点しました（all-MiniLM-L6-v2、`eval/tiny.ts`）。

| ラベルごとの例 | 1 | 2 | 3 | 5 |
|---|---|---|---|---|
| 精度 | 93.4% | 96.9% | 97.8% | 98.4% |
| ECE | 0.020 | 0.028 | 0.016 | 0.008 |

おおまかな計画です。

1. ラベルごとに 5 例から始め、取り分けておいたテキストで確かめます。
2. 間違いを見ます。例は全ラベルに均等にではなく、混同されるラベルに足します。
3. 精度が大事なところはラベルあたり 25 例にします。ラベルが BANKING77 のように近いならさらに増やします。

3 例未満のラベルがあると CLI は警告します。`confidence` のしきい値には精度より多くのデータが必要です。[確信度としきい値](/ja/decide/docs/guide-confidence)を参照してください。

## ラベル名 {#label-names}

デフォルトでは各ラベルの名前をヒントに使います。`card_arrival` は "card arrival" と読み、訓練の開始点で追加の 1 例として数えます。bge-small でラベルごとに 5 例のとき、dev の精度は BANKING77 で 80.8% から 81.8% に、CLINC150 で 90.4% から 91.4% に上がりました。ラベルごとに 1 例なら 58% → 73%、69% → 84% です。リリース前のビルド（all-MiniLM-L6-v2）は名前を 3 例と数えていました。そこではラベルごとに 5 例でのテスト精度が、BANKING77 で 77.4% から 78.7% に、CLINC150 で 86.0% から 87.3% に上がりました。この重みでは、bge-small はラベルごとに 5 例で良くなりませんでした（dev 分割で BANKING77 79.3%、CLINC150 90.5%）。そのため 0.1.0 より前のドキュメントは bge-small に `labelNames: false` を勧めていました。重み 1 ならオンのままで構いません（`eval/results-priors.jsonl`）。

次の場合は `{ labelNames: false }`（CLI では `--no-label-names`）でオフにしてください。

- ラベルが意味のない ID（`c17`、`INTENT_042`）や、単語に意味のないコードのとき。
- ラベルの名前が例の意味と違うとき（`other`、`misc`、社内のチーム名）。

ラベルは単語として比べます。`_` と `-` は空白になり、camelCase は分割されます。`cardArrival`、`card-arrival`、`card_arrival` はどれも "card arrival" と読みます。

## 訓練する {#train}

```ts
import { createDecider, saveHead } from "decidekit";
import { writeFile } from "node:fs/promises";

const d = await createDecider();
const head = await d.train(examples, {
  onEpoch: (epoch, acc) => console.log(epoch, acc),
});
await writeFile("support.head.json", saveHead(head));
```

ヘッドのラベルは `examples` の中の重複のないラベルを並べ替えたものです。同じ例と `seed` なら、訓練は正確に再現されます。他のオプションは [API リファレンス](/ja/decide/docs/api#trainoptions)にあります。デフォルトは、ラベル名を除いて上の表を計測したときの値です。

ブラウザでは `decidekit/browser` の `createWorkerDecider()` で訓練してください。同じ `train()` がワーカーで動きます。メインスレッドでは BANKING77 の訓練がページを 171 s 止めました。ワーカーでは最長の停止は 18 ms でした。タブの中で 10,003 例の訓練は 147-217 s、3 ラベル 9 例は 0.1 s でした。

## 再訓練する {#retrain}

例を足すか直して、同じデサイダーで `train()` をもう一度呼びます。前回の訓練セットの埋め込みを保持しているので、エンコードするのは新しいテキストだけです。訓練の残りは最初からやり直すので、新しいヘッドは古いヘッドに依存しません。

```ts
examples.push({ text: "card not here yet", label: "card_arrival" });
const updated = await d.train(examples);
```

キャッシュが持つのは直前の訓練セットだけです。無関係な 2 つのセットを交互に使うと、そのたびに各セットをエンコードします。

ブラウザでのラベル付けにはこのループが向いています。クラスごとに数行へラベルを付けて訓練し、ラベルのない行に `decideMany()` を実行します。確信度の最も低いものから見直して追加し、再訓練します。

## ヘッドを配布する {#ship-a-head}

`saveHead()` は重みを base64 にした JSON を書き出します。77 ラベルで 156 KB、150 ラベルで 303 KB です。静的ファイルとして配信し、`loadHead()` で読み込みます。

```ts
import { createWorkerDecider, loadHead } from "decidekit/browser";

const head = loadHead(await (await fetch("/support.head.json")).text());
const d = await createWorkerDecider();
const r = await d.decide(text, head);
```

ヘッドは訓練に使ったエンコーダーでしか動きません。モデル ID を記録していて、デサイダーが別のモデルを使っていると `decide()` は例外を投げます。モデルを変えたら再訓練してください。

## CLI {#the-cli}

コードを書かずに CSV や JSONL のファイルから訓練できます。

```sh
npx decidekit train tickets.csv -o support.head.json --types
npx decidekit decide support.head.json "my card never arrived" --min-confidence 0.9
```

CSV には `text` と `label` の列を持つヘッダーが必要です。JSONL は 1 行に 1 つの `{"text", "label"}` オブジェクトです。すべてのフラグは [API リファレンス](/ja/decide/docs/api#cli)にあります。

### --types {#types}

`--types` はヘッドの隣に `support.head.labels.ts` を書き出します。

```ts
// Written by decidekit train --types. Labels of the head next to this file.
export const labels = ["card_arrival","refund"] as const;
export type Label = (typeof labels)[number];
```

型を `loadHead` に渡すと、`choice` の型があなたのラベルになります。

```ts
import type { Label } from "./support.head.labels.ts";

const head = loadHead(json);
const r = await d.decide(text, head);
r.choice; // "card_arrival" | "refund"
```

ラベルのファイルはヘッドと一緒にコミットし、両方を一緒に再生成してください。`loadHead` は渡された型を信じ、ファイルと照合しません。

## モデルを選ぶ {#choosing-a-model}

ここで計測した英語向けのエンコーダーは 2 つです。他の言語は [FAQ](/ja/decide/docs/faq#which-languages-does-it-support) を参照してください。

| | all-MiniLM-L6-v2 | Decide Small、bge-small-en-v1.5（デフォルト） |
|---|---|---|
| モデル ID | `Xenova/all-MiniLM-L6-v2` | `debuffjp/decide-small` |
| ダウンロード | q8 で 23.7 MB | q8 で 34 MB |
| ラベルごとに 5 例、BANKING77 | 78.7% | 82.3% |
| ラベルごとに 5 例、CLINC150 | 87.3% | 89.6% |
| 全データ、BANKING77 | 90.6% | 90.7% |
| 全データ、CLINC150 | 94.0% | 94.4% |
| 判定 1 回の時間、Chrome の中央値 | 4.2 ms | 7.7 ms |
| ラベル名 | 効果あり | 重み 1 で効果あり |

bge-small の数字はラベル名なしのものです（`eval/results-bge-small.json`）。0.1.1 より前の ID は `Xenova/bge-small-en-v1.5` で、重みは同じです。この ID とそれを記録したヘッドは今も動きます。ラベルごとの例がわずかなら bge-small のほうが良い出発点で、それがデフォルトになった理由です。リリース前のビルドのデフォルトは all-MiniLM-L6-v2 でした。データが十分なら 2 つは近く、MiniLM のほうが小さく速くなります。MiniLM で訓練したヘッドには `createDecider({ model: "Xenova/all-MiniLM-L6-v2" })` が必要です。

```ts
const d = await createDecider({ model: "debuffjp/decide-small" });
const head = await d.train(examples);
```

ONNX の重みを持つ Hugging Face の文埋め込みモデルなら `model` に渡せますが、他のモデルは計測していません。