大幅にコストが安くなったOpenAIの最新のエンベディングモデルを使ってみた
イー・エージェンシーで広報・PRを担当している甲斐大樹です。
ノンプログラマーなのですが、OpenAIの最新のエンベディングモデル(embedding model)のコストが以前のモデルより大幅に安くなったので、これはぜひ試してみたい!ということで、さっそく使ってみることにしました。
ノンプログラマーなので、今回も生成AI(ChatGPT plusとBard)を最大限に利用して、自分がわからない部分を説明してもらったり、コードを書いてもらったりしています。
求人募集中!自信を持って業務に取り組める職場環境を提供しています!
OpenAIの最新のエンベディングモデル(embedding model)とは?
エンベディング(embedding)とは?
エンベディング(embedding)とは、単語やテキストなどのデータを、AIが扱いやすいように、数値ベクトルデータに変換する技術のことです。これによって、単語やテキストなどのデータ同士が意味的に近いかどうか、AIが判別できるようになります。
▼参考:Embedding(エンベディング:埋め込み、埋め込み表現)とは?:AI・機械学習の用語辞典 - @IT
大幅にコストが安く、効率も向上した最新モデル
OpenAIによると、最新のエンベディングモデルは、コストが大幅に安くなっただけではなく、効率も大幅に向上しているようです。
OpenAIの最新のエンベディングモデルを使って、CSVデータをエンベディングして検索してみた
ということで、今回は小さい方のモデル「text-embedding-3-small」を使ってみることにしました。
参考にしたのは、OpenAI公式のサンプルコード集「openai-cookbook」にある次の2つのプログラムです。
これらをGoogle Colab上で動かすことで、エンベディングベースの検索がどんな使用感になるか試してみることにしました。
1つ目のサンプルコードはこちら。
・Question answering using embeddings-based search
(エンベディングベースの検索を使用した質問応答)
2つ目のサンプルコードはこちら。
・Embedding Wikipedia articles for search
(検索のためのウィキペディア記事のエンベディング)
ここで、プログラムが2つに分かれているのがまどろっこしいように感じたので、また、エンベディングベースの検索を使って実際にやってみたいことがあったので、生成AIを利用して、次のように1つのプログラムにまとめてもらいました。
・CSVデータをエンベディングして検索する
https://colab.research.google.com/drive/1pv2D4COrYHbjYjuPJ6WxO4iYXtU8R2M5
ウィキペディアの記事データの代わりに、任意のCSVデータをエンベディングして検索できるようにしています。
Google Drive上の特定のディレクトリに自分で作ったCSVファイルを置けば、それをエンベディングデータとして処理し、そのファイル内を検索できるようになっています。
これによって、たとえば次のようなことが実現できるはずです。
自分の好きな小説をパラグラフごとに改行したCSVファイルをエンベディング検索する。
自分のTwitterのデータをエクスポートして投稿ごとに改行したCSVファイルをエンベディング検索する。
Q&Aを1セットで1行ずつ入れて改行したCSVファイルをエンベディング検索する。
今回は1つ目を試してみましたが、みなさんには他の使い方もぜひ試してみてほしいです。
では、今回のプログラムを紹介します。
1. エンベディングデータの作成
1-1. ライブラリのインポート
!pip install openai
#imports
import openai # for generating embeddings
import os # for environment variables
import pandas as pd # for DataFrames to store article sections and embeddings
from google.colab import files, userdata # この行を追加
import chardet
#google colabの環境変数を取得
from google.colab import userdata
#シークレット ※ からOPENAI_API_KEYを取得
client = openai.OpenAI(api_key=os.environ.get("OPENAI_API_KEY", userdata.get('OPENAI_API_KEY')))
1-2. CSVデータをエンベディング
CSVの1列目(項目名:textとする)の全行をエンベディング処理します。
実行するとCSV形式のファイルのアップロードインタフェースが動きます。
アップロードされたファイルを読み込み、Pandas(データ分析ライブラリ)を使用してDataFrame(表形式のデータ構造)に変換します。
CSVファイルには、夏目漱石の『吾輩は猫である』を1パラグラフごとに1行ずつ改行したテキストデータを用意しました。
import numpy as np
ファイルのアップロード
uploaded = files.upload()
アップロードされたファイル名を取得
file_name = next(iter(uploaded))
ファイルのエンコーディングを推測
with open(file_name, 'rb') as file:
result = chardet.detect(file.read())
encoding = result['encoding']
ファイルをDataFrameに読み込む
df = pd.read_csv(file_name, encoding=encoding)
エンベディングモデル「text-embedding-3-small」とバッチサイズの設定
EMBEDDING_MODEL = "text-embedding-3-small"
MAX_TOKENS = 5000
テキストを指定されたトークン数で分割する関数
def split_text(text, max_tokens=MAX_TOKENS):
return [text[i:i+max_tokens] for i in range(0, len(text), max_tokens)]
エンベディングの計算
all_embeddings = [] # 各行ごとのエンベディングを格納するリスト
各行のテキストを分割し、エンベディングを計算する
for index, text in enumerate(df['text']):
if index % 100 == 0:
print(f"Processing row {index}...") # 100行ごとの進行状況を表示
text_segments = split_text(text)
text_embeddings = []
for segment in text_segments:
response = client.embeddings.create(
model=EMBEDDING_MODEL,
input=[segment] # テキストセグメントをリストに変換
)
text_embeddings.append(response.data[0].embedding)
# セグメントのエンベディングの平均を計算
avg_embedding = np.mean(text_embeddings, axis=0)
all_embeddings.append(avg_embedding.tolist())
DataFrameにエンベディングを追加
df['embedding'] = all_embeddings
結果をCSVに保存
df.to_csv('embedded_data.csv', index=False)
ここで出力されたCSVデータは、次のように多次元の数値ベクトルで表現されています。
2. エンベディングデータを検索する
2-1. ライブラリのインポート
!pip install tiktoken
import ast # for converting embeddings saved as strings back to arrays
import tiktoken # for counting tokens
from scipy import spatial
GPT_MODEL = "gpt-3.5-turbo-1106"
2-2. エンベディングデータの確認
embeddings_path = "embedded_data.csv"
df = pd.read_csv(embeddings_path)
CSV形式のエンベディングデータをリスト形式に変換します。
df['embedding'] = df['embedding'].apply(ast.literal_eval)
データフレームには "text" と "embedding" の2つの列があります。
df
2-3. エンベディングによる検索
検索機能
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 = client.embeddings.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]
サンプルを見る
strings, relatednesses = strings_ranked_by_relatedness("吾輩は猫なの?", df, top_n=5)
for string, relatedness in zip(strings, relatednesses):
print(f"{relatedness=:.3f}")
display(string)
2-4. エンベディングによる質問
検索機能を使用することで、ユーザーのクエリに関連するテキストを自動的に取得して、GPTへのメッセージに挿入できます。
そこで、次のようなask関数を定義しました。
ユーザーのクエリを取得する。
そのクエリに関連するテキストを検索する。
そのテキストをGPTへのメッセージに挿入する。
そのメッセージを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 = '以下を参考にして、次の質問への回答を、参考文章に言及せずにわかりやすい文章にして答えてください。もし答えがセンテンスに見つからない場合は、「見つかりませんでした」とのみ出力してください。"'
question = f"\n\n質問: {query}"
message = introduction
for string in strings:
next_article = f'\n\n質問に相関性の高い文章:\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": "あなたは質問に丁寧に対応します"},
{"role": "user", "content": message},
]
response = client.chat.completions.create(
model=model,
messages=messages,
temperature=0.5
)
response_message = response.choices[0].message.content
return response_message
例1:GPT-3.5の場合
ask('楽しみはありましたか?',print_message = "true")
例2:GPT-4の場合
ask('主人は物語を通じてどんなことをしましたか?', model="gpt-4-0125-preview")
今回は例2のGPT-4の場合のみ試しました。
例2のGPT-4の場合、ask関数を実行した結果は次のとおりです。
3. やってみて気づいたこと
以上のように、今回はOpenAIの最新のエンベディングモデルを使って、CSVデータをエンベディングして検索してみました。
・CSVデータをエンベディングして検索する
https://colab.research.google.com/drive/1pv2D4COrYHbjYjuPJ6WxO4iYXtU8R2M5
夏目漱石の『吾輩は猫である』の全文を1パラグラフごとに1行として、各パラグラフを「text-embedding-3-small」でエンベディングしています。
クエリーを投げると、意味空間からそのクエリーに近いパラグラフをランキングして取得し、上位のパラグラフをGPT-4で統合して回答を出すという仕組みになっています。
小さい方のモデル「text-embedding-3-small」でも期待した通り動作
実は以前にOpenAIのファインチューニングを試したことがあったのですが、処理にとても時間が掛かりました。
一方、今回試したエンベディングでは、それほど時間も掛からず、コストもほとんど要しませんでした。
さらに、検索結果が確かにエンベディングしたデータから生成されており、小さい方のモデル「text-embedding-3-small」でも、期待した通りの動作を確認することができました。
エンベディングするためのデータをどのようなまとまりに分割していくのか
その反面、難しいと感じたのは、エンベディングするためのデータをどのようなまとまり(チャンク)に分割していくのかという点で、これにはノウハウがすごく必要だと感じました。
たとえば今回は、『吾輩は猫である』の文章をセンテンス単位でエンベディングするのか、パラグラフ単位でエンベディングするのかで悩みました。
ひとまとまりのデータの区切りをセンテンスにしたときは、意味が細切れとなり、本来の文章が持つ深い意味合いがなくなってしまったのか、単なるキーワード検索の結果のようになってしまった印象でした。
そのため、今回は最終的にパラグラフ単位でエンベディングすることにしました。
このように、ひとまとまりの意味を持ったデータをどのようなサイズにするのかはとても重要です。
しかしながら、そのように適切なデータに分ける作業を人間がやってしまうと、コストが高すぎて悪手のような気がします。
その解決策としては、適切なデータに分ける作業にも、生成AIを使えたらうまくいくのではないかと思うのです。
今回はそこまでできなかったのですが、また次の機会があればやってみたいところです。
今回試してみたことは以上です。
みなさんもぜひOpenAIのエンベディングモデルの活用方法を考えてみてください。
AIに関する記事はこちら
AIについてイー・エージェンシーの社長が考えていること
イー・エージェンシーでは一緒に働く仲間を募集しています!
イー・エージェンシーでは一緒に働く仲間を募集しています。下記の採用情報をぜひご覧ください!ご応募をお待ちしております
▰在宅勤務・全国内フルリモートOK!shutto翻訳開発エンジニア募集中!▰
▰採用サイトで、もっと先輩社員を知る!▰
▰在宅勤務・全国内フルリモートOK!エンジニア募集中!▰
▰当社代表取締役が創業時のこと、会社への想いを綴っています▰