NB-1 — BKT from scratch
Этот ноутбук — 30 строк numpy, восстанавливающие всю BKT-модель.
Цель — убедиться, что наша TS-реализация в web/lib/bkt.ts
числа в числа совпадает с эталонной Python-версией.
Скоро: интерактивная версия откроется в JupyterLite на
/lab/(Phase 3c). Пока — статически отрендеренная.
import numpy as npimport matplotlib.pyplot as pltfrom dataclasses import dataclass
DEFAULT = {"pInit": 0.2, "pTransit": 0.1, "pSlip": 0.1, "pGuess": 0.2}Модель в 4 функциях
Заголовок раздела «Модель в 4 функциях»def p_solve(pL, params=DEFAULT): return pL * (1 - params["pSlip"]) + (1 - pL) * params["pGuess"]
def bkt_update(pL, observed_correct, params=DEFAULT): pS, pG, pT = params["pSlip"], params["pGuess"], params["pTransit"] if observed_correct: post = (pL * (1 - pS)) / (pL * (1 - pS) + (1 - pL) * pG) else: post = (pL * pS) / (pL * pS + (1 - pL) * (1 - pG)) return post + (1 - post) * pT
def closeness(p, target=0.7, sigma2=0.03): return np.exp(-((p - target) ** 2) / sigma2)
def joint_p_solve(pLs, params=DEFAULT): """Geometric mean of per-skill P(solve).""" p = np.array([p_solve(x, params) for x in pLs]) return np.exp(np.mean(np.log(np.clip(p, 1e-6, 1.0))))Это — вся математика BKT. Меньше 20 строк.
Sanity check vs TS
Заголовок раздела «Sanity check vs TS»Воспроизведём пошаговый пример из главы 7:
Иван начинает с , делает 6 ответов в порядке ✗✓✓✗✗✗,
ожидаемый финальный .
pL = 0.2trace = [pL]for ans in [False, True, True, False, False, False]: pL = bkt_update(pL, ans) trace.append(round(pL, 4))
print(trace)# Ожидаем (с округлением):# [0.2, 0.057, 0.247, 0.595, 0.196, 0.116, 0.166]Цифры точно те же, что у нас в TS-реализации и в учебнике.
Визуализируем траекторию
Заголовок раздела «Визуализируем траекторию»fig, ax = plt.subplots(figsize=(8, 4))ax.plot(trace, marker='o', color='#9333ea', linewidth=2)ax.axhline(0.7, color='orange', linestyle='--', label='ZPD target')ax.set_xlabel('шаг')ax.set_ylabel('P(L)')ax.set_ylim(0, 1)ax.set_title("P(L) Ивана — навык 'раскрытие скобок'")ax.legend()ax.grid(alpha=0.3)plt.show()P(solve) как функция P(L)
Заголовок раздела «P(solve) как функция P(L)»xs = np.linspace(0, 1, 100)ys = [p_solve(x) for x in xs]
plt.figure(figsize=(8, 4))plt.plot(xs, ys, color='#0ea5e9', linewidth=2)plt.axhline(0.7, color='orange', linestyle='--', label='ZPD')plt.axvline(0.5, color='gray', linestyle=':', alpha=0.5)plt.fill_between(xs, 0.6, 0.8, alpha=0.15, color='orange', label='ZPD band')plt.xlabel('P(L)')plt.ylabel('P(solve)')plt.title('P(solve) = P(L)·(1−P(S)) + (1−P(L))·P(G)')plt.legend()plt.grid(alpha=0.3)plt.show()При дефолтах прямая из (0, 0.2) в (1, 0.9). → (попадание в ZPD).
Что в TS-реализации проверяется этим ноутбуком
Заголовок раздела «Что в TS-реализации проверяется этим ноутбуком»| Функция | TS | Python | Совпадает? |
|---|---|---|---|
pSolve(pL) | web/lib/bkt.ts:29 | p_solve | ✓ |
bktUpdate(pL, c) | web/lib/bkt.ts:33 | bkt_update | ✓ |
closeness(p) | web/lib/bkt.ts:120 | closeness | ✓ |
joint pSolve (GM) | web/lib/bkt.ts:111 | joint_p_solve | ✓ |
То есть весь TS-код можно переписать в Python в 30 строк — и наоборот. Это и есть простота BKT, главное преимущество.
Что дальше
Заголовок раздела «Что дальше»- NB-2 — Parameter sensitivity — что ломается при «плохих» параметрах.
- NB-3 — EM fitting (скоро).
- NB-4 — IRT vs BKT (скоро).
- NB-5 — Class simulation (скоро).