- 用隐语义模型来进行协同过滤的目标
- 揭示隐藏的特征,这些特征能够解释为什么给出对应的预测评分
- 这类特征可能是无法直接用语言解释描述的,事实上我们并不需要知道,类似“玄学”
- 通过矩阵分解进行降维分析
- 协同过滤算法非常依赖历史数据,而一般的推荐系统中,偏好数据又往往是稀疏的;这就需要对原始数据做降维处理
- 分解之后的矩阵,就代表了用户和物品的隐藏特征
- 隐语义模型的实例
- 基于概率的隐语义分析(pLSA)
- 隐式迪利克雷分布模型(LDA)
- 矩阵因子分解模型(基于奇异值分解的模型,SVD)
LFM 降维方法 —— 矩阵因子分解
- 假如用户物品评分矩阵为 R,现在有 m 个用户,n 个物品
- 我们想要发现 k 个隐类,我们的任务就是找到两个矩阵 P 和 Q,使这两个矩阵的乘积近似等于 R,即将用户物品评分矩阵 R 分解成为两个低维矩阵相乘:
R m × n ^ = P m × k T ⋅ Q k × k ≈ R \hat{R_{m\times n}} = P_{m\times k}^{T}\cdot Q_{k\times k} \approx R Rm×n^=Pm×kT⋅Qk×k≈R
LFM 的进一步理解
- 我们可以认为,用户之所以给电影打出这样的分数,是有内在原因的,我们可以挖掘出影响用户打分的隐藏因素,进而根据未评价电影与这些隐藏因素的关联度,决定此未评价电影的预测评分
- 应该有一些隐藏的因素,影响用户的打分,比如电影:演员、题材、年代… 甚至不一定是人直接可以理解的隐藏因子
- 找到隐藏因子,可以对 user 和 item 进行关联(找到是由于什么使得 user 喜欢/不喜欢此 item,什么会决定 user 喜欢/不喜欢此 item),就可以推测用户是否会喜欢某一部未看过的电影
- 对于用户看过的电影,会有相应的打分,但一个用户不可能看过所有电影,对于用户没有看过的电影是没有评分的,因此用户评分矩阵大部分项都是空的,是一个稀疏矩阵
`Movie1 Movie2 Movie3 Movie4 user1 1 2 user2 5 1 user3 2 0 user4 4 2 user5 1 2 user6 3 5 user7 5 1 user8 1 0 user9 2 3
- 如果我们能够根据用户已有电影的打分推测出用户会给没有看过的电影的打分,那么就可以根据预测结果给用户推荐他可能打高分的电影
矩阵因子分解
- 我们现在来做一个一般性的分析
- 一个
m
×
n
m\times n
m×n 的打分矩阵 R 可以用两个小矩阵
P
m
×
n
P_{m\times n}
Pm×n 和
Q
k
×
n
Q_{k\times n}
Qk×n 的乘积
R
^
\hat{R}
R^ 来近似:
R u i ^ = P u T ⋅ Q i = ∑ k = 1 K P u k ⋅ Q k i \hat{R_{ui}} = P_{u}^{T}\cdot Q_{i}= \sum_{k=1}^{K}P_{uk}\cdot Q_{ki} Rui^=PuT⋅Qi=k=1∑KPuk⋅Qki - 得到
P
m
×
k
P_{m\times k}
Pm×k 和
Q
k
×
n
Q_{k\times n}
Qk×n 的乘积
R
^
\hat{R}
R^ 不再是稀疏矩阵的,之前 R 中没有的项也可以由 P、Q 的乘积算出,这就得到了一个 预测评分矩阵
`Movie1 Movie2 Movie3 Movie4 user1 1 4 2 5 user2 3 5 1 1 user3 2 4 0 5 user4 1 4 0 2 user5 1 3 2 1 user6 3 1 2 5 user7 5 1 1 2 user8 2 1 0 0 user9 2 2 3 4
- 如果得到的预测评分矩阵
R
^
\hat{R}
R^ 与原评分矩阵 R 在已知评分位置上的值都近似,那么我们认为它们在预测位置上的值也是近似的
模型的求解算法——ALS
ALS算法具体过程如下:
- 1、为 Q 指定一个初值 Q 0 Q_{0} Q0,可以是 随机生成或者全局平均值
- 2、固定当前 Q 0 Q_{0} Q0值,求解 P 0 P_{0} P0
- 3、固定当前 P 0 P_{0} P0值,求解 Q 1 Q_{1} Q1
- 4、固定当前 Q 1 Q_{1} Q1值,求解 P 1 P_{1} P1
- 5、……(重复以上过程)
- 6、直到损失函数的值 C 收敛,迭代结束
算法实现代码如下:
- 1、LFM(隐语义模型)梯度下降算法实现
- 1.1、引入依赖
import numpy as np import pandas as pd
- 1.2、数据准备
# 评分矩阵 R R = np.array([[4, 0, 2, 0, 1], [0, 2, 3, 0, 0], [1, 0, 2, 4, 0], [5, 0, 0, 3, 1], [0, 0, 1, 5, 1], [0, 3, 2, 4, 1],])
- 1.3、算法实现
""" @输入参数: R:M*N 的评分矩阵 K:隐特征向量维度 max_iter:最大迭代次数 alpha:步长 lambada:正则化系数 @%输出: 分解之后的 P,Q P:初始化用户特征矩阵 M*K Q:初始化物品特征矩阵 N*K """ # 给定超参数 K = 5 max_iter = 5000 alpha = 0.0002 lambada = 0.004 # 核心算法 def LFM_grad_desc(R, K=2, max_iter=1000, alpha=0.0001, lambada=0.002): # 基本维度参数定义 M = len(R) N = len(R[0]) # P,Q 初始值,随机生成 P = np.random.rand(M, K) Q = np.random.rand(N, K) Q = Q.T # 开始迭代 for step in range(max_iter): # 对所有的用户 user、物品 item 做遍历,对应的特征向量 Pu、Qi梯度下降 for user in range(M): for item in range(N): # 对于每一个大于 0 的评分,求出预测评分误差 if R[user][item] > 0: eui = np.dot(P[user, :], Q[:, item]) - R[user][item] # 代入公式,按照梯度下降算法更新当前的 Pu、Qi for k in range(K): P[user][k] = P[user][k] - alpha * (2 * eui * Q[k][item] + 2 * lambada * P[user][k]) Q[k][item] = Q[k][item] - alpha * (2 * eui * P[user][k] + 2 * lambada * Q[k][item]) # user、item 遍历完成,所有特征向量更新完成,可以得到 P、Q,可以计算预测评分矩阵 predR = np.dot(P, Q) # 计算当前损失函数 cost = 0 for user in range(M): for item in range(N): if R[user][item] > 0: cost += (np.dot(P[user, :], Q[:, item]) - R[user][item]) ** 2 # 加上正则化项 for k in range(K): cost += lambada * (P[user][k] ** 2 + Q[k][item] ** 2) if cost < 0.0001: break return P, Q.T, cost
- 1.4、测试
P, Q, cost = LFM_grad_desc(R, K, max_iter, alpha, lambada) print(P) print(Q) print(cost) print(R) preR = P.dot(Q.T) preR
[[ 0.77647888 0.63447284 0.80512534 0.55895106 0.96240798] [ 1.32550446 0.88331475 0.62267974 0.02980579 0.7550383 ] [ 1.03659297 0.93987934 0.72430912 -0.22346398 -0.4749907 ] [ 0.77301525 0.49679317 0.57141146 0.693392 1.60075878] [ 0.57612612 0.63976908 1.49718539 0.77990291 -0.03548665] [ 0.50209193 0.92510101 0.89396375 0.69306917 1.07528177]] [[ 0.85738419 0.73837126 0.69663037 1.09029338 1.75496549] [ 0.07742206 0.60860856 1.0905473 0.54186881 0.92432084] [ 1.25494006 0.96409017 -0.01302121 -0.32074926 0.63812769] [ 1.2074824 1.52394386 1.92146259 0.53896068 -0.09707023] [ 0.05525611 0.37630539 0.54292178 -0.10977234 0.30147462]] 0.5609786914291215 [[4 0 2 0 1] [0 2 3 0 0] [1 0 2 4 0] [5 0 0 3 1] [0 0 1 5 1] [0 3 2 4 1]] array([[3.99350543, 2.51674135, 2.01049581, 3.65933526, 0.94756504], [3.58002164, 2.03332638, 2.97916634, 4.08587363, 0.96805775], [1.01008047, 0.88203516, 1.96613029, 4.00139316, 0.68553548], [4.99292856, 2.84069443, 2.24068429, 3.00675796, 0.94636437], [2.79737849, 2.45652931, 1.04750538, 4.97120172, 0.98912751], [4.37904712, 2.94626505, 1.97420123, 4.00294624, 1.10930686]])