Как кодировать BERT с использованием PyTorch — руководство с примерами

МЕНЮ


Главная страница
Поиск
Регистрация на сайте
Помощь проекту
Архив новостей

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


Если вы энтузиаст НЛП, возможно, вы слышали о BERT. В этой статье мы собираемся изучить BERT: что это такое? и как это работает? и научитесь программировать его с помощью PyTorch.

В 2018 году Google опубликовал статью под названием « Предварительное обучение глубоких двунаправленных преобразователей для понимания языка ». В этой статье они представили языковую модель под названием BERT (представление двунаправленного кодировщика с помощью преобразователей) , которая обеспечивает высочайшую производительность в таких задачах, как вопросы-ответы , вывод естественного языка, классификация и общая оценка понимания языка или (GLUE). .

Выпуск BERT последовал за выпуском трех архитектур, которые также достигли самых современных характеристик. Эти модели были: 

  • УЛМ-Фит (январь)
  • ЭЛМО (февраль), 
  • OpenAI GPT (июнь) 
  • БЕРТ (октябрь). 

OpenAI GPT и BERT используют архитектуру Transformer , которая не использует рекуррентные нейронные сети; это позволило архитектуре учитывать долгосрочные зависимости посредством механизма самообслуживания , который по своей сути изменил способ моделирования последовательных данных. Он представил архитектуру кодировщика-декодера , которая использовалась в приложениях компьютерного зрения, таких как генерация изображений с помощью вариационного автокодировщика. 

Так чем же BERT отличается от всех моделей, выпущенных в 2018 году? 

Что ж, чтобы ответить на этот вопрос, нам нужно понять, что такое BERT и как он работает. 

Итак, начнем. 

Что такое БЕРТ?

BERT означает «Представление двунаправленного энкодера с помощью трансформаторов». Проще говоря, BERT извлекает шаблоны или представления из данных или вложений слов, пропуская их через кодер. Сам кодер представляет собой архитектуру преобразователя, сложенную вместе. Это двунаправленный преобразователь, что означает, что во время обучения он учитывает контекст как слева, так и справа от словаря для извлечения шаблонов или представлений. 

BERT-кодер
Источник

BERT использует две парадигмы обучения: предварительное обучение и тонкая настройка

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

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

Может вас заинтересовать

Ограничения ИИ: могут ли модели глубокого обучения, такие как BERT, когда-либо понимать язык? 10 вещей, которые вам нужно знать о BERT и архитектуре трансформатора, которые меняют ландшафт искусственного интеллекта

Основные компоненты BERT

BERT заимствует идеи из моделей SOTA предыдущего выпуска. Давайте уточним это утверждение. 

Трансформеры

Основным компонентом BERT является архитектура трансформатора. Трансформаторы состоят из двух компонентов: кодера и декодера . Сам кодер содержит два компонента: уровень самообслуживания и нейронную сеть прямого распространения

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

Компоненты БЕРТ
Источник

Преимущество архитектуры преобразователя состоит в том, что она помогает модели сохранять бесконечно длинные последовательности, что было невозможно при использовании традиционных RNN, LSTM и GRU. Но даже из-за того, что он может достигать долгосрочных зависимостей, ему все еще не хватает контекстуального понимания

Джей Аламмар подробно объясняет трансформеры в своей статье «Иллюстрированный трансформер» , которую стоит прочитать. 

ЭЛМО

BERT заимствует еще одну идею у ELMo, которая означает «вложения из языковой модели». ELMo был представлен Peters et. ал. в 2017 году, посвященный идее контекстуального понимания. Принцип работы ELMo заключается в использовании двунаправленного LSTM для понимания контекста. Поскольку он рассматривает слова с обоих направлений, он может назначать различное встраивание слов словам, которые пишутся одинаково, но имеют разное значение. 

Например, «Вы, дети, должны держаться вместе в темноте» совершенно отличается от «Дайте мне эту палку ». Несмотря на то, что в обоих предложениях используется одно и то же слово, его значение различается в зависимости от контекста. 

Таким образом, ELMo назначает вложения, рассматривая слова как с правого, так и с левого направления, по сравнению с моделями, разработанными ранее, которые учитывали слова только слева. Эти модели были однонаправленными, как RNN, LSTM и т. д. 

Это позволяет ELMo собирать контекстную информацию из последовательностей, но поскольку ELMo использует LTSM, он не имеет долгосрочной зависимости по сравнению с преобразователями.

До сих пор мы видели, что BERT может получить доступ к последовательностям в документе, даже если за текущим словом в последовательности находится n слов, благодаря механизму внимания, присутствующему в преобразователях, то есть он может сохранять долгосрочные зависимости, а также может достигать контекстуальное понимание предложения из-за двунаправленного механизма, присутствующего в ELMo. 

УЛМ-ФиТ

В 2018 году Джереми Ховард и Себастьян Рудер опубликовали статью под названием « Точная настройка универсальной языковой модели» или ULM-FiT , в которой они утверждали, что трансферное обучение может использоваться в НЛП так же, как оно используется в компьютерном зрении. 

Раньше мы использовали предварительно обученные модели для встраивания слов, которые нацелены только на первый уровень всей модели, то есть слои внедрения, и вся модель обучалась с нуля, это занимало много времени и не приносило большого успеха. был найден в этом районе. Однако Говард и Рудер предложили 3 метода классификации текста:

  • Первый шаг включает в себя обучение модели на большем наборе данных, чтобы модель изучала представления. 
  • Второй шаг включал тонкую настройку модели с использованием набора данных для конкретной задачи для классификации, в ходе которой они представили еще два метода: дискриминативную тонкую настройку и скорость обучения по наклонному треугольнику (STLR). Первый метод пытается точно настроить или оптимизировать параметры для каждого уровня передачи в сети, а второй контролирует скорость обучения на каждом из этапов оптимизации. 
  • Третьим шагом была точная настройка классификатора на наборе данных для конкретной задачи для классификации. 
БЕРТ УЛМ-ФиТ
Источник

С выпуском ULM-FiT специалисты по НЛП теперь могут практиковать подход трансферного обучения в своих задачах НЛП. Но единственная проблема с подходом ULM-FiT к передаче обучения заключалась в том, что он включал тонкую настройку всех слоев сети, а это требовало больших усилий. 

OpenAI GPT

Генеративный предварительно обученный трансформатор или GPT был представлен командой OpenAI: Рэдфордом, Нарасимханом, Салимансом и Суцкевером. Они представили модель, в которой при однонаправленном подходе используются только декодеры преобразователя вместо кодеров. В результате он превзошел все предыдущие модели в различных задачах, таких как: 

  • Классификация
  • Вывод естественного языка
  • Семантическое сходство
  • Ответ на вопрос 
  • Большой выбор. 

Несмотря на то, что GPT использовал только декодер, он все равно мог сохранять долгосрочные зависимости. Более того, это свело тонкую настройку к минимуму по сравнению с тем, что мы видели в ULM-FiT. 

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

Сравнение языковых моделей
Источник

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

Давайте сравним все модели с BERT на предмет задач, которые они могут выполнять:

Трансформатор

ЭЛМО

УЛМ-ФиТ

OpenAI GPT

БЕРТ

Контекстуальное понимание

Нет

Да(Слабый)

Да(Слабый)

Да (умеренный)

Дарн (Сильный)

Долгосрочные зависимости

бесконечный

Конечный

Конечный

бесконечный

бесконечный

Машинный перевод

Да

Нет

Нет

Да

Да

Вывод на естественном языке

Нет

Да

Нет

Да

Да

Ответ на вопрос

Нет

Да

Нет

Да

Да

Классификация или
анализ настроений

Нет

Да

Да

Да

Да

Генерация текста

Нет

Нет

Нет

Да, (бедный)

Нет

Заполняющая маска

Нет

Нет

Нет

Нет

Да

Вы можете проверить модели Huggingface , чтобы проверить производительность модели при выполнении каждой задачи. 

Почему БЕРТ?

BERT попадает в модель самоконтроля . Это означает, что он может генерировать входные данные и метки из необработанного корпуса без явного программирования людьми. Помните, что данные, на которых он обучается, неструктурированы.

BERT был предварительно обучен двум конкретным задачам: модель языка в маске и предсказание следующего предложения. В первом используется замаскированный ввод, например «мужчина [МАСКА] в магазин» вместо «мужчина пошел в магазин». Это не позволяет BERT видеть слова рядом с ним, что позволяет ему максимально изучить двунаправленные представления, что делает его более гибким и надежным для нескольких последующих задач. Последний предсказывает, связаны ли два предложения контекстуально друг с другом. 

BERT-маскировка
Источник

Например, если предложение A — «[CLS] человек [MASK] в магазин», а предложение B — «пингвин [MASK] — нелетающие птицы [SEP]», то BERT сможет определить, являются ли оба предложения непрерывными. или нет. 

Во время обучения BERT использует специальные типы токенов, такие как [CLS], [MASK], [SEP] и т. д., которые позволяют BERT различать, когда начинается предложение, какое слово замаскировано и когда два предложения разделены. Я объяснил эти токены в табличном формате в разделе предварительной обработки

BERT также можно использовать для извлечения признаков из-за свойств, которые мы обсуждали ранее, и передавать эти извлечения в существующую модель. 

Извлечение признаков BERT
Источник

В исходной статье BERT он сравнивался с GPT в тесте оценки общего понимания языка , и вот результаты. 

Сравнение BERT GPT
Источник

Как видите, BERT превзошел GPT во всех задачах и в среднем на 7% лучше, чем GPT.

BERT-задачи
На изображении выше показаны различные задачи, для которых можно использовать BERT. | Источник

Кодирование BERT с помощью Pytorch

Давайте разберемся с помощью кода, как построить BERT с помощью PyTorch. 

Всю программу разобьем на 4 раздела:

  1. Предварительная обработка
  2. Модель здания
  3. Потери и оптимизация
  4. Обучение

Проверьте также

Как отслеживать эксперименты в PyTorch с помощью Neptune

Предварительная обработка

При предварительной обработке мы структурируем данные так, чтобы нейронная сеть могла их обработать. Начнем с назначения необработанного текста для обучения. 

text = (         'Привет, как дела? Я Ромео.n'         'Привет, Ромео. Меня зовут Джульетта. Приятно познакомиться.n'         'Тоже приятно познакомиться. Как ты сегодня?n'         'Отлично. Мой бейсбол команда выиграла соревнование.n'         'О, поздравляю, Джульетта'         'Спасибо, Ромео'     )

Затем мы очистим данные:

  • Преобразование предложений в нижний регистр.
  • Создание словарного запаса. Словарь — это список уникальных слов в документе. 
  предложения = re.sub( "[.,!?-]" , '' , text.lower()).split( 'n' )   # фильтр '.', ',', '?', '!'     word_list = список(set( " " .join(sentences).split())) 

Теперь, на следующем этапе, важно помнить, что BERT во время обучения принимает специальные токены. Вот таблица, объясняющая назначение различных токенов:

Токен

Цель

[КЛС]

Первый токен всегда является классификационным.

[сентябрь]

Разделяет два предложения

[КОНЕЦ]

Закончите предложение.

[ПАД]

Используйте для сокращения предложения до одинаковой длины.

[МАСКА]

Используйте для создания маски путем замены исходного слова.

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

word_dict = { '[PAD]' : 0 , '[CLS]' : 1 , '[SEP]' : 2 , '[MASK]' : 3 }  for i, w в enumerate(word_list):    word_dict[w] = i + 4     Number_dict = {i: w для i, w в перечислении (word_dict)}    vocab_size = длина (word_dict)

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

Что такое внедрение токена?

Например, если предложение «Кот гуляет. Собака лает», то функция должна создать последовательность в следующем виде: «[CLS] кошка гуляет [SEP] собака лает». 

После этого преобразуем все в индекс из словаря слов. Таким образом, предыдущее предложение будет выглядеть примерно так: «[1, 5, 7, 9, 10, 2, 5, 6, 9, 11]». Имейте в виду, что 1 и 2 — это [CLS] и [SEP] соответственно. 

Что такое внедрение сегмента?

Встраивание сегмента отделяет два предложения друг от друга, и они обычно определяются как 0 и 1. 

Что такое встраивание позиции?

Вложение позиции задает положение каждого вложения в последовательности. 

Позже мы создадим функцию для встраивания позиции. 

BERT-вложения
Источник

Теперь следующим шагом будет создание маскировки

Как упоминалось в оригинальной статье, BERT случайным образом назначает маски 15% последовательности. Но имейте в виду, что вы не назначаете маски специальным токенам. Для этого мы будем использовать условные операторы.

Как только мы заменим 15% слов токенами [MASK], мы добавим отступы. Заполнение обычно делается для того, чтобы все предложения имели одинаковую длину. Например, если мы возьмем предложение:

 «Кот гуляет. Собака лает на дерево»

тогда с заполнением это будет выглядеть так: 

«[CLS] Кот гуляет [PAD] [PAD] [PAD]. [CLS] Собака лает на дерево». 

Длина первого предложения равна длине второго предложения. 

защита  make_batch () :    партия = []    положительный = отрицательный = 0     , а положительный != размер_пакета/ 2  или отрицательный != размер_пакета/ 2 :        tokens_a_index, tokens_b_index= randrange(len(предложения)), randrange(len(предложения))         tokens_a, tokens_b= token_list[tokens_a_index], token_list[tokens_b_index]         input_ids = [word_dict[ '[CLS]' ]] + tokens_a + [word_dict[ '[SEP]' ]] + tokens_b + [word_dict[ '[SEP]' ]]        сегмент_ids = [ 0 ] * ( 1 + len(tokens_a) + 1 ) + [ 1 ] * (len(tokens_b) + 1 )         # MASK LM         n_pred = min(max_pred, max( 1 , int(round(len(input_ids) * 0.15 )))) # 15 % токенов в одном предложении         cand_maked_pos = [i for i, token in enumerate(input_ids)                           if token != word_dict[ '[CLS]' ] и токен != word_dict[ '[SEP]' ]]        перемешать (cand_made_pos)        Masked_Tokens, Masked_pos = [], []        для позиции в cand_made_pos[:n_pred]:            Masked_pos.append(поз)            Masked_tokens.append(input_ids[pos])            if random() < 0.8 :   # 80%                 input_ids[pos] = word_dict[ '[MASK]' ] # сделать маску             elif random() < 0.5 :   # 10%                 index = randint( 0 , vocab_size - 1 ) # случайный индекс в словарь                 input_ids[pos] = word_dict[number_dict[index]] # заменить         # Нулевые отступы        n_pad = maxlen - len(input_ids)        input_ids.extend([ 0 ] * n_pad)        сегмент_ids.extend([ 0 ] * n_pad)         # Токены с нулевым заполнением (100–15%),         если max_pred > n_pred:            n_pad = max_pred - n_pred            Masked_tokens.extend([ 0 ] * n_pad)            Masked_pos.extend([ 0 ] * n_pad)         если tokens_a_index + 1 == tokens_b_index и положительный < Batch_size/ 2 :            Batch.append([input_ids, сегмент_ids, Masked_tokens, Masked_pos, True ]) # IsNext             положительный += 1         elif tokens_a_index + 1 != tokens_b_index и отрицательный < Batch_size/ 2 :            пакет.append([input_ids, сегмент_ids, Masked_tokens, Masked_pos, False ]) # NotNext             отрицательный += 1     возвратный пакет

Поскольку мы имеем дело с предсказанием следующего слова, нам нужно создать метку, которая предсказывает, есть ли в предложении последовательное предложение или нет, т. е. IsNext или NotNext. Поэтому мы присваиваем True каждому предложению, которое предшествует следующему предложению, и используем для этого условный оператор. 

Например, два предложения в документе обычно следуют друг за другом, если они находятся в контексте. Итак, если предположить, что первое предложение — это А, то следующее предложение должно быть А+1. Интуитивно мы пишем код так, что если первое предложение позиционируется, т.е. tokens_a_index + 1 == tokens_b_index, т.е. второе предложение в том же контексте, то мы можем установить метку для этого ввода как True. 

Если вышеуказанное условие не выполняется, т.е. если tokens_a_index + 1 != tokens_b_index, тогда мы устанавливаем метку для этого входа как False. 

Модель здания

BERT — сложная модель, и если ее воспринимать медленно, вы теряете логику. Поэтому имеет смысл объяснять его компонент за компонентом и их функцию.

BERT имеет следующие компоненты:

  1. Встраивание слоев
  2. Маска внимания
  3. Уровень кодировщика
    1. Многоголовое внимание
      1. Масштабированное внимание к скалярному произведению
    2. Позиционная сеть прямой связи
  4. БЕРТ (сборка всех компонентов)

Чтобы облегчить обучение, вы всегда можете обратиться к этой схеме. 

Модель здания BERT
Источник: Автор

Слой внедрения

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

Уровень внедрения также сохраняет различные отношения между словами, такие как: семантические, синтаксические, линейные, а поскольку BERT является двунаправленным, он также сохраняет контекстные отношения. 

В случае BERT он создает три вложения для 

  • жетон, 
  • Сегменты и
  • Позиция. 

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

print(torch.arange( 30 , dtype=torch.long).expand_as(input_ids))
Выход:  tensor([[ 0 ,   1 ,   2 ,   3 ,   4 ,   5 ,   6 ,   7 ,   8 ,   9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 ,           18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 ],         [ 0 ,   1 ,   2 ,   3 ,   4 ,   5 ,   6 ,   7 ,   8 ,   9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 ,           18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 ],         [ 0 ,   1 ,   2 ,   3 ,   4 ,   5 ,   6 ,   7 ,   8 ,   9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 ,           18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 ],         [ 0 ,   1 ,   2 ,   3 ,   4 ,   5 ,   6 ,   7 ,   8 ,   9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 ,           18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 ],         [ 0 ,   1 ,   2 ,   3 ,   4 ,   5 ,   6 ,   7 ,   8 ,   9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 ,           18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 ],         [ 0 ,   1 ,   2 ,   3 ,   4 ,   5 ,   6 ,   7 ,   8 ,   9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 ,           18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 ]])

В прямой функции мы суммируем все вложения и нормализуем их. 

BERT-вложения
Источник
класс  Embedding (nn.Module) :     def  __init__ (self) :        супер(Вложение, сам).__init__()        self.tok_embed = nn.Embedding(vocab_size, d_model)   # встраивание токена         self.pos_embed = nn.Embedding(maxlen, d_model)   # встраивание позиции         self.seg_embed = nn.Embedding(n_segments, d_model)   # встраивание сегмента (тип токена)        self.norm = nn.LayerNorm(d_model)     def  вперед (self, x, seg) :         seq_len = x.size( 1 )        pos = torch.arange(seq_len, dtype=torch.long)        pos = pos.unsqueeze( 0 ).expand_as(x)   # (seq_len,) -> (batch_size, seq_len)        встраивание = self.tok_embed(x) + self.pos_embed(pos) + self.seg_embed(seg)        вернуть self.norm(вложение)

Читайте также

Обучение, визуализация и понимание встраивания слов: глубокое погружение в пользовательские наборы данных

Создание маски внимания

БЕРТу нужны маски внимания . И они должны быть в правильном формате. Следующий код поможет вам создать маски. 

Он преобразует [PAD] в 1, а в другом месте — в 0. 

def  get_attn_pad_mask (seq_q, seq_k) :    пакетный_размер, len_q = seq_q.size()    пакетный_размер, len_k = seq_k.size()    # eq(ноль) — это токен PAD     Pad_attn_mask = seq_k.data.eq( 0 ).unsqueeze( 1 )   # Batch_size x 1 x len_k(=len_q), один из них маскирует     return Pad_attn_mask.expand(batch_size, len_q, len_k)   # Batch_size х len_q х len_k
print(get_attn_pad_mask(input_ids, input_ids)[ 0 ][ 0 ], input_ids[ 0 ])
Выход: (tensor([ Ложь , Ложь , Ложь , Ложь , Ложь , Ложь , Ложь , Ложь , Ложь , Ложь ,           Ложь , Ложь , Ложь ,   Истина ,   Истина ,   Истина ,   Истина ,   Истина ,   Истина ,   Истина ,            Истина ,   Истина ,   Истина ,   Правда ,   Правда ,   Правда ,   Правда ,   Правда ,   Правда ,   Правда ]),  тензор([ 1 ,   3 , 26 , 21 , 14 ,   16 , 12 , 4 ,   2 , 27 ,   3 , 22 ,   2 ,   0 ,   0 ,   0 ,   0 ,   0 ,            0 ,   0 ,   0 ,   0 ,   0 ,   0 ,   0 ,   0 ,   0 ,   0 ,   0 ,   0 ]))

Кодер

Кодер состоит из двух основных компонентов: 

  • Внимание с несколькими головками
  • Позиционная сеть прямой связи. 

Работа кодировщика заключается в поиске представлений и шаблонов из входных данных и маски внимания. 

класс  EncoderLayer (nn.Module) :     def  __init__ (self) :        супер(EncoderLayer, self).__init__()        self.enc_self_attn = MultiHeadAttention()        self.pos_ffn = PoswiseFeedForwardNet()     def  вперед (self, enc_inputs, enc_self_attn_mask) :         enc_outputs, attn = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs, enc_self_attn_mask) # enc_inputs в тот же Q,K,V         enc_outputs = self.pos_ffn(enc_outputs) # enc_outputs: [batch _size x len_q x d_model]         return enc_outputs, attn

Многоголовое внимание

Это первый из основных компонентов кодера. 

Модель внимания принимает три входных параметра: Query , Key и Value

Я настоятельно рекомендую вам прочитать «Иллюстрированный трансформер» Джея Аламмара, в котором подробно объясняются модели внимания. 

Многоголовое внимание принимает четыре входа: Query , Key , Value и Attention Mask . Внедрения передаются в качестве входных данных для аргумента «Запрос», «Ключ» и «Значение», а маска внимания передается в качестве входных данных для аргумента маски внимания. 
Эти три входа и маска внимания обрабатываются с помощью операции скалярного произведения, которая дает два результата: векторы контекста и внимание . Вектор контекста затем передается через линейный слой и, наконец, дает выходные данные.

класс  MultiHeadAttention (nn.Module) :     def  __init__ (self) :        супер(MultiHeadAttention, self).__init__()        self.W_Q = nn.Linear(d_model, d_k * n_heads)        self.W_K = nn.Linear(d_model, d_k * n_heads)        self.W_V = nn.Linear(d_model, d_v * n_heads)     def  вперед (self, Q, K, V, attn_mask) :         # q: [batch_size x len_q x d_model], k: [batch_size x len_k x d_model], v: [batch_size x len_k x d_model]         остаток, пакет_размер = Q, Q.size( 0 )         # (B, S, D) -proj-> (B, S, D) -split-> (B, S, H, W) -trans-> (B, H, S, W) )         q_s = self.W_Q(Q).view(batch_size, -1 , n_heads, d_k).transpose( 1 , 2 )   # q_s: [batch_size x n_heads x len_q x d_k]         k_s = self.W_K(K).view (batch_size, -1 , n_heads, d_k).transpose( 1 , 2 )   # k_s: [batch_size x n_heads x len_k x d_k]         v_s = self.W_V(V).view(batch_size, -1 , n_heads, d_v). transpose( 1 , 2 )   # v_s: [batch_size x n_heads x len_k x d_v]         attn_mask = attn_mask.unsqueeze( 1 ).repeat( 1 , n_heads, 1 , 1 ) # attn_mask : [batch_size x n_heads x len_q x len_k]         # контекст: [batch_size x n_heads x len_q x d_v], attn: [batch_size x n_heads x len_q(=len_k) x len_k(=len_q)]        контекст, attn = ScaledDotProductAttention()(q_s, k_s, v_s, attn_mask)        context = context.transpose( 1 , 2 ).contigious().view(batch_size, -1 , n_heads * d_v) # context: [batch_size x len_q x n_heads * d_v]        вывод = nn.Linear(n_heads * d_v, d_model)(контекст)   return nn.LayerNorm(d_model)(выход + остаток), attn # вывод: [batch_size x len_q x d_model]

Теперь давайте изучим это масштабированное скалярное произведение:

  • Класс внимания масштабированного скалярного произведения принимает четыре аргумента: запрос, ключ, значение и маску внимания. По сути, первые три аргумента передаются с встраиванием слов, а аргумент маски внимания передается с встраиванием маски внимания.
  • Затем он выполняет матричное умножение запроса и ключа для получения оценок. 

После этого мы используем Scores.masked_fill_(attn_mask, -1e9) . Этот атрибут заполняет элемент оценок значением -1e9, где маски внимания имеют значение True , а остальные элементы получают оценку внимания , которая затем передается через функцию softmax, которая дает оценку от 0 до 1. Наконец, мы выполняем матричное умножение. между вниманием и ценностями, что дает нам векторы контекста. 

класс  ScaledDotProductAttention (nn.Module) :     def  __init__ (self) :        супер(ScaledDotProductAttention, self).__init__()     def  вперед (self, Q, K, V, attn_mask) :         оценки = torch.matmul(Q, K.transpose( -1 , -2 )) / np.sqrt(d_k) # оценки: [batch_size x n_heads x len_q( =len_k) x len_k(=len_q)]         Scores.masked_fill_(attn_mask, -1e9 ) # Заполняет элементы автотензора значением, где маска равна единице.         attn = nn.Softmax(dim= -1 )(оценки)        контекст = torch.matmul(attn, V)        возвращаемый показатель, контекст, внимание
emb = Встраивание() embeds = emb(input_ids, сегмент_ids)  attenM = get_attn_pad_mask(input_ids, input_ids)  SDPA= ScaledDotProductAttention()(встраивает, встраивает, встраивает, attenM)  S, C, A = СДПА  print( 'Маски' , маски[ 0 ][ 0 ]) Распечатать() print( 'Оценки:' , S[ 0 ][ 0 ], 'nnAttention Оценки после softmax: ' , A[ 0 ][ 0 ])
Выход:  Тензор масок([ False , False , False , False , False , False , False , False , False , False ,          False , False , False ,   True ,   True ,   True , True   ,   True ,   True ,   True ,   True ,           True ,   True ,   Правда ,   правда ,   правда ,   правда ,   правда ,   правда ,   правда ])  Оценки: тензор ([ 9,6000e+01 ,   3.1570e+01 ,   2,9415E+01 ,   3.3990E+01 ,   3,7752E+01 ,           3,7363E +01 ,   3,1683E+01 ,   3,2156E+01 ,   3.5942E+01 ,. -2.4670e+00 ,          -2.2461e+00 , -8.1908e+00 , -2.1571e+00 , -1.0000e+09 , -1.0000e+09 ,          -1.0000e+09 , -1.0000e+09 , -1.0000 e+09 , -1.0000e+09 , -1.0000e+09 ,          -1.0000e+09 , -1.0000e+09 , -1.0000e +09 , -1.0000e+09 , -1.0000e+09 ,          -1.0000e+ 09 , -1.0000e+09 , -1.0000e+09 , -1.0000e+09 , -1.0000e+09 ],        grad_fn=<ВыбратьНазад>)  Оценка внимания после Softmax :: Tensor ([ 1.0000E+00 , 1.0440E-28 , 1.2090E-29 , 1,1732E-27 , 5,0495E-26 , 3,4218E-26 ,          1,1689E-28 , 1,8746E-28 , 8,2677. e-27 , 1.7236e-43 , 2.1440e-43 , 0.0000e+00 ,          2.3542e-43 , 0.0000e+00 , 0.0000e+00 , 0.0000e+00 , 0.0000e+00 , 0.0000e+00 ,          0,0000 e+00 , 0.0000e+00 , 0.0000e+00 , 0.0000e+00 , 0.0000e+00 , 0.0000e + 00 ,          0.0000e+00 , 0.0000e+00 , 0.0000e+00 , 0.0000e+00 , 0,0000 е+00 , 0,0000е+00 ],        grad_fn=<ВыбратьНазад>)

Позиционно-ориентированная сеть прямой связи

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

Давайте переведем дух и повторим то, что мы уже узнали:

  • Входные данные поступают во встраивание, а также в функцию внимания. Оба из них подаются в кодер, который имеет функцию нескольких головок и сеть прямой связи. 
  • Сама функция multi-head имеет функцию, которая управляет вложениями и маской внимания с помощью операции скалярного произведения. 
Модель здания BERT
Источник: Автор

Сборка всех компонентов

Давайте продолжим с того места, где мы остановились, то есть с вывода кодировщика.

Кодер выдает два вывода: 

  • Тот, который исходит из слоя прямой связи и 
  • Маска внимания. 

Важно помнить, что BERT не использует декодер явно. Вместо этого он использует выходные данные и маску внимания для получения желаемого результата. 

Хотя секция декодера в преобразователях заменена мелкой сетью, которую можно использовать для классификации, как показано в коде ниже.
Кроме того, BERT выводит два результата: один для классификатора , а другой для маскированного .

класс  BERT (nn.Module) :     def  __init__ (self) :        супер(БЕРТ, сам).__init__()        self.embedding = Встраивание()        self.layers = nn.ModuleList([EncoderLayer() for _ in range(n_layers)])        self.fc = nn.Linear(d_model, d_model)        self.activ1 = nn.Tanh()        self.linear = nn.Linear(d_model, d_model)        self.activ2 = гель        self.norm = nn.LayerNorm(d_model)        self.classifier = nn.Linear(d_model, 2 )         # декодер используется совместно со слоем внедрения        embed_weight = self.embedding.tok_embed.weight        n_vocab, n_dim = embed_weight.size()        self.decoder = nn.Linear(n_dim, n_vocab, смещение= False )        self.decoder.weight = embed_weight        self.decoder_bias = nn.Parameter(torch.zeros(n_vocab))     def  вперед (self, input_ids, сегмент_ids, Masked_pos) :        вывод = self.embedding(input_ids, сегмент_ids)        enc_self_attn_mask = get_attn_pad_mask(input_ids, input_ids)        для слоя в self.layers:            вывод, enc_self_attn = слой (выход, enc_self_attn_mask)        # вывод : [batch_size, len, d_model], attn : [batch_size, n_heads, d_mode, d_model]         # это будет определено первым токеном (CLS)         h_pooled = self.activ1(self.fc(output[:, 0 ]) ) # [batch_size, d_model]         logits_clsf = self.classifier(h_pooled) # [batch_size, 2]         Masked_pos = Masked_pos[:, :, Нет ].expand( -1 , -1 , output.size( -1 )) # [batch_size, max_pred, d_model]         # получить замаскированную позицию из конечного вывода трансформатора.         h_masked = torch.gather(output, 1 , Masked_pos) # позиция маскировки [batch_size, max_pred, d_model]        h_masked = self.norm(self.activ2(self.linear(h_masked)))        logits_lm = self.decoder(h_masked) + self.decoder_bias # [batch_size, max_pred, n_vocab]         вернуть logits_lm, logits_clsf

Несколько вещей, которые следует иметь в виду:

  1. Вы можете назначить количество кодеров. В оригинальной статье базовая модель имеет 12. 
  2. Существует две функции активации: Tanh и GELU ( гауссова ошибка , линейная единица измерения ) .
def  gelu (x) :     return x * 0,5 * ( 1,0 + torch.erf(x / math.sqrt( 2.0 )))

Потери и оптимизация

Хотя в исходной статье распределение вероятностей рассчитывается по всему словарю, мы можем использовать приближение softmax. Но изящный способ сделать это — использовать перекрестную энтропийную потерю . Это комбинация softmax и отрицательного логарифмического правдоподобия

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

Когда дело доходит до оптимизации, мы будем использовать оптимизатор Adam

критерий = nn.CrossEntropyLoss() оптимизатор = optim.Adam(model.parameters(), lr= 0,001 )

Связанная статья

Функции потерь PyTorch: полное руководство

Обучение

Наконец, мы начнем обучение. 

модель = БЕРТ() пакет = make_batch() input_ids, сегмент_ids, Masked_Tokens, Masked_pos, isNext = карта (torch.LongTensor, zip (* пакет))     для эпохи в диапазоне ( 100 ):        оптимизатор.zero_grad()        logits_lm, logits_clsf = модель (input_ids, сегмент_ids, Masked_pos)        loss_lm = критерий(logits_lm.transpose( 1 , 2 ), Masked_tokens) # для замаскированного LM        loss_lm = (loss_lm.float()).mean()        loss_clsf = критерий(logits_clsf, isNext) # для классификации предложений        потеря = потеря_lm + потеря_clsf        если (эпоха + 1 ) % 10 == 0 :            print( 'Эпоха:' , '%04d' % (эпоха + 1 ), 'стоимость =' , '{:.6f}' .format(loss))        потеря.назад()        оптимизатор.шаг()     # Прогнозирование токенов маски     input_ids, сегмент_ids, Masked_tokens, Masked_pos, isNext = Map(torch.LongTensor, zip(batch[ 0 ]))    печать (текст)    print([number_dict[w.item()] для w в input_ids[ 0 ] if Number_dict[w.item()] != '[PAD]' ])     logits_lm, logits_clsf = модель (input_ids, сегмент_ids, Masked_pos)    logits_lm = logits_lm.data.max( 2 )[ 1 ][ 0 ].data.numpy()    print( 'список замаскированных токенов:' ,[pos.item() для позиции в Masked_tokens[ 0 ] , если pos.item()!= 0 ])    print( 'предсказать список замаскированных токенов: ' ,[pos for pos в logits_lm if pos != 0 ])     logits_clsf = logits_clsf.data.max( 1 )[ 1 ].data.numpy()[ 0 ]    print( 'isNext:' , True ,  если isNext , иначе  False )    print( 'predict isNext:' , True ,  если logits_clsf , иначе  False )
Выход:  Привет, как дела? Я Ромео. Привет, Ромео. Меня зовут Джульетта . Рад встрече. Мне тоже приятно познакомиться. Как вы сегодня? Большой. Моя бейсбольная команда выиграла соревнование. О, поздравляю, Джульетта Спасибо тебе, Ромео [ '[CLS]' , 'приятно' , 'познакомьтесь' , 'ты' , 'тоже' , 'как' , 'поживает' , 'ты' , 'сегодня' , '[SEP]' , '[MASK]' , 'поздравления' , '[МАСК]' , '[СЕНТЯБРЬ]' ] список замаскированных токенов: [ 27 , 22 ] предсказать список замаскированных токенов: [] isNext:   Ложное  предсказание isNext:   True

Итак, это было кодирование BERT с нуля. Если вы тренируете его на большом корпусе, вы можете использовать ту же модель для:

  1. Предварительное обучение: используйте любой корпус, но с точным форматом входного представления, как упоминалось ранее.
  2. Точная настройка: убедитесь, что вы используете для этого данные контролируемого обучения. 
  3. Экстрактор функций для различных задач или даже тематического моделирования. 

Полный блокнот вы можете найти здесь .

Есть ли способ получить предварительно обученную модель?

В исходной статье были выпущены две модели: BERT-базовая и BERT-большая. В статье я показал, как можно запрограммировать BERT с нуля. 

Как правило, вы можете загрузить предварительно обученную модель, чтобы вам не приходилось выполнять эти шаги. Библиотека Huggingface предлагает эту функцию: вы можете использовать библиотеку трансформеров Huggingface для PyTorch. Процесс остается прежним. 

У меня есть блокнот, где я использовал предварительно обученный BERT от Huggingface, посмотреть его можно здесь

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

Например:

импортные трансформаторы  класс  BERTClassification (nn.Module) :      def  __init__  (self) :         супер(BERTClassification, self).__init__()         self.bert = Transformers.BertModel.from_pretrained( 'bert-base-cased' )         self.bert_drop = nn.Dropout( 0.4 )         self.out = nn.Linear( 768 , 1 )      def  вперед (я, идентификаторы, маска, token_type_ids) :         _,pooledOut = self.bert(ids, focus_mask = маска,                                 token_type_ids=token_type_ids)         bertOut = self.bert_drop(pooledOut)         вывод = self.out(bertOut)          обратный вывод

Последние мысли

BERT — это очень мощная современная модель НЛП. Предварительно обученная модель обучается на большом корпусе, и вы можете точно настроить ее в соответствии со своими потребностями и на основе задачи на меньшем наборе данных. Самое лучшее в тонкой настройке то, что вы не делаете ее в течение 1000 эпох, она может имитировать производительность SOTA даже в 3–10 эпохах, в зависимости от параметров и того, насколько хорошо обрабатывается набор данных. 

Надеюсь, этот урок был интересным и информативным. И я надеюсь, что вы смогли что-то из этого извлечь. 

https://neptune.ai/blog/how-to-code-bert-using-pytorch-tutorial


Источник: neptune.ai

Комментарии: