目录
认知向量和敏感度向量意义;
敏感度向量 s 用来影响“做完这道题后,历史状态 h 怎样更新”;认知向量 m 用来辅助判断“学生现在面对这道题,能否答对”。
不同学生对同一道题的理解可能不同,同一个学生对不同题目的理解也可能不同。
可以这样区分**:
因为选择认知向量时,代码每次都重新输入当前题目表示 v 和当前历史状态 h。
IEKT 的关键顺序是:先用认知向量预测当前题,再结合当前真实作答选择敏感度向量,通过 GRU 更新状态,供下一题使用。
先看一批数据怎样进入模型。
***实际训练调用链是:
其中 predict_one_step() 虽然名字带有 one_step,内部实际上会遍历这个 batch 的整段序列:
历史信息h怎么来的
接下来是当前题目的表示 v (知识点向量+题目向量)怎么得到。
然后是你关心的认知向量m怎样选出来。
拿到认知向量m后,先预测当前题的答对概率。
截至这里,当前题的真实作答还没有参与预测。
得到预测之后,才进入学习敏感度向量的选择。(和取认知向量代码类似)
具体代码层面解析:
这样,敏感度策略就能够区分“学生实际答对/答错”和“模型预测答对/答错”的组合。
随后仍然是“算概率 → 抽编号 → 查表”:
你问的**“怎样组织进入 GRU”**,就在 [`update_state()` (line 76)](/D:/pykt-work/pykt-toolkit/pykt/models/iekt.py:76)。
认知向量 m 没有直接拼到 GRU 输入中。直接组织进 GRU 的是题目表示 v 和敏感度向量 s。
不过,认知向量参与了本题预测,而预测结果又参与敏感度选择,所以两条路径仍然有关联。
GRU 输出的 h_t 是一个历史状态向量,不是答对概率,也不是每个知识点的掌握概率。
当前题的概率由前面的预测器输出;GRU 更新出的状态留给下一题:
整段序列跑完之后,才由 train_one_step() 计算训练损失(一样是强化学习)。
它同时训练两件事:BCE和策略损失(认知策略&敏感度策略)
这里要区分两种“更新”:
每处理一道题,更新 h:更新这一条学生序列的历史状态。
每处理完一个 batch,执行 opt.step():更新模型参数,包括参与计算的向量表、预测器、策略网络和 GRU。
最后看验证和测试怎样评估。
***评估过程是:
AUC:使用预测分数衡量区分真实正例、负例的能力,不先把概率切成 0/1。指标说明
最后用一个学生、三道题走一遍。
认知向量和敏感度向量意义;
敏感度向量s用来影响“做完这道题后,历史状态h怎样更新”;认知向量m用来辅助判断“学生现在面对这道题,能否答对”。
你说的“学生有层次”,有个体差异这层意思,但这里更准确的是:
不同学生对同一道题的理解可能不同,同一个学生对不同题目的理解也可能不同。
论文因此分别建模“答题前的认知”和“作答、获得反馈后的知识获取”。原论文
可以这样区分**:
| 向量 | 想表达什么 | 在代码里影响什么 |
|---|---|---|
认知向量m | 学生当前对这道题的理解状态 | 当前题的预测概率p |
敏感度向量s | 这次练习及反馈对学生知识状态的影响 | 更新后的h,进而影响后续预测 |
举个直觉例子:甲、乙都在做一道乘法题。
- 甲已经很熟练:这题对他比较容易,但再做一次,可能没有多少新的收获。
- 乙正在学习乘法:这题对他比较困难,但经过练习和反馈,知识状态可能发生较明显的变化。
这里要表达的是两个不同的问题:
做题前:这道题对他来说理解得怎么样? ↓ 认知向量 m 做题后:这次经历对他后面的学习状态有什么影响? ↓ 敏感度向量 s因此,“当前理解得好”与“通过这次练习获得较多新知识”不必是同一回事。
在你这份代码里,敏感度向量的实际作用很具体:
# 根据真实作答,组织 GRU 输入 # 答对时:[v, s] # 答错时:[s, v] h_new = self.gru_h(inputs, h)所以,即使两次交互的题目和作答结果相同,选择的s不同,GRU 得到的输入也不同,就可以产生不同的状态更新。[对应源码 (line 76)](/D:/pykt-work/pykt-toolkit/pykt/models/iekt.py:76)
不过,不能直接把s理解成一个“学习速度分数”:它是一整条向量,要和题目表示、旧状态一起经过 GRU,才能决定怎样更新。编号大,也不意味着学得更多。
同样,认知向量也没有把学生永久分成固定等级:
同一个学生: 做熟悉的加法题 → 可能选择一种认知向量 做陌生的方程题 → 可能选择另一种认知向量 学习一段时间后再做方程题 → 选择又可能变化因为选择认知向量时,代码每次都重新输入当前题目表示v和当前历史状态h。
[对应源码 (line 214)](/D:/pykt-work/pykt-toolkit/pykt/models/iekt.py:214)
cog_levels=10表示模型有10 个可学习的认知候选向量;代码没有预先规定“编号 0 是差生、编号 9 是优生”,也没有规定编号之间的高低顺序。
IEKT 的关键顺序是:先用认知向量预测当前题,再结合当前真实作答选择敏感度向量,通过 GRU 更新状态,供下一题使用。
认知向量和敏感度向量都来自训练过程中不断更新的候选向量表。
每次选哪一个,由当前题目和学生历史信息决定。
下面按你本地当前源码说明,本轮没有修改文件。
先把几个文件的职责对应起来,后面看函数就不容易迷路:
| 文件 | 负责什么 |
|---|---|
| [wandb_iekt_train.py (line 4)](/D:/pykt-work/pykt-toolkit/examples/wandb_iekt_train.py:4) | 读取 IEKT 的参数,调用公共训练入口 |
| [wandb_train.py (line 93)](/D:/pykt-work/pykt-toolkit/examples/wandb_train.py:93) | 创建数据加载器、模型和优化器,启动训练 |
| [que_base_model.py (line 22)](/D:/pykt-work/pykt-toolkit/pykt/models/que_base_model.py:22) | 整理输入数据;把题号、知识点编号转换为向量 |
| [iekt.py (line 201)](/D:/pykt-work/pykt-toolkit/pykt/models/iekt.py:201) | 逐题选择向量、预测、更新状态,以及计算训练损失 |
| [iekt_utils.py (line 19)](/D:/pykt-work/pykt-toolkit/pykt/models/iekt_utils.py:19) | 实现 MLP 和单步 GRU |
| [train_model.py (line 310)](/D:/pykt-work/pykt-toolkit/pykt/models/train_model.py:310) | 循环训练、反向传播、更新参数、验证与保存模型 |
| [evaluate_model.py (line 52)](/D:/pykt-work/pykt-toolkit/pykt/models/evaluate_model.py:52) | 收集预测概率与真实标签,计算 AUC、ACC |
下面用D表示emb_size。入口默认D=64,但先看维度关系更容易理解。
先看一批数据怎样进入模型。
***实际训练调用链是:
wandb_iekt_train.py → wandb_train.main() → train_model() → model_forward() → IEKT.train_one_step(data) → IEKT.predict_one_step(data, return_details=True)其中predict_one_step()虽然名字带有one_step,内部实际上会遍历这个 batch 的整段序列:
历史信息h怎么来的
# 每条学生序列开始时,历史状态为全零 h = torch.zeros(data_len, self.model.emb_size).to(self.device) # 按时间顺序,一道题一道题处理 for seqi in range(seq_len): ...batch_to_device()会把移位后的两侧序列拼回完整形式:
cq:题目编号 [B, T] cc:每题的知识点编号 [B, T, 最大知识点数量] cr:真实作答 [B, T]例如,一条学生序列可以表示为:
题目: [11, 22, 33] 知识点: [[2, 5], [5, -1], [2, -1]] 真实作答: [1, 0, 1]这里知识点里的-1是补齐位置。`h` 则保存模型根据此前作答形成的历史信息。
接下来是当前题目的表示v(知识点向量+题目向量)怎么得到。
调用的是:
v = self.model.get_ques_representation(q, c)它进一步调用QueEmb.forward(q, c)。
当前 IEKT 默认参数虽然写着emb_type="qid",但代码会把iekt-qid映射为qc_merge,实际会同时使用题目和知识点信息。
[核心计算 (line 103)](/D:/pykt-work/pykt-toolkit/pykt/models/que_base_model.py:103) 是:
# 将本题涉及的有效知识点向量求平均 concept_avg = self.get_avg_skill_emb(c) # 按题号查出题目向量 que_emb = self.que_emb(q) # 拼接:知识点均值在前,题目向量在后 v = torch.cat([concept_avg, que_emb], dim=-1)于是:
知识点平均向量:D 维 题目向量: D 维 拼接后的 v: 2D 维get_avg_skill_emb()会让补齐的知识点对应零向量,并且不把补齐位置计入知识点数量。
这个v描述当前题目;学生此前的历史信息在h中。
然后是你关心的认知向量m怎样选出来。
模型初始化时创建了一张表:
self.cog_matrix = nn.Parameter( torch.randn(cog_levels, emb_size * 2), requires_grad=True )默认是:
10 个候选认知向量 每个向量 2D 维,即默认 128 维表中的数值开始时随机初始化,随后通过训练更新。
不同学生共用这张表,但每次选择哪一行可以不同;编号本身没有预先规定的“低、中、高”含义。
[逐题选择代码 (line 214)](/D:/pykt-work/pykt-toolkit/pykt/models/iekt.py:214) 是:
# 当前题目表示与历史状态拼起来 ques_h = torch.cat([v, h], dim=1) # 2D + D = 3D # MLP 给各候选项打分,softmax 转成选择概率 pi_cog = self.model.pi_cog_func(ques_h) # 按概率抽取一个候选编号 action_cog = Categorical(pi_cog).sample() # 按编号从候选表中取出认知向量 m = self.model.cog_matrix[action_cog, :] # 2D 维这里有两个不同的东西:
pi_cog:各个认知向量被选中的概率 m: 实际抽中的那个认知向量源码采用的是“抽一个编号,再取一行向量”,不是按概率对所有候选向量求加权平均。
拿到认知向量m后,先预测当前题的答对概率。
对应 [`obtain_v()` (line 68)](/D:/pykt-work/pykt-toolkit/pykt/models/iekt.py:68):
# 拼接顺序是历史状态、题目表示、认知向量 predict_input = torch.cat([h, v, m], dim=1) # 输出一个原始预测分数 logits = self.predictor(predict_input) # 转换为答对概率 prob = torch.sigmoid(logits)维度关系是:
h:D 维 v:2D 维 m:2D 维 ↓ 拼接 预测器输入:5D 维 ↓ MLP logit:1 个数 ↓ sigmoid 答对概率 p:1 个数注意两个地方的拼接顺序不同:
选择认知向量时:[v, h] 实际预测时: [h, v, m]截至这里,当前题的真实作答还没有参与预测。
得到预测之后,才进入学习敏感度向量的选择。(和取认知向量代码类似)
它也有一张可训练的候选表:
self.acq_matrix = nn.Parameter( torch.randn(acq_levels, emb_size * 2), requires_grad=True )默认同样是10个候选,每个2D维。
这次选择需要结合:
- 当前题目v和历史信息h;
- 学生实际上答对还是答错;
- 模型刚才预测学生会答对还是答错。
[源码中的组织过程 (line 228)](/D:/pykt-work/pykt-toolkit/pykt/models/iekt.py:228),可以写成下面的数学形式:
a = [h, v] 共 3D 维 r = 当前真实作答 0 或 1 r_hat = 1,如果 p > 0.5,否则为 0 out_x = [ r × a, (1-r) × a, r_hat × a, (1-r_hat) × a ]四块都是3D维,所以out_x一共12D维。
例如:
真实作答r | 预测作答r_hat | out_x的组成 |
|---|---|---|
| 1 | 1 | [a, 0, a, 0] |
| 1 | 0 | [a, 0, 0, a] |
| 0 | 1 | [0, a, a, 0] |
| 0 | 0 | [0, a, 0, a] |
表中的0表示与a等长的零向量。
具体代码层面解析:
先分清: - **`out_x`:提供给选择网络的信息。** - **`pi_sens`:选择各个敏感度候选的概率。** - **`s`:按概率选中后,从表里取出的敏感度向量。** 它们是三个不同的东西。 **① `out_x` 到底装了什么?** 先把学生旧状态和当前题目表示拼起来: ```python a = [h, v] ``` 再用四个固定位置,表达“实际答题结果”和“模型预测结果”: ```text out_x = [ 实际答对的位置, 实际答错的位置, 预测答对的位置, 预测答错的位置 ] ``` 每个位置放的都是 **`a` 或者与 `a` 等长的零向量**。 比如学生实际答对,但模型预测会答错: ```text out_x = [a, 0, 0, a] ``` 网络因此既能看到学生和题目的信息,也能从所在位置区分这次答题的情况。 **② 为什么这么大?因为四个位置都占长度,补零也占长度。** 按你现在的 `emb_size=64`: ```text h:64 个数 v:128 个数 a = [h, v]:192 个数 out_x:4 × 192 = 768 个数 ``` 即使其中两块全是零,它仍然有 768 个数。**长度增加来自重复放置和补零,并没有凭空增加四倍的新信息。** 缩小来看,假设: ```text a = [2, 3, 4] 实际答对、预测答错: out_x = [2,3,4, 0,0,0, 0,0,0, 2,3,4] 实际对 实际错 预测对 预测错 ``` 它就是一个拼接后的向量。实际按批处理时,形状是 `[B, 768]`。 **③ 把 `out_x` 传进去,为什么就得到 `pi_sens`?** 看 [pi_sens_func 的实际代码](/D:/pykt-work/pykt-toolkit/pykt/models/iekt.py:101): ```python def pi_sens_func(self, x, softmax_dim=1): return F.softmax(self.checker_emb(x), dim=softmax_dim) ``` 它内部有一个训练出来的神经网络 `checker_emb`。按你的配置,计算过程是: ```text out_x:768 个数 ↓ Linear + ReLU 768 个中间特征 ↓ Linear 10 个候选的分数 ↓ softmax pi_sens:10 个候选的选择概率 ``` 例如,假设网络算出: ```text 候选编号: 0 1 2 3 4 5 6 7 8 9 选择概率:0.05 0.10 0.05 0.10 0.30 0.10 0.05 0.10 0.10 0.05 ``` 这 10 个概率加起来是 1。 **`pi_sens[4]=0.30` 表示“有 30% 的概率选择第 4 个敏感度候选”,不是“学生答对概率为 30%”。** 然后: ```python action_sens = Categorical(pi_sens).sample() # 如果这次抽到编号 4,就取敏感度表的第 4 行 s = self.model.acq_matrix[action_sens, :] ``` 取出来的 `s` 才是 **128 维敏感度向量**,之后再与题目表示 `v` 组合送进 GRU。 ```text out_x【768 维信息】 ↓ 选择网络 pi_sens【10 个选择概率】 ↓ 抽样 编号,例如 4 ↓ 从 acq_matrix 取第 4 行 s【128 维敏感度向量】 ```这样,敏感度策略就能够区分“学生实际答对/答错”和“模型预测答对/答错”的组合。
随后仍然是“算概率 → 抽编号 → 查表”:
pi_sens = self.model.pi_sens_func(out_x) action_sens = Categorical(pi_sens).sample() s = self.model.acq_matrix[action_sens, :] # 2D 维s用于更新本题之后的状态。当前题的预测概率已经算完,不会被这次更新重新计算。
你问的**“怎样组织进入 GRU”**,就在 [`update_state()` (line 76)](/D:/pykt-work/pykt-toolkit/pykt/models/iekt.py:76)。
这里传入的是:
h = self.model.update_state( h, # 旧历史状态 v, # 当前题目表示 s, # 本次选择的敏感度向量 r # 当前真实作答 )源码先通过真实作答,把v和s放到不同位置。用分块形式表示:
v_cat = [r×v, (1-r)×v] e_cat = [(1-r)×s, r×s] inputs = v_cat + e_cat因为r只能是0或1,可以直接化简:
| 学生真实作答 | v_cat | e_cat | 最终 GRU 输入 |
|---|---|---|---|
| 答对,`r=1` | [v, 0] | [0, s] | [v, s] |
| 答错,`r=0` | [0, v] | [s, 0] | [s, v] |
所以最终是:
GRU 本题输入 inputs:4D 维 GRU 旧状态 h: D 维 ↓ GRU 新状态: D 维认知向量m没有直接拼到 GRU 输入中。直接组织进 GRU 的是题目表示v和敏感度向量s。
不过,认知向量参与了本题预测,而预测结果又参与敏感度选择,所以两条路径仍然有关联。
这个项目使用的是自定义的单步 GRU:[`mygru.forward()` (line 36)](/D:/pykt-work/pykt-toolkit/pykt/models/iekt_utils.py:36)。
核心计算如下:
def forward(self, x, h): # 重置门:生成候选新状态时,使用多少旧状态信息 # 这里的 r_t 是门值,不是学生的真实作答标签 r_t = self.sigmoid( self.g_ir(x) + self.g_hr(h) ) # 更新门:最终保留多少旧状态 z_t = self.sigmoid( self.g_iz(x) + self.g_hz(h) ) # 根据本题输入和旧状态,生成候选新状态 n_t = self.tanh( self.g_in(x) + self.g_hn(h).mul(r_t) ) # 将候选新状态与旧状态混合 h_t = (1 - z_t) * n_t + z_t * h return h_t最需要理解的是最后一行:
新状态 = (1 - 更新门) × 候选新状态 + 更新门 × 旧状态在这份实现里,`z_t` 越接近1,越倾向于保留旧状态。
GRU 输出的h_t是一个历史状态向量,不是答对概率,也不是每个知识点的掌握概率。
当前题的概率由前面的预测器输出;GRU 更新出的状态留给下一题:
第 1 题:用 h0 预测 → 看到 r1 → 更新为 h1 第 2 题:用 h1 预测 → 看到 r2 → 更新为 h2 第 3 题:用 h2 预测 → 看到 r3 → 更新为 h3整段序列跑完之后,才由train_one_step()计算训练损失(一样是强化学习)。
它同时训练两件事:BCE和策略损失(认知策略&敏感度策略)
| 损失 | 训练目的 |
|---|---|
| 答题预测损失 BCE | 让预测答对概率接近真实作答标签 |
| 认知策略损失、敏感度策略损失 | 根据累计奖励,调整候选向量的选择概率 |
奖励仍然是前面讲过的:
模型预测正确:原始奖励 1 模型预测错误:原始奖励 0代码将奖励除以序列长度,再从后往前计算:
G_t = 当前奖励 + gamma × G_(t+1)两套策略分别计算:
认知策略损失 = -log(本次认知动作的概率) × G_t 敏感度策略损失 = -log(本次敏感度动作的概率) × G_t这是用回报训练离散选择概率的策略梯度做法。PyTorch 官方说明
当前实现给两套策略使用相同的累计回报;敏感度动作发生在当前预测之后,但它使用的回报仍包含当前奖励。这是源码的具体处理方式。
最终:
总损失 = BCE + lamb × 两套策略损失之和 / 参与计算的作答记录总数然后外层执行:
opt.zero_grad() # 清除上一次的梯度 loss.backward() # 根据总损失计算梯度 opt.step() # 更新网络参数和候选向量表这里要区分两种“更新”:
每处理一道题,更新
h:更新这一条学生序列的历史状态。每处理完一个 batch,执行
opt.step():更新模型参数,包括参与计算的向量表、预测器、策略网络和 GRU。
当前源码的训练 BCE 会包含首题,并按seq_num截取数据;前面确认过的“题号0导致seq_num少算”问题仍在,这里没有修改。
最后看验证和测试怎样评估。
每个 epoch 训练结束后,外层调用:
evaluate(model, valid_loader, model.model_name)***评估过程是:
切换到评估模式,不计算梯度 ↓ predict_one_step() 按顺序预测整条序列 ↓ 返回 prob_tensor[:, 1:],去掉首题预测 ↓ 用 smasks 筛出参与评估的位置 ↓ 与对应的真实标签 rshft 对齐 ↓ 拼接所有 batch 的预测概率和标签 ↓ 计算一次整体 AUC 和 ACC[实际汇总代码 (line 186)](/D:/pykt-work/pykt-toolkit/pykt/models/evaluate_model.py:186) 相当于:
# 先从每个 batch 中取出需要计分的位置 y = torch.masked_select(y, sm) t = torch.masked_select(rshft, sm) # 收集完所有 batch 后,拼接在一起 ps = np.concatenate(y_scores) ts = np.concatenate(y_trues) auc = metrics.roc_auc_score(ts, ps) acc = metrics.accuracy_score( ts, [1 if p >= 0.5 else 0 for p in ps] )其中:
- ACC:按
0.5阈值判断答对/答错,看判断正确的比例。 AUC:使用预测分数衡量区分真实正例、负例的能力,不先把概率切成
0/1。指标说明
这里算的是所有被选中的作答记录汇总后的整体 AUC,不是各学生或各 batch 的 AUC 平均值。
评估时模型参数不更新,但仍会在每题预测之后,用真实作答更新临时状态h。因此,预测第 3 题时可以使用前两题的真实作答历史。
还有两个源码细节:
eval()后仍会执行两处Categorical.sample(),所以认知、敏感度选择仍然带随机性。- 验证 AUC 比已记录值高出超过
0.001才保存新检查点;连续10轮没有达到这个更新条件就早停。之后通过 [wandb_predict.py (line 60)](/D:/pykt-work/pykt-toolkit/examples/wandb_predict.py:60) 加载检查点,分别计算普通测试集和窗口测试集指标。
意思是,每次都随机抽某一行
最后用一个学生、三道题走一遍。
以下向量、选择结果和网络输出是教学用假设值,不是你的真实训练日志。
为方便看清数组,设:
D = 2 题目序列:[11, 22, 33] 真实作答:[1, 0, 1] 初始历史状态 h0 = [0, 0]先详细展开第 1 题。
假设它关联知识点2、5,当前向量表中:
知识点 2 的向量:[0.1, 0.3] 知识点 5 的向量:[0.3, 0.5] 平均后: [0.2, 0.4] 题目 11 的向量: [0.6, 0.8] 所以题目表示: v1 = [0.2, 0.4, 0.6, 0.8] 共 2D=4 维认知策略接收:
[v1, h0] = [0.2, 0.4, 0.6, 0.8, 0, 0] 共 3D=6 维假设网络算出的选择概率是[0.2, 0.6, 0.2],这次抽中了编号1:
m1 = cog_matrix[1] = [0.1, 0.2, 0.3, 0.4]那么预测器的输入就是:
[h0, v1, m1] = [0, 0, 0.2, 0.4, 0.6, 0.8, 0.1, 0.2, 0.3, 0.4] 共 5D=10 维假设预测器输出:
logit ≈ 1.386294 p1 = sigmoid(logit) = 0.8模型预测学生会答对,随后读到真实作答r1=1。
因此,敏感度策略的输入是:
a = [h0, v1] = [0, 0, 0.2, 0.4, 0.6, 0.8] out_x = [a, 六个0, a, 六个0] 共 12D=24 维假设敏感度选择概率是[0.4, 0.6],这次抽中了编号0:
s1 = acq_matrix[0] = [0.05, 0.1, 0.15, 0.2]因为学生真实答对,所以 GRU 输入是:
u1 = [v1, s1] = [0.2, 0.4, 0.6, 0.8, 0.05, 0.1, 0.15, 0.2] 共 4D=8 维为了把 GRU 也算出具体数值,假设示例权重使两个门都是[0.5, 0.5],并且:
g_in(u1) = [u1[0] + u1[4], u1[1] + u1[5]] = [0.25, 0.5] g_hn(h0) = h0 = [0, 0]那么:
候选新状态 n1 = tanh([0.25, 0.5] + 0.5 × [0, 0]) ≈ [0.244919, 0.462117] 新历史状态 h1 = 0.5 × n1 + 0.5 × h0 ≈ [0.122459, 0.231059]下一题就带着这个h1继续计算。
假设三题最终得到:
| 题目 | 预测时使用的历史状态 | 预测答对概率 | 真实作答 | 本题 GRU 输入 | 更新后的状态 |
|---|---|---|---|---|---|
| 11 | h0 | 0.8 | 1 | [v1, s1] | h1 |
| 22 | h1 | 0.6 | 0 | [s2, v2] | h2 |
| 33 | h2 | 0.7 | 1 | [v3, s3] | h3 |
训练时:
预测概率: [0.8, 0.6, 0.7] 真实作答: [1, 0, 1] 原始奖励: [1, 0, 1] 除以长度 3:[1/3, 0, 1/3] gamma = 0.93 时: 累计回报 G ≈ [0.621633, 0.31, 0.333333] BCE ≈ 0.498703如果三个时间步抽中动作的概率分别是:
认知策略: [0.6, 0.5, 0.7] 敏感度策略: [0.4, 0.8, 0.5]则:
认知策略损失合计 ≈ 0.651314 敏感度策略损失合计 ≈ 0.869820 lamb = 40 时: 总损失 = 0.498703 + 40 × (0.651314 + 0.869820) / 3 ≈ 20.780489这个总损失被交给backward()和opt.step(),用于更新模型参数。
评估时,假设仍得到上述预测概率,并且后两题都参与计分,代码会先去掉首题:
参与评估的概率:[0.6, 0.7] 对应真实标签: [0, 1]- 按
0.5阈值分类,预测为[1, 1],两题只对一题,所以ACC = 0.5。 - 真实正例的概率
0.7高于真实负例的概率0.6,这一对样本排序正确,所以这个小例子的AUC = 1。这只是两条记录的计算示范,不代表实际模型效果。