Мета цього п'ятого етапу дуже проста: Розробити архітектуру повного LLM. З'єднайте все разом, застосуйте всі шари та створіть усі функції для генерації тексту або перетворення тексту в ID та назад.
Ця архітектура буде використовуватися як для навчання, так і для прогнозування тексту після його навчання.
Input (Tokenized Text): Процес починається з токенізованого тексту, який перетворюється на числові представлення.
Token Embedding and Positional Embedding Layer: Токенізований текст проходить через шар вбудовування токенів та шар позиційного вбудовування, який захоплює позицію токенів у послідовності, що є критично важливим для розуміння порядку слів.
Transformer Blocks: Модель містить 12 блоків трансформера, кожен з яких має кілька шарів. Ці блоки повторюють наступну послідовність:
Masked Multi-Head Attention: Дозволяє моделі зосереджуватися на різних частинах вхідного тексту одночасно.
Layer Normalization: Крок нормалізації для стабілізації та покращення навчання.
Feed Forward Layer: Відповідає за обробку інформації з шару уваги та прогнозування наступного токена.
Dropout Layers: Ці шари запобігають перенавчанню, випадковим чином відключаючи одиниці під час навчання.
Final Output Layer: Модель виводить тензор розміром 4x50,257, де 50,257 представляє розмір словника. Кожен рядок у цьому тензорі відповідає вектору, який модель використовує для прогнозування наступного слова в послідовності.
Goal: Мета полягає в тому, щоб взяти ці вбудовування та перетворити їх назад у текст. Конкретно, останній рядок виходу використовується для генерації наступного слова, представленого як "вперед" у цій діаграмі.
Code representation
import torchimport torch.nn as nnimport tiktokenclassGELU(nn.Module):def__init__(self):super().__init__()defforward(self,x):return0.5* x * (1+ torch.tanh(torch.sqrt(torch.tensor(2.0/ torch.pi)) *(x +0.044715* torch.pow(x, 3))))classFeedForward(nn.Module):def__init__(self,cfg):super().__init__()self.layers = nn.Sequential(nn.Linear(cfg["emb_dim"], 4* cfg["emb_dim"]),GELU(),nn.Linear(4* cfg["emb_dim"], cfg["emb_dim"]),)defforward(self,x):return self.layers(x)classMultiHeadAttention(nn.Module):def__init__(self,d_in,d_out,context_length,dropout,num_heads,qkv_bias=False):super().__init__()assert d_out % num_heads ==0,"d_out must be divisible by num_heads"self.d_out = d_outself.num_heads = num_headsself.head_dim = d_out // num_heads # Reduce the projection dim to match desired output dimself.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)self.out_proj = nn.Linear(d_out, d_out)# Linear layer to combine head outputsself.dropout = nn.Dropout(dropout)self.register_buffer('mask', torch.triu(torch.ones(context_length, context_length), diagonal=1))defforward(self,x):b, num_tokens, d_in = x.shapekeys = self.W_key(x)# Shape: (b, num_tokens, d_out)queries = self.W_query(x)values = self.W_value(x)# We implicitly split the matrix by adding a `num_heads` dimension# Unroll last dim: (b, num_tokens, d_out) -> (b, num_tokens, num_heads, head_dim)keys = keys.view(b, num_tokens, self.num_heads, self.head_dim)values = values.view(b, num_tokens, self.num_heads, self.head_dim)queries = queries.view(b, num_tokens, self.num_heads, self.head_dim)# Transpose: (b, num_tokens, num_heads, head_dim) -> (b, num_heads, num_tokens, head_dim)keys = keys.transpose(1, 2)queries = queries.transpose(1, 2)values = values.transpose(1, 2)# Compute scaled dot-product attention (aka self-attention) with a causal maskattn_scores = queries @ keys.transpose(2, 3)# Dot product for each head# Original mask truncated to the number of tokens and converted to booleanmask_bool = self.mask.bool()[:num_tokens,:num_tokens]# Use the mask to fill attention scoresattn_scores.masked_fill_(mask_bool, -torch.inf)attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)attn_weights = self.dropout(attn_weights)# Shape: (b, num_tokens, num_heads, head_dim)context_vec = (attn_weights @ values).transpose(1, 2)# Combine heads, where self.d_out = self.num_heads * self.head_dimcontext_vec = context_vec.contiguous().view(b, num_tokens, self.d_out)context_vec = self.out_proj(context_vec)# optional projectionreturn context_vecclassLayerNorm(nn.Module):def__init__(self,emb_dim):super().__init__()self.eps =1e-5self.scale = nn.Parameter(torch.ones(emb_dim))self.shift = nn.Parameter(torch.zeros(emb_dim))defforward(self,x):mean = x.mean(dim=-1, keepdim=True)var = x.var(dim=-1, keepdim=True, unbiased=False)norm_x = (x - mean) / torch.sqrt(var + self.eps)return self.scale * norm_x + self.shiftclassTransformerBlock(nn.Module):def__init__(self,cfg):super().__init__()self.att =MultiHeadAttention(d_in=cfg["emb_dim"],d_out=cfg["emb_dim"],context_length=cfg["context_length"],num_heads=cfg["n_heads"],dropout=cfg["drop_rate"],qkv_bias=cfg["qkv_bias"])self.ff =FeedForward(cfg)self.norm1 =LayerNorm(cfg["emb_dim"])self.norm2 =LayerNorm(cfg["emb_dim"])self.drop_shortcut = nn.Dropout(cfg["drop_rate"])defforward(self,x):# Shortcut connection for attention blockshortcut = xx = self.norm1(x)x = self.att(x)# Shape [batch_size, num_tokens, emb_size]x = self.drop_shortcut(x)x = x + shortcut # Add the original input back# Shortcut connection for feed forward blockshortcut = xx = self.norm2(x)x = self.ff(x)x = self.drop_shortcut(x)x = x + shortcut # Add the original input backreturn xclassGPTModel(nn.Module):def__init__(self,cfg):super().__init__()self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])self.drop_emb = nn.Dropout(cfg["drop_rate"])self.trf_blocks = nn.Sequential(*[TransformerBlock(cfg) for _ inrange(cfg["n_layers"])])self.final_norm =LayerNorm(cfg["emb_dim"])self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)defforward(self,in_idx):batch_size, seq_len = in_idx.shapetok_embeds = self.tok_emb(in_idx)pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device))x = tok_embeds + pos_embeds # Shape [batch_size, num_tokens, emb_size]x = self.drop_emb(x)x = self.trf_blocks(x)x = self.final_norm(x)logits = self.out_head(x)return logitsGPT_CONFIG_124M ={"vocab_size":50257,# Vocabulary size"context_length":1024,# Context length"emb_dim":768,# Embedding dimension"n_heads":12,# Number of attention heads"n_layers":12,# Number of layers"drop_rate":0.1,# Dropout rate"qkv_bias":False# Query-Key-Value bias}torch.manual_seed(123)model =GPTModel(GPT_CONFIG_124M)out =model(batch)print("Input batch:\n", batch)print("\nOutput shape:", out.shape)print(out)
GELU Активізаційна Функція
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04classGELU(nn.Module):def__init__(self):super().__init__()defforward(self,x):return0.5* x * (1+ torch.tanh(torch.sqrt(torch.tensor(2.0/ torch.pi)) *(x +0.044715* torch.pow(x, 3))))
Мета та Функціональність
GELU (Гаусова Помилка Лінійний Одиниця): Активаційна функція, яка вводить нелінійність у модель.
Плавна Активація: На відміну від ReLU, яка обнуляє негативні вхідні дані, GELU плавно відображає вхідні дані на виходи, дозволяючи невеликі, ненульові значення для негативних вхідних даних.
Математичне Визначення:
Мета використання цієї функції після лінійних шарів всередині шару FeedForward полягає в тому, щоб змінити лінійні дані на нелінійні, щоб дозволити моделі вивчати складні, нелінійні зв'язки.
FeedForward Нейронна Мережа
Форми були додані як коментарі для кращого розуміння форм матриць:
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04classFeedForward(nn.Module):def__init__(self,cfg):super().__init__()self.layers = nn.Sequential(nn.Linear(cfg["emb_dim"], 4* cfg["emb_dim"]),GELU(),nn.Linear(4* cfg["emb_dim"], cfg["emb_dim"]),)defforward(self,x):# x shape: (batch_size, seq_len, emb_dim)x = self.layers[0](x)# x shape: (batch_size, seq_len, 4 * emb_dim)x = self.layers[1](x)# x shape remains: (batch_size, seq_len, 4 * emb_dim)x = self.layers[2](x)# x shape: (batch_size, seq_len, emb_dim)return x # Output shape: (batch_size, seq_len, emb_dim)
Мета та Функціональність
Позиційна FeedForward мережа: Застосовує двошарову повністю з'єднану мережу до кожної позиції окремо та ідентично.
Деталі Шарів:
Перший Лінійний Шар: Розширює розмірність з emb_dim до 4 * emb_dim.
Активація GELU: Застосовує нелінійність.
Другий Лінійний Шар: Зменшує розмірність назад до emb_dim.
Як ви можете бачити, мережа Feed Forward використовує 3 шари. Перший - це лінійний шар, який множить розміри на 4, використовуючи лінійні ваги (параметри для навчання всередині моделі). Потім функція GELU використовується у всіх цих вимірах, щоб застосувати нелінійні варіації для захоплення багатших представлень, і нарешті, ще один лінійний шар використовується для повернення до початкового розміру вимірів.
Механізм Багатоголової Уваги
Це вже було пояснено в попередньому розділі.
Мета та Функціональність
Багатоголова Самоувага: Дозволяє моделі зосереджуватися на різних позиціях у вхідній послідовності під час кодування токена.
Ключові Компоненти:
Запити, Ключі, Значення: Лінійні проекції вхідних даних, які використовуються для обчислення оцінок уваги.
Голови: Кілька механізмів уваги, що працюють паралельно (num_heads), кожен з зменшеною розмірністю (head_dim).
Оцінки Уваги: Обчислюються як скалярний добуток запитів і ключів, масштабованих і замаскованих.
Маскування: Застосовується каузальна маска, щоб запобігти моделі звертатися до майбутніх токенів (важливо для авторегресивних моделей, таких як GPT).
Ваги Уваги: Softmax замаскованих і масштабованих оцінок уваги.
Контекстний Вектор: Вагова сума значень відповідно до ваг уваги.
Вихідна Проекція: Лінійний шар для об'єднання виходів усіх голів.
Мета цієї мережі - знайти відносини між токенами в одному контексті. Більше того, токени діляться на різні голови, щоб запобігти перенавчанню, хоча фінальні відносини, знайдені для кожної голови, об'єднуються в кінці цієї мережі.
Крім того, під час навчання застосовується каузальна маска, щоб пізні токени не враховувалися при пошуку специфічних відносин до токена, і також застосовується деякий dropout для запобігання перенавчанню.
Нормалізація Шарів
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04classLayerNorm(nn.Module):def__init__(self,emb_dim):super().__init__()self.eps =1e-5# Prevent division by zero during normalization.self.scale = nn.Parameter(torch.ones(emb_dim))self.shift = nn.Parameter(torch.zeros(emb_dim))defforward(self,x):mean = x.mean(dim=-1, keepdim=True)var = x.var(dim=-1, keepdim=True, unbiased=False)norm_x = (x - mean) / torch.sqrt(var + self.eps)return self.scale * norm_x + self.shift
Мета та Функціональність
Layer Normalization: Техніка, що використовується для нормалізації вхідних даних по ознаках (вимірах вбудовування) для кожного окремого прикладу в партії.
Компоненти:
eps: Маленька константа (1e-5), що додається до дисперсії, щоб запобігти діленню на нуль під час нормалізації.
scale та shift: Навчальні параметри (nn.Parameter), які дозволяють моделі масштабувати та зміщувати нормалізований вихід. Вони ініціалізуються одиницями та нулями відповідно.
Процес Нормалізації:
Обчислення Середнього (mean): Обчислює середнє значення вхідного x по виміру вбудовування (dim=-1), зберігаючи вимір для трансляції (keepdim=True).
Обчислення Дисперсії (var): Обчислює дисперсію x по виміру вбудовування, також зберігаючи вимір. Параметр unbiased=False забезпечує обчислення дисперсії за допомогою упередженого оцінювача (ділення на N замість N-1), що є доречним при нормалізації по ознакам, а не зразкам.
Нормалізація (norm_x): Віднімає середнє від x і ділить на квадратний корінь з дисперсії плюс eps.
Масштабування та Зміщення: Застосовує навчальні параметри scale та shift до нормалізованого виходу.
Мета полягає в забезпеченні середнього значення 0 з дисперсією 1 по всіх вимірах одного й того ж токена. Мета цього - стабілізувати навчання глибоких нейронних мереж шляхом зменшення внутрішнього зсуву коваріат, що відноситься до зміни розподілу активацій мережі через оновлення параметрів під час навчання.
Transformer Block
Форми були додані як коментарі для кращого розуміння форм матриць:
Додати Залишок (x + shortcut): Об'єднати з входом з першого залишкового шляху.
Блок трансформера об'єднує всі мережі разом і застосовує деяку нормалізацію та випадкові випадання для покращення стабільності навчання та результатів.
Зверніть увагу, що випадкові випадання виконуються після використання кожної мережі, тоді як нормалізація застосовується перед.
Крім того, він також використовує швидкі з'єднання, які полягають у додаванні виходу мережі до її входу. Це допомагає запобігти проблемі зникнення градієнта, забезпечуючи, щоб початкові шари вносили "стільки ж", скільки останні.
GPTModel
Форми були додані як коментарі для кращого розуміння форм матриць:
Token Embeddings (tok_emb): Перетворює індекси токенів на embeddings. Нагадаємо, це ваги, які надаються кожному виміру кожного токена в словнику.
Positional Embeddings (pos_emb): Додає позиційну інформацію до embeddings, щоб зафіксувати порядок токенів. Нагадаємо, це ваги, які надаються токену відповідно до його позиції в тексті.
Dropout (drop_emb): Застосовується до embeddings для регуляризації.
Transformer Blocks (trf_blocks): Стек з n_layers трансформерних блоків для обробки embeddings.
Final Normalization (final_norm): Нормалізація шару перед вихідним шаром.
Output Layer (out_head): Проектує фінальні приховані стани на розмір словника для отримання логітів для прогнозування.
Мета цього класу полягає в тому, щоб використовувати всі інші згадані мережі для прогнозування наступного токена в послідовності, що є основоположним для завдань, таких як генерація тексту.
Зверніть увагу, як він використовуватиме стільки трансформерних блоків, скільки вказано, і що кожен трансформерний блок використовує одну мережу з багатоголовим увагою, одну мережу прямого проходження та кілька нормалізацій. Тож, якщо використовується 12 трансформерних блоків, помножте це на 12.
Крім того, шар нормалізації додається передвиходом, а фінальний лінійний шар застосовується в кінці, щоб отримати результати з правильними розмірами. Зверніть увагу, що кожен фінальний вектор має розмір використаного словника. Це тому, що він намагається отримати ймовірність для кожного можливого токена в словнику.
Кількість параметрів для навчання
Маючи визначену структуру GPT, можна дізнатися кількість параметрів для навчання:
GPT_CONFIG_124M ={"vocab_size":50257,# Vocabulary size"context_length":1024,# Context length"emb_dim":768,# Embedding dimension"n_heads":12,# Number of attention heads"n_layers":12,# Number of layers"drop_rate":0.1,# Dropout rate"qkv_bias":False# Query-Key-Value bias}model =GPTModel(GPT_CONFIG_124M)total_params =sum(p.numel() for p in model.parameters())print(f"Total number of parameters: {total_params:,}")# Total number of parameters: 163,009,536
Покроковий Розрахунок
1. Вкладки Векторів: Векторне Вкладення та Позиційне Вкладення
Маючи модель, яка передбачає наступний токен, як і попередній, потрібно просто взяти останні значення токенів з виходу (оскільки вони будуть значеннями передбаченого токена), які будуть значенням для кожного елемента в словнику, а потім використати функцію softmax, щоб нормалізувати виміри в ймовірності, які в сумі дорівнюють 1, а потім отримати індекс найбільшого елемента, який буде індексом слова в словнику.
defgenerate_text_simple(model,idx,max_new_tokens,context_size):# idx is (batch, n_tokens) array of indices in the current contextfor _ inrange(max_new_tokens):# Crop current context if it exceeds the supported context size# E.g., if LLM supports only 5 tokens, and the context size is 10# then only the last 5 tokens are used as contextidx_cond = idx[:,-context_size:]# Get the predictionswith torch.no_grad():logits =model(idx_cond)# Focus only on the last time step# (batch, n_tokens, vocab_size) becomes (batch, vocab_size)logits = logits[:,-1,:]# Apply softmax to get probabilitiesprobas = torch.softmax(logits, dim=-1)# (batch, vocab_size)# Get the idx of the vocab entry with the highest probability valueidx_next = torch.argmax(probas, dim=-1, keepdim=True)# (batch, 1)# Append sampled index to the running sequenceidx = torch.cat((idx, idx_next), dim=1)# (batch, n_tokens+1)return idxstart_context ="Hello, I am"encoded = tokenizer.encode(start_context)print("encoded:", encoded)encoded_tensor = torch.tensor(encoded).unsqueeze(0)print("encoded_tensor.shape:", encoded_tensor.shape)model.eval()# disable dropoutout =generate_text_simple(model=model,idx=encoded_tensor,max_new_tokens=6,context_size=GPT_CONFIG_124M["context_length"])print("Output:", out)print("Output length:", len(out[0]))