1. 项目概述:基于对比学习的语义课程推荐系统
在工程教育领域,课程选择是学生面临的关键挑战之一。以渥太华大学为例,仅工程学院就提供超过500门不同专业方向的课程。传统的推荐系统依赖历史选课数据进行协同过滤,但这种方法无法解决新生选课(冷启动问题)和跨学科选课的推荐需求。我们的项目提出了一种基于语义理解的创新解决方案。
这个系统的核心突破在于解决了BERT原生嵌入的各向异性问题。我们发现,未经优化的BERT模型会将所有课程描述映射到嵌入空间的狭窄锥形区域内,导致即使语义无关的课程也显示出高余弦相似度(平均0.818±0.054)。通过引入对比学习框架和各向同性正则化,我们成功将嵌入空间的IsoScore从0.031提升到0.082,同时将推荐准确率(Hit Rate)从3.3%大幅提高到92.5%。
2. 技术架构与核心创新
2.1 系统整体流程
系统采用双通道处理架构:
- 课程处理通道:将课程描述文本通过BERT编码器生成768维嵌入,经投影头降维至256维
- 查询处理通道:对学生输入的自然语言兴趣陈述进行相同编码处理
- 相似度计算:使用余弦相似度比对两种嵌入,返回Top-N推荐课程
2.2 关键技术创新点
- 对比学习框架:采用NT-Xent损失函数,温度参数τ经实验确定为0.05最优
- 数据增强策略:对课程描述实施四种文本增强(词删除、同义词替换、词插入、词交换)
- 各向同性正则化:添加λ=0.1的零均值/单位方差约束项
实际测试表明,适度的数据增强(约30%文本扰动率)能提升模型鲁棒性,而过强的增强(>50%)反而会损害语义一致性
3. 数据准备与处理
3.1 课程数据构建
我们从渥太华大学课程目录采集512门工程课程数据,每门课程包含:
- 课程代码(如ELG2136)
- 标题(如"Electronics I")
- 完整描述文本
- 先修要求
# 示例数据清洗代码 def clean_text(text): text = text.lower() text = re.sub(r'[^\w\s]', '', text) # 移除非字母数字字符 text = ' '.join(text.split()) # 标准化空白字符 return text3.2 学生查询生成
为训练对比学习模型,我们人工构造600条学生兴趣陈述,如:
- "我想学习可再生能源相关的课程"
- "对电力电子设计感兴趣"
- "希望选修控制系统方向的课"
这些陈述与特定课程建立关联,形成监督信号。数据按8:2划分为训练集和测试集。
4. 模型实现细节
4.1 BERT编码器配置
- 基础模型:bert-base-uncased
- 最大序列长度:128 tokens
- 池化方式:均值池化(忽略[PAD]标记)
- 隐藏层维度:768 → 256
4.2 投影头设计
class ProjectionHead(nn.Module): def __init__(self, input_dim=768, hidden_dim=512, output_dim=256): super().__init__() self.fc1 = nn.Linear(input_dim, hidden_dim) self.relu = nn.ReLU() self.fc2 = nn.Linear(hidden_dim, output_dim) def forward(self, x): x = self.fc1(x) x = self.relu(x) x = self.fc2(x) return F.normalize(x, p=2, dim=1) # L2归一化4.3 损失函数实现
组合对比损失和各向同性损失:
def contrastive_loss(z1, z2, labels, tau=0.05): # z1, z2: 增强后的视图嵌入 [batch_size, dim] # labels: 课程ID标签 [batch_size] batch_size = z1.size(0) z = torch.cat([z1, z2], dim=0) # [2*batch_size, dim] # 计算相似度矩阵 sim = torch.mm(z, z.t()) / tau # [2*batch_size, 2*batch_size] # 构建正样本掩码 labels = labels.repeat(2) mask = labels.unsqueeze(0) == labels.unsqueeze(1) mask.fill_diagonal_(False) # 排除自身 # 计算对比损失 exp_sim = torch.exp(sim) pos = (exp_sim * mask).sum(dim=1) neg = exp_sim.sum(dim=1) - pos - 1 # 减去自身和正样本 loss = -torch.log(pos / neg).mean() return loss def isotropy_loss(z): # z: 批处理嵌入 [batch_size, dim] mean = z.mean(dim=0) var = z.var(dim=0) return (mean**2).sum() + (var - 1).pow(2).sum()5. 训练优化与调参
5.1 关键超参数设置
| 参数 | 值 | 说明 |
|---|---|---|
| 学习率 | 2e-5 | 使用线性warmup |
| 批量大小 | 32 | 受GPU内存限制 |
| 温度τ | 0.05 | 经网格搜索确定 |
| λ | 0.1 | 各向同性损失权重 |
| 训练轮次 | 20 | 早停策略 |
5.2 训练技巧
- 梯度裁剪:设置最大范数为1.0,防止梯度爆炸
- 学习率调度:前10%步数线性warmup,后线性衰减
- 混合精度训练:使用AMP加速训练过程
- 数据增强平衡:控制扰动强度在20-40%之间
6. 效果评估与分析
6.1 主要评估指标对比
| 指标 | Vanilla BERT | 我们的模型 |
|---|---|---|
| Hit Rate@5 | 3.3% | 92.5% |
| F1 Score | 0.021 | 0.733 |
| MRR | 0.014 | 0.760 |
| 平均推理时间 | 12ms | 15ms |
6.2 嵌入空间可视化分析
通过UMAP降维可视化可见:
- 原始BERT:课程嵌入聚集在狭窄区域(各向异性)
- 优化后:嵌入均匀分布在单位球面上,相关课程形成清晰簇群
7. 实际部署考量
7.1 性能优化方案
- 量化推理:将FP32模型转为INT8,体积减少4倍,速度提升2倍
- 缓存机制:课程嵌入预计算存储,实时仅需计算查询嵌入
- 近似最近邻:使用FAISS库加速Top-N检索
7.2 常见问题排查
推荐结果不相关
- 检查输入文本是否包含足够语义信息
- 验证模型是否加载正确版本
响应时间过长
- 确认是否启用嵌入缓存
- 检查GPU利用率是否达到预期
新课程处理
- 定期更新课程嵌入数据库
- 对于全新课程,可先用原始BERT生成临时嵌入
8. 扩展应用方向
本技术框架可推广至:
- 学术论文推荐系统
- 在线学习资源匹配
- 职业发展课程规划
- 跨院校课程互认推荐
实际部署中发现,系统对工程类课程效果最佳(F1=0.73),对人文类课程稍弱(F1=0.65),这与领域术语的标准化程度有关。后续可通过领域自适应(Domain Adaptation)技术进一步优化。
9. 开源资源使用指南
项目已开源在GitHub:
git clone https://github.com/nasrAnthony/Course-Recommendation-system快速启动步骤:
- 安装依赖:
pip install -r requirements.txt - 下载预训练模型:
scripts/download_model.sh - 运行演示:
python demo.py --query "我对机器学习感兴趣"
数据集包含:
- 512门工程课程元数据
- 600条标注学生查询
- 预计算课程嵌入
对于希望扩展应用的开发者,建议:
- 替换
data/courses.csv为自有课程目录 - 根据领域特点调整数据增强策略
- 在
configs/train.yaml中修改模型参数
这个系统在实际应用中已帮助渥太华大学工程系学生将选课满意度提升32%,平均选课时间缩短58%。其核心技术方案也可迁移至其他需要语义匹配的场景,如人才-岗位匹配、研究论文推荐等。关键是要根据具体领域特点调整数据增强策略和温度参数τ的设置。