手把手!基于領(lǐng)域預(yù)訓(xùn)練和對(duì)比學(xué)習(xí)SimCSE的語(yǔ)義檢索(附源碼)

之前看到有同學(xué)問,希望看一些偏實(shí)踐,特別是帶源碼的那種,安排!今天就手把手帶大家完成一個(gè)基于領(lǐng)域預(yù)訓(xùn)練和對(duì)比學(xué)習(xí)SimCSE的語(yǔ)義檢索小系統(tǒng)。
所謂語(yǔ)義檢索(也稱基于向量的檢索),是指檢索系統(tǒng)不再拘泥于用戶 Query 字面本身(例如BM25檢索),而是能精準(zhǔn)捕捉到用戶 Query 后面的真正意圖并以此來(lái)搜索,從而更準(zhǔn)確地向用戶返回最符合的結(jié)果。
最終可視化demo如下,一方面?可以獲取文本的向量表示;另一方面可以做文本檢索,即得到輸入Query的top-K相關(guān)文檔!

語(yǔ)義檢索,底層技術(shù)是語(yǔ)義匹配,是NLP最基礎(chǔ)常見的任務(wù)之一。之所以選題這個(gè),從廣度上看,語(yǔ)義匹配可以應(yīng)用到QA、搜索、推薦、廣告等各大方向;從技術(shù)深度上看,語(yǔ)義匹配需要融合各種SOTA模型、雙塔和交互兩種常用框架的魔改、以及樣本處理的藝術(shù)和各種工程tricks。
比較有趣的是,當(dāng)我在查相關(guān)資料的時(shí)候,發(fā)現(xiàn)百度飛槳PaddleNLP最近剛開源了類似的功能,可以的國(guó)貨之光!之前使用過PaddleNLP,基本覆蓋了NLP的各種應(yīng)用和SOTA模型,調(diào)用起來(lái)也非常方便,強(qiáng)烈推薦大家試試!
接下去我們以搜索場(chǎng)景為例,即輸入Query返回Document集合,基于PaddleNLP提供的輪子一步步搭建語(yǔ)義檢索系統(tǒng)。整體框架如下,由于計(jì)算量與資源的限制,一般工業(yè)界的搜索系統(tǒng)都會(huì)設(shè)計(jì)成多階段級(jí)聯(lián)結(jié)構(gòu),主要有召回、排序(粗排、精排、重排)等模塊,各司其職。
- step-1:利用預(yù)訓(xùn)練模型離線構(gòu)建候選語(yǔ)料庫(kù)
- step-2:召回模塊,對(duì)于在線查詢Query,利用Milvus快速檢索得到top1000候選集
- step-3:排序模塊,對(duì)于召回的top1000,再做更精細(xì)化的排序,得到top100結(jié)果返回給用戶。
語(yǔ)義檢索技術(shù)框架圖1、整體概覽
1.1 數(shù)據(jù)
數(shù)據(jù)來(lái)源于某文獻(xiàn)檢索系統(tǒng),分為有監(jiān)督(少量)和無(wú)監(jiān)督(大量)兩種,具體示例在下文介紹
- 數(shù)據(jù)下載地址:https://bj.bcebos.com/v1/paddlenlp/data/literature_search_data.zip
1.2 代碼
首先clone代碼:
git?clone[email protected]:PaddlePaddle/PaddleNLP.git??
cd?applications/neural_search
運(yùn)行環(huán)境是
- python3
- paddlepaddle==2.2.1
- paddlenlp==2.2.1
還有一些依賴包可以參考requirements.txt
2、離線建庫(kù)
從上面的語(yǔ)義檢索技術(shù)框架圖中可以看出,首先我們需要一個(gè)語(yǔ)義模型對(duì)輸入的Query/Doc文本提取向量,這里選用基于對(duì)比學(xué)習(xí)的SimCSE,核心思想是使語(yǔ)義相近的句子在向量空間中臨近,語(yǔ)義不同的互相遠(yuǎn)離。關(guān)于更具體的SimCSE介紹,可以閱讀我們之前的文章:
那么,如何訓(xùn)練才能充分利用好模型,達(dá)到更高的精度呢?對(duì)于預(yù)訓(xùn)練模型,一般常用的訓(xùn)練范式已經(jīng)從『通用預(yù)訓(xùn)練->領(lǐng)域微調(diào)』的兩階段范式變成了『通用預(yù)訓(xùn)練->領(lǐng)域預(yù)訓(xùn)練->領(lǐng)域微調(diào)』三階段范式。更多可以參看我們之前的
具體地,在這里我們的模型訓(xùn)練分為幾步(代碼和相應(yīng)數(shù)據(jù)在下一節(jié)介紹):
- 在無(wú)監(jiān)督的領(lǐng)域數(shù)據(jù)集上對(duì)通用ERNIE 1.0 進(jìn)一步領(lǐng)域預(yù)訓(xùn)練,得到領(lǐng)域ERNIE
- 以領(lǐng)域ERNIE為熱啟,在無(wú)監(jiān)督的文獻(xiàn)數(shù)據(jù)集上對(duì) SimCSE 做預(yù)訓(xùn)練
- 在有監(jiān)督的文獻(xiàn)數(shù)據(jù)集上結(jié)合In-Batch Negative策略微調(diào)步驟2模型,得到最終的模型,用于抽取文本向量表示,即我們所需的語(yǔ)義模型,用于建庫(kù)和召回。
由于召回模塊需要從千萬(wàn)量級(jí)數(shù)據(jù)中快速召回候選集合,通用的做法是借助向量搜索引擎實(shí)現(xiàn)高效 ANN,從而實(shí)現(xiàn)候選集召回。這里采用 Milvus 開源工具,關(guān)于Milvus的搭建教程可以參考官方教程
- https://milvus.io/cn/docs/v1.1.1/
Milvus 是一款國(guó)產(chǎn)高性能檢索庫(kù),?和Facebook 開源的 Faiss 功能類似,不熟悉的同學(xué)可以先閱讀?億級(jí)向量相似度檢索庫(kù)Faiss 原理+應(yīng)用?了解下。
離線建庫(kù)的代碼位于PaddleNLP/applications/neural_search/recall/milvus
|——?scripts
????|——?feature_extract.sh??#提取特征向量的bash腳本
├──?base_model.py?#?語(yǔ)義索引模型基類
├──?config.py??#?milvus配置文件
├──?data.py?#?數(shù)據(jù)處理函數(shù)
├──?embedding_insert.py?#?插入向量
├──?embedding_recall.py?#?檢索topK相似結(jié)果?/?ANN
├──?inference.py?#?動(dòng)態(tài)圖模型向量抽取腳本
├──?feature_extract.py?#?批量抽取向量腳本
├──?milvus_insert.py?#?插入向量工具類
├──?milvus_recall.py?#?向量召回工具類
├──?README.md
└──?server_config.yml?#?milvus的config文件,本項(xiàng)目所用的配置
2.1 抽取向量
依照Milvus教程搭建完向量引擎后,就可以利用預(yù)訓(xùn)練語(yǔ)義模型提取文本向量了。運(yùn)行feature_extract.py即可,注意修改需要建庫(kù)的數(shù)據(jù)源路徑。
運(yùn)行結(jié)束會(huì)生成1000萬(wàn)條的文本數(shù)據(jù),保存為corpus_embedding.npy。
2.2 插入向量
接下來(lái),修改config.py中的Milvus ip等配置,將上一步生成的向量導(dǎo)入到Milvus庫(kù)中。
embeddings=np.load('corpus_embedding.npy')?
embedding_ids?=?[i?for?i?in?range(embeddings.shape[0])]
client?=?VecToMilvus()
collection_name?=?'literature_search'
partition_tag?=?'partition_2'
data_size=len(embedding_ids)
batch_size=100000
for?i?in?tqdm(range(0,data_size,batch_size)):
????cur_end=i+batch_size
????if(cur_end>data_size):
????????cur_end=data_size
????batch_emb=embeddings[np.arange(i,cur_end)]
????status,?ids?=?client.insert(collection_name=collection_name,?vectors=batch_emb.tolist(),?ids=embedding_ids[i:i+batch_size],partition_tag=partition_tag)
抽取和插入向量?jī)刹剑绻麢C(jī)器資源不是很"富裕"的話,可能會(huì)花費(fèi)很長(zhǎng)時(shí)間。這里建議可以先用一小部分?jǐn)?shù)據(jù)進(jìn)行測(cè)試功能,快速感知,等真實(shí)部署的階段再進(jìn)行全庫(kù)的操作。
插入完成后,我們就可以通過Milvus提供的可視化工具[1]查看向量數(shù)據(jù),分別是文檔對(duì)應(yīng)的ID和向量。

3、文檔召回
召回階段的目的是從海量的資源庫(kù)中,快速地檢索出符合Query要求的相關(guān)文檔Doc。出于計(jì)算量和對(duì)線上延遲的要求,一般的召回模型都會(huì)設(shè)計(jì)成雙塔形式,Doc塔離線建庫(kù),Query塔實(shí)時(shí)處理線上請(qǐng)求。
召回模型采用 Domain-adaptive Pretraining + SimCSE + In-batch Negatives 方案。
另外,如果只是想快速測(cè)試或部署,發(fā)現(xiàn)PaddleNLP也貼心地開源了訓(xùn)練好的模型文件,下載即可用,這里直接貼出模型鏈接:
- 領(lǐng)域預(yù)訓(xùn)練ERNIE:https://bj.bcebos.com/v1/paddlenlp/models/ernie_pretrain.zip
- 無(wú)監(jiān)督SimCSE:https://bj.bcebos.com/v1/paddlenlp/models/simcse_model.zip
- 有監(jiān)督In-batch Negatives:https://bj.bcebos.com/v1/paddlenlp/models/inbatch_model.zip
3.1 領(lǐng)域預(yù)訓(xùn)練
Domain-adaptive Pretraining的優(yōu)勢(shì)在之前文章已有具體介紹,不再贅述。直接給代碼,具體功能都標(biāo)注在后面。
domain_adaptive_pretraining/
|——?scripts
????|——?run_pretrain_static.sh?#?靜態(tài)圖與訓(xùn)練bash腳本
├──?ernie_static_to_dynamic.py?#?靜態(tài)圖轉(zhuǎn)動(dòng)態(tài)圖
├──?run_pretrain_static.py?#?ernie1.0靜態(tài)圖預(yù)訓(xùn)練
├──?args.py?#?預(yù)訓(xùn)練的參數(shù)配置文件
└──?data_tools?#?預(yù)訓(xùn)練數(shù)據(jù)處理文件目錄3.2 SimCSE無(wú)監(jiān)督預(yù)訓(xùn)練
雙塔模型,采用ERNIE 1.0熱啟,引入 SimCSE 策略。訓(xùn)練數(shù)據(jù)示例如下
代碼結(jié)構(gòu)如下,各個(gè)文件的功能都有備注在后面,清晰明了。
simcse/
├──?model.py?#?SimCSE?模型組網(wǎng)代碼
|——?deploy
????|——?python
????????|——?predict.py?#?PaddleInference
????????├──?deploy.sh?#?Paddle?Inference的bash腳本
|——?scripts
????├──?export_model.sh?#?動(dòng)態(tài)圖轉(zhuǎn)靜態(tài)圖bash腳本
????├──?predict.sh?#?預(yù)測(cè)的bash腳本
????├──?evaluate.sh?#?召回評(píng)估bash腳本
????├──?run_build_index.sh??#?索引的構(gòu)建腳本
????├──?train.sh?#?訓(xùn)練的bash腳本
|——?ann_util.py?#?Ann?建索引庫(kù)相關(guān)函數(shù)
├──?data.py?#?無(wú)監(jiān)督語(yǔ)義匹配訓(xùn)練數(shù)據(jù)、測(cè)試數(shù)據(jù)的讀取邏輯
├──?export_model.py?#?動(dòng)態(tài)圖轉(zhuǎn)靜態(tài)圖
├──?predict.py?#?基于訓(xùn)練好的無(wú)監(jiān)督語(yǔ)義匹配模型計(jì)算文本?Pair?相似度
├──?evaluate.py?#?根據(jù)召回結(jié)果和評(píng)估集計(jì)算評(píng)估指標(biāo)
|——?inference.py?#?動(dòng)態(tài)圖抽取向量
|——?recall.py?#?基于訓(xùn)練好的語(yǔ)義索引模型,從召回庫(kù)中召回給定文本的相似文本
└──?train.py?#?SimCSE?模型訓(xùn)練、評(píng)估邏輯
對(duì)于訓(xùn)練、評(píng)估和預(yù)測(cè)分別運(yùn)行scripts目錄下對(duì)應(yīng)的腳本即可。訓(xùn)練得到模型,我們一方面可以用于提取文本的語(yǔ)義向量表示,另一方面也可以用于計(jì)算文本對(duì)的語(yǔ)義相似度,只需要調(diào)整下數(shù)據(jù)輸入格式即可。
3.3 有監(jiān)督微調(diào)
對(duì)上一步的模型進(jìn)行有監(jiān)督數(shù)據(jù)微調(diào),訓(xùn)練數(shù)據(jù)示例如下,每行由一對(duì)語(yǔ)義相似的文本對(duì)組成,tab分割,負(fù)樣本來(lái)源于引入In-batch Negatives采樣策略。
關(guān)于In-batch Negatives?的細(xì)節(jié),可以參考之前的文章:
整體代碼結(jié)構(gòu)如下
|——?data.py?#?數(shù)據(jù)讀取、數(shù)據(jù)轉(zhuǎn)換等預(yù)處理邏輯
|——?base_model.py?#?語(yǔ)義索引模型基類
|——?train_batch_neg.py?#?In-batch?Negatives?策略的訓(xùn)練主腳本
|——?batch_negative
????|——?model.py?#?In-batch?Negatives?策略核心網(wǎng)絡(luò)結(jié)構(gòu)
|——?ann_util.py?#?Ann?建索引庫(kù)相關(guān)函數(shù)
|——?recall.py?#?基于訓(xùn)練好的語(yǔ)義索引模型,從召回庫(kù)中召回給定文本的相似文本
|——?evaluate.py?#?根據(jù)召回結(jié)果和評(píng)估集計(jì)算評(píng)估指標(biāo)
|——?predict.py?#?給定輸入文件,計(jì)算文本?pair?的相似度
|——?export_model.py?#?動(dòng)態(tài)圖轉(zhuǎn)換成靜態(tài)圖
|——?scripts
????|——?export_model.sh??#?動(dòng)態(tài)圖轉(zhuǎn)換成靜態(tài)圖腳本
????|——?predict.sh??#?預(yù)測(cè)bash版本
????|——?evaluate.sh?#?評(píng)估bash版本
????|——?run_build_index.sh?#?構(gòu)建索引bash版本
????|——?train_batch_neg.sh??#?訓(xùn)練bash版本
|——?deploy
????|——?python
????????|——?predict.py?#?PaddleInference
????????|——?deploy.sh?#?Paddle?Inference部署腳本
|——?inference.py?#?動(dòng)態(tài)圖抽取向量
訓(xùn)練、評(píng)估、預(yù)測(cè)的步驟和上一步無(wú)監(jiān)督的類似,聰明的你肯定一看就懂了!
3.4?語(yǔ)義模型效果
前面說了那么多,來(lái)看看幾個(gè)模型的效果到底怎么樣?對(duì)于匹配或者檢索模型,常用的評(píng)價(jià)指標(biāo)是Recall@K,即前TOP-K個(gè)結(jié)果檢索出的正確結(jié)果數(shù)與全庫(kù)中所有正確結(jié)果數(shù)的比值。

對(duì)比可以發(fā)現(xiàn),首先利用ERNIE 1.0做Domain-adaptive Pretraining,然后把訓(xùn)練好的模型加載到SimCSE上進(jìn)行無(wú)監(jiān)督訓(xùn)練,最后利用In-batch Negatives 在有監(jiān)督數(shù)據(jù)上進(jìn)行訓(xùn)練能獲得最佳的性能。
3.5 向量召回
終于到了召回,回顧一下,在這之前我們已經(jīng)訓(xùn)練好了語(yǔ)義模型、搭建完了召回庫(kù),接下來(lái)只需要去庫(kù)中檢索即可。代碼位于PaddleNLP/applications/neural_search/recall/milvus/inference.py
def?search_in_milvus(text_embedding):
????collection_name?=?'literature_search'??#?之前搭建好的Milvus庫(kù)
????partition_tag?=?'partition_2'
????client?=?RecallByMilvus()
????status,?results?=?client.search(collection_name=collection_name,?vectors=text_embedding.tolist(),
????????????????????????????????????partition_tag=partition_tag)
????corpus_file?=?"../../data/milvus/milvus_data.csv"
????id2corpus?=?gen_id2corpus(corpus_file)
????for?line?in?results:
????????for?item?in?line:
????????????idx?=?item.id
????????????distance?=?item.distance
????????????text?=?id2corpus[idx]
????????????print(idx,?text,?distance)
以輸入 國(guó)有企業(yè)引入非國(guó)有資本對(duì)創(chuàng)新績(jī)效的影響——基于制造業(yè)國(guó)有上市公司的經(jīng)驗(yàn)證據(jù) 為例,檢索返回效果如下
返回結(jié)果的最后一列為相似度,Milvus默認(rèn)使用的是歐式距離,如果想換成余弦相似度,可以在Milvus的配置文件中修改。
4、文檔排序
不同于召回,排序階段由于面向的打分集合相對(duì)小很多,一般只有幾千級(jí)別,所以可以使用更復(fù)雜的模型,這里采用 ERNIE-Gram 預(yù)訓(xùn)練模型,loss選用 margin_ranking_loss。
訓(xùn)練數(shù)據(jù)示例如下,三列,分別為(query,title,neg_title),tab分割。對(duì)于真實(shí)搜索場(chǎng)景,訓(xùn)練數(shù)據(jù)通常來(lái)源業(yè)務(wù)線上的點(diǎn)擊日志,構(gòu)造出正樣本和強(qiáng)負(fù)樣本。
代碼結(jié)構(gòu)如下
ernie_matching/
├──?deply?#?部署
????└──?python
????????├──?deploy.sh?#?預(yù)測(cè)部署bash腳本
????????└──?predict.py?#?python?預(yù)測(cè)部署示例
|——?scripts
????├──?export_model.sh?#?動(dòng)態(tài)圖參數(shù)導(dǎo)出靜態(tài)圖參數(shù)的bash文件
????├──?train_pairwise.sh?#?Pair-wise?單塔匹配模型訓(xùn)練的bash文件
????├──?evaluate.sh?#?評(píng)估驗(yàn)證文件bash腳本
????├──?predict_pairwise.sh?#?Pair-wise?單塔匹配模型預(yù)測(cè)腳本的bash文件
├──?export_model.py?#?動(dòng)態(tài)圖參數(shù)導(dǎo)出靜態(tài)圖參數(shù)腳本
├──?model.py?#??Pair-wise?匹配模型組網(wǎng)
├──?data.py?#??Pair-wise?訓(xùn)練樣本的轉(zhuǎn)換邏輯?、Pair-wise?生成隨機(jī)負(fù)例的邏輯
├──?train_pairwise.py?#?Pair-wise?單塔匹配模型訓(xùn)練腳本
├──?evaluate.py?#?評(píng)估驗(yàn)證文件
├──?predict_pairwise.py?#?Pair-wise?單塔匹配模型預(yù)測(cè)腳本,輸出文本對(duì)是相似度
訓(xùn)練運(yùn)行sh scripts/train_pairwise.sh即可。
同樣,PaddleNLP也開源了排序模型,https://bj.bcebos.com/v1/paddlenlp/models/ernie_gram_sort.zip
對(duì)于預(yù)測(cè),準(zhǔn)備數(shù)據(jù)為每行一個(gè)文本對(duì),最終預(yù)測(cè)返回文本對(duì)的語(yǔ)義相似度。
{'query':?'中西方語(yǔ)言與文化的差異',?'title':?'第二語(yǔ)言習(xí)得的一大障礙就是文化差異。',?'pred_prob':?0.85112214}
{'query':?'中西方語(yǔ)言與文化的差異',?'title':?'跨文化視角下中國(guó)文化對(duì)外傳播路徑瑣談跨文化,中國(guó)文化,傳播,翻譯',?'pred_prob':?0.78629625}
{'query':?'中西方語(yǔ)言與文化的差異',?'title':?'從中西方民族文化心理的差異看英漢翻譯語(yǔ)言,文化,民族文化心理,思維方式,翻譯',?'pred_prob':?0.91767526}
{'query':?'中西方語(yǔ)言與文化的差異',?'title':?'中英文化差異對(duì)翻譯的影響中英文化,差異,翻譯的影響',?'pred_prob':?0.8601749}
{'query':?'中西方語(yǔ)言與文化的差異',?'title':?'淺談文化與語(yǔ)言習(xí)得文化,語(yǔ)言,文化與語(yǔ)言的關(guān)系,文化與語(yǔ)言習(xí)得意識(shí),跨文化交際',?'pred_prob':?0.8944413}
5、總結(jié)
本文我們基于PaddleNLP提供的Neural Search功能自己快速搭建了一套語(yǔ)義檢索系統(tǒng)。相對(duì)于自己從零開始,PaddleNLP非常好地提供了一套輪子。如果直接下載PaddleNLP開源訓(xùn)練好的模型文件,對(duì)于語(yǔ)義相似度任務(wù),調(diào)用現(xiàn)成的腳本幾分鐘即可搞定;對(duì)于語(yǔ)義檢索任務(wù),需要將全量數(shù)據(jù)導(dǎo)入Milvus構(gòu)建索引,除訓(xùn)練和建庫(kù)時(shí)間外,整個(gè)流程預(yù)計(jì)30-50分鐘即可完成。
在訓(xùn)練的間隙還研究了下,發(fā)現(xiàn)Github上的文檔也很清晰詳細(xì)啊。對(duì)于小白入門同學(xué),做到了一鍵運(yùn)行,不至于被繁雜的流程步驟困住而逐漸失去興趣;對(duì)于想要深入研究的同學(xué),PaddleNLP也開源了代碼,可以進(jìn)一步學(xué)習(xí)。贊!
另外我們還可以基于這些功能進(jìn)行自己額外的開發(fā),譬如開篇的動(dòng)圖,搭建一個(gè)更直觀的語(yǔ)義向量生成和檢索服務(wù)。Have Fun!
在跑代碼過程中也遇到一些問題,非常感謝Paddle同學(xué)的耐心解答。并且得知他們最近好像會(huì)針對(duì)這個(gè)項(xiàng)目開一次課,免費(fèi)的噢,這太香了!為感謝熱心解答,這里幫忙宣傳一下他們的公開課,歡迎感興趣的同學(xué)去聽呀!
12.28-12.30日,百度工程師將帶來(lái)直播講解,除語(yǔ)義檢索系統(tǒng)外,還將帶來(lái)問答、情感分析場(chǎng)景的系統(tǒng)方案,以及落地經(jīng)驗(yàn)分享。可以掃碼進(jìn)入課程群了解詳情報(bào)名
最后附上本次實(shí)踐項(xiàng)目的代碼:https://github.com/PaddlePaddle/PaddleNLP/tree/develop/applications/neural_search
本文參考資料
[1]可視化工具: https://zilliz.com/products/em
-?END?-


百度發(fā)布PLATO-XL,全球首個(gè)百億參數(shù)中英文對(duì)話預(yù)訓(xùn)練生成模型
EMNLP 2021 | 百度:多語(yǔ)言預(yù)訓(xùn)練模型ERNIE-M

