Содержание
- Что такое дообучение модели и какую задачу оно закрывает
- Что именно меняется в модели
- Полное дообучение
- LoRA и адаптеры
- QLoRA и квантованная база
- Принцип работы
- SFT и функция потерь только по ответу
- DPO и обучение на парах предпочтений
- Полное дообучение, LoRA и QLoRA
- Сценарии применения
- Когда дообучение не нужно
- Практика LoRA-дообучения на локальной машине
- Ограничения и грабли
- Заключение
- Референсные ссылки
Fine tuning (дообучение модели) это продолжение обучения уже готовой нейросети на своей небольшой выборке, при котором меняются её веса. Промптинг и RAG (Retrieval-Augmented Generation) работают с контекстом, то есть с тем, что модель видит на входе прямо сейчас, а fine-tuning переписывает саму модель. Разница принципиальная. После дообучения нужное поведение держится без единой подсказки в промпте, но и поправить его так же дёшево, как строчку инструкции, уже не выйдет.
Что такое дообучение модели и какую задачу оно закрывает
Базовая языковая модель обучена на общем корпусе и умеет всё понемногу. Она не знает вашего формата ответа, вашей терминологии и вашего тона. Дообучение берёт такую модель и прогоняет по ней ещё несколько эпох на маленькой размеченной выборке, обновляя веса тем же градиентным спуском, что и на большом корпусе, только несравнимо дешевле и короче.
Главную мысль про fine-tuning стоит зафиксировать сразу, потому что дальше всё крутится вокруг неё. Дообучение ставит модели форму и поведение, а не добавляет ей знаний. Если модель начала отвечать одной строкой строгого формата, это заслуга дообучения. Если она при этом путает Apache HBase с Apache Hadoop, дообучение такую беду не лечит, потому что фактическая точность живёт в другом месте, а именно в подложенном на вход контексте.
Что именно меняется в модели
Обучаемое внутри трансформера это матрицы весов линейных слоёв, прежде всего проекций внимания q_proj, k_proj, v_proj и o_proj, плюс слои прямого прохода. Методы дообучения различаются тем, какую часть этих матриц они трогают и в каком виде держат всё остальное.
Полное дообучение
Полное дообучение (full fine-tuning) обновляет все веса без исключения. Для модели на 7 млрд параметров это означает держать в памяти сами веса, их градиенты и состояние оптимизатора Adam, то есть на порядок величины больше, чем нужно на инференс той же модели. Результат самый гибкий из возможных, цена самая высокая, и на каждую задачу получается отдельная полная копия модели весом в десятки гигабайт.
LoRA и адаптеры
LoRA (Low-Rank Adaptation) исходит из наблюдения, что поправка к весам при дообучении имеет низкий ранг, поэтому её можно приблизить произведением двух узких матриц. Исходную матрицу замораживают, а рядом ставят пару обучаемых матриц A и B внутренней размерности r, обычно от 4 до 64. На выходе слоя результат базового умножения складывают с низкоранговой поправкой, домноженной на масштаб alpha, делённый на r. Учатся только A и B.
Практический эффект виден по одной цифре. В демо ниже на модели Qwen2.5-0.5B-Instruct обучаемыми оказались 1 081 344 параметра из 495 114 112, то есть 0.218 процента. Веса адаптера заняли 4,35 МБ против 953 МБ у базовой модели, в 219 раз меньше. Отсюда и вся экономика подхода, ведь на одной базе можно держать десяток адаптеров под разные задачи и переключать их прямо в рантайме. Кроме LoRA семейство PEFT (Parameter-Efficient Fine-Tuning) включает IA3, AdaLoRA и prefix tuning, их перечень и требования к версиям собраны в документации Hugging Face PEFT.
QLoRA и квантованная база
QLoRA идёт дальше и квантует замороженную базу в 4 бита, оставляя адаптеры в обычной точности. Память под базовые веса падает примерно вчетверо, поэтому дообучение семимиллиардной модели становится посильным для одной потребительской видеокарты. Расплата двойная. Квантование стоит части качества, а библиотека bitsandbytes собрана под CUDA, из-за чего на Apple Silicon этот вариант недоступен, и в демо ниже он поэтому не показан.
Принцип работы
Доводка модели под задачу почти всегда разбивается на два непохожих этапа. Сначала модель учат давать правильный ответ, потом учат выбирать лучший из двух правильных.
SFT и функция потерь только по ответу
SFT (Supervised Fine-Tuning) это обучение на парах вопрос-ответ. Тонкость в том, что в чат-разметку попадает и системный промпт, и реплика пользователя, а воспроизводить их модель учить незачем. Поэтому функция потерь считается только по токенам ответа ассистента, а всё остальное маскируется. В нашем прогоне на примере про HDFS из 87 токенов последовательности под loss попали 48, ровно текст ответа вместе со служебным маркером конца реплики.
DPO и обучение на парах предпочтений
DPO (Direct Preference Optimization) работает уже поверх SFT и учит модель не правильному ответу, а предпочтению. На вход идёт тройка из запроса, удачного ответа и неудачного, а модель сдвигается в сторону первого относительно опорной копии самой себя. Это упрощённая замена обучению с подкреплением на человеческой обратной связи, без отдельной модели вознаграждения. В библиотеке TRL версии 1.0 оба тренера, SFTTrainer и DPOTrainer, лежат в одном пакете вместе с GRPO и reward-моделями.
Нейронные сети на Python
Код курса
PYNN
Ближайшая дата курса
12 октября, 2026
Продолжительность
24 ак.часов
Стоимость обучения
66 000
Полное дообучение, LoRA и QLoRA
Три метода отличаются не качеством как таковым, а тем, сколько ресурсов вы готовы отдать за последние проценты. Сводка ниже собрана по документации PEFT и фреймворка запуска дообучения Axolotl, а строки про доступное железо дополнены фактом нашего стенда. Порядки величин здесь качественные, потому что конкретные цифры сильно зависят от длины контекста и размера батча.
| Критерий | Полное дообучение | LoRA | QLoRA |
|---|---|---|---|
| Что обучается | все веса модели | низкоранговые матрицы поверх замороженной базы | то же, база ещё и квантована в 4 бита |
| Память относительно инференса | кратно больше | чуть больше инференса | примерно четверть от LoRA по базе |
| Размер артефакта | полная копия модели, десятки ГБ | единицы или десятки МБ | единицы или десятки МБ |
| Железо | кластер или профессиональные ускорители | одна видеокарта, MPS, в пределе CPU | только CUDA из-за bitsandbytes |
| Потеря качества | нет | небольшая на сложных сдвигах домена | плюс потеря от квантования |
| Когда брать | меняется сама предметная область | меняются формат, стиль, поведение | то же, но железа не хватает |
Практика последних лет проста. Начинают с LoRA, к QLoRA переходят, когда модель не влезает в память, а полное дообучение остаётся уделом тех, кто строит свою базовую модель, а не приспосабливает чужую.
Сценарии применения
Дообучение оправдано там, где промптом задачу решить можно, но каждый раз и дорого. Типичные поводы взяться за fine-tuning выглядят так.
- Жёсткий формат вывода. Модель обязана отдавать строку, JSON или разметку определённого вида, а инструкция в промпте соблюдается через раз.
- Свой стиль и тон. Поддержка, юридические тексты, внутренняя документация, где важна манера изложения компании.
- Узкая доменная терминология. Модель должна понимать сокращения предприятия и не переспрашивать.
- Экономия на длине промпта. Инструкция на две тысячи токенов, которая едет с каждым запросом, переносится в веса и перестаёт оплачиваться.
- Дистилляция. Маленькая модель учится повторять поведение большой, чтобы работать на своём железе.
Во всех пяти случаях меняется поведение модели, а не набор фактов, которыми она оперирует, и это не совпадение.
Когда дообучение не нужно
Если задача в том, чтобы модель знала свежие или закрытые факты, дообучение это неверный инструмент. Факты меняются, а перезапуск обучения на каждое изменение никто не выдержит. Здесь работает RAG, где нужный фрагмент находится в базе и подкладывается в промпт. Обновить факт в такой базе стоит одной записи. Когда речь заходит про эксплуатацию всей этой конструкции, про версии адаптеров, откаты и мониторинг, начинается отдельная дисциплина, разобранная в статье Что такое LLMOps или MLOps для больших языковых моделей.
Практика LoRA-дообучения на локальной машине
По традиции весь код используемый в статье выкладываем на наш GitHub репозиторий
Прогон сделан на macOS 26.5.2 с Apple Silicon, на ускорителе MPS (Metal Performance Shaders), без всякой CUDA. Модель взята маленькая, Qwen2.5-0.5B-Instruct, а задача выбрана так, чтобы результат читался глазами. Модель учат отвечать одной строкой строгого формата с определением и категорией вместо нескольких абзацев свободного текста. Обучающая выборка это сорок пар по терминам инженерии данных, они лежат прямо в файле dataset.py, там же переписанный шаблон чата с разметкой реплик ассистента. Штатный шаблон Qwen2.5 такой разметки не содержит, поэтому без правки маска ответа получается пустой.
# torch 2.13.0, transformers 5.15.1, peft 0.20.0, trl 1.10.0, Qwen2.5-0.5B-Instruct,
# прогнано на macOS 26.5.2 (Apple Silicon, MPS) 2026-08-24
import json
import os
import subprocess
import time
import torch
from datasets import Dataset
from peft import LoraConfig
from transformers import AutoModelForCausalLM, AutoTokenizer
from trl import SFTConfig, SFTTrainer
from dataset import CHAT_TEMPLATE, train_records
BASE_MODEL = "Qwen/Qwen2.5-0.5B-Instruct"
ADAPTER_DIR = "lora_adapter"
SEED = 42
def main():
device = "mps" if torch.backends.mps.is_available() else "cpu"
print(f"устройство: {device}, torch {torch.__version__}")
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
# Шаблон с разметкой ответа: без него assistant_only_loss работать не может
tokenizer.chat_template = CHAT_TEMPLATE
# float32: на MPS половинная точность в обучении даёт нестабильный loss
model = AutoModelForCausalLM.from_pretrained(BASE_MODEL, dtype=torch.float32)
model.config.use_cache = False # несовместимо с расчётом градиентов
records = train_records()
dataset = Dataset.from_list(records)
print(f"примеров в обучающей выборке: {len(dataset)}")
# Ранг r задаёт размер низкоранговых матриц, alpha масштабирует их вклад.
# target_modules: проекции внутри блока внимания, классический минимум для LoRA.
lora_config = LoraConfig(
r=8,
lora_alpha=16,
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
task_type="CAUSAL_LM",
)
training_args = SFTConfig(
output_dir="checkpoints",
num_train_epochs=8,
per_device_train_batch_size=4,
learning_rate=2e-4,
lr_scheduler_type="constant",
logging_steps=5,
max_length=256,
packing=False,
# loss только по токенам ответа: вопрос модель воспроизводить не учим
assistant_only_loss=True,
save_strategy="no",
report_to=[],
seed=SEED,
disable_tqdm=True,
)
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset,
processing_class=tokenizer,
peft_config=lora_config,
)
# Доля обучаемых параметров: главная цифра, ради которой LoRA и берут
trainable = sum(p.numel() for p in trainer.model.parameters() if p.requires_grad)
total = sum(p.numel() for p in trainer.model.parameters())
print(f"всего параметров: {total:,}")
print(f"обучаемых параметров: {trainable:,} ({100 * trainable / total:.3f}%)")
started = time.time()
result = trainer.train()
elapsed = time.time() - started
history = [row for row in trainer.state.log_history if "loss" in row]
if history:
print(f"loss на старте: {history[0]['loss']:.4f}")
print(f"loss в конце: {history[-1]['loss']:.4f}")
print(f"шагов обучения: {result.global_step}")
print(f"время обучения: {elapsed:.1f} с")
trainer.save_model(ADAPTER_DIR)
size = subprocess.run(["du", "-sh", ADAPTER_DIR], capture_output=True, text=True)
print(f"адаптер сохранён: {size.stdout.strip()}")
for name in sorted(os.listdir(ADAPTER_DIR)):
print(f" {name}")
with open("train_metrics.json", "w", encoding="utf-8") as fh:
json.dump(
{
"trainable_params": trainable,
"total_params": total,
"trainable_share_percent": round(100 * trainable / total, 4),
"steps": result.global_step,
"seconds": round(elapsed, 1),
"loss_first": history[0]["loss"] if history else None,
"loss_last": history[-1]["loss"] if history else None,
},
fh,
ensure_ascii=False,
indent=2,
)
if __name__ == "__main__":
main()
Восемь эпох по сорока примерам дали 80 шагов и уложились в три минуты на ноутбуке. Вот что напечатал скрипт.
всего параметров: 495,114,112 обучаемых параметров: 1,081,344 (0.218%) loss на старте: 2.5928 loss в конце: 0.6465 шагов обучения: 80 время обучения: 171.2 с адаптер сохранён: 15M lora_adapter
Loss падал ровно, от 2.593 к 0.647, а доля верно предсказанных токенов ответа выросла с 0.505 до 0.884. Каталог адаптера весит 15 МБ, потому что рядом с весами сохраняется токенизатор, сами веса это те самые 4,35 МБ. Дальше скрипт compare_before_after.py задаёт одни и те же вопросы базовой модели и ей же с навешенным адаптером, генерация жадная, поэтому разница объясняется адаптером, а не случайностью выбора токенов.
ДО, HDFS: Hadoop Distributed File System (HDFS) - это распределенная файловая система,
используемый для хранения больших объемов данных в компьютерных системах.
ПОСЛЕ, HDFS: HDFS - распределённая файловая система Hadoop, которая хранит копии файлов на
нескольких серверах для обработки пакетов. Категория: хранение.
Формат встал. Тот же эффект виден и на отложенных терминах, которых в обучении не было, значит модель не заучила сорок строк, а подхватила манеру отвечать.
Ограничения и грабли
Самое интересное в этом прогоне не то, что получилось, а то, что не получилось. Формат перенёсся на все четыре отложенных термина, а фактическая точность нет. Про Apache HBase модель после дообучения начинает ответ словами «Apache Hadoop», Data Vault относит к категории MLOps, а в русское предложение про Apache Hive у неё вклиниваются китайские иероглифы. До дообучения ответы были ровно такими же неверными, просто длиннее. Лучшей иллюстрации главного тезиса не придумаешь, ведь дообучение поставило форму и не тронуло знания.
Остальные грабли этого класса задач стоит знать заранее.
- Переобучение на маленькой выборке. Сорок примеров и восемь эпох это край, дальше модель начинает выдавать заученные определения на любой вопрос.
- Половинная точность на MPS. В float16 обучение на Apple Silicon даёт нестабильные градиенты и NaN в loss, поэтому в коде стоит float32.
- Разметка ответа в шаблоне чата. Штатный шаблон Qwen2.5 не содержит блока generation, поэтому режим подсчёта loss только по ответу с ним не работает и шаблон приходится переписывать руками.
- Катастрофическое забывание. Долгое дообучение под узкую задачу портит общие способности модели, а LoRA страдает от этого меньше полного дообучения ровно потому, что база заморожена.
- Смена версий библиотек. В TRL 1.10.0 параметр называется max_length, а не max_seq_length, и подобные переименования между минорными версиями встречаются регулярно.
Ни одна из этих ловушек не видна на этапе планирования, все они всплывают на первом же реальном прогоне, что и есть аргумент в пользу маленькой модели на старте.
Разработка и внедрение ML-решений
Код курса
MLOPS
Ближайшая дата курса
9 ноября, 2026
Продолжительность
24 ак.часов
Стоимость обучения
66 000
Заключение
Дообучение модели меняет её веса, а не контекст, и потому закрывает вопросы формы, стиля и поведения, оставляя вопросы знания за RAG и промптингом. LoRA сделала эту процедуру дешёвой настолько, что рабочий адаптер снимается на ноутбуке за три минуты и весит меньше пяти мегабайт, а QLoRA растягивает тот же приём на модели, которые иначе не влезли бы в память. Дальше начинается инженерия, то есть версионирование адаптеров, регресс-тесты качества и выкатка. Как это устроено в промышленной эксплуатации, разбираем на курсе Архитектура ML систем в нашем лицензированном учебном центре в Москве.
Референсные ссылки
- Документация Hugging Face PEFT, перечень adapter-методов и требования к версиям библиотек.
- Релизы библиотеки TRL, состав версии 1.0 с тренерами SFT, DPO, GRPO и reward-моделями.
- Документация Axolotl, конфигурационный подход к запуску полного дообучения, LoRA и QLoRA.
- Релизы Unsloth, точечные примеры требований к видеопамяти по методам дообучения.



