diff --git a/notebooks/bpe.ipynb b/notebooks/bpe.ipynb index c999a41..d17a2ee 100644 --- a/notebooks/bpe.ipynb +++ b/notebooks/bpe.ipynb @@ -98,21 +98,10 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "e8c52a53", "metadata": {}, - "outputs": [ - { - "ename": "", - "evalue": "", - "output_type": "error", - "traceback": [ - "\u001b[1;31mRunning cells with '.venv (Python 3.10.9)' requires the ipykernel package.\n", - "\u001b[1;31mInstall 'ipykernel' into the Python environment. \n", - "\u001b[1;31mCommand: '/Users/sergey/Projects/ML/llm-arch-research/.venv/bin/python -m pip install ipykernel -U --force-reinstall'" - ] - } - ], + "outputs": [], "source": [ "import dill\n", "\n", @@ -269,7 +258,15 @@ "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.10.9" } }, diff --git a/notebooks/gpt_analysis.ipynb b/notebooks/gpt_analysis.ipynb index 03aba54..36c7dc2 100644 --- a/notebooks/gpt_analysis.ipynb +++ b/notebooks/gpt_analysis.ipynb @@ -5,120 +5,22 @@ "id": "6842e799", "metadata": {}, "source": [ - "\n", - "# Архитектура GPT-1: Принципы работы и ключевые компоненты\n", "\n", "Модель **GPT-1 (Generative Pretrained Transformer)** — это первая реализация идеи создания языковой модели на основе архитектуры **Transformer Decoder**, предложенной в работе *“Improving Language Understanding by Generative Pre-Training”* (OpenAI, 2018).\n", - "Она заложила фундамент всех последующих поколений GPT-моделей, показав, что модель, обученная на большом корпусе текстов в режиме **предсказания следующего токена**, способна эффективно адаптироваться к различным задачам обработки естественного языка.\n", - "\n", - "---\n", - "\n", - "## Основная архитектура\n", - "\n", - "![](https://ucarecdn.com/4ce51ba3-83fc-46c3-a6e8-efa064663df0/)\n", - "\n", - "Модель GPT-1 представляет собой **каскад из 12 идентичных слоев декодера трансформера**. Каждый слой обрабатывает входную последовательность токенов, улучшая их представление на каждом этапе.\n", - "Основная идея заключается в том, что модель учится предсказывать следующий токен в тексте, имея контекст всех предыдущих.\n", - "\n", - "Рассмотрим основные компоненты модели подробнее.\n", - "\n", - "---\n", - "\n", - "### 1. Векторные представления (Эмбеддинги)\n", - "\n", - "Перед тем как текст поступает в трансформер, он преобразуется в числовую форму — **векторные представления**.\n", - "\n", - "* **Эмбеддинги токенов (Token Embeddings)**\n", - " Каждый токен (слово, подслово или символ) преобразуется в вектор фиксированной размерности. Эти векторы формируются в процессе обучения модели и кодируют семантическое значение токенов — токены с похожим смыслом имеют близкие векторы в пространстве.\n", - "\n", - "* **Позиционные эмбеддинги (Positional Embeddings)**\n", - " Поскольку архитектура трансформера не учитывает порядок элементов последовательности (в отличие от RNN), в GPT добавляются позиционные эмбеддинги.\n", - " Они вводят информацию о позиции каждого токена в предложении, позволяя модели различать, например, «кот съел рыбу» и «рыба съела кота».\n", - "\n", - "---\n", - "\n", - "### 2. Блоки декодера трансформера\n", - "\n", - "Каждый слой модели GPT состоит из двух ключевых компонентов:\n", - "\n", - "#### a. Маскированное многоголовое внимание (Masked Multi-Head Attention)\n", - "\n", - "Механизм **внимания (attention)** позволяет модели определять, какие части предыдущего контекста наиболее важны для текущего токена.\n", - "В GPT используется **маскированное внимание**, что означает, что токен на позиции *i* может \"смотреть\" только на токены, стоящие перед ним (позиции ≤ *i*).\n", - "Это обеспечивает **каузальность** — свойство, благодаря которому модель не «знает будущее», что важно для генерации текста слева направо.\n", - "\n", - "Многоголовое внимание (multi-head attention) разбивает входные векторы на несколько подпространств (голов), каждая из которых учится улавливать разные типы зависимостей — синтаксические, семантические и др.\n", - "Результаты всех голов объединяются и проецируются обратно в исходное пространство признаков.\n", - "\n", - "#### b. Полносвязная сеть (Feed-Forward Network, FFN)\n", - "\n", - "После внимания каждый токен независимо проходит через небольшую двухслойную нейронную сеть с функцией активации (в GPT-1 используется ReLU).\n", - "Эта сеть увеличивает нелинейность модели и помогает ей лучше представлять сложные зависимости в данных.\n", - "\n", - "#### c. Остаточные связи и нормализация (Residual Connections + Layer Normalization)\n", - "\n", - "Чтобы стабилизировать обучение, выход каждого подблока (attention и FFN) складывается с его входом (residual connection), а затем нормализуется (LayerNorm).\n", - "Остаточные связи помогают избежать исчезновения градиентов, а нормализация ускоряет сходимость при обучении.\n", - "\n", - "---\n", - "\n", - "### 3. Выходной слой\n", - "\n", - "После прохождения всех блоков декодера итоговое представление токенов передается в **линейный слой**, который проецирует его в пространство размерности словаря.\n", - "Результатом являются **логиты** — сырые оценки вероятностей появления каждого токена из словаря.\n", - "\n", - "Далее применяется функция **Softmax**, которая преобразует логиты в вероятностное распределение.\n", - "Наиболее вероятный токен выбирается как следующий элемент последовательности.\n", - "\n", - "---\n", - "\n", - "## Токенизация и авторегрессия\n", - "\n", - "![image-2.png](attachment\\:image-2.png)\n", - "\n", - "### Токенизация\n", - "\n", - "**Токенизатор** — это отдельный компонент, преобразующий текст в последовательность токенов (целых чисел).\n", - "Он делит текст на минимальные осмысленные единицы (например, слова, подслова или символы) и сопоставляет каждой единице уникальный идентификатор.\n", - "\n", - "Пример:\n", - "\n", - "```\n", - "Текст: \"Привет, мир!\"\n", - "Токены: [15496, 11, 995]\n", - "```\n", - "\n", - "Модель GPT работает именно с этой числовой последовательностью.\n", - "На выходе модель также производит последовательность токенов, которые затем декодируются обратно в текст с помощью того же токенизатора.\n", - "\n", - "---\n", - "\n", - "### Авторегрессия\n", - "\n", - "GPT — **авторегрессионная модель**, то есть она предсказывает следующий токен, используя уже сгенерированные.\n", - "Процесс генерации происходит пошагово:\n", - "\n", - "1. Модели подается начальная последовательность (например, «Once upon a time»).\n", - "2. Модель вычисляет распределение вероятностей следующего токена и выбирает наиболее вероятный.\n", - "3. Новый токен добавляется в конец входной последовательности.\n", - "4. Процесс повторяется, пока не будет достигнут нужный размер текста или не сработает условие остановки.\n", - "\n", - "Таким образом, каждый новый токен порождается с учетом **всего контекста** — от начала до текущего шага.\n", - "Этот принцип обеспечивает связность и контекстуальную осмысленность текста.\n", - "\n", - "---\n", - "\n", - "## Заключение\n", - "\n", - "GPT-1 продемонстрировала, что **предобучение на больших объемах неразмеченных данных** с последующим **тонким дообучением** на конкретной задаче может дать отличные результаты в обработке естественного языка.\n", - "Несмотря на то, что модель GPT-1 сравнительно мала по современным меркам (117 млн параметров), именно она заложила архитектурные и концептуальные основы для всех последующих поколений — GPT-2, GPT-3 и GPT-4." + "Она заложила фундамент всех последующих поколений GPT-моделей, показав, что модель, обученная на большом корпусе текстов в режиме **предсказания следующего токена**, способна эффективно адаптироваться к различным задачам обработки естественного языка." ] }, { - "cell_type": "markdown", - "id": "d763e797", + "cell_type": "code", + "execution_count": 1, + "id": "a4fba924", "metadata": {}, - "source": [] + "outputs": [], + "source": [ + "import dill\n", + "from torch import nn\n", + "import torch" + ] }, { "cell_type": "markdown", @@ -130,32 +32,569 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "1a6f2914", "metadata": {}, - "outputs": [ - { - "ename": "", - "evalue": "", - "output_type": "error", - "traceback": [ - "\u001b[1;31mRunning cells with '.venv (Python 3.10.9)' requires the ipykernel package.\n", - "\u001b[1;31mInstall 'ipykernel' into the Python environment. \n", - "\u001b[1;31mCommand: '/Users/sergey/Projects/ML/llm-arch-research/.venv/bin/python -m pip install ipykernel -U --force-reinstall'" - ] - } - ], + "outputs": [], "source": [ - "# llm/models/gpt/gpt2.py\n", - "import torch\n", - "import torch.nn as nn\n", - "import torch.nn.functional as F\n", - "from llm.core.base_model import BaseModel\n", - "from llm.core.decoder import Decoder\n", - "from llm.core.token_embeddings import TokenEmbeddings\n", - "from llm.core.positional_embeddings import PositionalEmbeddings\n", + "class BPE:\n", + " def __init__(self, vocab_size: int):\n", + " self.vocab_size = vocab_size\n", + " self.id2token = {}\n", + " self.token2id = {}\n", "\n", - "class GPT(BaseModel):\n", + " def fit(self, text: str):\n", + " # 1. Получаем уникальные токены (символы)\n", + " unique_tokens = sorted(set(text))\n", + " tokens = unique_tokens.copy()\n", + "\n", + " # 2. Разбиваем текст на токены-символы\n", + " sequence = list(text)\n", + "\n", + " # 3. Объединяем токены до достижения нужного размера словаря\n", + " while len(tokens) < self.vocab_size:\n", + " #print(f'len={len(tokens)} < {self.vocab_size}')\n", + " # Считаем частоты пар\n", + " pair_freq = {}\n", + " for i in range(len(sequence) - 1):\n", + " pair = (sequence[i], sequence[i + 1])\n", + " #print(f'pair = {pair}')\n", + " if pair not in pair_freq:\n", + " pair_freq[pair] = 0\n", + " pair_freq[pair] += 1\n", + "\n", + "\n", + " #print(f'pair_freq = {pair_freq}') \n", + " if not pair_freq:\n", + " break # нет пар — выходим\n", + "\n", + " #for x in pair_freq.items():\n", + " # self.debug(x, sequence)\n", + "\n", + " # Находим самую частую пару (в случае равенства — та, что встретилась первой)\n", + " most_frequent_pair = max(pair_freq.items(), key=lambda x: (x[1], -self._pair_first_index(sequence, x[0])))[0]\n", + " #print(most_frequent_pair)\n", + " # Создаем новый токен\n", + " new_token = most_frequent_pair[0] + most_frequent_pair[1]\n", + " #print(f\"new token={new_token}\")\n", + " tokens.append(new_token)\n", + " #print(f\"tokens={tokens}\")\n", + "\n", + " i = 0\n", + " new_sequence = []\n", + "\n", + " while i < len(sequence):\n", + " if i < len(sequence) - 1 and (sequence[i], sequence[i + 1]) == most_frequent_pair:\n", + " new_sequence.append(new_token)\n", + " i += 2 # пропускаем два символа — заменённую пару\n", + " else:\n", + " new_sequence.append(sequence[i])\n", + " i += 1\n", + " sequence = new_sequence\n", + " #break\n", + " \n", + " # 4. Создаем словари\n", + " self.vocab = tokens.copy()\n", + " self.token2id = dict(zip(tokens, range(self.vocab_size)))\n", + " self.id2token = dict(zip(range(self.vocab_size), tokens))\n", + "\n", + " def _pair_first_index(self, sequence, pair):\n", + " for i in range(len(sequence) - 1):\n", + " if (sequence[i], sequence[i + 1]) == pair:\n", + " return i\n", + " return float('inf') # если пара не найдена (в теории не должно случиться)\n", + "\n", + "\n", + " def encode(self, text: str):\n", + " # 1. Разбиваем текст на токены-символы\n", + " sequence = list(text)\n", + " # 2. Инициализация пустого списка токенов\n", + " tokens = []\n", + " # 3. Установить i = 0\n", + " i = 0\n", + " while i < len(text):\n", + " # 3.1 Найти все токены в словаре, начинающиеся с text[i]\n", + " start_char = text[i]\n", + " result = [token for token in self.vocab if token.startswith(start_char)]\n", + " # 3.2 Выбрать самый длинный подходящий токен\n", + " find_token = self._find_max_matching_token(text[i:], result)\n", + " if find_token is None:\n", + " # Обработка неизвестного символа\n", + " tokens.append(text[i]) # Добавляем сам символ как токен\n", + " i += 1\n", + " else:\n", + " # 3.3 Добавить токен в результат\n", + " tokens.append(find_token)\n", + " # 3.4 Увеличить i на длину токена\n", + " i += len(find_token)\n", + "\n", + " # 4. Заменить токены на их ID\n", + " return self._tokens_to_ids(tokens)\n", + "\n", + " def _find_max_matching_token(self, text: str, tokens: list):\n", + " \"\"\"Находит самый длинный токен из списка, с которого начинается текст\"\"\"\n", + " matching = [token for token in tokens if text.startswith(token)]\n", + " return max(matching, key=len) if matching else None\n", + "\n", + " def _tokens_to_ids(self, tokens):\n", + " \"\"\"Конвертирует список токенов в их ID с обработкой неизвестных токенов\"\"\"\n", + " ids = []\n", + " for token in tokens:\n", + " if token in self.token2id:\n", + " ids.append(self.token2id[token])\n", + " else:\n", + " ids.append(0) # Специальное значение\n", + " return ids\n", + "\n", + "\n", + " def decode(self, ids: list) -> str:\n", + " return ''.join(self._ids_to_tokens(ids))\n", + "\n", + " def _ids_to_tokens(self, ids: list) -> list:\n", + " \"\"\"Конвертирует список Ids в их tokens\"\"\"\n", + " tokens = []\n", + " for id in ids:\n", + " if id in self.id2token:\n", + " tokens.append(self.id2token[id])\n", + " else:\n", + " tokens.append('') # Специальное значение\n", + " return tokens\n", + "\n", + "\n", + " def save(self, filename):\n", + " with open(filename, 'wb') as f:\n", + " dill.dump(self, f)\n", + " print(f\"Объект сохранён в {filename}\")\n", + "\n", + "\n", + " @classmethod\n", + " def load(cls, filename):\n", + " with open(filename, 'rb') as f:\n", + " obj = dill.load(f)\n", + " \n", + " print(f\"Объект загружен из {filename}\")\n", + " return obj" + ] + }, + { + "cell_type": "markdown", + "id": "ef121b7b", + "metadata": {}, + "source": [ + "# Архитектура GPT-1: Принципы работы и ключевые компоненты\n", + "\n", + "![](https://ucarecdn.com/b2551e49-de5c-490b-a371-9c4a81e35329/)\n", + "\n", + "Модель **GPT-1 (Generative Pretrained Transformer)** — это первая версия архитектуры семейства GPT, основанная на **декодере трансформера**. \n", + "Она была представлена исследователями **OpenAI** в 2018 году и стала основой для всех последующих моделей, включая GPT-2, GPT-3 и GPT-4. \n", + "\n", + "Главная идея GPT-1 заключается в том, что модель можно обучить **понимать и генерировать текст**, если она научится предсказывать **следующий токен** в последовательности. \n", + "Этот простой принцип позволил создать универсальную языковую модель, способную решать множество задач без ручного проектирования под каждую из них.\n" + ] + }, + { + "cell_type": "markdown", + "id": "47d11c5c", + "metadata": {}, + "source": [ + "## 1. Эмбеддинги (Embeddings)\n", + "\n", + "\n", + "![](https://ucarecdn.com/4ce51ba3-83fc-46c3-a6e8-efa064663df0/)\n", + "\n", + "Перед тем как текст подается в трансформер, его необходимо преобразовать в числовое представление. \n", + "Это делается с помощью **эмбеддингов** — плотных векторов, которые кодируют смысл и структуру слов.\n", + "\n", + "GPT использует два типа эмбеддингов:" + ] + }, + { + "cell_type": "markdown", + "id": "e046ecc8", + "metadata": {}, + "source": [ + "### 1.1 Token Embeddings\n", + "\n", + "Каждое слово или подслово сначала токенизируется и преобразуется в уникальный числовой идентификатор. \n", + "Затем этот идентификатор сопоставляется с вектором фиксированной длины — **эмбеддингом токена**. \n", + "\n", + "Вектор можно рассматривать как координаты слова в многомерном пространстве смыслов: токены, близкие по значению, располагаются рядом.\n", + "\n", + "Формально:\n", + "$$\n", + "E_{token}(t_i) = W_e[t_i]\n", + "$$\n", + "\n", + "где \n", + "$W_e$ — обучаемая матрица эмбеддингов (размером `vocab_size × d_model`), \n", + "$t_i$ — индекс токена в словаре." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "1464a012", + "metadata": {}, + "outputs": [], + "source": [ + "class TokenEmbeddings(nn.Module):\n", + " def __init__(self, vocab_size: int, emb_size: int):\n", + " super().__init__()\n", + " self._embedding = nn.Embedding(\n", + " num_embeddings=vocab_size,\n", + " embedding_dim=emb_size,\n", + " padding_idx=0 # чтобы 0 можно было безопасно использовать\n", + " )\n", + "\n", + " def forward(self, x: torch.Tensor) -> torch.Tensor:\n", + " return self._embedding(x)" + ] + }, + { + "cell_type": "markdown", + "id": "a7e6025b", + "metadata": {}, + "source": [ + "### 1.2 Positional Embeddings\n", + "\n", + "Архитектура трансформера не учитывает порядок слов, так как внимание обрабатывает все токены параллельно. \n", + "Чтобы сохранить последовательность, вводятся **позиционные эмбеддинги**, которые добавляют информацию о позиции каждого токена.\n", + "\n", + "В GPT-1 используются **синусоиды фиксированной формы**:\n", + "$$\n", + "PE_{(pos, 2i)} = \\sin\\left(\\frac{pos}{10000^{2i/d_{model}}}\\right), \\quad\n", + "PE_{(pos, 2i+1)} = \\cos\\left(\\frac{pos}{10000^{2i/d_{model}}}\\right)\n", + "$$\n", + "\n", + "Окончательное представление токена:\n", + "$$\n", + "x_i = E_{token}(t_i) + PE(pos_i)\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "94ddd50d", + "metadata": {}, + "outputs": [], + "source": [ + "class PositionEmbeddings(nn.Module):\n", + " def __init__(self, max_seq_len: int, emb_size: int):\n", + " super().__init__()\n", + " self.max_seq_len = max_seq_len\n", + " self.emb_size = emb_size\n", + " self.embedding = nn.Embedding(\n", + " num_embeddings=max_seq_len,\n", + " embedding_dim=emb_size\n", + " )\n", + "\n", + " def forward(self, seq_len: int) -> torch.Tensor:\n", + " if seq_len < 1 or seq_len > self.max_seq_len:\n", + " raise IndexError(f\"Длина {seq_len} должна быть от 1 до {self.max_seq_len}\")\n", + " positions = torch.arange(seq_len, device=self.embedding.weight.device)\n", + " return self.embedding(positions)" + ] + }, + { + "cell_type": "markdown", + "id": "5b04dff2", + "metadata": {}, + "source": [ + "## 2. Внимание (Attention)\n", + "\n", + "\n", + "![](https://ucarecdn.com/538faeda-c12d-4137-9f67-e87720d83e13/)\n", + "\n", + "Механизм внимания — ключевая идея трансформеров. \n", + "Он позволяет модели **взвешивать важность других токенов** при обработке текущего, то есть решать, на какие слова нужно обратить внимание при генерации следующего.\n", + "\n", + "---\n", + "\n", + "### 2.1 Матрица внимания\n", + "\n", + "Каждому токену сопоставляются три обучаемых вектора:\n", + "- **Query (Q)** — запрос: что текущий токен ищет в других;\n", + "- **Key (K)** — ключ: какую информацию несет токен;\n", + "- **Value (V)** — значение: само содержимое токена.\n", + "\n", + "Эти векторы вычисляются линейными преобразованиями входных эмбеддингов:\n", + "$$\n", + "Q = XW_Q, \\quad K = XW_K, \\quad V = XW_V\n", + "$$\n", + "\n", + "Затем вычисляется **взвешенное внимание**:\n", + "$$\n", + "\\text{Attention}(Q, K, V) = \\text{softmax}\\left(\\frac{QK^T}{\\sqrt{d_k}}\\right)V\n", + "$$\n", + "\n", + "---\n", + "\n", + "### 2.2 Матричные операции\n", + "\n", + "- $QK^T$ — матрица сходства между токенами; \n", + "- деление на $\\sqrt{d_k}$ стабилизирует градиенты; \n", + "- **softmax** превращает оценки в вероятности; \n", + "- умножение результата на $V$ даёт взвешенные представления токенов.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "8fe8d3bb", + "metadata": {}, + "outputs": [], + "source": [ + "class HeadAttention(nn.Module):\n", + " def __init__(self, emb_size: int, head_size: int, max_seq_len: int):\n", + " super().__init__()\n", + " self._emb_size = emb_size\n", + " self._head_size = head_size\n", + " self._max_seq_len = max_seq_len\n", + "\n", + " # Линейные преобразования для Q, K, V\n", + " self._k = nn.Linear(emb_size, head_size)\n", + " self._q = nn.Linear(emb_size, head_size)\n", + " self._v = nn.Linear(emb_size, head_size)\n", + "\n", + " # Создание causal маски\n", + " mask = torch.tril(torch.ones(max_seq_len, max_seq_len))\n", + " self.register_buffer('_tril_mask', mask.bool() if hasattr(torch, 'bool') else mask.byte())\n", + "\n", + " def forward(self, x: torch.Tensor) -> torch.Tensor:\n", + " seq_len = x.shape[1]\n", + " if seq_len > self._max_seq_len:\n", + " raise ValueError(f\"Длина последовательности {seq_len} превышает максимум {self._max_seq_len}\")\n", + "\n", + " # 1. Линейные преобразования\n", + " k = self._k(x) # [B, T, hs]\n", + " q = self._q(x) # [B, T, hs]\n", + " \n", + " # 2. Вычисление scores\n", + " scores = q @ k.transpose(-2, -1) / self._head_size ** 0.5\n", + " \n", + " # 3. Применение causal маски\n", + " scores = scores.masked_fill(~self._tril_mask[:seq_len, :seq_len], float('-inf'))\n", + " \n", + " # 4. Softmax и умножение на V\n", + " weights = F.softmax(scores, dim=-1)\n", + " return weights @ self._v(x)" + ] + }, + { + "cell_type": "markdown", + "id": "a920fbcf", + "metadata": {}, + "source": [ + "### 2.3 Мультихед (Multi-Head Attention)\n", + "\n", + "Вместо одной операции внимания GPT-1 использует несколько параллельных **голов внимания**. \n", + "Каждая голова фокусируется на различных аспектах контекста — синтаксисе, семантике или долгосрочных зависимостях.\n", + "\n", + "$$\n", + "\\text{MultiHead}(Q, K, V) = \\text{Concat}(\\text{head}_1, \\dots, \\text{head}_h)W_O\n", + "$$\n", + "\n", + "где каждая голова:\n", + "$$\n", + "\\text{head}_i = \\text{Attention}(QW_{Qi}, KW_{Ki}, VW_{Vi})\n", + "$$\n", + "\n", + "В GPT-1 используется **12 голов**, что обеспечивает богатое контекстное восприятие текста." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "d55276a9", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "class MultiHeadAttention(nn.Module):\n", + " def __init__(self, num_heads: int, emb_size: int, head_size: int, max_seq_len: int, dropout: float = 0.1):\n", + " super().__init__()\n", + " self._heads = nn.ModuleList([\n", + " HeadAttention(\n", + " emb_size=emb_size, \n", + " head_size=head_size, \n", + " max_seq_len=max_seq_len\n", + " ) for _ in range(num_heads)\n", + " ])\n", + " self._layer = nn.Linear(head_size * num_heads, emb_size)\n", + " self._dropout = nn.Dropout(dropout)\n", + "\n", + " def forward(self, x: torch.Tensor, mask: torch.Tensor = None):\n", + " # 1. Вычисляем attention для каждой головы\n", + " attention_outputs = [head(x) for head in self._heads]\n", + " \n", + " # 2. Объединяем результаты всех голов\n", + " concatenated_attention = torch.cat(attention_outputs, dim=-1)\n", + " \n", + " # 3. Проецируем в пространство эмбеддингов\n", + " projected_output = self._layer(concatenated_attention)\n", + " \n", + " # 4. Применяем dropout для регуляризации\n", + " final_output = self._dropout(projected_output)\n", + " \n", + " return final_output" + ] + }, + { + "cell_type": "markdown", + "id": "3ffafb56", + "metadata": {}, + "source": [ + "## 3. Feed Forward Network (FFN)\n", + "\n", + "![](https://ucarecdn.com/6af52549-95fa-45be-9764-9c399f387aa6/)\n", + "\n", + "После блока внимания каждый токен независимо проходит через двухслойную нейронную сеть — **Feed Forward Network**. \n", + "Она добавляет модели способность нелинейно преобразовывать информацию.\n", + "\n", + "$$\n", + "\\text{FFN}(x) = \\max(0, xW_1 + b_1)W_2 + b_2\n", + "$$\n", + "\n", + "Используется активация **ReLU**. \n", + "FFN применяется одинаково к каждому токену и не зависит от порядка слов, что делает вычисления высокопараллельными.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "84f57562", + "metadata": {}, + "outputs": [], + "source": [ + "class FeedForward(nn.Module):\n", + " def __init__(self, emb_size: int, dropout: float = 0.1):\n", + " super().__init__()\n", + " self.net = nn.Sequential(\n", + " nn.Linear(emb_size, 4 * emb_size),\n", + " nn.ReLU(),\n", + " nn.Linear(4 * emb_size, emb_size),\n", + " nn.Dropout(dropout)\n", + " )\n", + "\n", + " def forward(self, x: torch.Tensor):\n", + " self.net = self.net.to(x.dtype)\n", + " return self.net(x)" + ] + }, + { + "cell_type": "markdown", + "id": "8d9ce9d8", + "metadata": {}, + "source": [ + "## 4. Блок Декодера\n", + "\n", + "\n", + "![](https://ucarecdn.com/c7cc9bf3-cc75-4fac-97a9-ce122c74738e/)\n", + "\n", + "\n", + "Каждый слой GPT-1 — это **декодер**, состоящий из следующих элементов:\n", + "\n", + "1. **Masked Multi-Head Attention** \n", + " Маска запрещает токену видеть будущие позиции, чтобы сохранить авторегрессионное направление генерации (слева направо).\n", + "\n", + "2. **Остаточные связи + Layer Normalization** \n", + " Результат внимания складывается с входом слоя, а затем нормализуется: \n", + " $$\n", + " x' = \\text{LayerNorm}(x + \\text{Attention}(x))\n", + " $$\n", + "\n", + "3. **Feed Forward + Residual + LayerNorm** \n", + " $$\n", + " y = \\text{LayerNorm}(x' + \\text{FFN}(x'))\n", + " $$\n", + "\n", + "GPT-1 содержит **12 таких блоков**, соединённых последовательно." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "300acc96", + "metadata": {}, + "outputs": [], + "source": [ + "class Decoder(nn.Module):\n", + " def __init__(self, \n", + " num_heads: int,\n", + " emb_size: int,\n", + " head_size: int,\n", + " max_seq_len: int,\n", + " dropout: float = 0.1\n", + " ):\n", + " super().__init__()\n", + " self._heads = MultiHeadAttention(\n", + " num_heads=num_heads, \n", + " emb_size=emb_size, \n", + " head_size=head_size, \n", + " max_seq_len=max_seq_len, \n", + " dropout=dropout\n", + " )\n", + " self._ff = FeedForward(\n", + " emb_size=emb_size, \n", + " dropout=dropout\n", + " )\n", + " self._norm1 = nn.LayerNorm(emb_size)\n", + " self._norm2 = nn.LayerNorm(emb_size)\n", + " #self._dropout_attn = nn.Dropout(dropout)\n", + " #self._dropout_ffn = nn.Dropout(dropout)\n", + "\n", + " def forward(self, x: torch.Tensor, mask: torch.Tensor = None):\n", + " # Приведение типов параметров\n", + " self._heads = self._heads.to(x.dtype)\n", + " self._ff = self._ff.to(x.dtype)\n", + " \n", + " # Пропустим тензор x через экземпляр MultiHeadAttention.\n", + " attention = self._heads(x, mask)\n", + " #attention = self._dropout_attn(attention)\n", + " \n", + " # Выходной тензор из блока внимания сложим с исходным x.\n", + " out = attention + x\n", + " \n", + " # Получившийся тензор пропустим через первый слой нормализации.\n", + " norm_out = self._norm1(out)\n", + " \n", + " # Затем подадим его на вход экземпляру FFN.\n", + " ffn_out = self._ff(norm_out)\n", + " #ffn_out = self._dropout_ffn(ffn_out)\n", + " \n", + " # Выходной тензор из FFN сложим с тем, что поступил на вход.\n", + " out = ffn_out + norm_out\n", + " \n", + " # Пропустим получившийся тензор через второй слой нормализации.\n", + " norm_out = self._norm2(out)\n", + " \n", + " # Вернем итоговый тензор размером batch_size x seq_len x emb_size.\n", + " return norm_out" + ] + }, + { + "cell_type": "markdown", + "id": "888d1a1c", + "metadata": {}, + "source": [ + "## 5. Обучение GPT-1\n", + "\n", + "GPT-1 обучается в два этапа:\n", + "\n", + "- 1️⃣ **Предобучение (Unsupervised Pretraining)** \n", + "- 2️⃣ **Дообучение (Supervised Fine-Tuning)**\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "0eb26ef3", + "metadata": {}, + "outputs": [], + "source": [ + "class GPT(nn.Module):\n", " \"\"\"GPT-like трансформер для генерации текста\n", " \n", " Args:\n", @@ -168,36 +607,40 @@ " dropout: Вероятность dropout (default=0.1)\n", " device: Устройство (default='cpu')\n", " \"\"\"\n", - " def __init__(self, config):\n", - " super().__init__(config)\n", - "\n", + " def __init__(self,\n", + " vocab_size: int,\n", + " max_seq_len: int,\n", + " emb_size: int,\n", + " num_heads: int,\n", + " head_size: int,\n", + " num_layers: int,\n", + " dropout: float = 0.1,\n", + " device: str = 'cpu'\n", + " ):\n", + " super().__init__()\n", + " self.device = device\n", + " self.max_seq_len = max_seq_len\n", + " \n", " # Инициализация слоев\n", - " self._max_seq_len = config[\"max_position_embeddings\"]\n", " self._token_embeddings = TokenEmbeddings(\n", - " vocab_size=config[\"vocab_size\"], \n", - " emb_size=config[\"embed_dim\"]\n", + " vocab_size=vocab_size, \n", + " emb_size=emb_size\n", " )\n", - " self._position_embeddings = PositionalEmbeddings(\n", - " max_seq_len=config[\"max_position_embeddings\"], \n", - " emb_size=config[\"embed_dim\"]\n", + " self._position_embeddings = PositionEmbeddings(\n", + " max_seq_len=max_seq_len, \n", + " emb_size=emb_size\n", " )\n", - " self._dropout = nn.Dropout(config[\"dropout\"])\n", - " # head_size = emb_size // num_heads\n", + " self._dropout = nn.Dropout(dropout)\n", " self._decoders = nn.ModuleList([Decoder(\n", - " num_heads=config[\"num_heads\"],\n", - " emb_size=config[\"embed_dim\"],\n", - " head_size=config[\"embed_dim\"] // config[\"num_heads\"],\n", - " max_seq_len=config[\"max_position_embeddings\"],\n", - " dropout=config[\"dropout\"] \n", - " ) for _ in range(config[\"num_layers\"])])\n", - " self._linear = nn.Linear(config[\"embed_dim\"], config[\"vocab_size\"])\n", - " \n", - " @property\n", - " def max_seq_len(self):\n", - " \"\"\"Возвращает максимальную длину последовательности.\"\"\"\n", - " return self._max_seq_len\n", + " num_heads=num_heads,\n", + " emb_size=emb_size,\n", + " head_size=head_size,\n", + " max_seq_len=max_seq_len,\n", + " dropout=dropout \n", + " ) for _ in range(num_layers)])\n", + " self._linear = nn.Linear(emb_size, vocab_size)\n", "\n", - " def forward(self, x: torch.Tensor, attention_mask=None) -> torch.Tensor:\n", + " def forward(self, x: torch.Tensor) -> torch.Tensor:\n", " \"\"\"Прямой проход через GPT\n", " \n", " Args:\n", @@ -207,8 +650,8 @@ " Тензор логитов [batch_size, seq_len, vocab_size]\n", " \"\"\"\n", " # Проверка длины последовательности\n", - " if x.size(1) > self._max_seq_len:\n", - " raise ValueError(f\"Длина последовательности {x.size(1)} превышает максимальную {self._max_seq_len}\")\n", + " if x.size(1) > self.max_seq_len:\n", + " raise ValueError(f\"Длина последовательности {x.size(1)} превышает максимальную {self.max_seq_len}\")\n", " \n", " # Эмбеддинги токенов и позиций\n", " tok_out = self._token_embeddings(x) # [batch, seq_len, emb_size]\n", @@ -223,126 +666,10 @@ " \n", " return self._linear(out) # [batch, seq_len, vocab_size]\n", "\n", - "\n", - "# def forward(self, input_ids, attention_mask=None):\n", - "# B, T = input_ids.size()\n", - "# pos = torch.arange(0, T, device=input_ids.device).unsqueeze(0)\n", - "#\n", - "# x = self.token_emb(input_ids) + self.pos_emb(pos)\n", - "#\n", - "# for block in self.blocks:\n", - "# x = block(x, attention_mask)\n", - "#\n", - "# x = self.ln_f(x)\n", - "# logits = self.head(x)\n", - "# return logits\n", - "\n", - "\n", - " def generate(self,\n", - " x: torch.Tensor, \n", - " max_new_tokens: int, \n", - " do_sample: bool,\n", - " temperature: float = 1.0,\n", - " top_k: int = None,\n", - " top_p: float = None,\n", - " attention_mask: torch.Tensor = None, # Добавляем для совместимости с HF\n", - " **kwargs # Игнорируем остальные параметры\n", - " ) -> torch.Tensor:\n", - " \"\"\"Авторегрессивная генерация текста.\n", - " \n", - " Параметры:\n", - " x: Входной тензор с индексами токенов формы [batch_size, seq_len],\n", - " где batch_size - размер батча, seq_len - длина последовательности.\n", - " max_new_tokens: Максимальное количество новых токенов для генерации.\n", - " do_sample: Флаг выбора режима генерации:\n", - " - True: вероятностное сэмплирование\n", - " - False: жадный поиск (argmax)\n", - " temperature: Параметр температуры для сэмплирования:\n", - " - >1.0 - более случайные результаты\n", - " - 1.0 - нейтральное значение\n", - " - <1.0 - более предсказуемые результаты\n", - " Должна быть > 0 (по умолчанию: 1.0)\n", - " top_k: Если задан (и do_sample=True), используется top-k сэмплирование:\n", - " - Выбираются только top_k самых вероятных токенов\n", - " - Остальным токенам устанавливается вероятность 0\n", - " - None: отключено (по умолчанию)\n", - " top_p: Если задан (и do_sample=True), используется nucleus (top-p) сэмплирование:\n", - " - Выбираются токены с кумулятивной вероятностью ≤ top_p\n", - " - Гарантируется, что хотя бы один токен остаётся (даже если его вероятность > top_p)\n", - " - None: отключено (по умолчанию)\n", - " - Должен быть в диапазоне (0, 1]\n", - " \n", - " Возвращает:\n", - " torch.Tensor: Тензор с расширенной последовательностью токенов формы \n", - " [batch_size, seq_len + max_new_tokens]\n", - "\n", - " Исключения:\n", - " ValueError: Если входная последовательность длиннее max_seq_len\n", - " ValueError: Если temperature <= 0\n", - " ValueError: Если одновременно заданы top_k и top_p\n", - " ValueError: Если top_k задан и ≤ 0\n", - " ValueError: Если top_p задан и не в диапазоне (0, 1]\n", - "\n", - " Примеры:\n", - " >>> # Жадная генерация\n", - " >>> output = model.generate(input_ids, max_new_tokens=10, do_sample=False)\n", - " >>> \n", - " >>> # Вероятностная генерация с top-k\n", - " >>> output = model.generate(input_ids, max_new_tokens=10, do_sample=True, top_k=50)\n", - " >>>\n", - " >>> # Nucleus sampling (top-p)\n", - " >>> output = model.generate(input_ids, max_new_tokens=10, do_sample=True, top_p=0.9)\n", - " >>>\n", - " >>> # Комбинация температуры и top-k\n", - " >>> output = model.generate(input_ids, max_new_tokens=10, do_sample=True, \n", - " ... temperature=0.7, top_k=50)\n", - "\n", - " Примечания:\n", - " 1. Для детерминированных результатов в режиме сэмплирования \n", - " зафиксируйте random seed (torch.manual_seed).\n", - " 2. Температура влияет только на режим сэмплирования (do_sample=True).\n", - " 3. Одновременное использование top_k и top_p запрещено.\n", - " 4. При do_sample=False параметры top_k, top_p и temperature игнорируются.\n", - "\n", - " Args:\n", - " x (torch.Tensor): Входной тензор с индексами токенов формы [batch_size, seq_len],\n", - " где batch_size - размер батча, seq_len - длина последовательности.\n", - " max_new_tokens (int): Максимальное количество новых токенов для генерации.\n", - " do_sample (bool): Флаг выбора режима генерации:\n", - " - True: вероятностное сэмплирование\n", - " - False: жадный поиск (argmax)\n", - " temperature (float): Параметр температуры для сэмплирования:\n", - " - >1.0 - более случайные результаты\n", - " - 1.0 - нейтральное значение\n", - " - <1.0 - более предсказуемые результаты\n", - " Должна быть > 0 (по умолчанию: 1.0)\n", - "\n", - " Returns:\n", - " torch.Tensor: Тензор с расширенной последовательностью токенов формы \n", - " [batch_size, seq_len + max_new_tokens]\n", - "\n", - " Raises:\n", - " ValueError: Если входная последовательность длиннее max_seq_len\n", - " ValueError: Если temperature <= 0\n", - "\n", - " Examples:\n", - " >>> # Жадная генерация\n", - " >>> output = model.generate(input_ids, max_new_tokens=10, do_sample=False)\n", - " >>>\n", - " >>> # Вероятностная генерация с температурой\n", - " >>> output = model.generate(input_ids, max_new_tokens=10, do_sample=True, temperature=0.7)\n", - " >>>\n", - " >>> # Более случайная генерация\n", - " >>> output = model.generate(input_ids, max_new_tokens=10, do_sample=True, temperature=1.5)\n", - "\n", - " Note:\n", - " Для детерминированных результатов в режиме сэмплирования \n", - " зафиксируйте random seed (torch.manual_seed).\n", - " Температура влияет только на режим сэмплирования (do_sample=True).\n", - " \"\"\"\n", + " def generate(self, x: torch.Tensor, max_new_tokens: int):\n", " for _ in range(max_new_tokens):\n", " # 1. Обрезаем вход, если последовательность слишком длинная\n", - " x_cond = x[:, -self._max_seq_len:]\n", + " x_cond = x[:, -self.max_seq_len:]\n", "\n", " # 2. Передаем последовательность в метод forward класса GPT и полуаем логиты.\n", " logits = self.forward(x_cond)\n", @@ -350,72 +677,483 @@ " # 3. Берем логиты для последнего токена\n", " last_logits = logits[:, -1, :] # [batch_size, vocab_size]\n", "\n", - " # Масштабируем логиты температурой\n", - " if temperature > 0:\n", - " logits_scaled = last_logits / temperature\n", - " else:\n", - " logits_scaled = last_logits\n", - "\n", - " if do_sample == True and top_k != None:\n", - " _, topk_indices = torch.topk(logits_scaled, top_k, dim=-1)\n", - "\n", - " # # Заменим все НЕ top-k логиты на -inf\n", - " masked_logits = logits_scaled.clone()\n", - " vocab_size = logits_scaled.size(-1)\n", - "\n", - " # создаём маску: True, если токен НЕ в topk_indices\n", - " mask = torch.ones_like(logits_scaled, dtype=torch.bool if hasattr(torch, 'bool') else torch.uint8)\n", - " mask.scatter_(1, topk_indices, False if hasattr(torch, 'bool') else 0) # False там, где top-k индексы\n", - " masked_logits[mask] = float('-inf')\n", - "\n", - " logits_scaled = masked_logits\n", - "\n", - " if do_sample == True and top_p != None:\n", - " # 1. Применим softmax, чтобы получить вероятности:\n", - " probs = F.softmax(logits_scaled, dim=-1) # [B, vocab_size]\n", - " # 2. Отсортируем токены по убыванию вероятностей:\n", - " sorted_probs, sorted_indices = torch.sort(probs, descending=True, dim=-1)\n", - " # 3. Посчитаем кумулятивную сумму вероятностей:\n", - " cum_probs = torch.cumsum(sorted_probs, dim=-1) # [B, vocab_size]\n", - " # 4. Определим маску: оставить токены, пока сумма < top_p\n", - " sorted_mask = (cum_probs <= top_p) # [B, vocab_size]\n", - " # Гарантируем, что хотя бы первый токен останется\n", - " sorted_mask[:, 0] = True\n", - " # 5. Преобразуем маску обратно в оригинальный порядок:\n", - " # Создаём полную маску из False\n", - " mask = torch.zeros_like(probs, dtype=torch.bool if hasattr(torch, 'bool') else torch.uint8)\n", - " # Устанавливаем True в местах нужных токенов\n", - " mask.scatter_(dim=1, index=sorted_indices, src=sorted_mask)\n", - " # 6. Зануляем логиты токенов вне топ-p:\n", - " logits_scaled[~mask] = float('-inf')\n", - "\n", " # 4. Применяем Softmax\n", - " probs = F.softmax(logits_scaled, dim=-1) # [batch_size, vocab_size]\n", + " probs = F.softmax(last_logits, dim=-1) # [batch_size, vocab_size]\n", "\n", + " # 5. Выбираем токен с максимальной вероятностью\n", + " next_token = torch.argmax(probs, dim=-1, keepdim=True) # [batch_size, 1]\n", "\n", - " if do_sample == True:\n", - " # 5. Если do_sample равен True, то отбираем токен случайно с помощью torch.multinomial\n", - " next_token = torch.multinomial(probs, num_samples=1) # [batch_size, 1]\n", - " else:\n", - " # 5. Если do_sample равен False, то выбираем токен с максимальной вероятностью\n", - " next_token = torch.argmax(probs, dim=-1, keepdim=True) # [batch_size, 1]\n", - " \n", " # 6. Добавляем его к последовательности\n", - " x = torch.cat([x, next_token], dim=1) # [batch_size, seq_len+1]\n", - " return x\n", - "\n", - "# def generate(self, input_ids, max_length=50):\n", - "# for _ in range(max_length):\n", - "# logits = self.forward(input_ids)\n", - "# next_token = torch.argmax(logits[:, -1, :], dim=-1, keepdim=True)\n", - "# input_ids = torch.cat([input_ids, next_token], dim=1)\n", - "# return input_ids\n" + " x = torch.cat([x, next_token], dim=1) # [batch_size, seq_len+1] \n", + " return x" ] }, { "cell_type": "markdown", - "id": "303c8f8c", + "id": "b47966ba", "metadata": {}, + "source": [ + "\n", + "\n", + "### 5.1 Предобучение\n", + "\n", + "На первом этапе модель обучается без разметки: она получает большой корпус текстов и учится **предсказывать следующий токен** по предыдущим.\n", + "\n", + "Функция потерь:\n", + "$$\n", + "L = - \\sum_{t=1}^{T} \\log P(x_t | x_1, x_2, ..., x_{t-1})\n", + "$$\n", + "\n", + "Таким образом, модель учится строить вероятностную модель языка, \"угадывая\" продолжение текста.\n" + ] + }, + { + "cell_type": "markdown", + "id": "12e4624e", + "metadata": {}, + "source": [ + "Во время **предобучения** GPT-1 учится **предсказывать следующий токен** (language modeling task). \n", + "Формально: \n", + "$$ \n", + "P(x_t ,|, x_1, x_2, \\dots, x_{t-1}) \n", + "$$ \n", + "То есть, если на вход подаётся предложение `\"I love deep\"`, модель должна предсказать `\"learning\"`.\n" + ] + }, + { + "cell_type": "markdown", + "id": "87dcc10e", + "metadata": {}, + "source": [ + "### ✅ 5.1.1 Подготовка данных\n", + "\n", + "Создадим **датасет** на основе BPE-токенизатора:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "632eec77", + "metadata": {}, + "outputs": [], + "source": [ + "import torch\n", + "from torch.utils.data import Dataset, DataLoader\n", + "\n", + "class GPTDataset(Dataset):\n", + " def __init__(self, text: str, bpe: BPE, block_size: int):\n", + " self.bpe = bpe\n", + " self.block_size = block_size\n", + " self.data = bpe.encode(text)\n", + " \n", + " def __len__(self):\n", + " return len(self.data) - self.block_size\n", + "\n", + " def __getitem__(self, idx):\n", + " x = torch.tensor(self.data[idx:idx+self.block_size], dtype=torch.long)\n", + " y = torch.tensor(self.data[idx+1:idx+self.block_size+1], dtype=torch.long)\n", + " return x, y" + ] + }, + { + "cell_type": "markdown", + "id": "bb5d83d8", + "metadata": {}, + "source": [ + "- `x` — входная последовательность токенов\n", + " \n", + "- `y` — та же последовательность, но сдвинутая на один токен вперёд (цель)" + ] + }, + { + "cell_type": "markdown", + "id": "24de37be", + "metadata": {}, + "source": [ + "### ✅ 5.1.2 Цикл обучения\n", + "\n", + "Для обучения создадим функцию:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "8003ea24", + "metadata": {}, + "outputs": [], + "source": [ + "import torch.nn.functional as F\n", + "from torch import optim\n", + "\n", + "def train_gpt(model, dataset, epochs=5, batch_size=32, lr=3e-4, device='cpu'):\n", + " dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)\n", + " optimizer = optim.AdamW(model.parameters(), lr=lr)\n", + "\n", + " model.to(device)\n", + " model.train()\n", + "\n", + " for epoch in range(epochs):\n", + " total_loss = 0\n", + " for x, y in dataloader:\n", + " x, y = x.to(device), y.to(device)\n", + "\n", + " # Прямой проход\n", + " logits = model(x) # [B, T, vocab_size]\n", + "\n", + " # Перестроим выход под CrossEntropy\n", + " loss = F.cross_entropy(logits.view(-1, logits.size(-1)), y.view(-1))\n", + "\n", + " # Обратное распространение\n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " total_loss += loss.item()\n", + "\n", + " avg_loss = total_loss / len(dataloader)\n", + " print(f\"Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.4f}\")\n", + "\n", + " return model" + ] + }, + { + "cell_type": "markdown", + "id": "3c351b56", + "metadata": {}, + "source": [ + "### ✅ 5.1.3 Пример запуска" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "dd700a5c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dataset length: 20\n", + "Epoch 1/100, Loss: 4.5466\n", + "Epoch 2/100, Loss: 4.2532\n", + "Epoch 3/100, Loss: 3.9998\n", + "Epoch 4/100, Loss: 3.7857\n", + "Epoch 5/100, Loss: 3.5823\n", + "Epoch 6/100, Loss: 3.3802\n", + "Epoch 7/100, Loss: 3.2312\n", + "Epoch 8/100, Loss: 3.0589\n", + "Epoch 9/100, Loss: 2.8971\n", + "Epoch 10/100, Loss: 2.7329\n", + "Epoch 11/100, Loss: 2.5889\n", + "Epoch 12/100, Loss: 2.4817\n", + "Epoch 13/100, Loss: 2.3101\n", + "Epoch 14/100, Loss: 2.1343\n", + "Epoch 15/100, Loss: 2.0490\n", + "Epoch 16/100, Loss: 1.8943\n", + "Epoch 17/100, Loss: 1.7862\n", + "Epoch 18/100, Loss: 1.6848\n", + "Epoch 19/100, Loss: 1.5660\n", + "Epoch 20/100, Loss: 1.4896\n", + "Epoch 21/100, Loss: 1.3954\n", + "Epoch 22/100, Loss: 1.3091\n", + "Epoch 23/100, Loss: 1.2422\n", + "Epoch 24/100, Loss: 1.1602\n", + "Epoch 25/100, Loss: 1.1006\n", + "Epoch 26/100, Loss: 1.0547\n", + "Epoch 27/100, Loss: 0.9972\n", + "Epoch 28/100, Loss: 0.9414\n", + "Epoch 29/100, Loss: 0.8983\n", + "Epoch 30/100, Loss: 0.8630\n", + "Epoch 31/100, Loss: 0.7975\n", + "Epoch 32/100, Loss: 0.7723\n", + "Epoch 33/100, Loss: 0.7307\n", + "Epoch 34/100, Loss: 0.7038\n", + "Epoch 35/100, Loss: 0.6767\n", + "Epoch 36/100, Loss: 0.6498\n", + "Epoch 37/100, Loss: 0.6052\n", + "Epoch 38/100, Loss: 0.5923\n", + "Epoch 39/100, Loss: 0.5587\n", + "Epoch 40/100, Loss: 0.5362\n", + "Epoch 41/100, Loss: 0.5186\n", + "Epoch 42/100, Loss: 0.4819\n", + "Epoch 43/100, Loss: 0.4704\n", + "Epoch 44/100, Loss: 0.4753\n", + "Epoch 45/100, Loss: 0.4510\n", + "Epoch 46/100, Loss: 0.4102\n", + "Epoch 47/100, Loss: 0.3981\n", + "Epoch 48/100, Loss: 0.3920\n", + "Epoch 49/100, Loss: 0.3864\n", + "Epoch 50/100, Loss: 0.3532\n", + "Epoch 51/100, Loss: 0.3462\n", + "Epoch 52/100, Loss: 0.3315\n", + "Epoch 53/100, Loss: 0.3281\n", + "Epoch 54/100, Loss: 0.3150\n", + "Epoch 55/100, Loss: 0.3121\n", + "Epoch 56/100, Loss: 0.3134\n", + "Epoch 57/100, Loss: 0.2914\n", + "Epoch 58/100, Loss: 0.2914\n", + "Epoch 59/100, Loss: 0.2678\n", + "Epoch 60/100, Loss: 0.2641\n", + "Epoch 61/100, Loss: 0.2631\n", + "Epoch 62/100, Loss: 0.2479\n", + "Epoch 63/100, Loss: 0.2349\n", + "Epoch 64/100, Loss: 0.2383\n", + "Epoch 65/100, Loss: 0.2283\n", + "Epoch 66/100, Loss: 0.2229\n", + "Epoch 67/100, Loss: 0.2152\n", + "Epoch 68/100, Loss: 0.2116\n", + "Epoch 69/100, Loss: 0.2042\n", + "Epoch 70/100, Loss: 0.1961\n", + "Epoch 71/100, Loss: 0.1787\n", + "Epoch 72/100, Loss: 0.1907\n", + "Epoch 73/100, Loss: 0.1777\n", + "Epoch 74/100, Loss: 0.1813\n", + "Epoch 75/100, Loss: 0.1711\n", + "Epoch 76/100, Loss: 0.1836\n", + "Epoch 77/100, Loss: 0.1748\n", + "Epoch 78/100, Loss: 0.1684\n", + "Epoch 79/100, Loss: 0.1622\n", + "Epoch 80/100, Loss: 0.1739\n", + "Epoch 81/100, Loss: 0.1607\n", + "Epoch 82/100, Loss: 0.1657\n", + "Epoch 83/100, Loss: 0.1579\n", + "Epoch 84/100, Loss: 0.1588\n", + "Epoch 85/100, Loss: 0.1526\n", + "Epoch 86/100, Loss: 0.1405\n", + "Epoch 87/100, Loss: 0.1420\n", + "Epoch 88/100, Loss: 0.1531\n", + "Epoch 89/100, Loss: 0.1392\n", + "Epoch 90/100, Loss: 0.1355\n", + "Epoch 91/100, Loss: 0.1278\n", + "Epoch 92/100, Loss: 0.1331\n", + "Epoch 93/100, Loss: 0.1343\n", + "Epoch 94/100, Loss: 0.1355\n", + "Epoch 95/100, Loss: 0.1298\n", + "Epoch 96/100, Loss: 0.1254\n", + "Epoch 97/100, Loss: 0.1149\n", + "Epoch 98/100, Loss: 0.1265\n", + "Epoch 99/100, Loss: 0.1308\n", + "Epoch 100/100, Loss: 0.1178\n" + ] + }, + { + "data": { + "text/plain": [ + "GPT(\n", + " (_token_embeddings): TokenEmbeddings(\n", + " (_embedding): Embedding(100, 64, padding_idx=0)\n", + " )\n", + " (_position_embeddings): PositionEmbeddings(\n", + " (embedding): Embedding(8, 64)\n", + " )\n", + " (_dropout): Dropout(p=0.1, inplace=False)\n", + " (_decoders): ModuleList(\n", + " (0-1): 2 x Decoder(\n", + " (_heads): MultiHeadAttention(\n", + " (_heads): ModuleList(\n", + " (0-3): 4 x HeadAttention(\n", + " (_k): Linear(in_features=64, out_features=16, bias=True)\n", + " (_q): Linear(in_features=64, out_features=16, bias=True)\n", + " (_v): Linear(in_features=64, out_features=16, bias=True)\n", + " )\n", + " )\n", + " (_layer): Linear(in_features=64, out_features=64, bias=True)\n", + " (_dropout): Dropout(p=0.1, inplace=False)\n", + " )\n", + " (_ff): FeedForward(\n", + " (net): Sequential(\n", + " (0): Linear(in_features=64, out_features=256, bias=True)\n", + " (1): ReLU()\n", + " (2): Linear(in_features=256, out_features=64, bias=True)\n", + " (3): Dropout(p=0.1, inplace=False)\n", + " )\n", + " )\n", + " (_norm1): LayerNorm((64,), eps=1e-05, elementwise_affine=True)\n", + " (_norm2): LayerNorm((64,), eps=1e-05, elementwise_affine=True)\n", + " )\n", + " )\n", + " (_linear): Linear(in_features=64, out_features=100, bias=True)\n", + ")" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# 1. Исходный текст\n", + "text = \"Deep learning is amazing. Transformers changed the world. Attention is all you need. GPT models revolutionized NLP.\"\n", + "\n", + "# 2. Обучаем токенизатор\n", + "bpe = BPE(vocab_size=100)\n", + "bpe.fit(text)\n", + "\n", + "# 3. Создаем датасет\n", + "dataset = GPTDataset(text, bpe, block_size=8)\n", + "print(f\"Dataset length: {len(dataset)}\")\n", + "\n", + "# 4. Инициализируем модель\n", + "gpt = GPT(\n", + " vocab_size=len(bpe.vocab),\n", + " max_seq_len=8,\n", + " emb_size=64,\n", + " num_heads=4,\n", + " head_size=16,\n", + " num_layers=2,\n", + " dropout=0.1\n", + ")\n", + "\n", + "# 5. Обучаем\n", + "train_gpt(gpt, dataset, epochs=100, batch_size=4)" + ] + }, + { + "cell_type": "markdown", + "id": "c3714dfc", + "metadata": {}, + "source": [ + "\n", + "---\n", + "\n", + "### 5.2 Дообучение\n", + "\n", + "После предобучения GPT-1 уже знает структуру и грамматику языка. \n", + "На втором этапе она дообучается на конкретных задачах (например, классификация, QA) с помощью размеченных данных.\n", + "\n", + "Технически это почти то же обучение, только:\n", + "\n", + "- Загружаем модель с уже обученными весами.\n", + "- Используем новые данные.\n", + "- Можно уменьшить скорость обучения.\n", + "- Иногда замораживают часть слоёв (например, эмбеддинги).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "4afd7733", + "metadata": {}, + "outputs": [], + "source": [ + "def fine_tune_gpt(model, dataset, epochs=3, batch_size=16, lr=1e-5, device='cpu', freeze_embeddings=True):\n", + " if freeze_embeddings:\n", + " for param in model._token_embeddings.parameters():\n", + " param.requires_grad = False\n", + " for param in model._position_embeddings.parameters():\n", + " param.requires_grad = False\n", + "\n", + " dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)\n", + " optimizer = optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=lr)\n", + "\n", + " model.to(device)\n", + " model.train()\n", + "\n", + " for epoch in range(epochs):\n", + " total_loss = 0\n", + " for x, y in dataloader:\n", + " x, y = x.to(device), y.to(device)\n", + " logits = model(x)\n", + " loss = F.cross_entropy(logits.view(-1, logits.size(-1)), y.view(-1))\n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " optimizer.step()\n", + " total_loss += loss.item()\n", + " print(f\"Fine-tune Epoch {epoch+1}/{epochs}, Loss: {total_loss / len(dataloader):.4f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "d1698def", + "metadata": {}, + "source": [] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "71bb6b24", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Fine-tune Epoch 1/10, Loss: 4.3808\n", + "Fine-tune Epoch 2/10, Loss: 3.9245\n", + "Fine-tune Epoch 3/10, Loss: 3.5217\n", + "Fine-tune Epoch 4/10, Loss: 3.2451\n", + "Fine-tune Epoch 5/10, Loss: 3.0076\n", + "Fine-tune Epoch 6/10, Loss: 2.8133\n", + "Fine-tune Epoch 7/10, Loss: 2.6857\n", + "Fine-tune Epoch 8/10, Loss: 2.5984\n", + "Fine-tune Epoch 9/10, Loss: 2.5168\n", + "Fine-tune Epoch 10/10, Loss: 2.4128\n" + ] + } + ], + "source": [ + "# Например, мы хотим дообучить модель на стиле коротких технических фраз\n", + "fine_tune_text = \"\"\"\n", + "Transformers revolutionize NLP.\n", + "Deep learning enables self-attention.\n", + "GPT generates text autoregressively.\n", + "\"\"\"\n", + "\n", + "dataset = GPTDataset(fine_tune_text, bpe, block_size=8)\n", + "\n", + "\n", + "# Запуск дообучения\n", + "fine_tune_gpt(gpt, dataset, epochs=10, batch_size=4, lr=1e-4)" + ] + }, + { + "cell_type": "markdown", + "id": "d5ff63e9", + "metadata": {}, + "source": [ + "## 📝 6. Генерация текста после обучения" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "ccb9621a", + "metadata": {}, + "outputs": [], + "source": [ + "def generate_text(model, bpe, prompt: str, max_new_tokens=20, device='cpu'):\n", + " model.eval()\n", + " ids = torch.tensor([bpe.encode(prompt)], dtype=torch.long).to(device)\n", + " out = model.generate(ids, max_new_tokens=max_new_tokens)\n", + " text = bpe.decode(out[0].tolist())\n", + " return text" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "f1b82472", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Deep learning e els revolutionized NLP.\n" + ] + } + ], + "source": [ + "print(generate_text(gpt, bpe, \"Deep learning\", max_new_tokens=20))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb376510", + "metadata": {}, + "outputs": [], "source": [] } ], @@ -426,7 +1164,15 @@ "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.10.9" } }, diff --git a/pyproject.toml b/pyproject.toml index 8e96c25..bf30674 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "hf-proxy", "llm", "tqdm>=4,<5", + "ipykernel", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index 00c6633..ed34823 100644 --- a/uv.lock +++ b/uv.lock @@ -1417,6 +1417,7 @@ source = { virtual = "." } dependencies = [ { name = "accelerate" }, { name = "hf-proxy" }, + { name = "ipykernel" }, { name = "llm" }, { name = "tqdm" }, ] @@ -1439,6 +1440,7 @@ requires-dist = [ { name = "accelerate", specifier = ">=0.26.0" }, { name = "black", marker = "extra == 'dev'", specifier = ">=24.0.0" }, { name = "hf-proxy", editable = "hf-proxy" }, + { name = "ipykernel" }, { name = "jupyter", marker = "extra == 'dev'", specifier = ">=1.0.0" }, { name = "llm", editable = "llm" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8.0" },