更新、更全的《机器学习》的更新网站,更有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
测试结果能够看出模型其实还行,由于评分太高和无评分的现象不多,即符合正态分布。