Русский
Русский
English
Статистика
Реклама

Trax

Краткость сестра таланта Как сделать TransformerSummarizer на Trax

22.02.2021 10:14:04 | Автор: admin

В новой курсеровской специализации 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 токенов.

Пример сегментированного текста

['ученые', 'придума', 'ли', 'новый', 'способ', 'взаимо', 'действия', 'с', 'граф', 'ен', 'ом', ',', 'который', 'позволяет', 'избавиться', 'от', '"', 'сли', 'па', 'ющихся', '"', 'ли', 'стов', '.', 'статья', 'ученых', 'появилась', 'в', 'журнале', 'ac', 's', 'n', 'an', 'o', ',', 'а', 'ее', 'крат', 'кое', 'из', 'ложение', 'приво', 'дится', 'на', 'сайте', 'северо', '-', 'запа', 'дного', 'университета', ',', 'сотрудники', 'которого', 'принимали', 'участие', 'в', 'работе', '.']

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

Обучается модель благодаря вот такой нехитрой конструкции:

import sentencepiece as spmspm.SentencePieceTrainer.train('--input=full_text.txt \                                --pad_id=0 --bos_id=-1 --eos_id=1 --unk_id=2 \                                --model_prefix=bpe --vocab_size=16000 --model_type=bpe')

Результат два файла: словарь для контроля и модель, которую можно загрузить в обертку токенайзера. Для выбранной мной модели статья и заголовок должны быть преобразованы в последовательности целых чисел и объединены с разделением служебными токенами EOS :1 и PAD :0 (конец последовательности и заполнитель).

После преобразования последовательность помещается в корзину фиксированной длинны. У меня их три: 256, 512 и 1024. Последовательности в корзине автоматически дополняются заполнителями до фиксированной длинны и собираются в пакеты (batches). Количество последовательностей в пакете зависит от корзины, соответственно 16, 8, 4.

Рефлексия по поводу последовательностей длиннее 512 токенов

Трудно представить, что 2000 символов могут дать что-то длиннее 512 токенов, но на всякий случай сделал три корзины. А длиннее 1024 не может быть в принципе из-за фильтра в пайплайне.

Сегментация и конкатенация выполняются в пайплайне trax:

input_pipeline = trax.data.Serial(    trax.data.Tokenize(vocab_type='sentencepiece',                       vocab_dir='/content/drive/MyDrive/',                       vocab_file='bpe.model'),    preprocessing,    trax.data.FilterByLength(1024))train_stream = input_pipeline(train_data_stream())eval_stream = input_pipeline(eval_data_stream())

preprocessing это моя функция конкатенации, генератор. Сортировка по корзинам и формирование пакетов осуществляется благодаря следующей конструкции:

boundaries =  [256, 512]batch_sizes = [16, 8, 4]train_batch_stream = trax.data.BucketByLength(    boundaries, batch_sizes)(train_stream)eval_batch_stream = trax.data.BucketByLength(    boundaries, batch_sizes)(eval_stream)

Модель

Transformer, работающий с двумя последовательностями, например при машинном переводе, включает два блока энкодер и декодер, но для саммаризации достаточно только декодера. Такая архитектура в общем реализует языковую модель, где вероятность следующего слова определяется по предыдущим. Еще её называют Decoder-only Transformer и она похожа на GPT (Generative Pre-trained Transformer). Разобраться в деталях архитектур можно здесь.

Для моего случая в библиотеке Trax есть отдельный класс моделей trax.models.transformer.TransformerLM(...), то есть создать модель можно одной строчкой кода. В упомянутой специализации модель строится from scratch. Я же выбрал нечто среднее построил модель из готовых блоков, используя примеры кода.

Схема модели показана на рисунке:

PositionlEncoder() это блок, обеспечивающий построение векторного пространства и кодирование позиции токена во входной последовательности. Код:

from trax import layers as tldef PositionalEncoder(vocab_size, d_model, dropout, max_len, mode):    return [         tl.Embedding(vocab_size, d_model),          tl.Dropout(rate=dropout, mode=mode),         tl.PositionalEncoding(max_len=max_len, mode=mode)] 

Аргументы:
vocab_size (int): размер словаря
d_model (int): количество признаков векторного пространства
dropout (float): степень использования dropout
max_len (int): максимальная длина последовательности для позиционного кодирования
mode (str): 'train' или 'eval' для dropout и поз. кодирования.

FeedForward формирует блок прямого распространения с выбранной функций активации:

def FeedForward(d_model, d_ff, dropout, mode, ff_activation):    return [         tl.LayerNorm(),         tl.Dense(d_ff),         ff_activation(),        tl.Dropout(rate=dropout, mode=mode),         tl.Dense(d_model),         tl.Dropout(rate=dropout, mode=mode)     ]

Аргументы:
d_model (int): количество признаков векторного пространства
d_ff (int): ширина блока или количество юнитов в выходном плотном слое
dropout (float): степень использования dropout
mode (str): 'train' или 'eval' чтобы не использовать dropout при оценке качества модели
ff_activation (function): функция активации, в моей модели ReLU

DecoderBlock(...) - это два блока с Residual-соединием. Вряд ли перевод остаточный точно отражает смысл, но это обходное соединение для борьбы с исчезающим градиентом в глубоких архитектурах.

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

def DecoderBlock(d_model, d_ff, n_heads, dropout, mode, ff_activation):            return [      tl.Residual(          tl.LayerNorm(),           tl.CausalAttention(d_model, n_heads=n_heads, dropout=dropout, mode=mode)         ),      tl.Residual(          FeedForward(d_model, d_ff, dropout, mode, ff_activation)        ),      ]

Из неизвестных аргументов только n_heads (int) количество головок внимания, надеюсь это удачный термин для attention heads. Каждая головка учится обращать внимание на что-то своё.

Собираю все части вместе и задаю параметры модели. У меня шесть декодеров, в каждом из которых по восемь головок внимания. Общее количество обучаемых параметров 37 412 480.

Из неизвестных мне уровней пожалуй только ShiftRight. Он сдвигает входную последовательность вправо, заполняя освободившееся место нулями, по умолчанию на одну позицию. Это нужно для teacher forcing, специальной техники, упрощающей обучение языковой модели, особенно на ранних этапах. Идея здесь в следующем: когда модель учится прогнозировать следующее слово по предыдущим, вместо прогноза модели, возможно неверного, в качестве этих предыдущих слов используются правильные ответы (ground truth). Коротко это можно описать формулой:
y(t) = x(t+1). Здесь подробное объяснение для RNN.

def SumTransformer(vocab_size=vocab_size,                  d_model=512,                  d_ff=2048,                  n_layers=6,                  n_heads=8,                  dropout=0.1,                  max_len=4096,                  mode='train',                  ff_activation=tl.Relu):    decoder_blocks = [DecoderBlock(d_model, d_ff, n_heads, dropout, mode,                       ff_activation) for _ in range(n_layers)]     return tl.Serial(        tl.ShiftRight(mode=mode),         PositionalEncoder(vocab_size, d_model, dropout, max_len, mode),        decoder_blocks,         tl.LayerNorm(),         tl.Dense(vocab_size),         tl.LogSoftmax()     )

Обучение

По моему опыту 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%. Этого оказалось достаточно, чтобы генерировать почти приемлемые заголовки.

Цикл обучения выглядит следующим образом:

from trax.supervised import trainingdef training_loop(SumTransformer, train_gen, eval_gen, output_dir = "~/model"):    output_dir = os.path.expanduser(output_dir)    train_task = training.TrainTask(         labeled_data=train_gen,        loss_layer=tl.CrossEntropyLoss(),        optimizer=trax.optimizers.Adam(0.0001),        n_steps_per_checkpoint=100    )    eval_task = training.EvalTask(         labeled_data=eval_gen,         metrics=[tl.CrossEntropyLoss(), tl.Accuracy()]     )    loop = training.Loop(SumTransformer(),                         train_task,                         eval_tasks=[eval_task],                         output_dir=output_dir)        return loop

Аргументы:
SumTransformer (trax.layers.combinators.Serial): модель
train_gen (generator): поток данных для обучения
eval_gen (generator): поток данных для оценки качества.
output_dir (str): папка для файла модели, откуда её можно скопировать на Google Drive перед выключением виртуальной машины.

Дальше всё просто:

loop = training_loop(SumTransformer, train_batch_stream, eval_batch_stream)loop.run(20000)

и три часа ожидания...

Оценка результатов

Для оценки результатов я использовал жадный декодер на базе 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).

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru