Surprise(Simple Python Recommendation System Engine)是一款推薦系統庫,是scikit系列中的一個。簡單易用,同時支持多種推薦算法(基礎算法、協同過濾、矩陣分解等)。
設計surprise時考慮到以下目的:
- 讓用戶完美控制他們的實驗。為此,特別強調 文檔,試圖通過指出算法的每個細節盡可能清晰和准確。
- 減輕數據集處理的痛苦。用戶可以使用內置數據集(Movielens, Jester)和他們自己的自定義 數據集。
- 提供各種即用型預測算法, 例如基線算法, 鄰域方法,基於矩陣因子分解( SVD, PMF, SVD ++,NMF)等等。此外, 內置了各種相似性度量(余弦,MSD,皮爾遜......)。
- 可以輕松實現新的算法思路。
- 提供評估, 分析 和 比較 算法性能的工具。使用強大的CV迭代器(受scikit-learn優秀工具啟發)以及 對一組參數的詳盡搜索,可以非常輕松地運行交叉驗證程序 。
1.Surprise安裝
pip install numpy
pip install scikit-surprise
在安裝之前首先確認安裝了numpy模塊。
2.基本算法
算法類名 | 說明 |
---|---|
random_pred.NormalPredictor | 根據訓練集的分布特征隨機給出一個預測值 |
baseline_only.BaselineOnly | 給定用戶和Item,給出基於baseline的估計值 |
knns.KNNBasic | 最基礎的協同過濾 |
knns.KNNWithMeans | 將每個用戶評分的均值考慮在內的協同過濾實現 |
knns.KNNBaseline | 考慮基線評級的協同過濾 |
matrix_factorization.SVD | SVD實現 |
matrix_factorization.SVDpp | SVD++,即LFM+SVD |
matrix_factorization.NMF | 基於矩陣分解的協同過濾 |
slope_one.SlopeOne | 一個簡單但精確的協同過濾算法 |
co_clustering.CoClustering | 基於協同聚類的協同過濾算法 |
其中基於近鄰的方法(協同過濾)可以設定不同的度量准則
相似度度量標准 | 度量標准說明 |
---|---|
cosine | 計算所有用戶(或物品)對之間的余弦相似度。 |
msd | 計算所有用戶(或物品)對之間的均方差異相似度。 |
pearson | 計算所有用戶(或物品)對之間的Pearson相關系數。 |
pearson_baseline | 計算所有用戶(或物品)對之間的(縮小的)Pearson相關系數,使用基線進行居中而不是平均值。 |
支持不同的評估准則
評估准則 | 准則說明 |
---|---|
rmse | 計算RMSE(均方根誤差)。 |
mae | 計算MAE(平均絕對誤差)。 |
fcp | 計算FCP(協調對的分數)。 |
3.Surprise使用
(1)載入自帶的數據集
#-*- coding:utf-8 -*- # 可以使用上面提到的各種推薦系統算法 from surprise import SVD from surprise import Dataset, print_perf from surprise.model_selection import cross_validate # 默認載入movielens數據集 data = Dataset.load_builtin('ml-100k') # k折交叉驗證(k=3),此方法現已棄用 # data.split(n_folds=3) # 試一把SVD矩陣分解 algo = SVD() # 在數據集上測試一下效果 perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3) #輸出結果 print_perf(perf)
運行結果:
(2)載入自己的數據集
from surprise import SVD from surprise import Dataset, print_perf, Reader from surprise.model_selection import cross_validate import os # 指定文件所在路徑 file_path = os.path.expanduser('data.csv') # 告訴文本閱讀器,文本的格式是怎么樣的 reader = Reader(line_format='user item rating', sep=',') # 加載數據 data = Dataset.load_from_file(file_path, reader=reader) algo = SVD() # 在數據集上測試一下效果 perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3) #輸出結果 print_perf(perf)
需要注意:
1.無法識別中文,如果有中文,需要將其轉換成ID號再進行操作(以下列出一種簡單的轉換方式)
2.不能有表頭,需要去掉表頭和元數據中有中文的列
3.需要修改Reader,line_format 就是數據的列,sep 是分隔方式(表格格式初始分割方式是‘,’)
一種簡單的數據轉換方式:
#-*- coding:utf-8 -*- # 構建物品id import pandas as pd df = pd.read_csv('train_score.csv', encoding="gbk") # 讀取第二列的數據 item_name = df.iloc[:, 1] item = {} item_id = [] num = 0 # 將每個不同的物品與id號進行關聯 for i in item_name: if i in item: item_id.append(item[i]) else: item[i] = num item_id.append(num) num += 1 print item_id df['itemId'] = item_id df.to_csv("data.csv", encoding="gbk", index=False)
4.算法調參
這里實現的算法用到的算法無外乎也是SGD等,因此也有一些超參數會影響最后的結果,我們同樣可以用sklearn中常用到的網格搜索交叉驗證(GridSearchCV)來選擇最優的參數。簡單的例子如下所示:
# 定義好需要優選的參數網格 param_grid = {'n_epochs': [5, 10], 'lr_all': [0.002, 0.005], 'reg_all': [0.4, 0.6]} # 使用網格搜索交叉驗證 grid_search = GridSearch(SVD, param_grid, measures=['RMSE', 'FCP']) # 在數據集上找到最好的參數 data = Dataset.load_builtin('ml-100k') data.split(n_folds=3) grid_search.evaluate(data) # 輸出調優的參數組 # 輸出最好的RMSE結果 print(grid_search.best_score['RMSE']) # >>> 0.96117566386 # 輸出對應最好的RMSE結果的參數 print(grid_search.best_params['RMSE']) # >>> {'reg_all': 0.4, 'lr_all': 0.005, 'n_epochs': 10} # 最好的FCP得分 print(grid_search.best_score['FCP']) # >>> 0.702279736531 # 對應最高FCP得分的參數 print(grid_search.best_params['FCP']) # >>> {'reg_all': 0.6, 'lr_all': 0.005, 'n_epochs': 10}
GridSearchCV 方法:
# 定義好需要優選的參數網格 param_grid = {'n_epochs': [5, 10], 'lr_all': [0.002, 0.005], 'reg_all': [0.4, 0.6]} # 使用網格搜索交叉驗證 grid_search = GridSearchCV(SVD, param_grid, measures=['RMSE', 'FCP'], cv=3) # 在數據集上找到最好的參數 data = Dataset.load_builtin('ml-100k') # pref = cross_validate(grid_search, data, cv=3) grid_search.fit(data) # 輸出調優的參數組 # 輸出最好的RMSE結果 print(grid_search.best_score)
1.estimator 選擇使用的分類器,並且傳入除需要確定最佳的參數之外的其他參數。 每一個分類器都需要一個scoring參數,或者score方法: 如estimator=RandomForestClassifier( min_samples_split=100, min_samples_leaf=20, max_depth=8, max_features='sqrt', random_state=10), 2.param_grid 需要最優化的參數的取值,值為字典或者列表,例如: param_grid =param_test1, param_test1 = {'n_estimators':range(10,71,10)}。 3. scoring=None 模型評價標准,默認None,這時需要使用score函數;或者如scoring='roc_auc', 根據所選模型不同,評價准則不同。字符串(函數名),或是可調用對象, 需要其函數簽名形如:scorer(estimator, X, y);如果是None,則使用estimator的誤差估計函數。 4.n_jobs=1 n_jobs: 並行數,int:個數,-1:跟CPU核數一致, 1:默認值 5.cv=None 交叉驗證參數,默認None,使用三折交叉驗證。指定fold數量,默認為3,也可以是yield產生訓練/測試數據的生成器。 6.verbose=0, scoring=None verbose:日志冗長度,int:冗長度,0:不輸出訓練過程,1:偶爾輸出,>1:對每個子模型都輸出。 7.pre_dispatch=‘2*n_jobs’ 指定總共分發的並行任務數。當n_jobs大於1時,數據將在每個運行點進行復制,這可能導致OOM, 而設置pre_dispatch參數,則可以預先划分總共的job數量,使數據最多被復制pre_dispatch次 8.return_train_score=’warn’ 如果“False”,cv_results_屬性將不包括訓練分數。 9.refit :默認為True,程序將會以交叉驗證訓練集得到的最佳參數,重新對所有可用的訓練集與開發集進行, 作為最終用於性能評估的最佳模型參數。即在搜索參數結束后,用最佳參數結果再次fit一遍全部數據集。 10.iid:默認True,為True時,默認為各個樣本fold概率分布一致,誤差估計為所有樣本之和,而非各個fold的平均。 進行預測的常用方法和屬性 grid.fit():運行網格搜索 grid_scores_:給出不同參數情況下的評價結果 best_params_:描述了已取得最佳結果的參數的組合 best_score_:成員提供優化過程期間觀察到的最好的評
5.使用不同的推薦系統算法進行建模比較
from surprise import Dataset, print_perf from surprise.model_selection import cross_validate data = Dataset.load_builtin('ml-100k') ### 使用NormalPredictor from surprise import NormalPredictor algo = NormalPredictor() perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3) print_perf(perf) ### 使用BaselineOnly from surprise import BaselineOnly algo = BaselineOnly() perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3) print_perf(perf) ### 使用基礎版協同過濾 from surprise import KNNBasic, evaluate algo = KNNBasic() perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3) print_perf(perf) ### 使用均值協同過濾 from surprise import KNNWithMeans, evaluate algo = KNNWithMeans() perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3) print_perf(perf) ### 使用協同過濾baseline from surprise import KNNBaseline, evaluate algo = KNNBaseline() perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3) print_perf(perf) ### 使用SVD from surprise import SVD, evaluate algo = SVD() perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3) print_perf(perf) ### 使用SVD++ from surprise import SVDpp, evaluate algo = SVDpp() perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3) print_perf(perf) ### 使用NMF from surprise import NMF algo = NMF() perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3) print_perf(perf)
6.movielens推薦實例
#-*- coding:utf-8 -*- from __future__ import (absolute_import, division, print_function, unicode_literals) import os import io from surprise import KNNBaseline from surprise import Dataset import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s', datefmt='%a, %d %b %Y %H:%M:%S') # 訓練推薦模型 步驟:1 def getSimModle(): # 默認載入movielens數據集 data = Dataset.load_builtin('ml-100k') trainset = data.build_full_trainset() #使用pearson_baseline方式計算相似度 False以item為基准計算相似度 本例為電影之間的相似度 sim_options = {'name': 'pearson_baseline', 'user_based': False} ##使用KNNBaseline算法 algo = KNNBaseline(sim_options=sim_options) #訓練模型 algo.fit(trainset) return algo # 獲取id到name的互相映射 步驟:2 def read_item_names(): """ 獲取電影名到電影id 和 電影id到電影名的映射 """ file_name = (os.path.expanduser('~') + '/.surprise_data/ml-100k/ml-100k/u.item') rid_to_name = {} name_to_rid = {} with io.open(file_name, 'r', encoding='ISO-8859-1') as f: for line in f: line = line.split('|') rid_to_name[line[0]] = line[1] name_to_rid[line[1]] = line[0] return rid_to_name, name_to_rid # 基於之前訓練的模型 進行相關電影的推薦 步驟:3 def showSimilarMovies(algo, rid_to_name, name_to_rid): # 獲得電影Toy Story (1995)的raw_id toy_story_raw_id = name_to_rid['Toy Story (1995)'] logging.debug('raw_id=' + toy_story_raw_id) #把電影的raw_id轉換為模型的內部id toy_story_inner_id = algo.trainset.to_inner_iid(toy_story_raw_id) logging.debug('inner_id=' + str(toy_story_inner_id)) #通過模型獲取推薦電影 這里設置的是10部 toy_story_neighbors = algo.get_neighbors(toy_story_inner_id, 10) logging.debug('neighbors_ids=' + str(toy_story_neighbors)) #模型內部id轉換為實際電影id neighbors_raw_ids = [algo.trainset.to_raw_iid(inner_id) for inner_id in toy_story_neighbors] #通過電影id列表 或得電影推薦列表 neighbors_movies = [rid_to_name[raw_id] for raw_id in neighbors_raw_ids] print('The 10 nearest neighbors of Toy Story are:') for movie in neighbors_movies: print(movie) if __name__ == '__main__': # 獲取id到name的互相映射 rid_to_name, name_to_rid = read_item_names() # 訓練推薦模型 algo = getSimModle() ##顯示相關電影 showSimilarMovies(algo, rid_to_name, name_to_rid)
運行結果: