一篇長文學懂入門推薦算法庫:surprise

一篇长文学懂入门推荐算法库:surprise

不知不覺寫完了七篇文章來分析這個推薦算法庫 surprise,基本上我們從頭到尾所有代碼都自己完成,並且可以成功 run 起來一個基於鄰域的協同過濾算法。是哪個算法這件事其實我覺得不重要,surprise 支持的每個算法本身思路並不複雜,代碼也不晦澀難懂,我們主要的目的是理解它的架構,學習框架各個部分的交互。

按理之前的文章寫完後,這個系列就可以算是初步完成了。但是我自己回頭再看的時候,發現七篇文章缺乏一個總體性的視角,這可能會導致有的同學讀起來覺得前後不太連貫。

文章的順序是按照我自己在學習源碼過程中的順序編排的,但是如果沒有告訴讀者我的思維過程,讀者可能不能很好的理解這其中的關係。

所以這篇文章是想從一個整體的視角,以我當初的思路為主線進行介紹,觀察並思考如何一步一步的讓模型 run 起來。至於某些具體的細節部分,我們給出之前寫的對應文章鏈接,大家可以回頭再去看相應的文章來了解。

1 先搞個模型跑起來

我們首先從一個總體性的代碼看一下,很簡單的幾行代碼,開始我們的 surprise 之旅。

一篇长文学懂入门推荐算法库:surprise

這裡需要導入的部分,我都已經重寫過了,但是大家可以在自己本地的代碼上嘗試一下,直接利用 surprise 庫就可以運行一個簡單的 KNN 算法,本質上也就是基於鄰域的協同過濾算法。按照我在代碼上標註出來的紅線部分,分別給對應的四個 import 模塊,前面加上 surpris. 的路徑就可以從 surprise 中進行導入。

這裡需要提一下,我用的是自己之前已經在 movielens 官網下載的數據集,大家可以自己直接下載,也可以在網上找一下教程,surprise 支持自動下載 movielens 的數據集。

接下來我們只看 surprise_code 函數,這個函數就是我們需要學習的所有內容,從這個例子開始,我們要去一步步深扒 surprise 的執行過程。

<code>def surprise_code:/<code><code> reader = Reader(line_format="user item rating", sep=',', skip_lines=1)/<code><code> data = Dataset.load_from_file('./ml-latest-small/ratings.csv', reader)/<code>
<code> algo = KNNBasic/<code><code> perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=2, verbose=0)/<code><code> for keys in perf:/<code><code> print(keys, ": ", perf[keys])/<code>

對核心的 surprise_code 函數,可以分為兩個部分來看:數據載入,算法執行並檢測結果。

數據載入,由 Reader 和 Dataset 兩個類來提供功能,具體的思路是由 Reader 提供讀取數據的格式,然後 Dataset 按照 Reader 的設置來完成對數據的載入。

算法執行並檢測結果,這裡由一個 cross_validate 來完成,提前導入需要執行的算法並實例化,然後將數據,算法,要檢測的指標等都傳入 cross_validate,它會完成對算法的訓練擬合,然後進行預測結果,再對結果進行驗證,最終返回目標的檢測指標對應的結果。

所以我們可以看到直接調用接口是很容易跑起來一個模型的,僅這麼幾行簡單的代碼就可以將一個算法完整的運行起來。但是如果要深入到代碼的執行細節上,就需要捋順它們的關係,然後抽絲剝繭一點點展開。

在【第一篇文章:推薦實踐(1):從零開始寫一個自己的推薦算法庫】中,我們將上面的算法執行並檢測結果分成了兩部分,也就是將整個工作流程劃分為三部分:數據載入,算法設計,結果評估。這樣子更加細化的一步當然沒問題,邏輯上這樣子也更容易理解。

現在捋順了算法的執行思路以後,我們開始從數據集的載入開始去分析源碼。

trick:在正式細節代碼前,分享一些關於我學習源碼的方法,不一定適合所有人,也不一定適合所有源碼,僅供大家參考:

首先,要像我們一樣完成一個非常簡單的 demo,如這一小節的標題一樣,先搞個模型跑起來。至少我們得會用它,能用起來,這樣子我們才能分析下去。

其次,我們再捋順模型的流程,劃分為幾個主要的部分,按照這個邏輯順序,從最初的部分開始,嘗試自己完成該模塊的功能。導入自己寫的部分,替代掉源碼的那一部分,看看前面的 demo 能不能繼續運行,檢查結果是否一致。

最後,不要在分析的時候,拘泥於細節,先完成該模塊的主要功能。如果這部分功能與其它模塊相關,可以先導入相關的模塊,直接使用。不要嘗試一步將某一個模塊寫的盡善盡美。

這三個提到的內容,我們在接下來的文章可以再進一步體會。

一篇长文学懂入门推荐算法库:surprise

2 從第一部分的數據載入開始

我們前面分析了數據載入部分,由 Reader 和 Dataset 兩個類來提供功能。接下來要做的就是捋順這部分內容,然後自己嘗試寫出來對應的模塊,並且替代進去,看看我們前面的代碼能不能繼續正常運行。

當我們到了這一步的時候,首先就是打開這兩個模塊,看一看代碼,瞭解一下它們的功能。在【第二篇文章:推薦實踐(2):數據集的載入與切割】中,對這兩部分的內容進行了仔細分析,我在這裡就總體性的介紹一下,不深入到代碼細節上去了。

<code>reader = Reader(line_format="user item rating", sep=',', skip_lines=1)/<code>

對於這個 Reader 類,主要的功能是設置一個讀取器。從 Reader 的使用也可以看出來,要求的輸入是每行的格式,每行的分隔符,要忽略的行數。

從這個類實例時的輸入上,我們可以判斷出來,這個 Reader 類的作用是構造一個讀取器對象 reader,這個讀取器 reader 包含了一些如何去讀數據的屬性。比如 reader 知道每行的數據是按照 “user item rating” 來分佈的,知道每行數據由符號 "," 分割開,知道第一行的數據應該被跳過。

所以我們在構建了這個 reader 以後,就可以將它傳給 Dataset 類,來輔助我們從數據集中,按照我們想要的格式讀取出來數據內容。

<code>data = Dataset.load_from_file('./ml-latest-small/ratings.csv', reader)/<code>

由於這裡我們選擇了使用自己已經下載的數據集,調用的就是 Dataset.load_from_file 方法。可以看到的是,這個方法的輸入有兩個參數,第一個是數據集的路徑,第二個就是剛剛實例化的讀取器 reader。

所以這個 load_from_file 在讀取數據時,就會按照 reader 的定義的格式來讀取,最終返回一個自定義的數據格式。其實如果看了代碼,我們可以看到這裡返回的數據格式是:dataset.DatasetAutoFolds,但是正如我們前面說的,對於源碼不要陷入細節。我們知道這兩步對原始的數據集文件進行了處理,得到了後續可以處理的數據格式,就 OK 啦。

一篇长文学懂入门推荐算法库:surprise

3 進行結果交叉驗證

在完成數據集的載入以後,我們選擇的是利用 cross_validate 執行算法並交叉檢驗。這一部分的內容,我們分為兩部分去介紹。

首先忽略掉算法的實現,直接調用算法的接口。這也是一個很實用的 trick,適當的時候忽略掉一些代碼實現,即使你接下來要用到它,也可以直接調源碼的接口。所以我們這裡忽略了 KNN 算法的實現,直接調用它來實現訓練擬合以及後續在測試集上的預測。

那麼 cross_validate 裡面是什麼呢?我們看一下 validate 中的內容:

一篇长文学懂入门推荐算法库:surprise

validate 中有兩個函數,分別是 cross_validate 和 fit_and_score。我們簡單的介紹一下它們的功能,讓大家可以繼續沒有障礙的閱讀當前這篇文章,至於具體的代碼和功能分析可以看【第三篇文章:推薦實踐(3):調用算法接口實現一個 demo】。

<code>algo = KNNBasic/<code><code>perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=2, verbose=0)/<code>

可以看到 cross_validate 是被調用的外部接口,很容易可以猜到,fit_and_score 是在 cross_validate 中被調用的。

對 cross_validate 而言,它的輸入有算法對象,數據集,需要測量的指標,交叉驗證的次數等。這裡簡單的介紹一下它的內部邏輯。它對輸入的數據 data,分成 cv 份,然後每次選擇其中一份作為測試集,其餘的作為訓練集。在數據集劃分完後,對它們分別調用 fit_and_score,去進行算法擬合。

這裡注意一個小細節,對數據集的劃分不是靜態全部劃分完,然後分別在數據集上進行訓練和驗證,而是利用輸入的 data 構造一個生成器,每次拋出一組劃分完的結果。

對 fit_and_score 函數,它對輸入的算法在輸入的訓練集上進行擬合,然後在輸入的測試集上進行驗證,再計算需要的指標。

在進行到這裡的時候,同樣忽略掉對預測結果進行指標測量的步驟,直接調用 surprise 中的 accuracy 來進行處理

。當然到後面我們會補充這些內容,這裡要注意的重點是如何進行交叉驗證。

這一部分內容的核心是,在有了我們第 2 節輸入的數據後,該如何進行數據集的劃分以及如何進行算法的訓練和驗證。所以我們關注的重點是數據集在進行 k 折交叉驗證時如何劃分,又如何調用接口完成算法在數據集上的訓練和測試。

說這一段的意思是想告訴大家,在閱讀源碼,或者仿寫源碼時,需要把握住自己在這一步驟的核心思路,屏蔽掉暫時不重要,或者對當前步驟不是很關鍵的內容。哪怕只是調用各個接口來完成自己當前步驟的任務,只要你把握住了它們的交互關係,處理流程就 OK。

一篇长文学懂入门推荐算法库:surprise

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() 瞭解整個算法部分的結構梳理】結合源碼與文檔,就可以非常容易理解了。

關於其它的算法我們就不一一展開去分析了,授人以魚不如授人以漁,通過這一個算法的分析,我們就可以明白如何分析算法的具體實現了,也就可以很容易的瞭解其它算法的功能組成。

在解決了算法部分的問題後,我們瞭解了算法之間類的繼承關係,以及父類提供的接口和可以使用的方法。接下來即使再寫其它算法,我們也可以仿照這個思路,保證這些接口的基礎上,實現自己的算法代碼。

一篇长文学懂入门推荐算法库:surprise

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

總結

這篇文章本身算是一篇疏導性的總結,結合之前的文章,大家可以自己復現出來一個 surprise 中的 knn 算法,而且其它部分的接口也介紹的非常清晰。對於想要在 surprise 上繼續學習其它算法源碼的朋友,可以輕鬆的按照我們之前的分析基礎,繼續自己的學習;對於想要進行魔改,加入一些自己想要的算法的朋友,目前的介紹也已經清晰的解釋了各個接口,大家對應來封裝自己的算法就可以了。

另外一方面,本篇文章也從如何閱讀源碼的角度為大家分享了一些我自己的經驗,或許有些地方可以幫助到大家。從如何開始閱讀源碼,到從一個小 demo 逐漸剖析,暫時忽略掉一些不重要的模塊,一步步的完成自己的代碼對源碼的替代,以寫代讀。

更嚴格的講,這種以寫代讀比較適合代碼量不超過一萬行的小型庫。這種級別的代碼量,我們可以通過自己完整的寫一遍來加深理解。但是更高量級的代碼量,就不太適合寫了,還是以梳理邏輯架構為主了。


分享到:


相關文章: