本記事は日本オラクルが運営する下記Meetupで発表予定の内容になります。発表までに今後、内容は予告なく変更される可能性があることをあらかじめご了承ください。下記セッションでは、本記事の内容以外にデモンストレーションも実施する予定です。
はじめに
この記事をご覧の皆様の中には、ChatGPTをきっかけに大規模言語モデル(LLM)の存在を知った方も多いのではないかなと思います。そのような方にとっては、「大規模言語モデルと言えばGPT」となりがちで、その他のLLMプロバイダーが提供するLLMを検討してみようというモチベーションが二の次になっていることもあるのではないでしょうか。
Open AIはビジネス上では間違いなくこのマーケットでのパイオニアかつリーダーです。それゆえに、最大の知名度を誇る同社のLLMが最高の精度をもたらすものだと思われがちですが、企業の知名度とその製品がもたらす技術的メリットが比例しない例はよくあります。意外ともっと安価な、場合によってはオープンソースのLLMでも十分に使えるのでは?という可能性は常に模索していたいものですよね。
性能面では、LLMのベンチマーク結果は様々なサイトで公開されているものの、日本語コーパスでの結果がなかったり、目的のLLMとの比較がなかったりと、結局どのLLMを使えばよいかを検討する情報がなかなか得られないということも事実だと思います。
したがってベンチマーク結果掲載サイトだけに頼るのではなく、LLMのベンチマークを自分で実施できるようにしておけば何かと心強いのではないかと思い本記事を紹介させていただきます。
細かい話は興味ないよ、結果だけ知りたいよという方はこちら。
比較対象のLLM
今回はこちらの3つのLLMのEmbeddingの性能をベンチマークして比較してみたいと思います。Open AIは言わずと知れた最も知名度の高いモデル、2つ目はOpen AIと肩を並べる有料のLLMであるCohere、3つ目はオープンソースである無償のE5です。
知名度もトークン単価も異なるこの3つのLLMのEmbedモデルを使って、文章の類似度を算出するベンチマークを実行し、これらのモデルの性能を比較してみます。
細かい話は興味ないよ、結果だけ知りたいよという方はこちら。
OpenAI(text-embed-ada-002)
Open AI社が提供しているEmbeddingのモデル。text-embed-ada-002は価格と性能のバランスがいいとされており、Open AI社自体が推奨するモデル。今回は実施しませんが、テキストデータだけでなく画像データもEmbeddingできる優れものです。
モデルの仕様
https://platform.openai.com/docs/guides/embeddings
Cohere(embed-multilingual-v2.0, embed-english-v2.0)
同市場が拡大傾向に入った早い段階で、多数のベンチャーキャピタルや大手テック企業から膨大な額の資金調達に成功したスタートアップ企業。知名度の高いLLMの多くは基本、舶来品。我々日本人は当然 multilingual版を使うことになりますが、日本語使用時の、英語版モデルと多言語版モデルの性能差はどの程度なのか、参考までにチェックしてみようと思います。
モデルの仕様
https://docs.cohere.com/docs/models
E5(multilingual-e5-large)
Liang Wang, Nan Yang, Xiaolong Huang, Binxing Jiao, Linjun Yang, Daxin Jiang, Rangan Majumder, Furu Weiらによって開発されたオープンソースの多言語対応Embeddingモデル。E5とはEmbEddings from bidirEctional Encoder rEpresentationsの略称で、その名の通りgoogleのオープンソースLLMであるBERT(厳密にはBERT改良版のRoBERTa)を使ったEmbeddingモデル。
モデルの仕様
https://huggingface.co/intfloat/multilingual-e5-large
E5の論文
https://arxiv.org/abs/2212.03533
ベンチマークに使用するデータセット
今回はJGLUE)と呼ばれる日本語データセットを使います。こちらヤフー株式会社と早稲田大学川原研究室の共同プロジェクトで構築されたデータセットです。
もともとは英語圏でよく使われるGLUE:General Language Understanding Evaluationというデータセットがあり、その名前の頭にJapaneseのJが付いたものですが、このJGLUEはもちろんGLUEのデータセットを単純に和訳したものではなく、ゼロベースで構築されたものになります。単純に和訳しただけだと肝心の正解ラベルの値が参考になりませんからね。
このような地道な作業の成果物を無償で公開して下さい企業や研究機関には本当に頭が下がります。
ベンチマークの流れ
まずデータセットの構造を確認しましょう。本データセットでは、sentence1とsentence2の列があり、それぞれ1500行程度の文章から構成されています。そしてこの2つの文章の類似度を予め評価し、0から5までの範囲で算出した値が正解ラベルとしてlabelの列に記録されています。
例文をみるとなんとなくわかると思いますが、大きい値ほど、2つの文章の類似度が高いということになります。いわゆる文ペア分類(2つの文章の類似度を評価する)のベンチマークです。
今回のベンチマークでは候補となる3つのLLMのEmbedモデルを使い、上記2つの文章の類似度を計測するためのベクトルを生成します。類似度が高いモデルはベクトル生成の精度が高く、秀逸なモデルということになります。
ベンチマークの流れとしてはシンプルに以下3ステップです。
- step 1 : データセット内の文章データをベクトルデータに変換
- step 2 : データセットのsentence1とsentence2の類似度を算出
- step 3 : その計算結結果と、正解ラベルの相関係数を算出しモデルの精度とする
これら3ステップを各LLM毎で実行し、最後の相関係数を比較してみるという、極めてシンプルな方法でEmbedモデルの性能を確認してみようと思います。以下、この3ステップの簡単な説明です。
ステップ1 : データセットのベクトル化
LLMのEmbedモデルを使って各文章をベクトル化したデータフレームを作ります。実は勝負はこの時点で既に決まっています。
ステップ2以降の工程は作成したベクトルがいかに精度の高いものかを数値化し比較するための確認作業です。
ステップ2 : ベクトルの類似度を算出
sentence1とsentence2のベクトルの類似度をコサイン類似度によって算出します。
コサイン類似度はベクトルの類似度計算ではよく用いられる算出方法で、現在世に出回っているほとんどのベクトルデータベースのセマンティック検索ではこの手法が採用されています。
2つのベクトル(この場合、sentence1とsentence2)の内積を、そのベクトルの大きさで割り算しているだけのシンプルな計算です。ざっくり、2つのベクトルの内角を計算していると思えばよいかと。内角が小さいほど同じ向きを示すベクトル、つまり類似したベクトルということです。
例えば、「東京」という単語と「日本」という単語の類似度を計算する場合は下図のように、二つのベクトルの内角と距離を計算してその類似度を測っています。
そこ詳しく!という方はこちら
ステップ3 : ベクトルの類似度からモデルの精度を算出
ターゲットのLLMを使った類似度が算出できたので、この算出した値が元のデータセットの正解ラベルの値にどれくらい近いかを計算します。これはこの2つの値の相関係数を計算して値が高ければ、正解ラベルに近いということで精度が高いモデルということになります。
ご存じの方も多いと思いますが念のため。相関係数はピアソン相関係数とスピアマン相関係数の2つがよく使われます。
元のデータがパラメトリック(正規分布)の場合はピアソンを、ノンパラメトリック(特定の分布になっていない)の場合はスピアマンの値を使う、というのがざっくりとした使い分けです。ノンパラメトリックではスピアマン以外に、カイ二乗検定や、U検定など有名な検定方法があります。
因みに、「相関係数」とだけ記載されている場合は、ピアソンのほうを意味していると判断するのが一般的です。世の中の大抵のデータ分布は正規分布に沿っているケースが多いですからね。
パラメトリックかノンパラメトリックかはチャートを作れば一発でわかりますが、今回は本題から外れますし無駄に記事が長くなるので割愛して、とりあえず、両方の相関係数を比較してみます。
そもそも相関係数って何?という方はこちら。
どんな分布にも使えるならなにも考えずにノンパラメトリックの手法を使っておけばいいじゃないかと思ってしまいがちですが、統計ではこういうオールマイティな手法は往々にして精度が低くなる傾向があります。常にデータの分布の把握は必須ということですね。
コード概説 - Open AI
各ステップが理解できたところでサンプルコードを見ていきましょう。ここまでくればこの程度のサンプルコードの把握は簡単です。まずはOpen AIのコードから。
Step 1 : データセットのベクトル化
# データセットのロードに必要なライブラリをimport
import json
import pandas as pd
from urllib.request import urlopen
# データセットをロードしデータフレームを作成
dataset_url = "https://raw.githubusercontent.com/yahoojapan/JGLUE/main/datasets/jsts-v1.1/valid-v1.1.json"
df = pd.DataFrame([json.loads(line) for line in urlopen(dataset_url).readlines()])
#データフレームの中身を確認
df
# データセットのEmbeddingに必要なライブラリをimport
from langchain.embeddings import OpenAIEmbeddings
from tqdm import tqdm
import torch
import os
import concurrent.futures
import getpass
# Open AIのAPIキーをセット
API_KEY = getpass.getpass("APIKEY: ")
os.environ["OPENAI_API_KEY"] = API_KEY
# Open AIのEmbeddingモデルを定義
embeddings = OpenAIEmbeddings(model = 'text-embedding-ada-002')
# 定義したモデルの確認
embeddings
# データセットからsentence1とsentence2を抜き出して連結
set_sentence = set(df["sentence1"]).union(set(df["sentence2"]))
print(len(set_sentence))
# データセットをembedding(concurrent.futuresで並列処理)
dict_sentence = {}
with concurrent.futures.ThreadPoolExecutor() as executor:
# set_sentenceの各要素に対して、embed_query関数を並行に適用
future_to_sentence = {
executor.submit(embeddings.embed_query, sentence): sentence
for sentence in set_sentence
}
for future in concurrent.futures.as_completed(future_to_sentence):
sentence = future_to_sentence[future]
try:
dict_sentence[sentence] = future.result()
except Exception as exc:
print("%r generated an exception: %s" % (sentence, exc))
# ベクトル化したデータセット(ディクショナリ)を確認
print(len(dict_sentence))
Step 2 : ベクトルの類似度を算出
# コサイン類似度を使うためcosine_similarityをimport
from torch.nn.functional import cosine_similarity
# コサイン類似度を算出し配列に入力
similarities = []
for i, row in tqdm(df.iterrows()):
embed_sentence1 = dict_sentence[row["sentence1"]]
embed_sentence2 = dict_sentence[row["sentence2"]]
similarity = cosine_similarity(
torch.tensor(embed_sentence1).unsqueeze(0), torch.tensor(embed_sentence2).unsqueeze(0)
)
similarities.append(similarity.item())
Step 3 正解ラベルと比較し精度を算出
from scipy.stats import pearsonr, spearmanr
# ピアソン相関係数の算出
pearson_corr, _ = pearsonr(similarities, df["label"])
print(f'Pearson correlation: {pearson_corr}')
# スピアマン相関係数の算出
spearman_corr, _ = spearmanr(similarities, df["label"])
print(f'Spearman correlation: {spearman_corr}')
出力結果は以下の通り。
Pearson correlation: 0.837127181371111
Spearman correlation: 0.7900239562506536
コード概説 - Chere
基本的にCohereのコードもOpen AIと同じでLLMに関する部分をCohereに変えるだけです。ですが、Cohereのトライアルアカウントでは並列処理時に毎分の同時実行数に制限がありStep 2のEmbeddingのコードは並列度を落として計算しています。
Step 1 : データセットのベクトル化
# データセットのロードに必要なライブラリをimport
import json
import pandas as pd
from urllib.request import urlopen
# データセットをロードしデータフレームを作成
dataset_url = "https://raw.githubusercontent.com/yahoojapan/JGLUE/main/datasets/jsts-v1.1/valid-v1.1.json"
df = pd.DataFrame([json.loads(line) for line in urlopen(dataset_url).readlines()])
#データフレームの中身を確認
df
# データセットのEmbeddingに必要なライブラリをimport
from tqdm import tqdm
from langchain.embeddings import CohereEmbeddings
import torch
import os
import concurrent.futures
import getpass
# CohereのAPIキーをセット
APIKEY = getpass.getpass("APIKEY: ")
os.environ["COHERE_API_KEY"] = APIKEY
# CohereのEmbeddingモデルを定義
# マルチリンガル版は embed-multilingual-v2.0
# 英語版は embed-enblish-v2.0
embeddings = CohereEmbeddings(model='embed-multilingual-v2.0')
# 定義したモデルの確認
embeddings
# データセットからsentence1とsentence2を抜き出して連結
set_sentence = set(df["sentence1"]).union(set(df["sentence2"]))
print(len(set_sentence))
# 1分間のインターバルを置きながら90並列を繰り返す
import time
dict_sentence = {}
count = 0
for sentence in set_sentence:
try:
result = embeddings.embed_query(sentence)
dict_sentence[sentence] = result
except Exception as exc:
print("%r generated an exception: %s" % (sentence, exc))
count += 1
if count % 90 == 0:
print("Processed 90 lines. Sleeping for 1 minute...")
time.sleep(60)
print(len(dict_sentence))
Step 2 : ベクトルの類似度を算出
# コサイン類似度を使うためcosine_similarityをimport
from torch.nn.functional import cosine_similarity
# コサイン類似度を算出し配列に入力
similarities = []
for i, row in tqdm(df.iterrows()):
embed_sentence1 = dict_sentence[row["sentence1"]]
embed_sentence2 = dict_sentence[row["sentence2"]]
similarity = cosine_similarity(
torch.tensor(embed_sentence1).unsqueeze(0), torch.tensor(embed_sentence2).unsqueeze(0)
)
similarities.append(similarity.item())
Step 3 正解ラベルと比較し精度を算出
from scipy.stats import pearsonr, spearmanr
# ピアソン相関係数の算出
pearson_corr, _ = pearsonr(similarities, df["label"])
print(f'Pearson correlation: {pearson_corr}')
# スピアマン相関係数の算出
spearman_corr, _ = spearmanr(similarities, df["label"])
print(f'Spearman correlation: {spearman_corr}')
出力は以下の通り。
embed-multilingual-v2.0の場合
Pearson correlation: 0.8331382888158348
Spearman correlation: 0.789978803403507
embed-english-v2.0の場合
Pearson correlation: 0.7194434049334675
Spearman correlation: 0.7046147134313049
コード概説 - E5
E5のコードはこれまでとだいぶ変わります。LangChainがE5をサポートしていないので、下記huggingfaceのコードを参考に、tensorを使って入力トークンの細かい処理は自分で書く必要があります。
Step 1 : データセットのベクトル化
# データセットをロードしデータフレームを作成
dataset_url = "https://raw.githubusercontent.com/yahoojapan/JGLUE/main/datasets/jsts-v1.1/valid-v1.1.json"
df = pd.DataFrame([json.loads(line) for line in urlopen(dataset_url).readlines()])
#データフレームの中身を確認
df
Embedding処理をするためのaverage_poolという関数を定義しています。この関数は、文全体の埋め込み表現を取得し、各文のトークンの数に関する情報を使用して、文全体の平均埋め込みを計算しています。
from transformers import AutoTokenizer, AutoModel
from torch import Tensor
import pandas as pd
from torch.nn.functional import cosine_similarity
tokenizer = AutoTokenizer.from_pretrained("intfloat/multilingual-e5-large")
model = AutoModel.from_pretrained("intfloat/multilingual-e5-large")
def average_pool(last_hidden_states: Tensor, attention_mask: Tensor) -> Tensor:
last_hidden = last_hidden_states.masked_fill(~attention_mask[..., None].bool(), 0.0)
return last_hidden.sum(dim=1) / attention_mask.sum(dim=1)[..., None]
定義したaverage_poolを使って実際にEmbeddingを行います。
from tqdm import tqdm
# Embedingの処理を実行
embeddings_list = []
for i, row in tqdm(df.iterrows()):
input_texts = [row["sentence1"], row["sentence2"]]
# Tokenize and compute embeddings
batch_dict = tokenizer(
input_texts, max_length=512, padding=True, truncation=True, return_tensors="pt"
)
outputs = model(**batch_dict)
embeddings = average_pool(outputs.last_hidden_state, batch_dict["attention_mask"])
embeddings_list.append(embeddings)
Step 2 : ベクトルの類似度を算出
# cosine_similarityの処理
similarities = []
for emb_pair in embeddings_list:
similarity = cosine_similarity(emb_pair[0].unsqueeze(0), emb_pair[1].unsqueeze(0))
similarities.append(similarity.item())
Step 3 正解ラベルと比較し精度を算出
from scipy.stats import pearsonr, spearmanr
# ピアソン相関係数の算出
pearson_corr, _ = pearsonr(similarities, df["label"])
print(f'Pearson correlation: {pearson_corr}')
# スピアマン相関係数の算出
spearman_corr, _ = spearmanr(similarities, df["label"])
print(f'Spearman correlation: {spearman_corr}')
出力は以下の通り。
Pearson correlation: 0.850360228077024
Spearman correlation: 0.8098724271716128
結果
結果をチャートにすると下図のようになります。
まず、ピアソン、スピアマンともに、3つのLLMで大差はなかったということが分かりました。が、これは言い換えると、オープンソースのE5がトークン単価の高いOpen AIと肩を並べていることになり、少し驚きです。おそらく多くの方がご存じないであろうCohereもOpen AIと大差ないことがわかります。
そして興味本位で実施した、Cohereの英語版モデルとマルチリンガル版の違の違いについてですが、スコアとしては10%以上落ちるという結果となりました。さすがに日本語データを英語版モデルで扱うのは無理があるなという感じです。
Open AIのモデルはその知名度から、他社では実現できないようなダントツの性能をたたき出すのでは?とぼんやりと思っていたのですが、知名度がなくても以外と十分に性能が高いLLMは他にもあるんだなということがわかりました。
特に、最近では日本企業が日本語版のLLMをリリースしているニュースがよく見られますので今後期待したいところです。
さいごに
今回はEmbedの処理にフォーカスしてベンチマークをしてみました。Embedによるベクトル化は一見地味な処理に見えますが、LLMを使ったシステムでは非常に重要な役割を果たします。ご存じの方も多いと思いますが、企業内のデータをベクトルデータベースにロードし、LLMとの連携によりLLMの知識を拡張するためのRAGと呼ばれる構成が非常に注目されているからです。このRAGを構成したシステムでのLLMの応答精度は、まさに、このEmbedの優劣で決まるといっても過言ではなく、縁の下の力持ち的な存在となっています。とは言えLLMの醍醐味はやはりテキスト生成、今後機会がありましたらテキスト生成のベンチマーク記事も作ってみたいと思います。
最後まで読んでいただきありがとうございました。
おまけ
大規模言語モデル関連のその他の記事