实验和代码实现
https://github.com/Myolive-Lin/RecSys--deep-learning-recommendation-system
引言
推荐系统已经成为现代互联网的重要组成部分,特别是在电商、社交媒体、视频平台等领域,帮助用户发现他们可能感兴趣的商品或内容。推荐系统的核心目标是根据用户的历史行为或偏好,预测他们对未见过物品的评分或兴趣程度。矩阵分解作为推荐系统中的一种经典技术,在大规模数据上有着高效的表现。本文将详细介绍矩阵分解的基本原理、应用场景及其在推荐系统中的实现方法。(注其他方法可以在往期中进行查看
)
矩阵分解的基本原理
矩阵分解(Matrix Factorization)是将一个大的稀疏矩阵(例如用户-物品评分矩阵)分解成两个低秩矩阵的过程。这些低秩矩阵表示了用户和物品之间的隐含特征。通过矩阵分解,我们可以更高效地进行评分预测。
假设我们有一个用户-物品评分矩阵 R R R,其中行表示用户,列表示物品,矩阵中的每个元素 r u i r_{ui} rui 表示用户 u u u 对物品 i i i 的评分。如果评分矩阵是稀疏的,即很多评分为空,我们的目标是填补这些空缺的评分,预测用户对未评过的物品的兴趣。
矩阵分解的核心思想是将评分矩阵 R R R 分解成两个低秩矩阵:
- P P P:用户隐特征矩阵,形状为 m × k m \times k m×k,其中 m m m 是用户数量, k k k 是隐特征的维度。
- Q Q Q:物品隐特征矩阵,形状为 k × n k \times n k×n,其中 n n n 是物品数量, k k k 是隐特征的维度。
我们的目标是将评分矩阵 R R R 近似为: R ≈ P × Q R \approx P \times Q R≈P×Q
其中 P × Q P \times Q P×Q 是对评分矩阵的预测。每一行 P u P_u Pu 表示用户 u u u 的隐特征向量,每一列 Q i Q_i Qi 表示物品 i i i 的隐特征向量。
评分矩阵示例
假设我们有如下的用户-物品评分矩阵 R R R,其中用户对物品的评分是一个稀疏矩阵,一些评分缺失:
R = [ 5 3 0 1 4 0 0 1 1 1 0 5 1 0 0 4 0 1 5 4 ] R = \begin{bmatrix} 5 & 3 & 0 & 1 \\ 4 & 0 & 0 & 1 \\ 1 & 1 & 0 & 5 \\ 1 & 0 & 0 & 4 \\ 0 & 1 & 5 & 4 \\ \end{bmatrix} R= 54110301010000511544
其中, r u i r_{ui} rui 表示用户 u u u 对物品 i i i 的评分。例如,用户 1 对物品 1 的评分为 5,用户 2 对物品 2 的评分为 0(即缺失)。
用户矩阵 P P P 和物品矩阵 Q Q Q
在矩阵分解中,我们将评分矩阵 R R R 分解为两个矩阵:用户矩阵 P P P 和物品矩阵 Q Q Q。用户矩阵 P P P 表示用户对潜在特征的偏好,而物品矩阵 Q Q Q 表示物品在这些潜在特征上的表现。
假设我们将矩阵分解为 k = 2 k=2 k=2 个潜在特征,用户矩阵 P P P 和物品矩阵 Q Q Q 如下:
用户矩阵 P P P:
P = [ 2.31045989 0.74082556 1.8393161 0.67679475 0.29166742 2.17961771 0.32361523 1.75546914 0.4216019 1.77386313 ] P = \begin{bmatrix} 2.31045989 & 0.74082556 \\ 1.8393161 & 0.67679475 \\ 0.29166742 & 2.17961771 \\ 0.32361523 & 1.75546914 \\ 0.4216019 & 1.77386313 \end{bmatrix} P= 2.310459891.83931610.291667420.323615230.42160190.740825560.676794752.179617711.755469141.77386313
物品矩阵 Q Q Q:
Q = [ 2.10425048 1.20169726 1.97073538 − 0.31480952 0.1800648 0.29058456 2.34189001 2.33241682 ] Q = \begin{bmatrix} 2.10425048 & 1.20169726 & 1.97073538 & -0.31480952 \\ 0.1800648 & 0.29058456 & 2.34189001 & 2.33241682 \end{bmatrix} Q=[2.104250480.18006481.201697260.290584561.970735382.34189001−0.314809522.33241682]
预测评分矩阵 R ^ \hat{R} R^
通过矩阵分解,预测评分矩阵 R ^ \hat{R} R^ 可以通过 P P P 和 Q Q Q 的乘积得到:
R ^ = P Q \hat{R} = P Q R^=PQ
这里, P P P 是用户矩阵, Q Q Q 是物品矩阵。我们通过计算 P P P 和 Q Q Q 的矩阵乘积来得到预测的评分矩阵:
R ^ = [ 4.995183 2.991746 6.288237 1.000559 3.992249 2.406967 5.209784 0.999533 1.006214 0.983859 5.679224 4.991957 0.997066 0.899000 4.748876 3.992609 1.206566 1.022095 4.985058 4.004664 ] \hat{R} = \begin{bmatrix} 4.995183 & 2.991746 & 6.288237 & 1.000559 \\ 3.992249 & 2.406967 & 5.209784 & 0.999533 \\ 1.006214 & 0.983859 & 5.679224 & 4.991957 \\ 0.997066 & 0.899000 & 4.748876 & 3.992609 \\ 1.206566 & 1.022095 & 4.985058 & 4.004664 \end{bmatrix} R^= 4.9951833.9922491.0062140.9970661.2065662.9917462.4069670.9838590.8990001.0220956.2882375.2097845.6792244.7488764.9850581.0005590.9995334.9919573.9926094.004664
这就是根据用户矩阵和物品矩阵的乘积得到的预测评分矩阵,通过这个矩阵,就可以预测用户对未评价物品的评分
矩阵分解中的损失函数
矩阵分解的目标是最小化预测评分与实际评分之间的误差。为了衡量预测评分与实际评分之间的差异,我们使用均方误差(Mean Squared Error,MSE)作为损失函数:
L = ∑ ( u , i ) ∈ non-zero entries ( r u i − q i T p u ) 2 + λ ( ∣ ∣ q i ∣ ∣ 2 + ∣ ∣ p u ∣ ∣ 2 ) \begin{aligned} L = \sum_{(u,i) \in \text{non-zero entries}} (r_{ui} - q{_i}^{T} p_u)^2 + \lambda(||q_i||^2 + ||p_u||^2)\\ \end{aligned} L=(u,i)∈non-zero entries∑(rui−qiTpu)2+λ(∣∣qi∣∣2+∣∣pu∣∣2)
其中:
- r u i r_{ui} rui 是用户 u u u 对物品 i i i 的实际评分。
- $q_i^T p_u $ 是用户 u u u 对物品 i i i 的预测评分。其中 p u p_u pu是用户u在用户矩阵P中的对应行向量, q i q_i qi是物品i在物品矩阵Q中的对应列向量。
- λ \lambda λ 是正则化参数,防止模型过拟合。
正则化项 λ ( ∣ ∣ p u ∣ ∣ 2 + ∣ ∣ q i ∣ ∣ 2 ) \lambda \left(||p_u||^2 + ||q_i||^2 \right) λ(∣∣pu∣∣2+∣∣qi∣∣2) 是用来惩罚较大的特征值,使得模型能够避免过拟合。通过调整 λ \lambda λ 的值,可以控制正则化的强度。
梯度下降法优化矩阵分解
矩阵分解的目标是通过优化损失函数来得到 P P P 和 Q Q Q。常用的优化方法是梯度下降法(Gradient Descent)。梯度下降法通过计算损失函数的梯度并沿着梯度方向更新参数。
损失函数关于 P u P_u Pu 和 Q i Q_i Qi 的梯度分别为:
∂ L ∂ P u = − 2 ⋅ ( r u i − q i T p u ) ⋅ q i + 2 λ P u \begin{aligned} \frac{\partial L}{\partial P_u} = -2 \cdot (r_{ui} - q_i^Tp_u) \cdot q_i + 2 \lambda P_u \end{aligned} ∂Pu∂L=−2⋅(rui−qiTpu)⋅qi+2λPu
∂ L ∂ q i = − 2 ⋅ ( r u i − q i T p u ) ⋅ p u + 2 λ q i \begin{aligned} \frac{\partial L}{\partial q_i} = -2 \cdot (r_{ui} - q_i^Tp_u) \cdot p_u + 2 \lambda q_i \end{aligned} ∂qi∂L=−2⋅(rui−qiTpu)⋅pu+2λqi
然后通过更新规则:
P u : = P u − η ⋅ ∂ L ∂ P u Q i : = Q i − η ⋅ ∂ L ∂ Q i \begin{aligned} P_u := P_u - \eta \cdot \frac{\partial L}{\partial P_u} \\ Q_i := Q_i - \eta \cdot \frac{\partial L}{\partial Q_i} \end{aligned} Pu:=Pu−η⋅∂Pu∂LQi:=Qi−η⋅∂Qi∂L
其中, η \eta η 是学习率,控制更新步长。
通过迭代优化这些隐特征矩阵 P P P 和 Q Q Q,我们能够逐渐减少预测误差,最终得到较为准确的评分预测。
Code
#使用for循环方法
def matrix_factorization_original(Matrix, k, learning_rate=0.01, reg_param=0.1, iterations=1000, seed=None):
"""
Matrix: 评分矩阵
k: 隐特征维度
learning_rate: 学习率
iterations: 迭代次数
seed: 随机数种子
"""
if seed is not None:
np.random.seed(seed)
rows, cols = Matrix.shape
# Initialize user matrix and item matrix
user_matrix = np.random.rand(rows, k).astype(np.float32)
item_matrix = np.random.rand(k, cols).astype(np.float32)
pbar = tqdm(range(iterations), desc="Training Progress", ncols=100, unit="iter")
for iter in pbar:
total_loss = 0 # 用来计算每次的总误差
reg_loss = 0 # 用来计算每次的正则化损失
for i in range(rows):
for j in range(cols):
if Matrix[i, j] > 0:
error = Matrix[i, j] - np.dot(item_matrix[:, j], user_matrix[i, :])
# 计算物品矩阵的梯度
q_i = item_matrix[:, j].copy()
q_grad = -2 * error * user_matrix[i, :] + 2 * reg_param * item_matrix[:, j]
item_matrix[:, j] -= learning_rate * q_grad
# 计算用户矩阵的梯度
p_grad = -2 * error * q_i + 2 * reg_param * user_matrix[i, :]
user_matrix[i, :] -= learning_rate * p_grad
# 计算总损失(错误项)
total_loss += (error ** 2)
# 计算正则化损失
reg_loss += reg_param * (np.sum(user_matrix[i, :]**2) + np.sum(item_matrix[:, j]**2))
total_loss += reg_loss
pbar.set_postfix(error=total_loss)
return user_matrix, item_matrix
利用矩阵的计算方法
上面的式子可以推广到利用矩阵进行计算
E = ( R − U V ) \begin{aligned} E = (R - UV) \end{aligned} E=(R−UV)
更新用户矩阵 (U):
U
=
U
+
η
⋅
(
2
⋅
E
⋅
V
T
−
2
λ
U
)
\begin{aligned} U = U + \eta \cdot \left( 2 \cdot E \cdot V^T - 2\lambda U \right) \end{aligned}
U=U+η⋅(2⋅E⋅VT−2λU)
更新物品矩阵 (V):
V
=
V
+
η
⋅
(
2
⋅
E
T
⋅
U
−
2
λ
V
)
\begin{aligned} V = V + \eta \cdot \left( 2 \cdot E^T \cdot U - 2\lambda V \right) \end{aligned}
V=V+η⋅(2⋅ET⋅U−2λV)
其中
- ( U ∈ R m × k ) (U \in \mathbb{R}^{m \times k}) (U∈Rm×k): 用户矩阵,每一行表示一个用户的特征向量 ( p u ) (p_u) (pu)。
- ( V ∈ R k × n (V \in \mathbb{R}^{k \times n} (V∈Rk×n: 物品矩阵,每一列表示一个物品的特征向量 ( q i ) (q_i) (qi)。
Code
def matrix_factorization(matrix,k, learning_rate = 0.01, reg_param=0.1,iterations = 1000, seed = 42):
"""
matrix: 评分矩阵
k: 隐特征维度
learning_rate: 学习率
iterations: 迭代次数
seed: 随机数种子
"""
if seed:
np.random.seed(seed)
rows,cols = matrix.shape
# Initialize user matrix and item matrix
user_matrix = np.random.rand(rows, k)
item_matrix = np.random.rand(k,cols)
#Only use non-zero elements in the matrix to optimize
mask = (matrix > 0).astype(np.float32)
pbar = tqdm(range(iterations),desc= "Training",ncols = 100, unit = 'iter')
for i in pbar:
Predicted = np.dot(user_matrix,item_matrix)
#这里值计算了非零元素的损失
error = (matrix - Predicted) * mask
#Gradient calculation and update
user_grad = -2 *np.dot(error,item_matrix.T) + 2 * reg_param * user_matrix
user_matrix -= learning_rate * (user_grad)
item_grad = -2 * np.dot(user_matrix.T, error) + 2 * reg_param * item_matrix
item_matrix -= learning_rate * item_grad
#Calculate the error
mse = np.sum(error ** 2)
# 计算正则化项
reg_term = reg_param * (np.sum(user_matrix ** 2) + np.sum(item_matrix ** 2))
# 计算带正则化的代价函数
cost = mse + reg_term
pbar.set_postfix(MSE=mse, Cost=cost) # 显示MSE和代价函数
return user_matrix,item_matrix
矩阵分解中的偏差项
在实际应用中,除了用户和物品的隐特征矩阵外,我们还需要考虑用户和物品的偏差。不同用户的评分习惯不同,可能导致评分系统的偏差。例如,某些用户可能普遍给出较高的评分,而另一些用户可能给出较低的评分。此外,不同物品的评分标准也可能存在差异。 为了消除这些偏差,通常在矩阵分解模型中加入以下偏差项:
- 全局偏差( u u u):所有评分的平均值。
- 用户偏差( b u b_u bu):用户 u u u 对所有物品的评分平均值。
- 物品偏差( b i b_i bi):物品 i i i 的平均评分。 因此,评分的预测公式可以更新为:
r u i = u + b u + b i + q i T p u \begin{aligned} r_{ui} = u + b_u + b_i + q_i^Tp_u \end{aligned} rui=u+bu+bi+qiTpu
损失函数
$$
L = \sum_{(u,i) \in \text{non-zero entries}} \left[ (r_{ui} - (u + b_u + b_i + p_u^T q_i))^2 \right] \
- \lambda \left(||p_u||^2 + ||q_i||^2 + b_u^2 + b_i^2 \right)
$$
对用户矩阵
p
u
p_u
pu求导:
∂
L
∂
p
u
=
−
2
∑
i
∈
non-zero entries
(
r
u
i
−
(
u
+
b
u
+
b
i
+
p
u
T
q
i
)
)
q
i
+
2
λ
p
u
\frac{\partial L}{\partial p_u} = -2 \sum_{i \in \text{non-zero entries}} (r_{ui} - (u + b_u + b_i + p_u^T q_i)) q_i + 2 \lambda p_u
∂pu∂L=−2i∈non-zero entries∑(rui−(u+bu+bi+puTqi))qi+2λpu
对物品矩阵
q
i
q_i
qi 求偏导:
∂
L
∂
q
i
=
−
2
∑
u
∈
non-zero entries
(
r
u
i
−
(
u
+
b
u
+
b
i
+
p
u
T
q
i
)
)
p
u
+
2
λ
q
i
\frac{\partial L}{\partial q_i} = -2 \sum_{u \in \text{non-zero entries}} (r_{ui} - (u + b_u + b_i + p_u^T q_i)) p_u + 2 \lambda q_i
∂qi∂L=−2u∈non-zero entries∑(rui−(u+bu+bi+puTqi))pu+2λqi
对用户偏差
b
u
b_u
bu 求偏导:
∂
L
∂
b
u
=
−
2
∑
i
∈
non-zero entries
(
r
u
i
−
(
u
+
b
u
+
b
i
+
p
u
T
q
i
)
)
+
2
λ
b
u
\frac{\partial L}{\partial b_u} = -2 \sum_{i \in \text{non-zero entries}} (r_{ui} - (u + b_u + b_i + p_u^T q_i)) + 2 \lambda b_u
∂bu∂L=−2i∈non-zero entries∑(rui−(u+bu+bi+puTqi))+2λbu
对物品偏差
b
i
b_i
bi求偏导:
∂
L
∂
b
i
=
−
2
∑
u
∈
non-zero entries
(
r
u
i
−
(
u
+
b
u
+
b
i
+
p
u
T
q
i
)
)
+
2
λ
b
i
\frac{\partial L}{\partial b_i} = -2 \sum_{u \in \text{non-zero entries}} (r_{ui} - (u + b_u + b_i + p_u^T q_i)) + 2 \lambda b_i
∂bi∂L=−2u∈non-zero entries∑(rui−(u+bu+bi+puTqi))+2λbi
- 是用户
u
对物品i
的评分。 - μ μ μ 是全局偏差, b u b_u bu 是用户偏差, b i b_i bi 是物品偏差。
-
p
u
p_u
pu 和
q
i
q_i
qi 分别是用户
u
和物品i
的隐特征向量。 λ
是正则化系数,用于控制过拟合。
Code
import numpy as np
from tqdm import tqdm
def matrix_factorization_by_bias(matrix, k, learning_rate=0.01, reg_param=0.1, iterations=1000, seed=42):
"""
matrix: 评分矩阵
k: 隐特征维度
learning_rate: 学习率
reg_param: 正则化参数
iterations: 迭代次数
seed: 随机数种子
"""
np.random.seed(seed)
rows, cols = matrix.shape
# 初始化全局偏差
u = np.mean(matrix[matrix > 0]) # 所有评分的全局平均分
# 物品和用户的偏差初始化
b_i = np.mean(matrix, axis=0) # 物品偏差系数
b_u = np.mean(matrix, axis=1) # 用户偏差系数
# 随机初始化隐因子矩阵
user_matrix = np.random.rand(rows, k)
item_matrix = np.random.rand(k, cols)
# 迭代训练
pbar = tqdm(range(iterations), desc="Training", ncols=100, unit='iter')
for iter in pbar:
total_loss = 0
# 遍历每个评分
for i in range(rows):
for j in range(cols):
if matrix[i, j] > 0: # 只对非零评分进行优化
Prediction = u + b_i[j] + b_u[i] + np.dot(user_matrix[i, :], item_matrix[:, j])
error = matrix[i, j] - Prediction
# 计算梯度
user_grad = -2 * error * item_matrix[:, j] + 2 * reg_param * user_matrix[i, :]
item_grad = -2 * error * user_matrix[i, :] + 2 * reg_param * item_matrix[:, j]
# 更新隐因子矩阵
user_matrix[i, :] -= learning_rate * user_grad
item_matrix[:, j] -= learning_rate * item_grad
# 更新物品和用户的偏差
b_i[j] -= learning_rate * (-2 * error + 2 * reg_param * b_i[j])
b_u[i] -= learning_rate * (-2 * error + 2 * reg_param * b_u[i])
# 累加误差
total_loss += error ** 2
# 更新进度条
pbar.set_postfix({"Loss": total_loss})
return u, b_i, b_u, user_matrix, item_matrix
总结
矩阵分解通过将评分矩阵 R R R 分解为两个低秩矩阵 P P P 和 Q Q Q,利用隐因子表示用户和物品之间的潜在特征,从而帮助我们预测用户对未评分物品的兴趣。这一方法能够有效地填补评分矩阵中的缺失值,从而实现个性化推荐。
Reference
王喆 《深度学习推荐系统》