Cilj ove pete faze je vrlo jednostavan: Razviti arhitekturu celog LLM-a. Spojiti sve zajedno, primeniti sve slojeve i kreirati sve funkcije za generisanje teksta ili transformaciju teksta u ID-ove i obrnuto.
Ova arhitektura će se koristiti za obuku i predikciju teksta nakon što je obučena.
Ulaz (Tokenizovani Tekst): Proces počinje sa tokenizovanim tekstom, koji se konvertuje u numeričke reprezentacije.
Sloj Token Embedding i Sloj Pozicionog Embedding-a: Tokenizovani tekst prolazi kroz sloj token embedding-a i sloj pozicionog embedding-a, koji hvata poziciju tokena u sekvenci, što je ključno za razumevanje reda reči.
Transformer Blokovi: Model sadrži 12 transformer blokova, svaki sa više slojeva. Ovi blokovi ponavljaju sledeću sekvencu:
Maskirana Multi-Head Pažnja: Omogućava modelu da se fokusira na različite delove ulaznog teksta odjednom.
Normalizacija Slojeva: Korak normalizacije za stabilizaciju i poboljšanje obuke.
Feed Forward Sloj: Odgovoran za obradu informacija iz sloja pažnje i pravljenje predikcija o sledećem tokenu.
Dropout Slojevi: Ovi slojevi sprečavaju prekomerno prilagođavanje tako što nasumično isključuju jedinice tokom obuke.
Završni Izlazni Sloj: Model izlazi sa 4x50,257-dimenzionalnim tenzorom, gde 50,257 predstavlja veličinu rečnika. Svaki red u ovom tenzoru odgovara vektoru koji model koristi za predikciju sledeće reči u sekvenci.
Cilj: Cilj je uzeti ove embedding-e i konvertovati ih nazad u tekst. Konkretno, poslednji red izlaza se koristi za generisanje sledeće reči, predstavljene kao "napred" u ovoj dijagramu.
Reprezentacija Koda
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)
Hajde da objasnimo korak po korak:
GELU Aktivacijska Funkcija
# 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))))
Svrha i Funkcionalnost
GELU (Gaussian Error Linear Unit): Aktivacijska funkcija koja uvodi nelinearnost u model.
Gladka Aktivacija: Za razliku od ReLU, koja nula negativne ulaze, GELU glatko mapira ulaze na izlaze, omogućavajući male, nenulte vrednosti za negativne ulaze.
Matematička Definicija:
Cilj korišćenja ove funkcije nakon linearnih slojeva unutar FeedForward sloja je da se promeni linearni podaci u nelinearne kako bi se modelu omogućilo učenje složenih, nelinearnih odnosa.
FeedForward Neuronska Mreža
Oblici su dodati kao komentari kako bi se bolje razumele forme matrica:
# 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)
Svrha i Funkcionalnost
Mreža sa Prolazom po Pozicijama: Primena dvoslojne potpuno povezane mreže na svaku poziciju odvojeno i identično.
Detalji Slojeva:
Prvi Linearni Sloj: Proširuje dimenzionalnost sa emb_dim na 4 * emb_dim.
GELU Aktivacija: Primena nelinearnosti.
Drugi Linearni Sloj: Smanjuje dimenzionalnost nazad na emb_dim.
Kao što možete videti, mreža sa prolazom koristi 3 sloja. Prvi je linearni sloj koji će pomnožiti dimenzije sa 4 koristeći linearne težine (parametre za obučavanje unutar modela). Zatim, GELU funkcija se koristi u svim tim dimenzijama da primeni nelinearne varijacije kako bi uhvatila bogatije reprezentacije, a na kraju se koristi još jedan linearni sloj da se vrati na originalnu veličinu dimenzija.
Mehanizam Višestruke Glave Pažnje
Ovo je već objašnjeno u ranijem odeljku.
Svrha i Funkcionalnost
Višestruka Samo-Pažnja: Omogućava modelu da se fokusira na različite pozicije unutar ulazne sekvence prilikom kodiranja tokena.
Glave: Više mehanizama pažnje koji rade paralelno (num_heads), svaki sa smanjenom dimenzijom (head_dim).
Ocene Pažnje: Izračunate kao skalarni proizvod upita i ključeva, skalirane i maskirane.
Maskiranje: Primena uzročnog maskiranja kako bi se sprečilo da model obraća pažnju na buduće tokene (važan za autoregresivne modele poput GPT).
Težine Pažnje: Softmax maskiranih i skaliranih ocena pažnje.
Vektor Konteksta: Težinski zbir vrednosti, prema težinama pažnje.
Izlazna Projekcija: Linearni sloj za kombinovanje izlaza svih glava.
Cilj ove mreže je da pronađe odnose između tokena u istom kontekstu. Štaviše, tokeni su podeljeni u različite glave kako bi se sprečilo prekomerno prilagođavanje, iako se konačni odnosi pronađeni po glavi kombinuju na kraju ove mreže.
Takođe, tokom obuke se primenjuje uzročno maskiranje kako kasniji tokeni ne bi bili uzeti u obzir prilikom gledanja specifičnih odnosa sa tokenom, a takođe se primenjuje i neki dropout da se spreči prekomerno prilagođavanje.
Normalizacija Sloja
# 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
Svrha i Funkcionalnost
Normalizacija slojeva: Tehnika koja se koristi za normalizaciju ulaza preko karakteristika (dimenzije ugrađivanja) za svaki pojedinačni primer u seriji.
Komponente:
eps: Mala konstanta (1e-5) koja se dodaje varijansi kako bi se sprečila deljenje sa nulom tokom normalizacije.
scale i shift: Parametri koji se mogu učiti (nn.Parameter) koji omogućavaju modelu da skalira i pomera normalizovani izlaz. Inicijalizovani su na jedinice i nule, redom.
Proces normalizacije:
Izračunaj Srednju Vrednost (mean): Izračunava srednju vrednost ulaza x preko dimenzije ugrađivanja (dim=-1), zadržavajući dimenziju za emitovanje (keepdim=True).
Izračunaj Varijansu (var): Izračunava varijansu x preko dimenzije ugrađivanja, takođe zadržavajući dimenziju. Parametar unbiased=False osigurava da se varijansa izračunava koristeći pristrasnog procenjivača (deljenje sa N umesto N-1), što je prikladno kada se normalizuje preko karakteristika umesto uzoraka.
Normalizuj (norm_x): Oduzima srednju vrednost od x i deli sa kvadratnim korenom varijanse plus eps.
Skaliraj i Pomeri: Primena parametara scale i shift koji se mogu učiti na normalizovani izlaz.
Cilj je osigurati srednju vrednost 0 sa varijansom 1 preko svih dimenzija istog tokena. Cilj ovoga je da stabilizuje obuku dubokih neuronskih mreža smanjenjem unutrašnjeg pomeranja kovarijate, što se odnosi na promenu u distribuciji aktivacija mreže zbog ažuriranja parametara tokom obuke.
Transformer Blok
Oblici su dodati kao komentari kako bi se bolje razumele forme matrica:
Dodaj Rezidual (x + shortcut): Kombinuj sa ulazom iz prvog rezidualnog puta.
Transformator blok grupiše sve mreže zajedno i primenjuje neku normalizaciju i dropout kako bi poboljšao stabilnost i rezultate treniranja.
Obratite pažnju na to kako se dropout primenjuje nakon korišćenja svake mreže dok se normalizacija primenjuje pre.
Pored toga, koristi i prečice koje se sastoje od dodavanja izlaza mreže sa njenim ulazom. Ovo pomaže u sprečavanju problema nestajućeg gradijenta tako što osigurava da inicijalni slojevi doprinose "onoliko" koliko i poslednji.
GPTModel
Oblici su dodati kao komentari kako bi se bolje razumele dimenzije matrica:
Token Ugradnje (tok_emb): Konvertuje indekse tokena u ugradnje. Kao podsetnik, ovo su težine date svakoj dimenziji svakog tokena u rečniku.
Pozicijske Ugradnje (pos_emb): Dodaje pozicione informacije u ugradnje kako bi uhvatio redosled tokena. Kao podsetnik, ovo su težine date tokenu prema njegovoj poziciji u tekstu.
Dropout (drop_emb): Primena na ugradnje za regularizaciju.
Transformer Blokovi (trf_blocks): Stek od n_layers transformer blokova za obradu ugradnji.
Finalna Normalizacija (final_norm): Normalizacija sloja pre izlaznog sloja.
Izlazni Sloj (out_head): Projektuje konačne skrivene state-ove na veličinu rečnika kako bi proizveo logite za predikciju.
Cilj ove klase je da koristi sve ostale pomenute mreže da predvidi sledeći token u sekvenci, što je fundamentalno za zadatke poput generisanja teksta.
Obratite pažnju kako će koristiti onoliko transformer blokova koliko je naznačeno i da svaki transformer blok koristi jednu mrežu sa više glava, jednu mrežu za napredovanje i nekoliko normalizacija. Dakle, ako se koristi 12 transformer blokova, pomnožite ovo sa 12.
Štaviše, normalizacija sloj se dodaje preizlaza i konačni linearni sloj se primenjuje na kraju kako bi se dobili rezultati sa odgovarajućim dimenzijama. Obratite pažnju kako svaki konačni vektor ima veličinu korišćenog rečnika. To je zato što pokušava da dobije verovatnoću po mogućem tokenu unutar rečnika.
Broj Parametara za obuku
Imajući definisanu GPT strukturu, moguće je saznati broj parametara za obuku:
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
Postupak Izračunavanja
1. Ugradne Slojeve: Ugradnja Tokena i Ugradnja Pozicije
Imajući model koji predviđa sledeći token kao prethodni, potrebno je uzeti poslednje vrednosti tokena iz izlaza (jer će to biti vrednosti predviđenog tokena), što će biti vrednost po unosu u rečniku i zatim koristiti softmax funkciju da normalizuje dimenzije u verovatnoće koje se sabiraju na 1 i zatim dobiti indeks najvećeg unosa, koji će biti indeks reči unutar rečnika.
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]))