在 用戶 —— 物品(user - item)的數據關係下很容易收集到一些偏好信息(preference),好比評分。利用這些分散的偏好信息,基於其背後可能存在的關聯性,來爲用戶推薦物品的方法,即是協同過濾,或稱協做型過濾(collaborative filtering)。python
這種過濾算法的有效性基礎在於:算法
本文主要內容爲基於用戶偏好的類似性進行物品推薦,使用的數據集爲 GroupLens Research 採集的一組從 20 世紀 90 年代末到 21 世紀初由 MovieLens 用戶提供的電影評分數據。數據中包含了約 6000 名用戶對約 4000 部電影的 100萬條評分,五分制。數據包能夠從網上下載到,裏面包含了三個數據表——users、movies、ratings。由於本文的主題是基於用戶偏好的,因此只使用 ratings 這一個文件。另兩個文件裏分別包含用戶和電影的元信息。數組
本文使用的數據分析包爲 pandas,環境爲 IPython,所以其實還默認攜帶了 Numpy 和 matplotlib。下面代碼中的提示符看起來不是 IPython 環境是由於 Idle 的格式發在博客上更好看一些。 <br /> ###數據規整 首先將評分數據從 ratings.dat 中讀出到一個 DataFrame 裏:app
lang:python >>> import pandas as pd >>> from pandas import Series,DataFrame >>> rnames = ['user_id','movie_id','rating','timestamp'] >>> ratings = pd.read_table(r'ratings.dat',sep='::',header=None,names=rnames) >>> ratings[:3] user_id movie_id rating timestamp 0 1 1193 5 978300760 1 1 661 3 978302109 2 1 914 3 978301968 [3 rows x 4 columns]
ratings 表中對咱們有用的僅是 user_id、movie_id 和 rating 這三列,所以咱們將這三列取出,放到一個以 user 爲行,movie 爲列,rating 爲值的表 data 裏面。(其實將 user 與 movie 的行列關係對調是更加科學的方法,但由於重跑一遍太麻煩了,這裏就沒改。)dom
lang:python >>> data = ratings.pivot(index='user_id',columns='movie_id',values='rating') >>> data[:5] movie_id 1 2 3 4 5 6 user_id 1 5 NaN NaN NaN NaN NaN ... 2 NaN NaN NaN NaN NaN NaN ... 3 NaN NaN NaN NaN NaN NaN ... 4 NaN NaN NaN NaN NaN NaN ... 5 NaN NaN NaN NaN NaN 2 ...
能夠看到這個表至關得稀疏,填充率大約只有 5%,接下來要實現推薦的第一步是計算 user 之間的相關係數,DataFrame 對象有一個很親切的 .corr(method='pearson', min_periods=1)
方法,能夠對全部列互相計算相關係數。method 默認爲皮爾遜相關係數,這個 ok,咱們就用這個。問題僅在於那個 min_periods 參數,這個參數的做用是設定計算相關係數時的最小樣本量,低於此值的一對列將不進行運算。這個值的取捨關係到相關係數計算的準確性,所以有必要先來肯定一下這個參數。測試
相關係數是用於評價兩個變量間線性關係的一個值,取值範圍爲 [-1, 1],-1表明負相關,0 表明不相關,1 表明正相關。其中 0~0.1 通常被認爲是弱相關,0.1~0.4 爲相關,0.4~1 爲強相關。優化
<br /> ###min_periods 參數測定 測定這樣一個參數的基本方法爲統計在 min\_periods 取不一樣值時,相關係數的標準差大小,越小越好;但同時又要考慮到,咱們的樣本空間十分稀疏,min\_periods 定得過高會致使出來的結果集過小,因此只能選定一個折中的值。網站
這裏咱們測定評分系統標準差的方法爲:在 data 中挑選一對重疊評分最多的用戶,用他們之間的相關係數的標準差去對總體標準差作點估計。在此前提下對這一對用戶在不一樣樣本量下的相關係數進行統計,觀察其標準差變化。spa
首先,要找出重疊評分最多的一對用戶。咱們新建一個以 user 爲行列的方陣 foo,而後挨個填充不一樣用戶間重疊評分的個數:線程
lang:python >>> foo = DataFrame(np.empty((len(data.index),len(data.index)),dtype=int),index=data.index,columns=data.index) >>> for i in foo.index: for j in foo.columns: foo.ix[i,j] = data.ix[i][data.ix[j].notnull()].dropna().count()
這段代碼特別費時間,由於最後一行語句要執行 4000*4000 = 1600萬遍;(其中有一半是重複運算,由於 foo 這個方陣是對稱的)還有一個緣由是 Python 的 GIL,使得其只能使用一個 CPU 線程。我在它執行了一個小時後,忍不住去測試了一下總時間,發現要三個多小時後就果斷 Ctrl + C 了,在算了一小半的 foo 中,我找到的最大值所對應的行列分別爲 424 和 4169,這兩位用戶之間的重疊評分數爲 998:
lang:python >>> for i in foo.index: foo.ix[i,i]=0#先把對角線的值設爲 0 >>> ser = Series(np.zeros(len(foo.index))) >>> for i in foo.index: ser[i]=foo[i].max()#計算每行中的最大值 >>> ser.idxmax()#返回 ser 的最大值所在的行號 4169 >>> ser[4169]#取得最大值 998 >>> foo[foo==998][4169].dropna()#取得另外一個 user_id 424 4169 Name: user_id, dtype: float64
咱們把 424 和 4169 的評分數據單獨拿出來,放到一個名爲 test 的表裏,另外計算了一下這兩個用戶之間的相關係數爲 0.456,還算不錯,另外經過柱狀圖瞭解一下他倆的評分分佈狀況:
lang:python >>> data.ix[4169].corr(data.ix[424]) 0.45663851303413217 >>> test = data.reindex([424,4169],columns=data.ix[4169][data.ix[424].notnull()].dropna().index) >>> test movie_id 2 6 10 11 12 17 ... 424 4 4 4 4 1 5 ... 4169 3 4 4 4 2 5 ... >>> test.ix[424].value_counts(sort=False).plot(kind='bar') >>> test.ix[4169].value_counts(sort=False).plot(kind='bar')
對這倆用戶的相關係數統計,咱們分別隨機抽取 20、50、100、200、500 和 998 個樣本值,各抽 20 次。並統計結果:
lang:python >>> periods_test = DataFrame(np.zeros((20,7)),columns=[10,20,50,100,200,500,998]) >>> for i in periods_test.index: for j in periods_test.columns: sample = test.reindex(columns=np.random.permutation(test.columns)[:j]) periods_test.ix[i,j] = sample.iloc[0].corr(sample.iloc[1]) >>> periods_test[:5] 10 20 50 100 200 500 998 0 -0.306719 0.709073 0.504374 0.376921 0.477140 0.426938 0.456639 1 0.386658 0.607569 0.434761 0.471930 0.437222 0.430765 0.456639 2 0.507415 0.585808 0.440619 0.634782 0.490574 0.436799 0.456639 3 0.628112 0.628281 0.452331 0.380073 0.472045 0.444222 0.456639 4 0.792533 0.641503 0.444989 0.499253 0.426420 0.441292 0.456639 [5 rows x 7 columns] >>> periods_test.describe() 10 20 50 100 200 500 #998略 count 20.000000 20.000000 20.000000 20.000000 20.000000 20.000000 mean 0.346810 0.464726 0.458866 0.450155 0.467559 0.452448 std 0.398553 0.181743 0.103820 0.093663 0.036439 0.029758 min -0.444302 0.087370 0.192391 0.242112 0.412291 0.399875 25% 0.174531 0.320941 0.434744 0.375643 0.439228 0.435290 50% 0.487157 0.525217 0.476653 0.468850 0.472562 0.443772 75% 0.638685 0.616643 0.519827 0.500825 0.487389 0.465787 max 0.850963 0.709073 0.592040 0.634782 0.546001 0.513486 [8 rows x 7 columns]
從 std 這一行來看,理想的 min_periods 參數值應當爲 200 左右。可能有人會以爲 200 太大了,這個推薦算法對新用戶簡直沒意義。可是得說,隨便算出個有超大偏差的相關係數,而後拿去作不靠譜的推薦,又有什麼意義呢。 <br /> ###算法檢驗 爲了確認在 min_periods=200
下本推薦算法的靠譜程度,最好仍是先作個檢驗。具體方法爲:在評價數大於 200 的用戶中隨機抽取 1000 位用戶,每人隨機提取一個評價另存到一個數組裏,並在數據表中刪除這個評價。而後基於閹割過的數據表計算被提取出的 1000 個評分的指望值,最後與真實評價數組進行相關性比較,看結果如何。
lang:python >>> check_size = 1000 >>> check = {} >>> check_data = data.copy()#複製一份 data 用於檢驗,以避免篡改原數據 >>> check_data = check_data.ix[check_data.count(axis=1)>200]#濾除評價數小於200的用戶 >>> for user in np.random.permutation(check_data.index): movie = np.random.permutation(check_data.ix[user].dropna().index)[0] check[(user,movie)] = check_data.ix[user,movie] check_data.ix[user,movie] = np.nan check_size -= 1 if not check_size: break >>> corr = check_data.T.corr(min_periods=200) >>> corr_clean = corr.dropna(how='all') >>> corr_clean = corr_clean.dropna(axis=1,how='all')#刪除全空的行和列 >>> check_ser = Series(check)#這裏是被提取出來的 1000 個真實評分 >>> check_ser[:5] (15, 593) 4 (23, 555) 3 (33, 3363) 4 (36, 2355) 5 (53, 3605) 4 dtype: float64
接下來要基於 corr_clean 給 check_ser 中的 1000 個 用戶-影片 對計算評分指望。計算方法爲:對與用戶相關係數大於 0.1 的其餘用戶評分進行加權平均,權值爲相關係數:
lang:python >>> result = Series(np.nan,index=check_ser.index) >>> for user,movie in result.index:#這個循環看着很亂,實際內容就是加權平均而已 prediction = [] if user in corr_clean.index: corr_set = corr_clean[user][corr_clean[user]>0.1].dropna()#僅限大於 0.1 的用戶 else:continue for other in corr_set.index: if not np.isnan(data.ix[other,movie]) and other != user:#注意bool(np.nan)==True prediction.append((data.ix[other,movie],corr_set[other])) if prediction: result[(user,movie)] = sum([value*weight for value,weight in prediction])/sum([pair[1] for pair in prediction]) >>> result.dropna(inplace=True) >>> len(result)#隨機抽取的 1000 個用戶中也有被 min_periods=200 刷掉的 862 >>> result[:5] (23, 555) 3.967617 (33, 3363) 4.073205 (36, 2355) 3.903497 (53, 3605) 2.948003 (62, 1488) 2.606582 dtype: float64 >>> result.corr(check_ser.reindex(result.index)) 0.436227437429696 >>> (result-check_ser.reindex(result.index)).abs().describe()#推薦指望與實際評價之差的絕對值 count 862.000000 mean 0.785337 std 0.605865 min 0.000000 25% 0.290384 50% 0.686033 75% 1.132256 max 3.629720 dtype: float64
862 的樣本量能達到 0.436 的相關係數,應該說結果還不錯。若是一開始沒有濾掉評價數小於 200 的用戶的話,那麼首先在計算 corr 時會明顯感受時間變長,其次 result 中的樣本量會很小,大約 200+ 個。但由於樣本量變小的緣故,相關係數能夠提高到 0.5~0.6 。
另外從指望與實際評價的差的絕對值的統計量上看,數據也比較理想。 <br /> ###實現推薦 在上面的檢驗,尤爲是平均加權的部分作完後,推薦的實現就沒有什麼新東西了。
首先在原始未閹割的 data 數據上重作一份 corr 表:
lang:python >>> corr = data.T.corr(min_periods=200) >>> corr_clean = corr.dropna(how='all') >>> corr_clean = corr_clean.dropna(axis=1,how='all')
咱們在 corr_clean 中隨機挑選一位用戶爲他作一個推薦列表:
lang:python >>> lucky = np.random.permutation(corr_clean.index)[0] >>> gift = data.ix[lucky] >>> gift = gift[gift.isnull()]#如今 gift 是一個全空的序列
最後的任務就是填充這個 gift:
lang:python >>> corr_lucky = corr_clean[lucky].drop(lucky)#lucky 與其餘用戶的相關係數 Series,不包含 lucky 自身 >>> corr_lucky = corr_lucky[corr_lucky>0.1].dropna()#篩選相關係數大於 0.1 的用戶 >>> for movie in gift.index:#遍歷全部 lucky 沒看過的電影 prediction = [] for other in corr_lucky.index:#遍歷全部與 lucky 相關係數大於 0.1 的用戶 if not np.isnan(data.ix[other,movie]): prediction.append((data.ix[other,movie],corr_clean[lucky][other])) if prediction: gift[movie] = sum([value*weight for value,weight in prediction])/sum([pair[1] for pair in prediction]) >>> gift.dropna().order(ascending=False)#將 gift 的非空元素按降序排列 movie_id 3245 5.000000 2930 5.000000 2830 5.000000 2569 5.000000 1795 5.000000 981 5.000000 696 5.000000 682 5.000000 666 5.000000 572 5.000000 1420 5.000000 3338 4.845331 669 4.660464 214 4.655798 3410 4.624088 ... 2833 1 2777 1 2039 1 1773 1 1720 1 1692 1 1538 1 1430 1 1311 1 1164 1 843 1 660 1 634 1 591 1 56 1 Name: 3945, Length: 2991, dtype: float64
<br /> #補充 --- 上面給出的示例都是些原型代碼,有不少可優化的空間。好比 data 的行列轉換;好比測定 min_periods 時的方陣 foo 只需計算一半;好比有些 for 循環和相應運算能夠用數組對象方法來實現(方法版比用戶本身編寫的版本速度快不少);甚至確定還有一些 bug。另外這個數據集的體積還不算太大,若是再增加一個數量級,那麼就有必要針對計算密集的部分(如 corr)作進一步優化了,可使用多進程,或者 Cython/C 代碼。(<span style="text-decoration:line-through;">或者換更好的硬件</span>)
雖然協同過濾是一種比較省事的推薦方法,但在某些場合下並不如利用元信息推薦好用。協同過濾會遇到的兩個常見問題是
都是樣本量太少致使的。(上例中也使用了至少 200 的有效重疊評價數)所以在對於新用戶和新物品進行推薦時,使用一些更通常性的方法效果可能會更好。好比給新用戶推薦更多平均得分超高的電影;把新電影推薦給喜歡相似電影(如具備相同導演或演員)的人。後面這種作法須要維護一個物品分類表,這個表既能夠是基於物品元信息劃分的,也但是經過聚類獲得的。