API リファレンス
decidekit 0.1.1。次のエントリーと CLI があります。
decidekit:createDecider、saveHead、loadHeadと型。Node とブラウザで動きます。decidekit/browser:createWorkerDecider、saveHead、loadHeadと同じ型。ブラウザ専用です(Web Worker を起動します)。decidekit/react:useDeciderとuseDecision。decidekit/switch:switchDeciderとtextOf。decidekit/typesafe:typesafeClientとchoice。TypeSafe の SDK の形です。npx decidekit:コマンドラインのtrainとdecide(Node 22.18 以降)。
createDecider(options?)
function createDecider(options?: DeciderOptions): Promise<{
model: string;
decide: ...; // see decide()
decideMany: ...; // see decideMany()
train: ...; // see train()
embed: (texts: readonly string[]) => Promise<number[][]>;
}>;transformers.js で文エンコーダーを読み込み、準備ができると解決します。最初の呼び出しで Hugging Face(または modelHost)からモデルファイルをダウンロードします。以後はキャッシュ(ブラウザのキャッシュか、Node での transformers.js のキャッシュディレクトリ)から読み込みます。
エンコーダーは呼び出したスレッドで動きます。ブラウザでは代わりに createWorkerDecider() を使ってください。そうしないと処理中にページが固まります。
import { createDecider } from "decidekit";
const d = await createDecider();
const r = await d.decide("my card never arrived", ["card_arrival", "refund", "other"]);DeciderOptions
| オプション | 型 | デフォルト | 働き |
|---|---|---|---|
model | string | "debuffjp/decide-small" | ONNX の重みを持つ文埋め込みモデルの Hugging Face ID。0.1.1 より前のデフォルト ID は "Xenova/bge-small-en-v1.5" で、重みは同じです。この ID と "Xenova/multilingual-e5-small" は今も動き、"debuffjp/decide-small" と "debuffjp/decide-multilingual" に解決されます。0.1.0 より前のデフォルトは "Xenova/all-MiniLM-L6-v2" でした。それで訓練したヘッドを使い続けるなら渡してください。 |
device | "wasm" | "webgpu" | "cpu" | transformers.js の選択(ブラウザは wasm、Node は cpu) | エンコーダーを動かす場所。 |
dtype | "q8" | "fp16" | "fp32" | "q8" | エンコーダーの重みの精度。 |
modelHost | string | なし(Hugging Face) | 0.1.1 で追加。モデルファイルを <modelHost>/<model>/ から配信します。ページまたはプロセス全体に適用されます。セルフホストを参照してください。 |
onProgress | (p: { file: string; loaded: number; total: number }) => void | なし | モデルファイルのダウンロード中に呼ばれます。ダウンロード時だけで、キャッシュからの読み込みでは呼ばれません。 |
temperature | number | bge-small は 0.03(説明付きラベルは 0.025)、他のモデルは 0.05(0.1.0 より前はすべて 0.05) | ゼロショットの類似度に使う softmax の温度。設定すると名前と説明付きラベルに同じ値を使います。独自の温度を持つ訓練済みヘッドには影響しません。 |
返される model は使用中のモデル ID です。古い ID は新しい ID に解決されます。createDecider({ model: "Xenova/bge-small-en-v1.5" }) の model は "debuffjp/decide-small" です。訓練済みヘッドはこの ID を記録していて、別のモデルで訓練したヘッドを decide() は拒否します。古い ID を記録したヘッドは新しい ID で訓練したものとして扱います。0.1.1 から、2 つの Decide モデルのダウンロードはリビジョンに固定されます。Hugging Face 側の変更でインストール済みのバージョンの判定は変わりません。
decide(text, using, options?)
decide<const L extends string>(text: string, using: Choices<L>): Promise<Decision<L>>;
decide<const L extends string>(text: string, using: Choices<L>, options: DecideOptions): Promise<Decision<L, L | null>>;text に対して選択肢を 1 つ選びます。using は次のいずれかです。
- ラベルの配列(ゼロショット)。各ラベルは読みやすい単語としてテキストと比べます。
_と-は空白になり、camelCase は分割され、小文字になります。そのためcard_arrivalとcardArrivalはどちらも "card arrival" と読みます。 - ラベルから説明へのレコード(ゼロショット)。読みやすい名前と、書かれたとおりの説明の両方をテキストと比べ、各ラベルは良いほうのスコアになります。
choiceが返すのはキーです。 - 訓練済みの
Head。ヘッドの重みでテキストのスコアを出し、当てはめた温度で確率にします。
const L のおかげで、リテラルの配列は as const なしでリテラル型を保ちます。choice の型は string ではなく "card_arrival" | "refund" | "other" です。
options.minConfidence を設定すると、confidence がそれを下回るときの choice は null になり、戻り値の型に null が含まれます。confidence と probabilities はどちらの場合も入っています。
const r = await d.decide(text, { refund: "wants their money back", bug: "something is broken" });
r.choice; // "refund" | "bug"
const s = await d.decide(text, head, { minConfidence: 0.9 });
if (s.choice === null) sendToAPerson(text);ゼロショットの選択肢の埋め込みは、選択肢のテキストのリストをキーにしてデサイダーごとにキャッシュされます。同じ選択肢で decide() を再び呼ぶと、エンコードするのは新しいテキストだけです。
次の例外を投げます。
decidekit: decide() needs at least 2 options, got N:配列やレコードの要素が 2 未満のとき。decidekit: this head was trained on <head model>, but the decider uses <model>. Pass { model: "<head model>" } to createDecider.:ヘッドのmodelがデサイダーと一致しないとき。
decideMany(texts, using, options?)
decideMany<const L extends string>(texts: readonly string[], using: Choices<L>): Promise<Decision<L>[]>;
decideMany<const L extends string>(texts: readonly string[], using: Choices<L>, options: DecideOptions): Promise<Decision<L, L | null>[]>;多数のテキストに対する decide() です。結果は texts の順に返ります。テキストは 64 件のバッチでエンコードするので、Node では 1 件ずつ decide() を呼ぶよりずっと速くなります。訓練済みヘッドで 1 テキストあたり 0.8-1.5 ms です。例外は decide() と同じです。
const rs = await d.decideMany(reviews, head);
const bugs = reviews.filter((_, i) => rs[i].choice === "bug");train(examples, options?)
train<const L extends string>(examples: readonly Example<L>[], options?: TrainOptions): Promise<Head<L>>;ラベル付きの例でヘッドを訓練し、それを返して解決します。ヘッドのラベルは examples の中の重複のないラベルを並べ替えたものです。手順は仕組みで、必要な例の数はヘッドを訓練するで説明しています。
同じデサイダーで再訓練すると、前回の訓練セットの埋め込みを再利用します。エンコードするのは前回の呼び出しになかったテキストだけです。キャッシュが持つのは直前のセットだけです。
const head = await d.train([
{ text: "where is my card", label: "card_arrival" },
{ text: "I want my money back", label: "refund" },
// ...
]);TrainOptions
| オプション | 型 | デフォルト | 働き |
|---|---|---|---|
labelNames | boolean | true | 各ラベルの読みやすい名前を開始点のヒントに使います(追加の 1 例として数えます。0.1.0 より前は 3 例)。c17 のような意味のない ID ではオフにしてください。 |
solver | "adam" | "sgd" | ラベルごとに 8 例以上で adam、それ以外は sgd | ヘッドの訓練方法。0.2 で追加(それより前は常に sgd)。solver なしで learningRate か l2 を設定すると sgd になります。 |
epochs | number | adam は 100、sgd は 60 | 例を通す最大の回数。取り分けた例で最良のエポックを残します。0.2 より前のデフォルトは 60 です。 |
learningRate | number | adam は 0.05、sgd は 2 | ステップ幅。 |
l2 | number | adam は 0、sgd は 1e-4 | 重み減衰。 |
holdout | number | 0.1 | エポック数の選択と温度の当てはめのために取り分ける例の割合。その割合が 200 例未満なら、代わりに 5 分割を使います(すべての例が 1 回ずつ取り分けられます)。 |
seed | number | 7 | 分割とシャッフルのシード。同じ実行を正確に再現できます。 |
calibrateOn | Example<L>[] | なし | 正しいとわかっている例で、温度の当てはめにだけ使います。withFallback() 経由の LLM のように、訓練例の出どころがときどき間違えるなら、ラベルごとに約 2 例を渡してください。ラベルはヘッドのものに限ります。 |
onEpoch | (epoch: number, holdoutAccuracy: number) => void | なし | 取り分けを使う実行のエポックごとに、取り分けた例での精度とともに呼ばれます。epoch は 0 から数えます。5 分割では分割ごとに実行されるので、epoch は分割ごとに 0 に戻ります。全例での最後の再訓練では呼ばれません。 |
次の例外を投げます。
decidekit: example N needs a string text and a non-empty string label, got {...}:最初の不正な例に対して。decidekit: train() needs at least 2 labels, got N。
訓練は 64 件のバッチの間でイベントループに処理を譲るのでページは応答を続けますが、その前のエンコードは譲りません。ブラウザではワーカーで訓練してください。
embed(texts)
embed(texts: readonly string[]): Promise<number[][]>;エンコーダー単体です。テキストごとに、平均プーリングして単位長に正規化したベクトルを 1 つ返します(デフォルトのモデルで 384 個の数)。独自の類似度検索や、どの例が近くにあるかの確認に使えます。
const [a, b] = await d.embed(["refund please", "I want my money back"]);
const similarity = a.reduce((s, x, i) => s + x * b[i], 0); // cosine, since both have length 1createWorkerDecider(options?, worker?)
import { createWorkerDecider } from "decidekit/browser";
function createWorkerDecider(options?: DeciderOptions, worker?: Worker): Promise<{
decide: ...; // same overloads as createDecider's
decideMany: ...;
train: <const L extends string>(examples: readonly Example<L>[], options?: TrainOptions) => Promise<Head<L>>;
terminate: () => void;
}>;Web Worker の中で動く createDecider() です。エンコーダーの処理中もページは描画と入力の処理を続けます。同じ DeciderOptions を受け取り、onProgress と onEpoch はワーカーから中継されます。ワーカーがモデルを読み込むと解決します。
createDecider() との違いは次のとおりです。
- 結果に
embedとmodelはありません。 terminate()はワーカーを止め、保留中の呼び出しをすべて拒否します("decidekit: the worker was terminated")。- ワーカーで投げられたエラーは、元のメッセージを持つ
Errorとして返ります。 - 各呼び出しにメッセージのやり取りで約 1 ms かかります。
worker のデフォルトは new Worker(new URL("./worker.js", import.meta.url), { type: "module" }) です。バンドラーに別のワーカー URL が必要なら、自分で渡してください。ブラウザで使うとバンドラーを参照してください。
const d = await createWorkerDecider({
onProgress: ({ loaded, total }) => (bar.value = loaded / total),
});
const r = await d.decide(input.value, ["question", "bug_report", "praise"]);describeLabels(labels, complete, options?)
LLM に 1 回だけ尋ねて、各ラベルの 1 文の説明を、そのラベルの典型的なメッセージとして書いてもらいます。decide() に渡せる Record<L, string> を返します。complete(prompt) はあなたの LLM 呼び出しで、返信のテキストを返します。返信に抜けたラベルには読みやすい名前が入ります。options.context はテキストが何かを伝えます("message to a bank's support chat")。dev 分割では、説明でゼロショットの精度が 7 から 10 ポイント上がりました。
describePrompt(labels, options?) はプロンプトを返し、parseDescriptions(labels, reply) は返信を読みます。LLM の呼び出しを自分で(バッチジョブや別のプロセスで)行いたいときに使います。
saveHead(head)
function saveHead(head: Head): string;ヘッドを JSON 文字列へ変換します。重みとバイアスは base64 です。ファイルに書き出して、アプリと一緒に配信してください。
import { writeFile } from "node:fs/promises";
await writeFile("intent.head.json", saveHead(head));loadHead(json)
function loadHead<L extends string = string>(json: string): Head<L>;saveHead() が書いた文字列をパースします。ラベルの型は string です。ラベルの型を渡せばそれが付きます。たとえば decidekit train --types が書き出す型です。
import { loadHead } from "decidekit/browser";
import type { Label } from "./intent.head.labels.ts";
const head = loadHead<Label>(await (await fetch("/intent.head.json")).text());loadHead はラベルを L と照合しません。不正な入力には JSON.parse や atob が投げる例外をそのまま投げます。ヘッドは JSON.parse() ではなく loadHead() に通してください。Float32Array の重みを持たないパース済みオブジェクトはヘッドと認識されません。
saveHead と loadHead は decidekit と decidekit/browser の両方からエクスポートされています。ページでは decidekit/browser からインポートしてください。このエントリーならエンコーダーがバンドルに入りません(482 KB に対して 3.6 KB)。
型
Decision
interface Decision<L extends string, C extends L | null = L> {
choice: C; // the most likely option; null when below minConfidence
confidence: number; // probability of the top option, 0 to 1
probabilities: Record<L, number>; // probability of every option, sums to 1
similarity?: number; // zero-shot only: best cosine similarity to any option's name or description
}テキストがどの選択肢の話でもないと similarity は低くなります。bge-small では、話題外のテキストはおおむね 0.5 未満、範囲内のテキストは 0.7 を超えました。
choice が null のときでも、confidence は最上位の選択肢の確率です。
Head
interface Head<L extends string = string> {
model: string; // encoder the head was trained on
labels: readonly L[]; // sorted
dim: number; // embedding size (384 for the default model)
weights: Float32Array; // labels.length x dim, row-major
bias: Float32Array; // one per label
temperature: number; // fitted on held-out examples
}Choices
type Choices<L extends string> = readonly L[] | Readonly<Record<L, string>> | Head<L>;DecideOptions
interface DecideOptions {
minConfidence?: number; // below this, choice is null
}Example
interface Example<L extends string> {
text: string;
label: L;
}TrainOptions, DeciderOptions
train() と createDecider() の表を参照してください。
CLI
npx decidekit train <examples.csv|.jsonl> [-o head.json] [--model <hf-id>] [--no-label-names] [--types]
npx decidekit decide <head.json> "<text>" [--min-confidence <0-1>]
npx decidekit --helptrain
例を読み、ヘッドを訓練し、saveHead() で書き出します。
入力:1 行に 1 つの
{"text": ..., "label": ...}オブジェクトを持つ.jsonlファイル、または CSV として読むその他のファイル。CSV にはtextとlabelの列を持つヘッダー行が必要です(他の列は無視します)。フィールドは"で囲めて、引用符の中の""はリテラルの引用符です。-o, --out <file>:ヘッドの書き出し先。デフォルトはhead.json。--model <id>:訓練に使うエンコーダー。デフォルトはdebuffjp/decide-small(0.1.1 より前はXenova/bge-small-en-v1.5、0.1.0 より前はXenova/all-MiniLM-L6-v2)。--no-label-names:labelNames: falseと同じです。--types:<out without .json>.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];
例の数、ラベルの数、ラベルごとの最少の例数を stderr に出力し、3 例未満のラベルがあると警告します。最後にファイルサイズ、訓練時間、当てはめた温度を出力します。他の TrainOptions はデフォルトのままです。
npx decidekit train tickets.csv -o support.head.json --typesdecide
ヘッドを読み込み、ヘッドのモデルでデサイダーを作り、JSON を 1 行 stdout に出力します。
npx decidekit decide support.head.json "my card never arrived" --min-confidence 0.9出力の形は {"choice":"card_arrival","confidence":<number>} です。
confidence は小数第 4 位に丸めます。--min-confidence を下回ると choice は null です。
終了コード:成功で 0、エラーで 1(メッセージは stderr へ)、使い方の誤りで 2。
withFallback() と decidekit/react
withFallback(decider, head, options) とその NONE ラベル(decidekit と decidekit/browser から)は LLM へのフォールバックで説明しています。useDecider() と useDecision()(decidekit/react から)は React で説明しています。
decidekit/typesafe
ローカルのデサイダーで使う TypeSafe の SDK の形で、Choice の質問に対応します。違いはガイドを参照してください。
typesafeClient(decider)
createDecider() または createWorkerDecider() のデサイダーを包みます。client.systemOne({ state, questions }) は、質問ごとの答えを持つ { answers } で解決します。 各答えには choice、probabilities、confidence があります。confidence は TypeSafe の式 (n * top - 1) / (n - 1) に従うので、decide() の値より低く出ます。
choice(instructions, criteria)
Choice の質問です。criteria は選択肢の名前から説明(なければ null)へのマップ、または訓練済みヘッドです。instructions は無視します。decidekit は状態と選択肢を比べるからです。
noul と score もエクスポートしていますが、ローカルでは対応していないので例外を投げます。
decidekit/switch
switchDecider(decider, options?)
createDecider() のデサイダーを switch の Decider として包みます。choice、bool、score の質問にローカルで答えます。画像は無視します。
text(state, question, id):質問に対して Decide が読むもの。デフォルトはtextOf(question.instructions, state)。heads:質問 ID ごとの訓練済みヘッド。teacher:選択がminConfidence(デフォルト 0.9)を下回るときに尋ねる switch のデサイダー。その答えは、繰り返す質問ごとに 8 回答ずつヘッドを訓練します。
textOf(instructions, state)
指示のあとに、状態を key: value の行で続けたものです。switch のガイドを参照してください。