Published on

协同过滤推荐系统实践实验

Authors
  • avatar
    Name
    Allen Wang
    Twitter

欢迎来到这场关于协同过滤推荐系统的实践之旅!在本次实验中,您将实现协同过滤算法,并将其应用于电影评分数据集。通过学习,您将了解如何通过用户的评分数据生成个性化的电影推荐。

大纲

在本实验中,我们将使用以下 Python 包:

  • NumPy:用于高效的数值计算和数组操作。
  • TensorFlow:用于构建和训练机器学习模型。
  • recsys_utils.py:包含辅助函数,用于加载数据和处理。
Python
import numpy as np
import tensorflow as tf
from tensorflow import keras
from recsys_utils import *

1 - 符号说明

在本实验中,我们将使用以下符号来表示变量和参数:

通用符号描述Python(如果有)
r(i,j)r(i,j)标量;如果用户 jj 评分了电影 ii,则 = 1,否则 = 0
y(i,j)y(i,j)标量;用户 jj 对电影 ii 的评分(如果 r(i,j)=1r(i,j) = 1
w(j)\mathbf{w}^{(j)}向量;用户 jj 的参数
b(j)b^{(j)}标量;用户 jj 的偏置参数
x(i)\mathbf{x}^{(i)}向量;电影 ii 的特征向量
nun_u用户数num_users
nmn_m电影数num_movies
nn特征数num_features
X\mathbf{X}矩阵,由 x(i)\mathbf{x}^{(i)} 组成X
W\mathbf{W}矩阵,由 w(j)\mathbf{w}^{(j)} 组成W
b\mathbf{b}向量,由 b(j)b^{(j)} 组成b
R\mathbf{R}矩阵,由 r(i,j)r(i,j) 组成R

2 - 推荐系统

协同过滤是一种强大的推荐系统技术,它通过分析用户之间的相似性来预测用户对未评分项目的偏好。在本实验中,我们将实现协同过滤算法,并将其应用于电影评分数据集。

协同过滤的目标是为每个用户生成一个“参数向量”,反映其对电影的偏好,同时为每个电影生成一个“特征向量”。通过计算用户参数向量和电影特征向量的点积(加上偏置项),我们可以预测用户对特定电影的评分。

以下是协同过滤的学习过程:

  • 学习阶段:使用现有的用户评分数据(如矩阵 YY 和指示矩阵 RR)来训练模型,学习用户参数 W\mathbf{W}、电影特征 X\mathbf{X} 和偏置 b\mathbf{b}
  • 预测阶段:使用训练好的模型为用户推荐未评分的电影。

3 - 电影评分数据集

本实验使用的数据集来自 MovieLens "ml-latest-small" 数据集。该数据集包含了从 2000 年以来的一些电影的评分信息,共包括 nu=443n_u = 443 个用户和 nm=4778n_m = 4778 部电影。

  • 数据加载:我们将使用 load_ratings_small() 函数加载评分数据到矩阵 YYRR 中。
  • YY:一个 nm×nun_m \times n_u 的矩阵,存储用户对电影的评分(评分范围为 0.5 ~ 5,步长为 0.5;未评分的电影为 0)。
  • RR:一个二进制指示矩阵,R(i,j)=1R(i,j) = 1 如果用户 jj 对电影 ii 进行了评分,否则为 0。

此外,我们还将加载预计算的参数 X\mathbf{X}W\mathbf{W}b\mathbf{b},这些参数将在后续的训练中被优化。

Python
# 加载数据
X, W, b, num_movies, num_features, num_users = load_precalc_params_small()
Y, R = load_ratings_small()

print("Y", Y.shape, "R", R.shape)
print("X", X.shape)
print("W", W.shape)
print("b", b.shape)
print("num_features", num_features)
print("num_movies",   num_movies)
print("num_users",    num_users)

输出:

JavaScript
Y (4778, 443) R (4778, 443)
X (4778, 10)
W (443, 10)
b (1, 443)
num_features 10
num_movies 4778
num_users 443
  • 数据统计:例如,我们可以计算第一部电影的平均评分。
Python
tsmean = np.mean(Y[0, R[0, :].astype(bool)])
print(f"电影 1 的平均评分:{tsmean:0.3f} / 5")

输出:

JavaScript
电影 1 的平均评分:3.400 / 5

4 - 协同过滤学习算法

协同过滤的核心在于学习参数 X\mathbf{X}W\mathbf{W}b\mathbf{b},以最小化预测误差。具体来说,我们需要实现协同过滤的成本函数,并使用梯度下降法优化参数。

4.1 协同过滤成本函数

协同过滤的成本函数定义如下:

J(x(0),,x(nm1),w(0),b(0),,w(nu1),b(nu1))=[12(i,j):r(i,j)=1(w(j)x(i)+b(j)y(i,j))2]+[λ2j=0nu1k=0n1(wk(j))2+λ2i=0nm1k=0n1(xk(i))2]正则化项J(\mathbf{x}^{(0)}, \ldots, \mathbf{x}^{(n_m-1)}, \mathbf{w}^{(0)}, b^{(0)}, \ldots, \mathbf{w}^{(n_u-1)}, b^{(n_u-1)}) = \left[ \frac{1}{2} \sum_{(i,j):r(i,j)=1} (\mathbf{w}^{(j)} \cdot \mathbf{x}^{(i)} + b^{(j)} - y^{(i,j)})^2 \right] + \underbrace{\left[ \frac{\lambda}{2} \sum_{j=0}^{n_u-1} \sum_{k=0}^{n-1} (\mathbf{w}_k^{(j)})^2 + \frac{\lambda}{2} \sum_{i=0}^{n_m-1} \sum_{k=0}^{n-1} (\mathbf{x}_k^{(i)})^2 \right]}_{\text{正则化项}}
  • 第一部分:衡量模型预测与实际评分之间的误差。
  • 第二部分:正则化项,用于防止过拟合。

练习 1:实现协同过滤成本函数

以下是需要实现的成本函数 cofi_cost_func

Python
def cofi_cost_func(X, W, b, Y, R, lambda_):
    """
    返回协同过滤的成本。

    参数:
        X (ndarray (num_movies, num_features)): 电影特征矩阵
        W (ndarray (num_users, num_features)): 用户参数矩阵
        b (ndarray (1, num_users)): 用户偏置向量
        Y (ndarray (num_movies, num_users)): 用户对电影的评分矩阵
        R (ndarray (num_movies, num_users)): 指示矩阵,R(i,j) = 1 如果用户 j 评分了电影 i
        lambda_ (float): 正则化参数

    返回:
        J (float): 成本值
    """
    nm, nu = Y.shape
    J = 0

    ### START CODE HERE ###
    for j in range(nu):
        w = W[j, :]
        b_j = b[0, j]
        for i in range(nm):
            x = X[i, :]
            y = Y[i, j]
            r = R[i, j]
            J += np.square(r * (np.dot(w, x) + b_j - y))
    J = J / 2
    J += (lambda_ / 2) * (np.sum(np.square(W)) + np.sum(np.square(X)))
    ### END CODE HERE ###

    return J

5 - 学习电影推荐

现在,我们将使用梯度下降法优化参数 X\mathbf{X}W\mathbf{W}b\mathbf{b}。以下是训练过程:

Python
# 初始化参数
tf.random.set_seed(1234)
W = tf.Variable(tf.random.normal((num_users, num_features), dtype=tf.float64), name='W')
X = tf.Variable(tf.random.normal((num_movies, num_features), dtype=tf.float64), name='X')
b = tf.Variable(tf.random.normal((1, num_users), dtype=tf.float64), name='b')

# 优化器
optimizer = keras.optimizers.Adam(learning_rate=1e-1)

# 训练循环
iterations = 200
lambda_ = 1
for iter in range(iterations):
    with tf.GradientTape() as tape:
        cost_value = cofi_cost_func_v(X, W, b, Ynorm, R, lambda_)
    grads = tape.gradient(cost_value, [X, W, b])
    optimizer.apply_gradients(zip(grads, [X, W, b]))
    if iter % 20 == 0:
        print(f"Training loss at iteration {iter}: {cost_value:0.1f}")

6 - 推荐

使用训练好的模型,我们可以为用户生成电影推荐。例如:

Python
# 预测所有电影的评分
p = np.matmul(X.numpy(), np.transpose(W.numpy())) + b.numpy()
my_predictions = p[:, 0]

# 排序并显示推荐
ix = tf.argsort(my_predictions, direction='DESCENDING')
for i in range(10):
    j = ix[i]
    print(f'Predicting rating {my_predictions[j]:0.2f} for movie {movieList[j]}')

6.1 为什么协同过滤有时推荐得很“像”

如果你看到推荐列表风格过于集中(比如都是同类电影),通常不是程序出错,而是模型偏向“最稳妥的偏好方向”。

常见原因:

  • 用户历史评分覆盖面窄,向量空间信息不足
  • 热门电影样本更多,梯度更新更稳定
  • 正则化偏大,导致模型偏保守

可以尝试:

  1. 轻微降低 lambda_,观察个性化变化
  2. 在重排阶段增加多样性约束(同题材去重)
  3. 对冷门高分电影增加探索权重

6.2 协同过滤调试检查清单

  • YR 的 shape 是否一致
  • 评分是否先做了均值归一化
  • 训练损失是否稳定下降
  • 是否出现 nan / inf
  • 推荐结果是否被“热门偏差”主导

6.3 交互式可视化

7 - 总结 通过本次实验,你成功掌握了协同过滤推荐系统的核心实现方法。以下是本次学习的关键收获: -

算法实践:亲手实现了协同过滤的成本函数与梯度下降优化,深入理解了用户参数与电影特征之间的交互原理。 - 模型训练:运用TensorFlow搭建推荐模型,通过200次迭代优化,显著降低了预测评分与实际评分的误差。 - 实战应用:基于MovieLens数据集,构建了能输出个性化电影推荐的系统,并为用户生成TOP10推荐列表。 - 正则化技巧:在成本函数中引入L2正则化项,有效避免了过拟合问题,提升了模型的泛化能力。 下一步,你可以尝试调整学习率(如0.05或0.2)、正则化参数(如0.5或2),观察模型效果变化,或在完整版MovieLens数据集(包含27000部电影)上挑战更大规模的推荐任务。

完整代码已开源在GitHub仓库

本篇文章的部分内容和思想参考了 吴恩达 (Andrew Ng)Coursera 机器学习课程 中的讲解,感谢他对机器学习领域的卓越贡献。