IEKT向量矩阵流程&评估

目录

认知向量和敏感度向量意义;

[敏感度向量 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 的整段序列:)

历史信息h怎么来的

[接下来是当前题目的表示 v (知识点向量+题目向量)怎么得到。](#接下来是当前题目的表示 v (知识点向量+题目向量)怎么得到。)

然后是你关心的认知向量m怎样选出来。

拿到认知向量m后,先预测当前题的答对概率。

截至这里,当前题的真实作答还没有参与预测。

得到预测之后,才进入学习敏感度向量的选择。(和取认知向量代码类似)

具体代码层面解析:

这样,敏感度策略就能够区分"学生实际答对/答错"和"模型预测答对/答错"的组合。

[随后仍然是"算概率 → 抽编号 → 查表":](#随后仍然是“算概率 → 抽编号 → 查表”:)

[你问的**"怎样组织进入 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() 计算训练损失(一样是强化学习)。)

它同时训练两件事:BCE和策略损失(认知策略&敏感度策略)

这里要区分两种"更新":

[每处理一道题,更新 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。这只是两条记录的计算示范,不代表实际模型效果。
相关推荐
看浪的路人1 小时前
第2讲:LLM API 统一网关——把 OpenAI/Claude/文心一言 管起来
人工智能·文心一言
奈落241 小时前
工程界讲角色边界,控制论讲博弈与稳定性:从 Swarms 的实践清单到《自动化学报》的协同综述,多智能体‘协同‘这件事有两套语言
人工智能·深度学习·自动化·ai 编程
小王R语言学习1 小时前
PaddleNLP:基于飞桨的大模型开发套件
人工智能·其他·paddlepaddle
椒颜皮皮虾྅1 小时前
让 C# 读懂整张图:DeploySharp 驱动 PaddleOCR v4/v5/v6,打通 OCR 全流程
开发语言·人工智能·深度学习·学习·c#·ocr·cuda
roman_日积跬步-终至千里1 小时前
【数据建模入门】从一家小店看懂数仓与机器学习:先理业务,再串技术
数据仓库·人工智能·机器学习·数据分析·数据建模
Js.Tx1 小时前
【具体数学 Concrete Mathematics】2.1.7 无限和式
算法·具体数学
BJ_Bonree1 小时前
聊点技术 | Bonree ONE 业务链路观测实战 支付成功率下降,问题究竟出在哪一环?
大数据·人工智能
Bode_20021 小时前
TRIZ 理论知识体系与架构全景图
人工智能
richard_first1 小时前
第21章 训练一个简单的 GPT
人工智能·gpt·深度学习