Практикум · лекция 1

Своя модель: меняем манеру ответа

Запустим Qwen2.5-1.5B-Instruct, сохраним исходный ответ, дообучим LoRA на 512 готовых английских диалогах с Hugging Face и сравним результат на новых вопросах. Основной путь — NVIDIA, Python 3.11 и чистое ядро Jupyter.

Вопросы не содержат инструкции о стиле. Изменение манеры и правильность содержания оцениваются отдельно. Английские данные могут сдвинуть язык ответа даже на русский вопрос.

Учебный блокнот · Выполненный запуск · Блокнот переключения LoRA · Оба адаптера

Подготовим окружение

Установите Anaconda, если её ещё нет. Jupyter уже входит в дистрибутив. В Anaconda Prompt выполните команды ниже, затем откройте JupyterLab из Navigator и выберите Kernel → Change Kernel → LLM course.

Код шага
conda create -n llm-course python=3.11 pip ipykernel -y
conda activate llm-course
python -m ipykernel install --user --name llm-course --display-name "LLM course"

Conda создаёт отдельный Python. ipykernel регистрирует его в Jupyter. Интерфейс Jupyter может работать из base, ячейки — из llm-course.

1. Установим библиотеки

%pip устанавливает пакеты в Python текущего ядра. Первая команда берёт сборку PyTorch с CUDA 12.1 из официального репозитория. Вторая устанавливает согласованный набор библиотек курса. После установки перезапустите ядро через Kernel → Restart Kernel. Установка выполняется один раз для окружения.

Версии закреплены для повторения этого примера. После установки перезапустите ядро. В терминале замените %pip на python -m pip, предварительно активировав llm-course. Другие проекты остаются в своих окружениях.

Код шага
%pip install torch==2.5.1 --index-url https://download.pytorch.org/whl/cu121
%pip install transformers==4.57.1 datasets==4.8.3 peft==0.17.1 trl==0.22.2 accelerate==1.13.0 huggingface-hub==0.36.2 safetensors==0.7.0

2. Проверим видеокарту

torch.__version__ показывает сборку. cuda.is_available() проверяет, может ли этот Python использовать NVIDIA. assert останавливает основной путь с понятным сообщением, если CUDA недоступна. Номер 0 обозначает первую доступную видеокарту.

Код шага
# PyTorch выполняет вычисления модели и проверяет доступ к CUDA.
import torch

print("PyTorch:", torch.__version__)
assert torch.cuda.is_available(), "CUDA недоступна: см. раздел помощи"
print("Видеокарта:", torch.cuda.get_device_name(0))

3. Выберем модель

Слева от / — владелец репозитория, справа — его имя. Строка совпадает с концом адреса huggingface.co/Qwen/Qwen2.5-1.5B-Instruct. Для другой модели копируют её собственный идентификатор. Здесь пока выбирается только модель.

Код шага
# Идентификатор копируется из заголовка страницы на Hugging Face.
model_id = "Qwen/Qwen2.5-1.5B-Instruct"

4. Загрузим готовый генератор текста

text-generation — задача продолжения текста. model указывает репозиторий. device=0 размещает модель на первой NVIDIA. dtype="auto" использует формат весов из конфигурации; у выбранной Qwen это bfloat16, формат с двумя байтами на число. Внутренний алгоритм внимания и служебные токены выбирает библиотека.

Код шага
# pipeline объединяет модель, токенизатор и генерацию текста.
from transformers import pipeline

chat = pipeline(
    "text-generation", model=model_id, device=0, dtype="auto"
)

5. Посмотрим на вход модели

Токенизатор переводит строку в идентификаторы фрагментов текста. input_ids — список этих чисел, decode выполняет обратное преобразование. Числа обозначают элементы словаря, а не оценки смысла слов.

Код шага
# Токенизатор уже загружен вместе с моделью.
ids = chat.tokenizer("Hello, captain!")["input_ids"]
print(ids)
print(chat.tokenizer.decode(ids))

6. Зададим вопрос

role="user" обозначает пользователя, content содержит вопрос. Pipeline сам оформляет диалог для Qwen. max_new_tokens ограничивает продолжение 100 токенами, do_sample=False отключает случайную выборку. Из результата берём первый диалог и последнее сообщение — ответ помощника. Теперь ask("вопрос") вызывается одной строкой. before сохраняет исходный ответ.

Код шага
# Эта функция избавляет от повторения формата диалога.
def ask(question):
    reply = chat(
        [{"role": "user", "content": question}],
        max_new_tokens=100, do_sample=False
    )
    return reply[0]["generated_text"][-1]["content"]

question = "How can I organize my study time?"
before = ask(question)
print(before)

7. Возьмём готовые диалоги

load_dataset открывает набор как таблицу. train — имя раздела, а не команда начала обучения. В каждой строке messages — реплики. Фильтр оставляет короткие вопросы, select берёт первые 512 подходящих строк. Ответы не переписываются, стиль не добавляется вручную.

Источник: winglian/pirate-ultrachat-10k. Данные скачиваются из исходного репозитория; подготовка не переписывает ответы.

Код шага
from datasets import load_dataset

# В наборе уже есть вопросы и ответы в пиратском стиле.
data = load_dataset("winglian/pirate-ultrachat-10k", split="train")

# Возьмём 512 примеров с вопросами короче 300 символов.
data = data.filter(lambda row: len(row["messages"][0]["content"]) < 300)
data = data.select(range(512))
print(data[0])

8. Добавим обучаемую часть

PEFT — библиотека экономного дообучения. LoRA добавляет небольшие обучаемые матрицы к преобразованиям модели. CAUSAL_LM обозначает предсказание следующего токена по предыдущим. Стандартная конфигурация PEFT для Qwen выбирает поддерживаемые слои; базовые веса остаются замороженными.

Код шага
from peft import LoraConfig

# Для первого опыта используем стандартные настройки LoRA.
adapter = LoraConfig(task_type="CAUSAL_LM")

9. Зададим размер эксперимента

SFT — обучение на примерах желаемого поведения. Здесь: 400 обновлений; learning_rate=0.0003 задаёт масштаб изменения; по 2 диалога обрабатываются за проход, 4 прохода накапливаются перед обновлением. Это 8 примеров на шаг. max_length ограничивает диалог 384 токенами, длинный хвост обрезается. output_dir — папка результатов, report_to="none" отключает внешние сервисы журналирования. Остальные параметры берутся из выбранной версии TRL.

Код шага
from trl import SFTConfig, SFTTrainer

settings = SFTConfig(
    output_dir="pirate-output",
    max_steps=400, learning_rate=3e-4,
    per_device_train_batch_size=2, gradient_accumulation_steps=4,
    max_length=384,
    report_to="none"
)

10. Запустим дообучение

Передаём загруженную модель, её токенизатор, диалоги, LoRA и настройки. SFTTrainer готовит последовательности, собирает порции данных, вычисляет ошибку и обновляет параметры адаптера. train() запускает цикл. Прогресс появляется в выводе ячейки; loss — ошибка предсказания токенов, а не доля правильных ответов.

Код шага
trainer = SFTTrainer(
    model=chat.model, processing_class=chat.tokenizer,
    train_dataset=data, peft_config=adapter, args=settings
)
trainer.train()

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

В log_history есть измерения loss и итоговая статистика. Берём записи с loss и печатаем шаг и усреднённую ошибку. График в лекции строится по такому журналу реального запуска; учебная анимация показана отдельно.

Код шага
# Журнал доступен без подключения внешнего сервиса.
for row in trainer.state.log_history:
    if "loss" in row:
        print(row["step"], round(row["loss"], 3))

12. Повторим тот же вопрос

trainer.model содержит базу с адаптером. eval() включает режим использования. Вопрос и настройки генерации не меняются, инструкции «говори как пират» нет. Сравниваем сохранённый before с after.

Код шага
# Подключаем обученный адаптер к тому же генератору.
chat.model = trainer.model
chat.model.eval()

after = ask(question)
print("ДО:\n", before)
print("ПОСЛЕ:\n", after)

13. Проверим другой вопрос

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

Код шага
print(ask("Explain what a neural network is in two sentences."))
print(ask("Скажи, как лучше учиться программированию?"))

14. Сохраним результат

В pirate-adapter записываются веса LoRA, конфигурация и токенизатор. Базовая модель не копируется: при следующем запуске нужны и база, и адаптер. База уже находится в локальном кэше. Отдельный блокнот показывает загрузку в новом сеансе.

Код шага
trainer.save_model("pirate-adapter")
chat.tokenizer.save_pretrained("pirate-adapter")

15. Переключим сохранённую LoRA

load_adapter загружает вторую LoRA с именем long к той же базовой Qwen. set_adapter выбирает одну добавку: веса двух адаптеров здесь не складываются. default — имя первой LoRA, созданной SFTTrainer. eval() включает режим ответа. Архив длинного адаптера доступен в материалах, его папку нужно разместить рядом с блокнотом.

Дополнительный шаг: скачайте архив адаптеров и распакуйте папку pirate-adapter-long рядом с блокнотом. Вторая LoRA продолжала обучение после короткой: ещё 800 шагов при learning_rate=0.0001. Общая база та же. Это не повторное обучение во время занятия.

Код шага
# Сначала скачайте и распакуйте длинный адаптер рядом с блокнотом.
chat.model.load_adapter("pirate-adapter-long", adapter_name="long")
chat.model.set_adapter("long")
chat.model.eval()
print(ask(question))

# Вернём адаптер, обученный в этом сеансе.
chat.model.set_adapter("default")

Самостоятельный эксперимент

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

Сравните ответы до и после, опишите удачные и неудачные случаи. Использовать любые инструменты разрешено. Если опыт уже есть, предложите более содержательную проверку, чем повторение примера.

Пришлите через форму ссылку на GitHub или архив с кодом, версиями, описанием замысла, режима обучения и результатов. Форма будет предоставлена отдельно. Не включайте токены доступа.

Если запуск не удался

Сначала определите этап: окружение, установка пакета, загрузка или нехватка памяти. Подробные действия и отдельный CPU-путь — в помощи с запуском.