Перейти к содержимому

Обзор пайплайна — от ответа до рекомендации

Эта страница — компактный «бирд-вью» всего, что делает модель: что поступает на вход, какие 4 числа на каждый микро-навык, как один ответ ученика обновляет оценку владения и как мы выбираем ему следующую задачу. Подходит как точка входа в учебник или как шпаргалка для разговора с командой.

Все термины ниже — аббревиатуры или заимствования из английского. Чтобы не вспоминать их каждый раз, вот декодер:

СокращениеАнглийское полноеПо-русски
BKTBayesian Knowledge Tracingбайесовское отслеживание знания
EMExpectation–Maximizationалгоритм «ожидание–максимизация»
HMMHidden Markov Modelскрытая марковская модель
ZPDZone of Proximal Developmentзона ближайшего развития (Выготский)
P(L)Probability(Learned)вероятность, что ученик владеет навыком
P(L₀) / pInitprior P(Learned), t=0t=0априорная P(L) до первой попытки
pTprobability of Transitвероятность научиться за одну попытку
pSprobability of Slipвероятность ошибиться, зная (slip = «промах»)
pGprobability of Guessвероятность угадать, не зная
P(solve)Probability(Solve)вероятность правильно решить задачу
closeness(буквально «близость»)насколько P(solve) близко к target ≈ 0.7
mastery(буквально «владение»)численное P(L) ученика по всем навыкам
microskillmicro-skillатомарный навык, на нём считается один P(L)
prereqprerequisiteпредшественник навыка в графе зависимостей
rarity bonusbonus for rare skillsбонус за «недотренированные» навыки в задаче
Baum–Welch(фамилии авторов)конкретный вариант EM для HMM

Дальше в тексте используются короткие формы — возвращайся в эту таблицу при первой встрече с любым «pS» или «pT».

flowchart LR
classDef off fill:#fde68a,stroke:#a16207,color:#0f172a
A1[Лог]:::off --> A2[EM]:::off --> A3[Параметры]:::off
ШагЧто это и зачем
ЛогВсе ответы всех учеников за всё время — (ученик, задача, верно/неверно, время). Десятки тысяч строк в БД. Это вход для офлайн-этапа.
EMАлгоритм машинного обучения, который смотрит на лог и подбирает 4 числа BKT для каждого микро-навыка так, чтобы эти числа лучше всего объясняли, как ученики на практике отвечали. Запускается один раз перед запуском платформы (потом раз в N недель), не во время сессии ученика. На профессиональном жаргоне конкретный вариант EM, применяемый к BKT, называется Baum–Welch — это частный случай EM для скрытых марковских цепей.
ПараметрыГотовые 4 числа {pinit,pT,pS,pG}\{p_{init}, p_T, p_S, p_G\} на каждый из 9 микро-навыков — выход EM. «Зашиваются» в онлайн-движок до следующего цикла фитинга. На хакатоне используем литературные дефолты (0.2,0.1,0.1,0.2)(0.2, 0.1, 0.1, 0.2) для всех навыков, в v2 — реальные числа из EM.

Параметры передаются онлайн-движку и живут там до следующего цикла фитинга.

flowchart LR
classDef onl fill:#bbf7d0,stroke:#15803d,color:#0f172a
classDef sel fill:#e9d5ff,stroke:#7e22ce,color:#0f172a
B1[applyAttempt]:::onl --> B2["P(L)"]:::onl --> B3["P(solve)"]:::onl --> B4[ZPD-скор]:::sel --> B5[Top-N]:::sel
ШагЧто это и зачем
applyAttemptФункция, которая на каждый ответ ученика обновляет его P(L) для каждого микро-навыка, помеченного в задаче. Принимает (текущий P(L), верно/неверно, параметры BKT).
P(L)«Mastery-вектор» ученика: для каждого из 9 микро-навыков — число от 0 до 1, отражающее уверенность модели «знает ли ученик этот навык». Обновляется на каждом ответе.
P(solve)Вероятность правильно решить конкретную задачу прямо сейчас. Считается из P(L) ученика по микро-навыкам задачи (геом. среднее) и тех же параметров BKT.
ZPD-скорФинальный приоритет задачи. Складывается из «насколько P(solve) близок к 0.7» (closeness) и «сколько недотренированных навыков она задействует» (rarity bonus).
Top-NЛучшие N задач по ZPD-скору. Первая попадает ученику, остальные — резерв (на случай «недавно решал», см. §5).

Две фазы. Офлайн — медленная: раз в N недель прогоняем EM по большому архиву ответов и достаём 4 числа на каждый микро-навык. Онлайн — быстрая: на каждый клик «Готово» ученика обновляем один P(L) и пересчитываем рекомендацию.

ПолеТипПримерОткуда
student_idstring"u_142"сессия / БД пользователей
masteryRecord<skillId, number>{ "define.t1.add": 0.31 }накапливается онлайн
historyAttemptRecord[]см. нижежурнал ответов
task.idstring"q_007"пул задач
task.microskillsstring[]["define.t2.mix"]разметка задач
task.difficultynumber ∈ [0,1]0.55оценка автора (tie-breaker)

Пример AttemptRecord (как фиксируется один ответ):

{
"task_id": "q_007",
"correct": true,
"ts": "2026-05-07T18:42:11Z",
"per_skill": { "define.t2.mix": true }
}

Пример Task:

{
"id": "q_007",
"topic": "linear",
"microskills": ["define.t2.mix"],
"difficulty": 0.55,
"prompt_et": "Pille on 3 aastat vanem kui Mart…",
"answer": "x = 12"
}
ИмяСмыслДефолт
pinitp_{init}Априорная P(знал до первой попытки)0.20
pTp_TВероятность научиться за одну попытку0.10
pSp_SSlip — знал, но ошибся0.10
pGp_GGuess — не знал, но угадал0.20

Источник: packages/bkt-core/src/microskills.ts → DEFAULT_BKT.

Почему «консервативные». (0.2,  0.1,  0.1,  0.2)(0.2,\;0.1,\;0.1,\;0.2) — это литературные дефолты для школьной математики (Корбетт & Андерсон, 1995, и более поздние работы). Они не агрессивны: модель не делает резких выводов после одного ответа. pT=0.1p_T = 0.1 — медленное обучение (один ответ редко «выучивает»); pS,pG[0.1,0.2]p_S, p_G \in [0.1, 0.2] — толерантность к случайным промахам и угадываниям. Это страховка от перепрогноза «одна правильная попытка → выучил».

Почему фиксированы на хакатоне. Правильный фит требует ~3000 наблюдений на каждый навык, и EM-пайплайн в проде ещё не подключен. Литературные дефолты дают разумное стартовое поведение «из коробки», без необходимости что-то фитить.

Что изменится в v2. Запустим EM-fitting (подробно — в §6 «Откуда берутся параметры») на собранных данных и получим свои 4 числа на каждый из 9 микро-навыков — но всё ещё общие для всех учеников.

Для одного микро-навыка задачи:

P(Lcorrect)=P(L)(1pS)P(L)(1pS)+(1P(L))pGP(L \mid correct) = \frac{P(L) \cdot (1 - p_S)}{P(L) \cdot (1 - p_S) + (1 - P(L)) \cdot p_G} P(Lwrong)=P(L)pSP(L)pS+(1P(L))(1pG)P(L \mid wrong) = \frac{P(L) \cdot p_S}{P(L) \cdot p_S + (1 - P(L)) \cdot (1 - p_G)}

затем шаг «обучения»:

P(Lnew)=posterior+(1posterior)pTP(L_{new}) = posterior + (1 - posterior) \cdot p_T

Реализация — bkt-core/src/bkt.ts:36-46 (bktUpdate). Для multi-skill задач это делается для каждого микро-навыка независимо (applyAttempt, строки 65–84).

Интерактив: BktSimulator — пощёлкать «ответил верно / не верно» и смотреть, как двигается P(L)P(L).

Для каждой задачи в пуле считаем «совместный» P(solve)P(\text{solve})геометрическое среднее P(solve)P(\text{solve}) по микро-навыкам (строже арифметического: одна слабая компонента сильнее тянет вниз):

P(solve)joint=exp ⁣(1ni=1nlogP(solve)i)P(solve)_{joint} = \exp\!\left(\frac{1}{n} \sum_{i=1}^{n} \log P(solve)_i\right)

Скор задачи:

score=exp ⁣((P(solve)joint0.7)20.03)closeness+0.15{s:P(Ls)<0.4}nrarityscore = \underbrace{\exp\!\left(-\frac{(P(solve)_{joint} - 0.7)^2}{0.03}\right)}_{closeness} + 0.15 \cdot \underbrace{\frac{|\{s : P(L_s) < 0.4\}|}{n}}_{rarity}

Гауссиана даёт пик на 0.7 (ZPD), rarity bonus подталкивает к задачам, где много «недотренированных» навыков.

Реализация — bkt-core/src/bkt.ts:102-135 (scoreTaskForStudent), top-N выбирается в recommend (строки 137–152) с исключением последних 5 task_id из истории.

  • P(L)=0.166P(L) = 0.166 для микро-навыка «скобки».
  • P(solve)P(solve) задачи на одни скобки: 0.1660.9+0.8340.2=0.3170.166 \cdot 0.9 + 0.834 \cdot 0.2 = 0.317.
  • Closeness: exp((0.3170.7)2/0.03)0.0073\exp(-(0.317 - 0.7)^2 / 0.03) \approx 0.0073 — почти ноль, не выбирается.
  • Multi-skill (скобки + знакомая арифметика): P(solve)[0.55,0.65]P(solve) \in [0.55, 0.65] — попадает в ZPD.

Полная иллюстрация ниже:

closeness = exp(−(p−target)²/σ²). Выше у пика, быстро падает к краям. Чем меньше σ², тем уже «ZPD-окно».

6. Откуда берутся параметры (EM-fitting, офлайн)

Заголовок раздела «6. Откуда берутся параметры (EM-fitting, офлайн)»

Цель — восстановить (pinit,pT,pS,pG)(p_{init}, p_T, p_S, p_G) из массива ответов. Алгоритм: EM (Baum–Welch) для скрытых марковских цепей (HMM) с двумя состояниями «знает / не знает».

Сам алгоритм:

  1. Берём массив наблюдений: ~3000 ответов на навык.
  2. Гадаем стартовые значения 4 параметров (например, литературные дефолты).
  3. E-шаг: при текущих параметрах считаем, какова вероятность каждой скрытой последовательности «знал / не знал» в каждый момент.
  4. M-шаг: переоцениваем параметры так, чтобы наблюдаемые данные стали наиболее вероятными.
  5. Повторяем 3–4, пока параметры не стабилизируются (~20 итераций).
ПунктЗначение
Объём данных на навык~3000 наблюдений
Итераций до сходимости~20
Точность параметра±0.01

Подробнее — NB-3 EM-fitting.

Короткий ответ — нет. Граф нарисован в UI для людей, но код селектора его не читает.

ЧтоГде живётКто заполняетИспользуется в коде?
DAG зависимостей навыков (t3.mix → t1.add, t2.mix…)data/matx-define/microskills.json, поле prereqавтор curriculum один раз❌ нет — только рендерится в UI
Метки задачи (task.microskills = ["t3.mix", "t1.add", …])data/matx-define/tasks.jsonучитель при добавлении каждой задачи✅ да — читается каждый запуск recommend()

В microskills.json граф зависимостей выглядит так:

define.t2.mix → prereq: [t1.add, t1.mul, t2.add, t2.mul] ← 4 предка
define.t3.mix → prereq: [t1.add, t1.mix, t1.mul, t2.add,
t2.mix, t2.mul, t3.add, t3.mul] ← 8 предков

Этот граф рисуется в виджете ProgressionMatrix.tsx для UI (см. в книге), но recommend() его в память не загружает.

Когда учитель добавляет задачу в банк, он:

  1. Пишет текст задачи и правильный ответ.
  2. Руками перечисляет в task.microskills все микро-навыки, которые эта задача задействует, включая предков.

Например, для задачи на t3.mix учитель указывает:

{
"id": "MD-15",
"microskills": [
"define.t3.mix",
"define.t2.mix",
"define.t1.add",
"define.t1.mul"
],
"prompt_et": "...",
"answer": "..."
}

То есть учитель сам разворачивает граф в плоский список при разметке. Из 20 задач в банке у нас 16 — multi-skill (см. таблицу ниже).

Селектор смотрит только в task.microskills и state.mastery. Когда он считает геом. среднее P(solve), слабый t1.add (например, P(L)=0.0 у новичка) тащит общий P(solve) к 0.2 → closeness уходит в ~0 → задача автоматически не выбирается.

То есть проверка prereq случается «бесплатно» — потому что предок уже в task.microskills и попадает в формулу.

  • Уязвимость к ошибкам разметки. Если учитель забыл добавить t1.add в задачу на t3.mix, модель не узнает, что у ученика пробел в основах — и может выдать слишком сложную задачу.
  • Нельзя написать явные правила вида «не показывать t3.mix, пока t2.mix не достигнет P(L)0.5P(L) \geq 0.5» — нет точки в коде, где такое условие можно вычислить без чтения графа.
  • UI не может подсказать «вот навыки, которые надо освоить до этой задачи», потому что нужная информация (DAG) в селекторе отсутствует.

Распределение задач по навыкам (из текущих данных)

Заголовок раздела «Распределение задач по навыкам (из текущих данных)»

9 микро-навыков, 20 задач. Multi-skill — 16 / 20:

Навыков в задачеЗадач
14
26
35
53
61
91

То есть 80% задач завязаны на 2+ навыка → геометрическое среднее действительно работает почти на каждой попытке.

Каждый вопрос — про дизайн-решение, на которое надо ответить с командой. Структура одна: «как сейчас → как могло бы → о чём говорить».

1. Будем ли в v2 учить код читать граф зависимостей?

Заголовок раздела «1. Будем ли в v2 учить код читать граф зависимостей?»

Сейчас: код смотрит только на список навыков задачи (task.microskills) и на P(L) ученика. Файл с графом «t3 нужен t1» лежит рядом, но код его не открывает.

Если бы читал: код сам бы знал «без t1.add задачу на t3.mix давать рано» — даже если учитель забыл указать t1.add в этой задаче.

Что отвечать команде:

  • Делаем явное правило вроде «t3 закрыт, пока t2 < 0.5»? Это даёт предсказуемость, но добавляет код и возможные конфликты с ZPD-логикой.
  • Или оставляем как сейчас (правило срабатывает «случайно» через геом. среднее)? Меньше кода, но завязано на дисциплину учителя.

2. Кто отвечает за разметку — учитель или код?

Заголовок раздела «2. Кто отвечает за разметку — учитель или код?»

Сейчас: учитель. Добавляя задачу, перечисляет вручную все навыки, которые она задействует, включая предков. Это рутина и место для ошибок.

Если бы помогал код: учитель указывает только «верхний» навык (например, t3.mix), а код через граф сам добавляет всех его предков (t2.mix, t1.add, t1.mul).

Что отвечать команде:

  • Доверяем дисциплине учителя? Тогда нужен инструмент валидации разметки (линтер).
  • Или доверяем графу? Тогда учтите — одна ошибка в графе тиражируется на все задачи разом.

3. Один комплект pT,pS,pGp_T, p_S, p_G — или свой на каждый навык?

Заголовок раздела «3. Один комплект pT,pS,pGp_T, p_S, p_GpT​,pS​,pG​ — или свой на каждый навык?»

Сейчас: все 9 микро-навыков делят одни и те же 4 числа (0.2,0.1,0.1,0.2)(0.2, 0.1, 0.1, 0.2). То есть «вероятность ошибиться по невнимательности» (slip) одинакова и для простой арифметики (t1.add), и для длинного трёхсоставного уравнения (t3.mix).

По жизни они разные: на длинной задаче ошибиться легче (slip выше). В простой угадать без знания почти невозможно (guess ниже). Идеально — свои 4 числа на каждый из 9 навыков.

Что отвечать команде:

  • Когда у нас будет ~3000 ответов на каждый навык, чтобы фитить EM-ом per-skill?
  • Стоит ли начать собирать данные с этой целью прямо сейчас?

4. Принудительное «закрытие пробелов» против ZPD

Заголовок раздела «4. Принудительное «закрытие пробелов» против ZPD»

Кейс: Маша зашла впервые. У неё P(L)=0P(L) = 0 на базовой арифметике t1.add.

Когда селектор смотрит на любую задачу с t1.add в навыках, геом. среднее даёт P(solve)0.2P(\text{solve}) \approx 0.2 → closeness ≈ 0 → задача отбрасывается. Остаются только чистые задачи на t1.add.

Это правильно образовательно — заставляем закрыть базу.

Но может быть скучно — если в банке всего 3 чистые задачи на t1.add, Маша решает их по кругу 6 раз. Демотивация.

Что отвечать команде:

  • Добавить «спасательный режим»: если ученик 5 раз подряд получает задачу на один навык, ZPD-окно автоматически расширяется и в пул попадают «соседние» задачи?
  • Или жёстко закрываем пробелы, даже ценой одинаковых задач?

5. Когда подключим MATx — будут навыки из разных тем

Заголовок раздела «5. Когда подключим MATx — будут навыки из разных тем»

Сейчас: все 9 наших навыков — из одной темы (define, моделирование задач).

После интеграции с MATx: добавятся 9 навыков из 3 других тем — protsendid (проценты), vorrandid (уравнения), abivalemid (вспомогательные формулы). Итого 18 навыков, 4 темы.

Появится новый класс задач — межтемные. Например, «текстовая задача на проценты, где надо составить уравнение» — это define.t2.mul (моделирование) + vorrandid.lihtsad (вычисление).

Что отвечать команде:

  • Считаем такую задачу одной формулой по всем навыкам (математически работает, но семантически странно)?
  • Или вводим параллельные треки и переключаемся между темами?
  • Балансируем ли рекомендации — «не больше 5 задач подряд из одной темы»?

6. Жёсткость ZPD-окна (σ2=0.03\sigma^2 = 0.03) — статика или динамика?

Заголовок раздела «6. Жёсткость ZPD-окна (σ2=0.03\sigma^2 = 0.03σ2=0.03) — статика или динамика?»

Что это число: в формуле скоринга задачи σ2\sigma^2 управляет тем, насколько близко P(solve)P(\text{solve}) должен быть к 0.7. Меньше σ² — уже окно (только задачи с P(solve)[0.65,0.75]P(\text{solve}) \in [0.65, 0.75] проходят). Больше σ² — шире.

Сейчас: σ2=0.03\sigma^2 = 0.03 (статика). Допускает P(solve)[0.55,0.85]P(\text{solve}) \in [0.55, 0.85].

Если бы σ² двигалась:

  • Новичок только пришёл — σ2=0.10\sigma^2 = 0.10 (широкое окно: «любая задача, дающая хоть какой-то прогресс — годится»).
  • Опытный ученик — σ2=0.01\sigma^2 = 0.01 (узкое окно: «строго в зону роста, без скучной середины»).
  • После серии правильных — сужаем (поднимаем планку), после серии неправильных — расширяем (даём передышку).

Что отвечать команде:

  • Стоит ли автоматизировать, или оставить ручкой для учителя?
  • Если автоматизировать — на каких сигналах основываться (длина серии, средний P(L), скорость роста)?
  • Типы и параметры — packages/bkt-core/src/microskills.ts
  • Update / select — packages/bkt-core/src/bkt.ts
  • Skill-граф — data/matx-bridge.json
  • Виджеты-симуляторы — study-guide/src/widgets/