ヘッドを訓練する
ヘッドはあなたの例で訓練する小さな層です。エンコーダーの上に載り、埋め込みをラベルの選択に変えます。訓練したヘッドはゼロショットよりはるかに正確で(下の 2 つのインテントのベンチマークで 66-73% に対して 90-94%)、確信度は較正されています。
例を集める
例とは、テキストとそれに付くべきラベルの組です。
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 を参照)。
ラベルごとにいくつ必要か
各データセットのテスト分割での精度です。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 |
おおまかな計画です。
- ラベルごとに 5 例から始め、取り分けておいたテキストで確かめます。
- 間違いを見ます。例は全ラベルに均等にではなく、混同されるラベルに足します。
- 精度が大事なところはラベルあたり 25 例にします。ラベルが BANKING77 のように近いならさらに増やします。
3 例未満のラベルがあると CLI は警告します。confidence のしきい値には精度より多くのデータが必要です。確信度としきい値を参照してください。
ラベル名
デフォルトでは各ラベルの名前をヒントに使います。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" と読みます。
訓練する
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 リファレンスにあります。デフォルトは、ラベル名を除いて上の表を計測したときの値です。
ブラウザでは decidekit/browser の createWorkerDecider() で訓練してください。同じ train() がワーカーで動きます。メインスレッドでは BANKING77 の訓練がページを 171 s 止めました。ワーカーでは最長の停止は 18 ms でした。タブの中で 10,003 例の訓練は 147-217 s、3 ラベル 9 例は 0.1 s でした。
再訓練する
例を足すか直して、同じデサイダーで train() をもう一度呼びます。前回の訓練セットの埋め込みを保持しているので、エンコードするのは新しいテキストだけです。訓練の残りは最初からやり直すので、新しいヘッドは古いヘッドに依存しません。
examples.push({ text: "card not here yet", label: "card_arrival" });
const updated = await d.train(examples);キャッシュが持つのは直前の訓練セットだけです。無関係な 2 つのセットを交互に使うと、そのたびに各セットをエンコードします。
ブラウザでのラベル付けにはこのループが向いています。クラスごとに数行へラベルを付けて訓練し、ラベルのない行に decideMany() を実行します。確信度の最も低いものから見直して追加し、再訓練します。
ヘッドを配布する
saveHead() は重みを base64 にした JSON を書き出します。77 ラベルで 156 KB、150 ラベルで 303 KB です。静的ファイルとして配信し、loadHead() で読み込みます。
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
コードを書かずに CSV や JSONL のファイルから訓練できます。
npx decidekit train tickets.csv -o support.head.json --types
npx decidekit decide support.head.json "my card never arrived" --min-confidence 0.9CSV には text と label の列を持つヘッダーが必要です。JSONL は 1 行に 1 つの {"text", "label"} オブジェクトです。すべてのフラグは API リファレンスにあります。
--types
--types はヘッドの隣に support.head.labels.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 の型があなたのラベルになります。
import type { Label } from "./support.head.labels.ts";
const head = loadHead<Label>(json);
const r = await d.decide(text, head);
r.choice; // "card_arrival" | "refund"ラベルのファイルはヘッドと一緒にコミットし、両方を一緒に再生成してください。loadHead は渡された型を信じ、ファイルと照合しません。
モデルを選ぶ
ここで計測した英語向けのエンコーダーは 2 つです。他の言語は FAQ を参照してください。
| 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" }) が必要です。
const d = await createDecider({ model: "debuffjp/decide-small" });
const head = await d.train(examples);ONNX の重みを持つ Hugging Face の文埋め込みモデルなら model に渡せますが、他のモデルは計測していません。