「短縮化」と「共通用語化」でテキスト埋め込みを改善

みなさんこんにちは。エンジニアの佐藤です。今回はテキスト埋め込み(text embedding)についてお話しさせてください。(あらかじめお断りしますと、割と基本的な話です。なお、機能寄りの話で、数学は出てきません。)

疑問のきっかけは「RAG案件失敗」の知らせ

ある日のこと、他社某案件の失敗談が聞こえてきました。曰く、「PoCが失敗し、プロジェクトが打ち切りになった」と。恐ろしい話だと思って聞くと、RAG(Retrieval-Argumented Generation)が期待通りに動作しなかったようです。(詳しい話は省略します。)

RAGは、ご存知の方も多いと思いますが、一般的にはチャットボット応答を改善する技術で、ユーザーの発言内容に該当する情報をセマンティックサーチ(意味サーチ)で回収し、返信文面にLLMトレーニング時には入っていない最新情報やドメイン知識(会員情報や社内情報)を含める実装技術のことです。従ってその期待動作のためには、セマンティックサーチで期待された応答が得られる必要があります。

セマンティックサーチの実装手法のひとつがベクトルデータベースです。(テキスト検索を組み合わせるハイブリッドサーチや、グラフデータベースを使う実装もありますが、今回は省略します。)ベクトルデータベースの基本機能はただひとつ、クエリーとなるベクトル(=数字の集合体)に類似したベクトルと、その付属情報を返すことです。「類似した」の定義は各種ありますが、「コサイン類似度」が大きいことである場合がほとんどです。(が、今回は数学には立ち入りません。)

そのベクトルは、実のところ任意なのですが、RAGの文脈では「テキスト埋め込み」と呼ばれるもので、数百~数千の数値の集合体(ベクトル)です。テキストデータからテキスト埋め込みへの変換は「text embeddingモデル(テキスト埋め込みモデル)」と呼ばれるAIモデルが行なっており、大規模言語モデル(LLM)の進化に合わせる形で現在も精力的に開発されています。その目的は、あらゆる文章を適切にベクトル化することです。適切とはつまり、RAGのような用途で利用した場合に期待される検索対象との間のベクトルが他よりも類似度が高くなる、ということです。

しかし、「あらゆる文章」なんて、無理ですよね。

いくらテキスト埋め込みモデルが大規模なデータセットと最新の言語AI技術で調整されようとも、文章の意味は結局のところ文脈次第です。その文脈は、変換対象になるテキストだけを見ていては、わかるわけがないのです。仮にある文章に関するあらゆる文脈がひとつのベクトル表現されていたとしても、他のベクトルと望んだ形で比較するのはやはり無理というものです。(筆者はその道の研究者ではありませんが。)

とはいえ「やってみないとわかりません」「ダメだったらどうしようもありません」では仕事になりません。このつかみ所のない問題に対処する方法はないものでしょうか。少なくとも、危険を察知する手段は確保しなければなりません。

セマンティックサーチが期待動作しない例

以下のような2組の問題報告文面を考えてみましょう。(番号は通しで設定されています。)

外部REST API呼び出しでマニュアルに無い想定外応答で設計見直しの懸念を伝える文章

記号 人物像 文章例
S1 新人エンジニア お疲れ様です。
外部のREST APIを実装通りに呼び出したところ、マニュアルに記載されていない形式の応答が返ってきました。
正直なところ、どのように処理すべきか判断がつかず、現行の設計では対応が難しいかもしれません。
お手すきの際に一度ご相談させていただけますでしょうか…?
S2 ベテラン1 外部REST APIからのレスポンスに、仕様書上には存在しない項目が含まれていることを確認しました。
現行のパーサでは未対応のため、例外処理が発生しています。
この応答形式が安定して継続する場合、設計見直し(特にバリデーション部)を含めた対応が必要になる見込みです。
一旦、対応方針のドラフトを作成して午後に共有します。
S3 ベテラン2 なんか外のAPIが変なデータ返してきて、今のやり方だとちょっと通らないっぽいんだよね。
たぶん仕様が変わったんじゃないかな?ちゃんとしたドキュメントなかったし…。
一応ログは取ってるけど、まぁ見直し必要かも。どうしようか?
S4 理屈っぽい人 外部のREST APIに対して /v2/items エンドポイントをGETで叩いた際、仕様書上は"items": [Object]形式で返るはずなんですが、実際にはnullもしくは空配列が返るケースが複数確認されました。
ドキュメントには「itemsは必ず存在する」との記述があり、null許容については一切言及されていないので、これは仕様逸脱だと考えています。
我々のパーサーはstrict型で書いてあるため、非期待型の入力で例外を投げるのは当然の挙動です。暫定的な対応としてtry-catchで回避もできますが、それは構造的には“バグを受け入れる設計”になるので、慎重に判断すべきです。
仕様の曖昧さに起因する処理の分岐は、最終的にコードベースの汚染にもつながるので、まずはAPI提供側に問い合わせて明確化を求めるのが筋だと思います。

リファクタリングにより一部のテストコードが通らなくなっている状態を伝える文章

記号 人物像 文章例
S5 新人エンジニア お疲れさまです…。
リファクタリングを行った後、一部のユニットテストが失敗するようになってしまいました。
どこをどう直すべきか、ちょっと見当がついておらず、もしかすると設計意図を壊してしまったかもしれません…。
お手すきのときに一度見ていただけるとありがたいです。
S6 ベテラン1 昨日実施したロジックのリファクタリングにより、複数のテストケース(主に境界値と例外系)が現在Failしています。
原因は旧処理系が内部的に保持していた状態の扱いが変わったことによるものです。
テスト自体の見直しが必要か、もしくは処理設計を一部巻き戻すか、影響範囲を整理して今日中に方向性を出します。
S7 ベテラン2 なんかちょっとリファクタ入れたらテストの何個か通らなくなったっぽいけど、たぶんテスト側のほうが古い仕様のままなんじゃないかな?
ロジック的にはこっちのほうがキレイになってるし、まあそこまで問題じゃないと思うけど。
時間あるときに誰か見ておいてもらえる?
S8 理屈っぽい人 先日のリファクタリング(DataProcessorクラスの責務分離とストリーム化)後、ユニットテストのうちtest_process_with_invalid_input()とtest_edge_case_timestamps()がFailしています。
原因はリファクタ後の処理が、旧実装で暗黙的に処理していたフォールバックロジック(特定条件下での入力補正)を排除したことによるものです。
そもそもこの補正処理自体が設計として不透明で、「テストが通っているから正しい」という思考停止のもとに放置されていた傾向が否めません。
今回のFailは、むしろ本来あるべき仕様がコードとテストで乖離していたことの表出と捉えるべきであり、ここで対応を誤ると“テストのための仕様”という倒錯に陥ります。
したがって、安易に元の挙動に戻すのではなく、仕様の正当性から逆算して処理・テストのいずれが間違っているかを再定義すべきだと考えます。

これらの文章をmultilingual-e5-largeと呼ばれるテキスト埋め込みモデルで変換し、Faissを用いて相互にセマンティックサーチを行うと、以下のような想定外の結果が返ります。(今回はコサイン類似度での評価を行いました。)

search by S2: 1. S1 (score: 0.905)
search by S2: 2. S4 (score: 0.897)
search by S2: 3. S6 (score: 0.879) <= S2で検索するとS6がヒット
-----------
search by S3: 1. S1 (score: 0.900)
search by S3: 2. S7 (score: 0.894) <= S3で検索するとS7がヒット
search by S3: 3. S4 (score: 0.879) 

ベテラン1とベテラン2の文章が、2つのグループ(以下、クラスタ。ここではREST APIリファクタリングの2つ)の間で混同されているのです。このままでは、似たような文章をセマンティック検索して紹介したい場合に、関係無い文章が紹介されて困ってしまいそうです。

改善策

このような状態を改善する手段として筆者がお勧めしているのが、テキスト埋め込みモデルで変換する文書を事前に「短縮化」し、「共通用語化」する手法です。

以下のようなプロンプトで、例えばGPT-4oなどに元の文章を変換させます。

以下の共通用語をなるべく利用して、端的に、それぞれを書き直してください。
> 外部, REST API, 応答, データ形式, 設計, 例外処理, 動作, 仕様, ベンダー, 確認, 設計上, 変更, WBS, バッファ, 対応方針, リファクタリング, 自動テスト, Fail, 修正, コード, テスト, 整合性, スケジュール, 進捗レビュー, 復旧
---
(変換対象文章)

元の文章は以下のように変換されました。

(変換後)外部REST API呼び出しでマニュアルに無い想定外応答で設計見直しの懸念を伝える文章

記号 人物像 文章例
S1 新人エンジニア REST APIの応答にマニュアル外のデータ形式が含まれており、現行の設計では例外処理が発生しています。
設計上の対応が必要か判断がつかず、お手すきの際にご確認いただけると助かります。
S2 ベテラン1 外部REST APIの応答に非仕様のデータ形式が含まれており、例外処理が発生。
設計上の変更が必要な可能性があるため、WBSバッファ確認とともに対応方針を午後共有予定です。
S3 ベテラン2 外部APIの応答が変で、今の設計だと例外処理が起きています。
仕様かどうかベンダーに確認中ですが、とりあえず動作しないので設計見直しの可能性あります。
S4 理屈っぽい人 REST APIの応答に仕様と異なるデータ形式(null/空配列)が含まれ、strictな設計上、例外処理が発生。
暫定回避も可能ですが、設計上の整合性を保つため、まずベンダーに仕様確認し、対応方針を判断すべきです。

(変換後)リファクタリングにより一部のテストコードが通らなくなっている状態を伝える文章

記号 人物像 文章例
S5 新人エンジニア リファクタリング後、一部の自動テストがFailしています。
設計の意図とズレている可能性があり、コードとテストの整合性を確認したいので、ご相談させてください。
S6 ベテラン1 リファクタリングにより自動テストの一部がFail。
設計変更の影響でテストとの整合性にズレがあり、修正方針を本日中に整理・進捗レビュー予定です。
S7 ベテラン2 リファクタ後にテストが一部Failしていますが、テスト側の設計が古いかもです。
ロジックは修正済みなので、時間あるときに誰か見てください。
S8 理屈っぽい人 リファクタリング後、特定の自動テストがFail。
設計上不要と判断した補正処理が削除されたことでコードとテストに整合性ズレが発生。
設計意図に基づいて修正すべきであり、テスト基準の再定義が必要です。

これを同様に相互にセマンティックサーチしてみると、以下のように改善が確認できました。

search by S2: 1. S1 (score: 0.934)
search by S2: 2. S3 (score: 0.928) <= S6の代わりにS3がヒット(想定通り)
search by S2: 3. S4 (score: 0.918)
-----------
search by S3: 1. S2 (score: 0.928) <= S7の代わりにS2がヒット(想定通り)
search by S3: 2. S1 (score: 0.925)
search by S3: 3. S4 (score: 0.902)

全体的に改善を確認するには

しかし、本当にこれで全部OKなのでしょうか?たまたま一部の結果が改善しただけかもしれません。また、今回は相互サーチなので検索に使うクエリーと検索対象は決まっていましたが、クエリー文字列が事前に決まっていない場合(ユーザーがフリーテキストで質問してくる場合など)は、事前に試すというわけにもいきません。

テキスト埋め込みの全体的品質をどうやって評価したら良いのでしょうか?

ひとつの方法としてベクトルクラスタ分析手法のひとつシルエット分析が使えると思います。クラスタ番号が設定された個々のサンプル(この場合はそれぞれの文章)についてシルエット係数を計算し、その平均値と分布をプロットします。

改善前の文章に対するシルエット分析の結果は以下のようになりました。通常、シルエット分析はクラスタ内の係数値を降順に列挙しますが、ここではサンプル文章との関係を示すためにS1~S8の順に値を列挙しています。黒いバーが「REST API」の問題報告の各文章に関するシルエット係数の値で、緑色のバーが「リファクタリング」の問題報告の各文章に関するそれです。このバーの長さが1.0に近いほど、文章は他のクラスタ(REST APIの問題報告文章の場合はリファクタリングクラスタリファクタリングの問題報告の文章の場合はREST APIクラスタ)から「十分類似度が低い」結果を示し、バーの長さが0に近いと「類似度はクラスタ間の境界付近」、負の値は「別のクラスタと類似(望ましくない)」を意味します。今回は全てのシルエット係数がが正の値となりましたが、全体的に低めです。

平均は0.26で、特にS3の値が低くなっています。値が低いということは、S3は2つの文章クラスタの境界付近に位置しており、混同されるの危険が高いということです。またS7も相対的に低くなっており、このことは、セマンティックサーチのテストでS3でサーチした結果、(S1~S4のクラスタではなく)S7が引っかかった理由をよく説明しています。

改善後の文章に対するシルエット分析の結果は以下のようになりました。

平均は0.47で、全ての文章で値が改善しました。セマンティックサーチのテスト結果も改善を裏付ける結果です。

文章の短縮化と共通用語化は、一般的なテキスト埋め込み改善手法と言えそうです。

参考にしていただければ幸いです。最後までお読みいただきありがとうございました。