入力された日本語を、248 の意図のどれに当てるか
Mapping typed Japanese to one of 248 intents
D08-3-T15-intent-classification・技術・日本語入力でプレイする仕組み
港で佐伯に「ゆうべ、何時までどこにいたんですか」と入力すると、エンジンはこれを「事件の夜の居場所を聞く」という意図に当てます。
その意図には、作者が書いた言い換えが 20 本あり、入力にいちばん近い 1 本との近さが、その意図の点になります。
この回は、意図とは何か、どう当てているか、学習しないのになぜそれで足りるのかを書きます。
T03〜T14 で、入力の正規化、文字 n-gram と tf–idf、文の埋め込み、二つの点の混ぜ方、固有名詞・日付・節・否定・指示語の取り出しを書きました。
今日から、取り出したものを使って「決めて返す」側に入ります。
数字は全部、今日(10 月 29 日)にこの版で測った値です。
この回の位置と、前後の回#
区切り: T03〜T24 日本語入力を解いて返すまで(22 回のうち 13 回目)
先に読むとよい回: T04(文字 n-gram と tf–idf)・T05(文の埋め込み)・T08(二つの点を混ぜる)
次に読む回: T16(答えない判断)
名前 — intent classification#
- 正式名
- intent classification(意図分類)。intent detection とも呼ぶ
- 関連
- k-nearest neighbors(k-NN・最近傍法)を、意図ごとの例文で使う形(exemplar-based classification)
- 略称
- k-NN(最近傍法の側)
- 日本語
- 意図分類、意図推定、意図理解
- 初出・代表
- intent classification という語の初出は確かめられませんでした。課題としては、タスク指向の対話システムの言語理解(NLU)の一部として扱われてきました。航空券の問い合わせのデータ Hemphill, Godfrey, Doddington, "The ATIS Spoken Language Systems Pilot Corpus"(1990)は、後に意図分類の評価によく使われたデータです(論文自体は意図分類を定義していません)。最近傍法は Fix & Hodges, "Discriminatory Analysis, Nonparametric Discrimination: Consistency Properties"(USAF School of Aviation Medicine の報告, 1951)。解析は Cover & Hart, "Nearest Neighbor Pattern Classification"(IEEE Transactions on Information Theory 13(1), 1967)
意図分類とは#
意図分類(intent classification)とは、「明日の札幌行きを取りたい」を「航空券の予約」に、「払い戻しはできますか」を「払い戻しの問い合わせ」に振り分ける仕事です。
ラベルの集合(意図)は、システムを作る側が先に決めて閉じています。
多くの場合、同じ発話から日付や行き先を抜き出す slot filling と組で使います。
よく使われる作り方は二つです。
一つは、ラベルつきの発話を集めて分類器を学習する方法です。
古くはロジスティック回帰や SVM、いまは事前学習した言語モデルを微調整するのがふつうです。
もう一つは、意図ごとに例文を少し持ち、入力と例文の近さで決める方法です。
例文を文の埋め込みにして、いちばん近い例文の意図を返します。
学習の手間が無く、例文を足すだけで意図を増やせるので、例文の少ない場面(few-shot)で使われます。
最近傍法の式で書くと、こうです。
ŷ(x) = argmax_c max_{e ∈ E_c} sim(x, e)
E_c は意図 c の例文の集合です。
近い順に k 本取って多数決するのが k-NN、意図ごとに最も近い 1 本だけを見るのが 1-NN を意図ごとにとった形です。
Cover & Hart(1967)は、標本が十分に多ければ 1-NN の誤り率がベイズ誤り率の 2 倍以下に収まることを示しました。
よく知られた弱点は四つです。
- 範囲外の入力(out-of-scope)。閉じたラベルの集合は、どの意図でもない入力にも必ずどれかを返します。答えないための仕組みが別に要ります(T16 の回)
- 似た意図どうしの取り違え。「予約の変更」と「予約の取り消し」のように話題が近い意図は、例文が少ないとすぐ入れ替わります
- 例文の偏り。k を大きくした多数決では、例文の多い意図が有利になります
- 例文の書き方への依存。作る側が思いつかない言い方は、どれにも近くなりません
うちの意図の形#
意図は、作品の intents/ に JSON で書きます。
佐伯の「事件の夜の居場所」を、言い換えを 4 本に縮めて写します。
{
"id": "saeki.alibi_night",
"speaker": "saeki",
"where": ["port"],
"phase": [1],
"entities": ["alibi"],
"confirmText": "事件の夜の居場所",
"paraphrases": [
"昨日の夜はどこにいた",
"事件の夜のアリバイを聞かせてください",
"夕べどこで何してた",
"あの晩は船にいたのか"
],
"inputGuard": { "date": { "month": 2, "day": 15 }, "excludeAny": ["十二年前", "12年前"] },
"stages": [ … ]
}
欄の意味です。
- speaker・where・phase … 誰に、どこで、どの章で聞ける話か。合わなければ候補に入らない
- requires … 意図そのものが現れる条件(フラグ)。満たすまで、この意図は存在しない
- paraphrases … 例文。これが検索の索引になる
- entities … 固有名詞の加点(T10 の回)と、段の材料(T19 の回)に使う語の id
- confirmText … 聞き返しのときに読み上げる短い話題名(T17 の回)
- inputGuard … 入力の日付や語で、この意図を外す条件(T11 の回)
- stages … 返す台本の段(T19 の回)
もう一つ、kind という欄があります。
it.bSmalltalk = (ji.value("kind", "") == "smalltalk");
it.bDeduction = (ji.value("kind", "") == "deduction");
it.bAccuse = (ji.value("kind", "") == "accuse");
- smalltalk … 事件と関係のない雑談。フラグを立てられない(lint L15)。順位付けで 0.05 だけ不利にする
- deduction … 推理の決定(見立て・決め手)。僅差のときは受け取らずに聞き返す(T16 の回)
- accuse … 捜査中に疑いをぶつける手。段はフラグを持てない(lint L21)
照合の点数の計算は、kind で変わりません。
変わるのは、順位付けの減点と、受け取った後の扱いと、ログの種別だけです。
ソースが区別する kind はこの三つだけで、それ以外の値は黙ってふつうの意図として読まれます。
今日数えたら、作品データには三つ以外の値を書いた意図が 69 ありました(第一章の外)。
動きは変わらないので害はありませんが、書いた側の意図はエンジンに届いていません。
数#
今日の作品データです。
- 意図 248、言い換え 2,845 文(意図あたり平均 11.5)
- 言い換えの本数は、最小 6、中央値 12、最大 27
- 雑談 91(平均 7.1 本)、推理の決定 35(13.8 本)、疑い 8(12.4 本)、kind なしの本筋 45(15.7 本)、三つ以外の値 69(13.2 本)
- 第一章だけで 160 意図、言い換え 1,655 文。そのうち雑談が 91
第一章の意図の半分以上が雑談です。
本筋から外れた入力を受け止める側の方が多い、という作りです。
当て方#
照合は、言い換え 2,845 文を全部、文書として持っています。
1 回の入力で、全文書との近さを計算し、意図ごとに最大を取ります。
AdvMatcher.hpp の query から、その部分を写します。
for (const ParaDoc& D : Paras)
{
const double CosA = Dot(Q, D.Vec);
double C = CosA;
if (!QVecs.empty() && D.Embed)
{
double Raw = 0.0;
for (const std::vector<float>* QV : QVecs)
{
Raw = std::max(Raw, VectorCache::cosine(*QV, *D.Embed));
}
const double Scaled = std::clamp(
(Raw - Th.embedFloor) / std::max(1e-6, 1.0 - Th.embedFloor), 0.0, 1.0);
C = std::max(CosA, CosA * WA + Scaled * WB);
}
double& Any = CosAny[D.IntentPtr];
Any = std::max(Any, C);
}
for (auto& [It, C] : CosAny)
{
if (ExcludeIds && ExcludeIds->count(It->id) > 0) { continue; }
if (IntentActive(*It, State, Data)) { CosHere[It] = C; }
}
手順にすると五つです。
- 入力を、文字 n-gram の tf–idf の疎ベクトル Q にする(T04 の回)
- 入力の全体と、句読点で割った各節の埋め込みを引く(T05・T12 の回)
- 言い換え 1 本ごとに、表層の cos と、埋め込みの cos(各節の最大)を求め、混ぜた値と表層だけの値の大きい方をその言い換えの点にする(T08 の回)
- 意図ごとに、言い換えの点の最大を取る。これが「意図ごとの 1-NN」
- いまの状態で有効な意図だけを残す
5 の「有効」は、AdvRules.hpp の IntentActive が決めます。
話している相手、場所、章、時間帯、意図の requires、その人がいまそこにいるか、を順に見ます。
相手が決まっているときは、その人の意図と、現場を調べる意図だけが候補です。
このあと、固有名詞の加点(0.15 を最大 2 つ)、雑談の減点、既に聞いた段の減点を足し引きして順位を決めます(T08・T10 の回)。
答えるか、聞き返すか、受け流すかは、次の T16 の回です。
今日の実測 — intent classification#
港で佐伯に入力した結果です。
- 「ゆうべ、何時までどこにいたんですか」→ 事件の夜の居場所 0.647。2 位は「番屋を出た時刻の食い違い」0.531、3 位は雑談の「船を出す時刻」0.265
- 「昨日の夜はどこにいた」→ 表層の cos が 1.000。言い換えと同じ文なので、表層だけで決まる
- 「今日は寒いですね」→ 雑談の「天気」。佐伯「……荒れる。夜は出るな」
- 「十二年前の夜はどこにいた」→ 居場所の意図は inputGuard の excludeAny で候補から外れ、12 年前の話の意図に 0.554 で着地
1 位と 2 位の差は 0.116 でした。
2 位の「食い違い」は、女将の証言を手に入れた後に効いてくる意図で、話題がとても近いからです。
近い意図どうしを分けているのは、点数ではなく段の材料と主張です(T19・T20 の回)。
境界の扱い — intent classification#
- 空の入力。言い換えのどれとも cos が 0 になり、有効な意図があっても点は 0。採用線に届かないので受け流しになります
- 同点。意図ごとの最大を比べるとき、std::map を意図のポインタの順に回し、厳密に大きいときだけ 1 位を入れ替えます。意図は読み込み順に一つの配列に入っているので、同点なら先に読んだ意図が勝ちます。乱数は使いません
- 同じ文が二つの意図にある。今日数えたら 26 文ありました。「どうも」が 5 人の挨拶にある、のような形です。相手で候補を絞るので、取り合いにはなりません
- 相手のいない場所。現場を調べる意図(speaker が scene)だけが候補になります
名前どおりでない所 — intent classification#
intent classification の、学習しない形です。
- 分類器を学習していません。意図ごとの例文と、表層と埋め込みを混ぜた近さで、意図ごとに最も近い 1 本だけを見ます。k-NN の多数決ではありません
- ラベルの集合が状態で変わります。248 の中から選ぶのではなく、いまの場所・相手・章・フラグで有効なものの中から選びます。港で佐伯と話しているときの候補は、佐伯の意図と、現場を調べる意図だけです
- 意図の点は確率ではありません。点の上に固有名詞の加点などを足すので、1 を超えることもあります(今日の「昨日の夜はどこにいた」は加点込みで 1.150)
- 閉じた集合で終わりません。どの意図にも届かない入力は、受け流しや聞き返しに回します(T16・T17 の回)
多数決にしないのは、例文の本数がそろっていないからです。
雑談は平均 7.1 本、本筋は 15.7 本あります。
k 本の多数決にすると、本数の多い本筋が雑談を押しのけます。
1 本でも十分に近い例文があれば、その意図だと言ってよい、という考えです。
弱点と、採らなかった案 — intent classification#
今日、意味として外れた着地を一つ見ました。
佐伯に「朝食は何時からですか」→ 雑談の「船を出す時刻」。佐伯「三時だ。暗いうちに出て、昼前には戻る」
朝食は宿の主人に聞く話です。
佐伯の意図の中で「時刻」の近いものに当たりました。
雑談なので進行は動かず、佐伯らしい返事にもなっていますが、問いに答えてはいません。
例文で引く方法は、こういう「近いけれど違う」を避けられません。
ほかに知っている弱点です。
- 例文は作者が書きます。作者が思いつかない言い方は届きません
- 落ちた入力をそのまま例文に足すと、検査の数字だけが上がります。これは T50 の回で、Goodhart の法則として書きます
採らなかった案です。
- 分類器を学習する。実際のプレイヤーの入力にラベルを付けた標本がありません。作者が書いた例文で学習すれば、それは例文を覚えた分類器になるだけです。また、例文を 1 本足したら、その場で効く方が作りやすい
- 生成AIに意図を選ばせる。同じ入力に同じ意図が返る保証がなく、ネットにつながらないと動きません(T18・T23 の回)
- k 本の多数決。上に書いた本数の偏りのためです
例文を足すと、その場で効く。
効かなかったら、どの例文に近かったかがログに出る。
248 の意図を一人で書いて直すには、この見通しの良さが要りました。
検索語#
intent classification
intent detection
k-nearest neighbors classification
out-of-scope intent detection
港で佐伯に、昨夜のことを自分の言い方で聞いてみてください。『霧走町 流氷に眠る』の第一章はブラウザで無料で遊べます。プロフィールのリンクからどうぞ。