Learning Objectives
- 用矩陣乘法一次算出問題和所有文件的相似度,並取出前 k 名
- 用 recall@k 評估搜尋結果,說明 k 大小的取捨
- 寫一個簡單的中文關鍵字搜尋,並用真實結果比較它和向量搜尋各自擅長的問題
- 說明資料量多大時才需要向量資料庫與近似搜尋
在一整份手冊裡找答案
上一課會比較兩段文字了。實際應用常見的需求是:使用者問一個問題,要從幾十、幾千段文字裡找出最相關的幾段。
這一課用的教材是虛構的《優學大學學生手冊》,共 24 節,配上 16 個問題與標準答案。這份手冊是為了這門課編的,不是任何學校的規定。 做法分兩步:
- 事先處理(只做一次): 把每一節算成向量,存起來。
- 每次查詢: 把問題算成向量,和所有節比較,取分數最高的 k 節(top-k)。
全部比一遍的做法叫暴力搜尋(brute-force search),聽起來很笨,但用 numpy 寫只要幾行,而且結果一定是精確的。
逐步看:暴力搜尋
請看矩陣 M 和向量 q 的形狀,以及 M @ q 算出來的結果為什麼有 24 個數字。手冊的向量已經事先算好存在檔案裡,每次查詢只要多算問題這一個向量。
用 recall@k 評估搜尋結果
找對一題不代表整體可靠,要用一批有標準答案的問題來量。最常用的指標是 recall@k:
標準答案出現在前 k 名的題目,佔全部題目的比例。
k 越大越容易命中,但取回的內容也越多,後面送進模型的文字就越多、費用越高,也可能混進不相關的內容。
手冊的 16 題裡有 2 題是手冊沒有寫的(查無資料),沒有標準答案,所以這裡只算 14 題;查無資料的處理留到第 14 課。
逐步看:跑完全部問題
這次把問題也排成矩陣,一次算完。注意分數矩陣的形狀,以及標準答案名次的分布。
看沒找到的題目在錯什麼
光看 recall 數字不夠,要回去看每一題錯在哪裡:
| 題目 | 標準答案的名次 | 原因 |
|---|---|---|
| q01 感冒發燒沒辦法上課 | 第 2 名 | 第一名是學生健康中心,談生病的那一節分數更高 |
| q07 一百一十五學年度入學會被退學嗎 | 第 3 名 | 第一名是舊制 grade-2,問題的用字和舊制更接近;相似度看不出「入學年度」不適用 |
| q10 繳不出學費 | 第 5 名 | 就學貸款那一節沒有出現「繳不出」「學費」這些字眼 |
| q14 壓力很大睡不著 | 第 21 名 | 諮商中心那一節談的是預約與保密,手冊裡沒有壓力、失眠這些說法 |
這些失敗都有道理,而且不同題目的原因不同。接下來試另一種完全不看語意的方法:比較字面。
逐步看:字元雙連詞的關鍵字搜尋
中文沒有空格,切詞又是另一門學問。最簡單的做法是取相鄰的兩個字(字元雙連詞,character bigram),算問題和文件共有幾個。
逐步看:兩種方法比一比
用兩批問題比較。第一批是手冊的 14 題,多半是換句話說。第二批是另外寫的 16 題,刻意用手冊裡的原字(「UEDU-WiFi」「同意授權碼」「七天內」)。關鍵字分數已經用上一個範例的 bigrams() 算好存在檔案裡,因為全部逐步跑會太長。
誰贏取決於問題怎麼問
| 題目群 | k | 向量 | 關鍵字 |
|---|---|---|---|
| 手冊問題(14 題) | 1 | 10 | 8 |
| 3 | 12 | 10 | |
| 5 | 13 | 11 | |
| 精確字詞(16 題) | 1 | 11 | 16 |
| 3 | 13 | 16 | |
| 5 | 13 | 16 |
向量搜尋贏的題目: q11「家裡經濟困難但成績普通」、q13「書借了忘記還要罰多少錢」,問法和手冊用字不同,關鍵字幾乎對不到。q13 的關鍵字分數甚至是 0。
關鍵字贏的題目: x08「七天內要繳什麼」、x10「十五分鐘沒到會怎樣」、x11「同意授權碼是什麼」,向量搜尋分別排到第 8、9、6 名。數字、代碼、專有名詞這類內容,對 embedding 來說意思很接近或很模糊,字面比對卻一比就中。
有兩點要老實看:
- 精確字詞這 16 題是刻意用手冊原字寫的,對關鍵字有利,16 / 16 不代表關鍵字比較好。真實使用者兩種問法都會有。
- q01 關鍵字排第一,只是因為好幾節都同分(只有一個雙連詞「上課」),而標準答案剛好排在手冊最前面。這是運氣,不是能力。
實務上常把兩者合併(hybrid search),各取前幾名再一起排序;關鍵字那一側常用 BM25,它比單純計數多考慮了詞的稀有程度與文件長度。要不要合併,一樣用自己的問題集量過再決定。
資料量大才需要向量資料庫。 暴力搜尋的時間和文件數成正比。幾萬段文字以內,用 numpy 通常就夠了。資料量再大,或要一邊更新一邊查詢,才需要向量資料庫與近似最近鄰搜尋(approximate nearest neighbor,例如 HNSW):犧牲一點精確度換取速度,也就是有機會漏掉真正的第一名。這門課不實作,你只要知道暴力搜尋是它們要逼近的標準。
實作:top_k 函數
完成 top_k(scores, ids, k):回傳分數最高的 k 筆,每筆是 (編號, 分數),由高到低排列,分數用 round(float(...), 3) 取小數 3 位。
邊界情況: k 比資料筆數大時,回傳全部就好,不可以出錯。
問題的向量已經錄好,請不要改問題文字。
小測驗:向量搜尋
手冊有 24 節、每節向量 128 維。M 是手冊矩陣、q 是問題向量,(M @ q).shape 會印出什麼?
import numpy as np
M = np.zeros((24, 128))
q = np.zeros(128)
print((M @ q).shape)
本課手冊問題的 recall@1、@3、@5 是 10、12、13(共 14 題)。把 k 從 1 調到 5,有什麼代價?
使用者查「UEDU-WiFi」或「同意授權碼」這類專有名詞,本課的結果顯示什麼?
下面的程式會印出什麼?(a 是問題的雙連詞,b 是文件的雙連詞)
a = {"關門", "幾點"}
b = {"閉館", "時間", "幾點"}
print(len(a & b))
你有 3,000 段課程文件要做搜尋,同學建議馬上架一套向量資料庫。比較合理的回應是?
重點整理
- 暴力搜尋:文件向量事先算好排成矩陣,查詢時
M @ q算出全部分數,argsort取前 k 名。 - recall@k 是標準答案出現在前 k 名的比例;k 越大越容易命中,但取回的內容也越多。
- 向量搜尋擅長換句話說,關鍵字擅長數字、代碼與專有名詞;哪個好取決於你的使用者怎麼問,要用自己的問題集量。
- top-k 不管相關與否一定回傳 k 筆。
- 資料量大或要即時更新時,才需要向量資料庫與近似搜尋。
下一課: 我們一直用手冊裡切好的「節」來搜尋。如果手上只有一份長文件,要怎麼切?下一課看切法如何影響檢索結果。