第2章 相似匹配——萬物皆可Embedding
第2章 相似匹配——萬物皆可Embedding
??第一章我們簡單介紹了Embedding的概念,我們知道Embedding可以用來表示一個詞或一句話。讀者可能會有困惑,這和ChatGPT或者大模型有什么關系,為什么需要Embedding?在哪里需要Embedding?這兩個問題可以簡單用一句話概括:因為需要獲取“相關”上下文。具體來說,NLP不少任務以及大模型的應用都需要一定的“上下文”知識,而Embedding技術就用來獲取這些上下文。這一過程在NLP處理中也叫“相似匹配”——把相關內容轉成Embedding表示,然后通過Embedding相似度來獲取最相關內容作為上下文。
??本章我們首先將進一步了解相似匹配的相關基礎,尤其是如何更好地表示一段自然語言文本,以及如何衡量Embedding的相似程度。接下來我們將介紹ChatGPT相關接口使用,其他廠商提供的類似接口用法也差不多。最后,我們將介紹與Embedding相關的幾個任務和應用,這里面有些可以用大模型解決,但也可以不用大模型。無論是ChatGPT還是大模型,都只是我們工具箱中的一個工具,我們將側重任務和應用,重點介紹如何解決此類問題。也期望讀者能在閱讀的過程中能感受到目的和方法的區別,方法無論如何總歸是為目的服務的。
2.1 相似匹配基礎
2.1.1 更好的Embedding表示
1.Embedding表示回顧
??首先我們簡單回顧一下上一章介紹的Embedding表示。對于自然語言,因為它的輸入是一段文本,在中文里就是一個一個字,或一個一個詞,行業內把這個字或詞叫Token。如果要使用模型,拿到一段文本的第一件事就是把它Token化,當然,可以按字、也可以按詞,或按你想要的其他方式,比如每兩個字(Bi-Gram)一組、每三個字(Tri-Gram)一組。我們看下面這個例子。
- 給定文本:人工智能讓世界變得更美好。
- 按字Token化:人 工 智 能 讓 世 界 變 得 更 美 好 。
- 按詞Token化:人工智能 讓 世界 變 得 更 美好 。
- 按字Bi-Gram Token化:人/工 工/智 智/能 能/讓 讓/世 世/界 界/變 變/得 得/更 更/美 美/好 好/。
- 按詞Tri-Gram Token化:人工智能/讓 讓/世界 世界/變得 變得/更 更/美好 美好/。
??那自然就有一個新的問題:我們應該怎么選擇Token化方式?其實每種不同的方法都有自己的優點和不足,英文一般用子詞表示,中文以前常見的是字或詞的方式。大模型時代,中文為主的大模型基本都是以字+詞的方式。如第一章所述,這種方式一方面能夠更好地表示語義,另一方面對于沒見過的詞又可以用字的方式表示,避免了遇到不在詞表中的詞時導致的無法識別和處理的情況。
??Token化后,第二件事就是要怎么表示這些Token,我們知道計算機只能處理數字,所以要想辦法把這些Token給變成計算機能識別的數字才行。這里需要一個詞表,將每個詞映射成詞表對應位置的序號。以上面的句子為例,假設以字為粒度,那么詞表就可以用一個txt文件存儲,內容如下:
人
工
智
能
讓
世
界
變
得
更
美
好
一行一個字,每個字作為一個Token,此時,0=我,1=們,……,以此類推,我們假設詞表大小為N。這里有一點需要注意,就是詞表的順序是無關緊要的,不過一旦確定下來訓練好模型后就不能再隨便調整了。這里說的調整包括調整順序、增加詞、刪除詞、修改詞等。如果只是調整順序或刪除詞,則不需要重新訓練模型,但需要手動將Embedding參數也相應地調整順序或刪除對應行。如果是增改詞表,則需要重新訓練模型,獲取增改部分的Embedding參數。接下來就是將這些序號(Token ID)表示成稠密向量(Embedding)。它的主要思想如下。
- 把特征固定在某一個維度D,比如256、300、768等等,這個不重要,總之不再是詞表那么大的數字。
- 利用自然語言文本的上下文關系學習一個由D個浮點數組成的稠密表示。
??接下來是Embedding的學習過程。首先,隨機初始化這個數組。就像下面這樣:
import numpy as np
rng = np.random.default_rng(42)
# 詞表大小N=16,維度D=256
table = rng.uniform(size=(16, 256))
table.shape == (16, 256)
假設詞表大小為16,維度為256,初始化后,我們就獲得了一個16×256大小的二維數組,每一行浮點數就表示對應位置的Token。接下來就是通過一定算法和策略來調整(訓練)這里面的數字(更新參數)。當訓練結束時,最終得到的數組就是詞表的Embedding表示,也就是詞向量。這種表示方法在深度學習早期(2014年左右)比較流行,不過由于這個矩陣訓練好后就固定不變了,這在有些時候就不合適。比如“我喜歡蘋果”這句話在不同的情況下可能完全是不同的意思,因為“蘋果”可以指一種水果,也可以指蘋果手機。
??我們知道,句子才是語義的最小單位,相比Token,我們其實更加關注和需要句子的表示。而且,如果我們能夠很好地表示句子,詞也可以看作是一個很短的句子,表示起來自然也不在話下。我們還期望可以根據不同上下文動態地獲得句子表示。這中間經歷了比較多的探索,但最終走向了模型架構上做設計——當輸入任意一段文本,模型經過一定計算后就可以直接獲得對應的向量表示。
2.如何更好地表示
??前面的介紹我們都將模型當作黑盒,默認輸入一段文本就會給出一個表示。但這中間其實也有不少細節,具體來說,就是如何給出這個表示。下面,我們介紹幾種常見的方法,并探討其中機理。
??直觀來看,我們可以借鑒詞向量的思想,把這里的“詞”換成“句”,當模型訓練完后,就可以得到句子向量了。不過,稍微思考一下就會發現,其實它本質上只是粒度更大的一種Token化方式,粒度太大有的問題它更加突出。而且,這樣得到的句子向量還有個問題:無法通過句子向量獲取其中的詞向量,而有些場景下又需要詞向量。看來,此路難行。
??還有一種操作起來更簡單的方式,我們在第一章中也提到過:直接對詞向量取平均。無論一句話或一篇文檔有多少個詞,找到每個詞的詞向量,平均就好了,得到的向量大小和詞向量一樣。事實上,在深度學習NLP剛開始的幾年,這種方式一直是主流,也出現了不少關于如何平均的工作,比如使用加權求和,權重可以根據詞性、句法結構等設定一個固定值。
??2014年,也就是Google發布Word2Vec后一年,差不多是同一批人提出了一種表示文檔的方法——Doc2Vec,其思想是在每句話前面增加一個額外的“段落”Token作為段落的向量表征 ,我們可以將它視為該段落的主題。訓練模型可以采用和詞向量類似的方式,但每次更新詞向量參數時,需要額外更新這個“段落Token”向量。直觀看來,就是把文檔的語義都融入到這個特殊的Token向量里。不過這個方法有個很嚴重的問題,那就是推理時,如果遇到訓練集里沒有的文檔,需要將這個文檔的參數更新到模型里。這不僅不方便,而且效率也低。
??隨后,隨著深度學習進一步發展,涌現出一批模型,最經典的就是TextCNN和RNN。RNN我們在第一章有過介紹,TextCNN的想法來自圖像領域的卷積神經網絡(convolutional neural network,CNN)。它以若干個固定大小的窗口在文本上滑動,每個窗口從頭滑到尾就會得到一組浮點數特征,若干個不同大小窗口(一般取2、3、4)就會得到若干個不同的特征。將它們拼接起來就可以表示這段文本了。TextCNN的表示能力其實不錯,一直以來都是作為基準模型使用,很多線上模型也用它。它的主要問題是只利用了文本的局部特征,沒有考慮全局語義。RNN和它的幾個變種都是時序模型,從前到后一個Token接著一個Token處理。它也有不錯的表示能力,但有兩個比較明顯的不足:一個是比較慢,沒法并行;另一個問題是文本太長時效果不好。總的來說,這一階段詞向量用的比較少了,文本的表示主要通過模型架構體現,Token化的方式以詞為主。
??2017年,Transformer橫空出世,帶來了迄今最強特征表示方式:自注意力機制。模型開始慢慢變大,從原來的十萬、百萬級別逐漸增加到了億級別。文檔表示方法并沒有太多創新,但由于模型變大,表示效果有了明顯提升。自此,NLP進入了預訓練時代——基于Transformer架構訓練一個大模型,做任務時都以該模型為起點,在對應數據上進行微調訓練。代表性的工作是BERT和GPT,前者用了Transformer的編碼器,后者用了解碼器。BERT在每個文檔前面添加一個[CLS]Token用來表示整句話的語義,但與Doc2Vec不同的是,它在推理時不需要額外訓練,模型會根據當前輸入通過計算自動獲得表示。也就是說,同樣的輸入,相比Doc2Vec,BERT因為其強大的表示能力,可以通過模型計算,不額外訓練就能獲得不錯的文本表示。GPT在第一章中有相關介紹,這里不再贅述。無論是哪個預訓練模型,底層其實都是對每個Token進行計算(計算時一般都會利用到其他Token信息)。所以,預訓練模型一般都可以獲得每個Token位置的向量表示。于是,文檔表示依然可以使用那種最常見的方式——取平均。當然,由于模型架構變得復雜,取平均的方式也更加靈活多樣,比如用自注意力作為權重加權平均。
3.進一步思考
??ChatGPT的出現其實是語言模型的突破,并沒有涉及到Embedding,但是由于模型在處理超長文本上的限制(主要是資源限制和超長距離的上下文依賴問題),Embedding成了一個重要組件。我們先不討論大模型,依然把關注點放在Embedding上。
??接下來主要是筆者本人的一些思考,期望能與讀者共同探討。如前所述,如今Embedding已經轉變成了模型架構的副產物,架構變強——Token表示變強——文檔表示變強。這中間第一步目前沒什么問題,Token表示通過架構充分利用了各種信息,而且還可以得到不同層級的抽象。但第二步卻有點單薄,要么是[CLS]Token,要么就是變著法子的取平均。這些方法在句子上可能問題不大,因為句子一般比較短,但在段落、篇章,甚至更長文本下卻不一定了。
??還是以人類閱讀進行類比(很多模型都是從人類獲得啟發,比如CNN、自注意力等)。我們在看一句話時,會重點關注其中一些關鍵詞,整體語義可能通過這些關鍵詞就能表達一二。看一段話時可能依然是關鍵詞、包含關鍵詞的關鍵句等。但當我們在看一篇文章時,其中的關鍵詞和關鍵句可能就不那么突出了,我們可能會更加關注整體在表達什么,描述這樣的表達可能并不會用到文本中的詞或句。
??也就是說,我們人類在處理句子和篇章的方式是不一樣的。但是現在模型卻把它們當成同樣的東西進行處理,沒有考慮這中間量變引起的質變。通俗點來說,這是幾粒沙子和沙堆的區別。我們的模型設計是否可以考慮這樣的不同?
??最后我們簡單總結一下,Embedding本質就是一組稠密向量(不用過度關注它怎么來的),用來表示一段文本(可以是字、詞、句、段等)。獲取到這個表示后,我們就可以進一步做一些任務。讀者不妨先思考一下,當給定任意句子并獲得到它的固定長度的語義表示時,我們可以干什么?
2.1.2 如何度量Embedding相似度
??提起相似度,讀者可能首先會想到編輯距離相似度,它可以用來衡量字面量的相似度,也就是文本本身的相似程度。但如果是語義層面,我們一般會使用余弦(cosine)相似度,它可以評估兩個向量在語義空間上的分布情況,如式(2.1)所示。
其中,\(v\)和\(w\)分別表示兩個文本向量,\(i\)表示向量中第\(i\)個元素的值。
??我們舉個例子:
import numpy as np
a = [0.1, 0.2, 0.3]
b = [0.2, 0.3, 0.4]
cosine_ab = (0.1*0.2+0.2*0.3+0.3*0.4)/(np.sqrt(0.1**2+0.2**2+0.3**2) * np.sqrt(0.2**2+0.3**2+0.4**2))
cosine_ab == 0.9925833339709301
在這個例子中,我們給定兩個向量表示a和b,然后用式(1)計算相似度,得到它們的相似度為0.9926。
??上一小節我們得到了一段文本的向量表示,這一小節我們可以計算兩個向量的相似程度。換句話說,我們現在可以知道兩段給定文本的相似程度,或者說給定一段文本可以從庫里找到與它語義最相似的若干段文本。這個邏輯會用在很多NLP應用上,我們一般也會把這個過程叫作“語義匹配”。不過在正式介紹任務和應用之前,先來了解下ChatGPT的相關接口。
2.2 ChatGPT接口使用
??這部分我們主要為讀者介紹兩個接口,一個是ChatGPT提供的Embedding接口,另一個是ChatGPT接口。前者可以獲取給定文本的向量表示;后者可以直接完成語義匹配任務。
2.1 Embedding接口
??首先是一些準備工作,主要是設置OPENAI_API_KEY,這里建議讀者通過環境變量獲取,不要明文將自己的密鑰寫在任何代碼文件里。當然,更加不要上傳到開源代碼倉庫。
import os
import openai
# 用環境變量獲取
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
# 或直接填上自己的API key,不建議正式場景下使用
OPENAI_API_KEY = "填入專屬的API key"
openai.api_key = OPENAI_API_KEY
??接下來,我們輸入文本,指定相應模型,獲取文本對應的Embedding。
text = "我喜歡你"
model = "text-embedding-ada-002"
emb_req = openai.Embedding.create(input=[text], model=model)
??接口會返回輸入文本的向量表示,結果如下。
emb = emb_req.data[0].embedding
len(emb) == 1536
type(emb) == list
??我們看到,Embedding表示是一個列表,里面包含1536個浮點數。
??OpenAI官方還提供了一個集成接口,既包括獲取Embedding,也包括計算相似度,使用起來更加簡單(讀者也可以嘗試自己寫一個),如下所示。
from openai.embeddings_utils import get_embedding, cosine_similarity
text1 = "我喜歡你"
text2 = "我鐘意你"
text3 = "我不喜歡你"
# 注意它默認的模型是text-similarity-davinci-001,我們也可以換成text-embedding-ada-002
emb1 = get_embedding(text1)
emb2 = get_embedding(text2)
emb3 = get_embedding(text3)
??接口直接返回向量表示,結果如下。
len(emb1) == 12288
type(emb1) == list
??和上面不同的是Embedding的長度變了,從1536變成了12288。這個主要是因為get_embedding接口默認的模型和上面我們指定的模型不一樣。模型不同時,Embedding的長度(維度)也可能不同。一般情況下,Embedding維度越大,表示效果越佳,但同時計算速度越慢(從調用API的角度可能感知不明顯)。當然,它們的價格也可能不一樣。
??現在來計算一下幾個文本的相似度,直觀感受一下。
cosine_similarity(emb1, emb2) == 0.9246855139297101
cosine_similarity(emb1, emb3) == 0.8578009661644189
cosine_similarity(emb2, emb3) == 0.8205299527695261
??前兩句是一個意思,相似度高一些。第一句和第三句,第二句和第三句是相反的,所以相似度低一些。下面,我們換維度為1536的模型試一下效果,如下所示。
text1 = "我喜歡你"
text2 = "我鐘意你"
text3 = "我不喜歡你"
emb1 = get_embedding(text1, "text-embedding-ada-002")
emb2 = get_embedding(text2, "text-embedding-ada-002")
emb3 = get_embedding(text3, "text-embedding-ada-002")
??使用方法類似,只是第二個參數我們改成了1536維的模型,結果如下。
cosine_similarity(emb1, emb2) == 0.8931105629213952
cosine_similarity(emb1, emb3) == 0.9262074073566393
cosine_similarity(emb2, emb3) == 0.845821877417193
??這個結果不太令人滿意。不過,我們正好可以用來探討關于“相似度”的一個有意思的觀點。為什么很多語義匹配模型都會認為“我喜歡你”和“我不喜歡你”的相似度比較高?其實,從客觀角度來看,這兩句話是就是相似的,它們結構一樣,都是表達一種情感傾向,句式結構也相同,之所以我們覺得不相似只是因為我們只關注了一個(我們想要的)角度[4]。所以,如果想要模型的輸出和我們想要的一致,就需要重新設計和訓練模型。我們需要明確地告訴模型,“我喜歡你”與”我鐘意你“比”我喜歡你“和”我不喜歡你“更相似。
??因此,在實際使用時,我們最好能夠在自己的數據集上進行測試,明確各項指標的表現。如果不滿足需要,還需要考慮是否需要在自己的數據集上專門訓練一個Embedding模型。同時,應綜合考慮性能、價格等因素。
2.2 ChatGPT+提示詞
??接下來我們用萬能的ChatGPT嘗試一下,注意它不會返回Embedding,而是嘗試直接告訴我們答案,如下所示。
content = "請告訴我下面三句話的相似程度:\n1. 我喜歡你。\n2. 我鐘意你。\n3.我不喜歡你。\n"
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": content}]
)
response.get("choices")[0].get("message").get("content")
??這里,我們直接調用GPT-3.5(也就是ChatGPT)的API,返回結果如下所示。
1和2相似,都表達了對某人的好感或喜歡之情。而3則與前兩句截然相反,表示對某人的反感或不喜歡。
??效果看起來不錯,不過這個格式不太好,我們調整一下,讓它格式化輸出,如下所示。
content += "第一句話用a表示,第二句話用b表示,第三句話用c表示,請以json格式輸出兩兩相似度,類似下面這樣:\n{"ab": a和b的相似度}"
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": content}]
)
response.get("choices")[0].get("message").get("content")
??注意這里我們直接在原content基礎上增加格式要求,結果如下所示。
{"ab": 0.8, "ac": -1, "bc": 0.7}\n\n解釋:a和b的相似度為0.8,因為兩句話表達了相同的情感;a和c的相似度為-1,因為兩句話表達了相反的情感;b和c的相似度為0.7,因為兩句話都是表達情感,但一個是積極情感,一個是消極情感,相似度略低。
??可以看到,ChatGPT輸出了我們想要的格式,但后兩句b和c的結果并不是我們想要的。不過我們看它給出的解釋:“兩句話都是表達情感,但一個是積極情感,一個是消極情感,相似度略低。”這點和我們上一小節討論的關于“相似度”的觀點是類似的。不過,類似ChatGPT這樣的大模型接口要在自己的數據上進行訓練就不那么方便了。這時候可以在提示詞里先給一些類似的示例,讓它知道我們想要的是語義上的相似。讀者不妨自己嘗試一下。
2.3 相關任務與應用
??有讀者可能會疑惑,既然ChatGPT已經這么強大了,為什么還要介紹Embedding這種看起來好像有點“低級”的技術呢?這個我們在本章一開頭就簡單提到了。這里稍微再擴充一下,其實目前來看主要有兩個方面的原因。第一,有些問題使用Embedding解決(或其他非ChatGPT的方式)會更加合理。通俗來說就是“殺雞焉用牛刀”。第二,ChatGPT性能方面不是特別高效,畢竟是一個Token一個Token吐出來的。
??關于第一點,我們要額外多說幾句。選擇技術方案就跟找工作一樣,合適最重要。只要你的問題(需求)沒變,能解決的技術就是好技術。比如任務就是一個二分類,明明一個很簡單的模型就能解決,就沒必要非得用個很復雜的。除非ChatGPT這樣的大語言模型API已經大眾到一定程度——任何人都能夠非常流暢、自由地使用;而且,我們就是想要簡單、低門檻、快捷地實現功能。
??言歸正傳,使用Embedding的應用大多跟語義相關,我們這里介紹與此相關的幾個經典任務和相關的應用。
2.3.1 簡單問答:以問題找問題
??QA是問答的意思,Q表示Question,A表示Answer,QA是NLP非常基礎和常用的任務。簡單來說,就是當用戶提出一個問題時,我們能從已有的問題庫中找到一個最相似的,并把它的答案返回給用戶。這里有兩個關鍵點:第一,事先需要有一個QA庫。第二,用戶提問時,系統要能夠在QA庫中找到一個最相似的。
??用ChatGPT或其他生成模型做這類任務有點麻煩,尤其是當:QA庫非常龐大時;或給用戶的答案是固定的、不允許自由發揮時。生成方式做起來是事倍功半。但是Embedding卻天然的非常適合,因為該任務的核心就是在一堆文本中找出與給定文本最相似的文本。簡單總結,QA問題其實就是相似度計算問題。
??我們使用Kaggle提供的Quora數據集:all-kaggle-questions-on-qoura-dataset,數據集可以在Kaggle官網搜索下載。下載后是一個csv文件,先把它給讀進來。
import pandas as pd
df = pd.read_csv("dataset/Kaggle related questions on Qoura - Questions.csv")
df.shape == (1166, 4)
??數據集包括1166行,4列。
df.head()
??使用df.head()可以讀取數據集的前5條,如表2-1所示。
表2-1 Quora數據集樣例
| Questions | Followers | Answered | Link | |
|---|---|---|---|---|
| 0 | How do I start participating in Kaggle competi... | 1200 | 1 | /How-do-I-start-participating-in-Kaggle-compet... |
| 1 | Is Kaggle dead? | 181 | 1 | /Is-Kaggle-dead |
| 2 | How should a beginner get started on Kaggle? | 388 | 1 | /How-should-a-beginner-get-started-on-Kaggle |
| 3 | What are some alternatives to Kaggle? | 201 | 1 | /What-are-some-alternatives-to-Kaggle |
| 4 | What Kaggle competitions should a beginner sta... | 273 | 1 | /What-Kaggle-competitions-should-a-beginner-st... |
??第一列是問題列表,第二列是關注人數,第三列表示是否被回答,最后一列是對應的鏈接地址。
??這里,我們就把最后一列鏈接地址Link當作答案構造QA數據對,基本的流程如下。
- 第一步:對每個問題計算Embedding。
- 第二步:存儲Embedding,同時存儲每個問題對應的答案。
- 第三步:從存儲的地方檢索最相似的問題。
??第一步我們將借助OpenAI的Embedding接口,但是后兩步得看實際情況了。如果問題的數量比較少,比如只有幾萬條甚至幾千條,那我們可以把計算好的Embedding直接存儲成文件,每次服務啟動時直接加載到內存或緩存里就好了。使用時,挨個計算輸入問題和存儲的所有問題的相似度,然后給出最相似的問題的答案。演示代碼如下。
from openai.embeddings_utils import get_embedding, cosine_similarity
import openai
import numpy as np
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
openai.api_key = OPENAI_API_KEY
??首先依然是導入需要的工具包,并配置好OPENAI_API_KEY。然后遍歷DataFrame計算Embedding并存儲,如下所示。
vec_base = []
for v in df.itertuples():
emb = get_embedding(v.Questions)
im = {
"question": v.Questions,
"embedding": emb,
"answer": v.Link
}
vec_base.append(im)
??然后直接使用。比如給定輸入:"is kaggle alive?",我們先獲取它的Embedding,然后逐個遍歷vec_base計算相似度,并取相似度最高的一個或若干個作為響應。
query = "is kaggle alive?"
q_emb = get_embedding(query)
sims = [cosine_similarity(q_emb, v["embedding"]) for v in vec_base]
??為了方便展示,我們假設只有5條,如下所示。
sims == [
0.665769204766594,
0.8711775410642538,
0.7489853201153621,
0.7384357684745508,
0.7287129153982224
]
??此時,第二個相似度最高,我們返回第二個(索引為1)文檔即可。
vec_base[1]["question"], vec_base[1]["answer"] == ('Is Kaggle dead?', '/Is-Kaggle-dead')
??如果要返回多個,則返回前面若干個相似度高的文檔即可。
??當然,在實際中,我們不建議使用循環,那樣效率比較低。我們可以使用NumPy進行批量計算。
arr = np.array(
[v["embedding"] for v in vec_base]
)
??這里先將所有問題的Embedding構建成一個NumPy數組。
q_arr = np.expand_dims(q_emb, 0)
q_arr.shape == (1, 12288)
??對給定輸入的Embedding也將它變成NumPy的數組。需要注意的是,我們要給給它擴展一個維度,便于后面的計算。
from sklearn.metrics.pairwise import cosine_similarity
sims = cosine_similarity(arr, q_arr)
??使用sklearn包里的cosine_similarity可以批量計算兩個數組的相似度。這里的批量主要利用的NumPy的向量化計算,可以極大地提升效率,建議讀者親自嘗試兩種方案的效率差異。還是假設只有5條數據,結果如下所示。
sims == array([
[0.6657692 ],
[0.87117754],
[0.74898532],
[0.73843577],
[0.72871292]
])
??不過,當問題非常多時,比如上百萬甚至上億,這種方式就不合適了。一方面是內存里可能放不下,另一方面是算起來也很慢。這時候就必須借助一些專門用來做語義檢索的工具了。比較常用的工具有下面幾個。
- FaceBook的faiss:高效的相似性搜索和稠密向量聚類庫。
- milvus-io的milvus:可擴展的相似性搜索和面向人工智能應用的向量數據庫。
- Redis:是的,Redis也支持向量搜索。
此處,我們以Redis為例,其他工具用法類似。
??首先,我們需要一個Redis服務,建議使用Docker直接運行:
$ docker run -p 6379:6379 -it redis/redis-stack:latest
執行后,Docker會自動從hub把鏡像拉到本地,默認是6379端口,我們將其映射出來。
??然后安裝redis-py,也就是Redis的Python客戶端:
$ pip install redis
這樣我們就可以用Python和Redis進行交互了。
??先來個最簡單的例子:
import redis
r = redis.Redis()
r.set("key", "value")
??我們初始化了一個Redis實例,然后設置了一個key-value對,其中key就是字符串key,value是字符串value。現在就可以通過key獲取相應的value,如下所示。
r.get("key") == b'value'
??接下來的內容和剛剛放在內存的步驟差不多,但是這里我們需要先建索引,然后生成Embedding并把它存儲到Redis,再進行使用(從索引中搜索)。由于我們使用了向量工具,具體步驟會略微不同。
??索引的概念和數據庫中的索引有點類似,需要定義一組Schema,告訴Redis每個字段是什么,有哪些屬性。還是先導入需要的依賴。
from redis.commands.search.query import Query
from redis.commands.search.field import TextField, VectorField
from redis.commands.search.indexDefinition import IndexDefinition
??接下來就是定義字段和Schema。
# 向量維度
VECTOR_DIM = 12288
# 索引名稱
INDEX_NAME = "faq"
# 建好要存字段的索引,針對不同屬性字段,使用不同Field
question = TextField(name="question")
answer = TextField(name="answer")
embedding = VectorField(
name="embedding",
algorithm="HNSW",
attributes={
"TYPE": "FLOAT32",
"DIM": VECTOR_DIM,
"DISTANCE_METRIC": "COSINE"
}
)
schema = (question, embedding, answer)
??上面embedding字段里的HNSW是層級可導航小世界算法(hierarchical navigable small worlds,HNSW)。它是一種用于高效相似性搜索的算法,主要思想是將高維空間中的數據點組織成一個多層級的圖結構,使得相似的數據點在圖上彼此靠近。搜索時可以先通過粗略的層級找到一組候選數據點,然后逐漸細化搜索,直到找到最近似的鄰居。
??然后嘗試創建索引,如下所示。
index = r.ft(INDEX_NAME) # ft表示full text search
try:
info = index.info()
except:
index.create_index(schema, definition=IndexDefinition(prefix=[INDEX_NAME + "-"]))
??建好索引后,就可以往里面導入數據了。有時候我們可能需要刪除已有的文檔,可以使用下面的命令實現。
index.dropindex(delete_documents=True)
??再然后就是把數據導入Redis,整體邏輯和之前類似,不同的是需要將Embedding的浮點數存為字節。
for v in df.itertuples():
emb = get_embedding(v.Questions)
# 注意,redis要存儲bytes或string
emb = np.array(emb, dtype=np.float32).tobytes()
im = {
"question": v.Questions,
"embedding": emb,
"answer": v.Link
}
# 重點是這句set操作
r.hset(name=f"{INDEX_NAME}-{v.Index}", mapping=im)
??然后我們就可以進行搜索查詢了,這一步構造查詢輸入稍微有一點麻煩,需要寫一點查詢語言。
# 構造查詢輸入
query = "kaggle alive?"
embed_query = get_embedding(query)
params_dict = {"query_embedding": np.array(embed_query).astype(dtype=np.float32).tobytes()}
??獲取給定輸入的Embedding和之前一樣,構造參數字典就是將其轉為字節。接下來是編寫并構造查詢,如下所示。
k = 3
base_query = f"* => [KNN {k} @embedding $query_embedding AS score]"
return_fields = ["question", "answer", "score"]
query = (
Query(base_query)
.return_fields(*return_fields)
.sort_by("score")
.paging(0, k)
.dialect(2)
)
??query的語法為:{some filter query}=>[ KNN {num|$num} @vector_field $query_vec],包括以下幾項。
{some filter query}:字段過濾條件,可以使用多個條件。*表示任意。[ KNN {num|$num} @vector_field $query_vec]:K最近鄰算法(K Nearest Neighbors,KNN)的主要思想是對未知點分別和已有的點算距離,挑距離最近的K個點。num表示K。vector_field是索引里的向量字段,這里是embedding。query_embedding是參數字典中表示給定輸入Embedding的名稱,這里是query_embedding。AS score:表示K最近鄰算法計算結果的數據名稱為score。注意,這里的score其實是距離,不是相似度。換句話說,score越小,相似度越大。
??paging表示分頁,參數為:offset和num,默認值為0和10。 dialect表示查詢語法的版本,不同版本之間會有細微差別。
??此時,我們就可以通過searchAPI直接查詢了,查詢過程和結果如下。
# 查詢
res = index.search(query, params_dict)
for i,doc in enumerate(res.docs):
# 注意這里相似度和分數正好相反
similarity = 1 - float(doc.score)
print(f"{doc.id}, {doc.question}, {doc.answer} (Similarity: {round(similarity, 3) })")
??最終輸出內容如下。
faq-1, Is Kaggle dead?, /Is-Kaggle-dead (Score: 0.831)
faq-2, How should a beginner get started on Kaggle?, /How-should-a-beginner-get-started-on-Kaggle (Score: 0.735)
faq-3, What are some alternatives to Kaggle?, /What-are-some-alternatives-to-Kaggle (Score: 0.73)
??上面我們通過幾種不同的方法為大家介紹了如何使用Embedding進行QA任務。簡單回顧一下,要做QA任務首先咱們得有一個QA庫,這些QA就是我們的倉庫,每當一個新的問題過來時,我們就用這個問題去和咱們倉庫里的每一個問題去匹配,然后找到最相似的那個,接下來就把該問題的答案當作新問題的答案交給用戶。
??這個任務的核心就是如何找到這個最相似的,涉及兩個知識點:如何表示一個問題,以及如何查找到相似的問題。對于第一點,我們用API提供的Embedding表示,我們可以把它當作一個黑盒子,輸入任意長度的文本,輸出一個向量。查找相似問題則主是用到相似度算法,語義相似度一般用余弦距離來衡量。
??當然實際中可能會更加復雜一些,比如我們可能除了使用語義匹配,還會使用字詞匹配(經典的做法)。而且,一般都會找到前若干個最相似的,然后對這些結果進行排序,選出最可能的那個。不過,這個我們前面在講“ChatGPT提示詞”一節時舉過例子了,現在完全可以通過ChatGPT這樣的大模型API來解決,讓它幫你選出最好的那個。
2.3.2 聚類任務:物以類聚也以群分
??聚類的意思是把彼此相近的樣本聚集在一起,本質也是在使用一種表示和相似度衡量來處理文本。比如我們有大量的未分類文本,如果事先能知道有幾種類別,就可以用聚類的方法先將樣本大致分一下。
??這個例子我們使用Kaggle的DBPedia數據集:DBPedia Classes,數據集可以在Kaggle官網搜索下載。該數據集會對一段文本會給出三個不同層次級別的分類標簽,我們用第一層的類別作為示例。和上個任務一樣,依然是先讀取并查看數據。
import pandas as pd
df = pd.read_csv("./dataset/DBPEDIA_val.csv")
df.shape == (36003, 4)
df.head()
??數據如表2-2所示,第一列是文本,后面三列分別是三個層級的標簽。
表2-2 DBPedia數據集樣例
| text | l1 | l2 | l3 | |
|---|---|---|---|---|
| 0 | Li Curt is a station on the Bernina Railway li... | Place | Station | RailwayStation |
| 1 | Grafton State Hospital was a psychiatric hospi... | Place | Building | Hospital |
| 2 | The Democratic Patriotic Alliance of Kurdistan... | Agent | Organisation | PoliticalParty |
| 3 | Ira Rakatansky (October 3, 1919 – March 4, 201... | Agent | Person | Architect |
| 4 | Universitatea Re?i?a is a women handball club ... | Agent | SportsTeam | HandballTeam |
??接下來可以查看一下類別數量,value_counts可以統計出每個值的頻次,這里我們只看第一層的標簽。
df.l1.value_counts()
??結果如下所示,可以看到,Agent數量最多,Device數量最少。
Agent 18647
Place 6855
Species 3210
Work 3141
Event 2854
SportsSeason 879
UnitOfWork 263
TopicalConcept 117
Device 37
Name: l1, dtype: int64
??由于整個數據比較多,展示起來不太方便,我們隨機采樣200條。
sdf = df.sample(200)
sdf.l1.value_counts()
??隨機采樣使用sample接口,采樣數據的分布和采樣源是接近的。
Agent 102
Place 31
Work 22
Species 19
Event 12
SportsSeason 10
UnitOfWork 3
TopicalConcept 1
Name: l1, dtype: int64
??為了便于觀察,我們只保留3個數量差不多的類別:Place、Work和Species(類型太多時,樣本點會混在一塊難以觀察,讀者不妨自己嘗試一下)。
cdf = sdf[
(sdf.l1 == "Place") | (sdf.l1 == "Work") | (sdf.l1 == "Species")
]
cdf.shape == (72, 6)
??我們過濾了其他標簽,只保留了選定的3個,最終數據量是72條,相信觀察起來會非常直觀。
??由于需要把文本表示成向量,所以和上小節一樣,先把工具準備好。
from openai.embeddings_utils import get_embedding, cosine_similarity
import openai
import numpy as np
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
openai.api_key = OPENAI_API_KEY
??我們前面提到過,這個get_embeddingAPI可以支持多種模型(engine參數),它默認的是:text-similarity-davinci-001,我們這里用另一個:text-embedding-ada-002,它稍微快一些(維度比前面的少很多)。
cdf["embedding"] = cdf.text.apply(lambda x: get_embedding(x, engine="text-embedding-ada-002"))
??接下來用主成分分析算法(principal component analysis,PCA)進行特征降維,將原來的向量從1536維降到3維,便于顯示(超過3維就不好畫了)。
from sklearn.decomposition import PCA
arr = np.array(cdf.embedding.to_list())
pca = PCA(n_components=3)
vis_dims = pca.fit_transform(arr)
cdf["embed_vis"] = vis_dims.tolist()
arr.shape == (72, 1536), vis_dims.shape == (72, 3)
??可以看到,得到的vis_dims只有3維,這3個維度就是最主要的特征,我們可以說這3個特征能夠“大致上”代表所有的1536個特征。然后就是將所有數據可視化,也就是把三種類型的點(每個點3個維度)在三維空間中繪制出來。
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(subplot_kw={"projection": "3d"}, figsize=(8, 8))
cmap = plt.get_cmap("tab20")
categories = sorted(cdf.l1.unique())
# 分別繪制每個類別
for i, cat in enumerate(categories):
sub_matrix = np.array(cdf[cdf.l1 == cat]["embed_vis"].to_list())
x = sub_matrix[:, 0]
y = sub_matrix[:, 1]
z = sub_matrix[:, 2]
colors = [cmap(i/len(categories))] * len(sub_matrix)
ax.scatter(x, y, z, c=colors, label=cat)
ax.legend(bbox_to_anchor=(1.2, 1))
plt.show();
??結果如圖2-1所示。

圖2-1 聚類示意圖
??可以比較明顯地看出,3個不同類型的數據分別在空間的不同位置。如果我們事先不知道每個樣本是哪種標簽(但我們知道有3個標簽),這時候還可以通過KMeans算法將數據劃分為不同的族群,同族內相似度較高,不同族之間相似度較低。KMeans算法的基本思路如下。
- 隨機選擇K(此處為3)個點作為初始聚類中心。
- 將每個點分配到距離最近的那個中心。
- 計算每個族群的平均值,將其作為新的聚類中心。
- 重復上述步驟,直到聚類中心不再明顯變化為止。
??示例代碼如下:
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=3).fit(vis_dims)
??然后就可以通過kmeans.labels_得到每個數據的標簽了。
??聚類任務在實際中直接用作最終方案的情況不是特別多(主要是因為無監督方法精度有限),一般都會作為輔助手段對數據進行預劃分,用于下一步的分析。但有一些特定場景卻比較適合使用,比如異常點分析、客戶分群、社交網絡分析等。
2.3.3 推薦應用:一切都是Embedding
??我們在很多APP或網站上都能看到推薦功能。比如購物網站,每當你登陸或者選購一件商品后,系統就會給你推薦一些相關的產品。在這一小節中,我們就來做一個類似的應用,不過我們推薦的不是商品,而是文本,比如帖子、文章、新聞等。我們以新聞為例,基本邏輯如下。
- 首先要有一個基礎的文章庫,可能包括標題、內容、標簽等。
- 計算已有文章的Embedding并存儲。
- 根據用戶瀏覽記錄,推薦和瀏覽記錄最相似的文章。
??這次我們使用Kaggle的AG News數據集。上面的邏輯看起來好像和前面的QA差不多,事實也的確如此,因為它們本質上都是相似匹配問題。只不過QA使用的是用戶的問題去匹配已有QA庫,而推薦是使用用戶的瀏覽記錄去匹配。實際上推薦相比QA要更復雜一些,主要包括以下幾個方面。
- 剛開始用戶沒有記錄時如何推薦(一般被業界稱為冷啟動問題)。
- 除了相似還有其他要考慮的因素:比如熱門內容、新內容、內容多樣性、隨時間變化的興趣變化等等。
- 編碼問題(Embedding的輸入):我們應該取標題呢,還是文章,還是簡要描述或者摘要,或者考慮全部。
- 規模問題:推薦面臨的量級一般會遠超QA,除了橫向擴展機器,是否能從流程和算法設計上提升效率。
- 用戶反饋對推薦系統的影響問題:用戶反感或喜歡與文章本身并沒有直接關系,比如用戶喜歡體育新聞但討厭中國足球。
- 線上實時更新問題。
??當然,一個完整的線上系統要考慮的因素可能更多。列出這些只是希望讀者在設計一個方案時能夠充分調研和考慮,同時結合實際情況進行。反過來說,可能也并不需要考慮上面的每個因素。所以我們要活學活用,實際操作時充分理解需求后再動手實施。
??我們綜合考慮上面的因素給讀者一個比較簡單的方案,但務必注意,其中每個模塊的方案都不是唯一的。簡要設計方案如下。
- 用戶注冊登錄時,讓其選擇感興趣的類型(如體育、音樂、時尚等),我們通過這一步將用戶限定在幾個類別的范圍內(推薦時可以只考慮這幾個類別,提升效率),同時也可以用來解決冷啟動問題。
- 給用戶推薦內容時,根據用戶注冊時選擇的類別或瀏覽記錄對應的類別確認推薦類別后,接下來應依次考慮時效性、熱門程度、多樣性等。
- 考慮到性能問題,可以只編碼標題和摘要。
- 對大類別進一步細分,只在細分類別里進行相似度計算。
- 記錄用戶實時行為,如瀏覽、評論、收藏、點贊、轉發等。
- 動態更新內容庫,更新用戶行為庫。
??現實場景中最常用的是流水線方案:召回+排序。召回就是通過各種不同屬性或特征(如用戶偏好、熱點、行為等)先找到一批要推薦的列表。排序則是根據多樣性、時效性、用戶反饋、熱門程度等屬性對召回結果進行排序,將排在前面的優先推薦給用戶。我們這里只簡單展示召回。
??這個例子我們使用Kaggle的AG_News數據集:AG News Classification Dataset,數據集可以在Kaggle官網搜索下載。還是老樣子,先讀取并查看數據。
from dataclasses import dataclass
import pandas as pd
df = pd.read_csv("./dataset/AG_News.csv")
df.shape == (120000, 3)
df.head()
??數據如表2-3所示,共包含三列:類型、標題和描述。
表2-3 AG_News數據集樣例
| Class Index | Title | Description | |
|---|---|---|---|
| 0 | 3 | Wall St. Bears Claw Back Into the Black (Reuters) | Reuters - Short-sellers, Wall Street's dwindli... |
| 1 | 3 | Carlyle Looks Toward Commercial Aerospace (Reu... | Reuters - Private investment firm Carlyle Grou... |
| 2 | 3 | Oil and Economy Cloud Stocks' Outlook (Reuters) | Reuters - Soaring crude prices plus worries\ab... |
| 3 | 3 | Iraq Halts Oil Exports from Main Southern Pipe... | Reuters - Authorities have halted oil export\f... |
| 4 | 3 | Oil prices soar to all-time record, posing new... | AFP - Tearaway world oil prices, toppling reco... |
??用value_counts查看類型統計。
df["Class Index"].value_counts()
??一共4種類型,每個類型3萬條數據。
3 30000
4 30000
2 30000
1 30000
Name: Class Index, dtype: int64
??根據數據集介紹,四個類型分別是:1-World、2-Sports、3-Business、4-Sci/Tech。接下來,我們將使用上面介紹的知識來做一個簡單的流水線系統。為了便于展示,依然取100條樣本作為示例。
sdf = df.sample(100)
sdf["Class Index"].value_counts()
??樣本分布如下。
2 28
4 26
1 24
3 22
Name: Class Index, dtype: int64
??首先需要維護一個用戶偏好和行為記錄,我們將相關的數據結構創建為dataclass類。
from typing import List
@dataclass
class User:
user_name: str
@dataclass
class UserPrefer:
user_name: str
prefers: List[int]
@dataclass
class Item:
item_id: str
item_props: dict
@dataclass
class Action:
action_type: str
action_props: dict
@dataclass
class UserAction:
user: User
item: Item
action: Action
action_time: str
??創建幾條數據示例以便后面演示。
u1 = User("u1")
up1 = UserPrefer("u1", [1, 2])
i1 = Item("i1", {
"id": 1,
"catetory": "sport",
"title": "Swimming: Shibata Joins Japanese Gold Rush",
"description": "\
ATHENS (Reuters) - Ai Shibata wore down French teen-ager Laure Manaudou to win the women's 800 meters \
freestyle gold medal at the Athens Olympics Friday and provide Japan with their first female swimming \
champion in 12 years.",
"content": "content"
})
a1 = Action("瀏覽", {
"open_time": "2023-04-01 12:00:00",
"leave_time": "2023-04-01 14:00:00",
"type": "close",
"duration": "2hour"
})
ua1 = UserAction(u1, i1, a1, "2023-04-01 12:00:00")
??接下來計算所有文本的Embedding,這一步和之前一樣。
from openai.embeddings_utils import get_embedding, cosine_similarity
from sklearn.metrics.pairwise import cosine_similarity
import openai
import numpy as np
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
openai.api_key = OPENAI_API_KEY
sdf["embedding"] = sdf.apply(
lambda x: get_embedding(x.Title + x.Description, engine="text-embedding-ada-002"), axis=1)
??這里簡單起見,我們直接把標題Title和描述Description拼接。
??召回模塊涉及下面幾種不同的召回方式。
- 根據用戶行為記錄召回。首先獲取用戶行為記錄,一般在數據庫中查表可獲得。我們忽略數據庫操作,直接固定輸出。然后獲取用戶最感興趣的條目,可以選近一段時間內用戶瀏覽時間長、次數多,收藏、評論過的條目。最后根據用戶感興趣的條目進行推薦,這里和之前QA一樣——根據感興趣的條目,在同類別下找到相似度最高的條目。
- 根據用戶偏好召回。這一步比較簡單,我們在用戶偏好的類別下隨機選擇一些條目,這個往往會用在冷啟動上。
- 熱門召回。真實場景下這個肯定有一個動態列表的,我們這里就隨機選擇了。
import random
class Recall:
"""
召回模塊,代碼比較簡單,只是為了展示流程
"""
def __init__(self, df: pd.DataFrame):
self.data = df
def user_prefer_recall(self, user, n):
up = self.get_user_prefers(user)
idx = random.randrange(0, len(up.prefers))
return self.pick_by_idx(idx, n)
def hot_recall(self, n):
# 隨機選擇示例
df = self.data.sample(n)
return df
def user_action_recall(self, user, n):
actions = self.get_user_actions(user)
interest = self.get_most_interested_item(actions)
recoms = self.recommend_by_interest(interest, n)
return recoms
def get_most_interested_item(self, user_action):
idx = user_action.item.item_props["id"]
im = self.data.iloc[idx]
return im
def recommend_by_interest(self, interest, n):
cate_id = interest["Class Index"]
q_emb = interest["embedding"]
# 確定類別
base = self.data[self.data["Class Index"] == cate_id]
# 此處可以復用QA那一段代碼,用給定embedding計算base中embedding的相似度
base_arr = np.array(
[v.embedding for v in base.itertuples()]
)
q_arr = np.expand_dims(q_emb, 0)
sims = cosine_similarity(base_arr, q_arr)
# 排除掉自己
idxes = sims.argsort(0).squeeze()[-(n+1): -1]
return base.iloc[reversed(idxes.tolist())]
def pick_by_idx(self, category, n):
df = self.data[self.data["Class Index"] == category]
return df.sample(n)
def get_user_actions(self, user):
dct = {"u1": ua1}
return dct[user.user_name]
def get_user_prefers(self, user):
dct = {"u1": up1}
return dct[user.user_name]
def run(self, user):
ur = self.user_action_recall(user, 5)
if len(ur) == 0:
ur = self.user_prefer_recall(user, 5)
hr = self.hot_recall(3)
return pd.concat([ur, hr], axis=0)
??執行一下看看效果。
r = Recall(sdf)
rd = r.run(u1)
??我們得到8個條目,其中5個根據用戶行為推薦、3個熱門推薦,如表2-4所示。
表2-4 推薦結果列表
| Class Index | Title | Description | |
|---|---|---|---|
| 12120 | 2 | Olympics Wrap: Another Doping Controversy Surf... | ATHENS (Reuters) - Olympic chiefs ordered Hun... |
| 5905 | 2 | Saturday Night #39;s Alright for Blighty | Matthew Pinsents coxless four team, sailor Ben... |
| 29729 | 2 | Beijing Paralympic Games to be fabulous: IPC P... | The 13th Summer Paralympic Games in 2008 in Be... |
| 27215 | 2 | Dent tops Luczak to win at China Open | Taylor Dent defeated Australian qualifier Pete... |
| 72985 | 2 | Rusedski through in St Petersburg | Greg Rusedski eased into the second round of t... |
| 28344 | 3 | Delta pilots wary of retirements | Union says pilots may retire en masse to get p... |
| 80374 | 2 | Everett powerless in loss to Prince George | Besides the final score, there is only one sta... |
| 64648 | 4 | New Screening Technology Is Nigh | Machines built to find weapons hidden in cloth... |
??一個簡陋的推薦系統就做好了。需要再次說明的是,這只是一個大致的流程,而且只有召回。即便如此,實際場景中,上面的每個地方都需要優化。我們簡單羅列一些優化點供讀者參考。
- 建數據庫表(上面代碼中
get_開頭的方法實際上都是在查表),并處理增刪改邏輯。 - 將用戶、行為記錄等也Embedding化。這與文本無關,但確實是真實場景中的方案。
- 對“感興趣”模塊更多的優化,考慮更多行為和反饋,召回更多不同類型條目。
- 性能和自動更新數據的考慮。
- 線上評測,A/B測試等。
??可以發現,我們雖然只做了召回一步,但其中涉及到的內容已經遠遠不止之前QA那一點內容了,QA用到的知識在這里可能只是其中的一小部分。不過事無絕對,即便是QA任務也可能根據實際情況需要做很多優化。但總體來說,類似推薦這樣比較綜合的系統相對來說會更加復雜一些。
??后面的排序模塊需要區分不同的應用場景,可以做或不做。做也可以簡單或復雜做,比如簡單點就按發布時間,復雜點就綜合考慮多樣性、時效性、用戶反饋、熱門程度等多種因素。具體操作時,可以直接按相關屬性排序,也可以用模型排序。限于主題和篇幅,我們就不再探討了。
2.4 本章小結
??相似匹配是整個AI算法領域非常基礎和重要的任務,NLP、搜索、圖像、推薦等方向都涉及到此內容,而Embedding就是其中最重要的一項技術。通俗點來說,它就是把數據表示成空間中的一個點,通過稠密向量表示復雜語義信息。Embedding后,不同類型的數據就可以彼此進行交互、融合,從而得到更好的效果。即便強大如ChatGPT這樣的大模型,也依然需要Embedding技術來更好地獲取上下文。隨著多模態技術的不斷發展,Embedding在未來可能會變得更加重要。

【轉載】https://github.com/datawhalechina/hugging-llm
浙公網安備 33010602011771號