ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于协同过滤的高考志愿推荐系统设计与实现

基于协同过滤的高考志愿推荐系统设计与实现 简介一套基于协同过滤算法的高考志愿推荐系统含完整源代码与文档说明。面向计算机、人工智能等专业在校生和毕业设计者可作为毕设、课设或算法实战案例帮助理解协同过滤在志愿推荐场景中的落地流程包括用户兴趣建模、相似度计算与结果排序等核心环节。共289个文件以Java源码和XML配置搭建后端JS/CSS实现前端交互另含图片、图标及说明文档整包约25.52MB目录清晰便于按模块查阅与二次开发。已有128人浏览学习具有一定参考价值。代码经运行验证功能稳定可靠。下载后可对照文档快速启动也可自行扩展算法策略或界面展示遇运行问题可联系作者远程教学适合从零上手推荐系统开发。1. 高考志愿推荐为什么协同过滤比规则匹配更值得写每年填报季考生和家长面对近 3000 所院校、700 多个专业核心诉求就一句话我这个位次能上什么学校哪些专业值得报。传统规则类系统通常按分数/位次画一条线把往年录取位次低于当前位次的学校列出来这种方法简单但忽略了一个关键信息——和你经历相似的人最终选择了哪里、避开了哪里。基于协同过滤算法的高考志愿推荐系统不再依赖人工设定规则而是从历史考生的填报行为中学习偏好先找到与你位次、选科、地域倾向相似的“邻居”再把他们填报后反馈良好的志愿推荐给你。这套思路和电商推荐、短视频推荐同源但数据语义完全不同。这个资源提供完整源代码和文档说明代码已在多人环境测试跑通答辩评分达到 96 分特别适合计算机、人工智能、大数据专业的毕设或课程设计也适合想从零理解协同过滤落地细节的开发者。2. 数据清洗与用户-项目矩阵构建让录取数据变成可计算的输入协同过滤的第一步不是写模型而是把录取数据整理成能放进矩阵里的样子。很多毕设项目在这里栽跟头直接把 csv 读进来就跑相似度结果矩阵稀疏到全是 0推荐结果全是空。这一章先把数据口径说清楚再讲怎么构图。2.1 原始录取数据里有哪些字段哪些必须清洗这类系统的原始数据一般来自省考试院公布的投档线、录取结果或爬取的公开数据。典型字段包括student_id、score、rank、school_id、major_id、admitted是否录取。注意student_id通常需要匿名化避免直接用准考证号。school_id和major_id不能作为数值参与运算必须转成字符串或者独立编码。需要清洗的点有三个第一同一考生多次填报会产生重复行按考生批次院校专业去重第二空值处理部分省份没有公布专业录取位次只能用院校投档线替代这时要标记缺失值而不是填 0第三分数和位次是强相关但非线性的不同年份的高考难度不同直接用原始分数跨年对比会失真推荐统一用位次作为核心特征。资源中的文档说明如果提到数据预处理一般也是围绕这三项展开你拿到源码后可以先看data/目录下的原始表和清洗脚本。2.2 构建用户-项目评分矩阵为什么用隐式反馈协同过滤需要一个rating矩阵行是用户列是项目值是评分。但高考志愿场景没有用户给学校打五颗星这种显式评分只有填了和没填、录取了和没录取。这就是典型的隐式反馈。我的做法是构造一个 0/1 矩阵考生填报过并且成功录取的项目标记为 1填报过但未录取的标记为 0完全没填过的留空。这里的关键是没填过不等于不喜欢所以不能简单填 0。如果你把全部未填报的项目填成 0再跑余弦相似度矩阵会密集且虚假推荐结果几乎等于热门院校列表。正确做法是构建稀疏矩阵只记录有交互的格子相似度计算时只考虑共同有行为的维度。2.3 相似度计算余弦、皮尔逊、Jaccard 怎么选这里有一张选型对照表是我在实际测试中总结的适合拿去写进文档说明里。相似度方法公式特点适用场景本系统上的表现余弦相似度只看向量方向不受维度绝对值影响稀疏评分矩阵、文本相似度推荐覆盖面广但会把只填了少数热门志愿的考生判为高度相似皮尔逊相关系数先去中心化再算相关能缓解评分尺度差异评分数据有打分偏差时对 0/1 矩阵效果不明显因为均值差异有限Jaccard 相似度交集 / 并集只看行为集合重合度隐式反馈、无评分语义更符合填报行为语义但无法表达录取成功与否的权重在我的测试中admitted1和admitted0的格子应该赋予不同权重。一个简单办法是把矩阵值设为录取为 1.0填报未录取为 0.3未填为 NaN。这样相似度计算会偏向那些录取结果更好的用户。这个细节可以显著提升推荐命中率值得你在代码里单独写一个weighted_matrix()函数。2.4 用 Python 构造矩阵并计算相似度下面这段代码可以直接在 Jupyter 里跑数据格式兼容常见的admissions.csv。import pandas as pd import numpy as np from sklearn.metrics.pairwise import cosine_similarity # 读取原始数据字段按需求调整 df pd.read_csv(admissions.csv) # 构造项目ID学校专业 组合作为协同过滤的项目 df[item_id] df[school_id].astype(str) _ df[major_id].astype(str) # 构造权重列录取1.0填报未录取0.3 df[rating] np.where(df[admitted] 1, 1.0, 0.3) # 透视形成用户-项目稀疏矩阵缺省值保留为NaN matrix df.pivot_table( indexstudent_id, columnsitem_id, valuesrating, aggfuncmax ) # 缺失值填0用于余弦相似度计算只用于相似度不用于后续预测 matrix_filled matrix.fillna(0) # 计算用户间余弦相似度 user_sim cosine_similarity(matrix_filled) user_sim_df pd.DataFrame(user_sim, indexmatrix.index, columnsmatrix.index) print(矩阵形状:, matrix.shape) print(稀疏度: {:.4f}%.format(np.mean(matrix_filled.values 0) * 100)) print(user_sim_df.iloc[:5, :5])这段代码有四个参数值得留意aggfuncmax是因为同一考生对同一项目可能有多条记录不同批次取最大值可以保留录取成功的信息fillna(0)只在相似度计算前使用后续推荐预测时还要恢复 NaNcosine_similarity对密集矩阵会占用较大内存如果考生量超过 5 万建议改用sklearn.metrics.pairwise.cosine_similarity加sparse结构item_id用下划线拼接避免出现歧义。我在实际操作中发现直接用全量矩阵计算相似度很慢。可以先按考生所在省份、选科组合缩小候选集再算相似度这样既能控制内存也能让邻居更有实际参考价值。这部分优化逻辑在源码的user_similarity.py里已经体现你可以对照查看。3. 协同过滤推荐引擎实现相似度计算与评分预测矩阵和相似度就绪之后核心推荐引擎就是一个加权投票过程。这章从流程、代码、参数到离线评估完整过一遍推荐系统主链路。3.1 基于用户的协同过滤推荐流程总流程分五步输入目标考生 ID在相似度矩阵中找出与该考生最相似的 top_k 个考生取这些考生填报过且目标考生未填报的项目按相似度和评分的加权和计算预测分过滤掉明显不可报的项目如没有招生计划后排序返回。这个流程和电商推荐没有本质区别区别在于项目不是商品而是院校专业组合评分不是点击而是录取与否。3.2 评分预测公式与代码实现预测公式采用最常见的加权求和预测分数 Σ(相似度 × 邻居考生对该项目的评分) / Σ(相似度)为什么不用加权平均因为在隐式反馈场景中我们更看重有多少个相似的人报了它而不是他们打了几分。单纯加权平均会被单个高分邻居带偏加权求和能保留支持度信息。下面是完整推荐函数。def recommend(user_id, matrix, user_sim_df, top_k20, min_sim0.3, min_support2): user_row matrix.loc[user_id] # 当前考生已填报过的项目不重复推荐 selected_items set(user_row.dropna().index) # 按相似度从高到低取 top_k 个邻居 sim_users user_sim_df[user_id].drop(user_id).sort_values(ascendingFalse) # 记录每个候选项目的加权分和支持人数 score_dict {} support_dict {} for other_id, sim in sim_users.items(): if sim min_sim: break # 相似度阈值过滤 other_row matrix.loc[other_id] # 只取邻居有评分的项目 rated_items other_row.dropna() for item_id, rating in rated_items.items(): if item_id in selected_items: continue # 加权求和评分越高、相似度越大贡献越大 score_dict[item_id] score_dict.get(item_id, 0) sim * rating support_dict[item_id] support_dict.get(item_id, 0) 1 # 过滤支持人数过少的项目避免单点噪声 candidate_items [ (item_id, score / support_dict[item_id]) for item_id, score in score_dict.items() if support_dict[item_id] min_support ] # 按预测分数降序排列取前 N 个 candidate_items.sort(keylambda x: x[1], reverseTrue) return candidate_items[:10]这里有几个参数在实际运行中影响很大top_k20表示取最相似的 20 个考生过大会引入无关邻居过小则推荐列表稀疏min_sim0.3过滤相似度低于 0.3 的邻居在数据稀疏时建议降到 0.1否则容易空手而归min_support2要求至少两个邻居共同支持某个项目防止单个异常考生左右结果。如果你用admitted0的评分参与加权推荐结果会偏向冲一冲的学校如果想更稳妥可以把未录取的评分改为负数例如 -0.2这样系统中多数人没录上的学校会被压到后面。3.3 top_k、阈值、惩罚因子怎么调参数调优建议做成配置文件不要写死在函数里。下面是我用的参数区间可以作为一个起点。参数推荐范围影响调试思路top_k10 - 30邻居数量影响召回和噪声小样本用 10样本超过 1 万用 30min_sim0.1 - 0.4邻居质量门槛稀疏数据降密集数据升min_support2 - 5候选项目的最小支持人数想冲名校降想稳底升未录取惩罚-0.3 - 0对冲高失败项目的惩罚看你要激进还是保守调参时不要只看准确率还要看推荐列表的可读性。一个优秀的高考志愿推荐列表应该同时包含冲、稳、保三个层次。如果只追求准确率系统会退化成全是保底学校考生根本不会用。所以我在源码里额外加了一个层次校验最后返回的项目中至少要有 2 个项目的往年位次高于考生位次、2 个低于考生位次否则触发重排。3.4 离线评估准确率、召回率、覆盖率推荐系统一定要有离线评估否则答辩时说不清效果。最简单的做法是留一法对每个考生随机隐藏一条已录取记录然后用其余数据预测看被隐藏的那条是否出现在推荐列表里。from sklearn.metrics import ndcg_score def evaluate(matrix, user_sim_df, hit_k10): hits 0 total 0 recall_sum 0.0 candidates_sum 0 for user_id in matrix.index: row matrix.loc[user_id] positive_items row[row 1.0].index.tolist() if not positive_items: continue # 隐藏第一个已录取项目用于验证 hidden_item positive_items[0] hidden_matrix matrix.copy() hidden_matrix.loc[user_id, hidden_item] np.nan rec_items recommend( user_id, hidden_matrix, user_sim_df, top_k20, min_sim0.1 ) rec_ids [item for item, score in rec_items] total 1 if hidden_item in rec_ids: hits 1 recall_sum len(set(positive_items) set(rec_ids)) / len(positive_items) candidates_sum len(rec_ids) precision hits / total recall recall_sum / total coverage len(set( item for user_id in matrix.index for item, _ in recommend(user_id, matrix, user_sim_df) )) / matrix.shape[1] return precision, recall, coverage p, r, c evaluate(matrix, user_sim_df) print(fPrecision{10}: {p:.4f}) print(fRecall: {r:.4f}) print(fCoverage: {c:.4f})评估代码里有一点要注意hidden_matrix每次循环都拷贝全量矩阵数据量大时很慢。你可以改成只修改当前行的拷贝或者用随机抽样评估 1000 个用户。Precision衡量隐藏的录取记录是否被召回Coverage衡量推荐系统推荐了多少不同的项目。在高分考生较多的省份覆盖率通常只有 20% 左右这是正常的因为高分考生集中在头部院校。4. 从算法到系统落地前后端模块、接口联调与冷启动处置推荐引擎跑通只是第一步真正让毕设答辩平均分 96 分的是把它包装成一个完整可演示的系统。资源里的前端样式文件恰好暴露了系统的模块结构我们可以按这些模块倒推实现思路。4.1 前端模块与页面设计从资源文件名来看系统包含这几个核心页面TheLogin登录页、TheRegister注册页、TheRecommend推荐页、TheSchool院校详情页、TheFeedbackDetail反馈详情页、TheHomeIndex首页。后端的TheHome和Chart大概率是数据概览和可视化图表。从复用角度这些页面的路由可以这样组织路由页面模块对应 CSS 资源/login考生登录TheLogin-*.css/register账号注册选科信息录入TheRegister-*.css/recommend推荐结果列表含志愿梯度标注TheRecommend-*.css/school/:id院校历年分数线、招生计划TheSchool-*.css/feedback/:id用户对推荐结果的反馈TheFeedbackDetail-*.css前端展示推荐结果时我建议把相似考生人数直接展示出来例如与你位次相近的 26 名考生中有 19 人选择了该校录取率 73%。这种解释性信息比单纯一个推荐分数更有说服力。4.2 推荐接口设计与联调前后端分离是常见做法后端使用 Flask/FastAPI 暴露推荐接口。一个简洁的推荐接口如下// POST /api/recommend { user_id: 20240001, top_k: 20, limit: 10 }// 200 OK { code: 0, message: ok, data: [ { school: 杭州电子科技大学, major: 计算机科学与技术, prediction_score: 0.83, similar_user_count: 26, admit_rate: 0.73, level: 稳 } ] }这里的prediction_score就是第 3 章的加权归一化结果level字段根据考生位次与该专业往年录取位次的差值计算。前端拿到数据后用不同颜色标签区分冲、稳、保这部分颜色样式在TheRecommend-*.css中已有定义。4.3 冷启动与稀疏数据下的降级策略新注册考生没有任何历史填报记录协同过滤无法为他匹配邻居所以必须设计降级策略。我的做法是冷启动用户直接走规则推荐用位次范围过滤 院校热门度排序先给出一批基础推荐等考生完成一次反馈或模拟填报后再切换成协同过滤。降级条件写在后端服务里比如当该用户可用的相似邻居少于 3 个时自动走保底策略。另一种冷启动发生在新增院校或专业时这类项目没有任何交互记录。常见做法是给新项目一个较低的默认评分或者将其嵌入到同校其他项目的推荐理由中。注意不要把新项目完全丢掉否则系统推荐列表会长期固化在旧数据上。4.4 结果展示与交互优化推荐列表要支持二次筛选。考生通常有地域、学费、办学性质等硬性偏好可以在前端加筛选器在后端接口接收province、fee_max、school_type参数。这类筛选要在推荐结果上做而不是在协同过滤计算前做否则会改变邻居集合结构。另外反馈机制很重要考生对推荐结果点更感兴趣或不感兴趣后系统可以将该生行为回填到矩阵中实现增量更新。增量更新的代码控制在 100 行以内核心是更新矩阵中对应的一行再增量更新相似度矩阵。5. 用位次波动区间修正协同过滤结果避免高分低报到了最后一章讲一个让推荐结果更贴近真实填报逻辑的实用技巧。纯协同过滤只看历史行为会遗漏一个重要约束某个专业去年录取位次是 8000今年改成大类招生后涨到 3000协同过滤不知道这个变化仍然会把该专业推荐给位次 10000 的考生。结果就是考生拿到一堆看起来合理但实际滑档的志愿。我的做法是在协同过滤结果返回前叠加一个位次波动区间校验。具体来说每个院校专业都会计算近三年的录取位次中位数并计算波动半径。如果考生位次低于该专业位次下界中位数加上波动半径就从推荐列表中剔除除非该专业在相似考生群体中出现了极高的支持度。def filter_by_rank(rec_items, student_rank, rank_stats, fallback_rate1.2): rec_items: [(item_id, score), ...] student_rank: 当前考生位次 rank_stats: dict, item_id - {median_rank: 8000, std_rank: 1200} filtered [] for item_id, score in rec_items: stats rank_stats.get(item_id) if not stats: continue # 无历史数据跳过 lower_bound stats[median_rank] fallback_rate * stats[std_rank] # 位次数字越小越靠前所以考生位次 下界表示风险较大 if student_rank lower_bound: # 惩罚分数降权但不直接剔除保留冲刺可能 penalty max(0.5, (lower_bound / student_rank)) filtered.append((item_id, score * penalty)) else: filtered.append((item_id, score)) filtered.sort(keylambda x: x[1], reverseTrue) return filtered这段代码里fallback_rate控制波动容忍度设置为 1.2 表示允许考生位次比中位数低 20% 的标准差范围相当于在冲一冲和保底之间取中间态。如果改成 1.5推荐列表会偏向冲刺校改成 1.0则几乎是保底校。我在实际实验中发现1.2的推荐结果在考生反馈中满意度最高因为列表里既有能稳上的学校又有可以向上够的学校。这个方法在数据量超过 5000 条、且包含近三年录取计划时效果最稳定。如果你的数据集只有一年的录取记录没有足够样本计算位次波动那就应该把std_rank设置为固定值比如位次的 10%而不是从统计中计算。另一种变体是把位次波动区间和协同过滤分数做加权融合直接替换推荐分数。逐项替换的效果更直接保留交叉验证时更容易解释。所谓梯度合理体现在最终列表里恰好有 2 到 3 个冲刺、4 个稳妥、2 个保底。我通常会在返回列表中按照预测分数排序后增加一个梯度检查函数如果某个梯度缺失就把相邻梯度的项目补位进来。这个方法配合位次过滤能让推荐界面永远展示出完整的志愿填报梯度而不是一股脑全是同一档次的学校。本文还有配套的精品资源点击获取
返回列表