File Jupyter notebook của bài viết này nằm tại đây
1. Tổng quan
GPT-2 với số lượng tham số chỉ 124 triệu sẽ phù hợp để chạy trên các máy tính cá nhân có cấu hình vừa phải.
Những mô hình lớn hơn như GPT-3, GPT-4 ... sẽ cần thời gian huấn luyện hàng năm, thậm chí hàng chục, hàng trăm năm trên những chiếc laptop bình dân.
Hơn nữa, các tham số mô hình này cũng đã được OpenAI công bố thay vì giấu kín như các phiên bản tiếp theo.
Chúng ta cùng xem qua thông số của mô hình GPT-2
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
}
vocab_size
: Số lượng từ vựng là50257
context_length
: Số lượng token tối đa mà mô hình có thể xử lý trong 1 lầnemb_dim
: Số chiều vector inputs embeddingsn_heads
: Số lượng head trong khi sử dụng cơ chế multi-head attentionn_layers
: Số lượng decoderdrop_rate
: Tỷ lệ dropout bao nhiêu %qkv_bias
: Dạng boolean, mang ý nghĩa rằng có sử dụng tham số bias trong quá trình tính các ma trậnQ, K, V
hay không ?
Chúng ta tạo bộ khung dự án với 3 class sau đây:
import torch
import torch.nn as nn class DummyGPTModel(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"]) # Xử lý dữ liệu trong TransformerBlock self.trf_blocks = nn.Sequential( *[DummyTransformerBlock(cfg) for _ in range(cfg["n_layers"])]) # Chạy qua lớp chuẩn hóa LayerNorm self.final_norm = DummyLayerNorm(cfg["emb_dim"]) self.out_head = nn.Linear( cfg["emb_dim"], cfg["vocab_size"], bias=False ) def forward(self, in_idx): batch_size, seq_len = in_idx.shape tok_embeds = self.tok_emb(in_idx) pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device)) x = tok_embeds + pos_embeds x = self.drop_emb(x) x = self.trf_blocks(x) x = self.final_norm(x) logits = self.out_head(x) return logits # TODO
class DummyTransformerBlock(nn.Module): def __init__(self, cfg): super().__init__() def forward(self, x): return x # TODO
class DummyLayerNorm(nn.Module): def __init__(self, normalized_shape, eps=1e-5): super().__init__() def forward(self, x): return x
Ở các mục sau, chúng ta sẽ khám phá dần dần các mục sau:
2. Lớp chuẩn hóa (layer normalization)
Layer Normalization là một kỹ thuật nhằm giảm thiểu sự phân tán của các giá trị trong mô hình, giúp tăng tốc độ hội tụ và cải thiện hiệu suất.
Ý tưởng chính của Layer Normalization là điều chỉnh các giá trị đầu ra sao cho có trung bình bằng 0 và phương sai bằng 1 (được gọi là unit variance).
Trong GPT-2 và các kiến trúc Transformer hiện đại, Layer Normalization thường được áp dụng trước và sau module Multi-Head Attention.
Công thức chuẩn hóa:
- : Đầu ra sau khi áp dụng Layer Normalization cho mẫu 𝑖 i
- μ là trung bình
- là phương sai
- ϵ là một giá trị rất nhỏ để tránh chia cho 0. Trong pytorch, giá trị mặc định của
ϵ
là - γ: Tham số tỷ lệ (scale), điều chỉnh được trong quá trình huấn luyện.
- β: Tham số dịch chuyển (shift), điều chỉnh được trong quá trình huấn luyện.
class LayerNorm(nn.Module): def __init__(self, emb_dim): super().__init__() self.eps = 1e-5 self.scale = nn.Parameter(torch.ones(emb_dim)) self.shift = nn.Parameter(torch.zeros(emb_dim)) def forward(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
Tại sao cần có 𝛾
và 𝛽
?
Nếu chỉ đơn thuần cần chuẩn hóa trung bình bằng 0 và phương sai bằng 1 thì sẽ chẳng cần đến 2 giá trị trên. Tuy nhiên, một số mô hình nhất định có thể cần giá trị lớn hơn hoặc nhỏ hơn để hoạt động hiệu quả.
3. Mạng thần kinh truyền thẳng với hàm kích hoạt GELU (Feed forward network with GELU activations)
Feed forward network
Feed Forward Network (FFN) là một mạng nơ-ron đơn giản giúp mô hình học các đặc trưng phi tuyến tính từ dữ liệu.
Đặc trưng phi tuyến tính là những mối quan hệ phức tạp trong dữ liệu mà không thể biểu diễn dưới dạng tuyến tính. Chẳng hạn, nghĩa của một từ phụ thuộc vào ngữ cảnh, không chỉ đơn thuần là một phép cộng/trừ vector.
FNN có 3 phần bao gồm 2 lớp tuyến tính (Linear) và một hàm kích hoạt (activation function).
- Lớp tuyến tính mở rộng (expansion layer): Biến đổi vector đầu vào từ kích thước
d_model
thànhd_ff
(thườngd_ff = 4 × d_model
) - Lớp tuyến tính thu hẹp (projection layer): Biến đổi vector từ kích thước
d_ff
trở lạid_model
- Hàm kích hoạt (activation function): Đảm nhận nhiệm vụ chính của FNN là học các đặc trưng phi tuyến từ dữ liệu.
Công thức:
- x là vector đầu vào
- W₁, W₂ là ma trận trọng số
- b₁, b₂ là vector bias
- Activation hàm kích hoạt
- Lớp tuyến tính thứ nhất:
- Lớp tuyến tính thứ hai: với A là kết quả trả về từ hàm kích hoạt
class FeedForward(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"]), ) def forward(self, x): return self.layers(x)
Hàm kích hoạt GELU (Gaussian Error Linear Unit)
GELU là một hàm kích hoạt phi tuyến được giới thiệu vào năm 2016 trong bài báo Gaussian Error Linear Units (GELUs) của Dan Hendrycks và Kevin Gimpel. Hàm này đã trở thành lựa chọn phổ biến trong các mô hình ngôn ngữ lớn hiện đại như BERT, GPT ...
GELU được định nghĩa bằng công thức:
- x là đầu vào
- là hàm phân phối tích lũy của phân phối chuẩn (Gaussian CDF)
Tính toán là một công việc rất tốn kém, cho nên thường dùng công thức xấp xỉ sau:
class GELU(nn.Module): def __init__(self): super().__init__() def forward(self, x): return 0.5 * x * (1 + torch.tanh( torch.sqrt(torch.tensor(2.0 / torch.pi)) * (x + 0.044715 * torch.pow(x, 3)) ))
Biểu diễn GELU trên đồ thị, dễ dàng nhận ra nó là dạng phi tuyến tính.
4. Shortcut connections
Shortcut connection (hay skip connections hoặc residual connections) về cơ bản là một kỹ thuật cho phép "chập" đầu vào với đầu ra. Xem hình minh họa phía dưới sẽ hình dung ra được ngay
Các mô hình ngôn ngữ lớn thường sử dụng nó cho các khối Multi-head Attention và Feed Forward Network.
Với các mô hình có rất nhiều lớp, Shortcut connections giúp thông tin từ các lớp trước được truyền tới các ở xa phía sau => Từ đó giúp mô hình duy trì thông tin ngữ cảnh một cách xuyên suốt.
Ví dụ:
class ExampleDeepNeuralNetwork(nn.Module): def __init__(self, layer_sizes, use_shortcut): super().__init__() self.use_shortcut = use_shortcut # Ví dụ với 5 layer self.layers = nn.ModuleList([ nn.Sequential(nn.Linear(layer_sizes[0], layer_sizes[1]), GELU()), nn.Sequential(nn.Linear(layer_sizes[1], layer_sizes[2]), GELU()), nn.Sequential(nn.Linear(layer_sizes[2], layer_sizes[3]), GELU()), nn.Sequential(nn.Linear(layer_sizes[3], layer_sizes[4]), GELU()), nn.Sequential(nn.Linear(layer_sizes[4], layer_sizes[5]), GELU()) ]) def forward(self, x): for layer in self.layers: # Tính đầu ra của layer hiện tại layer_output = layer(x) # Nếu use_shorcut=true thì áp dụng if self.use_shortcut and x.shape == layer_output.shape: x = x + layer_output else: x = layer_output return x # In ra gradient trung bình của các lớp
def print_gradients(model, x): output = model(x) target = torch.tensor([[0.]]) loss = nn.MSELoss() loss = loss(output, target) loss.backward() for name, param in model.named_parameters(): if 'weight' in name: print(f"{name} has gradient mean of {param.grad.abs().mean().item()}")
- Khi không sử dụng shortcut connections
layer_sizes = [3, 3, 3, 3, 3, 1] sample_input = torch.tensor([[1., 0., -1.]]) torch.manual_seed(123)
model_without_shortcut = ExampleDeepNeuralNetwork( layer_sizes, use_shortcut=False
)
print_gradients(model_without_shortcut, sample_input) # layers.0.0.weight has gradient mean of 0.00020173587836325169
# layers.1.0.weight has gradient mean of 0.00012011159560643137
# layers.2.0.weight has gradient mean of 0.0007152039906941354
# layers.3.0.weight has gradient mean of 0.0013988736318424344
# layers.4.0.weight has gradient mean of 0.005049645435065031
- Khi sử dụng shortcut connections
torch.manual_seed(123)
model_with_shortcut = ExampleDeepNeuralNetwork( layer_sizes, use_shortcut=True
)
print_gradients(model_with_shortcut, sample_input) #layers.0.0.weight has gradient mean of 0.22169792652130127
#layers.1.0.weight has gradient mean of 0.20694106817245483
#layers.2.0.weight has gradient mean of 0.32896995544433594
#layers.3.0.weight has gradient mean of 0.2665732204914093
#layers.4.0.weight has gradient mean of 1.3258540630340576
- Từ 2 kết quả in ra ở trên, ta thấy khi sử dụng shortcut connections giữ được Gradient ở các lớp đầu không bị quá bé (tránh Gradient vanishing)
Gradient vanishing (sự mất mát gradient) là một hiện tượng trong quá trình huấn luyện mạng neural sâu (deep neural network), khi gradient trở nên cực kỳ nhỏ ở các lớp đầu.
5. Luồng xử lý của block Transformer
Bây giờ là lúc chúng ta xem luồng hoạt động trong block Transformer khi đã tìm hiểu qua hết các khái niệm như multi-head attention, dropout, feed forward ...
# If the `previous_chapters.py` file is not available locally,
# you can import it from the `llms-from-scratch` PyPI package.
# For details, see: https://github.com/rasbt/LLMs-from-scratch/tree/main/pkg
# E.g.,
# from llms_from_scratch.ch03 import MultiHeadAttention from previous_chapters import MultiHeadAttention class TransformerBlock(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"]) # Thứ tự các bước xử lý # 1. Chuẩn hóa 1 # 2. Multi-head attention # 3. Dropout # 4. Shortcut connections # 5. Chuẩn hóa 2 # 6. Feed forward # 7. Dropout # 8. Shortcut connections def forward(self, x): # Shortcut connection for attention block shortcut = x x = 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 block shortcut = x x = self.norm2(x) x = self.ff(x) x = self.drop_shortcut(x) x = x + shortcut # Add the original input back return x
6. Mô hình GPT
- GPT dựa trên kiến trúc Transformer nhưng có bổ sung thêm một số thành phần.
- GPT-2 có 12 khối Transformer.
- Linear Layer Output được sử dụng để chuyển đổi đầu ra thành các giá trị thô (logits). Từ đó biến đổi tiếp thành dạng vector xác suất cho các từ trong tệp từ vựng (vocabulary).
# Giá trị `cfg["n_layers"] = 12`
class GPTModel(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 _ in range(cfg["n_layers"])]) self.final_norm = LayerNorm(cfg["emb_dim"]) self.out_head = nn.Linear( cfg["emb_dim"], cfg["vocab_size"], bias=False ) def forward(self, in_idx): batch_size, seq_len = in_idx.shape tok_embeds = self.tok_emb(in_idx) pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device)) x = tok_embeds + pos_embeds # Kích thước tensor: [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 logits
7. Sinh văn bản
Từ kết quả vector đầu ra của mô hình GPT như ở phần trên, chúng ta vẫn cần một số bước xử lý nữa để có được dạng văn bản đầu ra. Hãy cùng xem qua cách mô hình sinh ra từ tiếp theo với đầu vào là chuỗi Hello, I am.
- Vector ở hàng cuối cùng từ đầu ra chứa suất về từ tiếp theo cần xuất hiện.
- Chuẩn hóa vector với hàm
Softmax
, ta thu được xác xuất dưới dạng % - Phần tử có xác suất lớn nhất sẽ là từ tiếp theo
- Chuyển đổi về dạng token ID rồi dạng văn bản, ta thu được từ tiếp theo là "a"
Qua mỗi lần lặp, các từ sẽ lần lượt được sinh ra thành 1 câu hoàn chỉnh và có nghĩa
def generate_text_simple(model, idx, max_new_tokens, context_size): # Tất nhiên mô hình không thể cứ sinh văn bản vô hạn mà có giới hạn bằng max_new_tokens for _ in range(max_new_tokens): idx_cond = idx[:, -context_size:] # Đầu ra của GPT dạng logits with torch.no_grad(): logits = model(idx_cond) # Lấy giá trị vector cuối cùng logits = logits[:, -1, :] # chạy softmax probas = torch.softmax(logits, dim=-1) # Tham chiếu giá trị xác suất lớn nhất ra tokenID idx_next = torch.argmax(probas, dim=-1, keepdim=True) # Tham tokenID vào input để chạy các vòng tiếp theo idx = torch.cat((idx, idx_next), dim=1) # (batch, n_tokens+1) return idx
Tổng kết
Chúng ta đã đi qua hoàn chỉnh việc 1 mô hình GPT xử lý văn bản như thế nào, Transformer biến đổi ra sao ..., code đầy đủ đã được tác giả Sebastian Raschka đẩy lên ở file gpt.py
Chạy file với đầu vào là chuỗi Hello, I am
, ta thu được kết quả đầu ra như sau:
================================================== IN
================================================== Input text: Hello, I am
Encoded input text: [15496, 11, 314, 716]
encoded_tensor.shape: torch.Size([1, 4]) ================================================== OUT
================================================== Output: tensor([[15496, 11, 314, 716, 27018, 24086, 47843, 30961, 42348, 7267, 49706, 43231, 47062, 34657]])
Output length: 14
Output text: Hello, I am Featureiman Byeswickattribute argue logger Normandy Compton analogous
Đầu ra của mô hình là một chuỗi khá vô nghĩa Hello, I am Featureiman Byeswickattribute argue logger Normandy Compton analogous, tại sao vậy 🫣 ?
Lý do rằng mô hình chưa hề được đào tạo, các tham số của mô hình đang được tạo ngẫu nhiên mà thôi. Do đó, chúng ta sẽ còn gặp nhau ở các bài tiếp theo để xem cách một mô hình ngôn ngữ lớn được đào tạo và tinh chỉnh như thế nào, sao cho đầu ra được tự nhiên và mạch lạc hơn.