简介:基于协同过滤算法的学习资源个性化推荐系统是一份完整的硕士毕业设计项目包,适合计算机及相关专业学生用于毕业设计或课程设计参考。压缩包共232个文件,以Java源码、JavaScript脚本、JSP页面和CSS样式为主,另含SQL数据库脚本、XML配置、文本与图片素材,整体仅1.74MB,便于直接获取与部署。目前已有59人学习,项目通过协同过滤算法分析用户行为,实现学习资源的个性化推荐,完整覆盖从用户评分、相似度计算到结果推荐的流程。内含前后端完整代码、相关配置文件与说明文档,涉及协同过滤算法实现、用户行为分析、数据库设计、API接口交互及安全防护等工程化知识点,既适合算法研究与系统二次开发,也可作为课设/毕设的参考资料。从导入数据到构建推荐模型,可完整理解推荐系统的开发脉络。
1. 学习资源推荐不是“看过的人也在看”,而是隐性反馈的建模题
学习资源推荐系统和电商推荐系统一个明显的不同,是用户几乎不主动打分。播放进度、完成率、收藏、加入学习计划,这些行为信号各自携带不同的置信度,直接把它们当成原始分数扔进协同过滤算法,得到的结果就会向热门资源倾斜,“个性化”也跟着消失。硕士阶段做这个题目,核心不是把 sklearn 接口调通,而是从数据建模阶段就回答一个问题:用户-资源交互矩阵怎么填,推荐结果才能在覆盖长尾的同时又可解释、可评估。下面按一条可复现的路径展开:先定交互矩阵和相似度度量,再实现 UserCF、ItemCF 两套算法对比,然后做离线评估与参数调优,最后落到多路召回和可演示的服务接口上。
2. 协同过滤的数学基础与 UserCF、ItemCF 的选型依据
2.1 交互矩阵不填评分,填“置信度”
协同过滤算法的输入是一张 M x N 的矩阵,行是用户,列是资源。学习资源领域几乎没有显式评分,所以矩阵里的每个非零值都是根据行为日志折算出来的。常见做法是给行为类型分配权重,再乘上完成度或观看时长占比,得到一个带有置信度含义的值。
import pandas as pd import numpy as np log = pd.read_csv("behavior_log.csv") log["completed_ratio"] = log["watch_seconds"] / log["duration_seconds"] weight_map = { "view": 0.2, "start": 0.4, "collect": 0.8, "complete": 1.0, } log["base_score"] = log["behavior_type"].map(weight_map) * log["completed_ratio"].clip(upper=1.0) # 同一个人对同一篇资源只保留一条聚合记录 rating_df = log.groupby(["user_id", "resource_id"], as_index=False)["base_score"].max() rating_matrix = rating_df.pivot_table( index="user_id", columns="resource_id", values="base_score" ).fillna(0.0)这段代码里有两个值得注意的选择:一是用max而不是sum做聚合,避免用户反复打开同一份文档导致分数虚高;二是用clip(upper=1.0)限制完成度上限,防止回放类行为把权重放大。这样生成的矩阵每个元素都落在 0 到 1 之间,语义是“该用户对这份资源的偏好置信度”,而不是“预测评分”。
选择max也有代价:丢失了“重复访问”这个有效期信息。如果同一篇资源在 30 天内被打开 20 次,说明用户在做针对性复习,此时max会低估。工业界更精细的做法是对重复访问次数也折算一个增量系数,但硕士毕设阶段用max加时间衰减已经足够匹配绝大多数学习场景。
2.2 余弦相似度、皮尔逊相关与 Jaccard 的适用边界
矩阵确定后,下一步是选相似度度量。学习资源交互矩阵非常稀疏,很多用户之间根本没有共同交互过的资源,所以度量方式的选择会直接影响邻居质量和随后的推荐覆盖面。
| 度量方式 | 公式特征 | 适用场景 | 需要注意的问题 |
|---|---|---|---|
| Jaccard | 交集大小除以并集大小 | 只看“是否交互”的布尔行为 | 丢失完成度、时长等强度信息 |
| 余弦相似度 | 向量余弦夹角 | 隐式反馈、值域非负 | 对不同用户评分尺度不敏感 |
| 皮尔逊相关 | 中心化后的余弦 | 显式评分、用户打分尺度差异大 | 共同交互数太少时方差极大 |
学习资源场景建议默认采用余弦相似度,因为交互值已经折算到 0~1 的置信度区间,不涉及不同用户“打分习惯”的差异。皮尔逊适合存在真实星级评分的系统,但本场景少见。
def cosine_similarity_item(matrix_values): norms = np.linalg.norm(matrix_values, axis=1, keepdims=True) + 1e-9 normalized = matrix_values / norms return normalized.dot(normalized.T)皮尔逊相关和余弦相似度只在“是否先减去均值”上有区别。对于冷启动用户,减均值会把大量无偏差的零向量变成异常值,因此不建议对冷用户做行中心化。若项目中必须支持皮尔逊,建议限定最低共同交互数,比如共同交互少于 3 项就返回相似度为 0。
2.3 UserCF 与 ItemCF 的选型:先看资源增长速度
两套协同过滤算法的差异可以用一句话概括:UserCF 找“喜欢相似内容的人”在学什么,ItemCF 找“和你学过内容相似的其他内容”。具体到学习资源系统,选型判断依据主要有四个维度。
| 维度 | UserCF | ItemCF |
|---|---|---|
| 用户数增长 | 用户增长快则计算量骤增 | 与用户数无关,只受资源数影响 |
| 资源数量 | 资源多反而有利 | 资源数受控时相似矩阵规模可控 |
| 新资源覆盖 | 用户行为实时反映,能较快发现新内容 | 新资源无交互,基本无法被推荐 |
| 推荐解释 | “和你相似的同学学过” | “因为你学过 XX” |
ItemCF 的相似矩阵可以离线午夜批量计算,线上只读内存里的相似度表,响应速度快;UserCF 则要求用户相似度随用户近端行为变化,实时更新成本高。多数小组件学习平台的资源数量在几千到几万之间,远小于用户数,所以 ItemCF 是更稳妥的第一步。但毕设答辩通常会要求算法对比,至少要把两套都实现出来,再在评估阶段用数据说明为什么最终选择 ItemCF。
from sklearn.metrics.pairwise import cosine_similarity def user_based_recommend(user_id, rating_matrix, k=20, top_n=10): user_sim = cosine_similarity(rating_matrix.values) np.fill_diagonal(user_sim, 0) uid = rating_matrix.index.get_loc(user_id) neighbor_idx = np.argsort(-user_sim[uid])[:k] sims = user_sim[uid][neighbor_idx] # 邻居对每个资源的交互强度加权 neighbor_rated = (rating_matrix.iloc[neighbor_idx].values > 0).astype(float) item_scores = neighbor_rated.T.dot(sims) scores = pd.Series(item_scores, index=rating_matrix.columns) scores[rating_matrix.loc[user_id] > 0] = 0 return scores.sort_values(ascending=False).head(top_n).index.tolist()这段代码用向量方式替代循环:先算用户相似度矩阵,再取每个用户最相似的 k 个邻居,按相似度累加“邻居是否交互过该资源”。注意最后把用户已经交互的资源归零,避免推荐已学过内容。UserCF 的实现同样可以用作第 5 章多路召回中的一路。
3. 学习资源域的数据表设计、评分矩阵构建与 ItemCF 实现
3.1 数据表该表大宽表,还是三张业务表
常见项目会设计一张包含所有字段的大宽表,用户信息、资源属性、行为记录全挤在一起。这种设计在数据量大时维护成本高,算法迭代也容易误伤特征列。推荐按业务实体拆成三张表:用户表、资源表、行为日志表。行为表只记录“谁在什么时间对哪个资源做了什么”,资源属性交给资源表自己维护。
CREATE TABLE sys_user ( user_id VARCHAR(32) PRIMARY KEY, grade TINYINT COMMENT '年级/学段', major_tag VARCHAR(64) COMMENT '专业或学科方向', created_at DATETIME ); CREATE TABLE resource ( resource_id VARCHAR(32) PRIMARY KEY, resource_type VARCHAR(16) COMMENT 'video/document/quiz/exercise', subject VARCHAR(64), difficulty TINYINT COMMENT '1~5', duration_sec INT, published_at DATETIME ); CREATE TABLE behavior_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id VARCHAR(32), resource_id VARCHAR(32), behavior_type VARCHAR(16) COMMENT 'view/start/collect/complete', watch_sec INT, duration_sec INT, created_at DATETIME, KEY idx_user_resource (user_id, resource_id), KEY idx_resource_time (resource_id, created_at) );这里两个索引有讲究:idx_user_resource覆盖推荐算法最频繁的查询“某个用户学过哪些资源”,idx_resource_time支撑离线任务按资源聚合近 30 天行为。日志表后续如果想做时间衰减,只需要在created_at上加时间范围条件,不应让业务查询每次都全表扫描。
3.2 ItemCF 的核心实现:归一化相似度、邻居筛选与 Top-N
有了交互矩阵,ItemCF 可以收敛到一个类里。下面的实现只依赖 NumPy,方便在 Jupyter 里逐段验证,也方便迁移到 Spark 或 Ray 做批量扩展。
class ItemCF: def __init__(self, k=20, alpha=0.1): self.k = k self.alpha = alpha def fit(self, rating_matrix): self.rating_matrix = rating_matrix # 对每个资源向量做归一化,再点积得到余弦相似矩阵 item_matrix = rating_matrix.values.T norms = np.linalg.norm(item_matrix, axis=1, keepdims=True) + 1e-9 self.sim = (item_matrix / norms).dot((item_matrix / norms).T) np.fill_diagonal(self.sim, 0.0) def recommend(self, user_id, top_n=10): matrix = self.rating_matrix row = matrix.loc[user_id].values interacted = row > 0 scores = np.zeros(matrix.shape[1]) for item_idx in np.where(~interacted)[0]: neighbor_idx = np.argsort(-self.sim[item_idx])[:self.k] sims = self.sim[item_idx][neighbor_idx] neighbor_ratings = row[neighbor_idx] mask = neighbor_ratings > 0 if mask.sum() == 0: continue scores[item_idx] = ( (sims[mask] * neighbor_ratings[mask]).sum() / (sims[mask].sum() + self.alpha) ) top_item_idx = np.argsort(-scores)[:top_n] return matrix.columns[top_item_idx].tolist()代码里最重要的参数是k和alpha。k控制邻居数量,太小推荐结果抖动,太大推荐结果趋向全局热门;alpha是分母上的平滑项,作用是对那些只有一两个弱邻居的候选资源降权。alpha取 0.05 到 0.2 之间通常表现稳定,它让“少数人高相似度”的候选不会盖过“很多人中等相似度”的候选。
注意这段实现有一个隐含假设:相似度矩阵已经提前算好并常驻内存。实际工程里,相似度矩阵要在离线任务中定期重算,而不是在推荐接口里现算。矩阵规模为“资源数 x 资源数”,一万个资源就是 1 亿个浮点数,约 400MB 内存,单机可以扛住;超过 5 万资源就该考虑用 Faiss 或 Spark 做分片计算了。
3.3 时间衰减与重复交互的合并策略
学习资源具有很强的时效性,比如考试大纲调整、软件版本更新。此时需要引入一个按天衰减的因子,让旧的交互权重逐步降低。
import numpy as np def time_decay(days_since, half_life_days=90): return 0.5 ** (days_since / half_life_days)在构造交互矩阵时,把这层衰减乘到行为分数上,公式变为:最终分值 = 行为权重 × 完成度 × 时间衰减系数。半衰期按资源类型调整:刷题类资源建议 30 天,系统性课程可以放宽到 180 天。这个参数的设置没有标准答案,正确的验证方式是在离线评估里比较不同半衰期下的命中率,而不是靠主观经验拍板。
4. 离线评估指标、K 值调参与冷启动缓解策略
4.1 用时间切分代替随机切分
训练测试集的划分方式直接决定评估结果可信度。随机切分会把未来行为混进训练集,让推荐看起来比实际效果更好,所以必须按时间切分。
log["created_at"] = pd.to_datetime(log["created_at"]) cutoff = log["created_at"].quantile(0.8) train_log = log[log["created_at"] < cutoff] test_log = log[log["created_at"] >= cutoff]测试集只保留训练集中没有出现过的“用户-资源”交互,否则命中率的计算会被历史行为污染。具体做法是在评估循环里,把test_log中已经出现在训练矩阵里的条目剔除。
4.2 命中率、精确率、召回率与 NDCG 的代码实现
离线评估的常见指标有四类:命中率用来回答“推荐列表里有没有用户真正点过的资源”,精确率和召回率衡量覆盖程度,NDCG 关注推荐顺序。
def evaluate_recommender(model, rating_matrix, test_dict, top_n=10): hit = 0.0 precision_sum = 0.0 recall_sum = 0.0 ndcg_sum = 0.0 for user_id, test_items in test_dict.items(): if user_id not in rating_matrix.index: continue rec_items = model.recommend(user_id, top_n=top_n) rec_set = set(rec_items) hit_set = rec_set & set(test_items) hit += len(hit_set) > 0 precision_sum += len(hit_set) / top_n recall_sum += len(hit_set) / len(test_items) ndcg_sum += ndcg_at_k(rec_items, test_items, top_n) n = len(test_dict) return { "hit_rate": hit / n, "precision": precision_sum / n, "recall": recall_sum / n, "ndcg": ndcg_sum / n, } def ndcg_at_k(ranked_list, rel_items, k=10): dcg = 0.0 for i, item in enumerate(ranked_list[:k]): if item in rel_items: dcg += 1 / np.log2(i + 2) idcg = sum(1 / np.log2(i + 2) for i in range(min(k, len(rel_items)))) return dcg / idcg if idcg > 0 else 0.0四个指标的角色不一样。命中率适合整体判断推荐是否有效;精确率和召回率是互斥的,调参时通常看召回率是否随top_n提升而稳定上升;NDCG 则专门检验“正确的资源是不是排在最前面”。如果 NDCG 一直低于 0.3,说明候选列表里虽然猜中了资源,但排序逻辑有问题,优先检查相似度归一化和邻居数k。
4.3 K 值、平滑项和热门惩罚的调参方向
ItemCF 的核心超参数集中在一个表里,建议在离线评估脚本中做网格搜索。
| 参数 | 推荐区间 | 调参信号 | 过拟合表现 |
|---|---|---|---|
| 邻居数 k | 10~50 | 命中率和 NDCG 同时上升 | 指标在某个 k 后回落 |
| 平滑项 alpha | 0.05~0.2 | 冷门资源曝光比例 | 数值太大导致推荐全变热门 |
| 时间半衰期 | 30~180 天 | 近期行为占比高的用户命中 | 半衰期过短导致长期偏好丢失 |
| 热门惩罚强度 | 0.05~0.2 | 专栏封面点击排序前的曝光位置 | 惩罚过头则推荐尾部噪声明显 |
热门惩罚的常见实现是在最终得分上乘一个冷门系数:
def popularity_penalty(resource_id, popular_dict, strength=0.1): count = popular_dict.get(resource_id, 0) return 1.0 / (1.0 + strength * np.log1p(count))strength越大,热门资源越难出现在推荐前列。学习资源领域有个特殊性:教学大纲里的重要知识点本身就在高频访问,过度惩罚会误伤刚需资源,所以强度建议从 0.05 起步逐步增加。
4.4 冷启动的缓解策略
推荐算法只能覆盖有历史行为的用户,新用户和刚上架的资源都会掉出候选池。常见做法有三个层次。
第一个层次是“热门兜底”,新用户在没有行为日志时返回站内近 7 天最热资源,这个策略虽然不个性化,但能保证首页不空。第二个层次是“基于元数据的相似度回退”,当资源没有交互时,用资源类型、学科、难度这些属性计算内容相似度,作为协同过滤的降级通道。第三个层次是“注册引导”,在用户注册界面收集学科方向和学习目标,把引导结果映射为虚拟交互,比如让用户勾选感兴趣方向并赋 0.5 分初始交互。
这三个策略在代码层面都不复杂,难点在于如何组织优先级。建议顺序是:用户有行为走 ItemCF;用户无行为但有注册画像走内容相似度回退;两者都没有则走热门榜单。评估冷启动效果时,只统计测试集中“用户行为数小于等于 3”的用户子集,单独计算指标。
5. 多路召回混合、在线接口与可演示的验证链路
5.1 多路召回加权融合的通用实现
单一 ItemCF 的推荐结果有较强的同质性,比如学完“Python 基础语法”后,推荐的还是语法相关内容。实际系统中可以把 ItemCF、UserCF、热门资源当作三路召回源,再按位次加权合并。
def ensemble_recall(rec_lists, weights=(0.5, 0.3, 0.2), top_n=10): final_score = {} for recs, w in zip(rec_lists, weights): for rank, item in enumerate(recs): final_score[item] = final_score.get(item, 0.0) + w / (rank + 1) return sorted(final_score.items(), key=lambda x: -x[1])[:top_n]这里的权重不需要手工编太久,离线评估时把三个通道的输出各自打分后,用网格搜索权重组合即可。注意:如果三路中某一路返回为空,权重需要重新做归一化,否则分数会整体偏低。常见做法是先过滤空召回源,再对剩余权重做等比缩放。
5.2 预计算 + 轻量 API 的最小服务
推荐服务属于读多写少的场景,离线算好相似度矩阵后,接口只需要做矩阵查询和组装。下面是最小可运行的 FastAPI 版本:
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() model = ItemCF(k=20, alpha=0.1) model.fit(rating_matrix) class RecommendRequest(BaseModel): user_id: str top_n: int = 10 @app.post("/recommend") def recommend(req: RecommendRequest): rec_items = model.recommend(req.user_id, top_n=req.top_n) return {"user_id": req.user_id, "items": rec_items}服务启动时一次性加载相似度矩阵,接口请求时只对当前用户的交互行做计算,响应时间基本在 20ms 内。如果模型在每天凌晨更新,需要再加一个/reload接口或重启服务,避免线上读到旧矩阵。
5.3 答辩现场的三步验证清单
毕业设计演示最怕“只能看不能碰”。准备三个可一键运行的命令,让评委自己操作更可信。
# 第一步:离线评估,输出 hit_rate / precision / recall / ndcg python evaluate.py --model itemcf --k 20 --alpha 0.1 --top_n 10 # 第二步:给一个老用户推荐,展示被过滤掉的已学资源 python recommend.py --user_id stu_202 # 第三步:给一个只有 2 条行为的新用户推荐,观察冷启动回退 python recommend.py --user_id stu_800 --cold_start验证清单里必须包含一个容易被人忽视的细节:新老用户的推荐结果要有明显区别。如果新用户和老用户返回的是同一批热门资源,冷启动逻辑就失败了。另一个值得演示的点是“资源去重”。
对已经学完的资源,推荐列表里不应再出现;如果 ItemCF 的交互矩阵没有把历史交互归零,就会出现这种低级 bug。最后把运行结果和评估表截图放进论文附录,整个“理论-实现-评估-落地”链路就闭合了。
本文还有配套的精品资源,点击获取