見出し画像

OpenAIの新鮮な埋め込みモデルとRAG: シンプルな実装で大きな可能性を探る

こんにちは、makokonです。
最近OpenAIが発表した新しいモデルとAPIの更新、聞いていますか?
多くの人が既にこの話題を取り上げていますが、私も試してみました。
特に、新しいEmbeddingモデルとGPT-4 Turbo Previewが注目点です。
では、さっそくRAGを作って新しいモデルの可能性を探ってみましょう。
なぜ、RAGなのかですか?やっぱりいちばん可能性がわかりやすいですよね。






新しいEmbeddingモデルの紹介

興味深い2つのモデルが追加されています。

小規模テキスト埋め込みモデルの特徴

text-embedding-3-small とても効率的で低コストで使えるようですよ。

text-embedding-3-smallは、当社の新しい高効率組み込みモデルであり、2022 年 12 月text-embedding-ada-002にリリースされた前モデルに比べて大幅なアップグレードが提供されます。
text-embedding-ada-002と比較するとtext-embedding-3-small、多言語検索で一般的に使用されるベンチマーク ( MIRACL ) の平均スコアは 31.4% から 44.0% に増加し、英語タスクで一般的に使用されるベンチマーク ( MTEB ) の平均スコアは 61.0% から 62.3 に増加しています。
text-embedding-3-smallの価格は、1,000 トークンあたりの価格は 0.0001 ドルから 0.00002 ドルになりました。

https://openai.com/blog/new-embedding-models-and-api-updates

大規模テキスト埋め込みモデルの特徴

text-embedding-3-large こっちはとても強力なパフォーマンスを提供してくれるようです。


text-embedding-3-largeは、新しい次世代のより大きなエンベディング モデルであり、最大 3072 次元のエンベディングを作成します。
より強力なパフォーマンス。text-embedding-3-largeは当社の新しい最高性能モデルです。比較するtext-embedding-ada-002とtext-embedding-3-large、MIRACL では平均スコアが 31.4% から 54.9% に増加し、MTEB では平均スコアが 61.0% から 64.6% に増加しました。

text-embedding-3-large価格は $0.00013 / 1,000 トークンです。

https://openai.com/blog/new-embedding-models-and-api-updates

GPT-4 Turbo Preview

GPT4モデルもGPT-4 Turbo Previewがリリースされます。タスクを最後まで完了しない怠け癖を直してくれたようです。今回のプログラムでは違いを実感することはできないでしょう。

本日、更新された GPT-4 Turbo プレビュー モデルをリリースしますgpt-4-0125-preview。このモデルは、コード生成などのタスクを以前のプレビュー モデルよりも徹底的に完了し、モデルがタスクを完了しない「怠惰」のケースを減らすことを目的としています。新しいモデルには、英語以外の UTF-8 世代に影響を与えるバグの修正も含まれています。
新しい GPT-4 Turbo プレビュー バージョンに自動的にアップグレードしたい場合は、gpt-4-turbo-preview常に最新の GPT-4 Turbo プレビュー モデルを指す新しいモデル名のエイリアスも導入します。
今後数か月以内に GPT-4 Turbo を一般公開する予定です。

https://openai.com/blog/new-embedding-models-and-api-updates

使い方はこちらを参考に、https://platform.openai.com/docs/introduction

検索拡張生成(RAG)とは

これは、今更なので簡単に。LLMを使って、”2023年のプロ野球優勝チームは?”とか聞いても、”そんな新しいことは知りません”とか言われたりしますよね。または、組織内のデータとか特別に専門性が高いデータが必要な質問も正しく答えてくれない可能性が高いです。(しれっともっともらしく教えてくれることもあるけど信用できない)
そんなときに、必要な事前情報をプロンプトと一緒に渡して、質問すれば正しいことを教えてくれるだろうということです。この事前情報を用意したり効率的に検索するための仕組みがRAGで、これにEmbeddingモデルを利用すると便利なんですよ。

今回作成するプログラムの概要

今回作成するプログラムについて説明します。
RAGを説明するときに、例題として何を利用するかは、結構悩んでしまいますが、今回は百人一首を取り扱うことにしました。馴染もそれなりにあって、どんな事前情報を利用して回答に至ったかをなんとなくわかりますからね。
したがって、歌番号、作者、元の歌、歌の現代語訳を含むデータベースを作って、そのデータベースに基づいて百人一首について答えてもらいます。
なお、現代語訳とかは下記リンクを利用させてもらいました。

必要なライブラリの紹介

とりあえず、以下のライブラリはアップデートしておきます。今回はlangchainはテキストスプリッター以外使っていないのですが、今後拡張する予定があるので、新しくするほうがいいでしょう。

pip install --upgrade openai
pip install --upgrade langchain
pip install --upgrade langchain-community
pip install --upgrade langchain-openai
from openai import OpenAI
import openai #from  openai.embeddings_utils import get_embedding, cosine_similarity # 現在のバージョンでは利用できない
from langchain.text_splitter import CharacterTextSplitter
import numpy as np
import os
import json
import datetime

#from openai.embeddings_utils import get_embedding, cosine_similarity # 現在のバージョンでは利用できない
埋め込みの生成と、類似度の計算はもともと以前のOpenAIライブラリにする組まれていたのですが、今回はなぜかエラーが出て使えません。このへんのサポートを辞める理由はないので、原因不明ですが、仕方ないので今回は同じ名前の関数を作ることにします。(多分すぐにでもアップデートで復活すると信じてます)

getembedding(),cosine_similarity()の実装です。
getembedding()は公式にも乗っていましたが、引数textが[text]だと、うまく動きませんでした。
cosine_similarity()はnumpyを使って、最も単純なものを作りました。効率的にどうかと思いましたが、全然速度的には気になりませんでした。

openai.api_key = os.getenv("OPENAI_API_KEY").strip()

client = OpenAI()

def cosine_similarity(x, y):
    # ベクトルxとyのドット積を計算
    dot_product = np.dot(x, y)
    # ベクトルxとyのノルム(長さ)を計算
    norm_x = np.linalg.norm(x)
    norm_y = np.linalg.norm(y)
    # コサイン類似度を計算
    similarity = dot_product / (norm_x * norm_y)
    return similarity



def get_embedding(text, model="text-embedding-3-small"):
   text = text.replace("\n", " ")
   return client.embeddings.create(input = text, model=model).data[0].embedding

# 

その他の関数の説明

残りの関数定義を示します。


def text_split(text):
    max_tokens=500
    
    # 長文対応
    text_splitter = CharacterTextSplitter(
        separator = "\n\n",  # セパレータ
        chunk_size = max_tokens,  # チャンクの文字数
        chunk_overlap = 00,  # チャンクオーバーラップの文字数
    )
    document_splits = text_splitter.split_text(text)
    return document_splits



def search_reviews(data, query, n=3, pprint=True):
    embedding = get_embedding(query, model='text-embedding-3-small')
    for i in data:
        i["similarities"]=cosine_similarity(i["embedding"],embedding)
 
    # 'similarities'の値で降順に並べ替え
    sorted_data = sorted(data, key=lambda x: x['similarities'], reverse=True)
    
    # 最も類似度が高いn個の辞書を取得
    top_n = sorted_data[0:n]
    
    # 上からn個の辞書を返す
    return top_n



# generative text
def get_gpt(data,query,model="gpt-4-turbo-preview"):
    # model gpt-4、、gpt-4-turbo-preview,gpt-3.5-turbo gpt-4-vision-preview
    system_prompt="与えられた事前情報をもとに、質問に答えてください。適切な事前情報がない場合は、推定および一般的な回答を述べてください"
    jizen="\n".join(item["text"] for item in data)
    prompt=f"事前情報:{jizen}\n質問:{query}"

    response = client.chat.completions.create(
    model=model,
    messages=[
        {"role": "system","content":system_prompt},
        {"role": "user", "content":prompt},
        #{"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
        #{"role": "user", "content": "Where was it played?"}
    ]
    )
    #print (response)
    #print (type(response))
    #print (dir(response))
    return response.choices[0].message.content

text_split(text):
与えられたテキストを適当なサイズに分館します。セパレータとmaxchunkで決められたサイズになります。適当な粒度と一貫性を保つように決めるのですが、今回は百人一首がテーマなので2,3首情報が含まれるようにすればいいかなと思いました。
search_reviews(data, query, n=3, pprint=True):
dataのリストに含まれる情報からqueryと関連の深い情報を指定した数だけピックアップします。デフォルトは3個です。処理の流れは、

  • queryをembeddedデータに変換する。

  • dataに含まれるすべてのebmeddedデータとqueryとの類似度をすべて計算して、辞書に追加する。

  • 類似度をキーとしてソートして、上からN個のリストを返します。

get_gpt(data,query,model="gpt-4-turbo-preview"):
N個の類似度の高いデータに基づいてGPTからの応答を取得します。
モデルはgpt-4-turbo-previewを選びました。(新しいのを試したかったから)別に、pt-4、gpt-4-turbo-preview,gpt-3.5-turbo gpt-4-vision-previewあたりを選んでも問題なく動く(はずはずはず)でしょう。
事前情報に基づいて答えてもらいますが、普通のチャットとしても機能してほしいので、プロンプトでは、事前情報が不十分な場合は、推測や、一般的な質問としての回答も要求しています。目的によっては、「情報が不十分な場合はわからないと答え、決して推測では答えないでください」などと書くかもしれません。
与えられたdataから本のテキストをすべて接続して、事前情報の文字列にします。LLMによってはembeddedデータをそのまま使ったりするほうが便利なこともありますが、今回は、元データをそのまま使います。
個の文字列もプロンプトに組み込みます。
あとは、
response = client.chat.completions.create()を呼び出すだけです。
#print (response)
#print (type(response))
#print (dir(response))
return response.choices[0].message.content
ところで、今回応答テキストを単独で返すのでresponse.choices[0].message.content の形ですが、この書き方はいっつもわからなくて、悩むことになります。[messge][contentとかかもしれないですしね。type(response),dir(resonse)を確認すればわかりますが、それでも、確実性がないですが、上記情報を渡してChatGPTに確認して(教えて)もらっています。ChatGPT便利

これで、必要な関数は揃いました。

全コードの公開と説明

全コードを示します。



from openai import OpenAI
import openai #from  openai.embeddings_utils import get_embedding, cosine_similarity # 現在のバージョンでは利用できない
from langchain.text_splitter import CharacterTextSplitter
import numpy as np
import os
import json
import datetime

openai.api_key = os.getenv("OPENAI_API_KEY").strip()

client = OpenAI()

def cosine_similarity(x, y):
    # ベクトルxとyのドット積を計算
    dot_product = np.dot(x, y)
    # ベクトルxとyのノルム(長さ)を計算
    norm_x = np.linalg.norm(x)
    norm_y = np.linalg.norm(y)
    # コサイン類似度を計算
    similarity = dot_product / (norm_x * norm_y)
    return similarity



def get_embedding(text, model="text-embedding-3-small"):
   text = text.replace("\n", " ")
   return client.embeddings.create(input = text, model=model).data[0].embedding

# 


def text_split(text):
    max_tokens=500
    
    # 長文対応
    text_splitter = CharacterTextSplitter(
        separator = "\n\n",  # セパレータ
        chunk_size = max_tokens,  # チャンクの文字数
        chunk_overlap = 00,  # チャンクオーバーラップの文字数
    )
    document_splits = text_splitter.split_text(text)
    return document_splits



def search_reviews(data, query, n=3, pprint=True):
    embedding = get_embedding(query, model='text-embedding-3-small')
    for i in data:
        i["similarities"]=cosine_similarity(i["embedding"],embedding)
 
    # 'similarities'の値で降順に並べ替え
    sorted_data = sorted(data, key=lambda x: x['similarities'], reverse=True)
    
    # 最も類似度が高いn個の辞書を取得
    top_n = sorted_data[0:n]
    
       # 上からn個の辞書を返す
    return top_n


# generative text
def get_gpt(data,query,model="gpt-4-turbo-preview"):
    # model gpt-4、、gpt-4-turbo-preview,gpt-3.5-turbo gpt-4-vision-preview
    system_prompt="与えられた事前情報をもとに、質問に答えてください。適切な事前情報がない場合は、推定および一般的な回答を述べてください"
    jizen="\n".join(item["text"] for item in data)
    prompt=f"事前情報:{jizen}\n質問:{query}"

    response = client.chat.completions.create(
    model=model,
    messages=[
        {"role": "system","content":system_prompt},
        {"role": "user", "content":prompt},
        #{"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
        #{"role": "user", "content": "Where was it played?"}
    ]
    )
    #print (response)
    #print (type(response))
    #print (dir(response))
    return response.choices[0].message.content

# main0
# make original data


if os.path.exists('index.json'):
    print('json File exists.')
else:
    print('json File does not exist.')

    # ファイルを読み込む
    filename='hyakunin.txt'
    with open(filename, 'r') as file:
        doc = file.read().replace('\u3000', '  ') # 全角スペース嫌い
        lines = text_split(doc) # 元データに合わせてセパレーターを確認しよう

    # 各テキスト行から埋め込みを生成
    index = [{"text": line, "embedding": get_embedding(line)} for line in lines]
    #index  = lines

    # indexをJSONファイルに書き出す
    with open('index.json', 'w') as f:
        json.dump(index, f)

# JSONファイルからindexを読み込む
with open('index.json', 'r') as f:
    index = json.load(f)

print("事前情報数 : ",len(index))


# main1

chat_history=[]
while True:
    query=input('百人一首について質問してください。 : ')
    if query == "exit":
        break
        
    res = search_reviews(index, query, n=5)
    """ #debug 
    for i in res:
        print(i["text"],i["similarities"])
    """

    ai = get_gpt(res,query,model="gpt-4-turbo-preview")
    chat_history.append(f"user : {query}")
    chat_history.append(f"ai : {ai}")
    print(ai)

# save history
filename= "rag-chatlog"+datetime.datetime.now().strftime("%Y%m%d-%H%M%S")+".txt"
with open(filename,'w',encoding='utf-8') as f:
    for chat in chat_history:
        f.write(chat)
        print(chat)

exit()


#main0

以下を説明しますね。
まず、オリジナルデータからembeddedデータを含む辞書データを作ります。ファイルからテキストをすべて読みこんで、適当に分断して、リストにします。そのリストをすべてembeddedして、辞書データindexに変換します。
index = [{"text": line, "embedding": get_embedding(line)} for line in lines]

なお、元でたーのテキストはこんな感じ。\n\nで一首のデータを区切っているのでこれを2,3首含めば、十分な粒度と補足的な情報が得られるでしょう。もちろん文学じゃなくて医療データや販売商品データなら1件ずつ完全に分けるようにしたほうがいいかもしれませんし、論文要約なら1ページとか1段落を単位としてあまり短くしないほうがいいでしょう。

(001) 天智天皇 
原文
秋の田の かりほの庵の 苫をあらみ わが衣手は 露にぬれつつ
(あきのたの かりほのいほの とまをあらみ わがころもでは つゆにぬれつつ)
現代訳
秋の田の側につくった仮小屋に泊まってみると、屋根をふいた苫の目があらいので、その隙間から忍びこむ冷たい夜露が、私の着物の袖をすっかりと濡らしてしまっているなぁ。

(002) 持統天皇
原文
春すぎて 夏来にけらし 白妙の 衣ほすてふ 天の香具山
(はるすぎて なつきにけらし しろたへの ころもほすてふ あまのかぐやま)
現代訳
もう春は過ぎ去り、いつのまにか夏が来てしまったようですね。香具山には、あんなにたくさんのまっ白な着物が干されているのですから。

出来上がったindexは、json.dumpを使って保存しておきます。毎回変換すると時間もお金もかかりますからね。
次回に実行したときに、index.jsonが存在したら、これを読み出すだけでOKなので楽ちんです。

# main1

ここは基本的に無限ループです。
ユーザーの質問をqueryに入力し、入力がexitならループを終了します。
ループ本体では、
res = search_reviews(index, query, n=5)
queryと関連の深いデータを5つピックアップして、
get_gpt(res,query,model="gpt-4-turbo-preview")
を呼び出します。

実行結果の解析

ザクッと
「秋の歌を紹介してください」と入れてみると、4種ほど紹介してくれました。まあ、結果には満足です。「秋」の言葉が直接入っていないのに関連性を抽出できているのかな。恐るべし。

user : 秋の歌を紹介してください
ai : 秋をテーマにした和歌は、万葉集から古今和歌集など、日本古来から数多く詠まれています。寂寥感や移ろいやすい自然の美しさを象徴する秋は、和歌にとって重要なテーマの一つです。ここでは事前情報より、秋に関連するいくつかの歌を紹介します。

(021) 素性法師
原文:今来むと いひしばかりに 長月の 有明の月を 待ち出でつるかな
現代訳:「今すぐに行きましょう」とあなたがおっしゃったので、(その言葉を信じて)九月の長い夜を待っていましたが、とうとう有明の月が出る頃を迎えてしまいました。

(022) 文屋康秀
原文:吹くからに 秋の草木の しをるれば むべ山風を あらしといふらむ
現代訳:山風が吹きおろしてくると、たちまち秋の草や木が萎れてしまうので、きっと山風のことを「嵐」というのだろう。

(069) 能因法師
原文:嵐吹く 三室の山の もみぢ葉は 竜田の川の 錦なりけり
現代訳:嵐が吹き散らした三室の山の紅葉の葉が、龍田川に一面に散っているが、まるで錦の織物のように美しいではないか。

(005) 猿丸大夫
原文:奥山に 紅葉踏み分け 鳴く鹿の 声聞くときぞ 秋は悲しき
現代訳:奥深い山の中で、(一面に散りしいた)紅葉を踏み分けて鳴いている鹿の声を聞くときは、この秋の寂しさが、いっそう悲しく感じられることだ。

これらの和歌では、秋の景色や感情が詩的に表現されています。秋の草木のしおれや立つ山風の強さ、紅葉の美しさや、それに寄り添う動物の声など、季節の移ろいとその中で感じられる人の情感が繊細に描かれています。

実行結果

まとめと今後の展望

今回はOpenAIの新しいEmbedding modelとGPT-4 Turbo Previewを用いて、シンプルながらも実用的なRAGを実装しました。既存のライブラリを使わずに、自分で埋め込み、類似度計算、関連性抽出を行い、期待通りの検索結果を得ることができました。今後、このモデルを用いてさらに実用的なRAGシステムの開発に取り組んでいきます。

今後は、マルチモーダルRAGは一応実装しておきたいですね。
動画の内容を説明してくれるRAGとかを紹介するかもしれません。

おまけ タイトル画の説明 by GPT-4V

この画像は拡大鏡を持ったキャラクターが、何かを一生懸命に探しているような表情で、大きな辞書を見ています。キャラクターの目は大きく驚いた様子で、顔は驚きや焦りを表しているかのようです。両サイドには積み重ねられた書籍があり、カップのコーヒーもテーブルの上に置かれています。文字のブロックが辞書から飛び出し、動きや活気を感じさせる演出がなされています。イラストは、研究や深い学び、あるいは情報の探求を表していると解釈できます。

ハッシュタグ:
#OpenAI #EmbeddingModel #GPT4TurboPreview #RAG #MachineLearning #AI #DeepLearning #PythonProgramming

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