会社の中の雑談で、Embeddings APIを使ってChatBot作れるよね、という話をしていました。 僕はこのあたり初心者なのですが、まずは動かすもの作ったらイメージが沸くだろうと、とりあえず実装してみます。
Embeddingsとは何か
Embeddingsとは、単語や文章をある種のベクトルに変換することを指します。例えば2つの文章を表現する2つのベクトルを生成すると、その間の類似度がコサイン類似度によって求められます。 このように、扱いづらい自然言語をベクトルで表現することによって、さまざまなことが可能になります。OpenAIの公式ページでは、以下のような応用例が示されています。
- 検索
- クラスタリング
- 推薦
- 異常検知
- 多様性の測定
- 分類
Embeddings API
ここでは、Embeddings APIを実際に利用してみましょう。 OpenAIの提供するAPIはシンプルであるため、curlで容易に呼び出せます。ここでは、結果となるベクトルの先頭5つを抽出してみます。
$ curl -s https://api.openai.com/v1/embeddings \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "input": "今日はいい天気ですね", "model": "text-embedding-ada-002" }' | jq '.data[].embedding | limit(5;.[])' 0.0022655425 0.0025153372 -0.0050211884 -0.02542214 0.008973636
当たり前ですが、上記のように文章がベクトルに変換できていることがわかります。 ベクトルの次元は1536です。
$ curl https://api.openai.com/v1/embeddings -H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" -d '{ "input": "今日はいい天気ですね", "model": "text-embedding-ada-002" }' | jq '.data[].embedding | length' 1536
コスト
OpenAIの他のAPIと同様に、Embeddingsのコストについても以下の2つの要素で決まります。
- 利用するモデル
- 入出力トークン数
6/24現在、ほぼ全てのユースケースに対してモデルtext-embedding-ada-002
が推奨されています。このモデルで定義されるコストは$0.0001/1K Tokenです。
入力トークン数
text-embedding-ada-002
の最大の入力トークン数は8,191となっています。
日本語にすると、およそ8,000文字程度になるでしょうか。長大な文章をベクトル化しようとすると、分割せざるを得ないようです。
ChatBotを作ってみる
では、このEmbeddings APIを用いて、ChatBotを作ってみましょう。
ChatBotの作り方は以下の公式チュートリアルに載っていますが、少し凝った内容になっています。この記事ではもう少しシンプルにすることで、方法論のところを示したいです。
実行の様子
まず最初に、実行結果を示します。
ChatBotは自分の認識している情報を文章として保持しており、 与えられた質問に対する類似度を文章ごとに判断しています。 ここではコサイン類似度を用いています。
Chatbotが認識している文章のうち、類似している文章と質問をGPTに渡すことで、その回答を得ると言うのが基本的な動きです。
$ rye run python src/embeddings/main.py "明後日の天気を教えて" 類似度:0.102131, テキスト:明後日は台風のようです 類似度:0.133388, テキスト:明日は雨です。土砂降りですね。 類似度:0.163842, テキスト:今日はいい天気です 類似度:0.203095, テキスト:昨日は雪でした。寒かったですね。 類似度:0.244429, テキスト:私はkiririmodeです 質問: 明後日の天気を教えて 回答: 明後日は台風が接近しているため、大雨や強風が予想されます。外出の際はくれぐれもお気をつけください。 $ rye run python src/embeddings/main.py "明日の天気を予報してくれると嬉しいな〜〜〜〜" 類似度:0.116179, テキスト:明日は雨です。土砂降りですね。 類似度:0.130837, テキスト:明後日は台風のようです 類似度:0.134058, テキスト:今日はいい天気です 類似度:0.192421, テキスト:昨日は雪でした。寒かったですね。 類似度:0.247638, テキスト:私はkiririmodeです 質問: 明日の天気を予報してくれると嬉しいな〜〜〜〜 回答: 明日は雨の予報です。傘を持って出かけることをおすすめします。
GPTに渡すコンテキストの作成
コンテキストは以下のメソッドで作成しています。
まず最初に、Chatbotが保持している文章ごとにEmbeddings APIを呼び出し、1,536次元のベクトルを得ます。同様に質問に対応するベクトルも作成した上で、質問と保持文章それぞれのコサイン類似度を計算します。
コサイン類似度の高い文章が、質問に対する回答を得るための有力な情報源です。このため、これをコンテキストとして抽出しています。
もちろん長大なコンテキストをGPTに渡せれば良いのですが、前述の通りコンテキスト長にはトークン長の制限があるため、類似度の高いものを優先する必要があります。トークン長を得るためにはtiktokenを使って計算する必要があるのですが、そこは今回割愛しています。実装自体はあまり難しくはありません。
def generate_context_from_texts(df, question, engine): """ このメソッドは、テキストと質問から、GPTに提供するコンテキストを生成します。 df: 各テキストを含むDataFrame question: 質問 engine: 使用するEmbedding用エンジン """ # 各テキストについてEmbeddingsを生成します df["embeddings"] = np.array( df.text.apply( lambda x: openai.Embedding.create(input=x, engine=engine)["data"][0][ "embedding" ] ) ) # 質問に対応するベクトルを生成します q_embeddings = openai.Embedding.create(input=question, engine=engine)["data"][0][ "embedding" ] # 各テキストのエンコーディングと質問のエンコーディング間の距離を計算します df["distances"] = distances_from_embeddings( q_embeddings, df["embeddings"].values, "cosine" ) # 質問からの距離に基づいてテキストをソートします sorted_texts = [] for i, row in df.sort_values("distances", ascending=True).iterrows(): print("\t類似度:{:f}, テキスト:{}".format(row["distances"], row["text"])) # TODO: 本来は、ここでトークン長を制限しなければならない sorted_texts.append(row["text"]) # ソートされたテキストを結合します return "\n".join(sorted_texts)
回答を得る
コンテキストさえ作成できれば、あとはChatCompletion APIを呼び出すだけです。 この辺りは初めてのGPTのAPI(OpenAI API):コストの理解とTypeScriptでの呼び出し - 理系学生日記と同じですね。
def generate_response(model, context, question): """ このメソッドは、指定されたモデルを使用して、質問に対する応答を生成します。 model: 使用するOpenAIのモデル context: モデルに供給するコンテキスト question: モデルに供給する質問 """ return openai.ChatCompletion.create( model=model, messages=[ { "role": "system", "content": "あなたは天気予報をするキャスターです。以下の街の声のみを基にして、あなたの予測としての、信頼性のある天気予報を提供してください。ただし、晴れなのか雨なのか曇りなのか、といったことのみをシンプルに示してください。気温等の情報は不要です" + context, }, {"role": "user", "content": question}, ], temperature=0, )
実装ソース全文
import pandas as pd import openai import numpy as np from openai.embeddings_utils import distances_from_embeddings import sys EMBEDDING_ENGINE = "text-embedding-ada-002" CHAT_ENGINE = "gpt-3.5-turbo" def generate_context_from_texts(df, question, engine): """ このメソッドは、テキストと質問から、GPTに提供するコンテキストを生成します。 df: 各テキストを含むDataFrame question: 質問 engine: 使用するEmbedding用エンジン """ # 各テキストについて埋め込みを生成します df["embeddings"] = np.array( df.text.apply( lambda x: openai.Embedding.create(input=x, engine=engine)["data"][0][ "embedding" ] ) ) # 質問に対応するベクトルを生成します q_embeddings = openai.Embedding.create(input=question, engine=engine)["data"][0][ "embedding" ] # 各テキストのエンコーディングと質問のエンコーディング間の距離を計算します df["distances"] = distances_from_embeddings( q_embeddings, df["embeddings"].values, "cosine" ) # 質問からの距離に基づいてテキストをソートします sorted_texts = [] for i, row in df.sort_values("distances", ascending=True).iterrows(): print("\t類似度:{:f}, テキスト:{}".format(row["distances"], row["text"])) # TODO: 本来は、ここでトークン長を制限しなければならない sorted_texts.append(row["text"]) # ソートされたテキストを結合します return "\n".join(sorted_texts) def generate_response(model, context, question): """ このメソッドは、指定されたモデルを使用して、質問に対する応答を生成します。 model: 使用するOpenAIのモデル context: モデルに供給するコンテキスト question: モデルに供給する質問 """ return openai.ChatCompletion.create( model=model, messages=[ { "role": "system", "content": "あなたは天気予報をするキャスターです。以下の街の声のみを基にして、あなたの予測としての、信頼性のある天気予報を提供してください。ただし、晴れなのか雨なのか曇りなのか、といったことのみをシンプルに示してください。気温等の情報は不要です" + context, }, {"role": "user", "content": question}, ], temperature=0, ) # テキストのリストを辞書形式で定義します texts = { "text": [ "昨日は雪でした。寒かったですね。", "今日はいい天気です", "明日は雨です。土砂降りですね。", "明後日は台風のようです", "私はkiririmodeです", ], } df = pd.DataFrame.from_dict(texts) question = sys.argv[1] context = generate_context_from_texts(df, question, EMBEDDING_ENGINE) response = generate_response(CHAT_ENGINE, context, question) print("質問:\n\t{}\n回答:\n\t{}".format(question, response.choices[0].message.content))