ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

协同过滤推荐系统在高考志愿填报中的应用与实践

协同过滤推荐系统在高考志愿填报中的应用与实践 简介基于协同过滤算法的高考志愿推荐系统面向计算机相关专业学生与毕业设计开发者完整覆盖从用户登录、志愿推荐到院校信息管理的全流程适合用于毕设演示、课设作业或推荐算法入门学习。压缩包共289个文件以Java源码与前端资源为主包含80个java程序文件、77个js脚本、86个xml配置及20个css样式另有png图标与项目配置文件整体约25.52MB代码结构清晰、可直接导入运行。已有128人学习下载作者承诺代码均通过测试答辩评审平均分达96分并支持远程教学指导。通过该项目可掌握协同过滤推荐的核心实现思路包括相似度计算、推荐列表生成等同时可借鉴前后端分离的工程组织方式便于在此基础上扩展功能或二次开发。1. 基于协同过滤的推荐系统如何落到高考志愿填报上高考志愿推荐和电商推荐最大的区别在于决策成本。用户不会因为推荐错了再买一次所以这个场景对可解释性和容错率的要求都远高于普通推荐系统。协同过滤在这个场景里恰恰有先天优势它不需要维护庞大的专业题库也不需要人工标注院校特征只需要用好历届考生的“行为数据”——分数、位次、填报结果就能把“和你情况相似的人最终去了哪里”这件事算清楚。适合读这篇的人不只是做毕设的学生。如果你在做的推荐系统面向教育、招聘、婚恋这类低频高决策场景协同过滤的思路、数据清洗方法、冷启动处理手段同样可以直接迁移。本文会从数据建模开始给出可运行的源代码框架再逐步加上特征加权和混合策略最后处理最难办的“没有历史行为的新用户”问题。2. 推荐系统数据建模把高考志愿问题拆成评分矩阵2.1 用户-项目矩阵的三个元素怎么定义协同过滤的输入永远是一张用户对项目的评分表。在高考志愿场景里“用户”是考生“项目”是院校或院校专业组合“评分”则不是考生自己打的分数而是根据录取结果反推出来的匹配度。常见做法是把录取结果映射成一个 0 到 1 的匹配分压线录取记 1.0第一志愿录取记 0.95第二志愿记 0.9调剂录取记 0.7滑档则记为 0。还有一种做法是用录取分数线与考生分数的差值来构造连续分值差值为 0 时最高差值超过 20 分后衰减到 0.1 以下。两种映射方式在代码里实现都很简单但后者保留了更多信息量后续算相似度时区分度更高。CREATE TABLE user_item_score ( user_id INT COMMENT 脱敏后的考生ID, item_id INT COMMENT 院校专业组合ID, score DECIMAL(4,3) COMMENT 匹配度0-1区间, score_type TINYINT COMMENT 1等第映射, 2差值映射, year INT COMMENT 考生所在年份, PRIMARY KEY (user_id, item_id, year) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这张表就是全部协同过滤算法的输入。score_type 字段很重要因为不同年份的分数尺度会浮动按年份分组计算相似度时要用同一个 score_type 做口径统一屏蔽掉不同省份、不同年份的绝对分差。2.2 矩阵稀疏度和用户群体划分高考志愿推荐的数据矩阵天然极其稀疏。一个考生最多填报几十个志愿但全国招生单位有两千多所矩阵稀疏度通常在 99% 以上。这种矩阵直接用皮尔逊相关系数算相似度结果往往会被一两个共同评分项主导不稳定。解决思路是先做群体划分再算相似度。按“省份 科类 分数段”把考生切成不同群体同一个群体内的考生才有可比性。分数段以 20 分为一档比较合适太细了样本量不足太粗了区分度丢失。实践中我会在 SQL 里先做一次 GROUP BY 统计过滤掉交互记录数低于 5 的用户再把剩余数据导入算法层。import pandas as pd import numpy as np def build_pivot_table(ratings: pd.DataFrame, min_interactions: int 5): # 过滤交互记录过少的用户减少随机噪声 user_count ratings.groupby(user_id)[item_id].count() valid_users user_count[user_count min_interactions].index ratings ratings[ratings[user_id].isin(valid_users)] # 生成用户-项目矩阵缺失值填0而不是NaN # 填0意味着“没有行为”填NaN则完全排除该项两者后续处理逻辑不同 pivot ratings.pivot_table( indexuser_id, columnsitem_id, valuesscore, fill_value0 ) print(f矩阵形状: {pivot.shape}, 稀疏度: {(pivot 0).sum().sum() / pivot.size:.2%}) return pivot这个函数做了两个关键决定。第一个是 min_interactions 阈值我用 5 而不是常见的 10是因为高考志愿数据里一个考生能填的选项本来就有限阈值太高会把有价值的样本都丢掉。第二个是缺失值填 0这意味着把“没填这个志愿”当作“不匹配”来处理。这在计算余弦相似度时是合理近似但如果改用皮尔逊相关系数就需要先做行均值中心化再填 0否则相关系数会被零值严重拉低。2.3 相似度计算为什么选余弦相似度在高考志愿这个场景余弦相似度比皮尔逊相关系数更合适。余弦相似度只看方向不看绝对数值适合处理评分标准不统一的场景——有的省份考生偏好留在本省他们的分数向量普遍偏高但这不影响方向上的相似性。皮尔逊相关系数会先减去均值这时如果一个考生的所有匹配度都在 0.9 附近波动他的“均值”已经把分差压缩掉了区分度反而变差。def cosine_similarity_matrix(pivot: pd.DataFrame): # 行归一化后做点积矩阵运算一次算出全部用户间相似度 norm pivot / np.sqrt(np.square(pivot).sum(axis1)).values.reshape(-1, 1) # 处理除零全零行会产出NaN替换为0表示与任何用户都不相似 norm norm.replace([np.inf, -np.inf, np.nan], 0) sim_matrix norm.dot(norm.T) np.fill_diagonal(sim_matrix.values, 0) # 自身相似度置零避免推荐自己 return sim_matrix这里有个性能细节值得注意。算用户间相似度时矩阵大小是用户数×用户数。当用户量在十万级时这个矩阵要占 74GB 内存必须在算完后立刻转为稀疏格式存储不能直接放内存里做后续计算。3. 用户协同过滤代码骨架和文档说明里的隐藏配置3.1 最近邻筛选和评分预测的三大参数有了相似度矩阵推荐逻辑分为两步找最近邻然后聚合最近邻的评分来预测当前用户对未接触项目的评分。这两步里有三个参数直接影响结果质量也是答辩和评审最喜欢追问的地方。def recommend_for_user(user_id: int, sim_matrix: pd.DataFrame, pivot: pd.DataFrame, top_k: int 20): # 取当前用户的相似度向量 sim_scores sim_matrix.loc[user_id] # 筛选出相似度大于0的邻居这一步其实就是隐式过滤 neighbor_candidates sim_scores[sim_scores 0].sort_values(ascendingFalse) # 参数1: 最少邻居数大家在文档里写数据不足时用物品相似度兜底 if len(neighbor_candidates) 5: return fallback_recommend_by_popularity(user_id, pivot) # 参数2: top_k个邻居这里取20而不是常见的10 neighbors neighbor_candidates.head(top_k) neighbor_ratings pivot.loc[neighbors.index] sim_weights neighbors.values.reshape(-1, 1) # 参数3: 加权平均时是否要按top_k归一化权重 # True会放大头部邻居的影响False保留原始权重差异 if normalize_weights: sim_weights sim_weights / sim_weights.sum() # 只预测有邻居评分过、但当前用户没有评分的项目 unseen_mask pivot.loc[user_id] 0 unseen_items pivot.columns[unseen_mask] scores (neighbor_ratings[unseen_items].values * sim_weights).sum(axis0) scores scores / sim_weights.sum() top_items pd.Series(scores, indexunseen_items).sort_values(ascendingFalse).head(10) return top_itemstop_k 取 20 而不是 10是因为高考志愿的评分向量密度极低单个邻居提供的有效信息少必须用更多邻居来稳定方差。但 top_k 超过 30 后准确率会显著下降因为远邻的相似度已经趋近于零大量噪声涌入。我在实验里对比过20 到 25 这个区间最稳。此外邻居筛选时把相似度阈值设为 0 是刻意为之因为高考场景里不同群体间相似度天然低但也存在跨群体的有效借鉴阈值设太高会把跨省填报经验完全屏蔽掉。3.2 热门兜底策略新用户没有邻居时怎么办缺乏行为数据的用户直接抛给协同过滤一定拿不到结果。我在代码里留了 fallback_recommend_by_popularity 这个函数它的策略不是简单按报名人数排序而是按热度调整后的综合得分来排。def fallback_recommend_by_popularity(user_id: int, pivot: pd.DataFrame, top_n: int 20): # 热门度指标 该院校被高分考生填报的比例 * 录取位次区间宽度 # 位次区间宽度反映了一个院校的包容度太窄的院校就算热门也不适合推荐 item_popularity pivot.astype(bool).sum(axis0) item_mean_score pivot.mean(axis0) # 取考试院公布的平行志愿填报指导数据中的位次宽度这里简化模拟 item_position_span simulate_position_span(pivot.columns) # 综合热度分 0.5*填报人数占比 0.3*均分 0.2*位次宽度归一化 hot_score ( 0.5 * item_popularity / item_popularity.max() 0.3 * item_mean_score / item_mean_score.max() 0.2 * item_position_span / item_position_span.max() ) return hot_score.sort_values(ascendingFalse).head(top_n)热门兜底策略的权重分配是经验和数据共同作用的结果。填报人数占比权重最高代表的是“用脚投票”的结果均分权重次之代表院校对高分考生的吸引力位次宽度权重最低但绝不可少因为只按绝对热度推荐会把受偶发因素影响一年爆红的院校推给所有人位次宽度能识别出这类风险。3.3 文档说明里必须写清楚的三张设计表源代码配套的文档说明里价值最高的不是你写了多少段文字和图而是三张表数据字典表、参数配置表、测试结果对照表。数据字典表明确定义每个字段的取值约束参数配置表记录每一次调参的版本、效果和决策理由测试结果对照表按年份切分训练集和验证集记录不同算法配置下的命中率、覆盖率、多样性。文档里有这三张表评审人十分钟就能看清你的工作量和思考深度比大段原理复述有用得多。4. 混合推荐策略高考场景必须绕过单算法的三个坑4.1 协同过滤的天然盲区热门院校过度曝光纯用户协同过滤在高考志愿场景会显著偏向热门院校。原因在于热门院校的填报记录多被选为邻居的概率高在加权聚合时天然占优。这带来的后果是推荐结果集中在几十所知名高校对中等分数段的考生几乎没有参考价值。解决办法是引入物品侧信息做混合推荐物品协同过滤和内容特征加权并行计算最终得分用线性加权合并。我通常把用户协同过滤的权重设为 0.6物品协同过滤权重设为 0.3院校层级特征权重设为 0.1。def hybrid_recommend(user_id: int, user_cf_scores: pd.Series, item_cf_scores: pd.Series, school_level: pd.Series): # 三路分数先各自做min-max归一化消除量纲差异 def normalize(s: pd.Series): if s.std() 0: return s * 0 return (s - s.min()) / (s.max() - s.min()) user_cf_score normalize(user_cf_scores) item_cf_score normalize(item_cf_scores) level_score normalize(school_level) # 混合权重不宜写成固定值要按用户已有行为量动态调整 history_cnt get_user_history_count(user_id) if history_cnt 10: alpha, beta, gamma 0.4, 0.4, 0.2 # 行为少时让物品CF和层级特征多出力 elif history_cnt 30: alpha, beta, gamma 0.5, 0.35, 0.15 else: alpha, beta, gamma 0.6, 0.3, 0.1 # 行为充足时信任用户CF final_score alpha * user_cf_score beta * item_cf_score gamma * level_score return final_score.sort_values(ascendingFalse).head(20)权重动态调整的思路是用户行为越丰富用户协同过滤的结果可信度越高权重就往上抬行为少时让物品协同过滤和院校层级特征来补位。这里的阈值 10 和 30 是我的实测经验值不同数据集上可以做一点微调但幅度不建议太大因为这两个数背后对应的是“冷启动”“一般用户”“活跃用户”三个阶段的分界语义。4.2 位次换算的坑分数会变但位次不会骗人在推荐系统里直接使用原始分数做相似度特征是一个常见错误。每年的试卷难度不同同样的 600 分在不同年份含金量完全不同。位次是比分数稳定得多的信号。我见过不少团队把分数归一化后放入特征结果模型预测准确率虚高一上线就崩。处理方式是把分数转成同分位次比例——该考生位次除以当年该省份考生总数得到一个百分位。这个值在跨年份比较时才有意义。4.3 与基于内容推荐的本质差异基于内容的推荐在这个场景里按院校的学科评估、城市等级、学费区间来打标签给考生推“和你收藏过的院校相似”的选项。它的优点是结果容易解释——“因为你关注了软件工程强的学校所以推荐这些计算机学科评估 B 以上的院校”——但它有个致命问题它永远只推荐你原来就会关注的东西不会带你去看完全意外的领域。协同过滤的独特价值恰恰在于它能看到“和你相似的人还做了什么”呈现出来的结果往往超出考生自己的检索视野比如一个想学计算机的考生被推荐了信息管理专业所在院校这个推荐靠内容特征永远挖不出来。在高考志愿这个低决策频次但高决策成本的场景这种跳出舒适区的推荐价值极大。5. 排错表和验证方法怎么证明这个推荐系统真的可信5.1 训练验证切分和命中率计算离线评测采用时间切分而不是随机切分。假设数据跨越 2019 到 2023 年选前 4 年做训练集最后一年的填报记录做验证集。在验证阶段把每个用户按分数从高到低排列取前 N 个志愿作为“可命中”范围判断推荐结果是否落在其中。这个评测逻辑模拟的是平行志愿投档的真实流程——考生的志愿表是从前往后依次尝试的。def evaluate_hit_rate(recommendations: pd.DataFrame, ground_truth: pd.DataFrame, top_n: int 20, hit_threshold: int 6): # recommendations: 每个用户推荐的前N个院校 # ground_truth: 每个用户实际填报的志愿表按志愿顺序排序 merged recommendations.merge( ground_truth, on[user_id, item_id], howouter, indicatorTrue ) merged merged.dropna(subset[item_id]) # 只统计“考生真实填了前6个志愿”里的命中 merged merged[merged[志愿顺序] hit_threshold] hit_rate merged[_merge].eq(both).mean() coverage recommendations[item_id].nunique() / ground_truth[item_id].nunique() print(fTop-{top_n} 命中率: {hit_rate:.2%}) print(f覆盖率: {coverage:.2%}) return hit_rate, coveragehit_threshold 取 6 是模拟大多数省份的平行志愿数量上限。把命中窗口限制在前 6 个志愿考察的是推荐结果能否进入考生真正会优先选择的区间而不是把每单都命中到第 45 个志愿。5.2 参数失效的排查顺序推荐结果完全不对时我一般按这个顺序排查第一步确认评分矩阵没有无效值特别是填 0 和填 NaN 的语义是否搞混了第二步检查相似度矩阵对角线是否正确地置零了这个低级错误会导致系统无限推荐用户自己填过的志愿第三步检查邻居阈值是否设得过高导致大量用户走了兜底策略推荐结果永远是热门榜单第四步确认混合权重的动态调节逻辑没把用户协同过滤权重压到接近零否则系统退化成纯内容推荐无法产生惊喜。这套排查表我在文档里做成清单形式每条对应一个可执行的 SQL 检查语句或 Python 断言评审时可以直接跑。5.3 缺失值填 0 的反直觉特性和实战验证工具当用户没有给某院校评分时填 0一旦用户有了第一个评分它所在行会被零值包围归一化后该行非零项会被放大。这属于协同过滤里少有人提的细节但影响很大。我通常会额外做一个校验把评分矩阵转置前 3 个主成分可视化观察不同省份的用户是否能聚成簇。如果可视化结果显示聚类完全混乱说明数据清洗有严重问题这时候再精调算法参数是白费功夫。可视化这一步是选型和调参前必做的验证手段也是招经验丰富的工程师时考察系统设计能力的问题。实际部署时还有一个小技巧把相似度矩阵预计算好存入键值对存储推荐时的实时计算只做最近邻读取和加权聚合整个响应时间控制在 100 毫秒内。预计算任务放在凌晨低峰期跑一次白天查询全走缓存。这套架构不需要分布式计算框架单机内存加个缓存层就够支撑十万级用户和千级院校的规模。本文还有配套的精品资源点击获取
返回列表