对于推荐系统,有很多的很强大的算法。这里作为练习,只介绍基本的协同过滤算法(userbased)和FM(通过梯度下降的角度,还可以通过交替优化的角度来看)。
这里的例子是在七月算法的视频中看的,分析的内容基于自己的理解并对代码做了部分的勘误。
(一)简单的user-based协同过滤算法
先来看一眼数据:
userss = {"小明": {"中国合伙人": 5.0, "太平轮": 3.0, "荒野猎人": 4.5,"老炮儿": 5.0, "我的少女时代": 3.0,"肖洛特烦恼": 4.5, "火星救援": 5.0},"小红": {"小时代4": 4.0, "荒野猎人": 3.0, "我的少女时代": 5.0,
"肖洛特烦恼": 5.0, "火星救援": 3.0,"后会无期": 3.0},"小阳": {"小时代4": 2.0, "中国合伙人": 5.0, "我的少女时代": 3.0,
"老炮儿": 5.0, "肖洛特烦恼": 4.5,"速度与激情7": 5.0},"小四": {"小时代4": 5.0, "中国合伙人": 3.0, "我的少女时代": 4.0,"匆匆那年": 4.0, "速度与激情7": 3.5, "火星救援": 3.5, "后会无期": 4.5},"六爷": {"小时代4": 2.0, "中国合伙人": 4.0, "荒野猎人": 4.5,"老炮儿": 5.0, "我的少女时代": 2.0},"小李": {"荒野猎人": 5.0, "盗梦空间": 5.0, "我的少女时代": 3.0,
"速度与激情7": 5.0, "蚁人": 4.5,"老炮儿": 4.0, "后会无期": 3.5},"隔壁老王": {"荒野猎人": 5.0, "中国合伙人": 4.0,
"我的少女时代": 1.0, "Phoenix": 5.0,"甄嬛传": 4.0, "The Strokes": 5.0},"邻村小芳": {"小时代4": 4.0, "我的少女时代": 4.5,"匆匆那年": 4.5, "甄嬛传": 2.5, "The Strokes": 3.0}}
这里是假想的一份数据,分别是每个用户对于自己看过的电影的评分。这里要进行userbased协同过滤,首先就要解释一下什么是userbased协同过滤;所谓userbased协同过滤,就是把一个用户看成是一条记录,然后看到的电影作为属性,对应的评分作为属性值。然后通过计算两条记录(两个user)之间的距离来衡量两条记录(两个user)的相近度。最后把距离最近的K(自己指定)的users看过而该user未看过的电影推荐给该user。所以最重要的就是计算距离了,下面介绍几种距离的计算方式:
(1)欧几里得距离
def euclidean_dis(rating1, rating2):"""计算2个打分序列间的欧式距离. 输入的rating1和rating2都是打分dict格式为{'小时代4': 1.0, '疯狂动物城': 5.0}"""distance = 0common_ratings = Falsefor key in rating1:if key in rating2:distance += pow(rating1[key] - rating2[key], 2)common_ratings = True# 两个打分序列之间有公共打分电影if common_ratings:return distance# 无公共打分电影else:return float('inf')
(2)曼哈顿距离
def manhattan_dis(rating1, rating2):"""计算2个打分序列间的曼哈顿距离. 输入的rating1和rating2都是打分dict格式为{'小时代4': 1.0, '疯狂动物城': 5.0}"""distance = 0common_ratings = Falsefor key in rating1:if key in rating2:distance += abs(rating1[key] - rating2[key])common_ratings = True# 两个打分序列之间有公共打分电影if common_ratings:return distance# 无公共打分电影else:return float('inf')
(3)COS距离
def cos_dis(rating1, rating2):"""计算2个打分序列间的cos距离. 输入的rating1和rating2都是打分dict格式为{'小时代4': 1.0, '疯狂动物城': 5.0}"""distance = 0dot_product_1 = 0dot_product_2 = 0common_ratings = Falsefor score in rating1.values():dot_product_1 += pow(score, 2)for score in rating2.values():dot_product_2 += pow(score, 2)for key in rating1:if key in rating2:distance += rating1[key] * rating2[key]common_ratings = True# 两个打分序列之间有公共打分电影# 注意,上面的distance是相似度,相似度越大,距离就会越小,所以用1来减if common_ratings:return 1 - distance/sqrt(dot_product_1 * dot_product_2)# 无公共打分电影else:return 2
(4)Person距离(相关系数)
这里使用了,
def pearson_dis(rating1, rating2):"""计算2个打分序列间的pearson距离. 输入的rating1和rating2都是打分dict格式为{'小时代4': 1.0, '疯狂动物城': 5.0}"""sum_xy = 0sum_x = 0sum_y = 0sum_x2 = 0sum_y2 = 0n = 0for key in rating1:if key in rating2:n += 1x = rating1[key]y = rating2[key]sum_xy += x * ysum_x += xsum_y += ysum_x2 += pow(x, 2)sum_y2 += pow(y, 2)# now compute denominatordenominator = sqrt(sum_x2 - pow(sum_x, 2) / n) * sqrt(sum_y2 - pow(sum_y, 2) / n)# 相关系数的取值在-1到1之间,值越大相关性越大,距离就越小,所以要用1来减一下if denominator == 0:return 2else:return 1 - (sum_xy - (sum_x * sum_y) / n) / denominator
有了各种距离的定义,我们就可以根据定义的距离来计算两个user之间的想近程度了,计算方法如下:
# 查找最近邻
def compute_nearest_neighbor(username, users):"""在给定username的情况下,计算其他用户和它的距离并排序"""distances = []# 查找最近邻的时候要除去自身,要不然肯定是自己离自己最近了for user in users:if not user == username:distance = pearson_dis(users[user], users[username])# distance = cos_dis(users[user], users[username])# distance = manhattan_dis(users[user], users[username])# distance = euclidean_dis(users[user], users[username])distances.append((distance, user))distances.sort()print(distances)return distances
最终进行推荐:
# 推荐
def recommend(username, users):"""对指定的user推荐电影"""# 找到最邻近nearest = compute_nearest_neighbor(username, users)[0][1]print("爱好相同的人:", nearest)recommendations = []# 找到最邻近看过, 但是username本身没有看过的电影, 计算推荐neighbor_ratings = users[nearest]user_ratings = users[username]for artist in neighbor_ratings:if artist not in user_ratings:recommendations.append((artist, neighbor_ratings[artist]))results = sorted(recommendations, key=lambda artist_tuple: artist_tuple[1], reverse=True)print("推荐电影:")for result in results:print(result[0], result[1])
测试:
recommend('六爷', userss)
测试结果:
(二)简单的张量分解来实现推荐
首先,把数据整理成一个矩阵,缺失的用0(根据实际情况来定)来表示
主要的目标就是填充缺失的评分,这里使用FunkSVD,其代价函数为:
,
详情请参见https://www.cnblogs.com/pinard/p/6351319.html
使用SGD来做优化,代码如下:
def matrix_factorization(R, P, Q, K, steps=10000, alpha=0.0002, beta=0.02):Q = Q.Tfor step in range(steps):for i in range(len(R)):for j in range(len(R[i])):if R[i][j] > 0:e_ij = R[i][j] - np.dot(P[i, :], Q[:, j])for k in range(K):P[i][k] = P[i][k] + alpha * (2 * e_ij * Q[k][j] - beta * P[i][k])Q[k][j] = Q[k][j] + alpha * (2 * e_ij * P[i][k] - beta * Q[k][j])e = 0for i in range(len(R)):for j in range(len(R[i])):if R[i][j] > 0:e = e + pow(R[i][j] - np.dot(P[i, :], Q[:, j]), 2)for k in range(K):e = e + (beta / 2) * (pow(P[i][k], 2) + pow(Q[k][j], 2))if e < 0.001:breakreturn P, Q.T
使用上述函数进行缺失值的填充:
R = [[5.0, 0, 0, 3.5, 5.0, 0, 0, 0],[0, 0, 2.5, 0, 0, 4.0, 0, 0],[0, 0, 0, 0, 5.0, 0, 0, 0],[0, 0, 0, 0, 4.5, 0, 0, 0],[5.0, 5.0, 0, 0, 4.0, 0, 0, 5.0],[0, 0, 3.0, 0, 0, 5.0, 0, 0],[4.5, 0, 0, 0, 0, 0, 5.0, 4.5],[3.0, 2.0, 4.5, 4.0, 3.0, 1.0, 5.0, 3.0],[2.0, 2.0, 4.0, 5.0, 0, 0, 4.0, 0],[0, 0, 4.5, 4.0, 0, 0, 0, 0],[0, 0, 0, 3.5, 0, 0, 3.0, 5.0],[0, 0, 0, 0, 0, 5.0, 0, 0],[0, 0, 0, 0, 0, 0, 0, 3.0],[0, 0, 0, 4.5, 3.5, 0, 3.0, 0],[0, 4.5, 0, 0, 5.0, 5.0, 3.0, 4.5],[5.0, 4.0, 0, 3.0, 0, 4.0, 0, 5.0]]
R = np.array(R)
M, N = np.shape(R)
# 设置隐藏属性的个数为2
K = 2
P = np.random.rand(M, K)
Q = np.random.rand(N, K)
nP, nQ = matrix_factorization(R, P, Q, K)
nR = np.dot(nP, nQ.T)
nR = np.around(nR, decimals=1)
print("更新后的得分矩阵为")
print(nR)
运行结果: