Python 基於協同過濾的推薦

#協同過濾

在 用戶 —— 物品(user - item)的數據關係下很容易收集到一些偏好信息(preference),好比評分。利用這些分散的偏好信息,基於其背後可能存在的關聯性,來爲用戶推薦物品的方法,即是協同過濾,或稱協做型過濾(collaborative filtering)。python

這種過濾算法的有效性基礎在於:算法

  1. 用戶的偏好具備類似性,即用戶是可分類的。這種分類的特徵越明顯,推薦的準確率就越高
  2. 物品之間是存在關係的,即偏好某一物品的任何人,都極可能也同時偏好另外一件物品

不一樣環境下這兩種理論的有效性也不一樣,應用時需作相應調整。如豆瓣上的文藝做品,用戶對其的偏好程度與用戶自身的品位關聯性較強;而對於電子商務網站來講,商品之間的內在聯繫對用戶的購買行爲影響更爲顯著。當用在推薦上,這兩種方向也被稱爲基於用戶的和基於物品的。本文內容爲基於用戶的。 <br /> #影評推薦實例

本文主要內容爲基於用戶偏好的類似性進行物品推薦,使用的數據集爲 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')

424

4169

對這倆用戶的相關係數統計,咱們分別隨機抽取 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 的有效重疊評價數)所以在對於新用戶和新物品進行推薦時,使用一些更通常性的方法效果可能會更好。好比給新用戶推薦更多平均得分超高的電影;把新電影推薦給喜歡相似電影(如具備相同導演或演員)的人。後面這種作法須要維護一個物品分類表,這個表既能夠是基於物品元信息劃分的,也但是經過聚類獲得的。

相關文章
相關標籤/搜索