選擇最適合數(shù)據(jù)的嵌入模型:OpenAI 和開源多語言嵌入的對(duì)比測試
OpenAI最近發(fā)布了他們的新一代嵌入模型embedding v3,他們將其描述為性能最好的嵌入模型,具有更高的多語言性能。這些模型分為兩類:較小的稱為text- embeddings -3-small,較大且功能更強(qiáng)大的稱為text- embeddings -3-large。

這些模型的設(shè)計(jì)和訓(xùn)練方式的信息披露得很少,模型只能通過付費(fèi)API訪問。所以就出現(xiàn)了很多開源的嵌入模型但是這些開源的模型與OpenAI閉源模型相比如何呢?
本文將這些新模型與開源模型的性能進(jìn)行實(shí)證比較。我們將創(chuàng)建一個(gè)數(shù)據(jù)檢索工作流,在這個(gè)工作流中,必須根據(jù)用戶查詢找到語料庫中最相關(guān)的文檔。
我們的語料庫是歐洲人工智能法案,該法案目前處于驗(yàn)證的最后階段。這個(gè)語料庫除了是世界上第一個(gè)關(guān)于人工智能的法律框架外,還有一個(gè)重要的特點(diǎn)就是它有24種語言版本。這樣我們可以比較不同語系的數(shù)據(jù)檢索的準(zhǔn)確性。

我們將從多語言文本語料庫生成自定義合成問題/答案數(shù)據(jù)集,在此自定義數(shù)據(jù)集上比較OpenAI和最先進(jìn)的開源嵌入模型的準(zhǔn)確性。最后會(huì)提供完整的代碼,因?yàn)楸疚乃捎玫姆椒梢赃m用于其他數(shù)據(jù)語料庫。
生成自定義Q/ A數(shù)據(jù)集
讓我們首先從生成自定義數(shù)據(jù)的問答(Q/ A)數(shù)據(jù)集開始,生成自定義數(shù)據(jù)集的好處可以通過確保數(shù)據(jù)集不是嵌入模型訓(xùn)練的一部分來避免偏差,這可能發(fā)生在MTEB等參考基準(zhǔn)上。并且我們可以將評(píng)估調(diào)整為特定的數(shù)據(jù)語料庫,這可能與檢索增強(qiáng)應(yīng)用程序(RAG)等情況相關(guān)。
我們將使用Llama Index在其文檔中建議的簡單流程。語料庫首先被分成塊。然后對(duì)于每個(gè)分塊,通過大型語言模型(large language model, LLM)生成一組合成問題,使答案位于相應(yīng)的分塊中:

使用Llama Index之類的LLM數(shù)據(jù)框架實(shí)現(xiàn)此策略非常簡單,如下面的代碼所示。
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)語料庫是歐盟人工智能法案的英文版本,使用這個(gè)官方URL直接從Web上獲取。本文使用2021年4月的草案版本,因?yàn)樽罱K版本尚未適用于所有歐洲語言。所以我們選擇的這一版可以用其他23種歐盟官方語言中的任何一種語言替換URL中的language,檢索不同語言的文本(BG表示保加利亞語,ES表示西班牙語,CS表示捷克語,等等)。

使用SentenceSplitter對(duì)象將文檔分成每1000個(gè)令牌的塊。對(duì)于英語來說,這會(huì)生成大約100個(gè)塊。然后將每個(gè)塊作為上下文提供給以下提示(Llama Index庫中建議的默認(rèn)提示):
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."
 """這個(gè)提示可以生成關(guān)于文檔塊的問題,要為每個(gè)數(shù)據(jù)塊生成的問題數(shù)量作為參數(shù)“num_questions_per_chunk”傳遞,我們將其設(shè)置為2。然后可以通過調(diào)用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來完成這項(xiàng)任務(wù),結(jié)果對(duì)象' qa_dataset '包含問題和答案(塊)對(duì)。作為生成問題的示例,以下是前兩個(gè)問題的結(jié)果(其中“答案”是文本的第一部分):
- 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嵌入模型
評(píng)估函數(shù)也是遵循Llama Index文檔:首先所有答案(文檔塊)的嵌入都存儲(chǔ)在VectorStoreIndex中,以便有效檢索。然后評(píng)估函數(shù)循環(huán)遍歷所有查詢,檢索前k個(gè)最相似的文檔,并根據(jù)MRR (Mean Reciprocal Rank)評(píng)估檢索的準(zhǔn)確性,代碼如下:
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 '參數(shù)傳遞給評(píng)估函數(shù),對(duì)于OpenAI模型,該參數(shù)是一個(gè)用模型名稱和模型維度初始化的OpenAIEmbedding對(duì)象。
from llama_index.embeddings.openai import OpenAIEmbedding
 
 embed_model = OpenAIEmbedding(model=model_spec['model_name'],
                              dimensinotallow=model_spec['dimensions'])dimensions參數(shù)可以縮短嵌入(即從序列的末尾刪除一些數(shù)字),而不會(huì)失去嵌入的概念表示屬性。OpenAI在他們的公告中建議,在MTEB基準(zhǔn)測試中,嵌入可以縮短到256大小,同時(shí)仍然優(yōu)于未縮短的text-embedding-ada-002嵌入(大小為1536)。
我們在四種不同的嵌入模型上運(yùn)行評(píng)估函數(shù):
兩個(gè)版本的text-embedding-3-large:一個(gè)具有最低可能維度(256),另一個(gè)具有最高可能維度(3072)。它們被稱為“OAI-large-256”和“OAI-large-3072”。
OAI-small:text-embedding-3-small,維數(shù)為1536。
OAI-ada-002:傳統(tǒng)的文本嵌入text-embedding-ada-002,維度為1536。
每個(gè)模型在四種不同的語言上進(jìn)行評(píng)估:英語(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精度如下:

嵌入尺寸越大,性能越好。

開源嵌入模型
圍繞嵌入的開源研究也是非常活躍的,Hugging Face 的 MTEB leaderboard會(huì)經(jīng)常發(fā)布最新的嵌入模型。
為了在本文中進(jìn)行比較,我們選擇了一組最近發(fā)表的四個(gè)嵌入模型(2024)。選擇的標(biāo)準(zhǔn)是他們在MTEB排行榜上的平均得分和他們處理多語言數(shù)據(jù)的能力。所選模型的主要特性摘要如下。

e5-mistral-7b-instruct:微軟的這個(gè)E5嵌入模型是從Mistral-7B-v0.1初始化的,并在多語言混合數(shù)據(jù)集上進(jìn)行微調(diào)。模型在MTEB排行榜上表現(xiàn)最好,但也是迄今為止最大的(14GB)。
multilingual-e5-large-instruct(ML-E5-large):微軟的另一個(gè)E5模型,可以更好地處理多語言數(shù)據(jù)。它從xlm-roberta-large初始化,并在多語言數(shù)據(jù)集的混合上進(jìn)行訓(xùn)練。它比E5-Mistral小得多(10倍),上下文大小也小得多(514)。
BGE-M3:該模型由北京人工智能研究院設(shè)計(jì),是他們最先進(jìn)的多語言數(shù)據(jù)嵌入模型,支持100多種工作語言。截至2024年2月22日,它還沒有進(jìn)入MTEB排行榜。
nomic-embed-text-v1 (Nomic- embed):該模型由Nomic設(shè)計(jì),其性能優(yōu)于OpenAI Ada-002和text-embedding-3-small,而且大小僅為0.55GB。該模型是第一個(gè)完全可復(fù)制和可審計(jì)的(開放數(shù)據(jù)和開源訓(xùn)練代碼)的模型。
用于評(píng)估這些開源模型的代碼類似于用于OpenAI模型的代碼。主要的變化在于模型參數(shù):
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"])結(jié)果如下:

BGE-M3的表現(xiàn)最好,其次是ML-E5-Large、E5-mistral-7b和Nomic-Embed。BGE-M3模型尚未在MTEB排行榜上進(jìn)行基準(zhǔn)測試,我們的結(jié)果表明它可能比其他模型排名更高。雖然BGE-M3針對(duì)多語言數(shù)據(jù)進(jìn)行了優(yōu)化,但它在英語方面的表現(xiàn)也比其他模型更好。
因?yàn)槭介_源模型所以一般都需要本地運(yùn)行,所以我們還特意記錄了每個(gè)嵌入模型的處理時(shí)間。

E5-mistral-7b比其他模型大10倍以上,所以最慢是很正常的
總結(jié)
我們把所有的結(jié)果做一個(gè)匯總

采用開源模型獲得了最好的性能,BGE-M3模型表現(xiàn)最佳。該模型具有與OpenAI模型相同的上下文長度(8K),大小為2.2GB。
OpenAI的large(3072)、small 和ada模型的性能非常相似。減小large的嵌入尺寸(256)會(huì)導(dǎo)致性能下降,并且沒有像OpenAI說的那樣比ada更好。
幾乎所有型號(hào)(ML-E5-large除外)在英語上都表現(xiàn)最好。在捷克語和匈牙利語等語言中,表現(xiàn)存在顯著差異,這可能是因?yàn)橛?xùn)練的數(shù)據(jù)比較少。
我們應(yīng)該付費(fèi)訂閱OpenAI,還是托管一個(gè)開源嵌入模型?
OpenAI最近的價(jià)格調(diào)整使得他們的API變得更加實(shí)惠,現(xiàn)在每百萬令牌的成本為0.13美元。如果每月處理一百萬個(gè)查詢(假設(shè)每個(gè)查詢涉及大約1K令牌),沒那么成本約為130美元。所以可以根據(jù)實(shí)際需要計(jì)算來選擇是否托管開源嵌入模型。
當(dāng)然成本效益并不是唯一的考慮因素??赡苓€需要考慮延遲、隱私和對(duì)數(shù)據(jù)處理工作流的控制等其他因素。開源模型提供了完全數(shù)據(jù)控制的優(yōu)勢,增強(qiáng)了隱私性和定制性。
說到延遲,OpenAI的API也存在延遲問題,有時(shí)會(huì)導(dǎo)致響應(yīng)時(shí)間延長,所有有時(shí)候OpenAI的API不一定是最快的選擇。
總之,在開源模型和像OpenAI這樣的專有解決方案之間做出選擇并不是一個(gè)簡單的答案。開源嵌入提供了一個(gè)非常好的可選項(xiàng),它將性能與對(duì)數(shù)據(jù)的更好控制結(jié)合在一起。而OpenAI的產(chǎn)品可能仍然會(huì)吸引那些優(yōu)先考慮便利性的人,特別是如果隱私問題是次要的。
本文代碼:https://github.com/Yannael/multilingual-embeddings















 
 
 



 
 
 
 