Машинное обучение в человеческом обучении. Развитие проекта RuLearn
Уже больше года я занимаюсь проектом RuLearn. Это довольно большое мобильное приложение на ~10000 строчек кода, которое реализует метод интервальных повторений, об истории проекта можно прочитать в моих предыдущих публикациях 1 и 2. Проект получился удачным, и даже побывал в числе победителей школьного московского конкурса «Инженеры будущего». Школьного, потому что автор — школьник:)
За лето RuLearn в проекте многое изменилось, и сейчас я хочу зафиксировать результат, связанный с добавлением машинного обучения. Сейчас, когда модель готова и можно будет опять переключиться на программирование мобильной части, важно записать, что было сделано. Иначе потом и не вспомнишь.
Итак, интервальные повторения. Применяются в основном для изучения новых слов в иностранном языке. По мере изучения новых слов, вступает в действие кривая забывания Эббингауза. Поэтому если обучающему предлагать для повторения только те юниты, которые находятся на грани забывания, можно добиться повышения эффективности запоминания. У кривой Эббингауза фиксированные интервалы, и это, скорее всего, неправильно. Представьте разницу в изучении английского языка, с которым мы так или иначе сталкиваемся, даже когда его не учим, и, к примеру, китайского. Очевидно, что учить иероглифы и составлять из них слова сложнее, чем запомнить значение слова «cringe». С другой стороны, даже в рамках английского языка, слово «lugubrious» запомнить гораздо сложнее, чем слово «loot». Оценить различия в сложности языков, в принципе, задача невыполнимая. Очевидно, что арабский будет даваться жителю России проще, если он живет в мусульманском регионе. Финский как-то проще заходит жителям Питера итд. Иврит, если вы не сталкивались с ним раньше, сломает не только OpenCSV в вашем коде, но и ваш мозг.
Задача сложности внутри языка тоже не так проста, как кажется. Например, есть такой проект, в котором носителям английского давали в тестах оценить сложность для восприятия различных слов. Аналогичные исследования проводились всего несколько раз за последние десятилетия, потому что это очень дорого и довольно сложно. Понятно, что изучение сложности иностранных языков для «не носителей» вряд ли кто-то будет проводить вообще. В какой-то степени сложность внутри языка отражают частотные словари. Наиболее часто употребляющиеся слова должны быть, скорее всего, проще для восприятия как для носителей, так и для изучающих язык как иностранный.
И тут, как во время любой презентации новых гаджетов в 2024 году, самое время вспомнить про искусственный интеллект и машинное обучение. Если набрать датасет с данными, насколько успешно наш ученик отвечает на вопросы в программе интервальных повторений, выявить закономерности и динамически менять интервалы повторений Эббингауза, то задача будет решена!
Идея хорошая, но в начале мая я написал на Хабре:
Как это сделать практически, мне пока что непонятно. То есть ясно, что это тема из Natural Language Processing, но не совсем. Как использовать эту статистику? Как использовать то, что обычно делается на Python, в мобильнике? Одни вопросы. Хорошо бы найти научного руководителя.
Но научный руководитель не нашелся. Пришлось делать самому. Для начала добавил таблицу в базе данных, куда записывалась статистика ответов по одному из курсов в RuLearn. Что можно записать? Да все возможное! Идентификатор слова — отлично, потому что курс построен на базе частотного словаря, поэтому чем он больше, тем сложнее слово. Рейтинг до и после ответа (рейтинг повышается при правильном и сбрасывается при неправильном). Таймстемп. Время, потраченное на ответ. Число использованных подсказок. Тип теста. И… все — больше данных не осталось.
CREATE TABLE 'machine_learning'(id int, rating_before int, rating_after int,
time_to_answer int, test_type int, timestamp int,
correct int, hint_used int)
После того, как это стало понятно, отпал вопрос про Natural Language Processing. Это просто не нужно, у нас на входе готовые цифры. То есть обычная регрессия или классификация уровня «Hello world» для начинающих в Data Science.
Начинающие в Data Science пользуются готовыми датасетами, и это очень скучно. У меня будет свой, ура, но тут начались проблемы. В идеале, нужно было бы собирать статистику с нуля. Но среди семьи и знакомых не нашлось никого, кто бы вдруг решил учить что-то новое, поэтому пришлось пойти на компромисы и начинать сбор с середины курса с надеждой, что потом можно будет все исправить. За 2 месяца удалось набрать около 5000 ответов со статистикой и я решил, что можно начинать строить модель.
4703 ответа на начало августа, кто-то очень хорошо поработал :)
Оказалось, что для того, чтобы перенести в дальнейшем модель в мобильное приложение, нужно сразу использовать TensorFlow. А это инструмент для «настоящих профессионалов», и все простые обучалки с сайта TensorFlow давно выпилили. Но к счастью, интернет помнит все, и нашлелся классический python notebook для классификации цветков ириса. Задача похожая, и даже более простая — у меня классификация по вычисляемому полю correct (1=ответ правильный, 0=ответ неправильный), ну, а у ирисов целых 3 вида. Соответственно, меняем в модели loss = categorical_crossentropy на binary_crossentropy, модифицируем код и… модель не работает.
Нет, ну работает, конечно, правильно в 85% случаев, а то и выше. Потому что в статистике и так 85% правильных ответов. Поэтому модель быстро понимает, что ответ »1» дает отличный accuracy_score, и делает ничего больше. Я менял датасеты, исключая старые данные, оставляя слова, выученные с нуля после начала сбора статистики. Модель показывала лучшие результаты во время обучения, но как только я проверял это на полном наборе, все разваливалось. Я постепенно увеличивал сет для обучения. Я делал стандартизацию данных и не понимал, как в дальшнейшем на сделать ее мобильном — ведь там StandardScaler отстутствует. Я даже стал лучше понимать Python:)
Это было не очень приятно. Хорошая идея никак не хотела реализоваваться. Книжки по Data Science не особо удачные. Например, Data Science Bookcamp (Leonard Apeltsin) начинается с объявления массива функций для пояснения идеи, которая и так понятна. Я дальше читать не стал, если для очевидных вещей автору понадобился такой синтаксис, то наверное, я дальше не осилю. Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow (Aurélien Géron) — со второй главы вводит в такие дебри стратификации, из которых не хочется возвращаться. Но после него я хотя бы догадался посмотреть на корелляцию в моем сете:
result 1.000000
id -0.012985
ts -0.056333
lvl -0.074329
Очевидно, что регрессия здесь не поможет. К этому времени иллюзия, что машинное обучение выявит зависимости само по себе, без подсказок, у меня прошла. Я решил, что стоит преобразовать данные так, чтобы мне самому можно было бы понять, какая логика стоит за изменениями показателей. Например, результаты должны ухудшаться по мере увеличения сложности слов (то есть с ростом id). Или результаты должны быть лучше через минимальное время после первоначального обучения и падать в дальнейшем. Кроме того, классификация тоже неудачная идея. Мы не можем доверять модели на 100% (даже на 50%), поэтому стоит подстраховаться. Нужно вычислять вероятность, с которой мы получим правильный ответ и исходя из нее менять интервал для повторения. Даже совсем плохая модель не сможет сломать эту логику, если учитывать естественные ограничения (делить на 0 нельзя, вероятность > 1 быть не может). Итак, преобразуем данные. Создадим таблицу, в которой будут следующие поля:
id, номер слова
n_repeat: общее число повторений для этого id
cur_rating: рейтинг слова (это число, которое меняется от 10 до 15 для выученного слова, в зависимости от правильности ответов. При правильном ответе рейтинг увеличивается и соответственно, увеличивается интервал, после которого его нужно повторять. Рейтинг 0–9 используется во время изучения слова. 0 — слово никогда не показывалось ученику, 9 — почти выучено. При этом слово не считается «выученным» и не входит в статистику для машинного обучения).
s_lapsed: время, прошедшее с предыдущего повторения. Проблема в том, что я собирал значения timestamp. Поэтому потребуются преобразования и для тех единиц, где было всего одно повторение, возьмем за начальную минимальную дату по курсу, в данном случае 1715405940918, 11 мая 2024 года.
остальные параметры (type_repeat и n_hint) не имеют значения для построения модели на данном этапе. Поскольку в курсе использовался только один вид повторений, нет смысла учитывать это поле и число подсказок. Но в базе на всякий случай эти поля предусмотрим.
И вот что получается:
insert into ar5000ML_ml
WITH T as
(
SELECT id, count(id) as n_repeat, rating_before as cur_rating, MAX(timestamp) as max_ts, sum(correct) as sum_correct
FROM machine_learning
group by id
)
SELECT id, cur_rating, n_repeat, sum_correct, 1 as type_repeat, (max_ts - ifnull((SELECT MAX(timestamp)
FROM machine_learning
WHERE timestamp < T.max_ts and id = T.id),1715405940918))/1000 as s_lapsed, 0 as n_hint
FROM T
Корелляция по этому сету гораздо лучше:
result 1.000000
cur_rating 0.530378
s_lapsed 0.524887
id -0.190360
n_repeat -0.697639
Дальше по образцу с сайта TensorFlow делаем модель, обучаем ее и смотрим на результаты:
Mean squared error is: 0.003626453253135609
R2 score is: 0.6607994723531427
На мой взгляд, удовлетворительно. Попробуем визуализировать, меняя по одному параметру за раз и выставляя оставшиеся на средних значениях и попробуем понять, насколько адекватно работает модель с точки зрения обычной логики.
Зависимость очень логична — чем сложнее слово, тем меньше вероятность правильного ответа.
Чем больше проходит дней, тем хуже мы помним слово — логично! Есть перекос с вероятностью 1.3, но в приложении просто не может быть ситуации, когда статистика бы собиралась через 0 минут поле предыдущей.
Число повторений растет при сложных словах. 5 повторений — это значит, что рейтинг от 10 до 15 пройден без одной ошибки — слово железно выучено. Для некоторых же слов даже 30 повторений недостаточно — просто не запомниаются и все.
Что это???
Самый сложный для объяснения результат. Рейтинг 10 означает, что слово мы только что выучили и лучше всего его помним. Вероятность правильного ответа снижается к 13–14 рейтингу. Это значит, что интервалы Эббингауза для данного курса были изначально неправильными, их нужно было бы сократить. Рост вероятности при рейтинге 15 — следствие наличия давно и прочно выученных слов, которые показывались всего один раз за все время наблюдения. То есть, если я бы начал учить шведский язык, и собирал статистику с самого начала, этого «хвоста» бы не было.
Итак, модель готова. В следующей статье буду добавлять ее в мобильное приложение. К сожалению, эта часть вообще как-то странно документирована для разработчиков на Android, поэтому важно не забыть самому, как я это сделал :)