😎

Embeddingsの理解を深めるために「Question answering using embeddings」をやってみた

2023/06/19に公開

はじめに

OpenAIを使ったことがある方ならEmbeddingsという言葉は聞いたことがあると思います。ただ、具体的にこれがなんなのかと聞かれると正直僕はよく分かっていませんでした。

なんとなくモデルにたいして調教するイメージはあるけれど、具体的にどうするのか。また、別でよく聞くFine-tuningとはどう違うのか。
今回はこの辺りの理解を深めるためにOpenAIが用意している「Question answering using embeddings」をやってみました。

このドキュメントはOpenAIが用意しているもので、Cookbookとして他にもたくさんのexampleが用意されています。
https://github.com/openai/openai-cookbook

今回の内容は素のGPTでは答えられない質問である、
「2022年の冬季オリンピックでカーリングの金メダルを獲得したアスリートは誰ですか?」という問いにGPTが答えられる様にするものです。

※まだ理解が浅い部分があるので、もし間違っている部分があったら指摘お願いします🙏

試し方

各exampleはipynbファイルになっています。これはJupyter notebook形式のファイルで、JSON形式でありながらmarkdown方式と組み合わせることが可能となっています。
今回実行環境としてGoogle Colabを使いました。新しいノートブックを作って、そこに先ほどのファイルをアップロードするとドキュメント&実行環境が整います。便利!

今回の内容について

色々な質問に答えてくれるGPTも、苦手な質問があります。

  • 2021年9月より新しい情報
  • 公開されていないドキュメント
  • 過去の会話からの情報

これらを解決するために、「Search-Askメソッド」という方法があります。

  1. Search: テキストのライブラリを検索して、関連するテキストセクションを検索します。
  2. Ask: 取得したテキストセクションをGPTにインプットして、質問します。

ファインチューニングは使えないの?

GPTに知識を学ばせる方法は2つあります。

  • モデルの重みによる方法 (トレーニングセットでモデルをファインチューニングする)
  • モデルの入力による方法 (知識を入力メッセージにインプットする)

こうやって見るとファインチューニングの方が自然な選択肢に見えそうですが、実はモデルに知識を教える方法として推奨されていません。その理由は、事実の回想に信頼性がないためです。

一方、メッセージ入力は目の前にあるノートを広げながら質問に答える様なものです。そのため信頼性が高くなります。

ただし、1つ欠点として、各モデルが一度に読むことができるテキストの最大量に限りがあります。
これを解決するものが「Search-Askメソッド」です。

検索方法

検索方法にはいくつか種類がありますが、ここでは「Embedding-based search」を使用しています。ここで「Embedding」が出てきます。

僕も勘違いしていたのですが、Fine-tuningとEmbeddingを並列で考えていると少し混乱してしまいます。
まず、知識を学ばせる方法として、

  • Fine-tuning
  • Search-Ask

という方法があります。

そして、Search-Askメソッドを効率よく利用するために、Embeddingという手法を使うということになります。
効率性と効果性が高いためEmbeddingがよく使われますが、モデルが理解可能な形にさえ変換できれば他の方法でも可能です。

全体の流れ

今回のexampleの全体の流れです。

  1. 検索データを用意する(1文書につき1回)
    A. 収集する: 2022年のオリンピックに関するWikipediaの記事を数百件ダウンロードする
    B. チャンクする: ドキュメントを短く、ほとんど自己完結したセクションに分割し、埋め込む
    C. 埋め込む: 各セクションは、OpenAI APIを使って埋め込まれる
    D. 保存する: エンベディングが保存される(大規模なデータセットの場合、ベクターデータベースを使用)。
  2. 検索(クエリごとに1回)
    A. ユーザーの質問があった場合、OpenAI APIからクエリに対するエンベディングを生成する
    B. エンベディングを利用して、クエリとの関連性でテキストセクションをランク付けする。
  3. 質問する(クエリごとに1回)
    A. 質問と関連性の高い部分をGPTへのメッセージに挿入する。
    B. GPTの回答を返す

最初読んだ時、検索データをエンベディングすることと、検索のクエリをエンベディングすることがよく理解できませんでした。
エンべディングが何か調べてみても「情報を数値のベクトルに変換すること」と書かれていてピンときません。検索するということは、要するに型を合わせる様なものだとは思うのですが。

そこでChatGPTに聞いてみたところ、地図に例えて答えてくれました。これはなかなか分かりやすい説明だと思います。

また、エンベディングは「埋め込み」なので、何かに埋め込むのかなと想像します。ただ、実際にやっていることは「変換」です。ここもまた最初理解しにくい部分だと思います。
そんな時はChatGPTです。

何となくのイメージですが、3次元空間があって、そこにいろんな言葉がベクトル変換されてプロットされている。つまり点が打たれている。そこに対して、例えば「こんにちは」という言葉を変換すると、「おはようございます」という点の近くに置くことになる。これを「エンベディング(埋め込み)」と呼んでいるのかなと思いました。3D空間の中に「埋め込む」イメージです。違ったらごめんなさい😅

では順番に見ていきましょう。

Google Colabで実行

基本的には用意されているコードを実行していくだけでOKです。ただ、僕の場合はライブラリがインストールされていないと怒られてしまったので、
!pip install openai tiktokenを追加して実行しています。

OenAIのAPI_KEYについてはOPENAI_API_KEYという環境変数があるみたいですが、設定方法が分からなかったので直接指定しています。

何もせずに、2022年の情報を聞いてみます

# an example question about the 2022 Olympics
query = 'Which athletes won the gold medal in curling at the 2022 Winter Olympics?'

response = openai.ChatCompletion.create(
    messages=[
        {'role': 'system', 'content': 'You answer questions about the 2022 Winter Olympics.'},
        {'role': 'user', 'content': query},
    ],
    model=GPT_MODEL,
    temperature=0,
)

print(response['choices'][0]['message']['content'])

実行結果

I'm sorry, but as an AI language model, I don't have information about the future events. The 2022 Winter Olympics will be held in Beijing, China, from February 4 to 20, 2022. The curling events will take place during the games, and the winners of the gold medal in curling will be determined at that time.

当然ですが、未来のことはわからないと返ってきます。

入力メッセージにトピックに関する知識を入力してみます

wikipedia_article_on_curlingにはWikipediaの2022年冬季オリンピックのページの上半分が入っています。上半分なのは文字数オーバーを避けるためです。

query = f"""Use the below article on the 2022 Winter Olympics to answer the subsequent question. If the answer cannot be found, write "I don't know."

Article:
\"\"\"
{wikipedia_article_on_curling}
\"\"\"

Question: Which athletes won the gold medal in curling at the 2022 Winter Olympics?"""

response = openai.ChatCompletion.create(
    messages=[
        {'role': 'system', 'content': 'You answer questions about the 2022 Winter Olympics.'},
        {'role': 'user', 'content': query},
    ],
    model=GPT_MODEL,
    temperature=0,
)

print(response['choices'][0]['message']['content'])

実行結果

There were three events in curling at the 2022 Winter Olympics, so there were three sets of athletes who won gold medals. The gold medalists in men's curling were Sweden's Niklas Edin, Oskar Eriksson, Rasmus Wranå, Christoffer Sundgren, and Daniel Magnusson. The gold medalists in women's curling were Great Britain's Eve Muirhead, Vicky Wright, Jennifer Dodds, Hailey Duff, and Mili Smith. The gold medalists in mixed doubles curling were Italy's Stefania Constantini and Amos Mosaner.

正しい答えを返す様になりました。
しかし、これは何を聞くか分かっていたからインプット情報として冬季オリンピックの情報を渡していました。
次は、embeddings-basedの検索を使用してこの知識の挿入を自動化する方法を試してみます。

1. 検索データの準備

サンプルデータは既に用意されているので、そちらを使用します。
このサンプルデータの用意方法についてはまた別のEmbedding Wikipedia articles for searchというexampleに載っています。
もしかするとエンベディングについて理解するにはこっちの方が適してるかも、と思ったのですが今回は割愛します。

# download pre-chunked text and pre-computed embeddings
# this file is ~200 MB, so may take a minute depending on your connection speed
embeddings_path = "https://cdn.openai.com/API/examples/data/winter_olympics_2022.csv"

df = pd.read_csv(embeddings_path)

# convert embeddings from CSV str type back to list type
df['embedding'] = df['embedding'].apply(ast.literal_eval)

2. 検索

次の検索関数を定義します。

  • ユーザークエリと、テキストと埋め込み列を含むデータフレームを受け取ります →引数です。
  • ユーザークエリをOpenAI APIでエンべディングします →OpenAI APIを使って変換します。
  • クエリのエンベディングとテキストのエンベディングの間の距離を使用してテキストをランク付けします →お互いのベクトルから距離を計算します(relatedness_fn関数)
  • 2 つのリストを返します
    • 関連性順にランク付けされた上位 N 個のテキスト
    • 対応する関連性スコア
# search function
def strings_ranked_by_relatedness(
    query: str,
    df: pd.DataFrame,
    relatedness_fn=lambda x, y: 1 - spatial.distance.cosine(x, y),
    top_n: int = 100
) -> tuple[list[str], list[float]]:
    """Returns a list of strings and relatednesses, sorted from most related to least."""
    query_embedding_response = openai.Embedding.create(
        model=EMBEDDING_MODEL,
        input=query,
    )
    query_embedding = query_embedding_response["data"][0]["embedding"]
    strings_and_relatednesses = [
        (row["text"], relatedness_fn(query_embedding, row["embedding"]))
        for i, row in df.iterrows()
    ]
    strings_and_relatednesses.sort(key=lambda x: x[1], reverse=True)
    strings, relatednesses = zip(*strings_and_relatednesses)
    return strings[:top_n], relatednesses[:top_n]
# examples
strings, relatednesses = strings_ranked_by_relatedness("curling gold medal", df, top_n=5)
for string, relatedness in zip(strings, relatednesses):
    print(f"{relatedness=:.3f}")
    display(string)

3. 問い合わせ

上記の検索機能で、関連する知識を自動的に取得し、GPTへのメッセージに挿入することができるようになりました。

次は以下のようなask関数を定義します。

  • ユーザーからのクエリを受け取る →引数
  • クエリに関連するテキストを検索する →「2.検索」で作った「strings_ranked_by_relatedness」関数
  • そのテキストをGPTのメッセージとして渡す →message変数
  • GPTにメッセージを送る
  • GPTの回答を返す
def num_tokens(text: str, model: str = GPT_MODEL) -> int:
    """Return the number of tokens in a string."""
    encoding = tiktoken.encoding_for_model(model)
    return len(encoding.encode(text))


def query_message(
    query: str,
    df: pd.DataFrame,
    model: str,
    token_budget: int
) -> str:
    """Return a message for GPT, with relevant source texts pulled from a dataframe."""
    strings, relatednesses = strings_ranked_by_relatedness(query, df)
    introduction = 'Use the below articles on the 2022 Winter Olympics to answer the subsequent question. If the answer cannot be found in the articles, write "I could not find an answer."'
    question = f"\n\nQuestion: {query}"
    message = introduction
    for string in strings:
        next_article = f'\n\nWikipedia article section:\n"""\n{string}\n"""'
        if (
            num_tokens(message + next_article + question, model=model)
            > token_budget
        ):
            break
        else:
            message += next_article
    return message + question


def ask(
    query: str,
    df: pd.DataFrame = df,
    model: str = GPT_MODEL,
    token_budget: int = 4096 - 500,
    print_message: bool = False,
) -> str:
    """Answers a query using GPT and a dataframe of relevant texts and embeddings."""
    message = query_message(query, df, model=model, token_budget=token_budget)
    if print_message:
        print(message)
    messages = [
        {"role": "system", "content": "You answer questions about the 2022 Winter Olympics."},
        {"role": "user", "content": message},
    ]
    response = openai.ChatCompletion.create(
        model=model,
        messages=messages,
        temperature=0
    )
    response_message = response["choices"][0]["message"]["content"]
    return response_message

これで、問い合わせができるask関数ができました。

実際に質問してみる

実行結果

There were two gold medal-winning teams in curling at the 2022 Winter Olympics: the Swedish men's team consisting of Niklas Edin, Oskar Eriksson, Rasmus Wranå, Christoffer Sundgren, and Daniel Magnusson, and the British women's team consisting of Eve Muirhead, Vicky Wright, Jennifer Dodds, Hailey Duff, and Mili Smith.

2022年の冬季オリンピックでカーリングで金メダルを獲得したのは2チームありました:スウェーデンの男子チーム(メンバー:ニクラス・エディン、オスカー・エリクソン、ラスムス・ヴラナ、クリストファー・スンドグレン、ダニエル・マグヌソン)と、イギリスの女子チーム(メンバー:イヴ・ミュアヘッド、ヴィッキー・ライト、ジェニファー・ドッズ、ヘイリー・ダフ、ミリ・スミス)。
(chatGPTで翻訳)

gpt-3.5-turboには2022年冬季オリンピックに関する知識がありませんが、この様に正しい情報を取得することができました🙌

まとめ

OpenAIの「質問応答をエンベディングで行う」手法はいかがだったでしょうか。もともとGPTが対応できない最近の情報についても、Search-Askメソッドを使うことできちんと答えてくれることがわかりました。これを応用すれば社内WikiやNotionの情報を元に、Slackにbotを作るとかも出来そうです。

ちなみにですが、最近発表されたリリースがなかなかすごいです。

new 16k context version of gpt-3.5-turbo (vs the standard 4k version)
https://openai.com/blog/function-calling-and-other-api-updates

16kトークン使えるということは、短編小説をインプットとして渡せてしまいます。
https://note.com/mahlab/n/n99577fabf16e

あれ、Search-Askとかしなくて全部突っ込めば良いのではという気になってきます😅
もちろんこれ以上の情報を渡したい場合は必要になるんですけど、もう少し待てばもっと増えるのでは?と言う気にもなってきます。
ただ、コスト面ではSearch-Askした方が安くなると思うので、そこはプロダクトの特性によって変わってくるかなと思います。

なかなかアップデート多くてついていくのが大変ですが、新しくできることが増えるのは楽しいです。みなさんもぜひ試してみてください🙌

レスキューナウテックブログ

Discussion