見出し画像

大幅にコストが安くなったOpenAIの最新のエンベディングモデルを使ってみた

イー・エージェンシーで広報・PRを担当している甲斐大樹です。
ノンプログラマーなのですが、OpenAIの最新のエンベディングモデル(embedding model)のコストが以前のモデルより大幅に安くなったので、これはぜひ試してみたい!ということで、さっそく使ってみることにしました。
ノンプログラマーなので、今回も生成AI(ChatGPT plusとBard)を最大限に利用して、自分がわからない部分を説明してもらったり、コードを書いてもらったりしています。


OpenAIの最新のエンベディングモデル(embedding model)とは?

エンベディング(embedding)とは?

エンベディング(embedding)とは、単語やテキストなどのデータを、AIが扱いやすいように、数値ベクトルデータに変換する技術のことです。これによって、単語やテキストなどのデータ同士が意味的に近いかどうか、AIが判別できるようになります。

▼参考:Embedding(エンベディング:埋め込み、埋め込み表現)とは?:AI・機械学習の用語辞典 - @IT

大幅にコストが安く、効率も向上した最新モデル

OpenAIによると、最新のエンベディングモデルは、コストが大幅に安くなっただけではなく、効率も大幅に向上しているようです。

OpenAIが新しいエンベディングモデルを発表し、価格を大幅に削減しました。最新のtext-embedding-3-smallモデルは、前世代のtext-embedding-ada-002モデルに比べて大幅に効率が向上し、価格が1kトークンあたり$0.0001から$0.00002へと5倍減少しました。この新しいモデルは、多言語検索のベンチマークで31.4%から44.0%へ、英語タスクのベンチマークで61.0%から62.3%へと平均スコアが向上しています。
また、より大きな新モデルであるtext-embedding-3-largeも導入されました。このモデルは3072次元のエンベディングを作成し、同じくtext-embedding-ada-002と比較して、多言語検索ベンチマークで31.4%から54.9%へ、英語タスクで61.0%から64.6%へと平均スコアを向上させています。text-embedding-3-largeの価格は1kトークンあたり$0.00013と設定されています。

https://openai.com/blog/new-embedding-models-and-api-updates
上記URLのページの一部をChatGTPで翻訳して引用しています。

OpenAIの最新のエンベディングモデルを使って、CSVデータをエンベディングして検索してみた

ということで、今回は小さい方のモデル「text-embedding-3-small」を使ってみることにしました。
参考にしたのは、OpenAI公式のサンプルコード集「openai-cookbook」にある次の2つのプログラムです。
これらをGoogle Colab上で動かすことで、エンベディングベースの検索がどんな使用感になるか試してみることにしました。

1つ目のサンプルコードはこちら。

・Question answering using embeddings-based search
(エンベディングベースの検索を使用した質問応答)

https://github.com/openai/openai-cookbook/blob/main/examples/Question_answering_using_embeddings.ipynb

このノートブックでは、GPT が参照テキストのライブラリを使用して質問に回答できるようにするための 2 段階の Search-Ask メソッドを示します。
このサンプル ノートブックでは、エンベディングベースの検索を使用します。 エンベディングは実装が簡単で、質問はその回答と語彙的に重複しないことが多いため、特に質問でうまく機能します。
エンベディングベースの検索にあたって、エンベディングされたデータが必要なためそのデータを作るために「Embedding Wikipedia articles for search」のサンプルコードでエンベディングデータを作成します。

上記URLのGitHubのページの一部をChatGTPで翻訳して引用しています。

2つ目のサンプルコードはこちら。

・Embedding Wikipedia articles for search
(検索のためのウィキペディア記事のエンベディング)

https://github.com/openai/openai-cookbook/blob/main/examples/Embedding_Wikipedia_articles_for_search.ipynb

このノートブックは、「Question_answering_using_embeddings.ipynb」で使用される検索用のウィキペディア記事のデータセットを準備するためのコードです。
「Question answering using embeddings-based search」はエンベディングされたデータ を検索して回答を出すプログラムになります。「Embedding Wikipedia articles for search」はウィキペディアの記事からエンベディングされたデータを作ると言うものになります。

上記URLのGitHubのページの一部をChatGTPで翻訳して引用しています。

ここで、プログラムが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')))

※ シークレットについて
Google Colabでは、機密情報(APIキーなど)を安全に管理するためにシークレット機能を使用できます。

シークレットの追加方法
1. 左側のサイドバーにある「鍵」アイコンをクリックします。
2. 「新しいシークレットの追加」ボタンをクリックし、シークレットのキーと値を入力します。

シークレットの使用方法
シークレットを使用するには、以下のPythonコードをノートブックに記述します。
# google colabの環境変数を取得
from google.colab
import userdata userdata.get('OPENAI_API_KEY')

1-2. CSVデータをエンベディング

CSVの1列目(項目名:textとする)の全行をエンベディング処理します。
実行するとCSV形式のファイルのアップロードインタフェースが動きます。
アップロードされたファイルを読み込み、Pandas(データ分析ライブラリ)を使用してDataFrame(表形式のデータ構造)に変換します。

CSVファイルには、夏目漱石の『吾輩は猫である』を1パラグラフごとに1行ずつ改行したテキストデータを用意しました。

『吾輩は猫である』を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!エンジニア募集中!▰

▰当社代表取締役が創業時のこと、会社への想いを綴っています▰


この記事が参加している募集

企業のnote

with note pro

AIとやってみた

この記事が気に入ったらサポートをしてみませんか?