選擇最適合數據的嵌入模型:OpenAI 和開源多語言嵌入的對比測試
OpenAI最近發布了他們的新一代嵌入模型embedding v3,他們將其描述為性能最好的嵌入模型,具有更高的多語言性能。這些模型分為兩類:較小的稱為text- embeddings -3-small,較大且功能更強大的稱為text- embeddings -3-large。
這些模型的設計和訓練方式的信息披露得很少,模型只能通過付費API訪問。所以就出現了很多開源的嵌入模型但是這些開源的模型與OpenAI閉源模型相比如何呢?
本文將這些新模型與開源模型的性能進行實證比較。我們將創建一個數據檢索工作流,在這個工作流中,必須根據用戶查詢找到語料庫中最相關的文檔。
我們的語料庫是歐洲人工智能法案,該法案目前處于驗證的最后階段。這個語料庫除了是世界上第一個關于人工智能的法律框架外,還有一個重要的特點就是它有24種語言版本。這樣我們可以比較不同語系的數據檢索的準確性。
我們將從多語言文本語料庫生成自定義合成問題/答案數據集,在此自定義數據集上比較OpenAI和最先進的開源嵌入模型的準確性。最后會提供完整的代碼,因為本文所采用的方法可以適用于其他數據語料庫。
生成自定義Q/ A數據集
讓我們首先從生成自定義數據的問答(Q/ A)數據集開始,生成自定義數據集的好處可以通過確保數據集不是嵌入模型訓練的一部分來避免偏差,這可能發生在MTEB等參考基準上。并且我們可以將評估調整為特定的數據語料庫,這可能與檢索增強應用程序(RAG)等情況相關。
我們將使用Llama Index在其文檔中建議的簡單流程。語料庫首先被分成塊。然后對于每個分塊,通過大型語言模型(large language model, LLM)生成一組合成問題,使答案位于相應的分塊中:
使用Llama Index之類的LLM數據框架實現此策略非常簡單,如下面的代碼所示。
from llama_index.readers.web import SimpleWebPageReader
from llama_index.core.node_parser import SentenceSplitter
language = "EN"
url_doc = "https://eur-lex.europa.eu/legal-content/"+language+"/TXT/HTML/?uri=CELEX:52021PC0206"
documents = SimpleWebPageReader(html_to_text=True).load_data([url_doc])
parser = SentenceSplitter(chunk_size=1000)
nodes = parser.get_nodes_from_documents(documents, show_progress=True)
語料庫是歐盟人工智能法案的英文版本,使用這個官方URL直接從Web上獲取。本文使用2021年4月的草案版本,因為最終版本尚未適用于所有歐洲語言。所以我們選擇的這一版可以用其他23種歐盟官方語言中的任何一種語言替換URL中的language,檢索不同語言的文本(BG表示保加利亞語,ES表示西班牙語,CS表示捷克語,等等)。
使用SentenceSplitter對象將文檔分成每1000個令牌的塊。對于英語來說,這會生成大約100個塊。然后將每個塊作為上下文提供給以下提示(Llama Index庫中建議的默認提示):
prompts={}
prompts["EN"] = """\
Context information is below.
---------------------
{context_str}
---------------------
Given the context information and not prior knowledge, generate only questions based on the below query.
You are a Teacher/ Professor. Your task is to setup {num_questions_per_chunk} questions for an upcoming quiz/examination.
The questions should be diverse in nature across the document. Restrict the questions to the context information provided."
"""
這個提示可以生成關于文檔塊的問題,要為每個數據塊生成的問題數量作為參數“num_questions_per_chunk”傳遞,我們將其設置為2。然后可以通過調用Llama Index庫中的generate_qa_embedding_pairs來生成問題:
from llama_index.llms import OpenAI
from llama_index.legacy.finetuning import generate_qa_embedding_pairs
qa_dataset = generate_qa_embedding_pairs(
llm=OpenAI(model="gpt-3.5-turbo-0125",additional_kwargs={'seed':42}),
nodes=nodes,
qa_generate_prompt_tmpl = prompts[language],
num_questions_per_chunk=2
)
我們依靠OpenAI的GPT-3.5-turbo-0125來完成這項任務,結果對象' qa_dataset '包含問題和答案(塊)對。作為生成問題的示例,以下是前兩個問題的結果(其中“答案”是文本的第一部分):
- What are the main objectives of the proposal for a Regulation laying down harmonised rules on artificial intelligence (Artificial Intelligence Act) according to the explanatory memorandum?
- How does the proposal for a Regulation on artificial intelligence aim to address the risks associated with the use of AI while promoting the uptake of AI in the European Union, as outlined in the context information?
OpenAI嵌入模型
評估函數也是遵循Llama Index文檔:首先所有答案(文檔塊)的嵌入都存儲在VectorStoreIndex中,以便有效檢索。然后評估函數循環遍歷所有查詢,檢索前k個最相似的文檔,并根據MRR (Mean Reciprocal Rank)評估檢索的準確性,代碼如下:
def evaluate(dataset, embed_model, insert_batch_size=1000, top_k=5):
# Get corpus, queries, and relevant documents from the qa_dataset object
corpus = dataset.corpus
queries = dataset.queries
relevant_docs = dataset.relevant_docs
# Create TextNode objects for each document in the corpus and create a VectorStoreIndex to efficiently store and retrieve embeddings
nodes = [TextNode(id_=id_, text=text) for id_, text in corpus.items()]
index = VectorStoreIndex(
nodes, embed_model=embed_model, insert_batch_size=insert_batch_size
)
retriever = index.as_retriever(similarity_top_k=top_k)
# Prepare to collect evaluation results
eval_results = []
# Iterate over each query in the dataset to evaluate retrieval performance
for query_id, query in tqdm(queries.items()):
# Retrieve the top_k most similar documents for the current query and extract the IDs of the retrieved documents
retrieved_nodes = retriever.retrieve(query)
retrieved_ids = [node.node.node_id for node in retrieved_nodes]
# Check if the expected document was among the retrieved documents
expected_id = relevant_docs[query_id][0]
is_hit = expected_id in retrieved_ids # assume 1 relevant doc per query
# Calculate the Mean Reciprocal Rank (MRR) and append to results
if is_hit:
rank = retrieved_ids.index(expected_id) + 1
mrr = 1 / rank
else:
mrr = 0
eval_results.append(mrr)
# Return the average MRR across all queries as the final evaluation metric
return np.average(eval_results)
嵌入模型通過' embed_model '參數傳遞給評估函數,對于OpenAI模型,該參數是一個用模型名稱和模型維度初始化的OpenAIEmbedding對象。
from llama_index.embeddings.openai import OpenAIEmbedding
embed_model = OpenAIEmbedding(model=model_spec['model_name'],
dimensinotallow=model_spec['dimensions'])
dimensions參數可以縮短嵌入(即從序列的末尾刪除一些數字),而不會失去嵌入的概念表示屬性。OpenAI在他們的公告中建議,在MTEB基準測試中,嵌入可以縮短到256大小,同時仍然優于未縮短的text-embedding-ada-002嵌入(大小為1536)。
我們在四種不同的嵌入模型上運行評估函數:
兩個版本的text-embedding-3-large:一個具有最低可能維度(256),另一個具有最高可能維度(3072)。它們被稱為“OAI-large-256”和“OAI-large-3072”。
OAI-small:text-embedding-3-small,維數為1536。
OAI-ada-002:傳統的文本嵌入text-embedding-ada-002,維度為1536。
每個模型在四種不同的語言上進行評估:英語(EN),法語(FR),捷克語(CS)和匈牙利語(HU),分別涵蓋日耳曼語,羅曼語,斯拉夫語和烏拉爾語的例子。
embeddings_model_spec = {
}
embeddings_model_spec['OAI-Large-256']={'model_name':'text-embedding-3-large','dimensions':256}
embeddings_model_spec['OAI-Large-3072']={'model_name':'text-embedding-3-large','dimensions':3072}
embeddings_model_spec['OAI-Small']={'model_name':'text-embedding-3-small','dimensions':1536}
embeddings_model_spec['OAI-ada-002']={'model_name':'text-embedding-ada-002','dimensions':None}
results = []
languages = ["EN", "FR", "CS", "HU"]
# Loop through all languages
for language in languages:
# Load dataset
file_name=language+"_dataset.json"
qa_dataset = EmbeddingQAFinetuneDataset.from_json(file_name)
# Loop through all models
for model_name, model_spec in embeddings_model_spec.items():
# Get model
embed_model = OpenAIEmbedding(model=model_spec['model_name'],
dimensinotallow=model_spec['dimensions'])
# Assess embedding score (in terms of MRR)
score = evaluate(qa_dataset, embed_model)
results.append([language, model_name, score])
df_results = pd.DataFrame(results, columns = ["Language" ,"Embedding model", "MRR"])
MRR精度如下:
嵌入尺寸越大,性能越好。
開源嵌入模型
圍繞嵌入的開源研究也是非?;钴S的,Hugging Face 的 MTEB leaderboard會經常發布最新的嵌入模型。
為了在本文中進行比較,我們選擇了一組最近發表的四個嵌入模型(2024)。選擇的標準是他們在MTEB排行榜上的平均得分和他們處理多語言數據的能力。所選模型的主要特性摘要如下。
e5-mistral-7b-instruct:微軟的這個E5嵌入模型是從Mistral-7B-v0.1初始化的,并在多語言混合數據集上進行微調。模型在MTEB排行榜上表現最好,但也是迄今為止最大的(14GB)。
multilingual-e5-large-instruct(ML-E5-large):微軟的另一個E5模型,可以更好地處理多語言數據。它從xlm-roberta-large初始化,并在多語言數據集的混合上進行訓練。它比E5-Mistral小得多(10倍),上下文大小也小得多(514)。
BGE-M3:該模型由北京人工智能研究院設計,是他們最先進的多語言數據嵌入模型,支持100多種工作語言。截至2024年2月22日,它還沒有進入MTEB排行榜。
nomic-embed-text-v1 (Nomic- embed):該模型由Nomic設計,其性能優于OpenAI Ada-002和text-embedding-3-small,而且大小僅為0.55GB。該模型是第一個完全可復制和可審計的(開放數據和開源訓練代碼)的模型。
用于評估這些開源模型的代碼類似于用于OpenAI模型的代碼。主要的變化在于模型參數:
embeddings_model_spec = {
}
embeddings_model_spec['E5-mistral-7b']={'model_name':'intfloat/e5-mistral-7b-instruct','max_length':32768, 'pooling_type':'last_token',
'normalize': True, 'batch_size':1, 'kwargs': {'load_in_4bit':True, 'bnb_4bit_compute_dtype':torch.float16}}
embeddings_model_spec['ML-E5-large']={'model_name':'intfloat/multilingual-e5-large','max_length':512, 'pooling_type':'mean',
'normalize': True, 'batch_size':1, 'kwargs': {'device_map': 'cuda', 'torch_dtype':torch.float16}}
embeddings_model_spec['BGE-M3']={'model_name':'BAAI/bge-m3','max_length':8192, 'pooling_type':'cls',
'normalize': True, 'batch_size':1, 'kwargs': {'device_map': 'cuda', 'torch_dtype':torch.float16}}
embeddings_model_spec['Nomic-Embed']={'model_name':'nomic-ai/nomic-embed-text-v1','max_length':8192, 'pooling_type':'mean',
'normalize': True, 'batch_size':1, 'kwargs': {'device_map': 'cuda', 'trust_remote_code' : True}}
results = []
languages = ["EN", "FR", "CS", "HU"]
# Loop through all models
for model_name, model_spec in embeddings_model_spec.items():
print("Processing model : "+str(model_spec))
# Get model
tokenizer = AutoTokenizer.from_pretrained(model_spec['model_name'])
embed_model = AutoModel.from_pretrained(model_spec['model_name'], **model_spec['kwargs'])
if model_name=="Nomic-Embed":
embed_model.to('cuda')
# Loop through all languages
for language in languages:
# Load dataset
file_name=language+"_dataset.json"
qa_dataset = EmbeddingQAFinetuneDataset.from_json(file_name)
start_time_assessment=time.time()
# Assess embedding score (in terms of hit rate at k=5)
score = evaluate(qa_dataset, tokenizer, embed_model, model_spec['normalize'], model_spec['max_length'], model_spec['pooling_type'])
# Get duration of score assessment
duration_assessment = time.time()-start_time_assessment
results.append([language, model_name, score, duration_assessment])
df_results = pd.DataFrame(results, columns = ["Language" ,"Embedding model", "MRR", "Duration"])
結果如下:
BGE-M3的表現最好,其次是ML-E5-Large、E5-mistral-7b和Nomic-Embed。BGE-M3模型尚未在MTEB排行榜上進行基準測試,我們的結果表明它可能比其他模型排名更高。雖然BGE-M3針對多語言數據進行了優化,但它在英語方面的表現也比其他模型更好。
因為式開源模型所以一般都需要本地運行,所以我們還特意記錄了每個嵌入模型的處理時間。
E5-mistral-7b比其他模型大10倍以上,所以最慢是很正常的
總結
我們把所有的結果做一個匯總
采用開源模型獲得了最好的性能,BGE-M3模型表現最佳。該模型具有與OpenAI模型相同的上下文長度(8K),大小為2.2GB。
OpenAI的large(3072)、small 和ada模型的性能非常相似。減小large的嵌入尺寸(256)會導致性能下降,并且沒有像OpenAI說的那樣比ada更好。
幾乎所有型號(ML-E5-large除外)在英語上都表現最好。在捷克語和匈牙利語等語言中,表現存在顯著差異,這可能是因為訓練的數據比較少。
我們應該付費訂閱OpenAI,還是托管一個開源嵌入模型?
OpenAI最近的價格調整使得他們的API變得更加實惠,現在每百萬令牌的成本為0.13美元。如果每月處理一百萬個查詢(假設每個查詢涉及大約1K令牌),沒那么成本約為130美元。所以可以根據實際需要計算來選擇是否托管開源嵌入模型。
當然成本效益并不是唯一的考慮因素。可能還需要考慮延遲、隱私和對數據處理工作流的控制等其他因素。開源模型提供了完全數據控制的優勢,增強了隱私性和定制性。
說到延遲,OpenAI的API也存在延遲問題,有時會導致響應時間延長,所有有時候OpenAI的API不一定是最快的選擇。
總之,在開源模型和像OpenAI這樣的專有解決方案之間做出選擇并不是一個簡單的答案。開源嵌入提供了一個非常好的可選項,它將性能與對數據的更好控制結合在一起。而OpenAI的產品可能仍然會吸引那些優先考慮便利性的人,特別是如果隱私問題是次要的。
本文代碼:https://github.com/Yannael/multilingual-embeddings