目录
[敏感度向量 s 用来影响"做完这道题后,历史状态 h 怎样更新";认知向量 m 用来辅助判断"学生现在面对这道题,能否答对"。](#敏感度向量 s 用来影响“做完这道题后,历史状态 h 怎样更新”;认知向量 m 用来辅助判断“学生现在面对这道题,能否答对”。)
不同学生对同一道题的理解可能不同,同一个学生对不同题目的理解也可能不同。
[因为选择认知向量时,代码每次都重新输入当前题目表示 v 和当前历史状态 h。](#因为选择认知向量时,代码每次都重新输入当前题目表示 v 和当前历史状态 h。)
[IEKT 的关键顺序是:先用认知向量预测当前题,再结合当前真实作答选择敏感度向量,通过 GRU 更新状态,供下一题使用。](#IEKT 的关键顺序是:先用认知向量预测当前题,再结合当前真实作答选择敏感度向量,通过 GRU 更新状态,供下一题使用。)
[其中 predict_one_step() 虽然名字带有 one_step,内部实际上会遍历这个 batch 的整段序列:](#其中 predict_one_step() 虽然名字带有 one_step,内部实际上会遍历这个 batch 的整段序列:)
[接下来是当前题目的表示 v (知识点向量+题目向量)怎么得到。](#接下来是当前题目的表示 v (知识点向量+题目向量)怎么得到。)
得到预测之后,才进入学习敏感度向量的选择。(和取认知向量代码类似)
这样,敏感度策略就能够区分"学生实际答对/答错"和"模型预测答对/答错"的组合。
[随后仍然是"算概率 → 抽编号 → 查表":](#随后仍然是“算概率 → 抽编号 → 查表”:)
[你问的**"怎样组织进入 GRU"**,就在 \`update_state()\` (line 76)(/D:/pykt-work/pykt-toolkit/pykt/models/iekt.py:76)。](#你问的**“怎样组织进入 GRU”**,就在 update_state() (line 76)。)
[认知向量 m 没有直接拼到 GRU 输入中。直接组织进 GRU 的是题目表示 v 和敏感度向量 s。](#认知向量 m 没有直接拼到 GRU 输入中。直接组织进 GRU 的是题目表示 v 和敏感度向量 s。)
不过,认知向量参与了本题预测,而预测结果又参与敏感度选择,所以两条路径仍然有关联。
[GRU 输出的 h_t 是一个历史状态向量,不是答对概率,也不是每个知识点的掌握概率。](#GRU 输出的 h_t 是一个历史状态向量,不是答对概率,也不是每个知识点的掌握概率。)
[当前题的概率由前面的预测器输出;GRU 更新出的状态留给下一题:](#当前题的概率由前面的预测器输出;GRU 更新出的状态留给下一题:)
[整段序列跑完之后,才由 train_one_step() 计算训练损失(一样是强化学习)。](#整段序列跑完之后,才由 train_one_step() 计算训练损失(一样是强化学习)。)
[每处理一道题,更新 h:更新这一条学生序列的历史状态。](#每处理一道题,更新 h:更新这一条学生序列的历史状态。)
[每处理完一个 batch,执行 opt.step():更新模型参数,包括参与计算的向量表、预测器、策略网络和 GRU。](#每处理完一个 batch,执行 opt.step():更新模型参数,包括参与计算的向量表、预测器、策略网络和 GRU。)
[AUC:使用预测分数衡量区分真实正例、负例的能力,不先把概率切成 0/1。指标说明](#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。这只是两条记录的计算示范,不代表实际模型效果。