Files
llm-arch-research/notebooks/gpt_analysis.ipynb
Sergey Penkovsky f060497eb1 docs: add analysis notebooks for BPE and GPT
- Add bpe.ipynb with Byte Pair Encoding implementation analysis
- Update gpt_analysis.ipynb with GPT model experiments and visualizations
2025-10-05 08:23:09 +03:00

436 lines
28 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"cells": [
{
"cell_type": "markdown",
"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."
]
},
{
"cell_type": "markdown",
"id": "d763e797",
"metadata": {},
"source": []
},
{
"cell_type": "markdown",
"id": "6ed35205",
"metadata": {},
"source": [
"## BPE Tokenizator"
]
},
{
"cell_type": "code",
"execution_count": null,
"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'"
]
}
],
"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",
"\n",
"class GPT(BaseModel):\n",
" \"\"\"GPT-like трансформер для генерации текста\n",
" \n",
" Args:\n",
" vocab_size: Размер словаря\n",
" max_seq_len: Макс. длина последовательности\n",
" emb_size: Размерность эмбеддингов\n",
" num_heads: Количество голов внимания\n",
" head_size: Размерность голов внимания\n",
" num_layers: Количество слоёв декодера\n",
" dropout: Вероятность dropout (default=0.1)\n",
" device: Устройство (default='cpu')\n",
" \"\"\"\n",
" def __init__(self, config):\n",
" super().__init__(config)\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",
" )\n",
" self._position_embeddings = PositionalEmbeddings(\n",
" max_seq_len=config[\"max_position_embeddings\"], \n",
" emb_size=config[\"embed_dim\"]\n",
" )\n",
" self._dropout = nn.Dropout(config[\"dropout\"])\n",
" # head_size = emb_size // num_heads\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",
"\n",
" def forward(self, x: torch.Tensor, attention_mask=None) -> torch.Tensor:\n",
" \"\"\"Прямой проход через GPT\n",
" \n",
" Args:\n",
" x: Входной тензор [batch_size, seq_len]\n",
" \n",
" Returns:\n",
" Тензор логитов [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",
" \n",
" # Эмбеддинги токенов и позиций\n",
" tok_out = self._token_embeddings(x) # [batch, seq_len, emb_size]\n",
" pos_out = self._position_embeddings(x.size(1)) # [seq_len, emb_size]\n",
" \n",
" # Комбинирование\n",
" out = self._dropout(tok_out + pos_out.unsqueeze(0)) # [batch, seq_len, emb_size]\n",
" \n",
" # Стек декодеров\n",
" for decoder in self._decoders:\n",
" out = decoder(out)\n",
" \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",
" for _ in range(max_new_tokens):\n",
" # 1. Обрезаем вход, если последовательность слишком длинная\n",
" x_cond = x[:, -self._max_seq_len:]\n",
"\n",
" # 2. Передаем последовательность в метод forward класса GPT и полуаем логиты.\n",
" logits = self.forward(x_cond)\n",
"\n",
" # 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",
"\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"
]
},
{
"cell_type": "markdown",
"id": "303c8f8c",
"metadata": {},
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.10.9"
}
},
"nbformat": 4,
"nbformat_minor": 5
}