☰
基于协同过滤的选修课推荐微信小程序设计与实现
2026/10/15 11:38:57 网站建设 项目流程

1. 为什么想做这样一个选修课推荐小程序

先说一个我身边很真实的场景。某高校每到选课季,论坛里全是“求推荐水课”“XX老师给分怎么样”“这门课和那门课哪个更有用”之类的帖子。信息高度分散,学生靠学长学姐口口相传,效率低不说,很多有价值的课程因为没人讨论,根本不会被注意到。

我自己做过一阵子教务相关的数据整理,发现选修课的选择本质上是一个典型的个性化推荐问题:学生有历史选课行为和兴趣偏好,课程有属性特征,两者之间存在隐含关系。只要能把数据打通,用推荐算法算出“和你相似的人都在学什么”或者“你这类偏好的人对什么课评价高”,就能把选课从“盲选”变成“有依据的选择”。

所以这个项目的定位很直接:做一个基于协同过滤算法的选修课推荐微信小程序,让学生打开小程序就能看到“猜你喜欢”“和你相似的人也在选”“高分口碑课”这几个板块,同时还能搜索、筛选、查看课程评价。对于高校信息化来说,这种小工具比新建一个庞大系统要实用得多,落地周期也短。下面我会从算法选型、数据设计、前后端实现到踩坑排查,把整个项目的来龙去脉讲清楚,适合准备做推荐系统课设、毕业设计,或者想在校园场景里做点实用产品的同学参考。

2. 协同过滤选型与整体设计思路

2.1 两种协同过滤怎么选

一提到协同过滤,最常见的两条路:基于用户的协同过滤(UserCF)和基于物品的协同过滤(ItemCF)。两者逻辑完全不同,选错了整个推荐效果都会跑偏。

UserCF 的核心是“人以群分”:找到与你历史选课、评分行为最相似的一批用户,再看他们选过什么课、给过什么分,把这些课推荐给你。适合用户数量相对稳定、用户个体差异明显的场景。ItemCF 的核心是“物以类聚”:根据所有用户的行为统计出课程之间的相似度,你在选某门课时,系统会推荐和它相似的课程。

放在高校选修课场景里,用户是学生,物品是课程。学生数量一门课选课规模通常几百到几千人,不算特别大。但课程数量通常也就是几十到几百门,而且课程之间“语义关联”很强——比如“Python数据分析”和“机器学习导论”天然相关,这种关联通过学生行为也能反推出来。

我的实际选择是两者结合,以 UserCF 为主,ItemCF 为辅。为什么?因为选修课推荐的核心诉求是“帮你找到同类人认可的课”,而不是单纯做“看了这个课再看那个课”。选课这种行为本身带有很强的群体趋同性——同一个专业、同一类兴趣的学生,选课曲线很接近。UserCF 给出的解释也更自然:“和你兴趣相似的学生中有80%选了这门课”。

ItemCF 作为补充,主要用在冷启动或用户行为太少时,靠课程标签和基础属性做相似推荐,这个后面细说。

2.2 系统架构与核心流程

整个系统我拆成四个部分:小程序端、后端服务、推荐引擎、数据库。小程序端只负责界面展示和用户交互;后端服务负责接口、登录态、业务逻辑;推荐引擎独立成模块,定时计算推荐结果,也支持实时触发计算;数据库用 MySQL 存储业务数据,Redis 存推荐结果缓存和相似度矩阵,避免每次都全量跑算法。

推荐流程是这么走的:

  1. 学生首次进入小程序,微信授权拿到 openid,绑定学号信息。
  2. 前端请求/api/recommend,后端拿到 userId 先查 Redis 有没有缓存推荐结果,有就直接返回。
  3. 没有缓存,就调用推荐引擎。推荐引擎先去读用户的历史行为数据(选课、评分、收藏、浏览时长)。
  4. 如果用户行为数据足够(超过10条),走 UserCF 主流程;如果不足,切换到 ItemCF 冷启动策略。
  5. 推荐结果生成后写回 Redis,设置过期时间(比如2小时),同时返回给小程序端渲染。

这个结构避免了一个很常见的问题:如果推荐算法和业务接口耦合在一起,只要算法一改,整个服务都要重新部署。单独拆出来,后面调试相似度阈值、换权重参数都方便得多。

2.3 数据模型设计

推荐系统成败一半在数据。我建了这几张核心表,先说设计思路。

用户表user:字段包括user_id、student_no、college、major、grade、interests。其中interests是用户自己选择的兴趣标签,比如“人工智能”“数据分析”“人文历史”“创新创业”,逗号分隔,冷启动的时候很管用。

课程表course:字段有course_id、course_name、credit、hours、category(通识选修、专业选修、艺术限定等)、tags、teacher_id、capacity、selected_count、avg_rating。avg_rating是历史平均评分,既用来展示,也可以作为推荐排序的辅助分数。

用户行为表user_behavior:这是核心中的核心。behavior_type有view(浏览)、select(选课)、like(收藏)、rate(评分)、drop(退课)。每种行为会映射成不同的权重分。我用的权重经验值:select=5分,rate=3分(如果打高分再加2分附加分),like=2分,view=0.5分,drop=-2分。为什么要这么做?因为选课是强信号,退课虽然是负向信号但也很有价值,不能直接忽略。

评分表course_rating:单独存用户对课程的评分(1-5分)和评价文本。评分数据是协同过滤最干净的显式反馈,但数量少,所以只能作为辅助信息。

课程相似度表course_similarity_cache:存储 ItemCF 预计算好的课程间相似度,离线任务每天更新一次。

行为数据的时间衰减也做了处理。选课、评分这类事件用半衰期公式:weight = base_weight * exp(-ln2 * age / half_life),把时间因素压进去。选课如果发生在一个学期前,权重只有现在的四分之一左右,这样推荐结果能反映学生当前的兴趣变化。

3. 核心算法实现细节

3.1 基于用户的协同过滤实现流程

我用的 UserCF 分四步走:

第一步,构建用户-课程评分矩阵。矩阵行是用户,列是课程,值是综合行为权重分。比如某个用户浏览过某课(+0.5),随后选课(+5),期末给了4分评价(基础3分+高分附加2分),那这个交叉点的值就是10.5。不单纯用原始评分,目的是把弱行为也吸收进来。

第二步,计算用户相似度矩阵。相似度我用皮尔逊相关系数,因为不同学生的打分尺度不一样,有的人习惯给高分,有的人很苛刻,皮尔逊能消除这种个人尺度偏差。公式是:

similarity(u,v) = Σ((r_ui - r̄_u)(r_vi - r̄_v)) / (sqrt(Σ(r_ui - r̄_u)^2) * sqrt(Σ(r_vi - r̄_v)^2))

这个公式看着复杂,实际上就是两个用户对共同评过分的课程的打分趋势是否一致。两个人都喜欢高数、都讨厌某门思政课,那相似度就高。

第三步,筛选最相似的K位用户。K值我调过很多次,20到30之间效果最好。K太小推荐结果抖动厉害,K太大又把个性化稀释掉了。筛选时还会加一个过滤条件:优先选择同学院、同专业的用户,因为他们的培养方案和兴趣倾向更接近。

第四步,生成推荐候选集并排序。目标用户没有行为过的课程中,把所有相似用户的行为加权求和,权重就是用户相似度。公式为:

predict_score(u, i) = Σ( sim(u, v) * r_vi ) / Σ|sim(u, v)|

其中r_vi是相似用户v对课程i的评分值。预测分数要结合课程平均分、选课人数、评价数量做综合排序,避免某门只有两三个人评分的小众课靠高分冲到最前面。

3.2 ItemCF 的相似度计算与冷启动衔接

前面说了冷启动要切 ItemCF。课程相似度的计算方法是:把每个用户的行为向量当成课程的“画像”,课程A和课程B的相似度 = 两个课程被同一批用户行为的重合程度。实现用余弦相似度:

similarity(i,j) = (用户对i和j的行为向量点积) / (|行为向量i| * |行为向量j|)

但这里有个坑:原始行为向量很容易让所有课程都相似,因为大部分学生选课都很广。所以在算课程相似度前,我对行为分值做了一步标准化,只保留“超出用户自身平均行为水平”的部分。换句话说,两门课都被人选过不算相似,两门课都被同样偏好的人强烈喜欢过才算相似。这样能明显过滤掉“因为必修所以大家都选”的虚假信号。

冷启动衔接逻辑是这样的:用户历史行为少于10条时,先用用户注册时选择的兴趣标签做粗筛,同时结合课程本身标签的属性相似度做 ItemCF 推荐。比如一个选了“人工智能”标签的学生,推荐引擎会把所有带人工智能、Python、机器学习标签的课程拉出来,再按选课人数、平均评分、口碑热度排序,推荐结果里注明“根据兴趣标签推荐”,不至于让新用户啥也看不到。

3.3 针对选修课场景的算法优化

纯基础算法放到真实场景里会很糙,我加了三个选修课场景特有的处理。

第一,对专业课和通识课的区别对待。有些课是某个专业的必修/限选课,外专业学生选了之后匹配度很低。所以我用课程类别特征做加权:如果目标用户和相似用户专业相同,那么他们共同选择过的课程对相似度的贡献权重×1.5;同时推荐候选集里会尽量剔除只对特定专业开放的课程。

第二,处理“水课”效应。选修课圈子里所谓“水课”就是给分高、事少、不点名,这种课会被大量学生盲选,信号价值很低。算法里我引入了一个课程区分度因子:

distinctiveness(i) = log(N / n_i)

N 是总用户数,n_i 是选过这门课的人数。区分度因子乘到预测分数上。一门课被800人选了,区分度只有log(3000/800)≈1.32;另一门课被80人选了,区分度是log(3000/80)≈3.62。这样小众但优质的课程有更大机会浮上来,而不是永远被热门课压住。

第三,引入课程评价的情感倾向。有评分还不够,很多高分是“给分好”而不是“学到了东西”,所以我把评价文本做了轻量情感分析,分出一个口碑质量分,和协同过滤的预测分按 7:3 加权。口碑质量分主要看评价里出现“干货”“有用”“收获大”这类正向词,和“浪费”“水”“坑”这类负向词的占比。这个模块不用太复杂,一个简单的词典匹配就足够用。

4. 微信小程序端与后端联动

4.1 小程序端功能与页面设计

小程序端我是按“低门槛、快启动”的思路设计的。首页是推荐流,顶部几个板块:“猜你喜欢”(UserCF结果)、“人气口碑榜”(按评分和区分度加权排序)、“为你精选”(冷启动结果)。

选课模块用卡片流,每张卡片显示课程名、学分、任课教师、平均评分、选中人数和标签。卡片上直接放两个按钮:“收藏”和“加入对比”,用户轻点一下就能产生行为数据,这些行为会回流到推荐引擎成为下一轮推荐的输入。

课程详情页展示课程大纲、评价列表,并允许用户打分写评语。这里有一个关键设计:不能只让用户选课,还要引导用户做评价反馈。因为协同过滤是越用越准的,没有评价数据就只能靠隐式行为,精度会差很多。

“我的”页面显示选课记录、收藏记录、兴趣标签编辑和个人推荐阈值设置(比如“更看重给分”“更看重干货”),这个设置会作为推荐结果的调整因子传到后端。

页面交互上面,初始版本我用了原生微信小程序框架,后来发现数据列表复杂了之后渲染性能跟不上,就切到了 WeUI 组件库搭配分包加载。推荐流使用recycle-view做长列表渲染,避免一次渲染几百张卡片导致的白屏问题。

4.2 后端接口设计与推荐服务拆分

后端我用的是 Spring Boot 框架,接口风格是 RESTful。核心接口有这么几个:

  • POST /api/user/login:接收微信登录的 code,换取 openid,首次登录时要求绑定学号和兴趣标签。
  • POST /api/user/behavior:上报用户行为事件,body 里包括courseId、behaviorType、value(浏览时长、评分值等)。
  • GET /api/recommend?type=guess:获取推荐列表,type 可切换猜你喜欢、人气榜、精选等。
  • GET /api/course/detail?id=xxx:课程详情与评价列表。
  • POST /api/course/rate:提交评分和评价文本。
  • GET /api/user/similar:给用户展示“和你相似的选课达人”,增加产品趣味性。

推荐服务单独用 Python 写了一个模块,通过 HTTP 接口暴露给后端。为什么用 Python?因为算法脚本我用纯 Python 的numpy和pandas写了大量原型,直接复用比在 Java 里重写一遍效率高得多。两个服务之间用 JSON 交互,推荐结果算完直接落到 Redis。整个链路中GET /api/recommend的平均响应时间实测在 180ms 左右,大部分时间和算法无关,主要是数据库查询和网络开销。

4.3 数据上报的细节与埋点

这个项目最关键的一环不是算法,而是用户行为数据能不能完整、准确地回流。我在小程序端埋了四类事件:

  • 页面停留时间:进入课程详情页时记录进入时间,离开时上报停留时长,超过30秒会标记为“深度浏览”。
  • 滑动手势:首页推荐卡片被滑动超过卡片高度的一半,认为产生了有效曝光。
  • 收藏、选课跳转、评分等显式操作。
  • 搜索关键词:学生在搜索框搜了什么课,也能反映当下最真实的兴趣。

埋点的核心原则是轻量。上报通过一个统一的sendBehavior函数异步发送,失败就丢弃,绝不阻塞业务逻辑。后台会对同一用户一段时间内的行为做去重,比如10秒内重复浏览同一课程只算一次,防止用户手抖产生噪音数据。

5. 实操过程:从0到1搭建完整流程

5.1 环境准备与依赖选择

我用的技术栈具体版本可以给你参考:

  • 前端:微信开发者工具 + 原生语言 + WeUI
  • 后端:JDK 17 + Spring Boot 3.2 + MyBatis-Plus
  • 算法:Python 3.10 + numpy + pandas + scikit-learn(只用到了评估函数)
  • 存储:MySQL 8.0 + Redis 6.2
  • 部署:本地开发环境用 Docker Compose,后端和算法服务打包成两个镜像

安装环境这个环节我建议别贪新。Spring Boot 3 对 JDK 版本要求高,如果你电脑里还留着 JDK 8,建议直接装 JDK 17 并配好JAVA_HOME。Python 这边唯一要注意的是 numpy 和 pandas 用清华源安装,pip install numpy pandas scikit-learn -i https://pypi.tuna.tsinghua.edu.cn/simple。Redis 不需要额外安装客户端,直接用 Docker 拉一个官方镜像就能起。

5.2 数据准备与模拟数据生成

推荐系统项目最难搞的是没有真实数据。我又不可能拿全校学生的真实选课记录来跑实验,所以先用程序生成了模拟数据集。生成逻辑参考了真实选课数据的分布规律:

  • 模拟了 2000 个学生、120 门课程。
  • 每个学生有 5 到 15 条历史行为记录,类型包括浏览、选课、评分等。
  • 学生按专业分布,每个专业对课程有先验概率偏好,比如“计算机”专业的学生选“算法导论”的概率是 0.6,选“音乐鉴赏”的概率只有 0.15。
  • 给分习惯上模拟了两类学生:80% 的学生打 3-5 分,20% 习惯打 1-2 分。

数据生成直接写一个 Python 脚本,按上面的规则随机生成并写入 MySQL。表结构建好后,脚本跑三分钟就能造出 3 万多条行为记录,基本够验证算法了。

5.3 算法核心代码实现

这里贴一段 UserCF 的核心实现,代码精简过,只保留主干逻辑。

import numpy as np from collections import defaultdict class UserCFRecommender: def __init__(self, behavior_df, user_sim_threshold=0.3, top_k=30): self.behavior_df = behavior_df self.user_sim_threshold = user_sim_threshold self.top_k = top_k self.user_item_matrix = None self.user_sim_matrix = None self._build_matrix() def _build_matrix(self): # 行为表至少要有 user_id, course_id, weight_score 三个字段 uid = self.behavior_df["user_id"].astype("category") cid = self.behavior_df["course_id"].astype("category") self.user_item_matrix = np.zeros((len(uid.categories), len(cid.categories))) for row in self.behavior_df.itertuples(): i = uid.categories.get_loc(row.user_id) j = cid.categories.get_loc(row.course_id) self.user_item_matrix[i, j] = getattr(row, "weight_score", 1.0) self.uids = list(uid.categories) self.cids = list(cid.categories) def _calc_similarity(self): # 皮尔逊相关系数矩阵 matrix = self.user_item_matrix mean_matrix = matrix - matrix.mean(axis=1, keepdims=True) norm = np.linalg.norm(mean_matrix, axis=1)[:, None] norm[norm == 0] = 1e-9 normalized = mean_matrix / norm self.user_sim_matrix = np.dot(normalized, normalized.T) def recommend(self, user_idx, ignore_selected=True): self._calc_similarity() sims = self.user_sim_matrix[user_idx] # 剔除自己和相似度低于阈值的用户 candidate_neighbors = np.argsort(sims)[::-1][1:self.top_k+1] candidate_neighbors = [n for n in candidate_neighbors if sims[n] > self.user_sim_threshold] # 物品候选集:邻居有行为而目标用户没有行为的课程 scores = defaultdict(float) for n in candidate_neighbors: neighbor_items = np.where(self.user_item_matrix[n] > 0)[0] for item_idx in neighbor_items: if ignore_selected and self.user_item_matrix[user_idx, item_idx] > 0: continue scores[item_idx] += sims[n] * self.user_item_matrix[n, item_idx] # 归一化 total_sim = sum([abs(sims[n]) for n in candidate_neighbors]) for item_idx in scores: scores[item_idx] /= total_sim return sorted(scores.items(), key=lambda x: x[1], reverse=True)

这里有三个细节值得说。一是归一化分母用的是相似度绝对值之和,防止负相似度邻居把分数拉成负数。二是ignore_selected参数必须为 True,否则推荐结果里全是用户已经选的课。三是top_k和相似度阈值要配合用,阈值高了会过滤掉大部分邻居,低了会混入大量无关用户。我实测阈值设为 0.25 到 0.35 之间最合适,低于 0.2 推荐结果基本等于热门榜。

ItemCF 的实现逻辑类似,只是把矩阵转置后计算课程之间的相似度,这里不再单独贴完整代码。冷启动逻辑里,我直接用 pandas 把课程标签做 one-hot 向量,然后算余弦相似度,选相似度最高的 10 门课。

5.4 评价指标与推荐效果验证

推荐不能光靠感觉,我用离线评测的方式验证效果。把模拟行为数据按 8:2 切分,80% 作为训练集来生成推荐,20% 作为测试集看用户实际选的课有没有出现在推荐列表里。

两个指标最常用:Precision@10(推荐10门课里有多少被用户真实选中)和Recall@10(用户真实选中的课里有多少在推荐列表的Top10内)。我实验的一组结果可以参考:

推荐策略Precision@10Recall@10
纯热门榜0.0820.117
UserCF 基础版0.1460.203
UserCF + 区分度因子0.1810.256
UserCF + 区分度 + 时间衰减0.1970.278

可以看到,加了区分度因子和时间衰减之后,Precision 从 14.6% 提升到了 19.7%,Recall 提升到了 27.8%。这里数据是模拟的,真实场景数字会变,但方向非常明确:单纯协同过滤能跑通,但加了场景化优化才有实用价值。

6. 踩坑记录与问题排查实录

6.1 实时推荐接口响应超时

第一次联调时,/api/recommend接口的响应时间达到了 2.3 秒,用户体验完全不能忍。排查发现每次请求都实时跑一遍用户相似度矩阵计算,2000 个用户可能还好,但矩阵乘法在 Python 里计算加网络传输开销就大了。

解决思路是把离线计算和在线计算分开。用户相似度矩阵不需要每次都算,每天更新一次就够了。于是写了一个定时任务,每天凌晨把全量用户相似度矩阵算好,序列化存到 Redis;在线推荐时只做“查矩阵→取邻居→算TopN”这几步,耗时直接降到 150ms 以内。如果用户量增长到十万级,这个离线矩阵的存储和计算压力会变大,到时候需要上 Spark 或向量检索,但那是后话了。

6.2 数据稀疏导致的相似度全为0

模拟数据生成阶段,我故意把用户行为控制在 5 到 15 条,发现很多用户对之间的共同选课记录只有一两条,甚至没有。皮尔逊相关系数算出来要么是1(完全相关但实际是巧合),要么分母为0无法计算。这直接导致推荐结果里出现大量奇怪课程。

解决方法是给相似度加了一个显著性惩罚:如果两个用户共同评过的课程数量小于 5,就把相似度乘上一个min(1, common_count/5)的可信度系数。共同交互数量越多,相似度越可信;只有一两条共同课程就不信。这个做法比直接设阈值更平滑,也避免把“只有一门课重叠且评价一致”的两个用户当成完美相似。

6.3 小程序登录态与 openid 绑定问题

微信小程序登录不像网页登录那么简单,wx.login()拿到的 code 只能换一次 openid,而且 openid 每次都是同一个,只要用户删小程序再进,还会复用之前的 openid。但坑在于开发环境、体验版、正式版的环境隔离,我一开始没注意,导致用户在有多个环境切着测试时,每次都被当成新用户,重走一遍冷启动流程。

改善办法是后端在绑定学号时额外生成一个user_token,把openid + user_token一起作为登录凭证存到后端,Redis 里设置 7 天有效期。小程序端把 token 存到storage里,每次请求带上。这样即使 openid 环境变了,只要 token 还在,用户身份就不会丢。

6.4 用户反馈,推荐结果总是那几门热门课

算法本身没问题,但索引和缓存设计出问题了。推荐结果缓存 Redis 时用的是所有用户共用一个 key,导致所有学生拿到的都是第一批请求计算出来的结果,变成了伪个性化。

修复方案:Redis key 改成recommend:{userId}:{type},并且缓存过期时间设置为 2 小时。同时热门榜和个性化推荐分成两个独立接口,热门榜单独计算,不再占用推荐缓存。这个坑很典型,写缓存的时候只想着“加速”,没想到 key 粒度过大会把个性化逻辑直接毁掉。

7. 改进方向与个人实操体会

7.1 后续可以扩展的几个方向

第一,引入深度学习排序模型。协同过滤能解决“找出候选集”的问题,但对“如何排序”不够精细。可以用 DeepFM 或简单的双塔模型把用户画像、课程画像、上下文特征加进去做粗排和精排,推荐精度应该还能再涨一截。

第二,增加实时行为流处理。现在行为数据是直接写 MySQL,推荐引擎按需从 MySQL 汇总。如果用户量大了,可以用消息队列收集行为,再用 ClickHouse 或 Redis 做实时特征汇总,让推荐结果能在分钟级内更新。

第三,把“解释推荐理由”做得更好。现在的推荐详情页只有一句“因为和你相似的同学也喜欢这门课”,比较生硬。后续可以在算法输出相似用户和相似课程时,把评分贡献最大的若干用户行为摘出来,展示成“3位同专业的学生都给这门课打了4分以上”这种具体理由,会更让人信服。

7.2 一些真心的踩坑心得

这个项目从头到尾做完,我最大的感受是:推荐系统里,算法只占三成,其余七成是数据和工程。真正花时间的不是写相似度函数,而是理清楚行为权重怎么调、相似度阈值怎么选、冷启动怎么处理、缓存 key 怎么设计。如果你也想做类似的项目,我建议先把数据流画清楚,明确每一步的数据来源和去向,再动手写代码。

另外,做一个工具型小程序,别一上来就堆砌高深算法。选修课推荐本质上需要的是“足够好并且能解释”的推荐,而不是“最优但黑盒”的推荐。你在产品界面上告诉用户为什么推荐这堂课、有哪些依据,用户就会愿意用,也会更信任你。信任建立起来,用户才会持续打分和评价,推荐系统的数据飞轮才能转起来。

还有一个细节:项目命名虽然叫“基于协同过滤算法的选修课推荐微信小程序”,实际落地时建议把协同过滤做成可配置的算法模块,后面不管换 ItemCF、加深度学习模型,还是改成混合推荐,都不需要推翻重来。留好扩展位,比一次做完美更重要。

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

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

立即咨询