Перейти к содержимому
Ступень 66.2 Параметрически-эффективное дообучение

Практика — LoRA-дообучение Qwen

Дообучаем Qwen2.5-0.5B-Instruct с LoRA на русском датасете: замер до обучения, крошечный прогон на CPU и полный на GPU, оценка после, проверка забывания, слияние весов — и тот же эксперимент в LLaMA-Factory.

УРОКstage-6/6-2-peft/02-finetune-practice.mdx2 ч 30 минУрок 2 из 2
После урока вы сможете
  • Подготовить русский SFT-датасет в формате messages и честно измерить качество модели до обучения
  • Дообучить Qwen2.5-0.5B-Instruct (или Qwen3-0.6B) с LoRA на PyTorch и PEFT — крошечный прогон на CPU и полный на GPU
  • Сравнить модель до и после, проверить забывание, слить адаптер и повторить эксперимент в LLaMA-Factory

В двух прошлых уроках вы собрали все детали: формат диалога и маску loss (урок 6.1.2), LoRA и слияние весов (урок 6.2.1). Теперь соберём их в настоящий эксперимент: дообучим Qwen2.5-0.5B-Instruct на русском датасете и измерим результат.

Главный навык здесь — не «запустить обучение», а честно измерить. Нужно знать, что умела модель до обучения, проверять её на примерах, которых она не видела, и следить, что она не разучилась остальному. Без этого LoRA превращается в «алхимию» («ляньдань», букв. «выплавка эликсира бессмертия» — так китайские инженеры в шутку называют обучение нейросетей с подбором гиперпараметров наугад), где непонятно, что сработало.

  1. Задача и данные. Генерируем русский датасет с точной автоматической проверкой.
  2. Замер до. Как исходная модель справляется с задачей одной инструкцией в промпте?
  3. Обучение LoRA. Маска loss из урока 6.1.2, адаптеры на всех линейных слоях.
  4. Замер после. Точность на новых формулировках и проверка забывания.
  5. Слияние и сохранение. Адаптер отдельно и слитая модель.
  6. То же в LLaMA-Factory. Китайский фреймворк дообучения: один YAML-файл вместо скрипта.

Из корня репозитория курса: pip install -r homework/requirements-llm.txt (transformers, peft, trl, datasets, accelerate; что делает каждая библиотека и как закрепить версии — урок 4.6.5). Модель скачается при первом запуске, около 1 ГБ. Если Hugging Face недоступен, скачайте её с ModelScope: pip install modelscope, затем modelscope download --model Qwen/Qwen2.5-0.5B-Instruct --local_dir ./qwen-0.5b и передайте скрипту --model ./qwen-0.5b.

РежимЖелезоЧто запускатьВремя
Крошечный прогонCPU, 8+ ГБ RAM30 шагов, батч 4, 20 тестовых примеровобучение — минуты, генерация для оценки — ещё несколько минут
Полный прогонGPU 8+ ГБ (в том числе бесплатные в Colab и Kaggle)300 шагов, батч 8, 200 тестовых примеровминуты
ДальшеGPU 16–24 ГБQwen2.5-7B через QLoRA (прошлый урок)от получаса

Для ориентира: на 4-ядерном облачном CPU один шаг обучения Qwen2.5-0.5B (батч 4 × ~80 токенов, LoRA r=16r = 16) у нас занимал около 5 секунд.

Задача — извлечь из свободного сообщения клиента заказ в JSON:

Пришлите, пожалуйста, лунцзина — 3 упаковки. Я живу в Новосибирске.
→ {"tea": "лунцзин", "qty": 3, "city": "Новосибирск"}

Почему именно она:

  • Точная проверка. Ответ либо совпадает с эталоном, либо нет. Никаких «LLM-судей» (о них — в модуле 7.3).
  • Русская морфология. В тексте «в Новосибирске», «во Владивосток», «пуэра», а в ответе нужны именительный падеж и число. Модель должна нормализовать формы, а не копировать.
  • Тест на обобщение. Обучающие и тестовые примеры собраны из разных шаблонов фраз. Модель, которая просто запомнила 8 обучающих формулировок, на тесте провалится.

Генератор целиком (только стандартная библиотека Python):

"""Синтетический русский датасет: заказ китайского чая -> JSON. Запуск: python make_tea_data.py"""
import json
import random
TEAS = [ # (именительный, родительный)
("пуэр", "пуэра"), ("шу пуэр", "шу пуэра"), ("шэн пуэр", "шэн пуэра"), ("лунцзин", "лунцзина"),
("улун", "улуна"), ("молочный улун", "молочного улуна"), ("те гуаньинь", "те гуаньинь"),
("да хун пао", "да хун пао"), ("жасминовый чай", "жасминового чая"), ("белый чай", "белого чая"),
("красный чай", "красного чая"),
]
CITIES = [ # (именительный, «куда», «где»)
("Москва", "в Москву", "в Москве"), ("Санкт-Петербург", "в Санкт-Петербург", "в Санкт-Петербурге"),
("Казань", "в Казань", "в Казани"), ("Новосибирск", "в Новосибирск", "в Новосибирске"),
("Екатеринбург", "в Екатеринбург", "в Екатеринбурге"), ("Самара", "в Самару", "в Самаре"),
("Тула", "в Тулу", "в Туле"), ("Пермь", "в Пермь", "в Перми"), ("Владивосток", "во Владивосток", "во Владивостоке"),
("Иркутск", "в Иркутск", "в Иркутске"), ("Уфа", "в Уфу", "в Уфе"), ("Минск", "в Минск", "в Минске"),
]
TRAIN_TEMPLATES = [
"Здравствуйте! Хочу заказать {n} {pack_acc} {tea_gen}, доставка {city_to}.",
"Пришлите, пожалуйста, {tea_gen} — {n} {pack_acc}. Я живу {city_at}.",
"Нужно {n} {pack_acc} {tea_gen} {city_to}, оплата при получении.",
"{city_nom}. {tea_nom_cap}, {n} шт.",
"Добрый день! Можно {n} {pack_acc} {tea_gen}? Отправьте {city_to}.",
"Закажу {tea_gen} {n} {pack_acc}, город — {city_nom}.",
"Мне бы {n} {pack_acc} {tea_gen} с доставкой {city_to}. Спасибо!",
"Оформите заказ: {tea_nom}, {n} шт., {city_nom}.",
]
TEST_TEMPLATES = [ # этих формулировок модель на обучении не увидит
"Привет! Отправьте {city_to} {n} {pack_acc} {tea_gen}, пожалуйста.",
"Город {city_nom}, нужен {tea_nom}, количество — {n}.",
"Я из города {city_nom}. Возьму {tea_gen} {n} {pack_acc}.",
]
SYSTEM = "Извлеки из сообщения клиента заказ. Ответь только JSON с ключами tea, qty, city."
def pack_acc(n):
"""1 упаковку, 2 упаковки, 5 упаковок, 11 упаковок, 21 упаковку."""
if n % 10 == 1 and n % 100 != 11:
return "упаковку"
if 2 <= n % 10 <= 4 and not 12 <= n % 100 <= 14:
return "упаковки"
return "упаковок"
def make_example(rng, templates):
(tea_nom, tea_gen), (city_nom, city_to, city_at) = rng.choice(TEAS), rng.choice(CITIES)
n = rng.randint(1, 12)
text = rng.choice(templates).format(
n=n, pack_acc=pack_acc(n), tea_nom=tea_nom, tea_nom_cap=tea_nom.capitalize(), tea_gen=tea_gen,
city_nom=city_nom, city_to=city_to, city_at=city_at,
)
answer = json.dumps({"tea": tea_nom, "qty": n, "city": city_nom}, ensure_ascii=False)
return {"messages": [
{"role": "system", "content": SYSTEM},
{"role": "user", "content": text},
{"role": "assistant", "content": answer},
]}
if __name__ == "__main__":
rng = random.Random(42)
for name, templates, size in [("train", TRAIN_TEMPLATES, 2000), ("test", TEST_TEMPLATES, 200)]:
rows = [make_example(rng, templates) for _ in range(size)]
with open(f"tea_{name}.json", "w", encoding="utf-8") as f:
json.dump(rows, f, ensure_ascii=False, indent=1)
print(f"tea_{name}.json: {len(rows)} примеров")
for m in make_example(random.Random(7), TRAIN_TEMPLATES)["messages"]:
print(f"{m['role']:>9}: {m['content']}")
tea_train.json: 2000 примеров
tea_test.json: 200 примеров
system: Извлеки из сообщения клиента заказ. Ответь только JSON с ключами tea, qty, city.
user: Здравствуйте! Хочу заказать 7 упаковок молочного улуна, доставка в Казань.
assistant: {"tea": "молочный улун", "qty": 7, "city": "Казань"}

Формат файла — тот же список messages, что в уроке 6.1.2. Его без изменений понимают и наш скрипт, и LLaMA-Factory.

Шаг 2. Скрипт: оценка → LoRA → оценка → слияние

Заголовок раздела «Шаг 2. Скрипт: оценка → LoRA → оценка → слияние»

Весь эксперимент — один файл lora_qwen.py из четырёх частей. Код целиком прогнан на transformers 5.x и peft 0.21 (на маленькой модели той же архитектуры Qwen2). Мы намеренно пишем цикл обучения сами, без Trainer: так видно каждую деталь из прошлых уроков.

"""LoRA-дообучение Qwen на заказах чая. CPU: --steps 30 --eval-n 20; GPU: --steps 300 --batch 8 --eval-n 200"""
import argparse
import json
import random
import time
import torch
from peft import LoraConfig, get_peft_model
from transformers import AutoModelForCausalLM, AutoTokenizer
# argparse разбирает аргументы командной строки: `python lora_qwen.py --steps 300` → args.steps == 300
ap = argparse.ArgumentParser()
ap.add_argument("--model", default="Qwen/Qwen2.5-0.5B-Instruct")
ap.add_argument("--steps", type=int, default=30)
ap.add_argument("--batch", type=int, default=4)
ap.add_argument("--lr", type=float, default=2e-4)
ap.add_argument("--rank", type=int, default=16)
ap.add_argument("--eval-n", type=int, default=40)
ap.add_argument("--out", default="lora-tea")
args = ap.parse_args()
torch.manual_seed(0)
device = "cuda" if torch.cuda.is_available() else "cpu"
dtype = torch.bfloat16 if device == "cuda" and torch.cuda.is_bf16_supported() else torch.float32
tok = AutoTokenizer.from_pretrained(args.model)
model = AutoModelForCausalLM.from_pretrained(args.model).to(device=device, dtype=dtype)
train = json.load(open("tea_train.json", encoding="utf-8"))
test = json.load(open("tea_test.json", encoding="utf-8"))[: args.eval_n]
GENERAL = ["Какая столица Франции?", "Переведи на английский: «Я люблю зелёный чай».", "Сколько будет 17 * 3?"]
# ── 1. Оценка: генерация ответа и сравнение JSON ────────────────────────────────
@torch.no_grad()
def generate(messages, max_new_tokens=48):
text = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, enable_thinking=False)
enc = tok(text, return_tensors="pt", add_special_tokens=False).to(device)
out = model.generate(**enc, max_new_tokens=max_new_tokens, do_sample=False, repetition_penalty=1.0,
pad_token_id=tok.pad_token_id)
return tok.decode(out[0, enc["input_ids"].shape[1]:], skip_special_tokens=True).strip()
def parse(text):
"""Строгий разбор (весь ответ — JSON) и мягкий (JSON где-то внутри, например в ```json … ```)."""
try:
return json.loads(text), True
except json.JSONDecodeError:
pass
try: # вырезаем кусок от первой «{» до последней «}» и пробуем разобрать его
return json.loads(text[text.index("{"): text.rindex("}") + 1]), False
except ValueError: # нет скобок или внутри не JSON (JSONDecodeError — подкласс ValueError)
return None, False
def evaluate(tag):
model.eval()
strict = correct = 0
for ex in test:
gold = json.loads(ex["messages"][-1]["content"])
pred, is_strict = parse(generate(ex["messages"][:-1]))
strict += is_strict
correct += pred == gold
print(f"[{tag}] строгий JSON: {strict / len(test):.0%} | заказ извлечён верно: {correct / len(test):.0%}")
print(f"[{tag}] пример: {generate(test[0]['messages'][:-1])}")
for q in GENERAL: # не разучилась ли модель обычным вещам?
print(f"[{tag}] {q} -> {generate([{'role': 'user', 'content': q}], 40)[:80]!r}")
evaluate("до")
# ── 2. Данные: шаблон чата + маска loss (как в уроке 6.1.2) ─────────────────────
def encode(messages, max_len=256):
prompt = tok.apply_chat_template(messages[:-1], tokenize=False, add_generation_prompt=True, enable_thinking=False)
full = tok.apply_chat_template(messages, tokenize=False, enable_thinking=False)
assert full.startswith(prompt), "шаблон устроен иначе: промпт не является префиксом диалога"
p_ids = tok(prompt, add_special_tokens=False)["input_ids"]
ids = tok(full, add_special_tokens=False)["input_ids"][:max_len]
assert ids[: len(p_ids)] == p_ids, "токены промпта поменялись на стыке — стройте маску по смещениям"
return ids, [-100] * len(p_ids) + ids[len(p_ids):] # loss только на ответе (+ <|im_end|> и \n после него)
def collate(rows):
T = max(len(ids) for ids, _ in rows)
x = torch.full((len(rows), T), tok.pad_token_id)
y = torch.full((len(rows), T), -100)
att = torch.zeros((len(rows), T), dtype=torch.long)
for i, (ids, labels) in enumerate(rows):
x[i, : len(ids)], y[i, : len(ids)], att[i, : len(ids)] = torch.tensor(ids), torch.tensor(labels), 1
return x.to(device), y.to(device), att.to(device)
data = [encode(ex["messages"]) for ex in train]
ids0, lab0 = data[0]
print(f"пример: {len(ids0)} токенов, из них в loss: {sum(l != -100 for l in lab0)}")
# ── 3. LoRA: замораживаем модель, добавляем адаптеры во все линейные слои блока ──
config = LoraConfig(
r=args.rank, lora_alpha=2 * args.rank, lora_dropout=0.05, task_type="CAUSAL_LM",
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
)
model = get_peft_model(model, config)
model.print_trainable_parameters()
opt = torch.optim.AdamW([p for p in model.parameters() if p.requires_grad], lr=args.lr, weight_decay=0.0)
sched = torch.optim.lr_scheduler.LambdaLR(opt, lambda s: min(1.0, (s + 1) / 10)) # 10 шагов warmup
rng = random.Random(0)
model.train()
t0 = time.time()
for step in range(1, args.steps + 1):
x, y, att = collate(rng.sample(data, args.batch))
loss = model(input_ids=x, attention_mask=att, labels=y).loss # сдвиг на 1 токен HF делает сам
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
opt.step()
sched.step()
opt.zero_grad()
if step % 10 == 0 or step == 1:
print(f"шаг {step:4d} | loss {loss.item():.3f} | {(time.time() - t0) / step:.1f} с/шаг")
evaluate("после")
# ── 4. Сохраняем адаптер (мегабайты) и слитую модель (полный размер) ────────────
model.save_pretrained(args.out)
merged = model.merge_and_unload()
merged.save_pretrained(args.out + "-merged")
tok.save_pretrained(args.out + "-merged")
print("сохранено:", args.out, "и", args.out + "-merged")

Что здесь важно:

  • Оценка — жадная генерация (do_sample=False). Иначе результат будет меняться от запуска к запуску. repetition_penalty=1.0 отключает штраф за повторы из generation_config модели: в JSON повторяющиеся кавычки — норма.
  • Две метрики. «Строгий JSON» — ответ целиком парсится как JSON, без пояснений и обёрток ```json. «Заказ извлечён верно» — найденный JSON совпадает с эталоном, даже если вокруг есть лишний текст. Первая метрика проверяет формат, вторая — понимание задачи.
  • Маска — способ «префикс» из урока 6.1.2. Два assert ловят случаи, когда шаблон или токенизация на стыке ведут себя неожиданно. Ошибки здесь не бывают «тихими».
  • enable_thinking=False нужен Qwen3: без него гибридная модель начнёт ответ с рассуждения. Есть и второй эффект: с этим флагом пустой блок <think>\n\n</think> оказывается в промпте и не входит в loss. Без флага он попал бы в ответ, и модель училась бы его печатать. Шаблон Qwen2.5 этот параметр просто игнорирует.
  • LoRA r=16r = 16, α=32\alpha = 32, LR 2⋅10−42 \cdot 10^{-4} с коротким warmup и обрезкой градиента. Обучаются 8.8 млн параметров. print_trainable_parameters() покажет 1.75%: PEFT делит на сумму базы и адаптера (8.8/502.88.8 / 502.8), а не на 494 млн.
Окно терминала
python make_tea_data.py
python lora_qwen.py --steps 30 --eval-n 20 # CPU: крошечный прогон
python lora_qwen.py --steps 300 --batch 8 --eval-n 200 # GPU: полный прогон
python lora_qwen.py --model Qwen/Qwen3-0.6B --steps 300 --batch 8 --eval-n 200 # то же для Qwen3

Для Qwen3 нужна transformers не ниже 4.51. На шаблоне Qwen3 с enable_thinking=False промпт — префикс полного диалога, так что оба assert должны пройти. Если на вашей версии шаблона первый всё же сработает, перейдите на построение маски поиском маркеров — как в задании урока 6.1.2.

Заполните таблицу своими числами. Мы не приводим «правильные» значения: они зависят от версий библиотек и железа, а честный замер — часть задания.

МетрикаДоПосле: CPU, 30 шаговПосле: GPU, 300 шагов
Строгий JSON
Заказ извлечён верно
Ответ на «Какая столица Франции?»

На что смотреть.

  1. Ошибки до обучения. Разберите 5–10 ответов глазами. Типичные отклонения у маленьких моделей: обёртка ```json, пояснения до или после JSON, ключи по-русски, город в той форме, что в тексте («Тулу»), число строкой. Каждая из этих ошибок — то, чему должна научить LoRA.
  2. Loss. Он должен заметно упасть за первые десятки шагов: формат ответа модель перенимает быстро. Если loss равен nan уже на первом шаге, скорее всего, в батче не осталось ни одного обучаемого токена (все labels равны −100-100).
  3. Обобщение. Тест собран из новых формулировок. Временно подставьте вместо test первые 100 примеров train и сравните. Если на обучающих фразах почти 100%, а на тесте заметно ниже — модель запомнила шаблоны фраз. Помогают более разнообразные данные, а не больше шагов.
  4. Забывание. Сравните ответы на три общих вопроса. Если модель отвечает на «Сколько будет 17 * 3?» JSON-ом, она переучилась на узкую задачу — это катастрофическое забываниекатастрофическое забываниеEN catastrophic forgettingПри дообучении на узкой задаче модель теряет ранее освоенные навыки..
  • Меньше обучения: меньше шагов, ниже LR, меньше ранг. 30 шагов часто достаточно для формата.
  • Подмешайте общие данные. Добавьте 10–30% обычных диалогов. Простой приём — сгенерировать ответы исходной модели на разнообразные вопросы и добавить их в обучение. Так модель «повторяет» своё прежнее поведение.
  • Не сливайте адаптер. Держите базу отдельно и включайте LoRA только для этой задачи — для остального модель останется исходной.
СимптомВероятная причинаЧто сделать
loss = nanпустые labels, FP16 (например, fp16: true в LLaMA-Factory), слишком большой LRпроверить labels, BF16 или FP32, LR ниже
Loss не падаетвсе labels равны −100-100 или LR слишком малнапечатайте число токенов в loss (скрипт это делает)
Out of memoryдлинные примеры, большой батч--batch меньше, max_len в encode меньше, QLoRA
Модель не останавливается<|im_end|> не попал в lossпроверьте конец labels у примера
Срабатывает assertшаблон модели устроен иначемаска поиском маркеров (урок 6.1.2)
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
tok = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
# Вариант 1: база + адаптер (адаптер можно отключить или заменить другим)
model = PeftModel.from_pretrained(AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct"), "lora-tea")
# Вариант 2: слитая модель — обычная модель transformers, peft не нужен
merged = AutoModelForCausalLM.from_pretrained("lora-tea-merged")
msgs = [{"role": "system", "content": "Извлеки из сообщения клиента заказ. Ответь только JSON с ключами tea, qty, city."},
{"role": "user", "content": "Добрый вечер! Пришлите 2 упаковки улуна в Пермь."}]
enc = tok.apply_chat_template(msgs, add_generation_prompt=True, return_tensors="pt", return_dict=True)
for m in (model, merged):
out = m.generate(**enc, max_new_tokens=40, do_sample=False)
print(tok.decode(out[0, enc["input_ids"].shape[1]:], skip_special_tokens=True))

Оба варианта должны дать одинаковый ответ — это проверка того, что слияние ничего не испортило (то же свойство вы проверяли в задании прошлого урока). Если модели загружены в BF16, изредка возможны расхождения: слитый вес округляется иначе, чем сумма двух веток. Слитую модель можно сконвертировать в GGUF и запустить в llama.cpp или Ollama — это модуль 7.2.

Писать цикл PEFTпараметрически-эффективное дообучение (PEFT)EN parameter-efficient fine-tuning (PEFT)Обучается малая доля параметров (адаптеры, LoRA, префиксы), основная модель заморожена. Так же называется библиотека Hugging Face. вручную полезно для понимания. На практике чаще берут готовый фреймворк. Самый популярный — LLaMA-Factory (Apache-2.0): обучение, диалог и слияние описываются YAML-файлами, есть веб-интерфейс.

1. Установка (по README проекта):

Окно терминала
git clone --depth 1 https://github.com/hiyouga/LlamaFactory.git
cd LlamaFactory
pip install -e .
pip install -r requirements/metrics.txt

2. Датасет. Скопируйте tea_train.json в LlamaFactory/data/ и добавьте в data/dataset_info.json описание в формате OpenAI. Это частный случай формата sharegpt:

"tea_orders": {
"file_name": "tea_train.json",
"formatting": "sharegpt",
"columns": {"messages": "messages"},
"tags": {"role_tag": "role", "content_tag": "content", "user_tag": "user",
"assistant_tag": "assistant", "system_tag": "system"}
}

3. Конфиг обучения tea_lora.yaml — по образцу examples/train_lora/qwen3_lora_sft.yaml:

### model
model_name_or_path: Qwen/Qwen2.5-0.5B-Instruct
### method
stage: sft
do_train: true
finetuning_type: lora
lora_rank: 16 # lora_alpha по умолчанию = 2 * lora_rank
lora_target: all # все линейные слои
### dataset
dataset: tea_orders
template: qwen # для Qwen3-0.6B: qwen3 и строка enable_thinking: false
cutoff_len: 256
### output
output_dir: saves/qwen2.5-0.5b/lora/tea
logging_steps: 10
plot_loss: true
overwrite_output_dir: true
report_to: none
### train
per_device_train_batch_size: 8
gradient_accumulation_steps: 1
learning_rate: 2.0e-4
num_train_epochs: 1.0
lr_scheduler_type: cosine
warmup_ratio: 0.1
bf16: true # на GPU без BF16 (например, T4) замените на fp16: true

4. Три команды — обучение, диалог, слияние:

Окно терминала
llamafactory-cli train tea_lora.yaml
llamafactory-cli chat tea_chat.yaml # model_name_or_path, adapter_name_or_path, template
llamafactory-cli export tea_merge.yaml # то же + export_dir: куда сохранить слитую модель

Конфиги для chat и export — это три–пять строк: путь к базе, путь к адаптеру (saves/qwen2.5-0.5b/lora/tea), template: qwen, а для экспорта ещё export_dir. Образцы лежат в examples/inference/ и examples/merge_lora/. Команда llamafactory-cli webui открывает тот же функционал в браузере.

Что LLaMA-Factory делает «под капотом» — то, что вы уже писали руками. Флаг train_on_prompt по умолчанию выключен: loss только на ответах. Значение template — тот самый chat-шаблон. README предупреждает: используйте одинаковый шаблон при обучении и инференсе.

Шаблоны Qwen3 устроены так. qwen3_nothink — обычный ChatML без тегов рассуждения, он для моделей без режима мышления (например, Qwen3-4B-Instruct-2507 в примерах LLaMA-Factory). Гибридную Qwen3-0.6B учите с template: qwen3 и enable_thinking: false: тогда пустой блок <think>\n\n</think> уходит в промпт, без loss, как в нашем скрипте.

Проверь себя5 вопросов
1 Зачем тестовые примеры собраны из других шаблонов фраз, чем обучающие?
2 После обучения на «Сколько будет 17 * 3?» модель отвечает {"tea": ...}. Какие меры помогут?Можно выбрать несколько ответов
3 Qwen2.5-0.5B: 494 млн параметров, LoRA r=16r = 16 на всех линейных слоях — 8.8 млн. Сколько это процентов от всех параметров? Округлите до десятых.
4 Что даёт merge_and_unload() по сравнению с save_pretrained() адаптера?
5 Вы обучили LoRA в LLaMA-Factory с template: qwen, а запускаете модель с шаблоном Llama 3. Что вероятнее всего?
  • Полный цикл дообучения: данные с точной проверкой → замер до → LoRA → замер после на новых формулировках → проверка забывания → слияние.
  • Скрипт на PyTorch и PEFT — около 120 строк. В нём всё из модуля: chat-шаблон, маска loss, LoRA на всех линейных слоях, merge_and_unload.
  • Узкое дообучение может испортить общие навыки. Помогают меньшие шаги и LR, общие данные в смеси и раздельное хранение адаптера.
  • LLaMA-Factory делает то же самое через YAML: llamafactory-cli train / chat / export. Шаблон обучения и инференса должен совпадать.

Дальше: SFT учит подражать образцам. Чтобы модель отвечала лучше образцов, нужны предпочтения и RL — модуль 6.3: основы RL, RLHF и DPO.