{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "519f7a47",
      "metadata": {
        "id": "519f7a47"
      },
      "source": [
        "# Часть I. Теория\n",
        "\n",
        "## 1. Что такое языковая модель и зачем нам GPT\n",
        "\n",
        "Языковая модель — это программа, которая умеет вычислять вероятность следующего слова при заданном контексте.  \n",
        "Например:  \n",
        "`\"кошка села на ...\"` — модель говорит, что `\"стул\"` более вероятно, чем `\"метеорит\"`.\n",
        "\n",
        "GPT — **авторегрессионная** языковая модель: она порождает текст по одному токену, каждый раз подставляя дополненный текст с только что сгенерированным словом еще раз обратно в качестве входных данных.\n",
        "\n",
        "---\n",
        "\n",
        "## 2. Краткая история нейросетей для обработки текстов на ЕЯ\n",
        "\n",
        "### 2.1. Рекуррентные сети (RNN, LSTM, GRU)\n",
        "\n",
        "Сначала тексты обрабатывались рекуррентными сетями (RNN).  \n",
        "Сеть читала последовательность слово за словом, обновляя «скрытое состояние», которое хранило память о предыдущих словах.\n",
        "\n",
        "**Проблемы:**\n",
        "- Очень медленно (нельзя обрабатывать слова параллельно, потому что каждое состояние зависит от предыдущего).\n",
        "- Трудно запоминать связи между дальними словами (проблема долгой зависимости).\n",
        "\n",
        "### 2.2. Seq2Seq и механизм внимания\n",
        "\n",
        "Для задач машинного перевода придумали конструкцию **кодер‑декодер**:\n",
        "- Кодер (обычно RNN) превращает всё исходное предложение в один вектор.\n",
        "- Декодер (тоже RNN) по этому вектору порождает перевод.\n",
        "\n",
        "Затем к этому добавили **механизм внимания (Bahdanau, 2014)** — декодер при каждом шаге «подсматривал» сразу на все скрытые состояния кодера, а не только на конечный вектор. Это резко улучшило качество перевода.\n",
        "\n",
        "### 2.3. Архитектура трансформер (Vaswani et al., 2017)\n",
        "\n",
        "Революционная статья **“Attention Is All You Need”** предложила вообще отказаться от рекуррентности и использовать **только внимание**.  \n",
        "Такой подход:\n",
        "- Обрабатывает все токены параллельно.\n",
        "- Легко захватывает дальние связи.\n",
        "- Хорошо масштабируется (можно обучать на огромных датасетах).\n",
        "\n",
        "Исходный трансформер состоял из двух половин: **кодер** (превращает входной текст в представление) и **декодер** (генерирует перевод, пользуясь этим представлением).\n",
        "\n",
        "### 2.4. GPT — Generative pre‑trained transformer\n",
        "\n",
        "Создатели GPT (Radford et al., 2018) взяли **только декодерную часть** и обучили её на предсказание следующего слова в огромном корпусе текстов без разметки.  \n",
        "\n",
        "Почему только декодер?\n",
        "Потому что для генерации текста нам не нужно внешнее представление от кодера — модель учится предсказывать следующее слово, глядя только на предыдущие. Так появился **GPT‑1**, затем **GPT‑2**, **GPT‑3** и современные версии. Архитектурно они почти не изменились: выросли размеры, улучшились данные, но основа — всё тот же авторегрессионный трансформер‑декодер.\n",
        "\n",
        "---\n",
        "\n",
        "## 3. Как текст попадает на вход нейросети\n",
        "\n",
        "Для машины текст представляется последовательностью чисел. Процесс превращения предложения в понятный сети вид состоит из трёх шагов.\n",
        "\n",
        "### 3.1. Токенизация\n",
        "\n",
        "Текст разбивается на **токены** — минимальные смысловые кусочки. Это могут быть:\n",
        "- Целые слова (например, `\"где\"`),\n",
        "- Части слов (например, `\"игр\" + \"ать\"`),\n",
        "- Знаки препинания,\n",
        "- Специальные символы.\n",
        "\n",
        "Для каждого токена в словаре модели есть фиксированный **целочисленный индекс**.\n",
        "\n",
        "Пример:\n",
        "```\n",
        "Текст:   \"кот сидит\"\n",
        "Токены:  [\"кот\", \"сидит\"]\n",
        "Индексы: [1205, 3807]\n",
        "```\n",
        "(индексы зависят от словаря; обычно нейросеть добавляет токен «конец строки» или специальные маркеры.)\n",
        "\n",
        "### 3.2. Эмбеддинги (векторы слов)\n",
        "\n",
        "Каждый индекс превращается в вектор чисел длиной, например, 512 (параметр `d_model`). Для этого используется обучаемая таблица эмбеддингов — большая матрица `(размер_словаря × d_model)`.  \n",
        "Индекс `1205` даёт одну строчку этой матрицы — вектор из 512 чисел, который и будет подан на вход сети.\n",
        "\n",
        "### 3.3. Позиционные эмбеддинги\n",
        "\n",
        "Сеть не знает, в каком порядке идут слова, потому что все токены обрабатываются параллельно. Поэтому к векторам слов **добавляют** ещё векторы, кодирующие позицию (первое, второе, … слово).  \n",
        "Обычно это также обучаемая таблица размером `(максимальная_длина × d_model)`.  \n",
        "Затем векторы слов и позиций складываются поэлементно.\n",
        "\n",
        "Итак, после трёх шагов мы имеем тензор формы `(batch, длина_последовательности, d_model)`, и он идёт в стопку декодер‑блоков.\n",
        "\n",
        "---\n",
        "\n",
        "## 4. Механизм внимания или ядро GPT\n",
        "\n",
        "Внимание помогает каждому слову «взглянуть» на все актуальные предыдущие слова и решить, какие из них сейчас важны.\n",
        "\n",
        "### 4.1. Идея запрос‑ключ‑значение\n",
        "\n",
        "У нас есть последовательность из T токенов, каждый представлен вектором размера `d_model`.  \n",
        "Для **каждой** позиции мы вычисляем три вектора (проекции):\n",
        "- **Запрос (query, Q)** — «что я ищу?».\n",
        "- **Ключ (key, K)** — «что я могу предложить?».\n",
        "- **Значение (value, V)** — «моя содержательная информация».\n",
        "\n",
        "Эти векторы получаются умножением исходного вектора на три обучаемые матрицы.\n",
        "\n",
        "### 4.2. Применение\n",
        "\n",
        "Внимание одного токена `i` к токену `j` вычисляется так:\n",
        "1. Скалярное произведение `Q_i · K_j` - насколько запрос «подходит» к ключу.\n",
        "2. Деление на `√d_k` (где `d_k` = `d_model / n_heads`), чтобы градиенты не взрывались.\n",
        "3. Применяется softmax по строке — получаем веса (вероятности), с которыми `i` смотрит на все `j`.\n",
        "4. Выход для токена `i` = сумма `V_j`, взвешенных полученным вниманием.\n",
        "\n",
        "**Матрично для всех токенов сразу:**\n",
        "```\n",
        "Attention(Q, K, V) = softmax( (Q·Kᵀ) / √d_k ) · V\n",
        "```\n",
        "\n",
        "### 4.3. Каузальная (причинная) маска\n",
        "\n",
        "GPT должен предсказывать следующее слово, глядя на предыдущие, и не подглядывать в будущее. Поэтому здесь мы запрещаем токену `i` видеть токены `j > i`.  \n",
        "Делается это добавлением **минус бесконечности** (`-∞`) в матрицу коэффициентов для запрещённых позиций до softmax. Тогда после softmax эти веса становятся ≈0, и токен «не видит» будущие слова.\n",
        "\n",
        "Маска в простейшем виде — верхнетреугольная матрица из нулей (запрещено) и единиц (разрешено), которая накладывается на `attn_scores`.\n",
        "\n",
        "### 4.4. Многоголовое внимание\n",
        "\n",
        "Чтобы модель могла улавливать разные виды связей (синтаксические, смысловые, позиционные), делается несколько «голов» внимания параллельно.  \n",
        "Каждая голова — независимые линейные проекции в своё подпространство размером `d_k`. Результаты всех голов склеиваются обратно и проходят через ещё один линейный слой.\n",
        "\n",
        "Таким образом, вектор каждого токена после блока внимания содержит информацию обо всех разрешённых контекстных связях.\n",
        "\n",
        "---\n",
        "\n",
        "## 5. Устройство одного блока декодера и как слои идут по очереди\n",
        "\n",
        "GPT целиком состоит из **N одинаковых блоков**, поставленных друг за другом. Каждый блок содержит два подслоя:\n",
        "\n",
        "1. **Masked Multi‑Head Self‑Attention** (с остаточной связью и LayerNorm).\n",
        "2. **Feed Forward Network** (полносвязная сеть с нелинейностью, также с остаточной связью и LayerNorm).\n",
        "\n",
        "Остаточная связь (`x = x + sublayer(x)`) помогает градиентам не затухать, а LayerNorm стабилизирует обучение.\n",
        "\n",
        "**Поток данных через один блок:**\n",
        "```\n",
        "Вход: тензор x формы (B, T, d_model)\n",
        "\n",
        "┌───────────────────┐\n",
        "│ LayerNorm         │\n",
        "├───────────────────┤\n",
        "│ Masked Multi‑Head │\n",
        "│ Self‑Attention    │──── + (x)  ──► x1\n",
        "└───────────────────┘\n",
        "\n",
        "┌───────────────────┐\n",
        "│ LayerNorm         │\n",
        "├───────────────────┤\n",
        "│ Feed‑Forward      │──── + (x1) ──► Выход\n",
        "└───────────────────┘\n",
        "```\n",
        "\n",
        "И так **N раз подряд**: выход блока 1 — вход блока 2, и так далее.\n",
        "\n",
        "После последнего блока снова применяется LayerNorm (финальная нормализация), а затем линейный слой `(d_model → vocab_size)`, который превращает финальные векторы в вероятности слов.\n",
        "\n",
        "---\n",
        "\n",
        "## 6. Иллюстрация прохождения данных\n",
        "\n",
        "```text\n",
        "Текст: \"кот сидит на\"\n",
        "    ↓ Tokenizer\n",
        "[1205, 3807, 1681]\n",
        "    ↓ Embedding\n",
        "   (1, 3, 512)          ← сложили эмбеддинги токенов + позиционные\n",
        "    ↓\n",
        "┌───────────────────────┐\n",
        "│   Decoder Block 1     │\n",
        "│   (Attention + FFN)   │\n",
        "└─────────┬─────────────┘\n",
        "          ↓\n",
        "┌───────────────────────┐\n",
        "│   Decoder Block 2     │\n",
        "│   ...                 │\n",
        "└─────────┬─────────────┘\n",
        "          ↓\n",
        "        ...\n",
        "          ↓\n",
        "┌───────────────────────┐\n",
        "│   Decoder Block N     │\n",
        "└─────────┬─────────────┘\n",
        "          ↓\n",
        "    LayerNorm (финальный)\n",
        "          ↓\n",
        "    Linear(512 → 50000)    (vocab_size)\n",
        "          ↓\n",
        "    softmax → вероятности\n",
        "          ↓\n",
        "   предсказание: \"стуле\" (индекс 9842)\n",
        "```\n",
        "\n",
        "Теперь, вооружённые теорией, перейдём к практике.\n",
        "\n",
        "---\n",
        "\n",
        "# Часть II. Практическая реализация на PyTorch\n",
        "\n",
        "Мы напишем учебную модель MiniGPT — очень похожую на настоящие, но с минимальными зависимостями. Для примеров будем считать:\n",
        "\n",
        "- `vocab_size = 10000` (словарь)\n",
        "- `d_model = 256`\n",
        "- `n_heads = 8`\n",
        "- `d_ff = 1024` (внутренняя размерность FFN)\n",
        "- `n_layers = 6`\n",
        "- `max_len = 512`\n",
        "\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "id": "5c83f88e",
      "metadata": {
        "id": "5c83f88e"
      },
      "outputs": [],
      "source": [
        "import torch\n",
        "import torch.nn as nn\n",
        "import torch.nn.functional as F\n",
        "\n",
        "#1 - Подготовка эмбеддингов и маски\n",
        "\n",
        "class Embeddings(nn.Module):\n",
        "    def __init__(self, vocab_size, d_model, max_len):\n",
        "        super().__init__()\n",
        "        self.tok_emb = nn.Embedding(vocab_size, d_model)   # (V, d_model)\n",
        "        self.pos_emb = nn.Embedding(max_len, d_model)      # (max_len, d_model)\n",
        "\n",
        "    def forward(self, x):  # x: (batch, seq_len)\n",
        "        seq_len = x.size(1)\n",
        "        positions = torch.arange(0, seq_len, device=x.device).unsqueeze(0)  # (1, seq_len)\n",
        "        tok = self.tok_emb(x)          # (B, T, d_model)\n",
        "        pos = self.pos_emb(positions)  # (1, T, d_model)\n",
        "        return tok + pos\n",
        "\n",
        "def generate_causal_mask(seq_len):\n",
        "    \"\"\" Возвращает маску (1, 1, seq_len, seq_len), где 1 – разрешено, 0 – запрещено \"\"\"\n",
        "    mask = torch.tril(torch.ones(seq_len, seq_len)).view(1, 1, seq_len, seq_len)\n",
        "    return mask\n",
        "\n",
        "#2 - Многоголовое причинное внимание\n",
        "\n",
        "class CausalMultiHeadAttention(nn.Module):\n",
        "    def __init__(self, d_model, n_heads):\n",
        "        super().__init__()\n",
        "        assert d_model % n_heads == 0\n",
        "        self.d_model = d_model\n",
        "        self.n_heads = n_heads\n",
        "        self.d_k = d_model // n_heads\n",
        "\n",
        "        # Линейные слои для Q, K, V и выходной проекции\n",
        "        self.q_proj = nn.Linear(d_model, d_model)\n",
        "        self.k_proj = nn.Linear(d_model, d_model)\n",
        "        self.v_proj = nn.Linear(d_model, d_model)\n",
        "        self.out_proj = nn.Linear(d_model, d_model)\n",
        "\n",
        "    def forward(self, x, mask=None):\n",
        "        B, T, C = x.shape  # C = d_model\n",
        "\n",
        "        # Проекции и разделение на головы\n",
        "        Q = self.q_proj(x).view(B, T, self.n_heads, self.d_k).transpose(1, 2)  # (B, n_heads, T, d_k)\n",
        "        K = self.k_proj(x).view(B, T, self.n_heads, self.d_k).transpose(1, 2)\n",
        "        V = self.v_proj(x).view(B, T, self.n_heads, self.d_k).transpose(1, 2)\n",
        "\n",
        "        # 1. Scaled dot-product\n",
        "        attn_scores = (Q @ K.transpose(-2, -1)) * (self.d_k ** -0.5)  # (B, n_heads, T, T)\n",
        "\n",
        "        # 2. Каузальная маска: запрещаем смотреть в будущее\n",
        "        if mask is not None:\n",
        "            attn_scores = attn_scores.masked_fill(mask == 0, float('-inf'))\n",
        "\n",
        "        # 3. Softmax и взвешенная сумма\n",
        "        attn_weights = F.softmax(attn_scores, dim=-1)  # (B, n_heads, T, T)\n",
        "        attn_output = attn_weights @ V                  # (B, n_heads, T, d_k)\n",
        "\n",
        "        # 4. Объединяем головы и пропускаем через линейный слой\n",
        "        attn_output = attn_output.transpose(1, 2).contiguous().view(B, T, C)\n",
        "        return self.out_proj(attn_output)\n",
        "\n",
        "#3 - Feed‑Forward сеть (FFN)\n",
        "\n",
        "class FeedForward(nn.Module):\n",
        "    def __init__(self, d_model, d_ff):\n",
        "        super().__init__()\n",
        "        self.linear1 = nn.Linear(d_model, d_ff)\n",
        "        self.linear2 = nn.Linear(d_ff, d_model)\n",
        "\n",
        "    def forward(self, x):\n",
        "        return self.linear2(F.gelu(self.linear1(x)))\n",
        "\n",
        "#4 - Один блок декодера\n",
        "\n",
        "class DecoderBlock(nn.Module):\n",
        "    def __init__(self, d_model, n_heads, d_ff):\n",
        "        super().__init__()\n",
        "        self.ln1 = nn.LayerNorm(d_model)\n",
        "        self.attn = CausalMultiHeadAttention(d_model, n_heads)\n",
        "        self.ln2 = nn.LayerNorm(d_model)\n",
        "        self.ff = FeedForward(d_model, d_ff)\n",
        "\n",
        "    def forward(self, x, mask=None):\n",
        "        # Attention + Add & Norm\n",
        "        x = x + self.attn(self.ln1(x), mask)\n",
        "        # FFN + Add & Norm\n",
        "        x = x + self.ff(self.ln2(x))\n",
        "        return x\n",
        "\n",
        "#5 - Собираем MiniGPT\n",
        "\n",
        "class MiniGPT(nn.Module):\n",
        "    def __init__(self, vocab_size, d_model, n_heads, d_ff, n_layers, max_len):\n",
        "        super().__init__()\n",
        "        self.embed = Embeddings(vocab_size, d_model, max_len)\n",
        "        self.layers = nn.ModuleList([\n",
        "            DecoderBlock(d_model, n_heads, d_ff) for _ in range(n_layers)\n",
        "        ])\n",
        "        self.ln_final = nn.LayerNorm(d_model)\n",
        "        self.head = nn.Linear(d_model, vocab_size)\n",
        "\n",
        "    def forward(self, x):\n",
        "        # Создаём маску для текущей длины последовательности\n",
        "        mask = generate_causal_mask(x.size(1)).to(x.device)\n",
        "        # Эмбеддинги\n",
        "        x = self.embed(x)                     # (B, T, d_model)\n",
        "        # Передаём через все N блоков\n",
        "        for layer in self.layers:\n",
        "            x = layer(x, mask)\n",
        "        # Финальная нормализация и проекция на словарь\n",
        "        x = self.ln_final(x)\n",
        "        logits = self.head(x)                 # (B, T, vocab_size)\n",
        "        return logits\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "67cbccdc",
      "metadata": {
        "id": "67cbccdc"
      },
      "source": [
        "## 6. Пример использования (инференс, генерация)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "id": "cfa59821",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "cfa59821",
        "outputId": "3aa58bc3-f649-414f-f184-53517031d336"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "[1205, 3807, 1681, 5406, 9861, 7730, 4418, 2747, 3245, 1315, 662, 3760, 5327]\n"
          ]
        }
      ],
      "source": [
        "def generate_text(model, prompt_tokens, max_new_tokens, max_len, device):\n",
        "    model.eval()\n",
        "    tokens = torch.tensor([prompt_tokens], device=device)  # (1, T)\n",
        "\n",
        "    for _ in range(max_new_tokens):\n",
        "        # Обрезаем, если больше max_len\n",
        "        inp = tokens[:, -max_len:]                         # (1, current_len)\n",
        "        with torch.no_grad():\n",
        "            logits = model(inp)                            # (1, current_len, vocab_size)\n",
        "            # Берём предсказание только для последнего токена\n",
        "            next_logits = logits[:, -1, :]                 # (1, vocab_size)\n",
        "            probs = F.softmax(next_logits, dim=-1)\n",
        "            next_token = torch.multinomial(probs, num_samples=1)  # сэмплируем\n",
        "\n",
        "        tokens = torch.cat([tokens, next_token], dim=1)    # добавляем\n",
        "\n",
        "    return tokens[0].tolist()\n",
        "\n",
        "#Пример вызова с условным словарём (индексы):\n",
        "\n",
        "# Предположим, у нас есть токенизатор:\n",
        "prompt_indices = [1205, 3807, 1681]  # \"кот сидит на\"\n",
        "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
        "model = MiniGPT(vocab_size=10000, d_model=256, n_heads=8, d_ff=1024, n_layers=6, max_len=512).to(device)\n",
        "model.eval()\n",
        "\n",
        "generated = generate_text(model, prompt_indices, max_new_tokens=10, max_len=512, device=device)\n",
        "# Полученный список индексов можно декодировать обратно в текст (реализовано далее)\n",
        "print(generated)"
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "# Создадим простой маппинг для демонстрации восстановления текста\n",
        "# В реальной задаче это был бы словарь вашего токенизатора\n",
        "itos = {\n",
        "    1205: \"кот\",\n",
        "    3807: \"сидит\",\n",
        "    1681: \"на\",\n",
        "    5406: \"коврике\",\n",
        "    9861: \"и\",\n",
        "    7730: \"смотрит\",\n",
        "    4418: \"в\",\n",
        "    2747: \"окно\",\n",
        "    3245: \"когда\",\n",
        "    1315: \"солнце\",\n",
        "    662: \"ярко\",\n",
        "    3760: \"светит\",\n",
        "    5327: \".\"\n",
        "}\n",
        "\n",
        "# generated - это список индексов из предыдущей ячейки\n",
        "decoded_text = \" \".join([itos.get(idx, f\"[{idx}]\") for idx in generated])\n",
        "\n",
        "print(f\"Входные индексы: {prompt_indices}\")\n",
        "print(f\"Сгенерированные индексы: {generated}\")\n",
        "print(f\"\\nВосстановленный текст:\\n{decoded_text}\")"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "D7LzaWxQ-Syq",
        "outputId": "06ccab02-e385-455d-937c-e9dedc05c598"
      },
      "id": "D7LzaWxQ-Syq",
      "execution_count": 3,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Входные индексы: [1205, 3807, 1681]\n",
            "Сгенерированные индексы: [1205, 3807, 1681, 5406, 9861, 7730, 4418, 2747, 3245, 1315, 662, 3760, 5327]\n",
            "\n",
            "Восстановленный текст:\n",
            "кот сидит на коврике и смотрит в окно когда солнце ярко светит .\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "id": "64e9fc84",
      "metadata": {
        "id": "64e9fc84"
      },
      "source": [
        "## 7. Краткое резюме того, как слои идут по очереди\n",
        "\n",
        "1. Входные индексы → эмбеддинги токенов + позиционные эмбеддинги.\n",
        "2. **Первый блок декодера:**  \n",
        "   LayerNorm → Causal Multi‑Head Attention → остаточное сложение → LayerNorm → FFN → остаточное сложение.\n",
        "3. Выход первого блока подаётся на **второй блок** (той же структуры).\n",
        "4. И так N раз.\n",
        "5. Финальный LayerNorm.\n",
        "6. Линейный слой → `vocab_size` — предсказание вероятностей для каждого токена последовательности.\n",
        "\n",
        "Именно так внутри устроен GPT."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "02b57e80",
      "metadata": {
        "id": "02b57e80"
      },
      "source": []
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "70b9f5f7",
      "metadata": {
        "id": "70b9f5f7"
      },
      "outputs": [],
      "source": []
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.11.4"
    },
    "colab": {
      "provenance": []
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}