В новой курсеровской специализации «NLP» от deeplearning.ai в качестве библиотеки глубокого обучения используется Trax. В последнем курсе подробно разбирается механизм внимания и его использование в архитектуре Transformer, в том числе в таких «новеллах»  как BERT и T5. Имея некоторое количество свободного времени специализацию можно пройти за несколько недель, что я собственно и сделал, соблазнившись возможностью построить собственный трансформер. Очень хотелось сделать модель, которая может работать с текстами на русском языке. Для эксперимента я выбрал саммаризатор, эта конструкция получает на вход статью и генерирует короткий текст с описанием сути. Summary может быть и просто заголовком. Попробую рассказать обо всём в деталях. Trax — полнофункциональная библиотека для глубокого обучения с фокусом на понятный код и быстрые вычисления. По синтаксису она в общем похожа на Keras, а модель на Trax можно сконвертировать в модель на Keras. Библиотека активно развивается и поддерживается командой Google Brain. Trax использует Tensorflow и является одной из библиотек в его экосистеме. Она работает на CPU, GPU и TPU, при этом используется одна и та же версия. Не буду говорить неправду, TPU я пока не попробовал. Transformer - архитектура глубоких нейронных сетей, представленная в 2017 году исследователями из Google Brain. Transformer предназначен для работы с последовательностями, в том числе текстовыми, но в отличие от архитектур на рекуррентных сетях, не требует обрабатывать последовательность по порядку. Сильно упрощая можно сказать, что если из архитектуры Seq2Seq на LSTM с механизмом внимания оставить только механизм внимания и добавить нейронную сеть прямого распространения (Feed Forward), то он и получится. Подробнее про трансформеры с картинками здесь на английском, здесь на русском. В качестве набора данных для эксперимента я решил использовать корпус новостей Lenta.Ru, свежую версию которого нашел на Kaggle. Корпус содержит более 800 тыс. новостных статей в формате (url, title, text, topic, tags, date). Если статья — это text, то summary для моей модели — title. Это законченное предложение, содержащее основную мысль новостной статьи. Конечно это не полное summary как, например, в англоязычном корпусе cnn_dailymail, но я подумал, что так даже интереснее. Процесс подготовки данных представлен на схеме: Для начала я отфильтровал аномально короткие и аномально длинные статьи. Затем выделил из набора тексты и заголовки, преобразовал всё к нижнему регистру, сохранил в виде списка кортежей и в виде полного текста. Список кортежей разбил на две части — для обучения (train) и оценки (eval). Далее написал «бесконечный» генератор, который дойдя до конца списка, перемешивает его и начинает сначала. Неприятно же, когда генератор «заканчивается» где-то в середине эпохи. Это важно прежде всего для оценочного набора, я взял всего 5% от общего количества статей, примерно 36 тысяч пар. На основе полного текста я обучил токенайзер, а в качестве токенов использовал части слов. Проблема токенизации или сегментации на целые слова заключается в том, что некоторые слова в тексте встречаются редко, возможно единственный раз, и таких слов очень много, а размер словаря конечен и хочется его сделать не очень большим, чтобы поместиться в память виртуальной машины. Приходится заменять некоторые слова именованными шаблонами, часто использовать заполнитель  для слов, которых в словаре нет и даже использовать специальные техники вроде pointer-generator. А разбиение на подслова позволяет сделать токенайзер с небольшим по объему словарем, который еще и работает практически без потерь информации. Для такой сегментации существует несколько сравнительно честных способов, познакомиться с ними можно например здесь. Я выбрал модель на основе Byte Pair Encoding (BPE), реализованную в библиотеке sentencepiece. BPE – способ кодирования текста со сжатием. Для кодирования часто повторяющейся последовательности символов используется символ, которого нет в исходной последовательности. Всё тоже самое и при сегментации, только последовательность часто встречающихся символов становится новым токеном, и так пока не будет достигнут заданный размер словаря. Мой словарь содержит 16000 токенов.  Видно, что разбиваются даже слова на латинице, а знаки препинания кодируются как отдельные токены, просто мечта, а не токенайзер. Знак нижнего подчеркивания обозначает начало слова.  Обучается модель благодаря вот такой нехитрой конструкции: Результат — два файла: словарь для контроля и модель, которую можно загрузить в обертку токенайзера. Для выбранной мной модели статья и заголовок должны быть преобразованы в последовательности целых чисел и объединены с разделением служебными токенами  EOS :1 и  PAD :0 (конец последовательности и заполнитель). После преобразования последовательность помещается в корзину фиксированной длинны. У меня их три: 256, 512 и 1024. Последовательности в корзине автоматически дополняются заполнителями до фиксированной длинны и собираются в пакеты (batches). Количество последовательностей в пакете зависит от корзины, соответственно 16, 8, 4.  Трудно представить, что 2000 символов могут дать что-то длиннее 512 токенов, но на всякий случай сделал три корзины. А длиннее 1024 не может быть в принципе из-за фильтра в пайплайне. Сегментация и конкатенация выполняются в пайплайне trax: preprocessing — это моя функция конкатенации, генератор. Сортировка по корзинам и формирование пакетов осуществляется благодаря следующей конструкции: Transformer, работающий с двумя последовательностями, например при машинном переводе, включает два блока — энкодер и декодер, но для саммаризации достаточно только декодера. Такая архитектура в общем реализует языковую модель, где вероятность следующего слова определяется по предыдущим. Еще её называют Decoder-only Transformer и она похожа на GPT (Generative Pre-trained Transformer). Разобраться в деталях архитектур можно здесь.   Для моего случая в библиотеке Trax есть отдельный класс моделей trax.models.transformer.TransformerLM(...), то есть создать модель можно одной строчкой кода. В упомянутой специализации модель строится from scratch. Я же выбрал нечто среднее — построил модель из готовых блоков, используя примеры кода. Схема модели показана на рисунке: PositionlEncoder() – это блок, обеспечивающий построение векторного пространства и кодирование позиции токена во входной последовательности.  Код: Аргументы:vocab_size (int): размер словаряd_model (int):  количество признаков векторного пространстваdropout (float): степень использования dropoutmax_len (int): максимальная длина последовательности для позиционного кодированияmode (str): 'train' или 'eval' — для dropout и поз. кодирования. FeedForward формирует блок прямого распространения с выбранной функций активации: Аргументы:d_model (int): количество признаков векторного пространстваd_ff (int): «ширина» блока или количество юнитов в выходном плотном слоеdropout (float): степень использования dropoutmode (str): 'train' или 'eval' — чтобы не использовать dropout при оценке качества моделиff_activation (function): функция активации, в моей модели — ReLU DecoderBlock(...) - это два блока с Residual-соединием. Вряд ли перевод  «остаточный» точно отражает смысл, но это обходное соединение для борьбы с исчезающим градиентом в глубоких архитектурах. Если считать от входа к выходу, то первый блок содержит механизм внимания, я использовал готовый уровень из библиотеки. Второй — описанный выше блок прямого распространения. Механизм внимания здесь необычный, он «смотрит» на ту же последовательность, для которой генерируется следующий токен, а чтобы он «не заглядывал в будущее» при расчете весов используется специальная маска. Из неизвестных аргументов только n_heads (int) — количество головок внимания, надеюсь это удачный термин для attention heads. Каждая головка учится обращать внимание на что-то своё. Собираю все части вместе и задаю параметры модели. У меня шесть декодеров, в каждом из которых по восемь головок внимания. Общее количество обучаемых параметров 37 412 480.  Из неизвестных мне уровней пожалуй только  ShiftRight. Он сдвигает входную последовательность вправо, заполняя освободившееся место нулями, по умолчанию — на одну позицию. Это нужно для teacher forcing, специальной техники, упрощающей обучение языковой модели, особенно на ранних этапах.  Идея здесь в следующем: когда модель учится прогнозировать следующее слово по предыдущим, вместо прогноза модели, возможно неверного, в качестве этих предыдущих слов используются правильные ответы (ground truth). Коротко это можно описать формулой: y(t) = x(t+1). Здесь подробное объяснение для RNN. По моему опыту  Google Colab не очень «любит» длительное использование своих GPU и не всегда их выделяет, особенно во второй половине дня. Поэтому я обучал модель отдельными эпохами по 20 000 шагов, где шаг соответствует одному пакету (batch). Получалось сделать 1-2 эпохи в день. 100 шагов это примерно минута, а эпоха — около трех часов. Первая эпоха показала, что модель учится только несколько тысяч шагов, дальше никаких улучшений не происходит. Оказалось, что я выбрал слишком большой шаг обучения (learning_rate). Для моей модели он должен быть 0.0002 первые несколько эпох, затем 0.0001 и 0.00005 в конце. Если бы я учил модель за один проход, то можно было бы использовать lr_schedules из trax.supervised. Там есть разные удобные варианты и с прогревом и с постепенным уменьшением шага. В качестве метрик я использовал CrossEntropyLoss и Accuracy. За 12 эпох на оценочном наборе loss упал с 10 до 2, а доля правильных ответов возросла почти до 60%. Этого оказалось достаточно, чтобы генерировать почти приемлемые заголовки. Цикл обучения выглядит следующим образом: Аргументы:SumTransformer (trax.layers.combinators.Serial): модельtrain_gen (generator): поток данных для обученияeval_gen (generator): поток данных для оценки качества.output_dir (str): папка для файла модели, откуда её можно скопировать на Google Drive перед выключением виртуальной машины.  Дальше всё просто: и три часа ожидания... Для оценки результатов я использовал «жадный» декодер на базе argmax, который определяет индекс наиболее вероятного токена в словаре по положению максимального значения в выходном тензоре. Далее токен добавляется к входной последовательности и операция повторяется пока не появится символ EOS или не будет достигнута заданная максимальная длина предложения. Примеры из оценочного набора:(Исходный текст сокращен) Тест #1: швейцарская часовая компания audemars piguet представила новую модель из коллекции royal oak. как сообщает luxurylaunches, речь идет о часах с вечным календарем. официальная презентация пройдет в рамках международного салона высокого часового искусства sihh, который проходит в женеве...Образец: дом audemars piguet оснастил часы вечным календаремМодель: audemars piguet представила новую модель из коллекции royal oak Тест #2: на ежегодном фестивале в городе грэхэмстаун, юар, фокусник случайно выстрелил в голову своему напарнику во время представления. об этом сообщает местная газета the daily dispatch. инцидент произошел 30 июня. брендон пил (brendon peel) и его ассистент ли лау (li lau) выполняли магический трюк перед многочисленной аудиторией, когда пил по неосторожности пустил в затылок напарника стрелу...Образец: фокусник случайно подстрелил ассистента на глазах у зрителейМодель: на фестивале в грэлково напали с ножом(И не в грэлково, и не напали, и не с ножом, но спасибо, что это было холодное оружие, а не пистолет) Тест #3: международный валютный фонд (мвф) в среду, 15 мая, утвердил выделение кипру кредита в размере 1,33 миллиарда долларов (миллиард евро). как сообщает agence france-presse, в качестве первого транша кипрское правительство получит 110,7 миллиона долларов. утвержденный 15 мая кредит является частью плана помощи...Образец: мвф выделил кипру миллиард евроМодель: мвф утвердил кредит на кипрский кредит Тест #4: автопортрет энди уорхола, выполненный в 1965 году и ранее не выставлявшийся, продадут с аукциона, пишет the new york times. автопортрет более 40 лет хранила бывшая секретарша уорхола кэти нейсо (cathy naso), которая получила картину от художника в оплату ее работы. нейсо работала в студии уорхола...Образец: неизвестный автопортрет энди уорхола выставят на торги Модель: энди уорхола продадут с аукциона Тест #5: sony решила выпустить файтинг, который станет "ответом на игру super smash bros" от nintendo, пишет vg24/7 со ссылкой на paul gale network и neogaf. в новом проекте, в настоящее время известном под названием title fight, герои из нескольких игр издательства сразятся между собой...Образец: sony приписали разработку нового файтингаМодель: sony выпустит файтинг от nintendo Интересно, что на ранних этапах обучения вместо «белиберды» модель генерирует почти осмысленные фейки. Чтобы посмотреть как это происходит, я сделал скринкаст нескольких интересных на мой взгляд вариантов: Мой репозитарий с кодом эксперимента) Репозитарий trax Математика механизма внимания в знаменитой статье «Attention Is All You Need». Кстати один из авторов статьи, Lukasz Kaiser, штатный исследователь Google Brain, является также автором и инструктором специализации. Я использовал trax 1.3.7, он инсталлируется через pip, но не работает под Windows. На форумах пишут что можно под WSL. А еще там нет beam_search, который есть в документации и который я очень хотел попробовать. Параметры для модели взяты из заведомо работающей модели для cnn_dailymail. С учетом более коротких последовательностей, предполагаю, что размер плотного слоя в блоке FeedForward и максимальную длину последовательности можно уменьшить. Вопрос эксперимента. В упомянутой модели TransformerLM выход не нормализован (нет уровня softmax). Data Scientist