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.0pip install transformers==4.51.3pip install accelerate==1.6.0pip install datasets==3.5.1pip install peft==0.15.2pip install swanlab==0.5.7Поскольку у некоторых при настройке окружения бывают проблемы, мы подготовили на платформе AutoDL образ окружения для Qwen3: перейдите по ссылке ниже и сразу создайте экземпляр AutoDL. https://www.codewithgpu.com/i/datawhalechina/self-llm/Qwen3
Скачивание модели
Заголовок раздела «Скачивание модели»# Не забудьте заменить cache_dir на свой путьfrom modelscope import snapshot_downloadmodel_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)LoRA Config
Заголовок раздела «LoRA Config»В классе 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)Training Arguments
Заголовок раздела «Training Arguments»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",)Знакомство со SwanLab
Заголовок раздела «Знакомство со SwanLab»Рисунок в оригинале (images/05-2.png): логотип и интерфейс SwanLab.
SwanLab — открытый инструмент для записи обучения моделей, рассчитанный на
исследователей ИИ: визуализация обучения, автоматическое логирование, запись гиперпараметров, сравнение
экспериментов, совместная работа. В SwanLab исследователь по наглядным графикам находит проблемы обучения,
сравнивает эксперименты в поисках идей и, делясь онлайн-ссылками и обучая модели в рамках организации, ломает
барьеры в общении команды.
Зачем записывать обучение
По сравнению с разработкой программ обучение моделей больше похоже на экспериментальную науку. За каждой хорошей моделью — тысячи экспериментов. Исследователь постоянно пробует, записывает, сравнивает и копит опыт, чтобы найти лучшую архитектуру, гиперпараметры и пропорции данных. Насколько эффективно ведётся запись и сравнение — от этого сильно зависит скорость исследований.
Создаём SwanLabCallback
Заголовок раздела «Создаём SwanLabCallback»Советуем сначала зарегистрироваться на сайте SwanLab, а при инициализации обучения выбрать
(2) Use an existing SwanLab account и войти с личным (private) API Key.
import swanlabfrom swanlab.integration.transformers import SwanLabCallback
# Создаём SwanLabCallbackswanlab_callback = SwanLabCallback( project="Qwen3-Lora", # не забудьте поправить experiment_name="Qwen3-8B-LoRA-experiment" # не забудьте поправить)Обучение через Trainer
Заголовок раздела «Обучение через Trainer»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
Инференс с LoRA-весами
Заголовок раздела «Инференс с LoRA-весами»Получив любую контрольную точку (checkpoint), загружаем веса lora для инференса:
from transformers import AutoModelForCausalLM, AutoTokenizerimport torchfrom peft import PeftModel
mode_path = 'ПОМЕНЯЙ_МЕНЯ/Qwen/Qwen3-8B' # не забудьте поправитьlora_path = './output/Qwen3_8B_lora/checkpoint-699' # не забудьте поправить
# Загружаем токенизаторtokenizer = AutoTokenizer.from_pretrained(mode_path)
# Загружаем базовую модель Qwen3model = AutoModelForCausalLM.from_pretrained(mode_path, device_map="auto",torch_dtype=torch.bfloat16, trust_remote_code=True)
# Загружаем веса LoRAmodel = 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))Я Чжэнь Хуань, мой отец — Чжэнь Юаньдао, заместитель главы Далисы.(Ответ переведён: модель, обученная на оригинальном датасете, отвечает по-китайски.)
От переводчика: как запустить на своей машине
Заголовок раздела «От переводчика: как запустить на своей машине»Что сделать руками:
- Повторите рецепт на Qwen3-1.7B. Сравните ответы базовой и дообученной модели на 5 вопросах в стиле датасета.
- Поменяйте
r(4, 8, 32) иlora_alpha, сравните кривые лосса в SwanLab или TensorBoard — урок 6.2.2. - Соберите свой маленький датасет в том же формате (например, 200 вопросов и ответов по курсу на русском) и дообучите модель на нём. Проверьте, не «забыла» ли модель общие знания после дообучения.
Словарик
Заголовок раздела «Словарик»| Русский | Пояснение |
|---|---|
| обучение с учителем, SFT | дообучение на парах «указание — ответ» |
| низкоранговая адаптация (LoRA) | обучаем маленькие матрицы , |
| ранг (rank, r) | внутренняя размерность и |
| масштаб (lora_alpha) | множитель |
| накопление градиента | несколько микробатчей на шаг |
| пересчёт активаций | экономия памяти ценой времени |
| контрольная точка (checkpoint) | сохранённые веса адаптера |
| гибридная модель рассуждений | умеет отвечать с размышлением и без |
| запись и визуализация обучения | SwanLab, W&B, TensorBoard |
| отыгрыш роли (role-play) | модель говорит «в образе» |