<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      第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)所示。

      \[\text{cosine}(v,w) = \frac {v·w}{|v||w|} = \frac {\displaystyle \sum_{i=1}^N v_iw_i} {\displaystyle \sqrt{\sum_{i=1}^N v_i^2} \sqrt{\sum_{i=1}^N w_i^2}} \qquad (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
      

      在這個例子中,我們給定兩個向量表示ab,然后用式(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輸出了我們想要的格式,但后兩句bc的結果并不是我們想要的。不過我們看它給出的解釋:“兩句話都是表達情感,但一個是積極情感,一個是消極情感,相似度略低。”這點和我們上一小節討論的關于“相似度”的觀點是類似的。不過,類似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是索引里的向量字段,這里是embeddingquery_embedding是參數字典中表示給定輸入Embedding的名稱,這里是query_embedding
      • AS score:表示K最近鄰算法計算結果的數據名稱為score。注意,這里的score其實是距離,不是相似度。換句話說,score越小,相似度越大。

      ??paging表示分頁,參數為:offsetnum,默認值為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個數量差不多的類別:PlaceWorkSpecies(類型太多時,樣本點會混在一塊難以觀察,讀者不妨自己嘗試一下)。

      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在未來可能會變得更加重要。

      posted @ 2024-04-17 11:32  3cH0_Nu1L  閱讀(3001)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 久久99国产精一区二区三区!| 亚洲自拍偷拍福利小视频| 海丰县| 女同亚洲精品一区二区三| 国产精品一码二码三码| 十八禁午夜福利免费网站| 欧美人与动牲交精品| 日韩av一区二区三区在线| 一区二区三区无码免费看| 无码内射成人免费喷射| 日韩精品理论片一区二区| 中文字幕无码av不卡一区| 久久精品夜色国产亚洲av| 无码人妻丝袜在线视频| 精品视频在线观看免费观看| 日韩免费无码视频一区二区三区| 欧美一级高清片久久99| 99久久精品久久久久久婷婷| 欧美性猛交xxxx免费看| 国产成人午夜福利在线播放| 深夜释放自己在线观看| 亚洲伊人五月丁香激情| 亚洲色av天天天天天天| 草草线在成年免费视频2| 一区二区三区精品不卡| 久久人人妻人人爽人人爽| 国产精品久久无码不卡黑寡妇| 国产超碰人人做人人爰| 巴楚县| 国产情侣激情在线对白| 欧美精品一区二区三区中文字幕| 久久久一本精品99久久精品88| 97se亚洲国产综合自在线观看| 亚洲 欧美 唯美 国产 伦 综合| 亚洲天堂领先自拍视频网| 国产超碰无码最新上传| 欧美巨大极度另类| 激情在线一区二区三区视频| 欧美成人精品在线| 亚洲国产精品色一区二区| 国产一区二区三区小说|