简介:这份资源是面向计算机相关专业学生与项目实战学习者的Python电影推荐系统完整源码包,基于协同过滤推荐算法实现,可直接用于毕业设计、课程设计或期末大作业。压缩包共1197个文件,约76.75MB,其中22个py文件承载推荐算法与业务逻辑,14个html、8个css与12个js构成前端交互界面,4个csv与1个sqlite3、1个sql提供用户评分与电影数据支撑,另有1095张jpg图片用于电影海报与页面素材,整体结构完整、层次清晰。项目已通过严格调试,下载后即可运行,省去环境搭建与数据准备的繁琐环节。目前已有147人学习关注,适合希望快速理解协同过滤原理、掌握推荐系统从数据到界面完整链路的同学,也可作为二次开发与功能扩展的起点。
1. 电影推荐系统:从协同过滤到可交付源码,毕业设计到底该怎么做
很多计算机专业的同学拿到「基于协同过滤的电影推荐系统」这个题目时,第一反应是去 GitHub 上找一个现成仓库改改交差。但真正动手才发现,推荐算法部分能跑通,数据预处理、冷启动、评估指标、前后端联调这些环节全是坑。更麻烦的是答辩时老师一问「你的相似度矩阵怎么算的」「为什么用 ItemCF 不用 UserCF」,如果只抄了代码没理解逻辑,当场就翻车。
这个方向本质上解决的是一个经典问题:给定用户对电影的评分历史,预测用户对未看电影的评分,并按预测分排序生成推荐列表。协同过滤是推荐系统里最成熟、最容易复现的入门方案,不需要 GPU,一台普通笔记本就能跑完全流程。它适合作为毕业设计的原因也很直接——算法原理清晰、数据集公开(MovieLens 系列)、评估指标标准化(RMSE、Precision@K、Recall@K),而且从数据处理到 Web 展示可以串起完整的工程链路。
接下来我会按「数据准备 → 算法实现 → 系统搭建 → 评估调参 → 避坑」的顺序,把每个环节的具体做法、参数设置和常见问题讲清楚。你跟着走一遍,至少能拿到一个自己能讲明白、答辩时不怕追问的系统。
2. 数据准备与协同过滤的两种路线:UserCF 和 ItemCF 到底选哪个
2.1 MovieLens 数据集的加载与清洗
MovieLens 是最常用的公开电影评分数据集,常见的有 ml-latest-small(约 10 万条评分、600 个用户、9000 部电影)和 ml-1m(约 100 万条评分)。毕业设计用 small 版本足够,跑得快,结果也好看。
数据文件通常是 ratings.csv、movies.csv、tags.csv 三个。ratings.csv 的核心字段是 userId、movieId、rating、timestamp。加载和初步清洗的代码如下:
import pandas as pd import numpy as np # 加载评分数据 ratings = pd.read_csv('ml-latest-small/ratings.csv') movies = pd.read_csv('ml-latest-small/movies.csv') # 查看基本信息 print(f"评分总数: {len(ratings)}") print(f"用户数: {ratings['userId'].nunique()}") print(f"电影数: {ratings['movieId'].nunique()}") print(f"评分范围: {ratings['rating'].min()} - {ratings['rating'].max()}") # 过滤掉评分次数少于 5 次的用户(冷启动用户对协同过滤没有贡献) user_counts = ratings['userId'].value_counts() active_users = user_counts[user_counts >= 5].index ratings_clean = ratings[ratings['userId'].isin(active_users)] # 过滤掉被评分少于 5 次的电影 movie_counts = ratings_clean['movieId'].value_counts() popular_movies = movie_counts[movie_counts >= 5].index ratings_clean = ratings_clean[ratings_clean['movieId'].isin(popular_movies)] print(f"清洗后评分总数: {len(ratings_clean)}") print(f"清洗后用户数: {ratings_clean['userId'].nunique()}") print(f"清洗后电影数: {ratings_clean['movieId'].nunique()}")这段代码做了三件事:加载原始数据、统计基本信息、过滤长尾数据。过滤阈值设为 5 是一个经验值——低于 5 条评分的用户或电影,在协同过滤中几乎无法计算有意义的相似度,保留它们只会增加噪声和计算量。清洗后数据量通常会减少 10% 到 20%,这是正常的。
参数方面,阈值不是固定的。如果数据集本身很稀疏(比如 ml-100k 只有 10 万条评分),阈值可以降到 3;如果数据量大,可以提高到 10。判断标准是:过滤后用户-物品矩阵的稀疏度不要超过 99.9%,否则相似度计算会大面积失效。
2.2 UserCF 与 ItemCF 的选型逻辑
协同过滤分两条路线:基于用户的协同过滤(UserCF)和基于物品的协同过滤(ItemCF)。很多同学直接抄了 ItemCF 的代码但说不清为什么,答辩时容易被问住。
UserCF 的核心逻辑是:找到和目标用户兴趣相似的一群用户,把他们喜欢但目标用户没看过的电影推荐过来。适合用户数量远小于物品数量的场景,比如新闻推荐——新闻每天更新,物品数巨大,但用户群体相对稳定。
ItemCF 的核心逻辑是:找到和目标用户历史喜欢的电影相似的电影,推荐给该用户。适合物品数量相对稳定、用户数量大的场景。电影推荐恰好属于这一类——电影库相对固定,用户可能很多。
对于毕业设计,我一般推荐用 ItemCF,原因有三个:第一,电影相似度矩阵可以离线预计算,线上响应快;第二,可解释性强,答辩时可以说「因为你看过 A,A 和 B 相似度高,所以推荐 B」;第三,MovieLens 数据集中电影数量通常少于用户数量,ItemCF 的相似度矩阵规模更小。
选型不是绝对的。如果指导老师明确要求 UserCF,或者你的数据集用户数远小于电影数,那就用 UserCF。关键是你能说清楚选择理由,而不是「网上都这么写」。
2.3 用户-物品评分矩阵的构建
无论选哪种路线,第一步都是构建用户-物品评分矩阵。代码如下:
# 构建用户-物品评分矩阵 # 行是用户,列是电影,值是评分,缺失值填 0 user_item_matrix = ratings_clean.pivot_table( index='userId', columns='movieId', values='rating', fill_value=0 ) print(f"矩阵形状: {user_item_matrix.shape}") print(f"矩阵稀疏度: {(user_item_matrix == 0).sum().sum() / user_item_matrix.size:.4f}") # 转为 numpy 数组,方便后续计算 matrix = user_item_matrix.values user_ids = user_item_matrix.index.tolist() movie_ids = user_item_matrix.columns.tolist()pivot_table 是 pandas 里构建透视表的标准方法,fill_value=0 表示缺失评分填 0。注意这里的 0 不是「评分为 0 分」,而是「没有评分」,后续计算相似度时要处理这个区别。
稀疏度通常在 95% 到 99.9% 之间。如果超过 99.9%,说明数据太稀疏,协同过滤效果会很差,需要考虑降维(如 SVD)或者换数据集。这个指标答辩时经常被问到,建议提前算好记住。
3. 相似度计算与推荐生成:从余弦相似度到 Top-N 推荐
3.1 余弦相似度与皮尔逊相关系数的实现差异
相似度计算是协同过滤的核心。常用的有余弦相似度和皮尔逊相关系数两种。
余弦相似度衡量两个向量在方向上的接近程度,不考虑评分尺度差异。皮尔逊相关系数先减去均值再算余弦,能消除用户评分偏好的影响——有人习惯打高分,有人习惯打低分,皮尔逊能修正这个偏差。
from sklearn.metrics.pairwise import cosine_similarity import numpy as np # 基于物品的余弦相似度 # 转置矩阵,使行变成电影,列变成用户 item_user_matrix = matrix.T item_similarity = cosine_similarity(item_user_matrix) # 将对角线置零(自己和自己相似度为 1,但推荐时不应推荐自己) np.fill_diagonal(item_similarity, 0) print(f"物品相似度矩阵形状: {item_similarity.shape}") print(f"相似度范围: {item_similarity.min():.4f} - {item_similarity.max():.4f}")cosine_similarity 返回的是对称矩阵,item_similarity[i][j] 表示电影 i 和电影 j 的相似度。np.fill_diagonal 把对角线置零,防止推荐系统把用户已经看过的电影又推一遍。
如果要用皮尔逊相关系数,可以用 numpy 的 corrcoef,但注意它按行计算相关系数,需要转置。实际项目中余弦相似度更常用,因为计算快、结果稳定,而且对于评分数据,余弦相似度已经能捕捉大部分信息。
参数方面,相似度矩阵的维度是电影数 × 电影数。9000 部电影就是 9000×9000 的矩阵,约 8100 万个浮点数,内存占用约 648MB(float64)。如果内存吃紧,可以转成 float32,或者只保留 Top-K 相似邻居。
3.2 基于 ItemCF 的评分预测与 Top-N 推荐
有了相似度矩阵,就可以预测用户对未看电影的评分了。公式是:预测评分 = 用户已评分电影的评分加权和,权重是电影间的相似度。
def predict_rating(user_idx, movie_idx, matrix, item_similarity, top_k=20): """ 预测用户对某部电影的评分 user_idx: 用户在矩阵中的行索引 movie_idx: 电影在矩阵中的列索引 top_k: 只取最相似的 K 部电影参与计算 """ # 获取用户已评分的电影索引 rated_items = np.where(matrix[user_idx] > 0)[0] if len(rated_items) == 0: return 0 # 冷启动用户,无法预测 # 获取这些电影与目标电影的相似度 similarities = item_similarity[movie_idx, rated_items] # 取 Top-K 相似电影 if len(similarities) > top_k: top_indices = np.argsort(similarities)[-top_k:] else: top_indices = np.arange(len(similarities)) # 加权平均预测评分 sim_values = similarities[top_indices] ratings_values = matrix[user_idx, rated_items[top_indices]] if sim_values.sum() == 0: return 0 predicted = np.dot(sim_values, ratings_values) / sim_values.sum() return predicted def recommend_movies(user_idx, matrix, item_similarity, movie_ids, top_n=10, top_k=20): """ 为用户生成 Top-N 推荐 """ # 用户已评分的电影 rated_items = set(np.where(matrix[user_idx] > 0)[0]) # 对所有未评分的电影预测评分 predictions = [] for movie_idx in range(matrix.shape[1]): if movie_idx not in rated_items: pred = predict_rating(user_idx, movie_idx, matrix, item_similarity, top_k) if pred > 0: predictions.append((movie_idx, pred)) # 按预测评分降序排列,取 Top-N predictions.sort(key=lambda x: x[1], reverse=True) top_n_movies = predictions[:top_n] # 返回电影 ID 和预测评分 return [(movie_ids[idx], score) for idx, score in top_n_movies]predict_rating 函数的核心是加权平均:相似度越高的电影,其评分对预测结果影响越大。top_k 参数控制参与计算的邻居数量,太小会欠拟合,太大会引入噪声。经验值在 20 到 50 之间,数据量大时可以适当增加。
recommend_movies 遍历所有未评分电影,逐个预测评分后排序。这个做法在电影数量少时可行,但如果电影上万部,逐个预测会很慢。优化方法是用矩阵运算批量计算,或者只对候选集(比如相似度 Top-100 的电影)做预测。
3.3 用 Surprise 库快速验证算法效果
如果不想从零实现,可以用 Surprise 库快速验证。它封装了常见的协同过滤算法,几行代码就能跑出 RMSE。
from surprise import Dataset, Reader, KNNBasic from surprise.model_selection import cross_validate # 加载数据 reader = Reader(rating_scale=(0.5, 5.0)) data = Dataset.load_from_df(ratings_clean[['userId', 'movieId', 'rating']], reader) # 使用 ItemCF(基于物品的 KNN) sim_options = { 'name': 'cosine', # 相似度度量 'user_based': False, # False 表示 ItemCF 'min_support': 3 # 至少 3 个共同评分才计算相似度 } algo = KNNBasic(sim_options=sim_options) # 5 折交叉验证 results = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=5, verbose=True) print(f"平均 RMSE: {results['test_rmse'].mean():.4f}") print(f"平均 MAE: {results['test_mae'].mean():.4f}")Surprise 的 KNNBasic 默认用余弦相似度,user_based=False 切换到 ItemCF。min_support 参数控制共同评分的最小数量,低于这个值不计算相似度,能有效减少噪声。
RMSE 在 0.85 到 0.95 之间算正常,低于 0.85 说明模型拟合很好,高于 1.0 需要检查数据或参数。这个结果可以直接写进毕业论文的实验章节。
4. 系统搭建:从离线算法到可交互的 Web 演示
4.1 Flask 后端接口设计
毕业设计通常需要展示界面,Flask 是最轻量的选择。核心接口有三个:获取电影列表、获取推荐结果、提交新评分。
from flask import Flask, request, jsonify import pandas as pd import numpy as np from sklearn.metrics.pairwise import cosine_similarity app = Flask(__name__) # 全局变量,启动时加载 ratings = pd.read_csv('ml-latest-small/ratings.csv') movies = pd.read_csv('ml-latest-small/movies.csv') user_item_matrix = ratings.pivot_table( index='userId', columns='movieId', values='rating', fill_value=0 ) matrix = user_item_matrix.values user_ids = user_item_matrix.index.tolist() movie_ids = user_item_matrix.columns.tolist() # 预计算物品相似度 item_similarity = cosine_similarity(matrix.T) np.fill_diagonal(item_similarity, 0) @app.route('/api/movies', methods=['GET']) def get_movies(): """返回电影列表,支持分页""" page = int(request.args.get('page', 1)) size = int(request.args.get('size', 20)) start = (page - 1) * size end = start + size movie_list = movies.iloc[start:end][['movieId', 'title', 'genres']].to_dict('records') return jsonify({'movies': movie_list, 'total': len(movies)}) @app.route('/api/recommend/<int:user_id>', methods=['GET']) def get_recommendations(user_id): """为指定用户生成推荐""" if user_id not in user_ids: return jsonify({'error': '用户不存在'}), 404 user_idx = user_ids.index(user_id) top_n = int(request.args.get('top_n', 10)) # 调用推荐函数(省略具体实现,见上一章) recommendations = recommend_movies(user_idx, matrix, item_similarity, movie_ids, top_n) # 补充电影标题 result = [] for movie_id, score in recommendations: title = movies[movies['movieId'] == movie_id]['title'].values result.append({ 'movieId': int(movie_id), 'title': title[0] if len(title) > 0 else '未知', 'score': round(float(score), 2) }) return jsonify({'userId': user_id, 'recommendations': result}) if __name__ == '__main__': app.run(debug=True, port=5000)接口设计遵循 RESTful 风格,/api/movies 返回分页电影列表,/api/recommend/<user_id> 返回指定用户的推荐结果。相似度矩阵在应用启动时预计算,避免每次请求都重新算。
参数方面,top_n 默认 10,可以通过查询参数调整。分页的 size 默认 20,防止一次返回太多数据导致前端卡顿。生产环境应该把 debug 设为 False,并用 gunicorn 等 WSGI 服务器部署。
4.2 前端页面与推荐结果展示
前端可以用最简单的 HTML + JavaScript,不需要上 Vue 或 React。核心是一个用户选择框和一个推荐结果列表。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>电影推荐系统</title> <style> body { font-family: Arial, sans-serif; max-width: 800px; margin: 40px auto; } .movie-item { padding: 10px; border-bottom: 1px solid #eee; } .score { color: #e74c3c; font-weight: bold; } select, button { padding: 8px; font-size: 14px; } </style> </head> <body> <h1>电影推荐系统</h1> <div> <label>选择用户 ID:</label> <input type="number" id="userId" value="1" min="1"> <button onclick="getRecommendations()">获取推荐</button> </div> <div id="results"></div> <script> async function getRecommendations() { const userId = document.getElementById('userId').value; const response = await fetch(`/api/recommend/${userId}?top_n=10`); const data = await response.json(); const container = document.getElementById('results'); if (data.error) { container.innerHTML = `<p>${data.error}</p>`; return; } let html = '<h2>推荐结果</h2>'; data.recommendations.forEach((movie, index) => { html += `<div class="movie-item"> <span>${index + 1}. ${movie.title}</span> <span class="score">预测评分: ${movie.score}</span> </div>`; }); container.innerHTML = html; } </script> </body> </html>前端逻辑很简单:输入用户 ID,点击按钮,调用后端接口,渲染结果。fetch 是浏览器原生的 HTTP 请求方法,不需要引入 jQuery 或 axios。
这个页面虽然简陋,但足够答辩演示。如果想加分,可以加上电影海报(用 TMDB API 获取)、评分星级、分页加载等功能。但核心是推荐逻辑,界面不用花太多时间。
4.3 冷启动问题的工程处理
冷启动是推荐系统的经典问题:新用户没有评分历史,协同过滤无法工作。工程上有几种处理方式:
第一种是热门推荐兜底。新用户进来时,直接推荐评分人数最多、平均分最高的电影。代码很简单:
def get_popular_movies(ratings, movies, top_n=10, min_ratings=50): """返回热门电影作为冷启动推荐""" movie_stats = ratings.groupby('movieId').agg( avg_rating=('rating', 'mean'), count=('rating', 'count') ).reset_index() # 过滤评分人数少的电影 movie_stats = movie_stats[movie_stats['count'] >= min_ratings] # 按平均分排序 movie_stats = movie_stats.sort_values('avg_rating', ascending=False) # 合并电影标题 result = movie_stats.head(top_n).merge(movies, on='movieId') return result[['movieId', 'title', 'avg_rating', 'count']].to_dict('records')min_ratings 参数控制最低评分人数,设为 50 是为了避免「只有 1 个人打了 5 分」的电影排到第一。这个阈值可以根据数据量调整,数据量大就设高一点。
第二种是基于内容的推荐。用电影的 genres 字段计算内容相似度,给新用户推荐他注册时选择的偏好类型。这个方案实现复杂一些,但效果更好。
第三种是引导用户评分。新用户注册后,弹出 10 部热门电影让他打分,快速积累初始数据。这是工业界最常用的做法,但毕业设计中不一定要做。
实际项目中,通常是热门推荐 + 基于内容推荐组合使用。答辩时能说清楚冷启动的处理策略,是一个加分项。
5. 评估指标与调参:RMSE 之外你还需要看什么
5.1 RMSE、Precision@K、Recall@K 的计算与解读
RMSE 衡量评分预测的准确度,公式是预测评分和真实评分差值的均方根。但它只评估「预测评分准不准」,不评估「推荐列表好不好」。
推荐系统更常用的指标是 Precision@K 和 Recall@K。Precision@K 表示推荐的前 K 部电影中,用户实际喜欢的比例;Recall@K 表示用户实际喜欢的电影中,被推荐出来的比例。
def precision_recall_at_k(predictions, k=10, threshold=3.5): """ 计算 Precision@K 和 Recall@K predictions: 列表,每个元素是 (user_id, movie_id, true_rating, predicted_rating) threshold: 评分高于此值视为「喜欢」 """ # 按用户分组 user_est_true = {} for uid, _, true_r, est in predictions: if uid not in user_est_true: user_est_true[uid] = [] user_est_true[uid].append((est, true_r)) precisions = [] recalls = [] for uid, user_ratings in user_est_true.items(): # 按预测评分降序排列 user_ratings.sort(key=lambda x: x[0], reverse=True) # 前 K 个推荐中实际喜欢的数量 n_rel = sum((true_r >= threshold) for (_, true_r) in user_ratings) n_rec_k = sum((est >= threshold) for (est, _) in user_ratings[:k]) n_rel_and_rec_k = sum( (true_r >= threshold) and (est >= threshold) for (est, true_r) in user_ratings[:k] ) # Precision@K precisions.append(n_rel_and_rec_k / n_rec_k if n_rec_k != 0 else 0) # Recall@K recalls.append(n_rel_and_rec_k / n_rel if n_rel != 0 else 0) return sum(precisions) / len(precisions), sum(recalls) / len(recalls)threshold 设为 3.5 是一个常见选择,表示 4 分及以上算「喜欢」。这个值可以根据业务调整,如果评分普遍偏高,可以提到 4.0。
Precision@K 和 Recall@K 通常此消彼长:K 越大,Recall 越高但 Precision 越低。实际项目中需要根据业务需求平衡。毕业设计里报告 K=10 和 K=20 两组结果就够了。
5.2 相似度阈值与邻居数量的调参实验
协同过滤有几个关键参数需要调:相似度阈值、邻居数量 K、相似度度量方式。建议做一组对比实验,用表格呈现结果。
| 参数组合 | RMSE | Precision@10 | Recall@10 |
|---|---|---|---|
| K=10, cosine | 0.92 | 0.68 | 0.42 |
| K=20, cosine | 0.89 | 0.72 | 0.45 |
| K=40, cosine | 0.87 | 0.71 | 0.48 |
| K=20, pearson | 0.90 | 0.70 | 0.44 |
| K=20, cosine, min_sim=0.1 | 0.88 | 0.73 | 0.46 |
从表中可以看出:K 从 10 增加到 20,RMSE 和 Precision 都有提升;继续增加到 40,RMSE 还在降但 Precision 开始下降,说明引入了噪声。余弦相似度整体优于皮尔逊。加相似度阈值过滤低质量邻居,能小幅提升 Precision。
调参建议:先用 K=20 作为基线,然后上下调整。如果 RMSE 一直降但 Precision 也降,说明模型过拟合了评分预测,需要加正则化或降低 K。
5.3 训练集/测试集划分与交叉验证
评估结果的可信度取决于数据划分方式。常见的有两种:留出法(按时间或随机划分 80/20)和交叉验证(5 折或 10 折)。
from surprise.model_selection import train_test_split, KFold # 留出法 trainset, testset = train_test_split(data, test_size=0.2, random_state=42) # 交叉验证 kf = KFold(n_splits=5, random_state=42, shuffle=True) for trainset, testset in kf.split(data): algo.fit(trainset) predictions = algo.test(testset) # 计算指标...留出法简单快速,适合初步实验。交叉验证更稳定,但计算量是 5 倍。毕业设计建议用 5 折交叉验证,结果更有说服力。
注意:如果按时间划分,要用 timestamp 字段排序后切分,模拟「用过去预测未来」的场景。随机划分会导致数据泄露——用未来的评分预测过去的行为,评估结果虚高。答辩时如果老师问数据划分方式,能说清楚这一点会很加分。
6. 避坑与排查:那些年我们踩过的协同过滤的坑
6.1 相似度矩阵全为零或接近零
现象:计算出的物品相似度矩阵大部分值都是 0,推荐结果为空或全是热门电影。
原因:数据太稀疏,两个电影之间没有共同评分用户。MovieLens small 数据集的稀疏度约 98%,意味着 98% 的用户-电影对没有评分。如果过滤阈值设得太高,共同评分更少。
解决:降低过滤阈值(从 5 降到 3),或者用降维方法(SVD、NMF)先压缩矩阵再算相似度。另一个方法是改用基于内容的相似度,用 genres 字段计算电影相似度,不依赖用户评分。
6.2 推荐结果全是已经看过的电影
现象:推荐列表里出现了用户已经评分过的电影。
原因:相似度矩阵对角线没有置零,或者推荐时没有排除已评分电影。
解决:np.fill_diagonal(item_similarity, 0) 必须在计算完相似度后立即执行。推荐函数里用 rated_items = set(np.where(matrix[user_idx] > 0)[0]) 获取已评分集合,生成推荐时跳过这些电影。
6.3 RMSE 很低但推荐结果很差
现象:交叉验证 RMSE 只有 0.8,但实际推荐列表里全是用户不感兴趣的电影。
原因:RMSE 衡量的是评分预测准确度,不是推荐质量。模型可能学会了「用户对大多数电影都打 3.5 分」这个规律,预测值都接近 3.5,RMSE 很低但推荐没有区分度。
解决:同时看 Precision@K 和 Recall@K。如果 RMSE 低但 Precision 也低,说明模型没有学到用户偏好,需要调整算法或增加特征。另外可以看推荐列表的多样性,如果全是同一类型的电影,说明相似度计算有问题。
6.4 Flask 接口响应超时
现象:前端请求推荐接口,等了十几秒才返回,甚至超时。
原因:每次请求都重新计算相似度矩阵,或者逐个预测所有未评分电影的评分。
解决:相似度矩阵在应用启动时预计算,存为全局变量。推荐时只对候选集(比如相似度 Top-100 的电影)做预测,不要遍历全部电影。如果还慢,可以用缓存(functools.lru_cache)缓存热门用户的推荐结果。
6.5 冷启动用户直接报错
现象:新用户 ID 不在训练集中,调用推荐接口返回 404 或空列表。
原因:协同过滤只能对训练集中出现过的用户做推荐,新用户没有历史评分。
解决:在接口层做判断,如果 user_id 不在 user_ids 中,返回热门推荐而不是报错。前端也要处理这种情况,显示「暂无个性化推荐,以下是热门电影」。
7. 进阶技巧:用矩阵分解提升推荐效果并让答辩更有底气
协同过滤的局限在于它只用了用户-物品评分矩阵,没有利用电影类型、用户画像等辅助信息。矩阵分解(Matrix Factorization)是工业界更常用的方案,也是毕业设计里能拉开差距的加分项。
核心思路是把用户-物品矩阵分解成两个低维矩阵的乘积:用户隐向量矩阵和物品隐向量矩阵。预测评分就是两个隐向量的点积。用 Surprise 的 SVD 实现只要几行:
from surprise import SVD from surprise.model_selection import cross_validate # SVD 矩阵分解 algo = SVD( n_factors=50, # 隐向量维度 n_epochs=20, # 迭代次数 lr_all=0.005, # 学习率 reg_all=0.02, # 正则化系数 random_state=42 ) results = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=5, verbose=True) print(f"SVD 平均 RMSE: {results['test_rmse'].mean():.4f}")n_factors 控制隐向量维度,太小欠拟合,太大过拟合,50 是一个常用起点。n_epochs 是 SGD 迭代次数,20 到 50 之间。lr_all 是学习率,0.005 比较保守,0.01 收敛快但可能震荡。reg_all 是正则化系数,防止过拟合,0.02 到 0.1 之间调整。
SVD 的 RMSE 通常比 ItemCF 低 0.02 到 0.05,在毕业设计里是一个明显的提升。但要注意:SVD 的可解释性不如 ItemCF,答辩时老师问「为什么推荐这部电影」,ItemCF 可以说「因为和你看过的某部电影相似」,SVD 只能说是隐向量的匹配结果。所以建议两个都做,对比分析。
另一个进阶方向是混合推荐:用 ItemCF 做召回,用 SVD 做排序,或者用内容特征补充协同过滤。这个在毕业设计里属于「工作量饱满」的加分项,但不要为了复杂而复杂,先把基础方案跑通再考虑。
我自己的习惯是:先把 ItemCF 的完整链路跑通,记录好 RMSE 和 Precision@K,然后再上 SVD 做对比。这样即使 SVD 效果不好,也有保底方案。答辩时展示两组结果和对比分析,比只报一个高分更有说服力。希望帮到你。
本文还有配套的精品资源,点击获取