Тонкая настройка модели

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

  • Первый — предварительное обучение с нуля. Для этого требуется собрать огромные объёмы текстовых данных (возможно, триллионы токенов), арендовать десятки A100 GPU для обучения на протяжении месяцев и потратить миллионы долларов. Так делают гиганты вроде OpenAI и Google, и для подавляющего большинства людей это нереалистично.

  • Второй — Prompt engineering. Дайте универсальной большой модели тщательно продуманный промпт, например: «Вы профессиональный копирайтер для электронной коммерции. Пожалуйста, создайте привлекательное описание на основе следующей информации о товаре...» Этот подход не требует затрат и сразу применим, но есть проблемы: приходится каждый раз писать длинные промпты, результаты нестабильны, их легко нарушает нерелевантный ввод, и потребляется много токенов.

  • Третий — тонкая настройка. Возьмите обученную универсальную модель, продолжите её обучение в течение короткого времени на ваших собственных данных и позвольте ей «изучить» вашу конкретную задачу. Полученная модель сохраняет общие способности, при этом отлично справляется с вашей задачей, а затраты контролируемы.

Сравнение трёх подходов:

ПодходСтоимостьЭффективностьПрименимые сценарии
Предварительное обучение с нуляЧрезвычайно высокая (миллионы долларов)Полностью кастомизированнаяСоздание новой фундаментальной модели (только для гигантов)
Промпт-инжинирингПочти нольНестабильно, зависит от промптовПростые задачи, быстрая проверка
ДообучениеСредняя (от сотен до тысяч долларов)Стабильно и профессиональноЗадачи в конкретной предметной области, продуктизация

Используйте простую аналогию:

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

Почему необходима тонкая настройка

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

Ограничения промпт-инжиниринга

Промпт-инжиниринг — мощный подход, но у него есть несколько потолков, которые трудно преодолеть.

  • Первое ограничение — это лимит контекстного окна.Ваши промпты и примеры должны помещаться в контекстное окно модели (например, 4K, 8K, 128K, 1024K токенов). Если вашей задаче для ясного описания требуются сотни примеров, промпт не сможет их вместить.

  • Второе ограничение — нестабильные результаты.Один и тот же промпт может давать совершенно разные результаты при разной формулировке. Когда модель в «хорошем настроении» (случайность сэмплирования), вывод хороший; когда нет — он может полностью уйти от темы.

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

  • Четвёртое ограничение — проблема забывания.Хотя модель прочитала ваш промпт, огромный объём данных предварительного обучения, заложенный в неё ранее, всё ещё может тянуть её вниз. Например, если вы попросите её вывести JSON, она всё равно может добавить много объяснений на естественном языке.

Сценарии применения тонкой настройки

Тонкую настройку стоит рассматривать, когда ваша задача соответствует следующим характеристикам.

  • Характеристика 1: Чёткое определение задачи и фиксированный формат вывода. Например, «перевести техническую документацию в понятный блог», «генерировать краткие описания тикетов из записей службы поддержки», «преобразовать естественный язык в SQL-запросы».

  • Характеристика 2: У вас есть от нескольких сотен до нескольких тысяч высококачественных размеченных образцов данных. Слишком мало данных не даст результата от тонкой настройки, а слишком много — излишне; обычно 500-5,000 образцов — идеальный диапазон.

  • Характеристика 3: Вам нужна профессиональная производительность в конкретной предметной области. Например, интерпретация медицинских отчётов, суммирование юридических документов, анализ финансовых новостей. Модели общего назначения могут не понимать отраслевую терминологию и соглашения; дообучение может помочь им «войти в отрасль».

  • Характеристика 4: Вы чувствительны к скорости ответа и стоимости. Небольшая дообученная модель может превзойти большую модель общего назначения, обеспечивая в 10 раз более высокую скорость инференса и в 100 раз меньшую стоимость.

  • Характеристика 5: Вам нужно строгое соблюдение формата вывода. Например, вывод должен быть в формате JSON, должен использовать определённый тон, должен включать определённые поля. Дообучение позволяет модели «запомнить» эти требования, не повторяя их в промпте каждый раз.

Анализ затрат и выгод от дообучения

Прежде чем заниматься дообучением, сначала посчитайте.

Затраты в основном складываются из трёх аспектов: подготовка данных, вычислительные ресурсы и ручная отладка. Подготовка данных обычно требует больше всего работы — нужно собрать, очистить и разметить данные. Вычислительные ресурсы сейчас очень дешёвы; с LoRA + QLoRA потребительская GPU (RTX 3090/4090) может дообучать модели 7B/13B.

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

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

ФакторЛучше подходит для промптаЛучше подходит для дообучения
Объём данных< 100 примеров> 500 примеров
Стабильность задачиЧасто меняетсяОтносительно стабильна
Частота использованияЭпизодическое использованиеВысокочастотное использование
Чувствительность к стоимостиНизкаяВысокая
Требования к качествуДостаточно хорошего качестваСтремление к лучшему

Полное дообучение

Сначала разберём самый традиционный метод дообучения, а затем посмотрим, почему сегодня мы редко его используем.

Принципы и процесс

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

Процесс примерно такой:

  • Первый шаг: подготовьте данные. Организуйте данные вашей задачи в пары «вход-выход».

  • Второй шаг: загрузите предобученную модель. Например, скачайте открытые модели вроде LLaMA-2-7B и Mistral-7B.

  • Третий шаг: задайте параметры обучения. Скорость обучения должна быть очень маленькой (например, от 1e-5 до 5e-5), потому что мы не хотим полностью перезаписать знания, полученные при предобучении.

  • Шаг 4: начните обучение. Дайте модели пройти несколько эпох на ваших данных (несколько раз полностью пройти по набору данных).

  • Шаг пятый: сохраните модель. После обучения вы получите совершенно новый файл весов, того же размера, что и исходная модель.

Пример

# ============================================
# Демонстрация концепции полного дообучения (псевдокод, независимо от фреймворка)
# Показывает основную идею; в реальном коде будет использоваться HuggingFace.
# ============================================

def full_finetuning_concept():
    Демонстрация основных концепций полной тонкой настройки
    # 1. Загрузите предварительно обученную модель (все параметры обучаемы)
    model = load_pretrained_model("llama-2-7b")
    # Модель имеет 7 миллиардов параметров, и каждый параметр обновляется.
    print(fОбщее число параметров модели: {count_parameters(model)})
    # Вывод: Общее число параметров модели: 7000000000

    2. Подготовьте данные для вашей конкретной задачи.
    train_data = [
        {"input": Товар: Термокружка\nСгенерируйте описание для электронной коммерции.,
         "output": Эта высококачественная термокружка имеет внутреннюю колбу из нержавеющей стали 304 и технологию вакуумной изоляции, сохраняя напитки горячими до 12 часов.},
        {"input": Товар: Беспроводные наушники\nСгенерируйте описание для электронной коммерции.,
         "output": Полностью беспроводные Bluetooth-наушники, стабильное соединение Bluetooth 5.3, активное шумоподавление, 24 часа автономной работы, удобны в ношении.},
        # ... сотни или тысячи похожих записей данных
    ]

    # 3. Задайте оптимизатор и очень маленькую скорость обучения
    optimizer = create_optimizer(model, learning_rate=2e-5)

    # 4. Цикл обучения
    for epoch in range(3):  # Обычно обучение длится 2-5 эпох
        for batch in create_batches(train_data):
            Прямое распространение
            loss = model.compute_loss(batch)
            Обратное распространение, обновите все параметры!
            loss.backward()
            optimizer.step()
            optimizer.zero_grad()
        print(fЭпоха {epoch} завершена)

    # 5. Сохраните полную модель (7 миллиардов параметров, приблизительно 13GB)
    model.save("llama-2-7b-ecommerce-finetuned")
    print(Полное дообучение завершено, новая модель сохранена.)

Это всего лишь концептуальная демонстрация; для реального обучения требуется большой объём VRAM.

Проблема катастрофического забывания

Полное дообучение имеет серьёзную проблему: катастрофическое забывание.

Что это значит? Когда вы обучаете модель на своих данных, она постепенно «забывает» общие знания, полученные во время предварительного обучения. Это как человек, который в панике решает математические задачи, готовясь к экзамену по математике, и в итоге забывает китайский и английский, которые учил раньше.

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

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

Как это смягчить? Можно подмешивать данные предварительного обучения для совместного обучения, но это возвращает нас к старой проблеме «нужности огромных объёмов данных». Или можно использовать регуляризацию, чтобы ограничить слишком сильное изменение параметров, но эффект ограничен.

Расчёт требований к VRAM

Ещё одна проблема полного дообучения — слишком большие требования к VRAM. Давайте посчитаем:

VRAM, необходимая для обучения модели = параметры модели + градиенты + состояния оптимизатора + активации.

Для модели LLaMA-7B в полной точности (FP32): 7 миллиардов параметров × 4 байта = 28 GB. Градиенты также занимают 28 GB, а состояния оптимизатора (оптимизатор Adam) требуют 56 GB (вдвое больше градиентов). Активации зависят от размера батча и длины последовательности и требуют как минимум несколько GB.

Если всё сложить, для полного дообучения модели на 7B требуется около 100+ GB VRAM — это означает, что нужно как минимум 8 A100 GPU (40GB) или 2 A100 GPU (80GB).

Если используется половинная точность (FP16/BF16), требования к памяти можно уменьшить вдвое, но всё равно потребуется около 50-60 GB VRAM. Это по-прежнему не то, что может позволить себе обычный человек.

Размер моделиVRAM для полного дообучения (FP16)Требуемые GPU
7B50-60 GB2×A100 (40GB) или 1×A100 (80GB)
13B90-100 GB3×A100 (40GB) или 2×A100 (80GB)
70B400+ GB12×A100 (40GB) или 6×A100 (80GB)

Полное дообучение нельзя назвать непригодным, но оно дорого и рискованно. Если у вас нет веской причины (например, очень большого объёма данных и необходимости изменить фундаментальное поведение модели), следует отдавать приоритет методам параметрически эффективного дообучения (PEFT).


Параметрически эффективное дообучение (PEFT)

PEFT (Parameter-Efficient Fine-Tuning) — это общий термин для класса методов, которые обучают лишь небольшое число параметров. Он сохраняет эффективность тонкой настройки, значительно снижая затраты.

Основная идея PEFT

Ключевое наблюдение PEFT состоит в следующем:Предобученная модель уже содержит достаточно знаний. Нам не нужно изменять все параметры; достаточно «настроить» небольшую часть параметров, чтобы адаптироваться к новой задаче.。

Это как уже построенное пианино. Не нужно перестраивать всё пианино; достаточно подстроить несколько струн, чтобы получить нужный звук.

Преимущества PEFT очевидны:

  • Во-первых, требования к памяти значительно снижаются. Раньше требовалось 60 GB, теперь может потребоваться всего 6 GB.

  • Во-вторых, нет катастрофического забывания. Исходные параметры модели остаются неизменными; мы лишь добавляем немного нового, поэтому существующие знания не разрушаются.

  • В-третьих, низкие затраты на хранение. PEFT обычно сохраняет лишь небольшое число вновь добавленных параметров (от нескольких MB до нескольких сотен MB), и не нужно сохранять всю модель.

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

Обзор основных методов PEFT

PEFT — это не один метод, а собирательное название класса методов. Рассмотрим несколько основных методов:

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

  • Второй — Prefix Tuning. Добавьте обучаемые векторы "prefix" перед каждым слоем модели. Эти векторы префикса занимают лишь небольшую часть, но они могут направлять вывод модели.

  • Третий — Prompt Tuning. Добавьте обучаемые мягкие промпты только на входном слое. Это самый простой метод, но его эффект относительно ограничен.

  • Четвёртый — LoRA (Low-Rank Adaptation). Добавьте низкоранговые матрицы рядом со слоями внимания модели. В настоящее время это самый популярный и эффективный метод PEFT.

  • Пятый — QLoRA (Quantized LoRA). В дополнение к LoRA он добавляет квантование модели, чтобы ещё больше снизить требования к памяти. Это основной выбор на сегодня.

МетодПринципКоличество обучаемых параметровЭффект
Полная тонкая настройкаОбновление всех параметров100%Хорошо, но может забывать
AdapterВставка небольшой сети0.1%-1%Средняя
Prefix TuningДобавить векторы префикса0.1%-1%Средняя
Настройка промптовДобавление мягких промптов< 0.1%Среднее
LoRAНизкоранговая адаптация матриц0.1%-1%Близко к полному дообучению
QLoRAКвантование + LoRA0.1%-1%Близко к полному дообучению

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


LoRA(низкоранговая адаптация)

LoRA в настоящее время — наиболее распространённый метод дообучения, и важно понимать его принцип.

Принцип низкорангового разложения (интуитивное понимание)

Сначала разберитесь, что такое «ранг» и что означает «низкий ранг».

Предположим, у вас есть матрица 100 × 100, и данные в ней могут быть не полностью случайными. Если эти данные можно составить всего из нескольких «паттернов», мы говорим, что матрица имеет низкий ранг.

Например: если каждая строка кратна первой строке, то ранг этой матрицы равен 1. Если каждая строка является линейной комбинацией первых двух строк, то ранг равен 2. И так далее.

Основная идея LoRA такова:Когда большая модель адаптируется к новой задаче, изменение весов обычно является «низкоранговым» — то есть это изменение можно представить очень небольшим числом параметров.。

Это похоже на то, что хотя существует 1000 разных описаний товаров электронной коммерции, их стиль написания может определяться всего несколькими ключевыми факторами: тоном, длиной, расстановкой акцентов. Вам не нужно 1000 разных направлений, чтобы описать вариацию; достаточно 8, 16 или 32.

Итак, подход LoRA таков: заморозить все параметры исходной модели и добавить две небольшие матрицы A и B рядом со слоями внимания. A имеет размер d × r, B — r × d, где r — это ранг (Rank), обычно небольшое число вроде 8, 16 или 32.

Выход исходной модели = прямой проход исходной модели.

Выход LoRA = прямой проход исходной модели + (вход × A × B) × коэффициент масштабирования.

Во время обучения обучаются только A и B; параметры исходной модели остаются полностью неизменными.

Математический вывод LoRA

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

Предположим, что некоторый слой исходной модели является линейным преобразованием: h = W₀x, где W₀ — предобученная матрица весов размера d × d.

Во время дообучения мы хотим обновлять W₀, но не слишком сильно. Подход LoRA состоит в том, чтобы представить изменение W₀ как произведение двух небольших матриц: ΔW = BA.

Таким образом, выход после дообучения имеет вид: h = W₀x + (BA)x.

Здесь B имеет форму d × r, A имеет форму r × d, а r — это ранг (Rank).

Точнее, формула в статье LoRA выглядит так:

h = W₀x + α * (BA)x

где α — коэффициент масштабирования, обычно задаваемый как некоторое кратное r, так что при изменении r нет необходимости перенастраивать гиперпараметры.

Во время обучения W₀ полностью заморожена; обучаемыми являются только A и B.

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

Пример

# ============================================
# Минимальная реализация основного принципа LoRA
# Без зависимостей от фреймворков, демонстрация на чистом NumPy
# ============================================

import numpy as np


class LoRALayer:
    """Минимальная реализация слоя LoRA"""

    def __init__(self, d: int, r: int, alpha: float = 1.0):
        """
d: размерность входа/выхода (например, 4096)
r: ранг (например, 8, 16, 32)
alpha: коэффициент масштабирования
        """

        self.d = d
        self.r = r
        self.alpha = alpha

        # Замороженные исходные веса (имитация предобученной модели)
        self.W_0 = np.random.randn(d, d) * 0.01
        self.W_0.flags.writeable = False  # Отмечено как необучаемое

        # Матрицы A и B LoRA, обучаемые
        # A: r × d, инициализируется случайным гауссовым распределением
        self.A = np.random.randn(r, d) * 0.01
        # B: d × r, инициализируется нулями (не влияет на исходную модель в начале обучения)
        self.B = np.zeros((d, r))

    def forward(self, x: np.ndarray) -> np.ndarray:
        """Прямой проход: выход исходной модели + выход LoRA"""
        # Выход исходной модели
        original_output = x @ self.W_0.T
        # Выход LoRA: (x @ A.T) @ B.T = x @ (B @ A).T
        lora_output = (x @ self.A.T) @ self.B.T
        # Объединённый выход
        return original_output + self.alpha * lora_output

    def get_trainable_params(self) -> dict:
        """Получить обучаемые параметры (только A и B)"""
        return {"A": self.A, "B": self.B}

    def count_params(self) -> dict:
        """Подсчитать количество параметров"""
        total_original = self.d * self.d
        total_lora = self.r * self.d + self.d * self.r
        return {
            "original_params": total_original,
            "lora_params": total_lora,
            "ratio": total_lora / total_original * 100,
        }


# ============================================
# Демонстрация экономии параметров LoRA
# ============================================

# Предположим, d = 4096 (аналогично размерности скрытого слоя LLaMA-7B)
# r = 8 (обычно используемый размер ранга)
d = 4096
r = 8

lora = LoRALayer(d=d, r=r, alpha=r)
params_info = lora.count_params()

print(fКоличество исходных параметров: {params_info['original_params']:,})
print(fКоличество параметров LoRA: {params_info['lora_params']:,})
print(fСоотношение параметров: {params_info['ratio']:.4f}%)

# Пример вывода:
# Исходное количество параметров: 16,777,216
# Количество параметров LoRA: 65,536
# Доля параметров: 0.3906%

Видите, LoRA требует обучение только 0.4% параметров, но при этом может достигать результатов, близких к полному дообучению. В этом и заключается её магия.

Выбор параметров r (ранга) и α

У LoRA есть два ключевых гиперпараметра: ранг r и коэффициент масштабирования α.

r определяет ёмкость LoRA. Чем больше r, тем более сложные изменения она может представлять, но количество параметров также увеличивается.

Типичные значения r: 8, 16, 32, 64. Для большинства задач достаточно r = 8 или 16. Превышение r = 64 обычно даёт убывающую отдачу и увеличивает затраты на обучение.

Как выбрать r? Практический совет: сначала попробуйте r = 8, и если результаты плохие, увеличьте его до 16 или 32. Не начинайте с очень большого r.

α — это коэффициент масштабирования. Обычная настройка — α = r, чтобы масштаб ΔW = BA оставался примерно стабильным. Либо установите α = 1 и позвольте пользователю настроить его самостоятельно.

Другие параметры, которые стоит учесть:

Целевые модули: к каким слоям следует применять LoRA? Обычно хорошо работает добавление только проекций query и value (q_proj, v_proj) слоёв внимания. Её также можно добавлять к большему числу слоёв, но это увеличивает количество параметров.

Dropout: LoRA может добавлять dropout для предотвращения переобучения; типичные значения — 0.05 или 0.1.

ПараметрТипичное значениеОписание
r (ранг)8, 16, 32Большее значение означает более высокую ёмкость, но также больше параметров
альфаr или 1Коэффициент масштабирования, обычно устанавливается равным r
target_modules["q_proj", "v_proj"]Слои, к которым применяется LoRA
lora_dropout0.0, 0.05, 0.1Предотвращение переобучения

Объединение весов LoRA

После обучения LoRA во время инференса есть два варианта.

  • Первый — оставлять LoRA отдельно. Во время инференса загрузите исходную модель, затем загрузите веса LoRA и динамически вычислите объединённый вывод. Преимущество в том, что можно гибко переключать несколько LoRA; недостаток — инференс немного медленнее (обычно незначительно).

  • Второй — объединить веса LoRA. Добавьте ΔW = BA из LoRA к W₀ исходной модели, получив W = W₀ + α×BA. Это создаёт один файл модели, и инференс работает так же, как у обычной модели.

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

Пример

# ============================================
# Демонстрация объединения весов LoRA
# ============================================

import numpy as np


def merge_lora_weights(W_0: np.ndarray, A: np.ndarray, B: np.ndarray,
                      alpha: float = 1.0) -> np.ndarray:
    """
Объединение весов LoRA с исходными весами
    W = W_0 + alpha * (B @ A)

Параметры:
W_0: исходные веса (d, d)
A: матрица A LoRA (r, d)
B: матрица LoRA B (d, r)
alpha: коэффициент масштабирования

Возвращает:
Объединённые веса
    """

    # вычислить delta_W = alpha * (B @ A)
    delta_W = alpha * (B @ A)
    # слить с исходными весами
    W_merged = W_0 + delta_W
    return W_merged


# ============================================
# продемонстрировать процесс слияния
# ============================================

d = 4096  # размерность скрытого слоя
r = 8     # ранг LoRA

# смоделировать исходные веса и веса LoRA
W_0 = np.random.randn(d, d) * 0.01
A = np.random.randn(r, d) * 0.01
B = np.zeros((d, r))  # на практике B — результат после обучения

# объединённые веса
W_merged = merge_lora_weights(W_0, A, B, alpha=r)

print(f"исходная форма веса: {W_0.shape}")
print(f"форма объединённого веса: {W_merged.shape}")
print("объединение весов завершено, можно использовать как обычную модель")

QLoRA (квантованный LoRA)

QLoRA доводит низкую стоимость LoRA до предела — дообучение моделей на 70B возможно на потребительских GPU.

4-битная квантизация + LoRA

Идея QLoRA проста: квантуем модель до 4 бит, затем выполняем LoRA-дообучение поверх.

Что такое квантизация? Она преобразует числа с плавающей запятой (FP16/BF16, каждое занимает 2 байта) в представления с меньшей точностью. 4-битная квантизация использует 4 бита (0,5 байта) для представления веса.

Это уменьшает размер модели на 75%: модель 7B сокращается с 13 ГБ до примерно 3,5 ГБ, модель 13B — с 26 ГБ до примерно 6,5 ГБ, а модель 70B — с 140 ГБ до примерно 35 ГБ.

Но прямая квантизация теряет точность. Инновация QLoRA — это тип данных под названием NF4 (Normalized Float 4), который лучше сохраняет информацию в весах нейронных сетей.

Подход QLoRA:

  • 1. Квантовать предварительно обученную модель до 4 бит (формат NF4).

  • 2. Добавить сверху LoRA-адаптеры.

  • 3. Во время обучения 4-битная квантованная базовая модель полностью заморожена; обучаются только веса LoRA в FP16.

Результат: требования к VRAM снижаются ещё на 75%, практически без потери производительности.

Библиотека bitsandbytes

bitsandbytes — это основная библиотека для квантизации, разработанная Тимом Деттмерсом.

Она предоставляет:

  • 1. 4-битная квантизация (NF4).

  • 2. Двойная квантизация — квантует также константы квантизации, экономя ещё немного VRAM.

  • 3. Paged Optimizer — перемещает часть данных в память CPU, когда VRAM недостаточно.

Обычным пользователям не нужно напрямую взаимодействовать с bitsandbytes; библиотеки peft и transformers уже оборачивают её. Но полезно знать, что она существует.

Решения для тонкой настройки на GPU потребительского класса

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

RTX 3090/4090 (24GB VRAM) легко справляются с тонкой настройкой моделей 7B/13B и даже могут попробовать модели 70B (для чего требуются более длинные последовательности или меньший размер батча).

Оценка требований к VRAM:

Размер моделиТребования к VRAM для QLoRAПодходящая GPU
7B5-8 GBRTX 3060/4060 и выше
13B8-12 GBRTX 3070/4070 и выше
34B15-20 GBRTX 3090/4090
70B30-40 GBRTX 3090/4090 × 2 или A100

QLoRA — это переломный момент. Она превратила тонкую настройку больших моделей из «это под силу только крупным компаниям» в «обычные люди могут делать это на собственных игровых ноутбуках». Это одно из важнейших достижений в области ИИ с открытым исходным кодом.


Подготовка обучающих данных

Подготовка данных — самая трудоёмкая и критически важная часть процесса тонкой настройки. Мусор на входе — мусор на выходе; качество данных определяет верхний предел возможностей модели.

Форматы данных (форматы Alpaca, ShareGPT)

Есть несколько распространённых форматов данных; выберите один и придерживайтесь его.

Первый — формат Alpaca. Это один из самых популярных форматов, популяризированный проектом Stanford Alpaca.

Формат такой: instruction (инструкция), input (входные данные, необязательно), output (выходные данные).

Пример

# ============================================
# Пример формата данных Alpaca
# ============================================

alpaca_data = [
    {
        "instruction": Сгенерируйте описание товара для интернет-магазина.,
        "input": Название продукта: Умные спортивные часы\nФункции: мониторинг сердечного ритма, отслеживание сна, GPS-позиционирование, водонепроницаемость до 50 метров.,
        "output": Эти умные спортивные часы — идеальный компаньон для любителей фитнеса. Они обеспечивают точный мониторинг сердечного ритма и научное отслеживание сна, помогая вам полностью понимать состояние своего здоровья. Встроенный GPS позволяет записывать маршруты тренировок, не беря с собой телефон. С классом водонепроницаемости до 50 метров вы можете уверенно носить их во время плавания.
    },
    {
        "instruction": Перепишите следующий текст более разговорным языком.,
        "input": Этот продукт использует передовые технологии и отличается высокой производительностью и низким энергопотреблением.,
        "output": Этот продукт использует очень передовые технологии, обладает высокой производительностью и энергоэффективностью!
    },
    Больше данных
]

print(Формат Alpaca содержит три поля:)
print(- instruction: инструкция задачи (обязательно))
print(- input: дополнительный ввод (необязательно, может быть пустой строкой, если отсутствует))
print(- output: ожидаемый вывод (обязательно))

Второй — формат ShareGPT. Этот формат используется для диалоговых данных и поддерживает многоходовые разговоры.

Формат следующий: conversations (список диалогов), каждый ход содержит from (роль) и value (содержимое).

Пример

# ============================================
Пример формата данных ShareGPT
# ============================================

sharegpt_data = [
    {
        "conversations": [
            {
                "from": human",
                "value": Здравствуйте, я хотел бы узнать больше о ваших умных часах.
            },
            {
                "from": "gpt",
                "value": "Здравствуйте! Наши умные часы имеют такие функции, как мониторинг сердечного ритма, мониторинг сна, GPS и водонепроницаемость до 50 метров. Есть ли что-то, что вы хотели бы узнать?"
            },
            {
                "from": human",
                "value": Каково время работы батареи?
            },
            {
                "from": "gpt",
                "value": При обычном использовании батарея держится 7 дней; при включённом спортивном режиме GPS — 12 часов.
            }
        ]
    },
    # ... Больше диалогов
]

print(Формат ShareGPT используется для данных многоходовых диалогов:)
print(- conversations: список диалогов)
print(- from: role (обычно human и gpt))
print(- value: содержание диалога)

Существует также простой формат пар вход-выход, подходящий для простых задач.

Пример

# ============================================
# Простой формат пар вход-выход
# ============================================

simple_data = [
    {
        "text": "Пользователь: Сгенерируй описание товара\nТовар: Термокружка\n\nАссистент: Эта высококачественная термокружка использует..."
    },
    {
        "text": "Пользователь: Сгенерируй описание товара\nТовар: Беспроводные наушники\n\nАссистент: Полностью беспроводные Bluetooth-наушники, Bluetooth 5.3..."
    },
    # ... больше данных
]

print("Этот формат напрямую объединяет всё содержимое в одно текстовое поле, что является наиболее гибким, но также и наименее стандартизированным.")

Какой формат лучше? Формат Alpaca наиболее распространён; большинство фреймворков для обучения поддерживают его. Для диалоговых задач лучше подходит ShareGPT.

Ключевые моменты очистки данных

Очистка данных гораздо важнее, чем вы можете себе представить. 80% производительности вашей модели зависит от качества данных.

  • Во-первых, дедупликация. Дублирующиеся данные приведут к переобучению модели на этих повторяющихся шаблонах. Проверьте и удалите полностью идентичные или очень похожие данные.

  • Во-вторых, отфильтруйте низкокачественные данные. Удалите данные, которые слишком короткие, слишком длинные, плохо отформатированы или содержат очевидные ошибки. Например, выходные данные, состоящие всего из нескольких символов, или с искажённым текстом.

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

  • В-четвёртых, сбалансируйте распределение. Если данные некоторых категорий особенно многочисленны, а других — мало, модель будет смещена в сторону мажоритарной категории. Рассмотрите передискретизацию миноритарного класса или субдискретизацию мажоритарного класса.

  • В-пятых, проверьте на утечку данных. Убедитесь, что тестовая и валидационная выборки не появляются в обучающей выборке.

Пример

# ============================================
# Простой пример очистки данных
# ============================================

import json
from typing import List, Dict


def clean_alpaca_data(data: List[Dict]) -> List[Dict]:
    """Простая функция очистки данных"""
    cleaned = []
    seen_outputs = set()

    for item in data:
        # 1. Проверить наличие обязательных полей
        if "instruction" not in item or "output" not in item:
            continue

        instruction = item["instruction"].strip()
        output = item["output"].strip()

        # 2. Отфильтровать слишком короткие выходные данные
        if len(output) < 10:
            continue

        # 3. Отфильтровать слишком длинные выходные данные (настройте в соответствии с задачей)
        if len(output) > 2000:
            continue

        # 4. Дедупликация (приблизительная дедупликация с использованием выходных данных)
        if output in seen_outputs:
            continue
        seen_outputs.add(output)

        # 5. Простая унификация формата
        # Например, заменить китайские кавычки на английские кавычки
        output = output.replace(""", "\"").replace(""", "\"")
        # Удалить начальные и конечные пробелы
        output = output.strip()

        cleaned.append({
            "instruction": instruction,
            "input": item.get("input", "").strip(),
            "output": output
        })

    return cleaned


# Загрузить данные
with open("data/training_data_raw.json", "r", encoding="utf-8") as f:
    raw_data = json.load(f)

# Очистка данных
cleaned_data = clean_alpaca_data(raw_data)

print(f"Количество исходных данных: {len(raw_data)}")
print(f"Количество очищенных данных: {len(cleaned_data)}")
print(f"Отфильтровано {len(raw_data) - len(cleaned_data)} низкокачественных записей данных")

# Сохранить очищенные данные
with open("data/training_data_cleaned.json", "w", encoding="utf-8") as f:
    json.dump(cleaned_data, f, ensure_ascii=False, indent=2)

print("Очищенные данные сохранены в data/training_data_cleaned.json")

Эвристические правила для объема данных

Сколько данных вам нужно? Это один из самых часто задаваемых вопросов.

Ответ таков: это зависит от сложности задачи.

  • Простые задачи (например, сделать фрагмент текста более разговорным): может быть достаточно 100-300 записей.

  • Задачи средней сложности (например, генерация сводок в определённом формате): подходят 300-1000 записей.

  • Сложные задачи (например, вопросы и ответы в профессиональных областях): идеально подходят 1000-5000 записей.

Конечно, больше данных обычно лучше, но с убывающей предельной отдачей. 1000 записей намного лучше, чем 500, но 10000 может быть не намного лучше, чем 5000.

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

Качество данных >> количество данных. 1000 тщательно подготовленных записей работают намного лучше, чем 10000 наспех собранных.

Тратить больше времени на этап подготовки данных стоит того. Обучение модели — это просто вопрос «нажать кнопку», но качество данных определяет, будет ли нажатие этой кнопки ценным.


Контролируемое дообучение (SFT) на практике

Наконец, мы добрались до практической части. Мы будем использовать Unsloth или LLaMA-Factory для реализации полного рабочего процесса дообучения.

Настройка окружения

Шаг 1: Установите необходимые библиотеки.

Настройка окружения

# ============================================
# Установите зависимости (рекомендуется создать новое окружение conda)
# ============================================

# Создайте новую среду conda
conda create -n finetuning python=3.10 -y
conda activate finetuning

# Установите PyTorch (выберите подходящую команду в зависимости от вашей версии CUDA)
# Посетите https://pytorch.org/get-started/locally/ для получения последних команд
conda install pytorch pytorch-cuda=12.1 -c pytorch -c nvidia

# Установите библиотеки экосистемы HuggingFace
pip install transformers datasets accelerate peft bitsandbytes

# Установите Unsloth (необязательно, для более быстрого обучения)
pip install "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git"
pip install --no-deps trl xformers

# Или используйте LLaMA-Factory (универсальный фреймворк для обучения)
git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory
pip install -e ".[torch,metrics]"

echo "Настройка окружения завершена!"

Использование Unsloth/LLaMA-Factory

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

Давайте сначала посмотрим на код для обучения с Unsloth + peft:

Пример

# ============================================
# Использование Unsloth для тонкой настройки LoRA (полная версия)
# Работает на RTX 3090/4090
# ============================================

import torch
from datasets import load_dataset, Dataset
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    TrainingArguments,
    BitsAndBytesConfig,
)
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer
import json


# ============================================
# 1. Параметры конфигурации
# ============================================

MODEL_NAME = "unsloth/mistral-7b-v0.2-bnb-4bit"  # или другие модели
DATA_PATH = "data/training_data_cleaned.json"    # ваши данные
OUTPUT_DIR = "output/example-ecommerce-model"     # каталог вывода

# Конфигурация 4-битной квантизации
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16
)

# Конфигурация LoRA
lora_config = LoraConfig(
    r=16,                    # Ранг, 8-64 вполне подходят, 16 — хорошая отправная точка
    lora_alpha=16,           # Коэффициент масштабирования, обычно равен r
    target_modules=[         # Слои, к которым применяется LoRA, это распространённые варианты
        "q_proj",
        "k_proj",
        "v_proj",
        "o_proj",
        "gate_proj",
        "up_proj",
        "down_proj",
    ],
    lora_dropout=0.05,       # Dropout для предотвращения переобучения
    bias="none",
    task_type="CAUSAL_LM"    # Каузальная языковая модель
)

# Параметры обучения
training_args = TrainingArguments(
    output_dir=OUTPUT_DIR,
    per_device_train_batch_size=4,       # Размер батча на GPU
    per_device_eval_batch_size=4,
    gradient_accumulation_steps=4,       # Накопление градиентов, эквивалентно большему батчу
    learning_rate=2e-5,                  # Скорость обучения, обычный диапазон — от 2e-5 до 5e-5
    num_train_epochs=3,                  # Количество эпох, 2-5 эпох — подходящий вариант
    logging_steps=10,                    # Логировать каждые 10 шагов
    evaluation_strategy="epoch",         # Оценивать один раз за эпоху
    save_strategy="epoch",               # Сохранять один раз за эпоху
    fp16=True,                           # Использовать обучение со смешанной точностью
    bf16=False,                          # Для GPU Ampere и новее (серии 30/40) можно установить bf16=True
    optim="paged_adamw_32bit",           # Постраничный оптимизатор для предотвращения OOM
    report_to="none",                    # Не отправлять отчёты на платформы вроде wandb
    save_total_limit=3,                  # Сохранять не более 3 чекпоинтов
    load_best_model_at_end=True,         # В конце загрузить лучшую модель
    metric_for_best_model="eval_loss",   # Выбирать лучшую модель по eval loss
)


# ============================================
# 2. Загрузка данных и форматирование
# ============================================

def format_example(example):
    """Форматировать данные в формате Alpaca во входные данные модели"""
    instruction = example["instruction"]
    input_text = пример.get("input", "")
    output = пример["output"]

    if input_text.strip() == "":
        # Случай без дополнительного ввода
        prompt = f"""Ниже приведена инструкция, описывающая задачу. Напишите ответ, который соответствующим образом выполняет запрос.

### Инструкция:
{instruction}

### Ответ:
"""

    else:
        # Случай с дополнительным вводом
        prompt = f"""Ниже приведена инструкция, описывающая задачу, вместе с входными данными, которые предоставляют дополнительный контекст. Напишите ответ, который соответствующим образом выполняет запрос.

### Инструкция:
{instruction}

### Ввод:
{input_text}

### Ответ:
"""


    # Полный текст — это prompt + output
    return {
        "prompt": prompt,
        "completion": output,
        "text": prompt + output
    }


# Загрузить данные
dataset = load_dataset("json", data_files=DATA_PATH, split="train")

# Разделение на обучающий и валидационный наборы (90% — обучение, 10% — валидация)
dataset = dataset.train_test_split(test_size=0.1, seed=42)
train_dataset = dataset["train"]
eval_dataset = dataset["test"]

# Форматирование данных
train_dataset = train_dataset.map(format_example)
eval_dataset = eval_dataset.map(format_example)

print(f"Размер обучающей выборки: {len(train_dataset)}")
print(f"Размер валидационного набора: {len(eval_dataset)}")


# ============================================
# 3. Загрузка модели и токенизатора
# ============================================

print("Загрузка модели...")

tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
tokenizer.pad_token = tokenizer.eos_token  # Установить pad-токен
tokenizer.padding_side = "right"           # Дополнять справа

model = AutoModelForCausalLM.from_pretrained(
    MODEL_NAME,
    quantization_config=bnb_config,
    device_map="auto",
    trust_remote_code=True,
    use_auth_token=True,  # Если требуется вход в HuggingFace
)

# Обернуть LoRA
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()  # Вывести долю обучаемых параметров


# ============================================
# 4. Создать тренер и начать обучение
# ============================================

trainer = SFTTrainer(
    model=model,
    train_dataset=train_dataset,
    eval_dataset=eval_dataset,
    args=training_args,
    tokenizer=tokenizer,
    peft_config=lora_config,
    dataset_text_field="text",  # Какое поле содержит полный текст
    max_seq_length=2048,        # Максимальная длина последовательности
    packing=False,              # Не упаковывать образцы
)

print("Начало обучения...")
trainer.train()
print("Обучение завершено!")


# ============================================
# 5. Сохранить модель
# ============================================

# Сохранить адаптер LoRA (небольшой размер, от нескольких MB до десятков MB)
model.save_pretrained(f"{OUTPUT_DIR}/lora_adapter")
tokenizer.save_pretrained(f"{OUTPUT_DIR}/lora_adapter")
print(f"Адаптер LoRA сохранён в {OUTPUT_DIR}/lora_adapter")

# Если вы хотите объединить веса LoRA (необязательно)
# merged_model = model.merge_and_unload()
# merged_model.save_pretrained(f"{OUTPUT_DIR}/merged_model")
# tokenizer.save_pretrained(f"{OUTPUT_DIR}/merged_model")
# print(f"Объединённая модель сохранена в {OUTPUT_DIR}/merged_model")

Если вы хотите более простой подход, вы можете использовать LLaMA-Factory, который предоставляет как веб-интерфейс, так и интерфейс командной строки:

Пример

# ============================================
# Используйте LLaMA-Factory для обучения
# ============================================

# Способ 1: запустите веб-интерфейс (самый интуитивный)
cd LLaMA-Factory
python src/train_web.py

# Метод 2: Обучение из командной строки (подходит для автоматизации)
python src/train.py \
    --model_name_or_path mistralai/Mistral-7B-v0.2 \
    --dataset example_ecommerce \
    --dataset_dir ./data \
    --output_dir ./output/example-model \
    --finetuning_type lora \
    --quantization_bit 4 \
    --lora_rank 16 \
    --lora_alpha 16 \
    --lora_dropout 0.05 \
    --lora_target q_proj,v_proj,k_proj,o_proj,gate_proj,up_proj,down_proj \
    --per_device_train_batch_size 4 \
    --gradient_accumulation_steps 4 \
    --lr_scheduler_type cosine \
    --logging_steps 10 \
    --save_strategy epoch \
    --learning_rate 2e-5 \
    --num_train_epochs 3 \
    --fp16 true \
    --plot_loss true

Настройки гиперпараметров обучения

Выбор гиперпараметров — это искусство, но существуют некоторые эмпирические правила.

Скорость обучения: это самый важный гиперпараметр. Для LoRA безопасная начальная точка — от 2e-5 до 5e-5. Если обучение нестабильно (loss колеблется), немного уменьшите её. Если обучение идёт слишком медленно (loss уменьшается очень медленно), немного увеличьте её.

Размер батча: чем больше, тем лучше, но он ограничен объёмом видеопамяти (VRAM). per_device_train_batch_size можно установить равным 2, 4 или 8. Если VRAM недостаточно, используйте gradient_accumulation_steps для накопления градиентов. Например, batch_size=2, accumulation_steps=4 эквивалентно batch_size=8.

Эпохи: подходит 2–5 эпох. Слишком мало — модель не успеет достаточно обучиться; слишком много — может возникнуть переобучение. Можно следить за eval loss и остановиться, когда eval loss перестанет уменьшаться.

Ранг LoRA (r): 8–64 — всё нормально. 16 — хорошая начальная точка. Используйте 8 для небольших задач, а 32 или 64 — для сложных задач.

Максимальная длина последовательности: выбирайте исходя из ваших данных. 512, 1024, 2048, 4096 — все это распространённые значения. Чем она длиннее, тем больше требуется VRAM.

ГиперпараметрРекомендуемое начальное значениеНаправление настройки
learning_rate2e-5Если loss колеблется, уменьшите; если уменьшается слишком медленно, увеличьте
per_device_train_batch_size4Если VRAM достаточно, увеличьте; если недостаточно, уменьшите
gradient_accumulation_steps4Эквивалентный batch = batch_size × accumulation
num_train_epochs3При переобучении — уменьшите; при недообучении — увеличьте
lora_rank (r)16Простые задачи 8, сложные задачи 32-64
max_seq_length1024 или 2048Настройте в соответствии с длиной ваших данных

Мониторинг обучения и интерпретация loss

Во время обучения обратите внимание на несколько метрик.

  • Train Loss: loss на обучающем наборе должен стабильно уменьшаться. Если он уменьшается быстро, это означает, что скорость обучения может быть слишком большой или данных слишком мало. Если он уменьшается медленно, это означает, что скорость обучения слишком мала или емкости модели недостаточно.

  • Eval Loss: loss на валидационном наборе — это наиболее важная метрика. Он должен уменьшаться вместе с train loss. Если eval loss начинает расти, а train loss продолжает уменьшаться, это означает переобучение, и обучение следует остановить.

Признаки переобучения: eval loss растёт после первоначального падения, тогда как train loss продолжает снижаться. Решения: уменьшить количество эпох, увеличить dropout, добавить данные, уменьшить ранг LoRA.

Признаки недообучения: и train loss, и eval loss высоки, и они снижаются медленно. Решения: увеличить количество эпох, увеличить ранг LoRA, добавить данные, немного увеличить скорость обучения.


Оценка эффектов тонкой настройки

После обучения как узнать, хорошая ли модель?

Выбор метрик оценки

Метод оценки зависит от типа вашей задачи.

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

Можно составить простую таблицу оценок:

Пример

# ============================================
# Простой шаблон ручной оценки
# ============================================

evaluation_prompts = [
    {
        "instruction": "Сгенерируй описание товара для электронной коммерции",
        "input": "Название товара: портативная кофемашина\nФункция: приготовление одним нажатием, портативный дизайн, зарядка через USB",
        "reference": "Эта портативная кофемашина позволяет наслаждаться свежемолотым кофе в любое время и в любом месте. Приготовление одним нажатием простое в управлении, а компактный портативный дизайн подходит для офиса, путешествий и отдыха на природе. Дизайн с USB-зарядкой означает, что вам не нужно беспокоиться о проблемах с питанием."
    },
    {
        "instruction": "Сгенерируй описание товара для электронной коммерции",
        "input": "Название продукта: механическая клавиатура\nОсобенности: синие переключатели, RGB-подсветка, 87 клавиш, PBT-кейкапы",
        "reference": "Эта 87-клавишная механическая клавиатура использует четкие синие переключатели для превосходного ощущения при печати. Полная RGB-подсветка всех клавиш обеспечивает четкость в ночное время. Кейкапы PBT долговечны и не покрываются блестящим налетом, оставаясь как новые даже после длительного использования."
    },
    # ... 10-20 тестовых случаев
]


def evaluate_model(model, tokenizer, prompts):
    """Оцените модель с помощью набора промптов"""
    results = []
    for item in prompts:
        # Сгенерировать вывод
        inputs = format_example_for_inference(item)
        output = generate_with_model(model, tokenizer, inputs)

        results.append({
            "instruction": item["instruction"],
            "input": item["input"],
            "model_output": output,
            "reference": item["reference"],
            # Оставьте пустым для ручной оценки
            "score_relevance": None,  # 1-5, релевантность
            "score_quality": None,    # 1-5, качество
            "score_format": None,     # 1-5, корректность формата
        })

    return results


# Запуск оценки
results = evaluate_model(model, tokenizer, evaluation_prompts)

# Сохранить результаты для последующей ручной оценки
with open("evaluation_results.json", "w", encoding="utf-8") as f:
    json.dump(results, f, ensure_ascii=False, indent=2)

print(Результаты оценки сохранены. Пожалуйста, оцените вручную перед анализом.)

Второй — автоматическая оценка. Используйте такие метрики, как BLEU, ROUGE и BLEURT. Эти метрики не идеальны, но они позволяют быстро выполнять итерации.

Третий — A/B-тестирование. Смешайте выходы старой и новой модели вместе для слепой оценки, чтобы понять, действительно ли новая модель лучше.

Предотвращение переобучения

Переобучение — самая распространённая проблема при дообучении. Оно показывает хорошие результаты на обучающей выборке, но плохие — на новых данных.

Методы предотвращения переобучения:

1. Ранняя остановка: следите за eval loss и останавливайте обучение, когда он перестаёт уменьшаться.

2. Добавьте больше данных: больше данных и больше разнообразия — лучший способ предотвратить переобучение.

3. Аугментация данных: перепишите существующие данные, чтобы создать вариации.

4. Уменьшите ёмкость модели: уменьшите ранг LoRA и количество слоёв LoRA.

5. Увеличьте dropout: установите lora_dropout в конфигурации LoRA равным 0.05 или 0.1.

6. Затухание веса: добавьте weight_decay=0.01 в параметры обучения.


Развёртывание дообученных моделей

Когда модель обучена, как её использовать?

Загрузка LoRA для инференса

Первый способ — загрузить базовую модель, затем загрузить адаптер LoRA.

Пример

# ============================================
# Загрузить LoRA и выполнить инференс
# ============================================

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel


def load_finetuned_model(base_model_name, lora_adapter_path):
    """Загрузить базовую модель и адаптер LoRA"""
    # Загрузить токенизатор
    tokenizer = AutoTokenizer.from_pretrained(base_model_name)
    tokenizer.pad_token = tokenizer.eos_token

    # Загрузить базовую модель
    model = AutoModelForCausalLM.from_pretrained(
        base_model_name,
        device_map="auto",
        torch_dtype=torch.float16,
        trust_remote_code=True,
    )

    # Загрузить адаптер LoRA
    model = PeftModel.from_pretrained(model, lora_adapter_path)

    # Объединить веса (необязательно, ускоряет инференс)
    model = model.merge_and_unload()

    return model, tokenizer


def generate_product_description(model, tokenizer, product_name, features):
    """Сгенерировать описание товара для электронной коммерции"""
    # Сформировать промпт (так же, как при обучении!)
    prompt = f"""Ниже приведена инструкция, описывающая задачу. Напишите ответ, который соответствующим образом выполняет запрос.

### Инструкция:
Сгенерируйте описание товара для электронной коммерции

### Ввод:
Название товара: {product_name}
Характеристики: {features}

### Ответ:
"""


    # Сгенерировать
    inputs = tokenizer(prompt, return_tensors="pt").to(model.device)

    with torch.no_grad():
        outputs = model.generate(
            **inputs,
            max_new_tokens=200,
            temperature=0.7,      # Управление случайностью, 0-1, больше означает более случайный вывод
            top_p=0.9,
            do_sample=True,
            pad_token_id=tokenizer.eos_token_id,
        )

    # Декодировать вывод
    generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)

    # Взять только часть Response
    response = generated_text.split("### Response:")[-1].strip()

    return response


# ============================================
# Пример использования
# ============================================

BASE_MODEL = "mistralai/Mistral-7B-v0.2"
LORA_PATH = "output/example-ecommerce-model/lora_adapter"

print("Загрузка модели...")
model, tokenizer = load_finetuned_model(BASE_MODEL, LORA_PATH)
print("Модель успешно загружена!")

# Тестирование генерации
product_name = "Умный робот-пылесос"
features = "Лазерная навигация, автоперезарядка, управление через приложение, мощное всасывание 2000Pa"

description = generate_product_description(
    model, tokenizer, product_name, features
)

print(f"\nНазвание продукта: {product_name}")
print(fСгенерированное описание:\n{description}")

Методы развертывания

Есть несколько распространённых вариантов развертывания.

  • Первый — использовать vLLM. Это высокопроизводительный движок вывода, поддерживающий LoRA, непрерывную пакетную обработку и другие возможности. Рекомендуется для производственных сред.

  • Второй — использовать Text Generation Inference. Официальный сервис вывода от HuggingFace с богатыми возможностями.

  • Третий — обернуть его самостоятельно с помощью FastAPI. Просто и напрямую, подходит для развертывания небольшого масштаба.

  • Четвертый — использовать Ollama. Преобразуйте модель в формат GGUF и запустите её с помощью Ollama; подходит для локального развертывания.

Другие расширения