YAOTU INSIGHTS

学习资源推荐系统实战:从交互矩阵到ItemCF协同过滤

学习资源推荐系统实战:从交互矩阵到ItemCF协同过滤
简介基于协同过滤算法的学习资源个性化推荐系统是一份完整的硕士毕业设计项目包适合计算机及相关专业学生用于毕业设计或课程设计参考。压缩包共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(upper1.0) # 同一个人对同一篇资源只保留一条聚合记录 rating_df log.groupby([user_id, resource_id], as_indexFalse)[base_score].max() rating_matrix rating_df.pivot_table( indexuser_id, columnsresource_id, valuesbase_score ).fillna(0.0)这段代码里有两个值得注意的选择一是用max而不是sum做聚合避免用户反复打开同一份文档导致分数虚高二是用clip(upper1.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, axis1, keepdimsTrue) 1e-9 normalized matrix_values / norms return normalized.dot(normalized.T)皮尔逊相关和余弦相似度只在“是否先减去均值”上有区别。对于冷启动用户减均值会把大量无偏差的零向量变成异常值因此不建议对冷用户做行中心化。若项目中必须支持皮尔逊建议限定最低共同交互数比如共同交互少于 3 项就返回相似度为 0。2.3 UserCF 与 ItemCF 的选型先看资源增长速度两套协同过滤算法的差异可以用一句话概括UserCF 找“喜欢相似内容的人”在学什么ItemCF 找“和你学过内容相似的其他内容”。具体到学习资源系统选型判断依据主要有四个维度。维度UserCFItemCF用户数增长用户增长快则计算量骤增与用户数无关只受资源数影响资源数量资源多反而有利资源数受控时相似矩阵规模可控新资源覆盖用户行为实时反映能较快发现新内容新资源无交互基本无法被推荐推荐解释“和你相似的同学学过”“因为你学过 XX”ItemCF 的相似矩阵可以离线午夜批量计算线上只读内存里的相似度表响应速度快UserCF 则要求用户相似度随用户近端行为变化实时更新成本高。多数小组件学习平台的资源数量在几千到几万之间远小于用户数所以 ItemCF 是更稳妥的第一步。但毕设答辩通常会要求算法对比至少要把两套都实现出来再在评估阶段用数据说明为什么最终选择 ItemCF。from sklearn.metrics.pairwise import cosine_similarity def user_based_recommend(user_id, rating_matrix, k20, top_n10): 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, indexrating_matrix.columns) scores[rating_matrix.loc[user_id] 0] 0 return scores.sort_values(ascendingFalse).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, k20, alpha0.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, axis1, keepdimsTrue) 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_n10): 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_days90): 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_n10): 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_ntop_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, k10): 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 的核心超参数集中在一个表里建议在离线评估脚本中做网格搜索。参数推荐区间调参信号过拟合表现邻居数 k10~50命中率和 NDCG 同时上升指标在某个 k 后回落平滑项 alpha0.05~0.2冷门资源曝光比例数值太大导致推荐全变热门时间半衰期30~180 天近期行为占比高的用户命中半衰期过短导致长期偏好丢失热门惩罚强度0.05~0.2专栏封面点击排序前的曝光位置惩罚过头则推荐尾部噪声明显热门惩罚的常见实现是在最终得分上乘一个冷门系数def popularity_penalty(resource_id, popular_dict, strength0.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_n10): 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(), keylambda x: -x[1])[:top_n]这里的权重不需要手工编太久离线评估时把三个通道的输出各自打分后用网格搜索权重组合即可。注意如果三路中某一路返回为空权重需要重新做归一化否则分数会整体偏低。常见做法是先过滤空召回源再对剩余权重做等比缩放。5.2 预计算 轻量 API 的最小服务推荐服务属于读多写少的场景离线算好相似度矩阵后接口只需要做矩阵查询和组装。下面是最小可运行的 FastAPI 版本from fastapi import FastAPI from pydantic import BaseModel app FastAPI() model ItemCF(k20, alpha0.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_nreq.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。最后把运行结果和评估表截图放进论文附录整个“理论-实现-评估-落地”链路就闭合了。本文还有配套的精品资源点击获取