更新、更全的《機器學習》的更新網站,更有python、go、數據結構與算法、爬蟲、人工智能教學等着你:http://www.javashuo.com/article/p-vozphyqp-cm.htmlpython
目前推薦系統被應用於各個領域,例如淘寶的商品推薦、b站的視頻推薦、網易雲音樂的每日推薦等等,這些都是基於用於往日在平臺的行爲模式給用戶推薦他們可能喜歡的商品、視頻、音樂。算法
下面咱們將以電影推薦系統舉例,一步一步經過Python實現一個簡單的電影推薦系統。數組
因爲數據量的緣由,咱們可能沒法作到精度較高的推薦系統,可是作一個差很少能實現推薦功能的電影推薦系統是徹底沒有問題的。數據結構
import io import os import sys import numpy as np import pandas as pd import matplotlib.pyplot as plt from matplotlib.font_manager import FontProperties from sklearn.impute import SimpleImputer from sklearn.metrics.pairwise import cosine_similarity %matplotlib inline font = FontProperties(fname='/Library/Fonts/Heiti.ttc')
早期講構造機器學習系統的時候說到,咱們第一步每每是須要收集數據。app
因爲本次要作的推薦系統是和電影有關的,而爲了給用戶推薦他喜歡的電影,通常會收集每一個用戶對本身看過的電影的評分。這裏咱們假設用戶看完電影必定會給電影評分,而且評分範圍爲\(\{1,2,3,4,5\}\),即評分最低爲1分,最高爲5分。若是用戶沒有評分則意味着用戶沒有看過該電影。dom
若是是大型的推薦系統,每每須要經過不少途徑得到各類數據,如爬蟲、平臺合做,而且可能還會考慮用戶自己的各類信息,如身高、體重、年齡、興趣愛好……和電影的各類信息,如篇名、導演、演員陣容、上映時間……,一般這種大型系統針對的數據維度每每都是上萬,上十萬的。因爲數據限制,所以這裏只假設用戶喜歡看的電影和他對電影的評分有關。機器學習
因爲這是一個簡單的推薦系統版本,因此咱們假設咱們已經獲取了用戶對本身看過的電影的評分,即數據在movie.xlsx
表格中。學習
# 收集數據 # 沒有表格文件時自定義數據 csv_data = ''' 《肖申克的救贖》,《控方證人》,《這個殺手不太冷》,《霸王別姬》,《美麗人生》,《阿甘正傳》,《辛德勒的名單》,姓名 ,4.0,,4.0,,5.0,,'劉一' 4.0,,5.0,3.0,5.0,,,'陳二' 3.0,4.0,,3.0,2.0,3.0,3.0,'張三' 2.0,3.0,,3.0,,,,'李四' 3.0,4.0,,5.0,3.0,3.0,,'王五' ,,4.0,,4.0,2.0,,'趙六' 3.0,,1.0,5.0,3.0,3.0,2.0,'孫七' 2.0,,2.0,,1.0,,,'周八' 1.0,2.0,,,,2.0,,'吳九' ,5.0,,4.0,,3.0,3.0,'鄭十' ''' if not os.path.exists('datasets/movie.xlsx'): # 將文件讀入內存 csv_data = io.StringIO(csv_data) df = pd.read_csv(csv_data, header=0) df.index = df['姓名'].tolist() else: # 從表格中獲取數據 df = pd.read_excel('datasets/movie.xlsx', header=0) df.index = df['姓名'].tolist() df
《肖申克的救贖》 | 《控方證人》 | 《這個殺手不太冷》 | 《霸王別姬》 | 《美麗人生》 | 《阿甘正傳》 | 《辛德勒的名單》 | 姓名 | |
---|---|---|---|---|---|---|---|---|
劉一 | NaN | 4.0 | NaN | 4.0 | NaN | 5.0 | NaN | 劉一 |
陳二 | 4.0 | NaN | 5.0 | 3.0 | 5.0 | NaN | NaN | 陳二 |
張三 | 3.0 | 4.0 | NaN | 3.0 | 2.0 | 3.0 | 3.0 | 張三 |
李四 | 2.0 | 3.0 | NaN | 3.0 | 3.0 | 2.0 | NaN | 李四 |
王五 | 3.0 | 4.0 | NaN | 5.0 | 3.0 | 3.0 | NaN | 王五 |
趙六 | NaN | NaN | 4.0 | NaN | 4.0 | 2.0 | NaN | 趙六 |
孫七 | 3.0 | NaN | 1.0 | 5.0 | 3.0 | 3.0 | 2.0 | 孫七 |
周八 | 2.0 | NaN | 2.0 | NaN | 1.0 | NaN | 2.0 | 周八 |
吳九 | 1.0 | 2.0 | 2.0 | NaN | 1.0 | 1.0 | NaN | 吳九 |
鄭十 | NaN | 5.0 | NaN | 4.0 | NaN | 3.0 | 3.0 | 鄭十 |
從上述表格中能夠看到不少用戶有些電影是沒有看過的,這至關於數據裏的缺失值,《數據預處理》那一篇詳細講到了缺失值的處理,這裏再也不贅述,在此處可能使用中位數、平均數、衆數恐怕都不行。測試
這種狀況的缺失值爲用戶沒有看過此電影,而咱們之後推薦電影也定是推薦用戶沒有看過的電影,若是你把他沒有看過的電影就這樣輕易只依據你們對電影的平均評分,你就給他推薦電影,這可能並不合理。所以我在這裏統一用\(0\)替換全部的沒有打分的電影,即\(0\)表示用戶沒有看過這部電影,而後使用其餘算法給用戶推薦電影。
# 數據缺失值處理 def imputer_data(np_arr): # 中位數strategy=median,平均數strategy=mean,衆數strategy=most_frequent imputer = SimpleImputer(missing_values=np.nan, strategy='constant', fill_value=0) # 找到用戶沒有評分的電影 imr = imputer.fit(np_arr.iloc[:, :-1]) # 將用戶沒有評分的電影評分用0填充 imputed_df = imr.transform(np_arr.iloc[:, :-1]) return imputed_df imputed_df = imputer_data(df) imputed_df
array([[0., 4., 0., 4., 0., 5., 0.], [4., 0., 5., 3., 5., 0., 0.], [3., 4., 0., 3., 2., 3., 3.], [2., 3., 0., 3., 3., 2., 0.], [3., 4., 0., 5., 3., 3., 0.], [0., 0., 4., 0., 4., 2., 0.], [3., 0., 1., 5., 3., 3., 2.], [2., 0., 2., 0., 1., 0., 2.], [1., 2., 2., 0., 1., 1., 0.], [0., 5., 0., 4., 0., 3., 3.]])
獲取了數據並對數據進行了預處理,接下來就是從數據中獲得咱們想要的信息,然而咱們想要獲取什麼信息呢?
通常咱們想要獲取兩個用戶,或兩個電影之間的類似度,如下兩點將是咱們要從數據中得到的信息。
經過以上兩種方式,設定一個評分閾值,若是用戶可能會給該電影的評分大於該閾值,則能夠放心的給用戶推薦電影,反之則不推薦給用戶。
若是咱們想要獲取用戶user_1和用戶user_2之間的類似度時,能夠考慮使用餘弦類似度來評估二者之間的類似度。餘弦類似度的取值範圍爲\((-1,1)\),餘弦類似度越大,則用戶user_1與用戶user_2之間的類似度越大,反之則二者之間的類似度越小。
餘弦類似度:
\[ w_{uv}=\frac{|N(u)\bigcap{N(v)}|}{\sqrt{|N(u)||{N(v)}|}} \]
# 數據標準化前用戶之間的類似度 user_1 = imputed_df[[0]] user_2 = imputed_df[[1]] user_8 = imputed_df[[7]] user_9 = imputed_df[[8]] print('電影名:{}'.format(df.columns[:-1].values)) print('劉一:{}'.format(user_1)) print('陳二:{}'.format(user_2)) print('周八:{}'.format(user_8)) print('吳九:{}'.format(user_9)) print('劉一和陳二的餘弦類似度:{}'.format(cosine_similarity(user_1, user_2))) print('陳二和周八的餘弦類似度:{}'.format(cosine_similarity(user_2, user_8))) print('周八和吳九的餘弦類似度:{}'.format(cosine_similarity(user_8, user_9)))
電影名:['《肖申克的救贖》' '《控方證人》' '《這個殺手不太冷》' '《霸王別姬》' '《美麗人生》' '《阿甘正傳》' '《辛德勒的名單》'] 劉一:[[0. 4. 0. 4. 0. 5. 0.]] 陳二:[[4. 0. 5. 3. 5. 0. 0.]] 周八:[[2. 0. 2. 0. 1. 0. 2.]] 吳九:[[1. 2. 2. 0. 1. 1. 0.]] 劉一和陳二的餘弦類似度:[[0.18353259]] 陳二和周八的餘弦類似度:[[0.73658951]] 周八和吳九的餘弦類似度:[[0.58536941]]
從上面的輸出的結果能夠看到
對於上述狀況是由於評分爲\(0\)在餘弦類似度看來是有意義的,也就是較之評分爲\(1\)而言評分爲\(0\)更差。即餘弦類似度認爲兩我的對某一部電影評分都是\(0\)的話,那兩我的給電影評分都很低,二者天然會有很高的類似性,這也是爲何劉一和陳二的類似度很低,而陳二和周八類似度及周八和吳九的類似度都很高的緣由。
上一節講到了餘弦類似度可能會把電影評分爲\(0\)較於評分\(1\)認爲用戶可能討厭某部電影,即把\(0\)變得有意義。然而這部電影評分爲\(0\)即用戶沒有看過此電影,它並沒有實際意義,即在餘弦類似度中的\(0\)是應該是中性,也就說不能經過用戶對電影的評分爲\(0\)就所以判斷用戶喜不喜歡這部電影,所以咱們須要對數據作標準化處理。
首先考慮的多是熱編碼處理,它可以讓數據都變成中性,可是熱編碼以後,用戶對其餘電影的評分可能也變得沒有意義。而其餘的標準化處理,如歸一化是爲了作統一尺度處理。所以咱們能夠自定義一個標準化的方式。
該自定義的標準化方式是從新生成全部用戶的評分,使得用戶對全部電影的平均評分爲\(0\),這樣在用戶把用戶沒有打分的電影設爲\(0\)的時候這個\(0\)就成了一箇中性值,即\(0\)在餘弦類似度看來是中性的。而使得用戶的平均評分爲\(0\)的方法也很簡單,能夠對某個用戶的非零評分的每個評分值使用以下公式
\[ s_{std}^{(i)} = x^{(i)}-\mu \]
其中\(x^{(i)}\)表示用戶對某部電影的評分,\(\mu\)表示用戶對全部電影評分的平均評分。
# 標準化評分 def nonzero_mean(np_arr): """計算矩陣內每一行不爲0元素的平均數""" # 找到數組內評分不爲0的即非0元素 exist = (np_arr != 0) # 行非0元素總和 arr_sum = np_arr.sum(axis=1) # 行非0元素總個數 arr_num = exist.sum(axis=1) return arr_sum/arr_num def standard_data(np_arr): standardized_df = np_arr.copy() # 非0元素行下標 nonzero_rows = np.nonzero(np_arr)[0] # 非0元素列下標 nonzero_columns = np.nonzero(np_arr)[1] # 非0元素行平均值 nonzero_rows_mean = nonzero_mean(np_arr) # 遍歷並修改全部非0元素 for ind in range(len(nonzero_rows)): # 第ind個元素的行標和列標肯定一個元素 i = nonzero_rows[ind] j = nonzero_columns[ind] standardized_df[i, j] = round( np_arr[i, j]-nonzero_rows_mean[i], 2) return standardized_df standardized_df = standard_data(imputed_df)
# 數據標準化後用戶之間的類似度 user_1 = standardized_df[[0]] user_2 = standardized_df[[1]] user_8 = standardized_df[[7]] user_9 = standardized_df[[8]] print('電影名:{}'.format(df.columns[:-1].values)) print('劉一:{}'.format(user_1)) print('陳二:{}'.format(user_2)) print('周八:{}'.format(user_8)) print('吳九:{}'.format(user_9)) print('劉一和陳二的餘弦類似度:{}'.format(cosine_similarity(user_1, user_2))) print('陳二和周八的餘弦類似度:{}'.format(cosine_similarity(user_2, user_8))) print('周八和吳九的餘弦類似度:{}'.format(cosine_similarity(user_8, user_9)))
電影名:['《肖申克的救贖》' '《控方證人》' '《這個殺手不太冷》' '《霸王別姬》' '《美麗人生》' '《阿甘正傳》' '《辛德勒的名單》'] 劉一:[[ 0. -0.33 0. -0.33 0. 0.67 0. ]] 陳二:[[-0.25 0. 0.75 -1.25 0.75 0. 0. ]] 周八:[[ 0.25 0. 0.25 0. -0.75 0. 0.25]] 吳九:[[-0.4 0.6 0.6 0. -0.4 -0.4 0. ]] 劉一和陳二的餘弦類似度:[[0.30464382]] 陳二和周八的餘弦類似度:[[-0.3046359]] 周八和吳九的餘弦類似度:[[0.36893239]]
從上面輸出的結果能夠看到劉一和陳二的類似度提升了,而且陳二和周八的類似度明顯下降了,這是符合咱們心理預期的,因爲\(0\)變成了中性,周八和吳九的類似度也有着大幅的下降。
上述過程其實咱們已經自定義了一個模型,只是這個模型可能並無用到咱們以前學習的傳統機器學習算法,他用到的是另外一種算法,即一個推薦算法——《協同過濾算法》。
如今咱們能夠測試咱們的模型,固然因爲沒有真實數據,只能預測某個新用戶對某一部他沒看過的電影給多少評分。如今假設咱們從某個不知名網站弄來了王二麻子和爛穀子對電影的評分,咱們能夠作一個模型基於上述十我的對電影的評分預測他們會給他們沒看過的電影評多少分。
該模型主要是獲取某我的如王二麻子觀看了全部的電影的評分,而後計算王二麻子與其餘全部人之間的餘弦類似度,以後經過王二麻子與其餘用戶的類似度\(*\)類似度對應用戶對王二麻子未看電影的評分\(/\)王二麻子與其餘用戶的總類似度。假設王二麻子與張三的類似度爲\(0.2\),與李四的類似度爲\(0.6\),王二麻子沒看的電影爲《霸王別姬》,而張三對《霸王別姬》的評分爲\(5\),李四對《霸王別姬》的評分爲\(2\),則王二麻子對《霸王別姬》的加權評分爲
\[ {\frac{0.2*5+0.6*2}{0.2+0.6}}=2.75 \]
# 預測 # 對新數據處理成np.array數組 new_user = ''' 1,2,,2,2,2,,王二麻子 2,1,2,,5,1,4,爛穀子 1,2,2,2,3,,4,大芝麻 ''' def predict(new_user, similarity_tool='cosine_similarity'): rating_list = [] movie_list = [] # 對於輸入數據爲或str和numpy數組作不一樣的處理 if isinstance(new_user, str): new_df = pd.read_csv(io.StringIO(new_user), header=None) else: new_df = pd.DataFrame(new_user) new_df.columns = ['《肖申克的救贖》', '《控方證人》', '《這個殺手不太冷》', '《霸王別姬》', '《美麗人生》', '《阿甘正傳》', '《辛德勒的名單》', '姓名'] # 填充數據並對數據進行標準化 imputed_new_df = imputer_data(new_df) standardized_new_user = standard_data(imputed_new_df) # 經過餘弦類似度計算預測用戶與已有樣本之間的類似度 user_similarity_list = [] for ind in range(len(standardized_df)): user = standardized_df[[ind]] mod = sys.modules['__main__'] file = getattr(mod, similarity_tool, None) user_similarity_list.append(cosine_similarity( user, standardized_new_user)[0]) # 將餘弦類似度列表構形成numpy數組方便計算 users_similarity_arr = np.array(user_similarity_list).reshape( standardized_df.shape[0], standardized_new_user.shape[0]) # 遍歷全部用戶對她沒有評分的電影計算預測評分值 for ind in range(len(new_df)): empty_rating_ind = [] # 獲取名字信息 name = new_df['姓名'][ind] nonzero_list = np.nonzero(imputed_new_df[[ind], :])[1] user_similarity_arr = users_similarity_arr[:, [ind]].reshape(1, -1) # 找到預測用戶沒有評分電影的索引 for j in range(standardized_new_user.shape[1]): if j not in nonzero_list: empty_rating_ind.append(j) # 遍歷預測用戶沒有評分的電影計算預測評分值 for rating_ind in empty_rating_ind: # 計算用戶的加權評分總和 user_rating_list = imputed_df[:, rating_ind].reshape(1, -1) rating_arr = user_similarity_arr*user_rating_list rating_sum = rating_arr.sum(axis=1) # 計算用戶的總類似度 user_similarity_sum = user_similarity_arr.sum(axis=1) # 當用戶總類似度爲0時打印提示消息 if rating_sum == 0: print('親,{}不怎麼看電影我實在無能爲力!'.format(name)) else: rating = rating_sum/user_similarity_sum print('*{}*可能會給電影{}評分{}'.format(name, df.columns[:-1].values[rating_ind], round(rating[0], 2))) # 統計評分 rating_list.append(round(rating[0], 2)) empty_rating_ind = [] return rating_list rating_list = predict(new_user)
*王二麻子*可能會給電影《這個殺手不太冷》評分-0.21 *王二麻子*可能會給電影《辛德勒的名單》評分-0.31 *爛穀子*可能會給電影《霸王別姬》評分2.79 *大芝麻*可能會給電影《阿甘正傳》評分3.13
從上面的預測的數據看到,其實預測效果還很不錯。
上面講到了其實還有一種方法,即基於電影(項目)的推薦,即獲取類似度較高的電影。流程爲計算全部被你評分的電影與你未評分的電影之間的餘弦類似度,而後經過與基於用戶推薦相同的方法就能夠實現電影選擇你。實現方法即對上述給出的表格轉置而後修改代碼中的參數便可,此處很少贅述。
l = np.random.randint(0, 6, size=(50, 8)) l[:, [-1]] = 0 rating_list = predict(l)
*0*可能會給電影《這個殺手不太冷》評分0.09 *0*可能會給電影《肖申克的救贖》評分2.59 *0*可能會給電影《控方證人》評分1.9 *0*可能會給電影《肖申克的救贖》評分4.62 *0*可能會給電影《控方證人》評分19.66 *0*可能會給電影《肖申克的救贖》評分3.34 *0*可能會給電影《美麗人生》評分1.26 *0*可能會給電影《阿甘正傳》評分9.15 *0*可能會給電影《控方證人》評分0.27 *0*可能會給電影《控方證人》評分2.42 *0*可能會給電影《這個殺手不太冷》評分0.36 *0*可能會給電影《美麗人生》評分3.18 *0*可能會給電影《控方證人》評分0.46 *0*可能會給電影《這個殺手不太冷》評分1.67 *0*可能會給電影《控方證人》評分3.79 *0*可能會給電影《美麗人生》評分2.18 *0*可能會給電影《霸王別姬》評分1.67 *0*可能會給電影《美麗人生》評分2.36 *0*可能會給電影《阿甘正傳》評分-1.8 *0*可能會給電影《這個殺手不太冷》評分1.89 *0*可能會給電影《霸王別姬》評分2.5 *0*可能會給電影《辛德勒的名單》評分0.31 *0*可能會給電影《霸王別姬》評分0.87 *0*可能會給電影《美麗人生》評分2.98 *0*可能會給電影《肖申克的救贖》評分1.95 *0*可能會給電影《美麗人生》評分1.37 *0*可能會給電影《肖申克的救贖》評分-0.26 親,0不怎麼看電影我實在無能爲力! 親,0不怎麼看電影我實在無能爲力! 親,0不怎麼看電影我實在無能爲力! 親,0不怎麼看電影我實在無能爲力! *0*可能會給電影《肖申克的救贖》評分2.36 *0*可能會給電影《霸王別姬》評分-6.45 *0*可能會給電影《控方證人》評分1.23 *0*可能會給電影《這個殺手不太冷》評分0.41 *0*可能會給電影《霸王別姬》評分-3.71 *0*可能會給電影《美麗人生》評分1.7 *0*可能會給電影《這個殺手不太冷》評分0.8 *0*可能會給電影《美麗人生》評分1.82 *0*可能會給電影《肖申克的救贖》評分2.37 *0*可能會給電影《這個殺手不太冷》評分-0.43 *0*可能會給電影《霸王別姬》評分1.25 *0*可能會給電影《肖申克的救贖》評分1.73 *0*可能會給電影《這個殺手不太冷》評分1.02 *0*可能會給電影《阿甘正傳》評分2.18 *0*可能會給電影《辛德勒的名單》評分-0.66 *0*可能會給電影《美麗人生》評分3.2 *0*可能會給電影《這個殺手不太冷》評分-0.83 *0*可能會給電影《肖申克的救贖》評分2.59 *0*可能會給電影《美麗人生》評分2.14 *0*可能會給電影《辛德勒的名單》評分2.2 *0*可能會給電影《美麗人生》評分2.9 *0*可能會給電影《美麗人生》評分1.86 *0*可能會給電影《阿甘正傳》評分0.82 *0*可能會給電影《美麗人生》評分7.91 *0*可能會給電影《控方證人》評分10.1 *0*可能會給電影《這個殺手不太冷》評分-4.76 *0*可能會給電影《這個殺手不太冷》評分9.9 *0*可能會給電影《美麗人生》評分2.69 *0*可能會給電影《辛德勒的名單》評分2.41 *0*可能會給電影《肖申克的救贖》評分2.07 *0*可能會給電影《辛德勒的名單》評分2.55 *0*可能會給電影《肖申克的救贖》評分1.78 *0*可能會給電影《霸王別姬》評分1.12 *0*可能會給電影《肖申克的救贖》評分1.67 *0*可能會給電影《美麗人生》評分1.59 *0*可能會給電影《肖申克的救贖》評分1.7 *0*可能會給電影《這個殺手不太冷》評分3.54 *0*可能會給電影《美麗人生》評分2.0
測試結果能夠看出模型其實還行,由於評分太高和無評分的現象不多,即符合正態分佈。