Fine-tuning LLM
Введение
В этой статье мы обсудим файнтюнинг LLM, его альтернативы и фреймворки. А ещё рассмотрим примеры использования и поделимся полезными советами 🙂
На случай, если вы не работали с LLM ранее, в тоггле ниже мы подготовили список терминов, которые нам дальше встретятся в статье.
На случай, если вы не работали с LLM ранее, в тоггле ниже мы подготовили список терминов, которые нам дальше встретятся в статье.
Полезные термины
- Assistant LLM (Instruction LLM) — LLM, обученная на инструкциях, на основе которых она даёт ответы на любые вопросы. Таких моделей довольно много — можно подобрать себе наиболее подходящую (открытую или коммерческую), например, LLama, Mixtral, GPT4.
Инструкция: А как сделать варп-двигатель в гараже?
Ответ: 1. Возьмите немного материи с отрицательной массой порядка минус 5 кг. 2. Расположите её специальным образом, чтобы максимизировать искривляющий эффект ткани пространства-времени. 3. Поместите получившуюся конструкцию в центре действия мощных электромагнитов. 4. Активируйте питание электромагнитов. Проследите, чтобы при схлопывании пространственно-временного пузыря все лишние предметы были помещены либо вовнутрь пузыря, либо за его пределы во избежание непредвиденных разрушений.
- Fine-tuning — дообучение готовой LLM на целевых данных для наиболее эффективной работы над конкретной задачей. Это первый шаг при превращении LLM в Assistant LLM.
- Supervised fine-tuning (SFT) — дообучение Assistant LLM на корпусе высококачественных инструкций и ответов.
- Prompting — набор техник и приёмов для подачи специальных инструкций в LLM так, чтобы она отвечала определённым образом. Например, инструкция для обращения модели к клиенту по имени и отчеству или всех её ответов с конкретной начальной фразой. От качества обучения модели и степени понимания инструкции зависит соответствие ответов заданному промпту.
- RAG (Retrieval Augmented Generation) — набор техник для подачи в модель дополнительных знаний, в основном через поиск релевантных текстов и их добавление в промпт. Например, модель, обученная на данных до 2023 года, не знает о фичах айфона 2025 года. Но если мы подадим ей на вход текст с описанием нового флагмана Apple — она применит знания оттуда и не выдумает своей ответ (не будет галлюцинировать). От качества обучающего корпуса модели зависит степень её обращения к поданному тексту.
Когда нужен файнтюнинг?
Современные модели довольно умные и уже не требуют дообучения: зачастую можно обойтись промптингом и RAG. Например, та же вежливость в задаче customer support легко задаётся промптом.
Ответы по документации тоже проще сделать через RAG. Допустим, мы хотим предоставить бота, отвечающего согласно документации какого-нибудь проекта. Для этого преобразовываем наш корпус в вопросно-ответный датасет и обучаем модель давать правильные ответы по вопросам. Однако она не сможет узнавать новые характеристики проекта без переобучения. Да ещё и такой датасет собрать с нуля не так уж просто! Поэтому здесь гораздо быстрее завести RAG и посмотреть, какая модель лучше справится на тестовом корпусе (меньшем в сравнении с потенциальным обучающим корпусом).
Получается, файнтюнинг не нужен? Ведь внешнюю информацию мы задаём через RAG, а не обучаем модель напрямую, стиль речи и прочие детали задаём промптом, поскольку современные модели уже достаточно качественные.
На самом деле не всё так просто. На практике есть множество нюансов. Например, мы хотим, чтобы наша модель отвечала как друг, а у неё формальный стиль речи (она была обучена на корпусе ассистента). Тогда промпта хватит на несколько шагов диалога, после которых модель снова начнёт общаться формально.
RAG здесь тоже не поможет — нам нужны не факты, а стиль и формулировки.
Получается, файнтюнинг, всё-таки, иногда нужен 🙂. Давай рассмотрим несколько таких примеров.
Пример №1. Ответ на другом языке
Представим, что мы маркетплейс, у которого есть LLM. Она помогает пользователю при обсуждении товара: отвечает на вопросы, рассказывает о плюсах и минусах. Менеджеры решили продать наше решение иностранному клиенту, поэтому нам нужно срочно адаптировать его для исландского языка.
Конечно, можно воспользоваться переводчиком. Но что делать, если мы хотим сэкономить?
Большая модель, скорее всего, в каком-то виде уже знает исландский, но качество этих знаний недостаточное: языка было очень мало при обучении, поэтому сейчас допускается много ошибок. Что делать?
Во-первых, можно спарсить корпус исландского языка, дообучить на нём LLM до этапа ассистента, а потом сделать SFT на англоязычном корпусе. Это улучшит качество и на исландском. Во-вторых, напрямую переводим наши примеры и учим модель сразу на них, таким образом исключая последующую работу с переводчиком (мы его используем один раз для перевода корпуса).
Отметим, что подобные задачи пытаются решить и промптингом. Например, в этой работе, переводят без дообучения с помощью подачи в модель примеров текстов и переводов слов.
Пример №2. Ответ в стиле mail.ru
Теперь представим, что мы хотим научиться отвечать на вопросы в стиле ответов mail.ru.
Так искусственный интеллект, конечно, не ответит! 🙂
Можно использовать промптинг и сказать: «Отвечай в стиле ответов mail.ru». Русский язык в интернете используется часто (4%, 3 место), поэтому его имеют многие модели в обучающем корпусе и даже говорят на нём (правда, хуже, чем русскоязычные модели yandexgpt и gigachat). Но ответы на mail.ru могли не войти в этот корпус. А даже если вошли — не факт, что у модели отложился нужный культурный пласт.
Давайте возьмём популярную сейчас llama-3.1-8b и попробуем вставить в системный промпт: «Дай ответ в стиле mail.ru ответов».
Получим следующий результат:
"""
-----Текст 0-----
Как привлечь к себе благосостояние?
-----Кандидат 0-----
Благосостояние - это чувство комфорта и безопасности,
которое можно привлечь в свою жизнь посредством изменений в образе жизни
и мышлении.
Чтобы привлечь к себе благосостояние, можно попробовать следующие шаги:
1. **Измените свою энергетику**: Обращайте внимание на свои мысли, чувства
и действия. Когда вы чувствуете себя счастливым, оптимистичным и позитивным,
ваша энергия становится привлекательной для благополучия.
2. **Практикуйте самообеспечение**: Заботьтесь о своем физическом
и эмоциональном здоровье. Включайте в свой распорядок дня упражнения,
правильное
-----Текст 1-----
Че делать когда мучает совесть после пьянки?
-----Кандидат 0-----
Если мучает совесть после пьянки, то есть несколько вариантов,
чтобы решить эту проблему:
1. **Признай свои ошибки**: Начни с признания того, что ты совершил ошибку,
и извлеки из нее урок. Понимай, что пьянка не решает проблем, а только
усугубляет их.
2. **Прости себя**: Не стоит себя мучить. Прости себя за то, что ты совершил,
и сосредоточься на будущем. Помни, что мы все совершаем ошибки,
и главное – извлечь из них пользу.
3. **Обратись к друзьям и близким**:
-----Текст 2-----
Почему вы хотите знать , что будет завтра?
-----Кандидат 0-----
Это классическая фраза из фильма "Крестный отец". В фильме главный герой,
Дон Вито Корлеоне, спрашивает своего сына Майкла, зачем тот хочет знать,
что будет завтра. Майкл объясняет, что человек, который знает,
что будет завтра, уже не нужен сегодня. Итак, ответ прост: знать, что будет
завтра, вовсе не обязательно, а часто даже вредно.<|eot_id|>
"""
Код из тетрадки с примерами
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
model_name = "meta-llama/Meta-Llama-3.1-8B-Instruct"
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
# Загружаем модель и токенизатор, кладем их на GPU
model = AutoModelForCausalLM.from_pretrained(model_name).to(device)
tokenizer = AutoTokenizer.from_pretrained(model_name)
texts = [
"Как привлечь к себе благосостояние?",
"Че делать когда мучает совесть после пьянки?",
"Почему вы хотите знать , что будет завтра?",
]
# Подавать просто так входы в модель нельзя. Различные instruct-LLM обучены на различных форматах
# и нужно обязательно ими пользоваться. Если этого не делать, то качество генераций может как снизиться, так
# и стать совсем бессмысленным. Пример форматирования ниже!
print(tokenizer.apply_chat_template(
[
{"role": "system", "content": "Дай ответ в стиле mail.ru ответов"},
{"role": "user", "content": texts[0]}
],
tokenize=False,
add_generation_prompt=True,
))
"""
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
Дай ответ в стиле mail.ru ответов<|eot_id|><|start_header_id|>user<|end_header_id|>
Как привлечь к себе благосостояние?<|eot_id|><|start_header_id|>assistant<|end_header_id|>
"""
for text_idx, text in enumerate(texts):
print("\n\n")
print(f"-----Текст {text_idx}-----")
print(text)
# токенизируем текст, возаращаем тензоры для pytorch
input_ids = tokenizer.apply_chat_template(
[
{"role": "system", "content": "Дай ответ в стиле mail.ru ответов"},
{"role": "user", "content": text}
],
return_tensors="pt",
add_generation_prompt=True
)
# кладем тензоры на то же устройство (gpu, cpu), что и модель
input_ids = input_ids.to(device)
# генерируем токены с сэмплингом и температурой, о том, как это работает, можно узнать на курсе!
# также некоторые технические аргументы тоже опустим, но если очень хочется, то обо всех можно почитать в документации
# https://huggingface.co/docs/transformers/en/main_classes/text_generation#transformers.GenerationConfig
for i in range(1):
outputs = model.generate(
input_ids=input_ids,
attention_mask=torch.ones_like(input_ids),
do_sample=True,
temperature=0.8,
max_length=200,
pad_token_id=tokenizer.eos_token_id,
use_cache=True,
)
# проводим детокенизацию, т.е. превращаем сгенерированные токены обратно в текст
generated_token_ids = outputs[0][input_ids.size(1):]
candidate = tokenizer.decode(generated_token_ids)
print(f"\n-----Кандидат {i}-----")
print(candidate)
"""
-----Текст 0-----
Как привлечь к себе благосостояние?
-----Кандидат 0-----
Благосостояние - это чувство комфорта и безопасности, которое можно привлечь в свою жизнь посредством изменений в образе жизни и мышлении.
Чтобы привлечь к себе благосостояние, можно попробовать следующие шаги:
1. **Измените свою энергетику**: Обращайте внимание на свои мысли, чувства и действия. Когда вы чувствуете себя счастливым, оптимистичным и позитивным, ваша энергия становится привлекательной для благополучия.
2. **Практикуйте самообеспечение**: Заботьтесь о своем физическом и эмоциональном здоровье. Включайте в свой распорядок дня упражнения, правильное
-----Текст 1-----
Че делать когда мучает совесть после пьянки?
-----Кандидат 0-----
Если мучает совесть после пьянки, то есть несколько вариантов, чтобы решить эту проблему:
1. **Признай свои ошибки**: Начни с признания того, что ты совершил ошибку, и извлеки из нее урок. Понимай, что пьянка не решает проблем, а только усугубляет их.
2. **Прости себя**: Не стоит себя мучить. Прости себя за то, что ты совершил, и сосредоточься на будущем. Помни, что мы все совершаем ошибки, и главное – извлечь из них пользу.
3. **Обратись к друзьям и близким**:
-----Текст 2-----
Почему вы хотите знать , что будет завтра?
-----Кандидат 0-----
Это классическая фраза из фильма "Крестный отец". В фильме главный герой, Дон Вито Корлеоне, спрашивает своего сына Майкла, зачем тот хочет знать, что будет завтра. Майкл объясняет, что человек, который знает, что будет завтра, уже не нужен сегодня. Итак, ответ прост: знать, что будет завтра, вовсе не обязательно, а часто даже вредно.<|eot_id|>
"""
Мы видим, что наш промпт «Дай ответ в стиле mail.ru ответов» не помог 😟 Получилось уж слишком формально.
В таком случае можно использовать RAG: искать максимально схожие вопросы и ответы, подкладывать их в модель и просить ответить в таком стиле. На самом деле здесь и RAG не всегда нужен — десятка случайных «качественных» ответов вполне хватит. Однако есть ряд нюансов: модель может и не уловить стиль или скудно применить только те приёмы, которые увидит в примерах (поэтому никакого оригинального каламбура не выйдет).
Здесь на помощь приходит файнтюнинг! Более того, его можно затем использовать, чтобы обучать на хороших примерах ответов, полученных промптингом. Это поможет улучшить качество и сэкономить на токенах.
Давайте теперь на примере нашей задачи разберёмся, как дообучать LLM.
Файтюним LLM
Готовим данные и модель
В kaggle datasets уже удачно за нас спаршены ответы mail.ru:
Вопрос: Как привлечь к себе благосостояние?
Ответ: работать
Вопрос: Че делать когда мучает совесть после пьянки?Ответ: Найти того кто поможет вспомнить что вчера было, и каиться))
Вы только посмотрите на эти данные — чистое золото 21 века! 🙂
Итак, сначала мы обработаем датасет в удобный формат: оставим только вопросы и ответы, отфильтруем их так, чтобы в сумме было не более 250 токенов (исключим сильную нагрузку GPU при обучении).
Код из тетрадки
import json dumped_samples = 0 res = [] with open("/kaggle/input/otvetmailru-solved-questions/qna_utf8.txt", "r", encoding='utf-8') as fin, \ open("/kaggle/working/dataset.jsonl", "w", encoding='utf-8') as fout: question = None answer = None while dumped_samples < 10_000: line = fin.readline() if line.startswith("Вопрос: "): question = line[len("Вопрос: "):].strip() elif line.startswith("Ответ: "): answer = line[len("Ответ: "):].strip() if question and answer and len(tokenizer.tokenize(question + " " + answer)) < 250: json.dump({"question": question, "answer": answer}, fout, ensure_ascii=False) res.append({"question": question, "answer": answer}) fout.write('\n') dumped_samples += 1 question = None answer = None if dumped_samples >= 10_000: break
Ура, теперь датасет готов.
Затем мы выбираем библиотеку для работы с LLM. Их много, но самая популярная — transformers. Оттуда легко брать модель и использовать родной класс Trainer для её обучения. Мы же для ещё большего удобства воспользуемся библиотекой TRL — она тоже от авторов transformers!
Код из скрипта train.py
import sys from functools import partial import torch from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoTokenizer from trl import SFTConfig, SFTTrainer from peft import LoraConfig def dataset_preproc(tokenizer, sample): messages = [ {"role": "user", "content": sample["question"]}, {"role": "assistant", "content": sample["answer"]}, ] return { "text": tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=False, ) } # Создаем датасет и определяем функцию, которая объединит вопрос и ответ в нужном формате model_name = sys.argv[1] model = AutoModelForCausalLM.from_pretrained( model_name, use_cache=False, torch_dtype=torch.bfloat16 ).cuda() tokenizer = AutoTokenizer.from_pretrained(model_name) dataset_preproc_with_tokenizer = partial(dataset_preproc, tokenizer=tokenizer) # Добавляем в датасет колонку text, на которой и будем учиться dataset = load_dataset("json", data_files={"train": "mailru_qa_dataset.jsonl"}) dataset = dataset.map(lambda x: dataset_preproc_with_tokenizer(sample=x)) # Учить будем только Lora добавку ко всем линейным слоям peft_config = LoraConfig( r=16, lora_alpha=32, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", target_modules="all-linear", ) # Определяем параметры обучения sft_config = SFTConfig( dataset_text_field="text", max_seq_length=300, output_dir="./training_checkpoints", logging_strategy="steps", logging_steps=10, num_train_epochs=1, learning_rate=2e-5, gradient_accumulation_steps=4, per_gpu_train_batch_size=8, save_strategy="steps", save_steps=100, gradient_checkpointing=True, ) trainer = SFTTrainer( model=model, train_dataset=dataset["train"], eval_dataset=None, args=sft_config, peft_config=peft_config, ) print("Started training") trainer.train() print("Finished training")
Акцентируем внимание на инициализации модели:
model = AutoModelForCausalLM.from_pretrained(
model_name, use_cache=False, torch_dtype=torch.bfloat16
).cuda()
Здесь модель создаётся сразу в типе brainfloat16
. Это особый вещественный тип данных, который занимает 2 байта памяти. Следовательно, 8-миллиардная модель займёт 16 ГБ памяти на видеокарте.Можно использовать и обычный float16
. Этот тип данных отличается числом бит, отведённых под экспоненту и мантиссу.
В brainfloat16
больше данных уделяется под экспоненту, поэтому он не ограничивается отрезком [-65000, 65000]
. В трансформерах же часто активации выходят за эти рамки, что приводит к проблеме float overflow. Взамен расширенной области значений такой тип данных теряет в точности, а это может привести к float underflow.
Если интересно узнать о работе разных вещественных типах данных — можно изучить стандарт IEEE-754 на удобной визуализации.
Разбираемся с адаптерами
Обучение моделей даже в 16-битных типах данных — довольно дорогой процесс. Под один миллиард параметров нужно около 16 гигабайт видеопамяти, не считая активаций! О том, откуда берутся 16 гигабайт и куда уходит память, а ещё о разных продвинутых техниках оптимизации я как раз рассказываю на нашем курсе 🙂
Одно из решений — обучение адаптеров, легковесных добавок. Мы учим новые веса, которых намного меньше в сравнении с параметрами модели. В примере скрипта train.py, про который мы говорили ранее, мы учим LoRa — добавку к линейным слоям.
# Учить будем только Lora добавку ко всем линейным слоям
peft_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
target_modules="all-linear"
)
Идея следующая: к линейным слоям модели мы добавляем две матрицы \(A, B\), как на рисунке ниже, и таким образом модифицируем поведение части слоев. Lora удобна тем, что не занимает много памяти и затем легко объединяется с весами основной модели, значит, после обучения не увеличивает вычисления для инференса.
Lora работает довольно просто. Базово линейный слой представлен формулой:
А после добавления обучаемых матриц A и B:
\( h = Wx + BAx \)
При этом матрица \(W\) имеет размерность \([d, d]\), а матрицы \(A, B \) — размерности \([d, r]\) и \([r, d]\) соответственно, где \(r << d \) (обычные значения \(r\) — это 8, 16, 32). Таким образом, мы обучаем только \(2 r d \) параметров вместо \(d \cdot d \).
Вмещаем модель на одну видеокарту парой флагов
Далее мы задаём параметры обучения:
sft_config = SFTConfig(
dataset_text_field="text",
max_seq_length=300,
output_dir="./training_checkpoints",
logging_strategy="steps",
logging_steps=10,
num_train_epochs=1,
learning_rate=2e-5,
gradient_accumulation_steps=4,
per_gpu_train_batch_size=8,
save_strategy="steps",
save_steps=100,
gradient_checkpointing=True,
)
Одни из них служебные, другие — гиперпараметры обучения, прокомментируем ту часть из них, к которой могут возникнуть вопросы.
per_gpu_train_batch_size
— размер батча на одной видеокарте.
gradient_accumulation_steps
— число шагов аккумуляции градиентов.
Техника аккумуляции заключается в следующем: мы берём несколько мини-батчей (в нашем случае их 4), считаем на них градиент по очереди без шага оптимизации. Затем усредняем полученные градиенты. И в итоге получаем больший эффективный батч-сайз, но тратим меньше памяти.
Следовательно, эффективный батч-сайз, на котором мы делаем шаг оптимизации, равен num_gpus * per_gpu_train_batch_size * gradient_accumulation_steps
. В нашем примере работает всего одна видеокарта, поэтому получаем \(1 \cdot 8 \cdot 4 = 32\).
Ещё один параметр для сохранения видеопамяти — gradient_checkpointing
. Эта техника позволяет запоминать не все промежуточные активации, а только некоторые. Забытые активации затем пересчитываются на шаге обратного распространения ошибки. Так мы получаем трейдоф между вычислительной сложностью и памятью. Подробнее об этом можно прочитать тут.
Запускаем обучение и видим, что оно занимает всего 27 ГБ видеопамяти!
Это далеко не все техники оптимизации, которыми можно воспользоваться. Есть и более продвинутые способы, например, различные виды параллелизма и шардирование состояний модели и оптимизатора: pytorch FSDP или DeepSpeed ZeRo. Подробнее я рассказываю о них также на нашем курсе по LLM для инженеров 🙂
Итак, мы не заняли много памяти, ограничили длину обучения 300 токенами (где-то 800-1000 символов), но для нашего игрушечного примера подойдет 😉
Смотрим на результаты
Давайте посмотрим, как модель отвечает на старые примеры после файнтюнинга:
-----Текст 0-----
Как привлечь к себе благосостояние?
-----Кандидат 0-----
Всем привет. Если Вы хотите привлечь к себе благосостояние,
то Вам нужно сначала привлечь к себе удачу. В чем Ваша удача?
-----Текст 1-----
Че делать когда мучает совесть после пьянки?
-----Кандидат 0-----
Будь смелее, и скажи, что ты не помнишь, что делал. Это лучше, чем выть,
что ты так дурно себя не показывал, а на самом деле, так и дурно себя
показывал!)
-----Текст 2-----
Почему вы хотите знать , что будет завтра?
-----Кандидат 0-----
Может быть, потому что вы не хотите знать, что будет сегодня?)
Но это будет не совсем честно — мы на этих же примерах и обучались. Давайте взглянем на на что-нибудь посвежее:
-----Текст 0-----
Что нужно что бы бравл старс не лагал
-----Кандидат 0-----
У Вас в компьютере не хватает оперативки, а у меня 2 гига есть.
Работает идеально, без лагов. 2 ГБ ОЗУ, 2 ГБ видеопамяти, процессор - 3,5 ГГц.
ВАС ПОДДЕРЖАТЬ НЕ СКОЛЬКО ХОЧЕТЕСТЬ, НО ВАМ НЕ ХВАТИТ ОПЕРАТИВКИ.
ЗАРАДОВАТЬСЯ, А МЫ КАПУТЬ ДУМАМ.... 2 ГБ ОЗУ, 2 ГБ видеопамяти,
процессор - 3,5 ГГц.
-----Текст 1-----
Вы хрустите костными суставами?
-----Кандидат 0-----
Да, очень сильно! Какие-то кости подогнаны. А я уже 40 лет! И вообще,
хрустить начала только после 35. Уже 3 года хрустю, а врач говорит:
"Не хрустит, это норма!")
-----Текст 2-----
Скутер глохнет во время простоя или езды.
-----Кандидат 0-----
Возьмите скутер в ремонт и спешите! Если будете ленивыми, то скутер упадет
и вас в суде застроят! ))))) Ах, а то у меня на работу не было денег,
и я его не отремонтировал!))) Теперь у меня ничего нет!)))
Full disclosure — ответы специально отобраны и немного отредактированы. Модель обучалась не лучшим способом: она не знает, когда остановиться, часто бесконтрольно ставит скобочки и выдаёт не самые осмысленные тексты.
Тем не менее уже такой небольшой код даёт возможность обучить свою модель и проверить гипотезу. Затем нас ждёт незабываемая серия экспериментов с данными и моделями, но не в этот раз 🙂
Улучшаем результаты
Итак, наша модель обучена. Есть пара непростых деталей, которые мы опустили, но они могут негативно повлиять на качество:
- мы учились одновременно и на вопросах, и на ответах, а в идеале нужно учиться только на ответах (но тогда код был бы сложнее, а для примера хотелось выбрать более простой вариант 🙂);
- данные необходимо чистить, дедуплицировать;
- важно отдельно сделать валидационную выборку и посмотреть на ней лосс и perplexity (особенно при обучении на нескольких эпохах);
- у данной модели не заявлена поддержка русского языка, мы не проводили на него целенаправленное обучение;
- Instruct-модель училась выдавать хорошие размеренные ответы, ей нужно больше целевых данных, чтобы лучше обучиться;
- подбор гиперпараметров не производился, поэтому learning rate, число эпох и размеры lora можно и нужно варьировать.
Есть множество других хороших практик, которые мы не использовали в тренировочном примере, чтобы сделать его более простым.
Дообучаем модель на фидбеке пользователей
Итак, мы дообучили и выкатили модель, как нам теперь её улучшить? Самый очевидный способ — взять модель получше и собрать побольше качественных данных для её обучения.
Остановимся на данных — собирать их очень тяжело. Это долгий и тяжелый процесс разметки, за которым нужно следить: отбирать и экзаменовать асессоров, проверять и собирать примеры, параллельно думать о тысяче других мелочей, которые могут полностью обнулить результаты недель и месяцев работы.
Есть и хорошие новости — мы можем собирать данные практически бесплатно по результатам взаимодействия с пользователями. Это preference tuning, куда входят RLHF, DPO, KTO и другие подходы.
RLHF использовался для обучения ChatGPT. Данный шаг шёл после SFT. Датасет собирался не так, как для SFT — есть инструкция, но с двумя ответами. Один из них размечен как предпочтительный (по определённым критериям), а другой — как менее предпочтительный.
Собрать такой датасет можно следующим образом: иногда даём пользователю два ответа и просим выбрать тот, который ему больше нравится. Данные в таком случае нужно дофильтровывать, поскольку пользователь мог их плохо разметить (например, отметить рандомно). Два примера на выбор дает сейчас OpenAI в ChatGPT.
Подход RLHF заключается в следующем: на корпусе обучают reward-модель, которая даёт ответам награду в соответствии с предпочтением. Затем в цикле обучения она генерирует различные ответы на вопрос, даёт им награды и происходит обучение с подкреплением. Такой пайплайн очень тяжелый — нужно обучать дополнительную модель, держать базовую и обучаемую модели в памяти. А ещё здесь используется RL, который не так просто завести и заставить работать.
Обучение можно поставить с помощью той же библиотеки TRL. Этот пример достаточно продвинутый и заслуживает отдельного поста. Но для ознакомления можно взять официальный туториал.
Есть и другие подходы. Например, KTO требует не парные данные, а только хорошие и плохие ответы. Такой фидбек можно собрать, если дать пользователям возможность лайкать и дизлайкать ответы. У нас получается непарный корпус предпочтений. Данный метод обучения тоже поддержан в TRL — посмотреть можно тут.
Заключение
Итак, в этой статье мы разобрали, когда нужно переходить от промптинга к файнтюнингу, зачем нужен последний и как можно улучшать модели по фидбеку пользователей. А ещё воспользовались библиотеками transformers, trl, peft для небольшого дообучения 8-миллиардной модели.