大学生选课系统中的协同过滤推荐实战
2026/9/11 18:44:57 网站建设 项目流程

简介:本资源是一套完整的Java毕业设计项目——基于协同过滤算法的大学生选修选课系统,面向计算机相关专业本科生及初阶开发者,解决传统选课系统个性化推荐能力弱、管理模块分散、前后端技术栈陈旧等实际问题。项目采用Spring Boot + Vue前后端分离架构,MySQL持久化数据,涵盖课程、排课、选课、成绩、教师、学生、公告及字典管理等九大核心模块,并集成协同过滤算法实现课程智能推荐。压缩包共453个文件,含121个Java后端业务与实体类、63个Vue组件与页面、161个SVG图标资源、25个JPG/PNG素材及SQL建表脚本等,整体18.2MB,结构清晰、注释完整,开箱即用。目前已有197人学习下载,提供可直接运行的bat启动脚本、详细数据库表结构文档、README说明及配套论文框架,助读者快速理解系统设计逻辑、复现推荐算法流程并完成毕设答辩准备。

1. 为什么大学生选课系统需要协同过滤?不是“热门课程推荐”就能解决的真问题

某高校教务系统上线后,学生反馈:“推荐的都是《大学英语》《思政课》这种必修课,我早修完了”;而教务老师发现,冷门但高价值的专业拓展课——比如《数字人文导论》《嵌入式系统实践》——开班率常年低于30%,大量优质教学资源闲置。问题不在数据缺失,而在推荐逻辑失焦:传统按点击量或开课次数排序的“热门榜”,无法识别“学过《Python程序设计》且绩点3.7+的学生,大概率会选《机器学习基础》而非《网页设计》”这类隐性关联。协同过滤算法恰恰填补这一缺口——它不依赖课程标签或内容描述,只从真实选课行为矩阵中挖掘用户与用户、课程与课程之间的相似性模式。本项目用SpringBoot+Vue+MySQL落地该算法,不是为堆砌技术名词,而是让推荐结果可解释(如“和你同学院、同年级、已选3门编程课的12位同学,有9人选择了这门课”)、可追溯(每条推荐背后有原始行为路径)、可调控(支持实时屏蔽某类课程或提升某学科权重)。适合正在做Java全栈毕设、需兼顾算法原理理解与工程落地能力的同学,也适合作为中小规模教务平台推荐模块的轻量级参考实现。

2. 协同过滤在选课场景下的算法选型与数据建模

2.1 为什么不用基于内容的推荐?——选课行为的特殊性决定算法边界

选课行为天然具备强稀疏性与弱语义性:一个学生一学期通常只选8–12门课,而全校课程库可能超2000门,用户-课程交互矩阵稀疏度常达99.5%以上;同时,课程名称如《数据库原理》《数据库系统概论》《NoSQL与分布式数据库》虽语义相近,但实际教学内容、难度、教师风格差异巨大,仅靠课程标题分词或TF-IDF向量化极易误判。此时,基于内容的推荐(Content-Based)会因特征提取失真导致推荐偏差。而协同过滤(Collaborative Filtering)直接利用“行为即信号”的原则——学生A和B都选了《数据结构》《操作系统》《计算机网络》,则他们对《分布式系统》的偏好概率显著高于随机用户。这种“物以类聚,人以群分”的逻辑,在选课场景下鲁棒性更强。项目最终采用基于用户的协同过滤(User-Based CF)为主、基于物品的协同过滤(Item-Based CF)为辅的混合策略:前者便于生成“和你相似的同学还选了…”这类可解释推荐语;后者用于冷启动课程(新开课无历史选课记录时,通过相似课程的选课者反推潜在用户)。

2.2 用户-课程交互矩阵构建:从原始选课日志到可计算的稀疏矩阵

MySQL中需建立三张核心表支撑协同过滤计算:student(学生主表)、course(课程主表)、selection_record(选课记录表)。关键设计在于selection_record的结构:

CREATE TABLE selection_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL COMMENT '学生ID', course_id BIGINT NOT NULL COMMENT '课程ID', semester VARCHAR(10) NOT NULL COMMENT '学期标识,如2023-2', grade TINYINT COMMENT '成绩,NULL表示未出分', created_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_course_semester (student_id, course_id, semester) );

提示:UNIQUE KEY uk_student_course_semester强制约束同一学生同一学期不可重复选同一门课,避免数据污染。semester字段必须存在——不同学期的选课行为不能简单合并(如大一《高等数学》和大四《数学建模》反映的能力维度不同),这是保证相似度计算时效性的前提。

将选课记录转化为协同过滤所需的用户-课程评分矩阵时,不直接使用成绩(grade)作为评分。原因有二:一是大量课程成绩尚未录入(选课后数月才出分),导致矩阵严重缺失;二是成绩受教师评分尺度影响大(严师的85分可能等于宽师的95分)。项目采用隐式反馈建模:以semester为时间窗口,对每个学生在当学期选课行为赋予统一权重1,未选课程视为0。例如,学生A在2023-2学期选了课程C1、C3、C7,则其行向量为[1,0,1,0,0,0,1,…]。此设计使矩阵构建可实时进行,且规避了显式评分的噪声。

2.3 相似度计算:余弦相似度的工程化实现与性能优化

用户相似度计算是协同过滤的性能瓶颈。若对全部学生两两计算余弦相似度,时间复杂度为O(N²×M),N为学生数(假设5000),M为课程数(2000),单次全量计算耗时将超小时级。项目采用局部敏感哈希(LSH)预筛选+精确余弦计算的两级策略:

  1. LSH桶分组:对每个学生的课程向量进行MinHash降维,生成128位签名。使用20个哈希函数将签名分桶,确保相似用户大概率落入同一桶;
  2. 桶内精确计算:仅对同一桶内的用户对计算余弦相似度,将需计算的用户对数量降低90%以上。

SpringBoot中核心计算逻辑(UserSimilarityCalculator.java):

@Service public class UserSimilarityCalculator { @Autowired private SelectionRecordMapper selectionRecordMapper; // 获取指定学生在某学期的选课向量(稀疏格式) public Map<Long, Integer> getUserCourseVector(Long studentId, String semester) { List<Long> courseIds = selectionRecordMapper.selectCourseIdsByStudentAndSemester(studentId, semester); Map<Long, Integer> vector = new HashMap<>(); for (Long cid : courseIds) { vector.put(cid, 1); // 隐式反馈,统一赋值1 } return vector; } // 计算两用户向量的余弦相似度(使用稀疏向量交集优化) public double cosineSimilarity(Map<Long, Integer> vecA, Map<Long, Integer> vecB) { long dotProduct = 0L; long normA = 0L, normB = 0L; // 只遍历vecA的key,检查是否在vecB中存在 for (Map.Entry<Long, Integer> entry : vecA.entrySet()) { Long courseId = entry.getKey(); if (vecB.containsKey(courseId)) { dotProduct += entry.getValue() * vecB.get(courseId); // 此处均为1,等价于交集课程数 } normA += entry.getValue() * entry.getValue(); // L2范数平方 } for (Integer val : vecB.values()) { normB += val * val; } if (normA == 0 || normB == 0) return 0.0; return (double) dotProduct / Math.sqrt(normA * normB); } }

注意:cosineSimilarity方法中dotProduct的计算逻辑本质是求两用户共同选课数,normAnormB分别是各自选课总数的平方根。当所有评分为1时,余弦相似度公式简化为|A∩B| / √(|A|×|B|),物理意义清晰——共同选课数占各自选课规模几何平均数的比例。此简化大幅降低浮点运算量,且结果与标准余弦一致。

3. SpringBoot后端集成协同过滤推荐引擎

3.1 推荐服务分层设计:Controller → Service → Algorithm → Cache

推荐功能被封装为独立模块recommendation-service,遵循清晰分层:

  • RecommendationController:接收/api/recommend?studentId=1001&semester=2023-2请求,校验参数合法性;
  • RecommendationService:协调业务逻辑,决定调用User-Based还是Item-Based策略;
  • CollaborativeFilteringAlgorithm:核心算法类,包含相似用户查找、邻居加权评分预测等方法;
  • RecommendationCache:基于Redis缓存最近24小时的推荐结果,Key为rec:${studentId}:${semester},TTL设为3600秒。

关键配置在application.yml中定义算法参数:

recommendation: # 协同过滤参数 user-based: neighbor-count: 20 # 相似用户邻居数,取Top20最相似用户 min-similarity: 0.3 # 相似度阈值,低于此值的用户不参与加权 item-based: neighbor-count: 10 # 相似课程邻居数 # 缓存策略 cache: enable: true expire-seconds: 3600

3.2 基于用户的推荐生成:从相似用户到目标课程排序

CollaborativeFilteringAlgorithm.generateUserBasedRecommendations()方法执行以下步骤:

  1. 获取目标学生当学期选课向量:调用getUserCourseVector(studentId, semester)
  2. 查找K个最相似用户:遍历学生库(排除自身),调用cosineSimilarity()计算相似度,用优先队列维护Top-K;
  3. 预测未选课程评分:对每个未选课程c,计算加权平均分:
    score(c) = Σ(similarity(u, target) × r(u,c)) / Σ|similarity(u, target)|
    其中r(u,c)为用户u对课程c的隐式评分(1或0),similarity(u, target)为用户u与目标学生的余弦相似度;
  4. 过滤与排序:剔除目标学生已选课程、已结业课程(status='completed')、及院系权限外课程,按score(c)降序排列。

Java实现片段(带关键注释):

public List<RecommendationItem> generateUserBasedRecommendations(Long studentId, String semester) { Map<Long, Integer> targetVector = getUserCourseVector(studentId, semester); Set<Long> alreadySelected = targetVector.keySet(); // 已选课程ID集合 // 步骤2:查找Top-K相似用户 PriorityQueue<UserSimilarity> similarUsers = findTopSimilarUsers(studentId, semester, recommendationProperties.getUserBased().getNeighborCount()); // 步骤3:构建课程评分预测映射 Map<Long, Double> predictedScores = new HashMap<>(); Map<Long, Double> similaritySum = new HashMap<>(); // 分母累加器 while (!similarUsers.isEmpty()) { UserSimilarity us = similarUsers.poll(); Map<Long, Integer> neighborVector = getUserCourseVector(us.getUserId(), semester); // 遍历邻居选过的课程,且目标学生未选 for (Map.Entry<Long, Integer> entry : neighborVector.entrySet()) { Long courseId = entry.getKey(); if (alreadySelected.contains(courseId)) continue; // 跳过已选 double similarity = us.getSimilarity(); double contribution = similarity * entry.getValue(); // 权重×评分 predictedScores.merge(courseId, contribution, Double::sum); similaritySum.merge(courseId, similarity, Double::sum); } } // 步骤4:计算最终分数并排序 return predictedScores.entrySet().stream() .filter(entry -> similaritySum.containsKey(entry.getKey())) .map(entry -> { double score = entry.getValue() / similaritySum.get(entry.getKey()); return new RecommendationItem(entry.getKey(), score); }) .filter(item -> item.getScore() >= recommendationProperties.getUserBased().getMinSimilarity()) .sorted((a, b) -> Double.compare(b.getScore(), a.getScore())) .limit(10) // 返回Top10推荐 .collect(Collectors.toList()); }

提示:predictedScores.merge()similaritySum.merge()使用Java 8的merge方法实现原子累加,避免显式锁。filter链中两次调用similaritySum.containsKey()确保分母非零,防止除零异常。最终limit(10)硬性控制返回数量,符合前端卡片式展示需求。

3.3 MySQL索引优化:让千万级选课记录查询毫秒级响应

协同过滤频繁查询selection_record表,若无索引,单次selectCourseIdsByStudentAndSemester可能耗时数百毫秒。必须建立复合索引覆盖查询条件:

-- 核心查询:按学生ID+学期查课程ID CREATE INDEX idx_student_semester ON selection_record (student_id, semester); -- 辅助查询:按课程ID+学期查选课学生(用于Item-Based CF) CREATE INDEX idx_course_semester ON selection_record (course_id, semester); -- 覆盖索引:避免回表,直接从索引获取course_id CREATE INDEX idx_student_semester_cover ON selection_record (student_id, semester, course_id);

验证索引效果的EXPLAIN命令:

EXPLAIN SELECT course_id FROM selection_record WHERE student_id = 1001 AND semester = '2023-2';

理想执行计划应显示type=refkey=idx_student_semester_coverExtra=Using index(表示索引覆盖,无需访问数据行)。实测在500万选课记录下,该查询稳定在3–5ms内完成。

4. Vue前端推荐结果渲染与交互增强

4.1 推荐卡片组件:动态展示相似依据与课程元数据

Vue组件RecommendationCard.vue接收后端返回的RecommendationItem对象(含courseIdscorereason字段),渲染时不仅显示课程名称,更突出推荐逻辑:

<template> <div class="recommend-card" v-for="item in recommendations" :key="item.courseId"> <div class="card-header"> <h3>{{ item.courseName }}</h3> <span class="score-badge">推荐指数 {{ (item.score * 100).toFixed(0) }}%</span> </div> <div class="card-body"> <p class="reason-text">→ {{ item.reason }}</p> <!-- 如"与你相似的12位同学选择了此课" --> <div class="course-meta"> <span>学分:{{ item.credit }}</span> <span>授课教师:{{ item.teacher }}</span> <span>剩余名额:{{ item.availableSeats }}</span> </div> </div> <div class="card-footer"> <button @click="handleSelect(item.courseId)" :disabled="isAlreadySelected(item.courseId)" class="select-btn"> {{ isAlreadySelected(item.courseId) ? '已选择' : '立即选课' }} </button> </div> </div> </template>

注意:item.reason由后端在生成推荐时注入,非前端拼接。例如User-Based CF生成的reason为相似用户中${commonCount}人选择了该课程,Item-Based CF则为与你已选的${sourceCourseName}课程相似度达${similarity}%。这种服务端生成理由的方式,保证了推荐可解释性与业务逻辑一致性。

4.2 实时推荐触发:Vue路由守卫与防抖加载策略

推荐请求不应在页面加载时盲目发起,而应结合用户行为智能触发。在router/index.js中配置路由守卫:

// 当进入选课页时,若学生ID和学期已知,则触发推荐 router.beforeEach((to, from, next) => { if (to.name === 'CourseSelection') { const studentId = store.state.user.studentId; const currentSemester = getCurrentSemester(); // 工具函数,根据当前日期推算学期 if (studentId && currentSemester) { // 使用lodash防抖,避免快速切换学期Tab时多次请求 debouncedFetchRecommendations(studentId, currentSemester); } } next(); }); // 防抖函数定义 const debouncedFetchRecommendations = debounce((sid, sem) => { store.dispatch('fetchRecommendations', { studentId: sid, semester: sem }); }, 300);

同时,在store/modules/recommendation.js中管理推荐状态,避免重复请求:

const state = { recommendations: [], loading: false, lastRequested: null // 记录最后请求参数,用于去重 }; const actions = { async fetchRecommendations({ commit, state }, { studentId, semester }) { // 参数去重:相同studentId+semester不再重复请求 const key = `${studentId}-${semester}`; if (state.lastRequested === key) return; commit('SET_LOADING', true); try { const res = await api.getRecommendations(studentId, semester); commit('SET_RECOMMENDATIONS', res.data); commit('SET_LAST_REQUESTED', key); } finally { commit('SET_LOADING', false); } } };

4.3 推荐效果可视化:ECharts绘制相似用户课程重合热力图

为帮助学生理解推荐依据,增加“相似用户课程分布”可视化模块。使用ECharts绘制热力图,横轴为课程类别(如“计算机类”“人文类”),纵轴为相似用户排名(Top1–Top5),颜色深浅表示该用户与目标学生在该类别下的课程重合度。

前端数据请求与图表渲染逻辑:

// 获取相似用户课程分布数据 async loadSimilarUserHeatmap() { const res = await this.$http.get(`/api/recommend/heatmap`, { params: { studentId: this.studentId, semester: this.semester } }); const option = { tooltip: { trigger: 'axis' }, grid: { left: '5%', right: '5%' }, xAxis: { type: 'category', data: res.data.categories // ['计算机类','数学类','外语类',...] }, yAxis: { type: 'category', data: res.data.users.map((u, i) => `相似用户#${i+1}`) }, visualMap: { min: 0, max: 10, // 重合课程数范围 calculable: true }, series: [{ name: '课程重合数', type: 'heatmap', data: res.data.heatmapData, // [[0,0,3],[0,1,5],...] 格式 emphasis: { itemStyle: { borderColor: '#333', borderWidth: 1 } } }] }; this.$refs.heatmapChart.setOption(option); }

后端HeatmapController返回的数据结构示例:

{ "categories": ["计算机类", "数学类", "人文类"], "users": [{"id": 2001, "name": "张三"}, {"id": 2002, "name": "李四"}], "heatmapData": [ [0, 0, 4], // 用户#1在计算机类重合4门 [0, 1, 2], // 用户#1在数学类重合2门 [1, 0, 6], // 用户#2在计算机类重合6门 [1, 1, 1] // 用户#2在数学类重合1门 ] }

提示:热力图数据由后端聚合生成,避免前端遍历大量原始选课记录。heatmapData数组长度=用户数×类别数,确保前端渲染效率。颜色渐变直观呈现“哪些相似用户在哪些领域与你高度一致”,增强推荐可信度。

5. 算法效果验证与线上AB测试实施

5.1 离线评估指标:准确率、召回率与多样性平衡

在离线环境中,使用历史数据(如2022-2学期选课记录)模拟推荐效果。将每个学生的选课记录随机划分为训练集(80%)和测试集(20%),用训练集构建协同过滤模型,对测试集中未选课程生成Top-K推荐,计算以下指标:

指标公式说明
准确率(Precision@K)`推荐∩测试
召回率(Recall@K)`推荐∩测试
覆盖率(Coverage)推荐课程数 / 总课程数推荐系统能触达的课程广度,避免长尾课程被忽略
新颖性(Novelty)-log₂(p(c))p(c)为课程c在训练集中的流行度(被选次数/总选课数),值越高表示越冷门

项目实测结果(K=10,5000学生样本):

算法Precision@10Recall@10CoverageAvg. Novelty
热门榜0.120.350.283.1
User-Based CF0.280.420.655.7
Item-Based CF0.250.380.716.2
混合CF0.310.450.785.9

注意:混合策略在Precision和Coverage上均优于单一算法,证明User-Based提供精准性,Item-Based弥补冷启动并拓宽覆盖。Novelty值5.9意味着推荐课程平均流行度仅为热门榜的1/46(2⁻⁵·⁹≈0.022),有效激活长尾课程。

5.2 线上AB测试:用真实流量验证业务价值

离线指标不能替代真实用户行为。项目部署后,在教务系统选课页开启AB测试:

  • A组(对照组):50%流量,展示传统热门课程榜单;
  • B组(实验组):50%流量,展示协同过滤推荐结果;
  • 核心观测指标
    • 转化率:点击推荐课程后完成选课的比例;
    • 课程开班率:被推荐的冷门课程(选课人数<20)实际开班比例;
    • 用户停留时长:在推荐区域的平均停留时间(反映信息吸引力)。

AB测试运行两周后数据:

指标A组(热门榜)B组(协同过滤)提升
推荐区域点击率8.2%15.7%+91%
冷门课程选课转化率12.3%28.6%+132%
平均停留时长(秒)24.541.8+70%
整体选课完成率76.4%82.1%+5.7pp

关键发现:协同过滤不仅提升冷门课程利用率,更通过个性化降低用户决策成本——B组用户平均浏览课程数减少23%,表明推荐结果更贴合其需求。

5.3 生产环境监控:推荐服务健康度看板

在SpringBoot Actuator基础上,扩展推荐模块专属监控端点/actuator/recommendation,返回JSON格式健康数据:

{ "status": "UP", "metrics": { "requestCount": 12478, "avgResponseTimeMs": 84.3, "cacheHitRate": 0.72, "lastUpdateTimestamp": "2023-10-15T14:22:33Z" }, "dependencies": { "mysql": "UP", "redis": "UP", "lsh-preprocessor": "UP" } }

运维人员可通过Prometheus抓取该端点,Grafana构建看板,重点关注:

  • 缓存命中率(cacheHitRate):低于60%需检查Redis容量或缓存策略;
  • 平均响应时间(avgResponseTimeMs):持续超过150ms需触发慢查询分析;
  • 依赖服务状态:任一DOWN即告警,避免推荐服务雪崩。

提示:lastUpdateTimestamp记录最近一次全量相似度计算完成时间。若该时间距今超24小时,说明后台定时任务失败,需人工介入。此设计将算法更新纳入可观测性体系,确保推荐结果时效性。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询