{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "a5520c44",
   "metadata": {},
   "source": [
    "# Как оценить результат обучения\n",
    "Лекция 02 · Новый самостоятельный блокнот\n",
    "\n",
    "Начинаем с пустого ядра. Переменные и ячейки прошлого занятия не нужны: заново загрузим модель и сохранённый адаптер. Обучать его повторно не будем.\n",
    "\n",
    "**На занятии создаём свой блокнот:** создайте папку `lecture02`, откройте её в Jupyter, создайте Python-блокнот и сохраните как `evaluation.ipynb`. Из `lecture02-adapter.zip` положите папку `pirate-adapter` прямо рядом с ним. Внутри нужны `adapter_config.json` и `adapter_model.safetensors`: это результат уже выполненного обучения. До первого запуска другие файлы не нужны.\n",
    "\n",
    "Выберите подготовленное окружение курса: Python 3.11, torch 2.5.1+cu121, transformers 4.57.1, peft 0.17.1, sentencepiece 0.2.1, CUDA. Если создаёте окружение заново, установите зависимости до запуска ячеек. Для другого оборудования окружение и устройство выбираются отдельно.\n",
    "\n",
    "Этот полный блокнот — справочный материал со всеми ячейками по порядку. На лекции переносим код со слайдов в свой новый блокнот. Qwen (около 3 ГБ) и, когда он понадобится, переводчик (около 307 МБ) скачиваются кодом в кэш Hugging Face. Позже, на шаге с контрольными вопросами, сохраните `cases.json` рядом с вашим блокнотом. `03_evaluate.executed.ipynb` содержит авторский запуск."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "77efa451",
   "metadata": {},
   "source": [
    "**Как читать сообщения Jupyter.** Полосы загрузки файлов и строка об используемом устройстве — обычный ход работы. Предупреждения библиотек могут отображаться на красном фоне, хотя ячейка успешно завершилась; ниже разберём конкретные сообщения там, где они возникают. Traceback с исключением означает остановку выполнения — это уже ошибка, которую нужно устранить перед продолжением.\n",
    "\n",
    "**Задача:** выяснить, что изменилось после обучения, предложить одно исправление и проверить границы вывода. Критерии: содержание, выполнение инструкции, язык и уместность стиля."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3107d013",
   "metadata": {},
   "source": [
    "## 1. Рабочее место готово\n",
    "`tokenizer` превращает текст в токены и обратно. `base` — исходная Qwen. `model` — та же основа с пиратским адаптером после 400 шагов. `eval()` включает режим использования модели."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "1ba2d33e",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "# Выбираем обычную HTTP-загрузку файлов Hugging Face без ускорителя Xet.\n",
    "# Это нужно задать до импорта transformers; на веса и ответы настройка не влияет.\n",
    "os.environ[\"HF_HUB_DISABLE_XET\"] = \"1\"\n",
    "\n",
    "import torch\n",
    "# Auto-классы выбирают подходящую архитектуру по имени и конфигурации модели.\n",
    "from transformers import AutoTokenizer, AutoModelForCausalLM\n",
    "# PEFT подключает небольшую обученную добавку LoRA к исходной модели.\n",
    "from peft import PeftModel\n",
    "\n",
    "model_id = \"Qwen/Qwen2.5-1.5B-Instruct\"\n",
    "# Загружаем правила преобразования текста в токены; файлы попадут в кэш.\n",
    "tokenizer = AutoTokenizer.from_pretrained(model_id)\n",
    "# CausalLM — модель, которая продолжает текст, предсказывая следующий токен.\n",
    "# from_pretrained загружает готовые веса; bfloat16 уменьшает расход памяти.\n",
    "base = AutoModelForCausalLM.from_pretrained(\n",
    "    model_id, dtype=torch.bfloat16\n",
    ").to(\"cuda\")  # Переносим модель на видеокарту.\n",
    "\n",
    "# Берём LoRA из папки рядом с нашим новым блокнотом.\n",
    "# eval() включает режим предсказаний, отключая учебные слои вроде Dropout.\n",
    "model = PeftModel.from_pretrained(base, \"pirate-adapter\").eval()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "33f8230c",
   "metadata": {},
   "source": [
    "## 2. Получим один ответ\n",
    "Общая функция `ask` подготавливает диалог и генерирует строку. В опытах будем вызывать `ask_before` для исходной Qwen и `ask_after` для Qwen с обученным адаптером. Обе используют одинаковые настройки: без случайного выбора, до 160 новых токенов. Необязательный `system` позволяет передать инструкцию, не меняя веса."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "776288d3",
   "metadata": {},
   "outputs": [],
   "source": [
    "# На входе запрос и необязательная инструкция; на выходе — строка ответа.\n",
    "def ask(prompt, system=None):\n",
    "    messages = [{\"role\": \"user\", \"content\": prompt}]\n",
    "    if system:\n",
    "        # Если инструкция передана, ставим её перед сообщением пользователя.\n",
    "        messages.insert(0, {\"role\": \"system\", \"content\": system})\n",
    "    # Оформляем диалог по правилам Qwen и превращаем его в тензоры токенов.\n",
    "    # add_generation_prompt отмечает место, с которого отвечает ассистент.\n",
    "    inputs = tokenizer.apply_chat_template(\n",
    "        messages, tokenize=True, add_generation_prompt=True,\n",
    "        return_dict=True, return_tensors=\"pt\"\n",
    "    ).to(\"cuda\")  # Вход и модель должны находиться на одной видеокарте.\n",
    "    # Не строим граф градиентов для обучения: при генерации он не нужен.\n",
    "    # inference_mode экономит память; веса здесь не обучаются.\n",
    "    with torch.inference_mode():\n",
    "        # Генерируем до 160 новых токенов, без случайного выбора (sampling).\n",
    "        # pad_token_id задаёт токен заполнения; у Qwen берём токен конца текста.\n",
    "        output = model.generate(**inputs, max_new_tokens=160,\n",
    "            do_sample=False, pad_token_id=tokenizer.eos_token_id)\n",
    "    # generate вернул запрос вместе с продолжением: отрезаем токены запроса.\n",
    "    # decode превращает оставшиеся токены в текст без служебных меток.\n",
    "    return tokenizer.decode(output[0, inputs[\"input_ids\"].shape[-1]:],\n",
    "                            skip_special_tokens=True)\n",
    "\n",
    "# Два понятных вызова для всех следующих опытов.\n",
    "def ask_before(prompt, system=None):\n",
    "    # Временно отключаем LoRA: ответ исходной, не дообученной нами Qwen.\n",
    "    with model.disable_adapter():\n",
    "        return ask(prompt, system=system)\n",
    "\n",
    "def ask_after(prompt, system=None):\n",
    "    # Здесь LoRA активна: ответ модели после нашего обучения.\n",
    "    return ask(prompt, system=system)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "48c11446",
   "metadata": {},
   "source": [
    "При первом **вызове** ask может появиться: `The following generation flags are not valid and may be ignored: ['temperature', 'top_p', 'top_k']`. В конфигурации Qwen сохранены параметры случайного выбора, а мы задали `do_sample=False`. Поэтому они не участвуют в этом запуске. Это предупреждение, а не сбой: если ниже появился ответ и нет Traceback, ячейка выполнена. Само объявление функции ещё не генерирует ответ."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2a73561f",
   "metadata": {},
   "source": [
    "Вопрос по-русски: «При нормальном атмосферном давлении при какой температуре замерзает чистая вода по шкале Цельсия? Одно короткое предложение». Ожидаем 0 °C. Ограничение на ответ проверим отдельно."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "83481391",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "The following generation flags are not valid and may be ignored: ['temperature', 'top_p', 'top_k']. Set `TRANSFORMERS_VERBOSITY=info` for more details.\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Ahoy there, matey! Pure water freezes at 0 degrees Celsius, that's colder than a kraken's eye! \n",
      "\n"
     ]
    }
   ],
   "source": [
    "question = (\"At normal atmospheric pressure, at what temperature does pure water \"\n",
    "            \"freeze in degrees Celsius? Give one short sentence.\")\n",
    "answer = ask_after(question)  # Спрашиваем модель ПОСЛЕ обучения, с нашей LoRA.\n",
    "print(answer)  # Показываем сохранённый ответ; print сам модель не запускает."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d7abef82",
   "metadata": {},
   "source": [
    "## 3. Английский ответ неудобно обсуждать\n",
    "Может ли уже загруженная исходная модель перевести его? Проверим через `ask_before`. Внутри этой функции `disable_adapter()` временно отключает LoRA. После вызова адаптер снова включается; вторую копию весов не загружаем.\n",
    "\n",
    "Запрос: «Переведи на русский. Верни только перевод». Проверим число 0, условие про давление и образные выражения. Не предполагаем заранее ни успех, ни неудачу."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "023eddf5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Привет, моряк! Вода при замерзании достигает температуры -18°C, что холоднее глаза крокодила!\n"
     ]
    }
   ],
   "source": [
    "# Перевод поручаем исходной Qwen ДО нашего обучения — это ask_before.\n",
    "qwen_translation = ask_before(\n",
    "    \"Translate into Russian. Return only the translation:\\n\" + answer\n",
    ")\n",
    "print(qwen_translation)  # Печатаем перевод; исходная строка answer сохранена."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b123030b",
   "metadata": {},
   "source": [
    "## 4. Готовая модель именно для перевода\n",
    "Нам нужен небольшой локальный инструмент EN → RU. На Hugging Face выбираем задачу **Translation**, проверяем языки и пример вызова. Один кандидат — [Helsinki-NLP/opus-mt-en-ru](https://huggingface.co/Helsinki-NLP/opus-mt-en-ru): английский → русский, около 307 МБ весов. Это удобный кандидат для нашей задачи, не победитель универсального рейтинга.\n",
    "\n",
    "Файлы заранее не готовим: `from_pretrained` скачает их по имени репозитория и сохранит в кэш. `revision` закрепляет конкретную версию файлов. Для нашего PyTorch 2.5 выбрана версия с `model.safetensors`; веса из основной ветки в другом формате требуют более новой версии PyTorch. Это свойство выбранного репозитория, а не универсальная настройка всех моделей.\n",
    "\n",
    "`AutoModelForSeq2SeqLM` загружает модель преобразования входной последовательности в выходную: здесь английского текста в русский. После загрузки `pipeline` объединяет модель и токенизатор в один удобный вызов. Для другой модели сначала смотрим её карточку: направление языков, подходящий класс и пример запуска."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "8eddc97e",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "C:\\Users\\danii\\anaconda3\\envs\\torchcu121\\Lib\\site-packages\\transformers\\models\\marian\\tokenization_marian.py:175: UserWarning: Recommended: pip install sacremoses.\n",
      "  warnings.warn(\"Recommended: pip install sacremoses.\")\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Device set to use cuda:0\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Эй, приятель, чистая вода замерзает на 0 градусов по Цельсию, это холоднее, чем глаз кракена!\n"
     ]
    }
   ],
   "source": [
    "from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, pipeline\n",
    "\n",
    "# Имя публичного репозитория на Hugging Face, а не путь к папке на компьютере.\n",
    "translator_id = \"Helsinki-NLP/opus-mt-en-ru\"\n",
    "# Фиксируем версию с весами safetensors, совместимую с окружением курса.\n",
    "translator_revision = \"3ff4dbd98515ba253f08f183e0de0b7ccf189ee3\"\n",
    "\n",
    "# Скачиваем токенизатор; повторный запуск использует уже сохранённый кэш.\n",
    "translator_tokenizer = AutoTokenizer.from_pretrained(\n",
    "    translator_id, revision=translator_revision\n",
    ")\n",
    "# Seq2SeqLM преобразует входной текст в выходной; здесь это перевод EN → RU.\n",
    "translator_model = AutoModelForSeq2SeqLM.from_pretrained(\n",
    "    translator_id, revision=translator_revision, use_safetensors=True\n",
    ")\n",
    "# Объединяем токенизатор и модель; device=0 выбирает первую видеокарту.\n",
    "translator = pipeline(\"translation\", model=translator_model,\n",
    "                      tokenizer=translator_tokenizer, device=0)\n",
    "# pipeline возвращает список результатов; берём перевод первой строки.\n",
    "print(translator(answer)[0][\"translation_text\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d826e1b8",
   "metadata": {},
   "source": [
    "**Что появляется при загрузке.** Полосы `Downloading` показывают скачивание в кэш. `Device set to use cuda:0` сообщает, что выбрана первая видеокарта. `Recommended: pip install sacremoses` — рекомендация необязательного нормализатора текста: в проверенном окружении перевод работает и без него. При необходимости пакет устанавливается отдельной ячейкой `%pip install sacremoses`.\n",
    "\n",
    "На Windows возможно предупреждение о недоступных `symlinks`: кэш использует обычные копии файлов и может занять больше места; это не ошибка перевода. Если отсутствует виджет прогресса (`IProgress not found`), загрузка может показывать текстовый индикатор. Ошибки соединения, отсутствующего SentencePiece или Traceback — отдельные проблемы, их нельзя игнорировать. SentencePiece входит в подготовленное окружение курса."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d105db4",
   "metadata": {},
   "source": [
    "## 5. Оригинал и перевод в двух полях\n",
    "`SimpleNamespace` собирает именованные поля: `.en` хранит вход, `.ru` — перевод. Вход T — короткий английский текст. Числа, JSON и уже русский ответ переводить не нужно."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "0495d644",
   "metadata": {},
   "outputs": [],
   "source": [
    "from types import SimpleNamespace  # Простой объект с полями, доступными через точку.\n",
    "\n",
    "def T(text):\n",
    "    ru = translator(text)[0][\"translation_text\"]  # Запускаем перевод, берём текст.\n",
    "    return SimpleNamespace(en=text, ru=ru)  # Возвращаем исходную строку и перевод."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "e368ae08",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Ahoy there, matey! Pure water freezes at 0 degrees Celsius, that's colder than a kraken's eye! \n",
      "\n",
      "Эй, приятель, чистая вода замерзает на 0 градусов по Цельсию, это холоднее, чем глаз кракена!\n"
     ]
    }
   ],
   "source": [
    "result = T(answer)  # Один раз переводим сохранённый ответ.\n",
    "print(result.en)  # Печатаем исходную английскую строку.\n",
    "print(result.ru)  # Печатаем перевод из того же результата, без нового вызова модели."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0bfdfe67",
   "metadata": {},
   "source": [
    "Перевод помогает читать. Содержание, формат, язык и стиль проверяем по оригиналу: перевод может изменить манеру или потерять деталь. На ответе про воду отдельно оцените факт и просьбу об одном предложении. Инструмент готов — возвращаемся к оценке."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3b624d50",
   "metadata": {},
   "source": [
    "## 6. Несколько разных требований\n",
    "Теперь нам нужен набор контрольных вопросов. Скачайте `cases.json` по ссылке на слайде 19 и сохраните прямо рядом с вашим `evaluation.ipynb`. В файле есть оригиналы, русские подписи и критерии. Возьмём три разработческих вопроса: арифметика, длина списка и совет по-русски. Это маленькая демонстрация метода, не общая оценка модели. В дополнительных данных есть прежний полный авторский прогон 24 вопросов.\n",
    "\n",
    "На точных задачах нужны именно `42` и `3`, без приветствия. Русский совет придётся читать: его полезность не проверяется сравнением с одной строкой."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "5a0498b2",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "D06 Сколько будет 17 + 25? Верни только число. Ожидаем: 42\n",
      "D07 Чему в Python равно len([10, 20, 30])? Верни только число. Ожидаем: 3\n",
      "D09 Дай два коротких совета, как начать учиться программированию. Ответь по-русски. Ожидаем: Два применимых совета начинающему.\n"
     ]
    }
   ],
   "source": [
    "import json\n",
    "from pathlib import Path\n",
    "\n",
    "# Читаем файл рядом с блокнотом и превращаем JSON-текст в Python-список.\n",
    "cases = json.loads(Path(\"cases.json\").read_text(encoding=\"utf-8\"))\n",
    "# Выбираем три разработческих вопроса по их идентификаторам.\n",
    "dev = [q for q in cases if q[\"id\"] in [\"D06\", \"D07\", \"D09\"]]\n",
    "for q in dev:\n",
    "    # Показываем понятный запрос и критерий до запуска модели.\n",
    "    print(q[\"id\"], q[\"prompt_ru\"], \"Ожидаем:\", q[\"reference\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9ac1877a",
   "metadata": {},
   "source": [
    "## 7. Сравним с исходной версией\n",
    "`ask_before` и `ask_after` вызывают две версии модели. `before` и `after` — словари, в которых сохраняем ответы: например, `after[\"D07\"]` возвращает уже полученную строку и не запускает модель. Запрос и настройки одинаковы; меняется только адаптер. Сохраняем каждую строку, включая неудачные ответы."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "ae2e17f0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "D06 — обе версии готовы\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "D07 — обе версии готовы\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "D09 — обе версии готовы\n"
     ]
    }
   ],
   "source": [
    "# Словари ответов: before — ДО обучения, after — ПОСЛЕ обучения.\n",
    "before, after = {}, {}\n",
    "for q in dev:\n",
    "    before[q[\"id\"]] = ask_before(q[\"prompt\"])  # Исходная Qwen, без нашей LoRA.\n",
    "    after[q[\"id\"]] = ask_after(q[\"prompt\"])  # Та же Qwen с обученной LoRA.\n",
    "    print(q[\"id\"], \"— обе версии готовы\")  # Одна пара ответов на тот же запрос."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dd49c947",
   "metadata": {},
   "source": [
    "D07 — длина списка из трёх элементов. Для чтения переведём английские объяснения. Верное значение и лишние слова проверяем в `.en`. Если ответ состоит из одной цифры, читаем её напрямую."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "05705a15",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Исходная модель: The function `len()` in Python returns the length of an object, which is the number of items it contains. For a list like `[10, 20, 30]`, the length is 3.\n",
      "\n",
      "So, `len([10, 20, 30])` will return `3`. \n",
      "Перевод: Функция \"len()\" в Python возвращает длину объекта, т.е. количество элементов, которые она содержит. Для списка типа \"[10, 20, 30]\" длина равна 3. Таким образом, \"len([10, 20, 30]\" возвратит `3'.\n",
      "С адаптером: Ahoy there, matey! Shiver me timbers, ye be askin' about Python's `len()` function, eh? Well, shiver me timbers, it's like asking for the length of a treasure chest full of gold coins - just count 'em up and tell me how many coins we got!\n",
      "\n",
      "So, back to yer question: `len([10, 20, 30])`. That's a list of three numbers, like a pirate ship with three cannons. The `len()` function counts them up, and lo and behold, it returns **3**! \n",
      "\n",
      "Now, if you want to know how much loot we've got, just call `len()` on any iterable thing, like a string or even a dictionary filled with treasures! \n",
      "Перевод: Ну, давай же, приятель! Заткнись и расскажи мне, сколько у нас есть монет! Итак, вернемся к твоему вопросу: \"len(10, 20, 30)\" - это список из трёх чисел, как пиратский корабль с тремя пушками. Функция \"len()\" подсчитывает их, и вот, он возвращает **3**! Если ты хочешь знать, сколько у нас денег, просто назови \"len() любой подходящей вещью, как струна или даже словарь, наполненный сокровищами!\"\n"
     ]
    }
   ],
   "source": [
    "a = T(before[\"D07\"])  # Переводим ответ исходной модели на вопрос D07.\n",
    "b = T(after[\"D07\"])  # Переводим ответ с адаптером на тот же вопрос.\n",
    "# Показываем и оригиналы, и переводы; \\n переводит вывод на новую строку.\n",
    "print(\"Исходная модель:\", a.en, \"\\nПеревод:\", a.ru)\n",
    "print(\"С адаптером:\", b.en, \"\\nПеревод:\", b.ru)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "188501e1",
   "metadata": {},
   "source": [
    "## 8. Автоматизируем однозначное требование\n",
    "`.strip()` убирает крайние пробелы. Полное совпадение требует и верного числа, и отсутствия лишнего текста. При несовпадении читаем ответ и определяем причину.\n",
    "\n",
    "Знаменатель **2**, а не 3: русский совет этим правилом не оценивается. Полученная доля — не качество всей модели."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "03d00c68",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Исходная 1 / 2 точных ответов\n",
      "С адаптером 0 / 2 точных ответов\n",
      "Совет по-русски — читаем отдельно:\n",
      "Алло, братан! Вот тебе два коротеньких советика:\n",
      "\n",
      "1. Начни с простого: не бойся начинать с базовых знаний - это как плавание в море - нужно научиться держаться на поверхности.\n",
      "\n",
      "2. Используй интернет: есть масса бесплатных и платных ресурсов для обучения, от YouTube до онлайн-курсов. Не забывай о практике - каждый день пиши код!\n",
      "\n",
      "Надеюсь, эти советики помогут тебе стать программистом! А если хочешь больше информации, звони мне, я всегда готов помочь. Алло! \n",
      "\n"
     ]
    }
   ],
   "source": [
    "expected = {\"D06\": \"42\", \"D07\": \"3\"}\n",
    "for name, answers in [(\"Исходная\", before), (\"С адаптером\", after)]:\n",
    "    # strip убирает крайние пробелы; сравнение проверяет всю строку.\n",
    "    # sum считает совпадения: True даёт 1, False — 0.\n",
    "    passed = sum(answers[key].strip() == value for key, value in expected.items())\n",
    "    # len(expected) — число задач, к которым применимо наше правило.\n",
    "    print(name, passed, \"/\", len(expected), \"точных ответов\")\n",
    "\n",
    "# Свободный русский ответ этим правилом не оценён — выводим его для чтения.\n",
    "print(\"Совет по-русски — читаем отдельно:\")\n",
    "print(after[\"D09\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dfb699e7",
   "metadata": {},
   "source": [
    "## 9. Одно исправление\n",
    "Если приветствие появляется даже при просьбе «только число», уточним приоритеты системной инструкцией. Веса не меняем. Гипотеза: нарушений строгого формата станет меньше. Факты проверяем отдельно.\n",
    "\n",
    "Перевод инструкции: «Точно выполни просьбу, используй язык запроса, сохраняй фактическую точность. Лёгкая пиратская речь допустима, если не мешает формату. Если просят только число или JSON, верни только их, без приветствия и другого текста».\n",
    "\n",
    "В учебном протоколе инструкция заранее фиксирована. Не редактируйте её после просмотра отложенных ответов."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "5e37b4c9",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Единая системная инструкция для следующего опыта; русский смысл — выше.\n",
    "format_instruction = (\"Follow the user request exactly. Use the language of the user request. \"\n",
    "       \"Keep factual claims accurate. Use light pirate wording only when it does not \"\n",
    "       \"conflict with the requested format. If only a number or JSON is requested, \"\n",
    "       \"return only that number or JSON, with no greeting or extra text.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "ee495c57",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "D06 Сколько будет 17 + 25? Верни только число.\n",
      "После обучения: Ahoy there, matey! Shiver me timbers, that's a sum as tricky as a kraken in a barrel of rum! But fear not, I'll be here to help ye solve this puzzle, even if it be a bit more complicated than a parrot on a ship's mast!\n",
      "\n",
      "So, let's add these two numbers:\n",
      "\n",
      "**17 + 25 = ?**\n",
      "\n",
      "Now, imagine you've got a treasure chest full of gold doubloons, and you're counting them one by one. \n",
      "\n",
      "First, count those 17 gold doubloons... (counting) ...and then add another 25 doubloons... (counting again)\n",
      "\n",
      "And voila! The total comes out to **42**, which is just like finding a buried\n",
      "После обучения + инструкция: 42\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "D07 Чему в Python равно len([10, 20, 30])? Верни только число.\n",
      "После обучения: Ahoy there, matey! Shiver me timbers, ye be askin' about Python's `len()` function, eh? Well, shiver me timbers, it's like asking for the length of a treasure chest full of gold coins - just count 'em up and tell me how many coins we got!\n",
      "\n",
      "So, back to yer question: `len([10, 20, 30])`. That's a list of three numbers, like a pirate ship with three cannons. The `len()` function counts them up, and lo and behold, it returns **3**! \n",
      "\n",
      "Now, if you want to know how much loot we've got, just call `len()` on any iterable thing, like a string or even a dictionary filled with treasures!\n",
      "После обучения + инструкция: 5\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "D09 Дай два коротких совета, как начать учиться программированию. Ответь по-русски.\n",
      "После обучения: Алло, братан! Вот тебе два коротеньких советика:\n",
      "\n",
      "1. Начни с простого: не бойся начинать с базовых знаний - это как плавание в море - нужно научиться держаться на поверхности.\n",
      "\n",
      "2. Используй интернет: есть масса бесплатных и платных ресурсов для обучения, от YouTube до онлайн-курсов. Не забывай о практике - каждый день пиши код!\n",
      "\n",
      "Надеюсь, эти советики помогут тебе стать программистом! А если хочешь больше информации, звони мне, я всегда готов помочь. Алло! \n",
      "\n",
      "После обучения + инструкция: Конечно, вот два коротких совета:\n",
      "\n",
      "1. Найдите хорошего преподавателя или онлайн-курс, который будет поддерживать вас и помогать вам развиваться.\n",
      "2. Практикуйтесь регулярно, делая простые проекты каждый день. Это поможет вам лучше понять материал и улучшить свои навыки.\n"
     ]
    }
   ],
   "source": [
    "# Снова спрашиваем модель ПОСЛЕ обучения. Меняется только system-инструкция.\n",
    "# Прежние ответы этой же модели уже сохранены в after.\n",
    "after_with_instruction = {}\n",
    "for q in dev:\n",
    "    after_with_instruction[q[\"id\"]] = ask_after(\n",
    "        q[\"prompt\"], system=format_instruction\n",
    "    )\n",
    "    # Печатаем два ответа рядом: видим и улучшения, и новые ошибки.\n",
    "    print(q[\"id\"], q[\"prompt_ru\"])\n",
    "    print(\"После обучения:\", after[q[\"id\"]])\n",
    "    print(\"После обучения + инструкция:\", after_with_instruction[q[\"id\"]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "fb9d1101",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "После обучения 0 / 2 точных ответов\n",
      "После обучения + инструкция 1 / 2 точных ответов\n"
     ]
    }
   ],
   "source": [
    "# Тот же критерий: целиком правильное число, без лишнего текста.\n",
    "for name, answers in [(\"После обучения\", after),\n",
    "                      (\"После обучения + инструкция\", after_with_instruction)]:\n",
    "    # expected содержит эталоны D06 → 42 и D07 → 3.\n",
    "    passed = sum(answers[key].strip() == value for key, value in expected.items())\n",
    "    print(name, passed, \"/\", len(expected), \"точных ответов\")\n",
    "\n",
    "# Счётчик не объясняет каждую ошибку: прочитайте пары ответов выше.\n",
    "# В D07 сравните само число и наличие лишних слов — это разные свойства."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d4cf9f84",
   "metadata": {},
   "source": [
    "## 10. Перенеслось ли изменение?\n",
    "На знакомых вопросах мы сравнили `after` с `after_with_instruction`: одну обученную модель без и с нашей дополнительной инструкцией. Теперь проверим ту же `format_instruction` на других задачах: умножении, длине строки и объяснении словаря по-русски. Не подбираем её текст по этим ответам.\n",
    "\n",
    "Снова вызываем только `ask_after`. В `test_after` сохраняем ответы без нашей дополнительной инструкции, в `test_after_with_instruction` — с ней. После просмотра для следующего независимого опыта понадобится новый набор.\n",
    "\n",
    "Это воспроизведение подготовленного опыта. Вопросы уже известны автору курса. Для собственной работы заранее составьте другие вопросы и закройте их до фиксации решения."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "c2f0e402",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "T06 Сколько будет 18 умножить на 4? Верни только число.\n",
      "После обучения: Ahoy there, matey! 18 times 4 be 72, shiver me timbers! \n",
      "\n",
      "После обучения + инструкция: 72\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "T07 Чему в Python равно len('cat')? Верни только число.\n",
      "После обучения: Ahoy there, matey!  You want to know how many letters be in 'cat'? Shiver me timbers, that's easy as a kraken's eye!\n",
      "\n",
      "```python\n",
      "len('cat')\n",
      "```\n",
      "\n",
      "Behold the answer: **3**! \n",
      "\n",
      "Aye, it's just like counting your gold doubloons - one, two, three! \n",
      "\n",
      "После обучения + инструкция: 5\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "T09 Объясни по-русски, что такое словарь в Python. Два коротких предложения.\n",
      "После обучения: Алло, братан! Это слово \"словарь\" в Python - это как книжка с названиями и объяснениями. А то, что ты хочешь сказать? \n",
      "\n",
      "После обучения + инструкция: В Python словарь - это структура данных, которая позволяет хранить пары ключа и значения. Это удобно для работы с множеством информации, где нужно быстро найти нужную информацию. Например, можно использовать словарь для хранения слов и их переводов или для хранения данных о пользователе, таких как имя, возраст и любимые цвета.\n"
     ]
    }
   ],
   "source": [
    "# Теперь берём другие вопросы: умножение, длину строки и объяснение словаря.\n",
    "heldout = [q for q in cases if q[\"id\"] in [\"T06\", \"T07\", \"T09\"]]\n",
    "test_after, test_after_with_instruction = {}, {}\n",
    "for q in heldout:\n",
    "    # Два вызова ask_after: обе версии ПОСЛЕ обучения, меняем только инструкцию.\n",
    "    test_after[q[\"id\"]] = ask_after(q[\"prompt\"])\n",
    "    test_after_with_instruction[q[\"id\"]] = ask_after(\n",
    "        q[\"prompt\"], system=format_instruction\n",
    "    )\n",
    "    print(q[\"id\"], q[\"prompt_ru\"])  # Показываем запрос и обе исходные строки.\n",
    "    print(\"После обучения:\", test_after[q[\"id\"]])\n",
    "    print(\"После обучения + инструкция:\", test_after_with_instruction[q[\"id\"]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "b0d65d58",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "После обучения 0 / 2 точных ответов\n",
      "После обучения + инструкция 1 / 2 точных ответов\n"
     ]
    }
   ],
   "source": [
    "test_expected = {\"T06\": \"72\", \"T07\": \"3\"}\n",
    "for name, answers in [(\"После обучения\", test_after),\n",
    "                      (\"После обучения + инструкция\", test_after_with_instruction)]:\n",
    "    # Считаем полные совпадения, как на разработческой части, но с новыми эталонами.\n",
    "    passed = sum(answers[key].strip() == value for key, value in test_expected.items())\n",
    "    print(name, passed, \"/\", len(test_expected), \"точных ответов\")  # Явный знаменатель."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d980d6e9",
   "metadata": {},
   "source": [
    "## 11. Сохраним свидетельства\n",
    "Запишем оригиналы и две попытки перевода первого ответа. Файл сохраняется локально рядом с блокнотом."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "c8a3a3a2",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Сохранено: practice-results.json\n"
     ]
    }
   ],
   "source": [
    "results = {\"first\": {\"prompt\": question, \"answer\": answer,\n",
    "                     \"qwen_ru\": qwen_translation, \"dedicated_ru\": result.ru},\n",
    "           \"before\": before, \"after\": after,\n",
    "           \"after_with_instruction\": after_with_instruction,\n",
    "           \"test_after\": test_after,\n",
    "           \"test_after_with_instruction\": test_after_with_instruction,\n",
    "           \"instruction\": format_instruction}\n",
    "# dumps превращает результаты в JSON, сохраняя кириллицу и добавляя отступы.\n",
    "# write_text записывает этот текст в файл рядом с нашим блокнотом.\n",
    "Path(\"practice-results.json\").write_text(\n",
    "    json.dumps(results, ensure_ascii=False, indent=2), encoding=\"utf-8\"\n",
    ")\n",
    "print(\"Сохранено: practice-results.json\")  # Сообщаем имя созданного файла."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a8a19387",
   "metadata": {},
   "source": [
    "### Ваш вывод\n",
    "1. Для задачи … мы проверяли …, сравнивая … при одинаковых … .\n",
    "2. На разработческих вопросах … из …; на отложенных … из … .\n",
    "3. Улучшение видно в …; оставшаяся ошибка видна в … .\n",
    "4. Пока нельзя утверждать …, потому что … .\n",
    "\n",
    "Отдельно оцените полезность русских ответов и уместность стиля. Если свойство не оценено, так и напишите. Для своей работы подготовьте сценарий, вопросы и критерии до новых запусков."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (LLM course)",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
