ベクトル検索で「以外」や「数字の大小」を試してみたら難しそうだった

2023.07.10

はじめに

新規事業部 山本です。

ChatGPT(OpenAI API)をはじめとしたAIの言語モデル(Large Language Model:以下、LLM)を使用して、チャットボットを構築するケースが増えています。通常、LLMが学習したときのデータに含まれている内容以外に関する質問には回答ができません。そのため、例えば社内の文章ファイルに関する質問に回答するチャットボットを作成しようとしても、質問に対してわからないという回答や異なる知識に基づいた回答が(当然ながら)得られてしまいます。

この問題を解決する方法として、Retrieval Augmented Generation(以下、RAG)という手法がよく使用されます。RAGでは、ユーザからの質問に回答するために必要そうな内容が書かれた文章を検索し、その文章をLLMへの入力(プロンプト)にプラスして渡すことで、欲しい情報に関する回答をさせることができます。

以前にも、RAGについて考えた内容や、RAGを使って取り組んだ内容がありますので、こちらも合わせてご覧ください。

上の記事での取り組みにおいて、文章を検索するためにEmbeddingを使用していたのですが、「以外」「数字の大小」を含んでいるときの結果が意図通りでないと感じる点があったので、この記事で試してみようと思います。

類似度を計算してみる

概要

今回はRAGのうち、検索結果をもとに回答を生成する部分は含まず、検索部分のみを考えます。状況としては以下の図のような状況を想定します。

  • 説明書に条件との説明が書かれており、それらが分割されて格納されている
  • 複数のユーザからの質問がある

このような状況で、RAGを実行しようとした時、条件に合致する文章を選択されないと、回答を生成するのに必要な情報を抽出できず、おかしな回答になってしまいます。そのため、質問の条件に合致するテキストのスコアは高く、合致しないテキストのスコアが低く計算されるのが望ましいです。

これを調べるために、ユーザからの質問に適した条件の説明書のテキストを選択できるのかを、Embeddingによるベクトルの類似度でためしてみました。

使用したテキスト

以下のように、説明書のテキストとしてsentences_instructを、ユーザからの質問としてsentences_userをそれぞれ使用しました。

意図としてはそれぞれのユーザの質問の条件に合致している文章の類似度が高く選択でき、

  • 「以外」
sentences_instruct = [
    "赤色の靴下がほしい際はこちらから購入してください",
    "オレンジ色の靴下がほしい際はこちらから購入してください",
    "黄色の靴下がほしい際はこちらから購入してください",
    "緑色の靴下がほしい際はこちらから購入してください",
    "青色の靴下がほしい際はこちらから購入してください",
    "紫色の靴下がほしい際はこちらから購入してください",
    "灰色の靴下がほしい際はこちらから購入してください",
    "黒色の靴下がほしい際はこちらから購入してください",
]

sentences_user = [
    "赤色以外の靴下がほしいです",
    "オレンジ色以外の靴下がほしいです",
    "黄色以外の靴下がほしいです",
    "緑色以外の靴下がほしいです",
    "青色以外の靴下がほしいです",
    "紫色以外の靴下がほしいです",
    "灰色以外の靴下がほしいです",
    "黒色以外の靴下がほしいです",
    "オレンジ色または黄色の靴下がほしいです",
    "黄色またはオレンジ色の靴下がほしいです",
    "ピンクの靴下がほしいです",
]
  • 数字の大小
sentences_instruct = [
    "身長が100cm未満の方はこちらの入り口に進んでください",
    "身長が170cm未満の方はこちらの入り口に進んでください",
    "身長が200cm未満の方はこちらの入り口に進んでください",
    "身長が200cm以上の方はこちらの入り口に進んでください",
]

sentences_user = [
    "私の身長は80cmです",
    "私の身長は90cmです", 
    "私の身長は100cmです",
    "私の身長は110cmです",
    "私の身長は120cmです",
    "私の身長は130cmです",
    "私の身長は140cmです",
    "私の身長は150cmです",
    "私の身長は170cmです",
    "私の身長は171cmです",
    "私の身長は180cmです",
    "私の身長は190cmです",
    "私の身長は190cmより少し小さいです",
    "私の身長は190cmより少し大きいです",
    "私の身長は200cmです",
    "私の身長は210cmです",
    "私の身長は300cmです",
    "私の身長は80cmより小さいです",
    "私の身長は80cm以上です",
    "私の身長は120cm以下です",
    "私の身長は120cm以上です",
    "私の身長は170cmより小さいです",
    "私の身長は170cm以上です",
    "私の身長は300cmより小さいです",
    "私の身長は300cmより大きいです",
]

処理内容

類似度を計算するための処理は以下のようにしました。

  • 上記の入力テキストを、それぞれベクトルにする
  • ユーザからの質問のベクトルと、説明書のベクトルとのコサイン類似度を計算する
  • 質問に対して、類似度が一番高い説明書のテキストを選択する
  • (類似度のマトリックスをヒートマップで表示する)

使用コード

今回は、OpenAIのEmbeddings APIを使用し、モデルは"text-embedding-ada-002”を選択しました。(OpenAIのベクトルは長さ1に正規化されているので、内積を計算した結果をそのままコサイン類似度として出力しました)

クリックすると開きます
```python
from typing import List

import matplotlib.pyplot as plt
import numpy as np
import openai
import seaborn as sns

def sentence_to_vector(sentence: str):
    res = openai.Embedding.create(model="text-embedding-ada-002", input=sentence)

    embedding_vector = res["data"][0]["embedding"]
    return np.array(embedding_vector)

def compare_similarity(
    sentences_instruct: List[str],
    sentences_user: List[str],
):
    vectors_store = [sentence_to_vector(sentence) for sentence in sentences_instruct]
    vectors_user = [sentence_to_vector(sentence) for sentence in sentences_user]
    # cos_matrix = [
    #     [np.dot(vector1, vector2) / (np.linalg.norm(vector1) * np.linalg.norm(vector2)) for vector2 in vectors_store]
    #     for vector1 in vectors_user
    # ]
    cos_matrix = np.dot(np.array(vectors_user), np.array(vectors_store).T)
    return cos_matrix

def main(
    sentences_instruct: List[str],
    sentences_user: List[str],
):
    cos_matrix = compare_similarity(sentences_instruct, sentences_user)
    indexes_max = cos_matrix.argmax(axis=1)
    # print answer
    for sentence_user, index_store in zip(sentences_user, indexes_max):
        print(f"{sentence_user}{' '*(20-len(sentence_user))} {sentences_instruct[index_store]}")

    sns.heatmap(cos_matrix, vmin=0.7, vmax=1)
    plt.show()

if __name__ == "__main__":
    main(
        sentences_instruct=[
            "赤色の靴下がほしい際はこちらから購入してください",
            "オレンジ色の靴下がほしい際はこちらから購入してください",
            "黄色の靴下がほしい際はこちらから購入してください",
            "緑色の靴下がほしい際はこちらから購入してください",
            "青色の靴下がほしい際はこちらから購入してください",
            "紫色の靴下がほしい際はこちらから購入してください",
            "灰色の靴下がほしい際はこちらから購入してください",
            "黒色の靴下がほしい際はこちらから購入してください",
        ],
        sentences_user=[
            "赤色以外の靴下がほしいです",
            "オレンジ色以外の靴下がほしいです",
            "黄色以外の靴下がほしいです",
            "緑色以外の靴下がほしいです",
            "青色以外の靴下がほしいです",
            "紫色以外の靴下がほしいです",
            "灰色以外の靴下がほしいです",
            "黒色以外の靴下がほしいです",
            "オレンジ色または黄色の靴下がほしいです",
            "黄色またはオレンジ色の靴下がほしいです",
            "ピンクの靴下がほしいです",
        ],
    )

    main(
        sentences_instruct=[
            "Pythonの使い方を知りたい場合はこちらのページをご覧ください",
            "C++の使い方を知りたい場合はこちらのページをご覧ください",
            "TypeScriptの使い方を知りたい場合はこちらのページをご覧ください",
        ],
        sentences_user=[
            "Python以外のプログラミング言語の使い方を教えてください",
            "C++以外のプログラミング言語の使い方を教えてください",
            "TypeScript以外のプログラミング言語の使い方を教えてください",
        ],
    )

    main(
        sentences_instruct=[
            "身長が100cm未満の方はこちらの入り口に進んでください",
            "身長が170cm未満の方はこちらの入り口に進んでください",
            "身長が200cm未満の方はこちらの入り口に進んでください",
            "身長が200cm以上の方はこちらの入り口に進んでください",
        ],
        sentences_user=[
            "私の身長は80cmです",
            "私の身長は90cmです",
            "私の身長は100cmです",
            "私の身長は110cmです",
            "私の身長は120cmです",
            "私の身長は130cmです",
            "私の身長は140cmです",
            "私の身長は150cmです",
            "私の身長は170cmです",
            "私の身長は171cmです",
            "私の身長は180cmです",
            "私の身長は190cmです",
            "私の身長は190cmより少し小さいです",
            "私の身長は190cmより少し大きいです",
            "私の身長は200cmです",
            "私の身長は210cmです",
            "私の身長は300cmです",
            "私の身長は80cmより小さいです",
            "私の身長は80cm以上です",
            "私の身長は120cm以下です",
            "私の身長は120cm以上です",
            "私の身長は170cmより小さいです",
            "私の身長は170cm以上です",
            "私の身長は300cmより小さいです",
            "私の身長は300cmより大きいです",
        ],
    )

    main(
        sentences_instruct=[
            "I like my phone.",
        ],
        sentences_user=[
            "I don't like my phone.",
            "I like my android.",
        ],
    )

print()
```

結果

類似度が一番高い文書を選択した結果や、類似度のマトリックスを表示した結果は以下のとおりです。

「以外」

  • 類似度が一番高い文章

左:ユーザからの質問、右:説明書の文章のうち一番類似度が高かったもの

赤色以外の靴下がほしいです        赤色の靴下がほしい際はこちらから購入してください
オレンジ色以外の靴下がほしいです     オレンジ色の靴下がほしい際はこちらから購入してください
黄色以外の靴下がほしいです        黄色の靴下がほしい際はこちらから購入してください
緑色以外の靴下がほしいです        緑色の靴下がほしい際はこちらから購入してください
青色以外の靴下がほしいです        青色の靴下がほしい際はこちらから購入してください
紫色以外の靴下がほしいです        紫色の靴下がほしい際はこちらから購入してください
灰色以外の靴下がほしいです        灰色の靴下がほしい際はこちらから購入してください
黒色以外の靴下がほしいです        黒色の靴下がほしい際はこちらから購入してください
オレンジ色または黄色の靴下がほしいです  オレンジ色の靴下がほしい際はこちらから購入してください
黄色またはオレンジ色の靴下がほしいです  オレンジ色の靴下がほしい際はこちらから購入してください
ピンクの靴下がほしいです         紫色の靴下がほしい際はこちらから購入してください
  • 類似度のマトリックス

縦軸:ユーザからの質問、横軸:説明書のテキスト

  • 結果から言えること
    • 「以外」では、意図してような類似度の計算にならなかった
    • 質問に書かれている色が説明書に入っている方が類似度が高くなった
    • (全体的に類似度は高かった、ほとんど0.90以上)

数字の大小

  • 類似度が一番高い文章

左:ユーザからの質問、右:説明書の文章のうち一番類似度が高かったもの

私の身長は80cmです         身長が100cm未満の方はこちらの入り口に進んでください
私の身長は90cmです         身長が100cm未満の方はこちらの入り口に進んでください
私の身長は100cmです         身長が100cm未満の方はこちらの入り口に進んでください
私の身長は110cmです         身長が100cm未満の方はこちらの入り口に進んでください
私の身長は120cmです         身長が100cm未満の方はこちらの入り口に進んでください
私の身長は130cmです         身長が170cm未満の方はこちらの入り口に進んでください
私の身長は140cmです         身長が170cm未満の方はこちらの入り口に進んでください
私の身長は150cmです         身長が170cm未満の方はこちらの入り口に進んでください
私の身長は170cmです         身長が170cm未満の方はこちらの入り口に進んでください
私の身長は171cmです         身長が170cm未満の方はこちらの入り口に進んでください
私の身長は180cmです         身長が170cm未満の方はこちらの入り口に進んでください
私の身長は190cmです         身長が200cm以上の方はこちらの入り口に進んでください
私の身長は200cmです         身長が200cm以上の方はこちらの入り口に進んでください
私の身長は300cmです         身長が200cm以上の方はこちらの入り口に進んでください
私の身長は80cmより小さいです    身長が100cm未満の方はこちらの入り口に進んでください
私の身長は80cm以上です       身長が200cm以上の方はこちらの入り口に進んでください
私の身長は120cm以下です       身長が100cm未満の方はこちらの入り口に進んでください
私の身長は120cm以上です       身長が200cm以上の方はこちらの入り口に進んでください
私の身長は170cmより小さいです    身長が170cm未満の方はこちらの入り口に進んでください
私の身長は170cm以上です       身長が170cm未満の方はこちらの入り口に進んでください
私の身長は300cmより小さいです    身長が200cm未満の方はこちらの入り口に進んでください
私の身長は300cmより大きいです    身長が200cm以上の方はこちらの入り口に進んでください
  • 類似度のマトリックス

縦軸:ユーザからの質問、横軸:説明書のテキスト

  • 結果から言えること
    • 意図したような類似度の計算にならなかった
    • 近い数字が含まれているものや、「より小さい・未満・以上」の言葉が共通しているものの方が類似度が高かった
    • (全体的に類似度はそこそこ高かった、ほとんど0.80以上)

分析・考察

今回の結果から、以下のことが言えそうです。

  • 厳密な条件判定や判断が必要な場合、単純なEmbedding・ベクトル検索では対応が難しそう
  • 対応するには以下の方法がありそう
    • 検索方法を工夫する
    • 判定や判断は検索部分で行うのではなく、回答を生成する(=LLMに考えさせる)ところに任せる

妥当性について

今回の結果が妥当かどうか・一般的なことであると言えるかには、以下の点が不足していますので、ご注意ください。

  • 使い方が合っているか
    • そもそもEmbeddingってこうした検索用途が主なのか
      • 自分の知識不足でわかっていません。論文などを調べているところです。
      • OpenAIのリファレンスには、回答用の検索だけでなく、文章のクラスタリングや異常検知などに使われるケースもあると記載されています。

        https://platform.openai.com/docs/guides/embeddings

        Search (where results are ranked by relevance to a query string)
        Clustering (where text strings are grouped by similarity)
        Recommendations (where items with related text strings are recommended)
        Anomaly detection (where outliers with little relatedness are identified)
        Diversity measurement (where similarity distributions are analyzed)
        Classification (where text strings are classified by their most similar label)
      • どちらかというと同じ種類の文章同士を比較するケースが多く、今回のように質問と説明というのは異なる種類の比較になっているような気もします

    • 埋め込みモデルとして今回のような検索を目的とした学習方法だったか

      • text-embedding-ada-002の学習方法や使われたデータセットは、簡単に調べた範囲ではわかりませんでした。
      • 世の中のデータセットを見ていると、「2つの文章が類似かどうか」「トピックが近いか」を値で評価するというのが多く、OpenAIも同様のデータを利用して学習していそう(自分の推測です)。そうだとすると、「ある文章が質問の回答として使用できるか」という学習ではないので、今回のような使用方法が適切とは言えないかもしれません。

        https://github.com/yahoojapan/JGLUE

    • ada-002の想定している使い方だったか

      • 文章の長さが短いことも影響しているかもしれません。LlamaIndexなどの使い方では文字数がもう少し長いケース(chunk_size = 512など)が多いので、今回使用したテキストは文字数が少ないかもしれません。
  • サンプル数が少ない
    • 他にも試して同様の結果だったのですが、そこまで多くの数は試していません。10件くらいは試した方がよさそうです。

まとめ

「以外」「数字の大小」をベクトル検索で判定できるかを調べるために、OpenAIのEmbeddings APIを例として簡単な実験を行いました。結果、難しそうであるという印象をもちました。

解決策などについて検討した内容を、次のブログ記事でまとめたいと思っていますので、お待ちいただければ幸いです。

Appendix

今回の意図と結果の図

今回こうなってほしかった類似度と、実際の類似度の図です。太いほど類似度が高いことを示します(文章中だと分かりにくくなってしまったので、こちらに記載しました)。

  • 理想の類似度

  • 実際の類似度

他にも試したサンプル

「以外」のサンプルとしてプログラミング言語でもやってみましたが、上記と同様の結果でした。

プログラミング言語

  • 入力文章
sentences_instruct=[
    "Pythonの使い方を知りたい場合はこちらのページをご覧ください",
    "C++の使い方を知りたい場合はこちらのページをご覧ください",
    "TypeScriptの使い方を知りたい場合はこちらのページをご覧ください",
],
sentences_user=[
    "Python以外のプログラミング言語の使い方を教えてください",
    "C++以外のプログラミング言語の使い方を教えてください",
    "TypeScript以外のプログラミング言語の使い方を教えてください",
],
  • 結果

クエリと、一番類似していると判定された説明文

Python以外のプログラミング言語の使い方を教えてください
→ Pythonの使い方を知りたい場合はこちらのページをご覧ください

C++以外のプログラミング言語の使い方を教えてください
→ C++の使い方を知りたい場合はこちらのページをご覧ください

TypeScript以外のプログラミング言語の使い方を教えてください
→ TypeScriptの使い方を知りたい場合はこちらのページをご覧ください
  • 類似度のヒートマップ

英語の文章

  • 入力文章
sentences_instruct=[
    "Please purchase from here if you want red socks.",
    "Please purchase from here if you want orange socks.",
    "Please purchase from here if you want yellow socks.",
    "Please purchase from here if you want green socks.",
    "Please purchase from here if you want blue socks.",
    "Please purchase from here if you want purple socks.",
    "Please purchase from here if you want grey socks.",
    "Please purchase from here if you want black socks.",
],
sentences_user=[
    "I want socks that are any color but red.",
    "I want socks that are any color but orange.",
    "I want socks that are any color but yellow.",
    "I want socks that are any color but green.",
    "I want socks that are any color but blue.",
    "I want socks that are any color but purple.",
    "I want socks that are any color but grey.",
    "I want socks that are any color but black.",
    "I want socks that are either orange or yellow.",
    "I want socks that are either yellow or orange.",
    "I want pink socks.",
],
  • 結果

クエリと、一番類似していると判定された説明文

I want socks that are any color but red. Please purchase from here if you want red socks.
I want socks that are any color but orange. Please purchase from here if you want orange socks.
I want socks that are any color but yellow. Please purchase from here if you want yellow socks.
I want socks that are any color but green. Please purchase from here if you want green socks.  
I want socks that are any color but blue. Please purchase from here if you want blue socks.    
I want socks that are any color but purple. Please purchase from here if you want purple socks.
I want socks that are any color but grey. Please purchase from here if you want grey socks.    
I want socks that are any color but black. Please purchase from here if you want black socks.  
I want socks that are either orange or yellow. Please purchase from here if you want yellow socks.
I want socks that are either yellow or orange. Please purchase from here if you want yellow socks.
I want pink socks.   Please purchase from here if you want purple socks.
  • 類似度のヒートマップ