Parameter-Efficient Fine-Tuning (PEFT/LoRA): Теория и практика

50 минут Урок 24

Введение: Почему Full Fine-Tuning — это «пушка по воробьям»

Приветствую, коллеги! Мы подобрались к одной из самых захватывающих тем в современной разработке LLM — PEFT (Parameter-Efficient Fine-Tuning). Чтобы понять её ценность, давайте начнем с проблемы.

Представьте, что у вас есть модель уровня Gemini 1.5 Pro или Flash. Она обладает огромными знаниями о мире. Но вам нужно, чтобы она стала экспертом в узкой области — например, в юридическом документообороте РФ или в написании сценариев в стиле Тарантино.

Классический подход (Full Fine-Tuning) предлагает нам обновить все веса модели. Представьте, что для того, чтобы научить энциклопедиста новому рецепту пирога, вы заставляете его переписывать всю энциклопедию заново. Это:

  • Дорого: Требует огромных вычислительных мощностей (GPU-кластеров).
  • Тяжело: Полученная модель весит столько же, сколько исходная (сотни гигабайт).
  • Рискованно: Возникает эффект «катастрофического забывания» (Catastrophic Forgetting), когда модель учит новое, но забывает старое.

Здесь на сцену выходит PEFT и его главный герой — метод LoRA (Low-Rank Adaptation). Это технология, которая позволяет нам не переписывать энциклопедию, а просто вклеить в неё несколько стикеров с важными заметками, которые меняют поведение модели.

Что такое LoRA: Объясняем на пальцах (и матрицах)

LoRA замораживает веса предварительно обученной модели и внедряет обучаемые матрицы ранговой декомпозиции (rank decomposition matrices) в каждый слой архитектуры Transformer.

Как это работает?

В нейронной сети веса хранятся в огромных матрицах (обозначим их $W$). При обычном обучении мы меняем эти веса ($W_{updated} = W + \Delta W$). Проблема в том, что матрица изменений $\Delta W$ имеет тот же гигантский размер, что и $W$.

Авторы LoRA заметили, что изменения весов при адаптации под конкретную задачу на самом деле имеют «низкий внутренний ранг». Это значит, что нам не нужно управлять миллионом параметров, чтобы изменить стиль речи модели. Достаточно найти ключевые векторы изменений.

Вместо обучения полной матрицы $\Delta W$, мы представляем её как произведение двух очень маленьких матриц $A$ и $B$:

$\Delta W = B \times A$

Где:

  • Матрица $A$: Сжимает данные (например, размерность 4096 -> 8).
  • Матрица $B$: Разжимает данные обратно (8 -> 4096).
  • Ранг (Rank, $r$): Тот самый размер «горлышка» (в примере выше $r=8$).

Итог: Мы обучаем только матрицы $A$ и $B$. Исходная модель остается нетронутой. Это сокращает количество обучаемых параметров в 10,000 раз и потребление памяти GPU в 3 раза.

Gemini и адаптация: Практический подход

В экосистеме Google (Vertex AI и AI Studio) процесс Fine-Tuning для Gemini часто абстрагирован, но под капотом используются именно принципы PEFT/LoRA. Это позволяет вам создавать «тюнингованные» модели (Tuned Models), которые работают как адаптеры поверх базовой модели.

Ключевые преимущества такого подхода при работе с API:

  1. Мгновенное переключение: Вы можете использовать одну базовую модель и на лету подключать к ней разные адаптеры (один для саппорта, другой для маркетинга) просто меняя ID модели в запросе.
  2. Скорость обучения: Тюнинг занимает от 15 минут до пары часов, а не дни.
  3. Стоимость: Вы платите только за обучение маленького набора параметров.

Давайте посмотрим, как подготовить данные. Для PEFT качество данных критически важнее их количества. Вам хватит 100–500 качественных пар примеров.

python
# Пример структуры данных для тюнинга Gemini (JSONL формат)
# Каждая строка - это отдельный обучающий пример.

# Файл: training_data.jsonl

{"messages": [{"role": "system", "content": "Ты - саркастичный помощник IT-поддержки. Отвечай кратко и с юмором."}, {"role": "user", "content": "У меня не работает принтер."}, {"role": "model", "content": "А вы пробовали его включить? Или он просто устал от ваших распечаток мемов?"}]}
{"messages": [{"role": "system", "content": "Ты - саркастичный помощник IT-поддержки. Отвечай кратко и с юмором."}, {"role": "user", "content": "Забыл пароль."}, {"role": "model", "content": "Классика. Надеюсь, он не был '123456'. Пишите админу, несите шоколадку."}]}
{"messages": [{"role": "system", "content": "Ты - саркастичный помощник IT-поддержки. Отвечай кратко и с юмором."}, {"role": "user", "content": "Интернет медленный."}, {"role": "model", "content": "Пакеты данных идут пешком. Подождите, пока они отдышатся."}]}

Запуск процесса тюнинга через Python SDK

Теперь, когда у нас есть данные, давайте запустим процесс создания тюнингованной модели. Мы будем использовать библиотеку `google-generativeai`. Обратите внимание: хотя мы не настраиваем матрицы $A$ и $B$ вручную (как в PyTorch), мы задаем параметры обучения, которые влияют на сходимость адаптера.

Ключевые гиперпараметры (Hyperparameters):

  • Epochs (Эпохи): Сколько раз модель увидит весь ваш датасет. Для малых данных (100 примеров) ставьте 5-10 эпох. Если больше — модель просто «зазубрит» примеры (overfitting).
  • Batch Size: Сколько примеров обрабатывается за раз. Обычно API подбирает это автоматически, но полезно знать.
  • Learning Rate: Скорость обучения. Слишком быстро — пропустим оптимум, слишком медленно — будем учиться вечно. В Gemini Tuning API часто используется множитель (multiplier) для базового LR.

python
import google.generativeai as genai
import time

# Настройка API ключа
genai.configure(api_key="YOUR_API_KEY")

# 1. Загрузка файла с данными
base_model = "models/gemini-1.5-flash-001-tuning" # Базовая модель, поддерживающая тюнинг
training_file = genai.upload_file(path="training_data.jsonl")

print(f"Файл загружен: {training_file.name}")

# 2. Создание задачи на тюнинг (Fine-Tuning Job)
operation = genai.create_tuned_model(
    # Уникальный ID для вашей новой модели
    id="my-sarcastic-it-bot-v1",
    source_model=base_model,
    training_data=training_file,
    # Гиперпараметры
    epoch_count=5,
    batch_size=4,
    learning_rate=0.001,
    # Описание для удобства в AI Studio
    description="Бот техподдержки с саркастичным характером на базе LoRA подхода"
)

print("Запущен процесс обучения...")

# 3. Ожидание завершения (в реальном проекте лучше делать это асинхронно)
model_result = operation.result()

print(f"Модель готова! Имя ресурса: {model_result.name}")

# 4. Использование новой модели
model = genai.GenerativeModel(model_name=f"tunedModels/my-sarcastic-it-bot-v1")
response = model.generate_content("Сервер упал, что делать?")
print(response.text)

Когда PEFT лучше, чем Prompt Engineering?

Это самый частый вопрос. Зачем мучиться с обучением, если есть контекстное окно в 1-2 миллиона токенов?

КритерийPrompt Engineering / Few-ShotPEFT / Fine-Tuning
Стиль и тонСложно удерживать на длинных диалогах. Модель может «свалиться» в стандартный тон.Идеально. Тон «вшивается» в веса адаптера.
Новые знанияМожно загрузить в контекст (RAG), но дорого при каждом запросе.Плохо для фактов (галлюцинации), но хорошо для терминологии и сленга.
Формат выводаМожет ошибаться в сложных JSON/XML структурах.Жестко фиксирует структуру ответа (например, всегда выдавать SQL без пояснений).
Цена/ЗадержкаВысокая (платите за длинный промпт каждый раз).Низкая. Промпт короткий, модель уже «знает» контекст.

Золотое правило: Используйте PEFT, когда хотите изменить форму (как модель говорит) или научить узкоспециализированным паттернам, которые сложно описать словами. Не используйте PEFT, чтобы научить модель истории Древнего Рима — для этого есть RAG.

Упражнение

Спроектируйте датасет для задачи 'Summarization to Hashtags'.<br><br>Ваша задача:<br>1. Определить цель: модель должна получать текст новости и выдавать ТОЛЬКО список из 3-5 релевантных хештегов (без вводных слов).<br>2. Написать 2 примера обучающих данных в формате JSONL, которые научат модель этому поведению.<br>3. Объяснить, почему для этой задачи лучше подойдет PEFT, чем просто промпт, если мы планируем обрабатывать 100,000 новостей в день.

Вопрос

В методе LoRA мы используем параметр Rank (r). Что произойдет, если мы выберем слишком маленькое значение r (например, r=1) для сложной задачи?