Видео проигрыватель загружается.Воспроизвести видеоВоспроизвестиБез звукаТекущее время 0:00/Продолжительность 11:24Загрузка: 0.00%00:00Тип потока ОНЛАЙНSeek to live, currently behind liveОНЛАЙНОставшееся время -11:24 1xСкорость воспроизведения2x1.75x1.5x1.25x1x, выбрано0.75x0.5xГлавыГлавыОписанияОтключить описания, выбраноСубтитрынастройки субтитров, откроется диалог настройки субтитровСубтитры выкл., выбраноЗвуковая дорожкаPicture-in-PictureПолноэкранный режимThis is a modal window.Начало диалоговго окна. Кнопка Escape закроет или отменит окноТекстColorБелыйЧерныйКрасныйЗеленыйСинийЖелтыйПурпурныйГолубойTransparencyПрозрачностьПолупрозрачныйФонColorЧерныйБелыйКрасныйЗеленыйСинийЖелтыйПурпурныйГолубойTransparencyПрозрачностьПолупрозрачныйПрозрачныйОкноColorЧерныйБелыйКрасныйЗеленыйСинийЖелтыйПурпурныйГолубойTransparencyПрозрачныйПолупрозрачныйПрозрачностьРазмер шрифта50%75%100%125%150%175%200%300%400%Стиль края текстаНичегоПоднятыйПониженныйОдинаковыйТеньШрифтПропорциональный без засечекМоноширинный без засечекПропорциональный с засечкамиМоноширинный с засечкамиСлучайныйПисьменныйМалые прописныеСбросить сбросить все найстройки по умолчаниюГотовоЗакрыть модальное окноКонец диалогового окна.
Следующий вспомогательный элемент, который нам нужен — это позиционное кодирование. Как вы помните из лекции, механизм self-attention — он, в некотором смысле, похож на механизм свёрток, тем, что он инвариантен к позиции элемента в последовательности. Мы можем за одну операцию сравнить каждый элемент последователи с любым другим элементом последовательности. Но кажется, что, когда мы работаем с текстами, особенно с текстами с фиксированным порядком слов, нам важно учитывать позиции токенов. Даже если порядок слов и не фиксированный, то относительные позиции токенов уж точно полезны. Потому что всё-таки это достаточно редкий случай — когда связь между словами идёт через пол-текста. Даже для человека такие связи были бы очень сложными. Короче говоря, нам нужно учитывать относительные позиции токенов. Для того, чтобы закодировать позиции токенов, к эмбеддингу токена, который мы берём из таблички, будем прибавлять эмбеддинг позиции. Что такое "эмбеддинг позиций"? Это вектор такой же длины, что и эмбеддинг токена, который имеет разное значение для разных позиций. И самый, наверное, интуитивный способ закодировать позицию — это использовать какой-то периодический сигнал. Авторы трансформера предлагают использовать набор синусоид и косинусоид разной частоты. На экране вы видите график, который изображает сразу несколько таких векторов. Один срез графика (вертикальный) описывает нам эмбеддинг одной позиции. Вы можете видеть, что здесь есть как высокочастотные сигналы (как, например, вот этот), так и низкочастотные (как вот эта горизонтальная прямая, или вот этот голубой график). Таким образом, по изменению сигнала на определённых позициях мы можем определить, как далеко друг от друга два токена находятся. Давайте уже начнём строить какие-нибудь модельки. Для удобства, давайте определим общий класс — "языковая модель". Этот класс будет выполнять некоторые базовые операции вне зависимости от архитектуры нейросети, которая языковую модель будет реализовывать. К таким операциям относится хранение векторов слов, получение кодов позиций, а также предсказание токенов для каждой позиции. Давайте посмотрим на метод "forward" и разберём основные шаги. На вход к нам приходит прямоугольная матрица, в ней количество строк соответствует количеству примеров в батче, а количество столбцов — наибольшей длине последовательности. Зная длину последовательности мы можем сгенерировать маску зависимостей, то есть вот эту вот треугольную матрицу из нулей и минус бесконечности. А также мы можем сгенерировать ещё одну маску, которая нам помечает токены за границей последовательности. То есть, если последовательность короче, чем "max in_len" (то есть — чем наибольшая длина входной последовательности), то она будет "добиваться" нулями до конца, до наибольшей длины, и нам нужно исключить эти нули из рассмотрения, из предсказаний. Для этого мы используем "padding mask". Хорошо, маски построены, теперь нам нужно получить начальное представление токенов для того, чтобы их подать уже в нейросеть. Вектора токенов у нас будут складываться из двух компонент, а именно: эмбеддинг самого токена и эмбеддинг позиции. Эмбеддинги токенов мы берём просто из таблички, для этого мы используем стандартный модуль из pytorch — nn.embedding. Как обычно, мы помечаем, что нулевой токен — это фиктивный токен, то есть "padding". Напомню, что этот модуль, по сути, осуществляет всего лишь выборку строк из матрицы по индексам. И он позволяет обучать эмбеддинги, что называется, "end to end". То есть эмбеддинги будут получать обновления на каждом градиентном шаге. На выходе из эмбеддинг слоя мы уже имеем не двухмерную матрицу, прямоугольную, а трёхмерный тензор. У нас добавилось ещё одно измерение, соответствующее количеству элементов в эмбеддинге. Для того, чтобы получить эмбеддинги позиций, мы используем функцию, которую рассмотрели чуть ранее. Она возвращает нам прямоугольную матрицу размерности ["длина последовательности" на "размер эмбеддинга"]. То есть, в ней нет измерения, соответствующего количеству примеров в батче. Чтобы иметь возможность сложить два тензора эмбеддингов — то есть, эмбеддинги токенов и эмбеддинги позиции, мы добавляем некоторое фиктивное измерение (добавляем единичку). После этой операции тензоры "seed_embs" и "pos_codes" будут оба трёхмерными, их можно будет сложить — что мы, собственно, и делаем. Далее к полученным эмбеддингам мы применяем dropout. На самом деле, dropout здесь очень драматично влияет на возможности модели переобучаться. Я предлагаю вам поиграть с силой этого dropout и посмотреть, что будет, если, например, его вообще выключить. А, с другой стороны — насколько сильным это dropout вообще можно сделать, при том, что модель по-прежнему вот как-то учиться. Я предлагаю вам ответить на вопрос — какой dropout важнее: этот, или те dropout, которые находятся в основной нейросети (которая собственно, предсказывает токены). Далее мы подаём признаки токенов в некоторую нейросеть, которая здесь у нас лежит в переменной "backbone". Какая это нейросеть — здесь пока здесь не определено, это определяется при создании экземпляра класса "LanguageModel". Кроме эмбеддингов мы передаём туда две маски — маску зависимости и маску padding. Нейросеть "backbone" возвращает нам, также, трёхмерный тензор такой же размерности, что и была на входе, то есть ["количество элементов в батче", "максимальная длина последовательности" и "размер вектора представления"], (то есть рабочая размерность). Размерность модели, то есть величина последнего измерения тензора, не соответствует размеру словаря — это вполне нормально, мы не хотим растить ширину модели линейно с ростом количества токенов в словаре (это слишком дорого). Поэтому нам нужен дополнительный слой, который преобразует какой-то вектор в распределение вероятностей токенов в словаре. Для этого мы используем простой линейный слой. Когда вы подаёте на вход линейному слою какой-то многомерный тензор, то линейная проекция применяется к последнему измерению. Выходной тензор у нас представляет логиты (то есть он представляет не сами вероятности, распределения вероятностей, а логиты). Чтобы получить распределение вероятностей из логитов, нам нужно применить к этому тензору softmax по последнему измерению. Но мы не будем это делать, потому что мы знаем, что после логитов сразу пойдёт кросс-энтропия, а если у нас софтмакс и кросс энтропия, то можно софтмакс не брать полностью, можно лишние экспоненты и логарифмы сократить, и получить большую вычислительную стабильность. Короче говоря, мы не будем считать софтмакс на выходе модели. Также определим парочку стандартных вспомогательных компонент. Во-первых, нам нужна функция потерь. В качестве функции потерь мы будем использовать кросс-энтропию, но, перед тем, как подавать данные в функцию расчёта кросс-энтропии, мы просто их вытянем в линию. То есть мы, как бы, забудем, что у нас были отдельно — примеры в батче, и отдельно — токены в предложениях. Мы смешаем все предложения в кучу. Для оценки кросс-энтропии это совершенно не важно. А ещё мы скажем что нужно игнорировать padding. Это сделает фактическое распределение классов сильно менее скошенным и улучшит сходимости. Хотя вы можете выключить это и посмотреть, как это повлияет. А также, другая стандартная утилитка — это расписание изменения длины градиентного шага. Мы говорим, тем самым, что, если в течение двадцати эпох значение функции потерь на валидации не уменьшилось существенно, тогда — уменьшить длину градиентного шага в два раза. Использование такого "расписания" позволяет исключить ошибки, когда вы устанавливаете слишком большой "learning rate" при обучении, то есть если вы поставите слишком большой "learning rate", то модель просто не будет учиться и, в результате, "learning rate" автоматически уменьшится. Да, он уменьшится не сразу (а спустя вот такое вот количество эпох), но, тем не менее, если вы запустили эксперимент на ночь и ушли, то, скорее всего, утром вы получите обученную модель. Давайте предпримем первую попытку обучить языковую модель, используя реализацию трансформера из библиотеки pytorch. Трансформер в pytorch появился, начиная с версии 1.2, то есть, если вы хотите запускать этот семинар, вам нужно обновить библиотеку pytorch до этой версии. Мы будем использовать не весь трансформер, а только его первую часть — трансформер "encoder". Этот вспомогательный класс нам нужен для двух задач. По какой-то причине, стандартная реализация трансформера умеет работать с тензорами, в которых первое измерение соответствует не размеру батча, а длине последовательности, а "batch_size" стоит на втором месте. Лично мне это кажется не вполне удобным, хотя... это вопрос предпочтений в большей степени. То есть — вопрос удобства: если вам нужно меньше транспонировать тензоры туда-сюда, и, при этом, иметь "batch_size" на втором месте, тогда вам не нужен это класс — вы можете сэкономить лишнее транспонирование, reshape, и так далее. Таким образом, этот класс делает, по сути, всего лишь две вещи. Во-первых, он транспонирует тензор перед подачей в трансформер, транспонирует результаты работы трансформера обратно, и возвращает результат не изменённым. И — вторая важная деталь — это инициализация параметров. По умолчанию, в трансформере используется равномерный шум с амплитудой, подбираемой исходя из количества входных признаков. Эта схема инициализации реализовывается в pytorch функцией "xavier_uniform". Таким способом, мы инициализируем все веса, кроме bias, то есть все "матричные" веса. Давайте уже соберём нашего монстра и чему-нибудь обучим.

К сожалению, у нас пока нет статистики ответов на данный вопрос, но мы работаем над этим.