Перейти к содержимому
ПереводыDatawhale и китайские курсы

self-llm: Qwen3-8B — LoRA-дообучение и графики в SwanLab

Полный перевод рецепта self-llm «Qwen3-8B: LoRA и запись обучения в SwanLab»: датасет в формате instruction/input/output, токенизация с маскированием подсказки (-100), шаблон диалога Qwen3 с режимом размышления и без, LoraConfig, TrainingArguments, запись обучения в SwanLab и инференс с LoRA-весами.

Окно терминала
# Переключаемся на зеркало Цинхуа
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip install modelscope==1.25.0
pip install transformers==4.51.3
pip install accelerate==1.6.0
pip install datasets==3.5.1
pip install peft==0.15.2
pip install swanlab==0.5.7

Поскольку у некоторых при настройке окружения бывают проблемы, мы подготовили на платформе AutoDL образ окружения для Qwen3: перейдите по ссылке ниже и сразу создайте экземпляр AutoDL. https://www.codewithgpu.com/i/datawhalechina/self-llm/Qwen3

model_download.py
# Не забудьте заменить cache_dir на свой путь
from modelscope import snapshot_download
model_dir = snapshot_download('Qwen/Qwen3-8B', cache_dir='ПОМЕНЯЙ_МЕНЯ', revision='master') # укажите свой путь
print(f"Модель скачана, путь: {model_dir}")

Данные для supervised-finetuning (sft, обучение с учителем) больших языковых моделей имеют такой формат:

{
"instruction": "Ответь на вопрос пользователя, выведи только ответ.",
"input": "Сколько будет 1+1?",
"output": "2"
}

Здесь instruction — указание пользователя: какую задачу должна решить модель; input — входные данные пользователя, нужные для выполнения указания; output — ответ, который должна дать модель.

Цель обучения с учителем — научить модель понимать указания пользователя и следовать им. Поэтому датасет нужно строить прицельно под нашу задачу. Например, если мы хотим на большом корпусе диалогов персонажей дообучить модель, которая умеет отыгрывать (role-play) манеру речи Чжэнь Хуань, пример данных в этом сценарии будет таким:

{
"instruction": "Кто твой отец?",
"input": "",
"output": "Мой отец — Чжэнь Юаньдао, заместитель главы Далисы."
}

(Далисы — Палата судебного надзора.)

Все примеры датасетов для дообучения лежат в /dataset.

Данные для обучения LoRA (Low-Rank Adaptation) нужно отформатировать и закодировать, прежде чем подавать в модель: входной текст кодируем в input_ids, выходной — в labels; после кодирования получаются векторы. Сначала определим функцию предобработки: для каждого примера она кодирует и вход, и выход и возвращает словарь с закодированными данными:

def process_func(example):
MAX_LENGTH = 1024 # максимальная длина последовательности — 1024 токена
input_ids, attention_mask, labels = [], [], [] # инициализируем возвращаемые значения
# подгоняем под chat_template
instruction = tokenizer(
f"<s><|im_start|>system\nТеперь ты играешь женщину при императоре — Чжэнь Хуань<|im_end|>\n"
f"<|im_start|>user\n{example['instruction'] + example['input']}<|im_end|>\n"
f"<|im_start|>assistant\n<think>\n\n</think>\n\n",
add_special_tokens=False
)
response = tokenizer(f"{example['output']}", add_special_tokens=False)
# склеиваем input_ids части instruction и части response и добавляем в конец токен eos как признак конца
input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id]
# маска внимания: на какие позиции модель должна смотреть
attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1]
# для instruction ставим -100: на этих позициях loss не считается (модели не нужно их предсказывать)
labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id]
if len(input_ids) > MAX_LENGTH: # обрезаем, если длиннее максимума
input_ids = input_ids[:MAX_LENGTH]
attention_mask = attention_mask[:MAX_LENGTH]
labels = labels[:MAX_LENGTH]
return {
"input_ids": input_ids,
"attention_mask": attention_mask,
"labels": labels
}

Chat Template (шаблон диалога) у Qwen3 такой.

Поскольку Qwen3 — гибридная модель рассуждений, режим размышления можно включать вручную.

Без thinking mode:

messages = [
{"role": "system", "content": "===system_message_test==="},
{"role": "user", "content": "===user_message_test==="},
{"role": "assistant", "content": "===assistant_message_test==="},
]
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
enable_thinking=False
)
print(text)
<|im_start|>system
===system_message_test===<|im_end|>
<|im_start|>user
===user_message_test===<|im_end|>
<|im_start|>assistant
<think>
</think>
===assistant_message_test===<|im_end|>
<|im_start|>assistant
<think>
</think>

С thinking mode:

messages = [
{"role": "system", "content": "===system_message_test==="},
{"role": "user", "content": "===user_message_test==="},
{"role": "assistant", "content": "===assistant_message_test==="},
]
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
enable_thinking=True
)
print(text)
<|im_start|>system
===system_message_test===<|im_end|>
<|im_start|>user
===user_message_test===<|im_end|>
<|im_start|>assistant
<think>
</think>
===assistant_message_test===<|im_end|>
<|im_start|>assistant
tokenizer = AutoTokenizer.from_pretrained('ПОМЕНЯЙ_МЕНЯ/Qwen/Qwen3-8B') # ваш путь к модели
model = AutoModelForCausalLM.from_pretrained('ПОМЕНЯЙ_МЕНЯ/Qwen/Qwen3-8B', device_map="auto", torch_dtype=torch.bfloat16)

В классе LoraConfig много параметров, самые важные:

  • task_type: тип модели; сейчас почти все модели decoder_only — каузальные языковые модели, CAUSAL_LM;
  • target_modules: имена слоёв модели, которые нужно обучать, — в основном слои части attention; у разных моделей слои называются по-разному;
  • r: ранг LoRA — размерность низкоранговых матриц; меньший r — меньше параметров;
  • lora_alpha: параметр масштаба; вместе с r определяет силу обновления LoRA. Фактический множитель — lora_alpha/r, в нашем примере 32 / 8 = 4 раза;
  • lora_dropout: доля dropout в слоях LoRA, защищает от переобучения.
config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
inference_mode=False, # режим обучения
r=8, # ранг LoRA
lora_alpha=32, # alpha LoRA
lora_dropout=0.1 # доля Dropout
)
  • output_dir: куда сохранять модель;
  • per_device_train_batch_size: batch_size на одну карту;
  • gradient_accumulation_steps: накопление градиента;
  • num_train_epochs: как видно из названия, число epoch.
args = TrainingArguments(
output_dir="./output/Qwen3_8B_LoRA", # не забудьте поправить
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
logging_steps=10,
num_train_epochs=3,
save_steps=100,
learning_rate=1e-4,
save_on_each_node=True,
gradient_checkpointing=True,
report_to="none",
)

Рисунок в оригинале (images/05-2.png): логотип и интерфейс SwanLab.

SwanLab — открытый инструмент для записи обучения моделей, рассчитанный на исследователей ИИ: визуализация обучения, автоматическое логирование, запись гиперпараметров, сравнение экспериментов, совместная работа. В SwanLab исследователь по наглядным графикам находит проблемы обучения, сравнивает эксперименты в поисках идей и, делясь онлайн-ссылками и обучая модели в рамках организации, ломает барьеры в общении команды.

Зачем записывать обучение

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

Советуем сначала зарегистрироваться на сайте SwanLab, а при инициализации обучения выбрать (2) Use an existing SwanLab account и войти с личным (private) API Key.

import swanlab
from swanlab.integration.transformers import SwanLabCallback
# Создаём SwanLabCallback
swanlab_callback = SwanLabCallback(
project="Qwen3-Lora", # не забудьте поправить
experiment_name="Qwen3-8B-LoRA-experiment" # не забудьте поправить
)
trainer = Trainer(
model=model,
args=args,
train_dataset=tokenized_id,
data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
callbacks=[swanlab_callback] # передаём созданный выше swanlab_callback
)
trainer.train()
TrainOutput(global_step=699, training_loss=2.6425710331557988, metrics={'train_runtime': 879.9696, 'train_samples_per_second': 12.713, 'train_steps_per_second': 0.794, 'total_flos': 5.190619083415757e+16, 'train_loss': 2.6425710331557988, 'epoch': 2.990353697749196})

После обучения откройте SwanLab — там видны записанные параметры и график лосса во время обучения:

Рисунок в оригинале (images/05-1.png): кривая train/loss в SwanLab.

Публичная ссылка на запись обучения из примера, для справки:

Графики — Qwen3-Lora/Qwen3-8B-LoRA-shufan.jiang

Получив любую контрольную точку (checkpoint), загружаем веса lora для инференса:

from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
from peft import PeftModel
mode_path = 'ПОМЕНЯЙ_МЕНЯ/Qwen/Qwen3-8B' # не забудьте поправить
lora_path = './output/Qwen3_8B_lora/checkpoint-699' # не забудьте поправить
# Загружаем токенизатор
tokenizer = AutoTokenizer.from_pretrained(mode_path)
# Загружаем базовую модель Qwen3
model = AutoModelForCausalLM.from_pretrained(mode_path, device_map="auto",torch_dtype=torch.bfloat16, trust_remote_code=True)
# Загружаем веса LoRA
model = PeftModel.from_pretrained(model, model_id=lora_path)
prompt = "Кто ты?"
inputs = tokenizer.apply_chat_template(
[{"role": "user", "content": "Представь, что ты женщина при императоре — Чжэнь Хуань."},{"role": "user", "content": prompt}],
add_generation_prompt=True,
tokenize=True,
return_tensors="pt",
return_dict=True,
enable_thinking=False
)
# Параметры генерации
gen_kwargs = {"max_length": 2500, "do_sample": True, "top_k": 1}
with torch.no_grad():
outputs = model.generate(**inputs, **gen_kwargs)
outputs = outputs[:, inputs['input_ids'].shape[1]:]
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
Я Чжэнь Хуань, мой отец — Чжэнь Юаньдао, заместитель главы Далисы.

(Ответ переведён: модель, обученная на оригинальном датасете, отвечает по-китайски.)

От переводчика: как запустить на своей машине

Заголовок раздела «От переводчика: как запустить на своей машине»

Что сделать руками:

  1. Повторите рецепт на Qwen3-1.7B. Сравните ответы базовой и дообученной модели на 5 вопросах в стиле датасета.
  2. Поменяйте r (4, 8, 32) и lora_alpha, сравните кривые лосса в SwanLab или TensorBoard — урок 6.2.2.
  3. Соберите свой маленький датасет в том же формате (например, 200 вопросов и ответов по курсу на русском) и дообучите модель на нём. Проверьте, не «забыла» ли модель общие знания после дообучения.
РусскийПояснение
обучение с учителем, SFTдообучение на парах «указание — ответ»
низкоранговая адаптация (LoRA)обучаем маленькие матрицы AA, BB
ранг (rank, r)внутренняя размерность AA и BB
масштаб (lora_alpha)множитель α/r\alpha / r
накопление градиентанесколько микробатчей на шаг
пересчёт активацийэкономия памяти ценой времени
контрольная точка (checkpoint)сохранённые веса адаптера
гибридная модель рассужденийумеет отвечать с размышлением и без
запись и визуализация обученияSwanLab, W&B, TensorBoard
отыгрыш роли (role-play)модель говорит «в образе»