简介:基于itemCF与SVD的电影推荐算法本科毕业论文设计资源包,面向机器学习、人工智能方向的本科生及推荐系统研究者。该设计围绕协同过滤与矩阵分解两类主流技术,系统探讨item-based collaborative filtering与奇异值分解在电影推荐场景中的建模过程、优势互补及效果评估,适合作为毕业论文撰写、算法复现或课程设计的参考范例。资源包共5个文件,压缩后6.59MB。其中包含2个Python脚本,分别实现itemCF和SVD算法;1个DOCX论文正文和1个DOC文献综述;另有1个DAT评分数据集,可用于实际训练。内容覆盖代码、数据与文档三部分,便于对照学习。目前已有280人学习下载。通过该资源可掌握推荐系统核心算法的Python实现,理解矩阵分解与物品相似度计算的细节,同时学习论文中的实验设计、性能评估指标以及冷启动问题处理思路,对完成相关毕业设计或科研入门有实际帮助。
1. 从 itemCF 到 SVD:为什么电影推荐需要两种算法互补
做电影推荐相关研究,第一次跑通 itemCF 时结果通常不错,但把评分矩阵放大到真实规模后,基于物品的协同过滤会暴露出相似度噪声大、未评分位置等于默认值的问题。SVD 奇异值分解则擅长把用户与电影映射到低维隐因子空间,用全局信息补全局部缺失。把 itemCF 与 SVD 放在一起做混合推荐算法,既能保留物品间可解释的共现关系,又能获得矩阵分解的泛化能力。
下面围绕本科毕设任务“基于itemCF与SVD的电影推荐算法研究”展开,讲清楚两套方法的选型边界和实现路径,并给出可以直接复现的 Python 代码。适合正在写推荐系统论文、或者想将协同过滤与矩阵分解落实到生产环境的工程师。阅读时可以重点抓住三件事:相似度如何计算、SVD 如何训练、两者如何融合。
2. 基于 itemCF 与 SVD 的核心原理与选型边界
2.1 itemCF 的计算逻辑与相似度公式
itemCF 全称是 item-based Collaborative Filtering,即基于物品的协同过滤。它的假设很直观:很多用户给电影 A 打了高分,同时也给电影 B 打了高分,那么在行为空间里 A 和 B 就是相似的。用户看过 A 之后,系统就把与 A 最像的 B 推给用户。注意这里说的“相似”不是类型、导演、演员这些内容特征,而是评分共现关系。
最基本的相似度是余弦相似度。下面代码输入用户-物品评分矩阵,输出物品之间的相似度矩阵:
import numpy as np def cosine_similarity(ratings_matrix): # ratings_matrix: (users, items),0 表示未评分 item_matrix = ratings_matrix.T # 转成 (items, users) norm = np.sqrt((item_matrix ** 2).sum(axis=1)).reshape(-1, 1) norm[norm == 0] = 1e-6 # 防止全 0 列除零 sim = (item_matrix @ item_matrix.T) / (norm * norm.T) np.fill_diagonal(sim, 0) # 物品自身相似度置零 return simnorm * norm.T是列向量与行向量的外积,得到任意两个物品的范数之积;分子是共同评分向量的内积。这段代码没有对评分做中心化,如果用户 A 普遍打 3 分、用户 B 普遍打 5 分,余弦相似度会被用户打分尺度带偏。所以实际实现里一般先减去每个用户的平均分,用残差计算相关,等价于皮尔逊相关系数。
另一个容易被忽略的细节是相似度矩阵的行归一化。itemCF 在做加权平均时,如果某个物品与很多物品相似,它的相似度分数天然会高,导致推荐结果偏向“高连接度”物品。因此很多实现会在得到 sim 后按行减去均值或除以 max,让相似度分布更均衡。在电影场景中,这个操作也会让续集、同系列电影更容易排在前面。
电影推荐里还有一个常见问题是热门电影跟所有电影都有共现,相似度矩阵会堆向少数爆款,冷门电影即使相关也很难进候选。常用的修正方式是给相似度乘一个热门惩罚系数,例如用sim * np.power(1 - item_popularity_ratio, 0.5)削弱高频物品的相似度。
2.2 SVD 的矩阵分解思路与目标函数
SVD 奇异值分解把用户-电影矩阵 R 分解为 R ≈ U Σ V^T。U 的每一行可以理解成用户隐因子,V 的每一行可以理解成电影隐因子,Σ 是奇异值对角线矩阵。在推荐场景中,评分矩阵缺失项占绝大多数,如果直接把缺失位置填 0 再调用np.linalg.svd,会得到一个有偏的重建矩阵。比如下面这个 3×3 的示例:
R = np.array([[5, 4, 0], [0, 3, 4], [2, 0, 1]], dtype=float) U, S, Vt = np.linalg.svd(R, full_matrices=False) k = 2 R_approx = U[:, :k] @ np.diag(S[:k]) @ Vt[:k, :] print(R_approx[:, 2]) # 原来缺失的列会被非零值污染这个输出说明,直接 SVD 会用全局低秩近似去“猜测”缺失值,但训练目标不是真实评分,而是带了大量零值项。因此推荐系统里常说的 SVD 实际是 FunkSVD:只针对已经观测到的评分建目标函数,用随机梯度下降求解,目标是最小化:
min Σ_ui (r_ui - p_u · q_i^T)^2 + λ (||p_u||^2 + ||q_i||^2)p_u 是用户 u 的 k 维隐向量,q_i 是电影 i 的 k 维隐向量。FunkSVD 不再要求中间矩阵是严格对角阵,而是直接学两个低秩因子矩阵,这也是第 4 章实现的基础。FunkSVD 的梯度更新有一点和传统 SVD 不同:真实评分 r_ui 不是分解后的某个奇异值,而是 user-item 点积的目标值,因此训练误差能直接对应 RMSE。实战中往往加上早停,每轮在验证集上计算一次 RMSE,连续 5 轮不下降就停止,避免隐因子过拟合。
2.3 两种算法的选型边界与组合动机
| 对比维度 | itemCF | SVD 奇异值分解 |
|---|---|---|
| 输入 | 用户-物品评分矩阵 | 用户-物品评分矩阵 |
| 建模对象 | 物品间共同评分向量 | 用户与物品的低维隐因子 |
| 稀疏性处理 | 差,共同评分为 0 时相似度失效 | 较好,利用全局评分泛化 |
| 新物品冷启动 | 无评分行为时没法计算相似度 | 没有隐因子向量,也无法推荐 |
| 可解释性 | 高,能解释“喜欢 A 的也喜欢 B” | 低,隐因子没有明确语义 |
| 主要开销 | 物品数平方 | k 维度 × 迭代轮数 |
把两者组合看中的是互补性:itemCF 对冷门物品更敏感,可解释性也强,但在用户-电影矩阵稀疏时,相似度矩阵大量行是空行,召回不足。SVD 通过隐因子把稀疏矩阵压缩成 k 维向量,能对未评分项给出相对平滑的预估,但隐因子很难解释。两个模型的误差来源不同,混合后通常能带来可观的离线指标提升。
3. 构造评分矩阵并用 itemCF 计算电影相似度
3.1 从原始评分日志构造稀疏评分矩阵
正式实现第一步是把评分日志整理成三元组:user_id、movie_id、rating。下面代码以 MovieLens 风格数据为例,先过滤掉打分次数过少的用户和评分次数过少的电影。这个过滤是必要的,否则相似度矩阵会被只评过一两次的用户噪声主导。
import pandas as pd from scipy.sparse import coo_matrix df = pd.read_csv('ratings.csv') # 用户最少评分 20 次,过滤冷启动用户 user_count = df['userId'].value_counts() df = df[df['userId'].isin(user_count[user_count >= 20].index)] # 电影最少被评 5 次,过滤无人问津的长尾 item_count = df['movieId'].value_counts() df = df[df['movieId'].isin(item_count[item_count >= 5].index)] users = sorted(df['userId'].unique()) items = sorted(df['movieId'].unique()) user2id = {u: i for i, u in enumerate(users)} item2id = {m: i for i, m in enumerate(items)} id2item = {i: m for m, i in item2id.items()} rows = df['userId'].map(user2id).values cols = df['movieId'].map(item2id).values values = df['rating'].values.astype(np.float32) R = coo_matrix((values, (rows, cols)), shape=(len(users), len(items))).toarray()user2id和item2id是离散 id 到连续下标的映射,id2item用于最后输出电影编号。min_user_count=20、min_item_count=5 是常见起步值,数据量大时可以提高;数据特别稀疏时也可以适当降低,但不要低于 3,否则相似度不可信。
这里没有做评分归一化,因为 itemCF 和 SVD 对原始评分尺度各有自己的建模方式。如果一开始就做 z-score 归一化,反而会丢失用户评分的真实语义。实验时可以把原始评分和归一化后的评分各跑一遍,再决定用哪种。
3.2 itemCF 相似度计算的完整实现与参数说明
第 2 章的余弦相似度没有做中心化,实际代码里我会先计算每个用户的平均分,再用残差代替原始评分。这样可以消除打分尺度差异,让“给谁都打 5 分”的用户不会主导相似度。
def build_item_sim(R): # R: (users, items),0 表示缺失 user_mean = R.sum(axis=1, keepdims=True) / (R > 0).sum(axis=1, keepdims=True) residual = np.where(R > 0, R - user_mean, 0) norm = np.sqrt((residual ** 2).sum(axis=0)) norm[norm == 0] = 1e-6 sim = (residual.T @ residual) / np.outer(norm, norm) # 共同评分人数太少时相似度不可信,直接置 0 co_count = (R > 0).T.astype(np.float32) @ (R > 0).astype(np.float32) sim[co_count < 5] = 0 np.fill_diagonal(sim, 0) return simco_count统计两件商品被多少用户共同评过分,少于 5 对样本算出的相关系数不稳定,直接置 0 是降低噪声的常用操作。norm外积做归一化后,sim矩阵每行都能看成电影 i 与所有电影的相似度分布,可以直接缓存,线上推荐时只查表。
3.3 基于 itemCF 生成 top-N 推荐候选
有了相似度矩阵,对用户 u 预测电影 i 的分数采用加权平均公式,权重是相似度,分数是用户已评电影的评分。代码实现为:
def itemcf_predict(u, scored, R, sim, top_n=20): pred = np.zeros(R.shape[1]) for i in range(R.shape[1]): if scored[i]: pred[i] = -np.inf continue sim_u = sim[:, i][scored] if sim_u.size == 0 or np.abs(sim_u).sum() < 1e-12: continue pred[i] = (sim_u * R[u, scored]).sum() / np.abs(sim_u).sum() return np.argsort(pred)[::-1][:top_n]循环版本方便阅读,但生产环境建议改成矩阵运算:sim[scored][:, ~scored]一次得到所有候选电影对。还要注意,如果用户已评分电影太少,加权平均会被单个相似度放大,因此第 3.1 节的过滤阈值同样约束了在线推荐质量。
参数推荐如下:
| 参数 | 含义 | 起步值 | 调整方向 |
|---|---|---|---|
| min_user_count | 用户最少评分次数 | 20 | 稀疏数据下调到 10 |
| min_item_count | 电影最少被评次数 | 5 | 长尾数据下调到 3 |
| co_count_threshold | 最小共同评分人数 | 5 | 噪声多时提高到 10 |
| 热门惩罚 alpha | 相似度降权强度 | 0.5 | 0.3~0.8 网格搜索 |
4. 用 SVD 完成隐因子分解与混合推荐打分
4.1 FunkSVD 的训练过程与正则化
SVD 系列算法在推荐里的核心问题是评分矩阵存在大量缺失,不能直接用np.linalg.svd。常见做法是只优化有评分的位置,用随机梯度下降同时更新两个隐因子矩阵。下面是一个可以直接跑的实验版 FunkSVD:
def train_funksvd(R, k=20, lr=0.01, reg=0.1, epochs=50): m, n = R.shape P = np.random.normal(0, 0.1, size=(m, k)) Q = np.random.normal(0, 0.1, size=(n, k)) entries = [(u, i) for u in range(m) for i in range(n) if R[u, i] > 0] for epoch in range(epochs): np.random.shuffle(entries) for u, i in entries: err = R[u, i] - P[u] @ Q[i] # 负梯度更新,带 L2 正则 P[u] -= lr * (-err * Q[i] + reg * P[u]) Q[i] -= lr * (-err * P[u] + reg * Q[i]) return P, Qk是隐因子维度,lr 是学习率,reg 是正则强度,epochs 是训练轮数。这段代码没有加用户和物品偏置,实际使用中可以把评分拆成global_mean + b_u + b_i + P[u]@Q[i],让偏置项先吸收用户习惯和物品热度,隐因子专心建模剩余部分。初始化用均值为 0、标准差 0.1 的随机正态分布,比全 0 初始化更快打破对称。
训练收敛的判断不要只看训练集 RMSE,否则 k 增大到一定程度后会出现过拟合。更实用的做法是每 10 轮在验证集上计算一次 RMSE,连续 3 次不降则停止。由于 FunkSVD 的随机性,每次初始化和遍历顺序不同,最终模型会略有波动,论文里固定 random_state 更利于复现。
4.2 混合推荐的融合方式
itemCF 和 SVD 的预测分数虽然都在 1-5 分区间,但分布不一致。直接加权会被量纲带偏,所以先对两个分数做 min-max 归一化,再线性融合:
def normalize_score(scores): mn, mx = scores.min(), scores.max() return (scores - mn) / ((mx - mn) + 1e-8) itemcf_scores = normalize_score(itemcf_scores) svd_scores = normalize_score(svd_scores) alpha = 0.6 final = alpha * itemcf_scores + (1 - alpha) * svd_scores top_idx = np.argsort(final)[::-1][:20]alpha 就是混合权重,表示更信任 itemCF 还是 SVD。调参顺序建议先固定 SVD 和 itemCF 的参数,在验证集上从 0.2 到 0.8 按步长 0.1 扫 alpha。不要同时调三个模型的参数,否则无法定位是哪一侧出了问题。如果两个模型分数相关性很高,混合收益会变小,此时可以检查是不是数据划分泄漏了。
混合分数归一化时要注意极小值。当 itemCF 某个候选没有相似邻居时,分数会是 0,min-max 后变成负值,这与 SVD 的平滑预测冲突。我一般在归一化前先把未产生分数的候选置为一个很小的负值,例如 -1,保证它们排在最后但不会破坏排序区间。
4.3 训练集/测试集划分:避免数据泄漏
离线实验中数据划分方式直接影响指标的置信度。最简单的随机划分会带来泄漏:同一用户的评分可能一半在训练集、一半在测试集,itemCF 在测试时通过相似度矩阵间接“看到”了测试评分。推荐做法是按用户分层或按时间切分。
from sklearn.model_selection import train_test_split # 以用户为粒度切分,训练和测试用户不重叠 train_users, test_users = train_test_split( df['userId'].unique(), test_size=0.2, random_state=42 ) train_df = df[df['userId'].isin(train_users)] test_df = df[df['userId'].isin(test_users)]如果目标是模拟时间线,可以给每个用户取最后 10% 的评分作为测试集。划分方式对最终指标影响很大,论文里一定要写清楚。下表总结了四种常见划分:
| 划分方式 | 训练/测试关系 | 适合评估 |
|---|---|---|
| 随机行划分 | 同一用户可分到两侧 | 快速基线 |
| 用户分层划分 | 训练测试用户不重叠 | 新用户冷启动 |
| LeaveOneOut | 每个用户只留一个测试项 | top-N 排序 |
| 时间序划分 | 按时间先后切分 | 近线上场景 |
5. 评估指标、参数调优与 itemCF-SVD 混合的落地技巧
5.1 评估指标怎么选
评分预测和列表推荐要分开看。RMSE、MAE 评估预测分和真实分的差距,Precision@N、Recall@N 评估 top-N 列表的命中效果。建议至少报 RMSE 和 Precision@10 两个指标,一个说明回归误差,一个说明排序质量。覆盖率统计被推荐到的电影占全集的比例,用于观察是否只推爆款。
5.2 参数调优的优先级
调参顺序固定为:先调 SVD 的 k、lr、reg,再调 itemCF 的最小共同评分人数和热门惩罚,最后调融合权重 alpha。SVD 的 k 一般从 20 开始,k 太小欠拟合、太大过拟合;lr 从 0.005 起步,reg 从 0.1 起步。itemCF 的 co_count_threshold 不要超过 20,否则大部分电影都会因为没有足够共同评分而失去候选资格。
5.3 混合推荐的落地技巧
把 itemCF 相似度矩阵和 SVD 的 P、Q 存成 npz 文件,线上加载后只需要做矩阵乘法和查表,不需要重训。保存代码很简单:
np.savez('model_cache.npz', itemcf_sim=itemcf_sim, P=P, Q=Q) cache = np.load('model_cache.npz', allow_pickle=True)最后在验证集上跑完一轮后,将 alpha 从 0 调到 1,画出最终指标曲线。最佳权重往往会偏向某一侧,这比单独看一组 RMSE 更能说明 itemCF 和 SVD 各自的贡献边界。
本文还有配套的精品资源,点击获取