Чат-бот ChatGPT наделал много шума: люди задают ему вопросы и удивляются точности ответов, студенты с его помощью пишут дипломные работы, а копирайтеры публикуют статьи с названием типа «Сможет ли ChatGPT заменить копирайтеров?».
Мы решили проверить технологию, на которой основан ChatGPT, посмотреть актуальное состояние Open Source GPT-like моделей и ответить на вопрос — можно ли обучить GPT-like модель в домашних условиях?
Для эксперимента выбрали LLaMAGPT-J, а также не самый мощный ПК с видеокартой Nvidia GTX 1080TI с 11 GB VRAM. Оказалось, что этого достаточно не только для того, чтобы загрузить модели, но и дообучить их (fine-tune). Рассказываем – как мы это сделали.
LLaMA и GPT-J
GPT (англ. Generative Pre-trained Transformer — генеративный предобученный трансформер) — это гигантская нейронная сеть, которая обучена на огромном количестве данных предсказывать следующее за последовательностью слово. GPT моделирует естественные языки, помнит контекст и генерирует тексты. Для тех, кто хочет узнать побольше про архитектуру GPT, мы оставим ссылки [1] и [15].
Рассмотрим таблицу для сравнения некоторых наиболее популярных моделей этого типа.
Сравнить модели в таблице можно по значениям бенчмарков:
LAMBADA — это датасет, который используется как бенчмарк для предсказания слова по широкому контексту;
Winorgande — это датасет с текстовыми задачами и вариантами ответов;
Hellaswag — датасет с текстовыми задачами на здравый смысл;
PIQA — также бенчмарк для оценки здравого смысла – нацелен на оценку физических знаний модели.
Недавно представленная LLaMA [20] от компании Meta показывает отличные результаты и является лидером среди Open Source моделей, уступая лишь выдающейся ChatGPT. Самая маленькая LLaMA уступает только гигантской Davinci. Она обучена на триллионе токенов, что на порядок больше, чем у любой другой модели.
Стоит также обратить внимание на GPT-J от EleutherAI. Долгое время эта модель оставалась лучшей из Open Source и у нее отличная поддержка сообщества. В сравнении с моделями от OpenAI GPT-J она показывает сопоставимые результаты с моделью Curie и уступает лишь гигантской Davinci. Объем тренировочного датасета GPT-J также внушителен — 825 GB [4], а количество параметров (весов) модели ~ 6 миллиардов, что на один миллиард меньше, чем у младшей LLaMA.
В качестве основного стека для дообучения LLaMA и GPT-J мы выбрали Python, transformers, peft, gptq и pytorch lightning. Эти модели и другие достижения Open Source сообщества можно найти на Hugging Face Hub [5].
Quantized Large Language Models
Модель от EleutherAI с float16 параметрами требует около 21 GB VRAM, младшая модель от Meta требует примерно 24 GB — а это для нас проблема. Чтобы как-то уместить этих монстров на наш ПК, мы можем прибегнуть к квантизации [6, 7] — хитрым способом сопоставить оригинальные float16 параметры модели с int параметрами и уменьшить объем занимаемой памяти в два раза. Кажется, что будет потеря в точности, но судя по результатам сравнения GPT-J и GPT-J-8bit, разница в точности этих моделей в пределах погрешности [8].
8-битная квантизация уже доступна в transformers благодаря библиотеке bitsandbytes. Нам только нужно добавить параметры load_in_8bit и device_map при загрузке модели, чтобы указать на какое устройство загружать модель.
8-битная GPT-J занимает примерно 6 GB на GTX 1080 TI, LLaMA — около 7.5 GB. Но мы можем пойти дальше и квантизовать модель до 4 бит и еще больше сократить количество требуемой памяти. Квантизовать любую модель можно с помощью библиотеки GPTQ, что может потребовать большое количество RAM для загрузки модели на CPU. К счастью, на Hugging Face Hub уже можно найти разнообразие квантизованных моделей, в том числе 4-битные версии семейства LLaMA.
4-битная LLaMA-7B требует примерно 4.3 GB видео памяти. Такую модель можно дообучать даже на 8 GB видеокартах! 4-битная квантизация позволяет нам обучать LLaMA-13B на нашей видеокарте.
Прежде, чем приступать к дообучению, нужно разобраться, что принимают и что возвращают модели — input и output соответственно, и какая у этого размерность. Модель принимает последовательность слов и возвращает вероятности следующего слова для каждого из последовательности. GPT очень внимательный (привет, attention!) слушатель, который, услышав слово, сразу же пытается предсказать следующее.
Input
Так выглядят входные данные:
GPT-J и LLaMA принимают идентичные входные данные: вектор токенов (input_ids) и attention mask. Оба элемента с максимальной длиной 2048.
Вектор токенов состоит из целых чисел: id токенов из словаря модели. Словарь GPTJ содержит 50257 токенов, LLaMA — 32000.
Attention mask: вектор с нулями и единицами, задача которого — сообщить модели – на какие токены обращать внимание (1), а на какие — нет (0).
Проще говоря, нам нужно общаться с GPT словами, которые уже есть в его словаре, а также акцентировать внимание на важных для беседы словах. Например, есть несколько случаев, когда мы используем 0 в attention mask. Во-первых, мы используем 0 для паддинг-токенов, когда собираем сэмплы в батч (порцию для обучения) и нам нужно сделать паддинг, чтобы длины сэмплов совпадали. Во-вторых, в процессе обучения мы можем заставить модель не обращать внимания на некоторые токены в сэмпле (например, на те, которые мы хотим предсказать). Преобразовать текст в нужные векторы помогает токенизатор. Отметим, что токенизатор LLaMA в отличие от токенизатора GPTJ добавляет специальный токен начала текста.
На иллюстрации выше видно, как sample_text после токенизации превращается в input_ids и attention_mask.
Output
Внимательный GPT постоянно пытается предсказать следующее слово, но кроме внимательности мы еще можем рассчитывать на его хорошую память. Модель по умолчанию возвращает вероятности следующих токенов (logits) и внутреннюю память модели (past_key_values).
Размер тензора logits — (batch_size, sample_len, vocab_size). Наш исходный сэмпл 'Hello, GPT-J! How are you? -' содержит 12 токенов, поэтому logits имеет форму (1, 12, 50400) для GPTJ. Учитывая особенности токенизации и свойства словаря LLaMA для того же сэмпла logits будет иметь форму (1, 13, 32000). Меньший размер словаря также экономит нашу память.
Для каждого токена в исходном сэмпле мы получаем вероятности для всех токенов в словаре модели. При генерации текста нас интересует только вероятность для последнего токена. В дообучении модели могут участвовать все или только избранные вероятности для входной последовательности.
past_key_value — это и есть память нашей модели. Размер тензора: (n_layers, key_value, batch, n_attention_heads, sample_len, head_embedding_dimension);
n_layers — это количество слоев
key_value — кортеж из ключей и значений в контексте механизма внимания (Attention) [10];
batch — размер батча;
n_attention_heads — количество голов attention;
sample_len — длина сэмпла;
head_embedding_dimension — внутренний размер
n_layers, key_value, n_attention_heads, head_embedding_dimension — размерности, которые относятся к конфигурации модели.
Тестовая задача
Представим, что мы хотим научить GPT модерировать чаты — модель будет классифицировать сообщения на 3 класса: hate (содержащее ненависть), offensive (содержащее угрозу), neutral (нейтральное).
Для этого будем использовать Hate Speech and Offensive Language Dataset [11] . Поскольку GPT уже много знает о языке и словах, мы уменьшим количество тренировочных данных и возьмем случайным образом лишь 1000 примеров, предварительно сбалансировав классы. Для валидации выберем 200 сбалансированных примеров. Нам предстоит обучить GPT-J-8bit генерировать нужную метку класса.
Как подготовить данные?
Когда мы хотим чему-то научить GPT, нам нужно сказать ему начало фразы, а затем оценить его вариант завершения фразы, поэтому тренировочный сэмпл содержит две части — затравку и завершение.
Подготовка входного сэмпла играет важную роль для обучения модели. В тренировочный сэмпл, кроме самого текста, который подлежит классификации, можно добавить дополнительную информацию: начиная от специальных разделителей блоков сэмпла и заканчивая целыми инструкциями. К важному можно отнести специальные токены, которые отделяют затравку от завершения (то есть от того, чему мы хотим обучить модель). С помощью токенов-разделителей мы как бы указываем – когда ждем от нашего GPT выполнения желаемого.
Нам удалось добиться одинаково хорошей точности как в случае с затравками-инструкциями, так и в случае затравок, состоящих только из целевого текста с разделителем.
Единственный минус инструкций состоит в том, что входные сэмплы получаются длиннее. Но зато, благодаря инструкциям, модель лучше научится понимать – чего мы от нее хотим: GPT видит инструкцию и, соотнося ее с результатом, понимает, что все варианты ответов перечислены в инструкции и ей нужно только выбрать правильный. Это дает возможность научить GPT решать другие классы задач, которые могут быть сформулированы в рамках той же структуры инструкций.
Что обучать? (Low-Rank Adapters)
Тренировка квантизованных целочисленных параметров привычными алгоритмами не представляется разумным подходом хотя бы потому, что область значений функции потерь cross entropy loss лежит в [0, 1]. Но даже квантизация не избавляет нас от тренировки огромного количества параметров и затрат на вычисления.
Авторы статьи LoRA: Low-Rank Adaption of Large Language Models [12] предлагают очень интересный и эффективный способ дообучения огромных моделей. Зачем тренировать все параметры модели? Давайте заморозим все слои и к каждому добавим тренируемый адаптер с низкой размерностью. Адаптер представляет собой два линейных слоя A и B размера (d, r) и (r, d). И вместо тренировки «родного» слоя модели W размера (d,d) (а у GPT-J и LLaMA-7B d равен 4096) предлагается тренировать две матрицы поменьше. Авторы LoRA демонстрируют, что оптимальный r равен 2, чем оправдывают название своего подхода Low-Rank Adaption. Его логика отображена на схеме:
Как обучать? (AdamW8bit)
Загрузить модель в оперативную память видеокарты — это еще не приручить ее. Для дообучения GPT стандартными техниками требуется еще одна видеокарта, но мы обойдемся без нее и воспользуемся квантизованным оптимизатором.
Стандартным инструментом обучения параметров модели является оптимизатор Adam. Нам же нужен какой-то экономный аналог. В статье 8-Bit Optimizers via Block-wise Quantization [13] авторы предлагают квантизовать оптимизатор, в частности, его состояния, которые включают статистику по градиентам (поправкам) для параметров модели. Более того, предлагается блочная (block-wise) квантизация, которая может выполняться параллельно. Картинка из оригинальной статьи прекрасно иллюстрирует суть этого:
Спасибо авторам за то, что они также опубликовали репозиторий bitsandbytes [14] с реализацией их подхода.
Последние приготовления и обучение
Мы описали ключевые способы, которые помогут нам приручить GPT-J. Но для того, чтобы научить GPT делать то, что нам нужно, необходимо как-то указывать ему на его ошибки. Мы используем loss mask в своем коде для того, чтобы взять из предсказаний модели только то, что должно быть сгенерировано, и сделать обратное распространение ошибки только по этим токенам. Так мы не будем считать ошибку на затравке, а посчитаем loss только на продолжении. И модель будет лучше понимать, что мы от нее хотим. Также если мы решаем задачу классификации, можно подсчитать точность предсказаний — это и будет метрикой оценки модели.
Вот так выглядит шаг обучения модели:
В зависимости от длины сэмплов и объема памяти видеокарты мы можем использовать батчи. Нам потребуется привести к одинаковой длине все сэмплы — сделать паддинг коротких сэмплов и обрезать длинные. Батчинг поможет ускорить процесс обучения.
После того, как все необходимые архитектуры с адаптерами собраны, данные для обучения загружены и предобработаны, а оптимизатор AdamW8bit наготове, мы можем приступать к fine-tuning’у и валидации.
Будем обучать модели на протяжении пяти эпох. Ранг LORA - 16. Размер батча 2 - 4 в зависимости от модели. Также сравним следующие конфигурации:
GPT-J 6B 8 бит
LLaMA 7B 8 бит
LLaMA 7B 4 бита
LLaMA 13B 4 бита
Few-shot — без обучения
Мы имеем дело с «умной» моделью, которая много знает о языке, поэтому можем вообще отказаться от тренировочных данных и положиться на саму модель. Few-shot подход, описанный в статье от команды OpenAI [15], предлагает использовать несколько тренировочных сэмплов в затравке, получая в результате завершения для новых случаев.
Результаты
Для начала взглянем на отчет [16]. График с loss нам нужен, чтобы убедиться, что модель обучается и loss уменьшается со временем от эпохи к эпохе. А от accuracy мы ожидаем, что она будет расти.
Теперь испытаем модели OpenAI через API в идентичных условиях. Вот отчет по fine-tuning’у GPT-3 Ada и GPT-3 Davinci [17]. Предположительно, значение loss отличаются на порядок от нашей модели из-за различия функций потерь. В нашем подходе мы использовали стандартную cross entropy.
Взглянем на таблицу точности предсказаний всех опробованных моделей и подходов:
Нам удалось превзойти точность моделей от OpenAI в тестовой задаче модерации при идентичных условиях. Лучшая конфигурация – GPT-J 8 бит. ChatGPT (GPT-3.5-turbo) дает хорошую точность без дообучения, но уступает всем дообученным моделям. Few-shot LLaMA показала себя хуже ChatGPT.
Мы смогли дообучить GPT-J и LLaMA в домашних условиях, причем так, что они не уступают разработкам больших компаний. Однако важно отметить, что если дообучать OpenAI GPT-3 модели предложенным в документации API способом — без инструкций, только сырой текст с разделителем — то Davinci демонстрирует идентичную точность — 84 %. С другой стороны, использование дообученных моделей OpenAI будет стоить денег: например, за генерацию приблизительно двух печатных страниц текста мы заплатим 12 центов для самой крутой модели, причем в стоимость входят и затравки.
Преимущества инструкций в затравках
Чтобы подтвердить, что решение помещать в затравки инструкции действительно имеет преимущества, мы провели еще один эксперимент — предложили модели выбрать категорию для заголовков новостей с сайта BBC.
Дообученная модель показывает, что благодаря инструкциям в затравках она научилась понимать, что мы от нее хотим, без дополнительного обучения на новых категориях.
Также мы решили провалидировать различные конфигурации моделей на новом домене, чтобы оценить общую адекватность моделей. Мы случайным образом взяли 100 кратких описаний для новостей из датасета на Hugging Face [19]. Теперь мы попросим модели классифицировать новости следующих категорий: business, entertainment, food, parenting, politics, style, travel.
Оказалось, что лучшая в задаче модерации GPT-J довольно плохо справляется с новым доменом. В то же время дообученная нами LLaMA 7B 8bit показывает отличный результат и уступает лишь ChatGPT. А вот результаты 4-битных моделей оставляют желать лучшего. Также мы испытали Alpaca, основанную на LLaMA, которую недавно предложили в Стэнфордском университете. Alpaca как и ChatGPT обучена на выполнение инструкций.
Заключение
ChatGPT сегодня является самой универсальной моделью в решении задач, связанных с генерацией и извлечением информации из текста. Кажется, Open AI не собирается уступать первенство и будет стараться сохранять монополию. На сегодняшний день Open Source вместо универсальности может предложить более высокую точность в решении конкретных прикладных задач.
В вопросе точности нам до сих пор нужно быть нянькой для искусственного интеллекта: готовить данные и придумывать все более эффективные способы дообучения. Одомашнить многообещающую LLaMA, а также GPT-J помогли концепции тренируемых адаптеров, квантизации и квантизуемого оптимизатора. Мы также предлагаем ознакомиться с репозиторием [18], который воспроизводит все полученные здесь результаты.
Ссылки
https://training-transformers-together.github.io
[2106.09685] LoRA: Low-Rank Adaptation of Large Language Models (arxiv.org)
[2110.02861] 8-bit Optimizers via Block-wise Quantization (arxiv.org)
TimDettmers/bitsandbytes: 8-bit CUDA functions for PyTorch (github.com)
[2005.14165] Language Models are Few-Shot Learners (arxiv.org)
https://api.wandb.ai/links/vetka925/xwr6ici7
heegyu/news-category-balanced-top10 · Datasets at Hugging Face
llama/MODEL_CARD.md at main · facebookresearch/llama (github.com)
Автор статьи: Виталий Гречачин, Data Scientist в компании Neoflex.