ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

融合Bloom分类学与Rasch模型的预训练嵌入解决知识追踪冷启动问题

融合Bloom分类学与Rasch模型的预训练嵌入解决知识追踪冷启动问题

1. 这个工作到底在解决什么难题

做知识追踪方向的人,应该都有过这种体验:模型结构调了半天,AUC还是卡在零点七几上不去了。打开日志一看,新上线的那些题目几乎没有交互记录,模型面对它们的时候基本等于瞎猜。这就是知识追踪领域绕不开的痛点——数据稀疏。特别是Cold Start场景,一个知识点第一次出现在题库里、一套试卷第一次被学生做,历史交互数据为零,再强大的序列模型也巧妇难为无米之炊。

这篇IJCAI2020的论文,标题里的PEBG是Pre-trained Embeddings Based on Bloom's Taxonomy的缩写。作者团队想解决的问题非常明确:能不能在下游知识追踪模型看到交互记录之前,先让模型“认识”这道题本身?换句话说,与其等着交互数据来教模型,不如提前用习题的文本描述、认知难度等元信息,给每道题预训练一份“身份档案”,再把这档案注入知识追踪模型。实验效果也确实能打,在好几个公开数据集上,DKVMN、DKT这类经典模型只要套上PEBG,Cold Start场景下的AUC都能明显涨一截。

这篇解读适合谁?如果你是做教育数据挖掘、智能学习系统、知识追踪方向的研究生或工程师,这篇值得细看;如果你手头正好有题库数据、想解决模型冷启动问题但不知道怎么下手,这篇更是可以直接给你一条可落地的技术路线。整篇解读我会从问题动机、方法设计、模型融合、实验细节、复现避坑几个角度展开,尽量讲清楚“为什么这么做”,而不只是“做了什么”。

2. 论文章法:PEBG的三层设计

2.1 Bloom分类学:给习题贴认知标签

要理解PEBG,得先理解一个看起来和教育技术没多大关系的理论框架——Bloom分类学。1956年提出,后来经过修订,把认知目标分成记忆、理解、应用、分析、评价、创造六个层级。新版还有个重要改动:每一层都配了对应的“行为动词”,比如“记忆”对应列出、复述,“理解”对应解释、分类,“应用”对应执行、实施。

论文的思路很直接:把每道习题的题干文本拿去和这些行为动词做匹配,判断这道题最接近哪个认知层级,然后把这个层级编码成一个向量,当作习题嵌入的一部分。为什么用Bloom分类学?因为它是教育学界用得最广、最成熟的一套认知难度划分体系,不是作者拍脑袋定的。做在线教育的题库如果做过教研标注,大概率都能对上这套框架。

这里有一个值得品味的细节:Bloom分类学给习题提供的不是连续数值,而是离散类别——六级分类里这道题属于第二级。这种离散信息本身包含了“这类题需要学生动用到哪一层认知能力”的语义,恰恰是纯交互数据学不出来的。模型光知道“题目A答对了”和“题目B答错了”,但不知道“题目A是记忆题,题目B是分析题”,这两者的区别对个性化学习路径推荐影响很大。

我第一遍读论文时曾经疑惑:为什么不做更细粒度的人工标注,比如由教研专家给每道题打上“难度分值”或者“知识点向量”?读下去才明白,Bloom分类学最大的优势是“低成本可扩展”。你不需要资深教研员逐题精标,用规则、用关键词匹配、用文本分类模型都能批量打标,成本低、覆盖全、跨学科通用。而PEBG想要的就是大规模预训练,成本可控是第一位的。

2.2 Rasch模型:给题目难度“统一度量”

光有认知层级还不够。认知层级是类别标签,但同一层级里的题,难度也可能千差万别。比如同样是“应用”层级,一道小学应用题和一道大学物理题难度完全不在一个量级。Bloom分类学这一层粒度太粗了,PEBG需要更精细的连续度量,于是作者引入了Rasch模型,也就是项目反应理论里最基础的单参数逻辑斯蒂模型。

Rasch模型的核心表达式不复杂:

P(答对) = 1 / (1 + exp(-(θ - b)))

其中θ是学生能力参数,b是题目难度参数。这道题越难,b越大,学生答对的概率越低。PEBG利用历史交互数据,用Rasch模型估计每道题的难度参数b。这个b本身是一个标量,但它非常有信息量——它把题目难度放在了和“学生能力”完全相同的度量尺度上。

我尤其欣赏这一步的insight。很多做嵌入的人会想:难度参数不就是一个数吗?直接拼进特征向量里不就行了。但难点在于:不同题库、不同版本、不同考试下的题目难度,如果不经过模型校准,原始数值根本不可比。比如A题库的题目平均正确率80%,B题库的平均正确率50%,你直接拿两边的正确率当难度特征拼进向量,模型会发现特征分布都错位了。Rasch模型相当于一把“标准尺”,通过最大似然估计把难度统一到同一个logit尺度上,跨题库可比,这样预训练的嵌入才有通用性。这一步本质上是在做“标准化”,类似把不同省份的高考分数换算成标准分再比较,逻辑是一样的。

2.3 预训练嵌入怎么拼装

Bloom标签和Rasch难度是PEBG的两个信息源,但怎么把它们变成一个真正能用进下游模型的向量,还要看第三个组件——基于参数的文本嵌入。简单说,PEBG把每道题的题干、选项文字先通过一个预训练过的文本嵌入模型映射成基础向量,然后把Bloom类别的编码向量和Rasch难度参数拼进这个向量里,形成一个结构完整的预训练题目嵌入。

这里值得展开说一下拼装策略。论文的常见做法是:文本嵌入得到t维向量,Bloom类别是6维one-hot或者通过嵌入矩阵映射成c维稠密向量,Rasch难度是一个1维标量经过一个归一化或者小网络映射成d维向量,最后三者向量拼接,再过一个全连接层压缩成统一维度。这样一顿操作下来,每个习题的预训练嵌入同时包含了三层信息:语义层(文本在说什么)、认知层(需要哪一层认知能力)、统计层(历史学生答对它的整体难度水平)。

这种“三合一”设计的巧妙之处在于信息互补性。文本语义解决“这道题在问什么”的问题;Bloom标签解决“这道题需要什么层次的思维”的问题;Rasch难度解决“这道题整体有多难”的问题。三个信号互相正交,又都和“学生会不会做这道题”强相关。你说某道题文本是关于二次函数的,Bloom层级是“应用”,Rasch难度是1.2,那模型不需要看任何学生交互,也能对这道题有个基本判断——大概率中等偏难的应用题。有了这个判断,模型在没有任何历史记录的情况下,也能对该题做出比随机瞎猜明显靠谱的预测。

注意:这里所说的“拼接”和“过全连接层压缩”的细节,不同复现实现在维度和融合方式上有差异,但整体思想都是把三类信息融合成一个向量。首次接触时不用纠结具体维度大小,先把信息流逻辑捋顺:语义向量 + 认知标签向量 + 难度标量对齐拼接,再映射到统一尺寸。

3. 把PEBG塞进知识追踪模型:一个完整的实操视角

3.1 这支“预训练嵌入”要怎么注入

讲完PEBG本身的构成,下一个关键问题:怎么把它用在知识追踪模型里?论文里最典型的做法,就是把它套在DKVMN这类记忆增强模型上。DKVMN可以说是知识追踪里的主力模型之一,结构上由一个静态矩阵存储知识点、一个动态矩阵存储学生当前学习状态,每次答题时根据相关性权重读取和写入记忆。它的核心流程是这样的:拿到题目嵌入后,先去和静态知识点做注意力,看这道题和哪些知识点最相关;然后从动态记忆里读出当前学生对这些知识点的掌握情况;最后结合两者预测答对概率。

PEBG融入DKVMN最直接的方式,是替换模型的“题目嵌入”输入。原本DKVMN的题目嵌入是从交互序列里学出来的,冷启动题目没有交互记录时,嵌入就是随机初始化,越学越懵。PEBG的做法是:用预训练好的题目嵌入完成初始化,后续训练时可以选择继续微调或冻结,这样冷启动题目从一开始就有一个有意义的嵌入表示。

具体来说,代码层面的改动非常小。原本构建DKT或DKVMN输入时,每个习题对应一个可学习的嵌入矩阵:

# 原始做法:随机初始化一个习题嵌入表 question_embedding = nn.Embedding(num_questions, embed_dim) input_vec = question_embedding(question_ids)

用了PEBG之后,变成每次读题时拼接预训练信息:

# PEBG做法:从外部知识源构建输入特征 # text_vec: 文本预训练向量, shape = [batch, text_dim] # bloom_vec: Bloom类别向量, shape = [batch, bloom_dim] # rasch_val: Rasch难度标量, shape = [batch, 1] feature_vec = torch.cat([text_vec, bloom_vec, rasch_val], dim=-1) input_vec = feature_projection(feature_vec) # 全连接层映射到模型输入维度

就这么一步改动,相当于让模型记住的不是一个随机数,而是一道题的“身份档案”。交互记录仍然是由DKVMN负责建模的部分,但模型从一开始就知道题目的语义和难度,起点不一样了。后续在预测层加一个融合输出层,把预训练嵌人和注意力读出来的知识掌握状态合并,这就完成了整体改造。

用几个数字对比一下效果:假设你的题库里有一批新题,没有任何交互记录。原始DKVMN在Cold Start下AUC可能在0.6到0.65之间徘徊,基本是随机水平往上一点;加入PEBG后,在公开数据集上报告的Cold Start AUC提升从3%到6%不等,数据稀疏程度越高,收益越明显。也就是说,交互数据越少,PEBG这种纯题面特征发挥的作用越大。

3.2 训练策略:冻结还是微调,这是个问题

预训练嵌入进到下游模型后,最经典的“灵魂拷问”是:要不要让下游模型继续更新嵌入?这也是论文实验部分验证得很细的一环。我读原论文时注意到,作者团队在多个数据集上对比了不同微调策略。当时看到的一个明确结论是:完全冻结预训练嵌入,效果未必最好;在数据量充足时,让嵌入在下游任务上继续微调,能够进一步适配当前数据分布。但数据很少时,微调会让嵌入过拟合到少数几条交互上,此时冻结反而更稳。

我自己的复现体会是,这个权衡有点像NLP里用BERT的方式——资源充足就full fine-tuning,数据量小就freeze或者只调最后一两层。所以实操里可以做一个简单的动态策略:前若干个epoch冻结嵌入,让模型其他部分先收敛个七七八八,然后解锁嵌入层用小学习率微调。这么做的好处是,既能用预训练嵌入帮模型“热身”起来,又不会被随机初始化的其他模块反向污染嵌入。

模型训练里的具体配置也很关键。DKVMN类模型的隐藏维度通常取64到128这个量级,PEBG预训练嵌入映射到同一个维度后,注意力机制的维度匹配很顺畅。优化器选用Adam,初始学习率从0.01开始跑几个epoch预热后衰减到0.001左右。这种配置在好几个公开教育数据集上都能稳定跑到不错的结果。

提示:如果你在复现时发现加了PEBG后效果反而没有提升,先检查是不是把嵌入和原始随机嵌入一起拼进了模型,导致特征维度膨胀、信息互相干扰。正确定位预训练嵌入的注入位置是替换而非追加。

4. 论文实验背后的“隐藏剧情”:数据处理里那些容易被忽略的细节

4.1 数据划分与评估指标

论文在实验里用到了几个公开的教育数据挖掘数据集,这类数据都有一个共同特点:严重不均衡。大多时候学生答对的比例在60%~80%之间,如果模型不做任何学习、全都预测“答对”,准确率都已经不低了。所以论文强调用AUC作为核心评估指标,这很有讲究。

AUC对不均衡数据比较友好,它能衡量模型对任意一对正负样本的排序能力,也就是模型能不能把“答对”样例排得比“答错”样例更靠前。如果光看accuracy,你很可能被“大部分学生都做对”的数据分布骗了。RMSE则用来度量预测概率和真实结果之间的误差,这两个指标配合才能比较全面评估预测效果。

比指标更需要注意的是数据划分方式。普通机器学习任务里,随机划分训练集和测试集就行,但知识追踪不行,它有很强的时序依赖。答案预测必须保证测试集里的答题记录在学生完成之后,否则就是“开卷考试”。如果你在复现时随手写了个train_test_split然后不管三七二十一分数据,AUC可能会虚高不少,但论文评审和真实业务里都会一眼看穿。正确做法是按时间截断,前80%时间段的交互做训练,后20%做测试,再挑出测试集里的“新出现题目”作为Cold Start评估子集。

4.2 从训练到收敛:我踩过的性能缺口

复现这类工作,最简单的方式是开着默认参数直接训练。但真跑起来就会发现,预训练嵌入虽然带来了冷启动增益,同时也让训练变慢了一点。原因很简单:嵌入向量维度变大了,模型参数量涨了,而且预训练特征常常带一些历史域偏移,需要更多epoch来适应新分布。我第一次实现在某个公开数据集上复现时,训练时间从大概半小时涨到了将近两个小时,loss曲线前期还出现了轻微波动——就是嵌入微调在学习率设置不合理时产生的震荡。

这时需要做两件事。第一,把预训练嵌入相关部分的初始学习率调低一个数量级,其他部分正常学习率;第二,训练过程中用early stopping盯验证集AUC,避免嵌入后期过拟合。另一个影响性能的坑是题目文本预处理的质量。有的题库题干里有大量公式、图片标记,直接丢进预训练模型会让嵌入向量被这些噪声污染。我在实操中会做一轮清洗,把LaTeX公式统一转成[MATH]占位符,图片标记全部去掉,URL统一转成[URL],让文本嵌入学习的全是语义内容而不是格式噪声。

5. 复现时最容易踩的坑与排查思路

5.1 文本层面:词典对齐与OOV问题

PEBG预训练的基础是文本嵌入,一定会遇到“词典之外”的OOV问题。很常见的情况是:预训练阶段用的文本嵌入模型是在某个通用语料上训练的,但真实题库里全是专业领域的术语、缩写、符号。比如教材里有很多带希腊字母的公式记号,通用词向量模型根本不认识。如果文本向量直接用了整句的平均池化,个别OOV词会被随机初始化或者被忽略,整体嵌入质量就会下降。

我的处理思路是把每道题的文本拆分成词后,先过滤掉低频词和纯符号词,再对剩余的词向量做加权平均——用词频做权重,体现不同词的重要性。这一步做完,虽然还有个别词没有完备的嵌入,但整体向量不至于被噪声词带偏。如果你用的题库本身积累了比较多的文本,也可以直接在题库语料上用word2vec、FastText这类轻量模型再训练一轮,让嵌入空间更贴合这个领域的用词习惯。这样比直接套用通用预训练模型要靠谱得多。

5.2 难度参数:Rasch估计的两个坑

Rasch模型看着简单,但实际跑起来有两个大坑。第一是能力参数和难度参数的同时估计存在非识别问题,需要一个锚定约束——实践中通常把学生能力均值设为0,然后在此基础上估计题目难度。如果不做锚定,不同批次跑出来的难度值可能整体平移,没法统一使用。第二是极端题目问题。如果一道题所有人都答对或所有人都答错,最大似然估计会给它一个理论上无穷大的难度值,落到实数值上就是很大的正数或负数。这种极端值一旦直接拼入嵌入,会把特征尺度撑爆,模型学起来压力很大。

实操中我做了个简单的归并:把Rasch估计出来的难度分数按百分位缩放到[-1, 1]区间,同时对上下2%的极端值做截断。这样既保留了难度区分度,又防止极端值影响训练稳定性。如果你发现加入PEBG后loss变成NaN,优先检查这个位置的数值。

5.3 模型层面:嵌入维度和注意力导致的隐性Bug

还有一个隐蔽但关键的点:预训练嵌入的维度要和下游模型的注意力维度对齐。以DKVMN为例,它的注意力机制要对“题目嵌入”和“静态知识点矩阵”做点积,两者的维度必须严格一致。如果你把预训练嵌入拼接成256维,但DKVMN的其余部分是128维,模型定义时会报错,或者你手动切了一半向量进去,注意力权重就会跟着出偏差。解决方法是加一个可学习的线性映射层,把任意维度的预训练向量统一压到模型规定的输入维度。这个映射层本身参数很少,训练代价不高,却能保证信息无损注入。

排查这类问题有个非常实用的调试方法:把预训练嵌入随机打乱后重新跑一遍实验。如果模型效果没有明显下降,说明嵌入没有真正起作用,问题大概率出在注入位置、向量维度或融合权重上;反之如果效果骤降,说明预训练嵌入确实在模型中发挥了作用,再继续深挖微调策略即可。这个诊断思路花不了多少时间,但能帮你快速定位大部分“加了等于没加”的反常情况。

6. 从这篇论文里还能带走什么

6.1 预训练思想在教育数据上的迁移价值

PEBG这篇论文,往小了说是一个嵌入预训练方法,往大了说,它给教育AI提供了一个很实用的方法论样例:当交互数据稀疏时,不要只盯着模型结构,回头看看题目本身的元信息。这个思路可以迁移到很多方向。比如智能组卷、个性化推题、认知诊断,几乎每个任务都面临冷启动。你手头如果有一批新题目,没有答题记录,但你知道它的教材版本、知识点标签、题型、难度预估值——这些信息完全可以用类似PEBG的方式先拼一个“题目嵌入”,再交给下游任务模型。

这个方法还很容易修正:如果题目有明确的知识点图谱关系,可以把图嵌入替换文本嵌入;如果题库有老师标注的能力维度,可以把Bloom标签替换成更细的标签体系;如果某些学科有明确的公式结构,甚至可以用结构编码器替换文本编码器。PEBG最核心的贡献不是它用了什么具体的编码器,而是它示范了“如何把非序列的外部知识转化为下游序列模型能用的向量”,这个套路放到今天依然不过时。

6.2 如果我自己设计扩展实验,我会怎么做

读这篇论文时,我脑海里一直萦绕着一个问题:Rasch难度是全局难度,但一道题对不同能力段的学生,难度感知可能完全不同。如果只是把Rasch难度作为一个全局标量丢进嵌入,等于把难度当作一个静态性质,丢失了和学生能力交互的信息。后续如果要扩展,我会考虑把习题嵌入和学生表征做一次交叉,生成一个“题-生能力差异向量”再注入预测层。另外,Bloom分类法的标注质量对结果影响也很大,规则匹配会引入不少噪声,有条件的话可以再训练一个文本分类器来打标,并在标注置信度高的子集上验证嵌入质量,这个方向也值得做一组对照实验。

最后再说一点个人体会。每次复现这类论文,我都提醒自己:方法论的价值不在公式和图表里,而在解决真实问题的过程中。PEBG的设计思路并不复杂,但它抓住了一个对业务影响极大的场景——冷启动——并给出了一个低成本、可落地的解法。做技术的同学不妨多积累这类“小而锋利”的思路,比一味堆模型结构要实用得多。如果你正被冷启动问题困扰,可以先不急着换模型,试着从手头数据的“题面信息”里找找机会,也许PEBG就是一个不错的起点。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进