Duolingo на минималках

Привет, меня зовут Емельянов Михаил, я Python-программист и я хотел бы показать вам свой небольшой «проект выходного дня» — Flywheel, микро-платформу для изучения иностранных языков — смесь Duolingo и Anki, программу, которая может помочь вам правильно писать на английском. Flywheel доступен в исходниках, лежит на GitHub.

Flywheel

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

Если вы когда-нибудь пользовались Duolingo, то имеете представление о формате, в котором будет идти обучение. Последовательность проста: вот тебе фраза, переведи её на другой язык; программа запомнит, когда ты в последний раз переводил ту или иную фразу и насколько успешно у тебя это получилось; в зависимости от правильности ответа будет определено время, когда тебе нужно задать эту же фразу еще раз. В целом, на мой взгляд, как сам Duolingo, так и используемый им подход — просто гениальны. Но… Есть нюансы, которые несколько портят впечатления от процесса учёбы, и именно для их устранения я и задумал Flywheel.


Хотелки

Во-первых, и это самое главное, я хотел бы, чтобы все задания на перевод были только русско-английскими. Я хочу видеть только русские фразы, которые мне нужно перевести на английский. Переводить с английского на русский не хочу. Я не учусь на переводчика, я хочу изучить иностранный язык! А для этого гораздо правильнее, на мой взгляд, вообще не включать русскоязычную раскладку на клавиатуре во время учёбы. На Duolingo есть небольшой «лайфхак» — переключение с изучения английского для русскоговорящих на изучение русского для англоговорящих (этим, отчасти, и объясняется большое количество учащихся на этом курсе — в основном это вовсе не американцы или жители туманного Альбиона, изучающие русский, а как раз наоборот, жители России, зубрящие английский язык), тогда учебный курс будет содержать больше русско-английских заданий, но количество англо-русских переводов всё равно останется очень большим. А я хочу 100% времени урока писать на английском!

Во-вторых, я взрослый человек, и мне совершенно не нужна геймификация процесса обучения. Всё эти человечки, весело подмигивающие, приободряющие и дающие советы — один сплошной жирный, неуместный и раздражающий Круциатус. Доходит даже до выпуска расширений для браузера, которые пытаются вырезать весь этот ненужный функционал, сведя визуал сайта до необходимого уровня минимализма.

Третье — я взрослый человек (повторяюсь, да) и у меня иногда нет времени на полноразмерный урок. Хотя на Duolingo он довольно короток, но, тем не менее, разбивка процесса обучения на фиксированный уроки по… надцать вопросов удобна в первую очередь для обучающей платформы, а не для ученика. Я хочу, чтобы у меня была возможность повтора не двадцати фраз, а, скажем, пяти или трёх, даже одной, наконец. Хочу прерывать процесс обучения в любой момент без потери прогресса! В конце концов, я иногда могу заниматься только в редкие перерывы недетерминированной продолжительности между моими основными активностями, под чай с печенькой, или в передышке между общением с детьми. Если у меня есть буквально свободная минута, то я хочу сделать пару подходов и сохранить свой прогресс.

В-четвертых, хочу иметь возможность всегда, на любом этапе обучения добавлять новые фразы! Услышав или вычитав что-то новое, полезное или просто интересное, хочу добавить фразу в список, пусть теперь программа позаботится о том, чтобы я эта фраза осталась в моей памяти навсегда.

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

Вот такой вот список пожеланий у меня накопился — хочу Duolingo, но только с русско-английскими заданиями, без геймификации, с сохранением прогресса после каждого задания, с возможностью добавлять новые фразы и с визуализацией сделанных ошибок, даже мелких.

Думаю, на этом предисловие можно закончить и перейти к сути. Если вы просто хотите начать учить английский язык — переходите к следующему разделу, «Использование». Если вы хотите посмотреть, как программа устроена «под капотом», то переходите разделу «Как это работает» (ближе к концу статьи).


Использование

Использование Flywheel предельно просто. В самом начале у вас есть всего один файл — phrases.txt (в файле, идущем вместе с программой, около двух тысяч фраз). Там вы можете видеть множество фразовых пар, просто разделенных двойной вертикальной чертой, например:

Я люблю тебя || I love you

Если русскую фразу можно корректно перевести несколькими разными английскими фразами, то для их разделения используется одиночная вертикальная черта:

Я живу в этом городе || I live in this city | I live in this town

Наконец, если существуют две русскоязычные фразы, которые тоже могут иметь множество эквивалентных переводов, то для их разделения также используется одиночная вертикальная черта:

Кот сидит на столе | Кошка сидит на столе || A cat sits on the table | The cat sits on the table

Последнюю фразовую пару можно еще немного усложнить:

Кот сидит на столе | Кошка сидит на столе || A cat sits on the table | The cat sits on the table | A cat is sitting on the table | The cat is sitting on the table

Разумеется, в phrases.txt можно и нужно добавлять собственные фразовые пары. В этом-то и состоит самый цимес Flywheel — не обязательно зубрить то, что содержится в словаре, это просто заготовка. Корректируйте содержание уроков под свой уровень владения языком; перемещайте наиболее полезные, на ваш взгляд, фразовые пары повыше в словаре; добавляйте пары, связанные с вашей профессиональной деятельностью. Не говоря уже о том, что оболочке всё равно, какой язык вы учите. Хотите изучать испанский — bienvenido! Хотите изучать алеутский — да не вопрос. Хотите изучать алеутский, будучи носителем испанского? Легко!

Пожалуйста, не добавляйте в словарь слова! Разумеется, технически это возможно, но с точки зрения эффективности изучения языка не очень оправдано. Постарайтесь добавлять именно фразы, а если хотите добавить в свою личную копилку какое-нибудь конкретное новое слово, лучше возьмите фразу, где оно применяется в конкретном контексте. Так вы не только лучше запомните это слово, но и легче переведете его из пассивной фазы в активную — будете не просто распознавать его в тексте или в устной речи, но и начнете применять его при письме и при говорении.

Теперь просто запустите flywheel.py. В папке с программой появятся еще два файла — repetitions.json (здесь будет записан ваш прогресс и степень запоминания всех пройденных фразовых пар) и user_statistics.txt (здесь будет записано общее количество сделанных вами упражнений и будет сформирован общий список слов, которые вы успели изучить).


Небольшое отступление

Позволю себе крошечную ремарку, которая была бы несколько неуместна ни в начале статьи (когда вы только взвешивали её полезность) ни, тем более, в конце (чтобы не портить послевкусия). Раз уж вы, дойдя до секции с кодом, невольно верифицировали себя как представитель программной индустрии, то, следовательно, есть некоторая ненулевая вероятность того, что вы ищете нового сотрудника. Дело в том, что я, автор этой статьи, ищу новую работу, и если вам нужен middle backend Python-программист, то, возможно, вас заинтересует моя кандидатура.

Вот малюсенькая ссылочка на мою коротенькую самопрезентацию;, а теперь давайте вернёмся в основное русло нашего повествования.


Как это работает

Если вы — начинающий Python-разработчик и хотите поточить зубки обо что-нибудь простенькое, но не бесполезное, попробуйте Flywheel. Возможно, вам удастся прикрутить к нему какую-нибудь убервостребованную фичу, а в процессе отладки еще и английский подтяните. Разумеется, большую часть методов, используемых в программе, описывать особого смысла не имеет, остановлюсь только на общем подходе и на ключевых функциях, имеющих непосредственное отношение к анализу прогресса пользователя.

В последнее время я стал практиковать следующий метод: пишу заготовку main, как будто бы все методы программы уже разработаны и мне просто осталось их вызвать. Это позволяет взглянуть на код с высоты, так сказать, птичьего полёта (даже если высота полёта вызывает ассоциации скорее с пингвином, а не с орлом :) и оценить примерный уровень планируемых трудозатрат. В этот раз получилось следующее:

phrases_file_name = "phrases.txt"
repetitions_file_name = "repetitions.json"

if __name__ == "__main__":
    phrases_file_path = find_or_create_file(phrases_file_name)
    repetitions_file_path = find_or_create_file(repetitions_file_name)

    phrases = read_phrases(phrases_file_path)
    repetitions = read_repetitions(repetitions_file_path)
    can_work, error_message = data_assessment(phrases, repetitions)

    if can_work:
        message = merge(repetitions, phrases)
        print(message)
        while True:
            current_phrase = determine_current_phrase(repetitions)
            user_result = user_session(current_phrase)
            update_repetitions(repetitions, current_phrase, user_result)
            save_repetitions(repetitions_file_path, repetitions)
    else:
        print(error_message)
        exit()

Логика работы примерно такова:
• найдём в каталогах проекта файл phrases.txt (множество фразовых пар, разделенных двойной вертикальной чертой, подробности читайте в разделе «Использование»); если найти его не удалось, создадим пустышку для будущего редактирования пользователем;
• аналогично, поищем файл repetitions.json (записи прогресса и степень запоминания всех пройденных фразовых пар), если не нашли — создаем пустой файл;
• создаем структуры данных из информации, считанной из phrases.txt и repetitions.json, а потом оцениваем, можно ли работать с такой комбинацией. Не пустой phrases.txt — OK, мы сможем преобразовать фразовые пары в наш внутренний формат и переписать эту информацию в repetitions.json. Не пустой repetitions.json — тоже OK, можем работать с уже накопленной информацией. А вот две пустышки, и phrases.txt, и repetitions.json — уже не OK, нам просто неоткуда черпать информацию, необходимую для работы — жалуемся на этот факт пользователю, пусть создаст phrases.txt хоть с каким-то минимальным содержимым;
• в цикле подбрасываем пользователю новое задание, выбирая из фразового словаря ту фразу, которая наиболее актуальна на настоящий момент. Если есть фразы, требующие повторения, в первую очередь берем именно их; если все пройденные задания не требуют освежения памяти прямо сейчас, то начинаем подкидывать новые фразы.
• после каждого задания, вне зависимости от качества ответа, обновляем информацию в repetitions.json и статистику пользователя.

В процессе написания кода я разбил весь функционал на data_level (это, своего рода, квинтэссенция собственно языковой практики), system_level (функционал, зависящий от операционной системы) и ui_level (методы, определяющие способ взаимодействия с пользователем) плюс добавил файл статистики, отображающий общее количество «подходов», предпринятых пользователем, а также содержащий все как английские, так и русские слова, пройденные им в процессе обучения. Окончательный вариант получился примерно тем же самым, что и первоначальная заготовка, только чуточку разлапистее:

from data_level import DataOperations as dop
from system_level import FileOperations as fop
from ui_level import UiOperations as uop

phrases_file_name: str = 'phrases.txt'
repetitions_file_name: str = 'repetitions.json'
statistics_file_name: str = 'user_statistics.txt'

if __name__ == '__main__':
    phrases_file_path = fop.find_or_create_file(phrases_file_name)
    repetitions_file_path = fop.find_or_create_file(repetitions_file_name)
    user_statistics_file_path = fop.find_or_create_file(statistics_file_name)

    phrases: dict = fop.read_phrases(phrases_file_path)
    repetitions: dict = fop.read_json_from_file(repetitions_file_path)
    can_work, assesment_error_message = dop.data_assessment(phrases, repetitions)

    statistics: dict = fop.read_json_from_file(user_statistics_file_path)

    if can_work:
        is_merged, merge_message = dop.merge(phrases, repetitions)
        print(merge_message)
        if is_merged:
            fop.save_json_to_file(repetitions_file_path, repetitions)

        while True:
            current_phrase: str = dop.determine_next_phrase(repetitions)
            user_result, best_translation = uop.user_session(current_phrase, repetitions[current_phrase])

            dop.update_repetitions(repetitions, current_phrase, user_result)
            fop.save_json_to_file(repetitions_file_path, repetitions)

            statistics = dop.update_statistics(statistics, current_phrase, best_translation)
            fop.save_json_to_file(statistics_file_name, statistics)
    else:
        print(assesment_error_message)
        exit()

Сначала нужно определить, правильно ли ответил пользователь на предложенный вопрос, с учётом возможного существования нескольких правильных вариантов перевода:

# import jellyfish

def find_max_string_similarity(user_input: str, translations: str | List[str]) -> (float, str):
    """Compares user_input against each string in translations"""
    max_distance: float = 0

    if isinstance(translations, str):
        translations = [translations]
    best_translation: str = translations[0]

    # Cleanup and 'compactify' user input ('I   don't know!!!
    
            

© Habrahabr.ru