{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "323bda35",
   "metadata": {},
   "source": [
    "# Своя модель: меняем манеру ответа\n",
    "\n",
    "Основной путь: NVIDIA, Python 3.11, Qwen2.5-1.5B-Instruct и 512 готовых диалогов. Проходим ячейки по порядку в чистом ядре. Установка выполняется один раз, после неё перезапускаем ядро. Контрольный вопрос не содержит инструкции о стиле."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "955a774b",
   "metadata": {},
   "source": [
    "## Установим библиотеки\n",
    "\n",
    "%pip устанавливает пакеты в Python текущего ядра. Первая команда берёт сборку PyTorch с CUDA 12.1 из официального репозитория. Вторая устанавливает согласованный набор библиотек курса. После установки перезапустите ядро через Kernel → Restart Kernel. Установка выполняется один раз для окружения."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a81799ff",
   "metadata": {},
   "outputs": [],
   "source": [
    "%pip install torch==2.5.1 --index-url https://download.pytorch.org/whl/cu121\n",
    "%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"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8151ff3a",
   "metadata": {},
   "source": [
    "## Проверим видеокарту\n",
    "\n",
    "torch.__version__ показывает сборку. cuda.is_available() проверяет, может ли этот Python использовать NVIDIA. assert останавливает основной путь с понятным сообщением, если CUDA недоступна. Номер 0 обозначает первую доступную видеокарту."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ce4ac810",
   "metadata": {},
   "outputs": [],
   "source": [
    "# PyTorch выполняет вычисления модели и проверяет доступ к CUDA.\n",
    "import torch\n",
    "\n",
    "print(\"PyTorch:\", torch.__version__)\n",
    "assert torch.cuda.is_available(), \"CUDA недоступна: см. раздел помощи\"\n",
    "print(\"Видеокарта:\", torch.cuda.get_device_name(0))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a259330a",
   "metadata": {},
   "source": [
    "## Выберем модель\n",
    "\n",
    "Слева от / — владелец репозитория, справа — его имя. Строка совпадает с концом адреса huggingface.co/Qwen/Qwen2.5-1.5B-Instruct. Для другой модели копируют её собственный идентификатор. Здесь пока выбирается только модель."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "091e96e9",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Идентификатор копируется из заголовка страницы на Hugging Face.\n",
    "model_id = \"Qwen/Qwen2.5-1.5B-Instruct\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "398edef5",
   "metadata": {},
   "source": [
    "## Загрузим готовый генератор текста\n",
    "\n",
    "text-generation — задача продолжения текста. model указывает репозиторий. device=0 размещает модель на первой NVIDIA. dtype=\"auto\" использует формат весов из конфигурации; у выбранной Qwen это bfloat16, формат с двумя байтами на число. Внутренний алгоритм внимания и служебные токены выбирает библиотека."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "66803bf6",
   "metadata": {},
   "outputs": [],
   "source": [
    "# pipeline объединяет модель, токенизатор и генерацию текста.\n",
    "from transformers import pipeline\n",
    "\n",
    "chat = pipeline(\n",
    "    \"text-generation\", model=model_id, device=0, dtype=\"auto\"\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ed325799",
   "metadata": {},
   "source": [
    "## Посмотрим на вход модели\n",
    "\n",
    "Токенизатор переводит строку в идентификаторы фрагментов текста. input_ids — список этих чисел, decode выполняет обратное преобразование. Числа обозначают элементы словаря, а не оценки смысла слов."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "266ff209",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Токенизатор уже загружен вместе с моделью.\n",
    "ids = chat.tokenizer(\"Hello, captain!\")[\"input_ids\"]\n",
    "print(ids)\n",
    "print(chat.tokenizer.decode(ids))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1a442dd7",
   "metadata": {},
   "source": [
    "## Зададим вопрос\n",
    "\n",
    "role=\"user\" обозначает пользователя, content содержит вопрос. Pipeline сам оформляет диалог для Qwen. max_new_tokens ограничивает продолжение 100 токенами, do_sample=False отключает случайную выборку. Из результата берём первый диалог и последнее сообщение — ответ помощника. Теперь ask(\"вопрос\") вызывается одной строкой. before сохраняет исходный ответ."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9da61595",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Эта функция избавляет от повторения формата диалога.\n",
    "def ask(question):\n",
    "    reply = chat(\n",
    "        [{\"role\": \"user\", \"content\": question}],\n",
    "        max_new_tokens=100, do_sample=False\n",
    "    )\n",
    "    return reply[0][\"generated_text\"][-1][\"content\"]\n",
    "\n",
    "question = \"How can I organize my study time?\"\n",
    "before = ask(question)\n",
    "print(before)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "158a74fe",
   "metadata": {},
   "source": [
    "## Возьмём готовые диалоги\n",
    "\n",
    "load_dataset открывает набор как таблицу. train — имя раздела, а не команда начала обучения. В каждой строке messages — реплики. Фильтр оставляет короткие вопросы, select берёт первые 512 подходящих строк. Ответы не переписываются, стиль не добавляется вручную."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "05e0bf0d",
   "metadata": {},
   "outputs": [],
   "source": [
    "from datasets import load_dataset\n",
    "\n",
    "# В наборе уже есть вопросы и ответы в пиратском стиле.\n",
    "data = load_dataset(\"winglian/pirate-ultrachat-10k\", split=\"train\")\n",
    "\n",
    "# Возьмём 512 примеров с вопросами короче 300 символов.\n",
    "data = data.filter(lambda row: len(row[\"messages\"][0][\"content\"]) < 300)\n",
    "data = data.select(range(512))\n",
    "print(data[0])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3df4b11b",
   "metadata": {},
   "source": [
    "## Добавим обучаемую часть\n",
    "\n",
    "PEFT — библиотека экономного дообучения. LoRA добавляет небольшие обучаемые матрицы к преобразованиям модели. CAUSAL_LM обозначает предсказание следующего токена по предыдущим. Стандартная конфигурация PEFT для Qwen выбирает поддерживаемые слои; базовые веса остаются замороженными."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a5913e09",
   "metadata": {},
   "outputs": [],
   "source": [
    "from peft import LoraConfig\n",
    "\n",
    "# Для первого опыта используем стандартные настройки LoRA.\n",
    "adapter = LoraConfig(task_type=\"CAUSAL_LM\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0cf4503d",
   "metadata": {},
   "source": [
    "## Зададим размер эксперимента\n",
    "\n",
    "SFT — обучение на примерах желаемого поведения. Здесь: 400 обновлений; learning_rate=0.0003 задаёт масштаб изменения; по 2 диалога обрабатываются за проход, 4 прохода накапливаются перед обновлением. Это 8 примеров на шаг. max_length ограничивает диалог 384 токенами, длинный хвост обрезается. output_dir — папка результатов, report_to=\"none\" отключает внешние сервисы журналирования. Остальные параметры берутся из выбранной версии TRL."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f6348465",
   "metadata": {},
   "outputs": [],
   "source": [
    "from trl import SFTConfig, SFTTrainer\n",
    "\n",
    "settings = SFTConfig(\n",
    "    output_dir=\"pirate-output\",\n",
    "    max_steps=400, learning_rate=3e-4,\n",
    "    per_device_train_batch_size=2, gradient_accumulation_steps=4,\n",
    "    max_length=384,\n",
    "    report_to=\"none\"\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8fe2300a",
   "metadata": {},
   "source": [
    "## Запустим дообучение\n",
    "\n",
    "Передаём загруженную модель, её токенизатор, диалоги, LoRA и настройки. SFTTrainer готовит последовательности, собирает порции данных, вычисляет ошибку и обновляет параметры адаптера. train() запускает цикл. Прогресс появляется в выводе ячейки; loss — ошибка предсказания токенов, а не доля правильных ответов."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e7b21d94",
   "metadata": {},
   "outputs": [],
   "source": [
    "trainer = SFTTrainer(\n",
    "    model=chat.model, processing_class=chat.tokenizer,\n",
    "    train_dataset=data, peft_config=adapter, args=settings\n",
    ")\n",
    "trainer.train()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ac55bedb",
   "metadata": {},
   "source": [
    "## Посмотрим на ход обучения\n",
    "\n",
    "В log_history есть измерения loss и итоговая статистика. Берём записи с loss и печатаем шаг и усреднённую ошибку. График в лекции строится по такому журналу реального запуска; учебная анимация показана отдельно."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0eb49cef",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Журнал доступен без подключения внешнего сервиса.\n",
    "for row in trainer.state.log_history:\n",
    "    if \"loss\" in row:\n",
    "        print(row[\"step\"], round(row[\"loss\"], 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6e21f06b",
   "metadata": {},
   "source": [
    "## Повторим тот же вопрос\n",
    "\n",
    "trainer.model содержит базу с адаптером. eval() включает режим использования. Вопрос и настройки генерации не меняются, инструкции «говори как пират» нет. Сравниваем сохранённый before с after."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "89e9ef55",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Подключаем обученный адаптер к тому же генератору.\n",
    "chat.model = trainer.model\n",
    "chat.model.eval()\n",
    "\n",
    "after = ask(question)\n",
    "print(\"ДО:\\n\", before)\n",
    "print(\"ПОСЛЕ:\\n\", after)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3f7b686a",
   "metadata": {},
   "source": [
    "## Проверим другой вопрос\n",
    "\n",
    "Первый вопрос проверяет перенос стиля на другую тему, второй — на русский язык. Английский набор не гарантирует такой перенос. Смотреть нужно и на стиль, и на смысл: пиратское приветствие само по себе не делает ответ хорошим."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "daf7b305",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(ask(\"Explain what a neural network is in two sentences.\"))\n",
    "print(ask(\"Скажи, как лучше учиться программированию?\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0703ac33",
   "metadata": {},
   "source": [
    "## Сохраним результат\n",
    "\n",
    "В pirate-adapter записываются веса LoRA, конфигурация и токенизатор. Базовая модель не копируется: при следующем запуске нужны и база, и адаптер. База уже находится в локальном кэше. Отдельный блокнот показывает загрузку в новом сеансе."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8345df95",
   "metadata": {},
   "outputs": [],
   "source": [
    "trainer.save_model(\"pirate-adapter\")\n",
    "chat.tokenizer.save_pretrained(\"pirate-adapter\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "LLM · CUDA (torchcu121)",
   "language": "python",
   "name": "llm-course-2026"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
