Featured image of post チャットのルーティング精度を向上した時のアナリシス

チャットのルーティング精度を向上した時のアナリシス

目次

背景

  • ローカルLLMのchatのルーティングをした時の作業の備忘録
  • そのLLMのhidden stateを埋め込みとして使っている
  • 構成はかなりシンプル
1
2
3
4
5
6
7
8
9
conversation
  ↓
frozen ローカルLLM
  ↓
last hidden state
  ↓
回帰モデル
  ↓
5-class routing
  • 教師データは約1,400件
  • 当初の悩みは、「埋め込みでルーティングしているが、どうも精度が足りない」というものだった
  • そこで最初は当然、埋め込み側を疑った。multi-layer pooling、mean pooling、contrastive learning、prototype、rerankerなど、埋め込み分類の精度を上げる定番の案はいくつもある
  • しかし実験を繰り返した結果、最終的に見えてきたのはかなり違う景色だった
  • ボトルネックの多くは埋め込みモデルではなく、ラベル定義・評価方法・dialogue state・「分類すべき問題」と「状態遷移で処理すべき問題」の切り分けの方がはるかに大きかった
  • この記事では、その実験→分析→改善の過程をまとめる

最初の仮説: last hidden + 回帰モデルが弱いのでは

  • 最初に疑ったのはモデルだった
  • ローカルLLMの最終層+last token hidden state+回帰モデルという構成は単純なので、「もっと高度なpoolingを使えばいいのでは」「複数layerを使えば」「contrastive fine-tuningすれば」と考えた
  • ところが、すでにattention poolingなども試されており、小規模データではむしろfrozen last-hidden+回帰モデルの方が安定していた
  • 教師データが約1,400件しかない以上、学習可能パラメータを増やすと過学習しやすい
  • そこで方向を変えた。モデルを複雑にする前に、今の誤りが何なのかを分解しよう、というのが最初の転換点だった

実験1: 5-class分類を「query×route」の二値判定に変える

  • 先行研究のDNNCやUSLPに近い発想で、query→5-classではなく、query+candidate route→このrouteは適切か、というbinary classifierに変えてみた
  • route名だけを与えるP0、短いroute定義を与えるP1、Use when/Don’t use whenまで明示するP2を比較した

同じGroupKFold評価での結果は以下の通り。

方式AccuracyMacro F1
現行5-class95.22%.948
P0: route名のみ94.43%.944
P1: 短い定義95.00%.948
P2: 詳細policy95.36%.952
  • P0→P1→P2と、routeの意味を詳しく与えるほど改善する傾向はあった
  • しかしP2のbaseline差はわずか約+0.13pt。bootstrapの95%信頼区間もゼロを跨いでおり、有意な改善とは言えなかった
  • さらに実運用では、5つのcandidate routeを評価するため推論コストが大きく増える。実測ではおよそ9倍のレイテンシになるケースもあった
  • つまり、route descriptionを与えることには多少意味があるが、現在の5-class classifierを置き換えるほどの価値はない、という結論になった
  • これは重要だった。「classifierが単純すぎる」という仮説がかなり弱くなったからだ

評価方法にも問題が見つかった

  • 途中まで使っていたholdoutセットを何度も見ながら改善案を考えていた。これは当然、評価データへの人間側のoverfitを起こす
  • そこで評価を、StratifiedKFoldからGroupKFoldへ変更した
  • さらに重要なのはgroupの粒度だった
  • 同じ会話から派生した複数例やcounterfactualがtrain/testに跨ぐと、ほぼ同じ例を見て評価してしまう
  • 逆に、複数の独立した会話を一つの巨大groupとしてまとめると、今度は必要以上に厳しい評価になる。後でこのgroup設計自体にもバグが見つかることになる
  • この経験から、モデル評価以前に、何を独立サンプルとみなすかを正しく定義しないと数字そのものが信用できない、ということがよく分かった

最大の発見: 誤分類のかなりの部分が「教師ラベルの矛盾」だった

  • baselineはGroupKFoldで約95.22%。つまり誤りは数十件しかない。そこで、その誤りを人間が一件ずつ読むことにした
  • するとかなり衝撃的なことが分かった。同じ、あるいはほぼ同じhistory+current utteranceに対して、intent_unknownとapi_errorという正反対のラベルが付いている例が存在していた

例えば、以下のようなほぼ同一状況が、あるデータでは「ユーザーの意図が不明(intent_unknown)」、別のデータでは「意図は分かったがシステム処理のエラー状態(api_error)」になっていた。

Bot: 「申し訳ありません、よく分かりませんでした。もう一度お願いできますか?」 User: 「さっきの件、ちゃんと通じてる?」

  • これはモデルには絶対解けない。$X\to\text{label A}$と$X\to\text{label B}$を同時に教師として与えているからだ
  • ここで初めて、モデル性能の問題ではなく、label contractの問題だと分かった

「未解決」を一種類にまとめない

  • さらに分析すると、同じ「未解決のままチャットが止まっている状態」でも二種類あることが分かった
  • 一つは、ユーザーの意図そのものを理解できていないケース
  • もう一つは、ユーザーの意図は理解しているが、商品検索APIやDBの応答が途中で失敗したケース
  • この二つを両方同じラベルに押し込むと、意味が崩れる

そこでラベルの意味を次のように整理した。

  • intent_unknown: ユーザーが何を求めているかが未解決(semantic repair)

  • api_error: ユーザーの意味自体は理解済み。システム側の実行失敗・再試行の確認など(execution/response repair)

  • これはかなり重要だった。「対応が未解決か」と「意味理解に失敗しているか」は別の軸だからだ

ラベルを直しただけで95.22%→96.57%

  • 矛盾したラベルを整理し、label contractを統一した
  • モデルは一切変更していない。埋め込みも同じ、回帰モデルも同じ、推論レイテンシも同じ
  • それだけでGroupKFold OOFは、95.22%→96.57%まで改善した。約+1.35pt
  • ここまで試した複雑なモデル変更より大きな改善だった。これは今回の実験全体の中でも特に重要な結果だった
  • 高精度領域では、モデルを変えるより「教師データの意味を揃える」方が効く場合がある

残ったエラーを「全部モデルのせい」にしない

  • 次に残存エラーをtaxonomy化した。すると大きくいくつかの種類に分かれた。代表的だったのがconfirm_suggestion

例えば、以下のようなケース。

Bot: 「似た条件の別商品を検索してみましょうか?」 User: 「お願い」

  • 「お願い」単体にはrecommend_similarという意味はない。正しいrouteを決めるには、直前に何を提案したのかを理解する必要がある

もう一つはclarification解除。

Bot: 「うまく聞き取れませんでした」 User: 「昨日届いた商品の返品についてです」

  • のように、ユーザーが具体的な内容を言い直したなら不明状態を解除すべき
  • この二つに対してminimal counterfactualを追加してみた

Counterfactual data augmentationは、効くものと効かないものがあった

  • clarification解除については効果が大きかった。15件程度のminimal pairを追加しただけで、6件誤り→1件誤りまで改善した
  • これは典型的な「教師データに境界例が足りなかった」ケースだった
  • 一方でconfirm_suggestionは違った。約24件のcounterfactualを追加しても、12件中10件誤りとほとんど改善しなかった
  • この差はかなり示唆的だった。すべての誤りが「データ不足」で説明できるわけではない
  • clarification解除は分類境界の学習問題だった。しかしconfirm_suggestionは、もっと構造的な問題だった

「お願い」の意味を分類しているのではない

confirm_suggestionをよく考えると、必要な情報は以下のようになる。

  • previous speech act = SUGGESTION

  • offered action = RECOMMEND_SIMILAR

  • current speech act = CONFIRM

  • つまり、previous SUGGESTION(recommend_similar) + current confirm = recommend_similarという関係。これはユーザーの現在発話の埋め込みだけで解く問題ではない

  • そこでpending_suggestion_routeを構造化特徴として追加してみた

結果、confirm_suggestion 12件において、以下のように改善した。

  • 埋め込みのみ: 2/12 正解

  • pending_suggestion_route追加: 6/12 正解

  • 全体精度も約+0.34pt上がった

  • ここでかなり確信が強くなった。ローカルLLMが「お願い」を理解できないのではなく、「何をお願いされたのか」という会話状態まで埋め込みから逆算させている設計が無理筋なのではないか、ということ

Oracle実験で「分類問題ではない」ことを確認する

  • ただしここで一度問題が起きた。oracleで正しいpending_suggestion_routeを与えても改善しないケースがあった
  • 原因を調べると、GroupKFoldのgroup設計に問題があった
  • dialogue_scenarioという一つのデータセットの中に、実際には複数の独立した会話familyがまとめられていた
  • GroupKFoldでは同じgroupは必ず同じfoldに入る。その結果、そのfamily全体がtestに入ったとき、関連する例が一件もtrainに存在しないという不自然な評価になっていた
  • そこでgroupを実際のroot dialogue family単位へ修正した。全体を530ほどのgroupへ分割した
  • その状態で、pending_suggestionあり+純粋なconfirm→suggestionのrouteをそのまま継承という決定論的ルールを評価した
  • 関連ケースでは、34/34を正しく処理できた
  • 狙っていたconfirm_suggestionの12件もすべて処理でき、さらに「うん、でもその前にクーポンの使い方教えて」のような誤発火候補もconfirmではないと判定できた
  • ここでほぼ結論が出た

最終的に見えてきたアーキテクチャ

最初は、conversation→埋め込み→classifier→routeですべて解こうとしていた。しかし実験後の設計は以下のようになる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
User turn
  │
Dialogue state
  │
pending suggestion + confirm?
  ├─ YES → inherit suggestion route
  └─ NO  → ローカルLLM hidden
              ↓
            回帰モデル
              ↓
            route
  • つまり、会話状態から一意に決まるものはstate machineで処理し、意味分類が必要な部分だけ埋め込みの分類器へ渡す
  • これが今回得られた一番大きな設計上の学びだった

今回の実験から学んだこと

最初は「もっと良い埋め込み手法はないか」という問いだった。しかし実験を進めると、改善の順番はほぼ逆だった。

  • まず評価方法を疑う。同一会話や派生例のリーク、group粒度を確認する

  • 次にラベルを疑う。同一入力への矛盾ラベルや、曖昧なlabel contractを探す

  • 誤りをtaxonomy化する。context不足、仕様曖昧、ラベル誤り、純粋なモデル誤りを分離する

  • 本当に境界学習の問題だけcounterfactualで補う

  • 状態遷移で決まるものをclassifierに解かせない

  • それでも残った誤りに対して初めてモデルそのものを疑う

  • 今回、pairwise classifierやroute description、複雑な埋め込み方式よりも、ラベル修正とdialogue stateの明示化の方が大きく効いた

  • 特に印象的だったのは、95.22%→96.57%という改善が、モデル変更なしで得られたこと

  • そしてclarificationについては少量のcounterfactualがよく効いた一方、confirm_suggestionについては何十件足しても解けず、stateを明示した途端に問題構造が見えた

「モデルが間違えた」と言う前に

  • 今回の経験から、チャットボットのルーティングの誤分類を見たときには、「モデルが弱い」と考える前に、そもそも入力だけで答えられるか、教師ラベルは一貫しているか、これはclassification問題なのか、会話状態として保持すべき情報ではないか、を確認した方がいいと思うようになった
  • モデルは与えられた問題を解いているだけ。問題設定自体が矛盾していたり、必要な状態が入力に存在しなければ、より強い埋め込みモデルに交換しても本質的には解決しない
  • 今回の一連の実験で得られた一番大きな学びは、たぶんこれに尽きる。精度改善とは、常にモデルを賢くすることではなく、モデルに解かせる問題を、正しく定義し直すことの方が効く場合がある
  • チャットボットの開発では特にそうだと思う
  • 埋め込みは意味を表現し、classifierは境界を学習する。しかし「前のターンで何を約束したか」「現在どの状態にいるか」のようなものは、埋め込みに推測させるより、ボット側がシステムとして覚えておく方が自然
  • 今回のループは、最終的には埋め込みのチューニングというより、「何を学習させ、何を明示的に状態として持つべきか」を発見する作業だった

まとめ

  • 埋め込みモデルを疑う前に、評価方法(group粒度・リーク)とラベルの一貫性を疑うべき
  • 同一入力に矛盾したラベルが付いていないかを、誤り事例を人手で読むことで発見した。ラベルを整理しただけでモデル変更なしに95.22%→96.57%まで改善した
  • counterfactual data augmentationは、境界学習の問題(clarification解除)にはよく効いたが、構造的な問題(confirm_suggestion)にはほとんど効かなかった。この差が問題の性質を見抜く手がかりになった
  • 「前のターンで何を提案したか」のような会話状態は、埋め込みに逆算させるのではなく、構造化特徴やstate machineとして明示的に持たせる方が自然
  • Oracle実験や決定論的ルールでの検証は、GroupKFoldのgroup設計自体のバグ(dialogue_scenario内に複数の独立した会話familyが混在)を発見するきっかけにもなった
  • 最終的なアーキテクチャは、状態遷移で一意に決まる部分をstate machineで処理し、意味分類が必要な部分だけを埋め込み分類器に渡す、というハイブリッド構成になった

参考文献

Built with Hugo
テーマ Stack は Jimmy によって設計されています。