一篇長文學懂入門推薦演算法庫:surprise
- 2020 年 10 月 29 日
- AI
不知不覺寫完了七篇文章來分析這個推薦演算法庫 surprise,基本上我們從頭到尾所有程式碼都自己完成,並且可以成功 run 起來一個基於鄰域的協同過濾演算法。是哪個演算法這件事其實我覺得不重要,surprise 支援的每個演算法本身思路並不複雜,程式碼也不晦澀難懂,我們主要的目的是理解它的架構,學習框架各個部分的交互。
所以這篇文章是想從一個整體的視角,以我當初的思路為主線進行介紹,觀察並思考如何一步一步的讓模型 run 起來。至於某些具體的細節部分,我們給出之前寫的對應文章鏈接,大家可以回頭再去看相應的文章來了解。
1 先搞個模型跑起來
我們首先從一個總體性的程式碼看一下,很簡單的幾行程式碼,開始我們的 surprise 之旅。
這裡需要導入的部分,我都已經重寫過了,但是大家可以在自己本地的程式碼上嘗試一下,直接利用 surprise 庫就可以運行一個簡單的 KNN 演算法,本質上也就是基於鄰域的協同過濾演算法。按照我在程式碼上標註出來的紅線部分,分別給對應的四個 import 模組,前面加上 surpris. 的路徑就可以從 surprise 中進行導入。
這裡需要提一下,我用的是自己之前已經在 movielens 官網下載的數據集,大家可以自己直接下載,也可以在網上找一下教程,surprise 支援自動下載 movielens 的數據集。
def surprise_code():
reader = Reader(line_format="user item rating", sep=',', skip_lines=1)
data = Dataset.load_from_file('./ml-latest-small/ratings.csv', reader)
algo = KNNBasic()
perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=2, verbose=0)
for keys in perf:
print(keys, ": ", perf[keys])
對核心的 surprise_code() 函數,可以分為兩個部分來看:數據載入,演算法執行並檢測結果。
數據載入,由 Reader 和 Dataset 兩個類來提供功能,具體的思路是由 Reader() 提供讀取數據的格式,然後 Dataset 按照 Reader 的設置來完成對數據的載入。
演算法執行並檢測結果,這裡由一個 cross_validate() 來完成,提前導入需要執行的演算法並實例化,然後將數據,演算法,要檢測的指標等都傳入 cross_validate(),它會完成對演算法的訓練擬合,然後進行預測結果,再對結果進行驗證,最終返回目標的檢測指標對應的結果。
所以我們可以看到直接調用介面是很容易跑起來一個模型的,僅這麼幾行簡單的程式碼就可以將一個演算法完整的運行起來。但是如果要深入到程式碼的執行細節上,就需要捋順它們的關係,然後抽絲剝繭一點點展開。
在【第一篇文章:推薦實踐(1):從零開始寫一個自己的推薦演算法庫】中,我們將上面的演算法執行並檢測結果分成了兩部分,也就是將整個工作流程劃分為三部分:數據載入,演算法設計,結果評估。這樣子更加細化的一步當然沒問題,邏輯上這樣子也更容易理解。
現在捋順了演算法的執行思路以後,我們開始從數據集的載入開始去分析源碼。
trick:在正式細節程式碼前,分享一些關於我學習源碼的方法,不一定適合所有人,也不一定適合所有源碼,僅供大家參考:
最後,不要在分析的時候,拘泥於細節,先完成該模組的主要功能。如果這部分功能與其它模組相關,可以先導入相關的模組,直接使用。不要嘗試一步將某一個模組寫的盡善盡美。
這三個提到的內容,我們在接下來的文章可以再進一步體會。
2 從第一部分的數據載入開始
我們前面分析了數據載入部分,由 Reader 和 Dataset 兩個類來提供功能。接下來要做的就是捋順這部分內容,然後自己嘗試寫出來對應的模組,並且替代進去,看看我們前面的程式碼能不能繼續正常運行。
reader = Reader(line_format="user item rating", sep=',', skip_lines=1)
對於這個 Reader() 類,主要的功能是設置一個讀取器。從 Reader 的使用也可以看出來,要求的輸入是每行的格式,每行的分隔符,要忽略的行數。
從這個類實例時的輸入上,我們可以判斷出來,這個 Reader() 類的作用是構造一個讀取器對象 reader,這個讀取器 reader 包含了一些如何去讀數據的屬性。比如 reader 知道每行的數據是按照 「user item rating」 來分布的,知道每行數據由符號 “,” 分割開,知道第一行的數據應該被跳過。
所以我們在構建了這個 reader 以後,就可以將它傳給 Dataset() 類,來輔助我們從數據集中,按照我們想要的格式讀取出來數據內容。
data = Dataset.load_from_file('./ml-latest-small/ratings.csv', reader)
由於這裡我們選擇了使用自己已經下載的數據集,調用的就是 Dataset.load_from_file() 方法。可以看到的是,這個方法的輸入有兩個參數,第一個是數據集的路徑,第二個就是剛剛實例化的讀取器 reader。
所以這個 load_from_file() 在讀取數據時,就會按照 reader 的定義的格式來讀取,最終返回一個自定義的數據格式。其實如果看了程式碼,我們可以看到這裡返回的數據格式是:dataset.DatasetAutoFolds,但是正如我們前面說的,對於源碼不要陷入細節。我們知道這兩步對原始的數據集文件進行了處理,得到了後續可以處理的數據格式,就 OK 啦。
3 進行結果交叉驗證
在完成數據集的載入以後,我們選擇的是利用 cross_validate() 執行演算法並交叉檢驗。這一部分的內容,我們分為兩部分去介紹。
首先忽略掉演算法的實現,直接調用演算法的介面。這也是一個很實用的 trick,適當的時候忽略掉一些程式碼實現,即使你接下來要用到它,也可以直接調源碼的介面。所以我們這裡忽略了 KNN 演算法的實現,直接調用它來實現訓練擬合以及後續在測試集上的預測。
那麼 cross_validate() 裡面是什麼呢?我們看一下 validate 中的內容:
validate 中有兩個函數,分別是 cross_validate() 和 fit_and_score()。我們簡單的介紹一下它們的功能,讓大家可以繼續沒有障礙的閱讀當前這篇文章,至於具體的程式碼和功能分析可以看【第三篇文章:推薦實踐(3):調用演算法介面實現一個 demo】。
algo = KNNBasic()perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=2, verbose=0)
可以看到 cross_validate() 是被調用的外部介面,很容易可以猜到,fit_and_score() 是在 cross_validate() 中被調用的。
對 cross_validate() 而言,它的輸入有演算法對象,數據集,需要測量的指標,交叉驗證的次數等。這裡簡單的介紹一下它的內部邏輯。它對輸入的數據 data,分成 cv 份,然後每次選擇其中一份作為測試集,其餘的作為訓練集。在數據集劃分完後,對它們分別調用 fit_and_score(),去進行演算法擬合。
這裡注意一個小細節,對數據集的劃分不是靜態全部劃分完,然後分別在數據集上進行訓練和驗證,而是利用輸入的 data 構造一個生成器,每次拋出一組劃分完的結果。
對 fit_and_score() 函數,它對輸入的演算法在輸入的訓練集上進行擬合,然後在輸入的測試集上進行驗證,再計算需要的指標。
在進行到這裡的時候,同樣忽略掉對預測結果進行指標測量的步驟,直接調用 surprise 中的 accuracy 來進行處理。當然到後面我們會補充這些內容,這裡要注意的重點是如何進行交叉驗證。
這一部分內容的核心是,在有了我們第 2 節輸入的數據後,該如何進行數據集的劃分以及如何進行演算法的訓練和驗證。所以我們關注的重點是數據集在進行 k 折交叉驗證時如何劃分,又如何調用介面完成演算法在數據集上的訓練和測試。
說這一段的意思是想告訴大家,在閱讀源碼,或者仿寫源碼時,需要把握住自己在這一步驟的核心思路,屏蔽掉暫時不重要,或者對當前步驟不是很關鍵的內容。哪怕只是調用各個介面來完成自己當前步驟的任務,只要你把握住了它們的交互關係,處理流程就 OK。
4 再補充忽略的演算法部分
打開上面的 fit_and_score() 函數,我們可以看到對 algo 的使用只有兩句程式碼,一個是訓練階段的 algo.fit(trainset) ,一個是測試階段的 algo.test(testset)。前者是對演算法在訓練集上進行擬合,而後者是對演算法在測試集上進行測試。
針對 knn 的演算法而言,演算法的實現上是比較簡單的。主要包括了一個 fit() 方法,和一個 estimate() 方法。這裡主要是需要對類間的繼承關係進行梳理。具體的實現細節可以看【第四篇文章:推薦實踐(4):從KNNBasic() 了解整個演算法部分的結構梳理】。
在 surprise 中,所有的演算法類都繼承於一個父類:algo_base(),這個類中抽象出來了一些子類都容易用到的方法,有的給出了具體的實現,有的只是抽象出了一個介面,如 fit() 方法,在 algo_base() 中和其子類 knn() 中都有定義。具體的 algo_base() 的組成可以在【第五篇文章:推薦實踐(5):Algo_base() 類的功能介紹】中進行了解。
這裡幫助大家再進一步梳理演算法類之間的關係。對 algo_base() 而言,其是一切 surprise 中的演算法的父類。那麼以 knn 演算法為例,這裡就不是由 knn() 直接繼承 algo_base() 了,而是先有一個 SymmetricalAlgo() 類繼承 algo_base(),然後由對應的 knn() 類繼承 SymmetricalAlgo() 類。
這裡的 SymmetricalAlgo() 的主要工作是處理 knn 中經常需要考慮的一個問題。基於用戶還是基於物品的協同過濾。SymmetricalAlgo() 中主要有兩個方法,一個是前面一直提到的 fit() 方法,這裡的 fit() 方法主要是對 n_x 和 n_y 等做出調整,判斷是基於用戶相似性還是基於物品相似性,然後調整對應的數據指標。另外一個方法則是 switch() 方法,顧名思義,switch() 方法同樣是調整對應的指標,調整的依據則是判斷是基於用戶相似性還是物品相似性,調整後的結果用來在測試時使用。
這裡用語言描述可能稍微有點繞口,大家看一下【第四篇文章:推薦實踐(4):從KNNBasic() 了解整個演算法部分的結構梳理】結合源碼與文檔,就可以非常容易理解了。
關於其它的演算法我們就不一一展開去分析了,授人以魚不如授人以漁,通過這一個演算法的分析,我們就可以明白如何分析演算法的具體實現了,也就可以很容易的了解其它演算法的功能組成。
在解決了演算法部分的問題後,我們了解了演算法之間類的繼承關係,以及父類提供的介面和可以使用的方法。接下來即使再寫其它演算法,我們也可以仿照這個思路,保證這些介面的基礎上,實現自己的演算法程式碼。
5 指標測量和數據集的格式
我們在前面第三部分提到,對於預測結果如何進行指標計算的內容可以暫時忽略,這裡我們就補充這一部分內容。之所以選擇在這裡進行補充,我們可以看到,通過前面幾步,已經搭建起來了一個基本完整的 demo。其中只有少量內容調用了源碼,對預測結果的指標計算就是其中一個。
指標測量部分唯一需要注意的是預測結果的返回形式,也就是前面 Algo_base() 中 test() 方法返回的結果:predictions。
然後就是對計算的幾類指標的定義需要了解:MSE,RMSE,MAE,FCP。前三類都是比較常見的指標,FCP 在源碼中給出了一篇 paper 作為 reference,其中的定義也很清晰,我們參考 paper 中的定義便可。
具體的計算方法知道了以後,程式碼的實現就是很簡單的了。利用 numpy 可以快速的計算出想要的結果,具體的指標定義,程式碼實現的介紹,以及關於一些其它常見的指標該如何測量我也提供了一些思考,都可以在【第六篇文章:推薦實踐(6):accuracy()–surprise 支援哪些指標測量呢?】中看到。感興趣的朋友可以借鑒一下。
至此,整個 demo 可以運行完畢。但是還有一個前面留下的坑需要填一下,就是前面提到對於數據集的格式的問題。在前面提到的時候,我們說這些內容可以忽略過去。但是,其實在 surprise 中對數據格式的定義還是很值得學習的。
surprise 定義了一個 Trainset() 類,用來儲存所有與數據集相關的內容。比如用戶數量,物品數量,評分數量等比較簡單的內容,以及將數據集中的 user ID 轉化為新定義的數據結構中的內部編號 inner ID,獲取全局平均評分等稍複雜的功能。
通過定義一個數據集的類,可以對數據集進行一次處理,然後需要相應指標時只需直接調用。剩下了很多的運行時間,而且讓程式碼更簡潔。具體的進一步的分析可以閱讀【第七篇文章:推薦實踐(7):trainset.Trainset() 通過調整數據集讓程式碼更優雅】。
總結
這篇文章本身算是一篇疏導性的總結,結合之前的文章,大家可以自己復現出來一個 surprise 中的 knn 演算法,而且其它部分的介面也介紹的非常清晰。對於想要在 surprise 上繼續學習其它演算法源碼的朋友,可以輕鬆的按照我們之前的分析基礎,繼續自己的學習;對於想要進行魔改,加入一些自己想要的演算法的朋友,目前的介紹也已經清晰的解釋了各個介面,大家對應來封裝自己的演算法就可以了。
另外一方面,本篇文章也從如何閱讀源碼的角度為大家分享了一些我自己的經驗,或許有些地方可以幫助到大家。從如何開始閱讀源碼,到從一個小 demo 逐漸剖析,暫時忽略掉一些不重要的模組,一步步的完成自己的程式碼對源碼的替代,以寫代讀。
更嚴格的講,這種以寫代讀比較適合程式碼量不超過一萬行的小型庫。這種級別的程式碼量,我們可以通過自己完整的寫一遍來加深理解。但是更高量級的程式碼量,就不太適合寫了,還是以梳理邏輯架構為主了。
同時再稍微推薦一下之前的一篇總結文章,關於推薦系統從理論方面的一些介紹,感興趣的朋友可以了解一下。