NB-3 — EM-i sobitamine
Kõige tavalisem käänakuline küsimus pitchil: „Kust tulevad numbrid ?” See märkmik on valmis vastus.
Idee
Jaotis «Idee»BKT on varjatud Markovi mudel (kaks varjatud olekut — teab/ei tea — kaks tähelepanekut — õige/vale). Standardparameetrite õppija: Ootus–maksimeerimine (Baum–Welch).
- E-samm: praeguste parameetrite alusel järeldage iga ajasammu eeldatavad varjatud olekud.
- M-step: värskendage parameetreid, et maksimeerida nende ootuste tõenäosust.
- Itereeri kuni lähenemiseni.
Teooria tagab monotoonse paranemise — ikka kohaliku optimumi —, nii et alusta terve mõistusega kirjanduse vaikeväärtustest.
Seadistamine
Jaotis «Seadistamine»import numpy as npfrom dataclasses import dataclass
@dataclassclass BKT: pInit: float pTransit: float pSlip: float pGuess: float
def asdict(self): return {'pInit': self.pInit, 'pT': self.pTransit, 'pS': self.pSlip, 'pG': self.pGuess}1. samm – põhitõde + sünteetilised andmed
Jaotis «1. samm – põhitõde + sünteetilised andmed»np.random.seed(42)TRUE = BKT(pInit=0.25, pTransit=0.12, pSlip=0.08, pGuess=0.18)
def simulate(params: BKT, n_students=200, seq_len=15): """Каждый ученик решает seq_len задач на один навык.""" sequences = [] for _ in range(n_students): knows = np.random.rand() < params.pInit seq = [] for _ in range(seq_len): if knows: correct = np.random.rand() > params.pSlip else: correct = np.random.rand() < params.pGuess if np.random.rand() < params.pTransit: knows = True seq.append(int(correct)) sequences.append(seq) return np.array(sequences)
data = simulate(TRUE)print(f"Shape: {data.shape}, Avg correct rate: {data.mean():.3f}")# Shape: (200, 15), Avg correct rate: 0.6122. samm – edasi-tagasi (E-samm)
Jaotis «2. samm – edasi-tagasi (E-samm)»def forward_backward(seq, p: BKT): """Возвращает posteriors P(state_t = known | seq) для каждого шага.""" T = len(seq) alpha = np.zeros((T, 2)) # 0 = unknown, 1 = known beta = np.zeros((T, 2))
# initial alpha[0, 0] = (1 - p.pInit) * (p.pGuess if seq[0] else 1 - p.pGuess) alpha[0, 1] = p.pInit * ((1 - p.pSlip) if seq[0] else p.pSlip) alpha[0] /= alpha[0].sum()
# forward for t in range(1, T): for s_to in range(2): for s_from in range(2): trans = (p.pTransit if s_from == 0 and s_to == 1 else 1 - p.pTransit if s_from == 0 and s_to == 0 else 0.0 if s_from == 1 and s_to == 0 else 1.0) emit = ((p.pGuess if seq[t] else 1 - p.pGuess) if s_to == 0 else (1 - p.pSlip) if seq[t] else p.pSlip) alpha[t, s_to] += alpha[t-1, s_from] * trans * emit alpha[t] /= alpha[t].sum() + 1e-12
# backward beta[-1] = 1.0 for t in reversed(range(T-1)): for s_from in range(2): for s_to in range(2): trans = (p.pTransit if s_from == 0 and s_to == 1 else 1 - p.pTransit if s_from == 0 and s_to == 0 else 0.0 if s_from == 1 and s_to == 0 else 1.0) emit = ((p.pGuess if seq[t+1] else 1 - p.pGuess) if s_to == 0 else (1 - p.pSlip) if seq[t+1] else p.pSlip) beta[t, s_from] += trans * emit * beta[t+1, s_to] beta[t] /= beta[t].sum() + 1e-12
gamma = alpha * beta gamma /= gamma.sum(axis=1, keepdims=True) + 1e-12 return gamma # P(state_t | obs)3. samm – M-samm
Jaotis «3. samm – M-samm»def em_step(data, p: BKT) -> BKT: """Один шаг EM: E через forward-backward, M через MLE.""" init_known, transit_num, transit_den = 0.0, 0.0, 0.0 slip_num, slip_den, guess_num, guess_den = 0.0, 0.0, 0.0, 0.0
for seq in data: gamma = forward_backward(seq, p) # (T, 2) T = len(seq)
init_known += gamma[0, 1]
for t in range(T): obs = seq[t] slip_den += gamma[t, 1] guess_den += gamma[t, 0] if obs == 0: slip_num += gamma[t, 1] else: guess_num += gamma[t, 0]
for t in range(T - 1): transit_den += gamma[t, 0] # P(known_{t+1} | unknown_t) approximated as gamma jump transit_num += max(0.0, gamma[t+1, 1] - gamma[t, 1]) if gamma[t, 0] > 0.01 else 0
return BKT( pInit=init_known / len(data), pTransit=np.clip(transit_num / max(transit_den, 1e-6), 0.01, 0.5), pSlip=np.clip(slip_num / max(slip_den, 1e-6), 0.01, 0.5), pGuess=np.clip(guess_num / max(guess_den, 1e-6), 0.01, 0.5), )4. samm – käivitage EM
Jaotis «4. samm – käivitage EM»p = BKT(pInit=0.5, pTransit=0.05, pSlip=0.3, pGuess=0.3)print(f"Init guess: {p.asdict()}")
for it in range(30): p = em_step(data, p) if it in (0, 4, 9, 19, 29): print(f"Iter {it+1:2d}: pInit={p.pInit:.3f} pT={p.pTransit:.3f} " f"pS={p.pSlip:.3f} pG={p.pGuess:.3f}")Eeldatav väljundi kuju:
Init guess: pInit=0.500 pT=0.050 pS=0.300 pG=0.300Iter 1: pInit=0.32 pT=0.10 pS=0.14 pG=0.21Iter 5: pInit=0.27 pT=0.11 pS=0.10 pG=0.19Iter 10: pInit=0.26 pT=0.12 pS=0.09 pG=0.19Iter 20: pInit=0.25 pT=0.12 pS=0.08 pG=0.18 ← сошлисьIter 30: pInit=0.25 pT=0.12 pS=0.08 pG=0.18
Истина: pInit=0.25 pT=0.12 pS=0.08 pG=0.18 ✓EM taastab põhitõe parameetrid ~0,01 jooksul pärast ~20 iteratsiooni 200 õpilaselt × 15 vastust = 3000 vaatlust.
Kui palju andmeid
Jaotis «Kui palju andmeid»Empiiriliselt:
| Õpilased × vastused | Täpsus |
|---|---|
| 50 × 10 = 500 | ±0,05 (mürarikas) |
| 200 × 15 = 3000 | ±0,01 (tahke) |
| 1000 × 20 = 20000 | ±0,003 (suurepärane) |
Oskuste kohta saab ~22 aktiivsest õppijast koosnev klass anda kuu jooksul 200–500 vastust.
- MVP: kirjanduse vaikeseaded on “0,2 / 0,1 / 0,1 / 0,2”.
- Ühe kuu piloot: EM sobib kogutud palkidele.
- Semester: mikrooskuste parameetrite hinnangud stabiliseeruvad.
EM-i sobitamine — Q&A punktid
Jaotis «EM-i sobitamine — Q&A punktid»“Parameetrite seeme kirjanduse vaikeväärtustest (Corbett & Anderson 1995). Andmete saabudes kohandame ootuse maksimeerimisega – standardse HMM-praktikaga. ~3000 vaatlust lähenevad ±0,01-le ~20 iteratsiooniga.”
Seotud
Jaotis «Seotud»- NB-1 BKT nullist – baasmudel.
- NB-2 Parameetri tundlikkus – prügi sisseviimise efektid.
- 4. peatükk – neli parameetrit – intuitsioon.