本文へスキップ

ヘッドを訓練する ​

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

例を集める ​

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

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 を参照)。

ラベルごとにいくつ必要か ​

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

ラベルごとの例51025すべて
BANKING77(77 ラベル)83.7%87.5%90.2%93.1%
CLINC150(150 ラベル)90.2%92.8%94.3%96.3%
較正誤差(ECE)、BANKING770.0500.0150.0130.013
較正誤差(ECE)、CLINC1500.0200.0150.0160.008

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

ラベルごとの例51025すべて
BANKING77(77 ラベル)82.3%83.8%88.7%90.7%
CLINC150(150 ラベル)89.6%92.0%93.4%94.4%
較正誤差(ECE)、BANKING770.0710.0290.0110.017
較正誤差(ECE)、CLINC1500.0280.0100.0150.008

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

ラベルごとの例51025すべて
BANKING77(77 ラベル)78.7%83.0%87.4%90.6%
CLINC150(150 ラベル)87.3%89.8%92.8%94.0%
較正誤差(ECE)、BANKING770.0650.0480.0250.019
較正誤差(ECE)、CLINC1500.0290.0130.0120.012

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

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

ラベルごとの例1235
精度93.4%96.9%97.8%98.4%
ECE0.0200.0280.0160.008

おおまかな計画です。

  1. ラベルごとに 5 例から始め、取り分けておいたテキストで確かめます。
  2. 間違いを見ます。例は全ラベルに均等にではなく、混同されるラベルに足します。
  3. 精度が大事なところはラベルあたり 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" と読みます。

訓練する ​

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 リファレンスにあります。デフォルトは、ラベル名を除いて上の表を計測したときの値です。

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

再訓練する ​

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

ts
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() で読み込みます。

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 ​

コードを書かずに 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 リファレンスにあります。

--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<Label>(json);
const r = await d.decide(text, head);
r.choice; // "card_arrival" | "refund"

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

モデルを選ぶ ​

ここで計測した英語向けのエンコーダーは 2 つです。他の言語は FAQ を参照してください。

all-MiniLM-L6-v2Decide Small、bge-small-en-v1.5(デフォルト)
モデル IDXenova/all-MiniLM-L6-v2debuffjp/decide-small
ダウンロードq8 で 23.7 MBq8 で 34 MB
ラベルごとに 5 例、BANKING7778.7%82.3%
ラベルごとに 5 例、CLINC15087.3%89.6%
全データ、BANKING7790.6%90.7%
全データ、CLINC15094.0%94.4%
判定 1 回の時間、Chrome の中央値4.2 ms7.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 に渡せますが、他のモデルは計測していません。