昨天介紹了 BM25 的概念,它從 TF-IDF 發展而來,可以找出和使用者提問最相符的多筆文檔,在資訊檢索的領域中非常實用,今天就來實作看看吧!
網路上有很多開源的工具和套件可以使用,而我選的是 Pyserini 提供的工具,它是基於 Lucene 函式庫構建而成的,非常好實作。
首先,我讓 ChatGPT 生成了涵蓋體育、心理、自然等 20 筆關於不同主題的文章,用 json 格式儲存,因為接下來的實作會在 Colab 上執行,所以我把它放在 google 雲端上了,大家想要玩玩看的話可以去這個連結把 documents.json 載下來放在自己的雲端上。
我們這次想要實現的目的是讓使用者輸入一段文字,BM25 會找到和這段文字最相符的文章。那麼就開始吧!
首先要建立一個 Colaboratory 的 .ipynb 檔,檔案名稱可以自行命名,然後執行這段程式碼把路徑移到這個檔案所在的資料夾:
from google.colab import drive
drive.mount('/content/drive', force_remount = True)
%cd drive/MyDrive/yourpath # 你的檔案所在路徑
如果不知道自己的檔案在哪裡的話,可以到側邊欄檔案的地方找到路徑,複製貼上就好。
然後把剛剛放上雲端的 documents.json 打開來,如果在同一個資料夾底下的話可以直接執行:
import os, json
drive.mount('/content/drive', force_remount = True)
f = open('documents.json') # 請先確認檔案路徑正確
documents = json.load(f)
f.close()
如果打開檔案檢視的話,它的格式會長這樣:
[
{
'topic': 'Swimming',
'en': 'Swimming is a fantastic way to stay fit and healthy. It works out your entire body, improving cardiovascular health, building muscle, and enhancing flexibility. Swimming is also a low-impact exercise, making it ideal for people of all ages and fitness levels. Whether in a pool or the ocean, the rhythmic nature of swimming can be incredibly relaxing and therapeutic.',
'zh': '游泳是一種保持健康和身體健康的極好方式。它可以鍛煉全身,改善心血管健康,增強肌肉,增強柔韌性。游泳也是一種低衝擊的運動,適合所有年齡和體能水平的人。不論是在泳池還是海洋中,游泳的節奏性都可以非常放鬆和治療。'
},
...
]
不過這個還不是我們要用來檢索的資料,而且為了要符合 pyserini 建立索引所需的格式,需要先做一些修改。假設我們只想要檢索英文文檔,就要把英文的部分提取出來,然後為每一個文檔賦予 id 和我們想要的 contents,最後另外創建一個資料夾 corpus
存進去:
id_cnt = 0
data = []
for doc in documents:
data.append({"id":id_cnt,"contents":doc['en']}) # 中文請改成 'zh'
id_cnt += 1
!mkdir corpus
with open('./corpus/en.json', 'w', encoding = 'utf-8') as f:
json.dump(data, f, ensure_ascii = False)
這裡要補充一下,如果是檢索中文文檔的話,要把 en
修改成 zh
。
事前任務做完之後,我們要來玩玩看 Pyserini 吧!首先要把該下載的套件載下來,我自己測試的結果 0.22.1 版本是沒有問題的:
!pip install faiss-cpu pyserini==0.22.1 jq
再來是建立反向索引,這一步比較重要,因為在建立完成之後,就可以一直重複使用。我們可以按照官方文檔的範例做做看,這段程式碼的目的主要是對建立索引進行一些設定,比方說 input 的部分因為我們把文檔放在自己建立的 corpus
資料夾底下,所以要修改,index 的部分代表反向索引要存的位置,我們把它的路徑設定成 indexes
,其他的部分就和官方原始文檔的範例一模一樣:
!python -m pyserini.index.lucene \
--collection JsonCollection \
--input corpus \
--index indexes \
--generator DefaultLuceneDocumentGenerator \
--threads 1 \
--storePositions --storeDocvectors --storeRaw
同樣要注意的是,如果檢索的文檔是中文的話,要加上 --language zh
這一行指令。
點擊執行之後,沒有問題的話最後會顯示這樣的訊息,代表索引建立成功了!
然後我們使用 LuceneSearcher 來檢索,這裡的 indexes
就對應到我們剛剛建立反向索引所存的位置,query 是使用者輸入的文字,參數 k 代表的是要取到第幾名的文檔,比方說我設定 k 為 3,那最後就會回傳 3 筆相關性最高的文檔。
在檢索結果中,docid 和 score 分別是文章的 ID 和它所獲得的分數,data 是之前在 corpus 中處理好的英文文檔,我們把它用 dictionary 包起來回傳:
from pyserini.search.lucene import LuceneSearcher
def Retriever(query):
searcher = LuceneSearcher('indexes')
hits = searcher.search(query, k = 3)
result = [{
"id": hit.docid,
"score": hit.score,
"contents": data[int(hit.docid)]["contents"]
} for hit in hits]
return result
最後,只要呼叫 Retriever
函式並輸出結果就好了:
result = Retriever("Which science helps improve our mental state and interpersonal interactions?")
for i in result:
print(i)
{'id': '4', 'score': 3.0113000869750977, 'contents': 'Psychology is the scientific study of the mind and behavior. It explores how people think, feel, and act both individually and in groups. Understanding psychology can help improve mental health, relationships, and overall well-being. It encompasses various subfields, including cognitive, developmental, social, and clinical psychology, each offering insights into different aspects of human experience.'}
{'id': '10', 'score': 2.004699945449829, 'contents': 'Nature is the essence of our planet, encompassing the landscapes, wildlife, and ecosystems that make Earth unique. Spending time in nature has numerous benefits, including reducing stress and enhancing mental well-being. From majestic mountains to serene forests, nature offers endless beauty and inspiration. Protecting our natural environment is crucial for sustaining life on Earth.'}
{'id': '8', 'score': 1.398900032043457, 'contents': 'Gardening is a relaxing and rewarding hobby that connects you with nature. It involves cultivating plants, flowers, and vegetables, which can be therapeutic and satisfying. Gardening promotes physical activity, reduces stress, and provides a sense of accomplishment. Watching your garden grow and flourish brings a unique joy and a deeper appreciation for the environment.'}
在我的設定中,這個問題的確和心理學最相關,所以 BM25 成功找到了正確的文檔。
此外,如果你輸入的問題和被檢索文章中的單詞重複越高的話,它的 score 就會大幅上升,有興趣的可以去找規模較大的檢索資料集,我記得有看過 Wiki 的資料集,裡面就包含了好幾十萬筆的文章。
以上就是簡單的 BM25 實作,Pyserini 還提供了其他很多檢索方面的工具,可以去它的 github 官方文檔了解更多。
從 Day 11 到 Day 14 我們介紹了 TF-IDF 和 BM25 兩種檢索方式,它們可以解決 Boolean Retrieval 的缺陷,不僅找出相關文檔,還能給他們打分數並進行排名,這種檢索方式我們把它叫做 Rank Retrieval。
然而,這不代表 BM25 就是最好的檢索方式了,雖然它曾經稱霸了一陣子,但還是存在著一些問題。舉例來說,如果使用者輸入 I love football
,而檢索文檔中只有 I like soccer
,在我們看來兩者應該要是相似的,然而因為 BM25 只能處理一模一樣的單詞,它會認為 love
和 like
沒有關係,football
和 soccer
也沒有關係,導致這篇文檔幾乎找不到。
因此,後來也有提出利用密集向量判斷使用者提問和文章相似度的方式來進行檢索,這種檢索方式我們把它叫做密集檢索 ( Dense Retrieval ),它能夠捕捉單詞之間的語意關係,檢索表現普遍比像 BM25 這樣的稀疏檢索 ( Sparse Retrieval ) 來的好,但是執行時間也更長,所以通常會用在較小規模的檢索任務上。
詞向量 ( Word Vector ) 的部分我們會在檢索講完之後開始,明天想要聊的是如何評估檢索的結果好不好,我發現這個部分還蠻有趣的,有很多值得討論的地方。