Năm 2017, nhóm nghiên cứu tại Google công bố bài báo khoa học lịch sử: "Attention Is All You Need", giới thiệu kiến trúc mạng Transformer. Sự kiện này đã kết liễu thời kỳ thống trị kéo dài hàng thập kỷ của các mạng nơ-ron tuần hoàn RNN/LSTM và mở ra kỷ nguyên của các mô hình ngôn ngữ lớn (LLM) thay đổi thế giới ngày nay.

Điểm mấu chốt của Transformer là việc loại bỏ hoàn toàn các vòng lặp tuần tự theo bước thời gian, cho phép song song hóa tính toán 100% trên phần cứng đồ họa GPU. Bài học này sẽ đưa kiến trúc Transformer dưới kính hiển vi: mổ xẻ toán học của phép toán Scaled Dot-Product Self-Attention (Q, K, V), cơ chế Multi-Head Attention, mã hóa vị trí (Positional Encoding), và tự tay lắp ráp một khối Transformer Block hoàn chỉnh bằng PyTorch.

10.1 Cuộc cách mạng song song hóa: Cơ chế Self-Attention

Như chúng ta đã tìm hiểu ở Bài 9, mạng nơ-ron tuần hoàn (RNN) xử lý câu văn một cách tuần tự từng từ một. Điều này đồng nghĩa với việc nơ-ron không thể tính toán từ thứ 10 nếu chưa tính xong trạng thái ẩn của từ thứ 9.

Ngược lại, Transformer xử lý tất cả các từ trong câu văn cùng một lúc. Để bắt được ngữ cảnh liên kết giữa các từ, nó giới thiệu phép toán Self-Attention (Tự chú ý). Mỗi từ đầu vào sẽ tự động tính toán mức độ liên quan ngữ nghĩa của chính nó với tất cả các từ khác trong câu thông qua ba vector đại diện:

  • Query ($Q$ - Câu hỏi): Đại diện cho thông tin từ hiện tại đang đi tìm ngữ cảnh liên quan.
  • Key ($K$ - Từ khóa): Đại diện cho nhãn định danh đặc trưng của các từ khác trong câu để đối chiếu.
  • Value ($V$ - Giá trị): Đại diện cho nội dung thông tin thực tế của từ đó được nạp đi sau khi đã tìm thấy sự chú ý thích hợp.
📐 Công thức toán học Scaled Dot-Product Self-Attention
Với các ma trận Query $Q$, Key $K$, và Value $V$ (trong đó $d_k$ là kích thước số chiều của Key), phép toán tự chú ý được tính bằng công thức: $$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{Q K^T}{\sqrt{d_k}}\right) V$$ Giải phẫu từng bước của công thức:
  1. Nhân chập Q và K ($Q K^T$): Đo lượng điểm tương đồng (tích vô hướng) giữa tất cả các cặp từ trong câu văn. Kết quả trả về một ma trận vuông kích thước $N \times N$ (với $N$ là chiều dài chuỗi).
  2. Chia tỉ lệ cho $\sqrt{d_k}$ (Scaling factor): Khi số chiều $d_k$ rất lớn, tích vô hướng $Q K^T$ có xu hướng có trị số cực kỳ lớn, làm đẩy hàm Softmax vào vùng bão hòa có độ dốc cực nhỏ (Vanishing Gradient). Phép chia cho $\sqrt{d_k}$ giữ cho phân phối điểm số có phương sai bằng 1, giúp đạo hàm truyền ngược ổn định.
  3. Hàm kích hoạt Softmax: Chuyển các điểm tương đồng thành phân phối xác suất chú ý có giá trị từ 0 đến 1.
  4. Nhân với Value ($V$): Lấy trọng số xác suất chú ý nhân chập với nội dung thật của Value để thu về các vector ngữ cảnh tổng hợp cao cấp.
⚠️ Cạm bẫy: Độ phức tạp tính toán bình phương O(N^2)
Vì phép toán nhân ma trận vuông $Q K^T$ so sánh mọi từ với mọi từ khác trong câu văn, độ phức tạp không gian và thời gian của lớp Self-Attention tăng theo hàm mũ bình phương $\mathcal{O}(N^2)$ (với $N$ là độ dài ngữ cảnh). Đây là lý do tại sao các mô hình LLM tiêu tốn lượng VRAM khổng lồ khi bạn tăng độ dài cửa sổ chat (Context Window) và là điểm nghẽn lớn nhất của kiến trúc Transformer.
🔢 Con số thật: vì sao chia cho $\sqrt{d_k}$ ổn định được phương sai
Giả sử các thành phần của $Q$ và $K$ đều được khởi tạo ngẫu nhiên độc lập theo phân phối chuẩn $\mathcal{N}(0, 1)$. Mô phỏng thống kê thực tế (20.000 lần lấy mẫu mỗi trường hợp) cho thấy:
  • $d_k = 8$: phương sai của $Q K^T$ (chưa chia tỉ lệ) $\approx 8.09$ — gần đúng bằng $d_k$.
  • $d_k = 64$: phương sai $\approx 63.22$.
  • $d_k = 512$: phương sai $\approx 512.09$.
Đúng như lý thuyết dự đoán: phương sai của tích vô hướng $d_k$ số hạng độc lập tăng tuyến tính theo $d_k$. Sau khi chia cho $\sqrt{d_k}$, cả ba trường hợp đều đưa phương sai về xấp xỉ $1.0$ (0.988 → 1.011 → 1.0002) bất kể $d_k$ lớn hay nhỏ — đây chính là lý do phép chia tỉ lệ giữ cho phân phối điểm số ổn định, không phụ thuộc vào kích thước mô hình.

10.2 Học đa chiều: Multi-Head Attention & Mã hóa vị trí Positional Encoding

Nếu chỉ sử dụng một luồng Attention đơn lẻ, mô hình chỉ có thể tập trung chú ý vào một mối liên kết ngữ nghĩa duy nhất tại một thời điểm (ví dụ: liên kết ngữ pháp danh từ - động từ).

Để khắc phục, Transformer sử dụng cơ chế Multi-Head Attention (Chú ý đa luồng). Thay vì tính Attention trực tiếp trên các vector có số chiều gốc $d_{\text{model}}$, mạng nơ-ron chia nhỏ các chiều này thành $h$ luồng song song (mỗi luồng có số chiều nhỏ hơn là $d_k = d_{\text{model}} / h$). Mỗi luồng "Head" sẽ được khởi tạo các ma trận trọng số độc lập để tự do học các mối quan hệ ngữ cảnh khác nhau (ví dụ: Head 1 học quan hệ cú pháp chủ vị; Head 2 học quan hệ địa lý; Head 3 học quan hệ thời gian). Sau đó, kết quả từ toàn bộ các Head được ghép nối (concatenate) lại và chiếu qua một lớp Linear để trả về số chiều gốc.

Vì Transformer xử lý song song toàn bộ các từ cùng một lúc, mô hình không hề biết thứ tự từ trước sau. Với Transformer, câu văn "Tôi yêu bạn" và câu "Bạn yêu tôi" có ma trận biểu diễn hoàn toàn giống hệt nhau (xem câu văn như một túi từ xáo trộn không thứ tự - Bag of Words).

Để đưa thông tin thứ tự thời gian vào, ta bắt buộc phải cộng thêm một vector Positional Encoding (Mã hóa vị trí) trực tiếp vào vector nhúng từ (Word Embedding) trước khi đưa vào khối Attention đầu tiên.

📐 Công thức mã hóa vị trí hình sin (Sinusoidal Positional Encoding)
Trong bài báo gốc của Google, nhóm nghiên cứu sử dụng các sóng hình sin và cosin có tần số khác nhau để tự động tạo ra một vector mã hóa vị trí duy nhất ứng với tọa độ $pos$ và chiều thứ $i$: $$PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{\frac{2i}{d_{\text{model}}}}}\right)$$ $$PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{\frac{2i}{d_{\text{model}}}}}\right)$$ Mối liên hệ hình sin này cho phép mô hình dễ dàng học được vị trí tương đối giữa các từ (ví dụ từ đứng cách nhau $k$ ký tự) vì $PE_{pos+k}$ có thể biểu diễn dưới dạng hàm tuyến tính của $PE_{pos}$.
⚠️ Ngộ nhận thường gặp: Multi-Head Attention KHÔNG tốn thêm tham số
Nhiều người học lầm tưởng rằng chia thành $h$ luồng Head sẽ nhân số tham số lên $h$ lần. Thực tế hoàn toàn ngược lại: 4 lớp nn.Linear ($W_q, W_k, W_v, W_o$) luôn có kích thước cố định $d_{\text{model}} \times d_{\text{model}}$ bất kể chia thành bao nhiêu Head, vì việc "chia luồng" chỉ là thao tác .view() định hình lại (reshape) tensor sẵn có, không tạo ra ma trận trọng số mới. Với $d_{\text{model}} = 64$ (đã tính và kiểm chứng bằng code), tổng tham số của 4 lớp Linear là đúng $16.640$ tham số dù dùng $1$ Head hay $8$ Head — số Head chỉ thay đổi cách bộ nhớ được "cắt lát" để tính song song nhiều góc nhìn ngữ cảnh khác nhau, chứ không làm mô hình nặng hơn.

10.3 Kiến trúc Encoder-Decoder và mô hình GPT (Decoder-only)

Kiến trúc Transformer nguyên bản gồm hai phần lớn xếp chồng lên nhau:

  • Encoder (Bộ mã hóa): Đọc toàn bộ câu gốc để trích xuất đặc trưng hai chiều (Bidirectional). Được sử dụng trong các mô hình hiểu ngôn ngữ như BERT.
  • Decoder (Bộ giải mã): Tích hợp thêm cơ chế tự hồi quy (Autoregressive) để sinh từng từ tiếp theo.

Đối với phần Decoder, khi huấn luyện sinh văn bản, chúng ta bắt buộc phải sử dụng một bộ mặt nạ gọi là Causal Mask (Mặt nạ nhân quả / Look-ahead Mask). Lớp mặt nạ này che đi toàn bộ các từ ở tương lai bằng cách ghi đè điểm số tích vô hướng $Q K^T$ tại các vị trí đó thành $-\infty$ trước khi đưa vào hàm Softmax: $$\text{Softmax}(-\infty) = 0$$ Điều này đảm bảo khi dự đoán từ tiếp theo ở bước $t$, mô hình hoàn toàn không thể "ăn gian" nhìn thấy dữ liệu ở các bước $t+1, t+2$ trong quá trình huấn luyện.

💡 Sự thống trị của mô hình Decoder-only (GPT)
Hầu hết các LLM mạnh mẽ nhất hiện nay như GPT-4, Llama, Gemini hay Claude đều loại bỏ hoàn toàn phần Encoder truyền thống và chuyển sang thiết kế Decoder-only (Chỉ sử dụng bộ giải mã). Kiến trúc này đơn giản hơn rất nhiều, dễ dàng mở rộng quy mô (scalable) và tối ưu hóa tính toán tự hồi quy để sinh chữ một cách sáng tạo và thông minh.

10.4 Dự án thực hành bài 10: Xây dựng khối Transformer Block hoàn chỉnh bằng PyTorch

Dự án thực hành của bài học này là tự tay lập trình từ đầu một khối Transformer Block hoàn chỉnh sử dụng PyTorch.

Chúng ta sẽ xây dựng lớp tự chú ý đa luồng MultiHeadAttention, kết hợp với các kết nối tắt (Residual Connections), lớp chuẩn hóa LayerNorm và mạng thần kinh truyền thẳng FeedForward để đóng gói thành khối kiến trúc hoàn chỉnh.

transformer_block.py
import torch
import torch.nn as nn
import torch.nn.functional as F
 
class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads):
        super(MultiHeadAttention, self).__init__()
        assert d_model % num_heads == 0, "d_model phải chia hết cho num_heads"
        
        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model // num_heads
        
        # Tạo các ma trận chiếu tuyến tính cho Query, Key, Value
        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, d_model)
        self.W_v = nn.Linear(d_model, d_model)
        
        # Lớp tuyến tính cuối sau khi ghép các luồng Attention
        self.W_o = nn.Linear(d_model, d_model)
        
    def forward(self, q, k, v, mask=None):
        batch_size, seq_len, _ = q.size()
        
        # 1. Chiếu tuyến tính đầu vào và phân tách thành các Heads
        # Kích thước chuyển đổi: (Batch, Seq, d_model) -> (Batch, Seq, heads, d_k) -> (Batch, heads, Seq, d_k)
        Q = self.W_q(q).view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2)
        K = self.W_k(k).view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2)
        V = self.W_v(v).view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2)
        
        # 2. Tính điểm tương đồng Scaled Dot-Product
        # Q K^T: (Batch, heads, Seq, d_k) x (Batch, heads, d_k, Seq) -> (Batch, heads, Seq, Seq)
        scores = torch.matmul(Q, K.transpose(-2, -1)) / torch.sqrt(torch.tensor(self.d_k, dtype=torch.float32))
        
        # Áp dụng mặt nạ (nếu có)
        if mask is not None:
            scores = scores.masked_fill(mask == 0, -1e9)
            
        # 3. Softmax để tạo phân phối trọng số chú ý
        attention_weights = F.softmax(scores, dim=-1)
        
        # 4. Nhân với Value và chuyển đổi lại số chiều gốc
        # (Batch, heads, Seq, Seq) x (Batch, heads, Seq, d_k) -> (Batch, heads, Seq, d_k)
        context = torch.matmul(attention_weights, V)
        
        # Ghép (Concatenate) các heads lại với nhau: (Batch, Seq, d_model)
        context = context.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model)
        
        # Lớp chiếu tuyến tính đầu ra
        output = self.W_o(context)
        return output
 
class FeedForwardNetwork(nn.Module):
    def __init__(self, d_model, d_ff):
        super(FeedForwardNetwork, self).__init__()
        self.linear1 = nn.Linear(d_model, d_ff)
        self.relu = nn.ReLU()
        self.linear2 = nn.Linear(d_ff, d_model)
        
    def forward(self, x):
        return self.linear2(self.relu(self.linear1(x)))
 
class TransformerBlock(nn.Module):
    def __init__(self, d_model, num_heads, d_ff, dropout=0.1):
        super(TransformerBlock, self).__init__()
        
        # Khối chú ý đa luồng
        self.attention = MultiHeadAttention(d_model, num_heads)
        
        # Lớp chuẩn hóa Layer Normalization
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        
        # Mạng truyền thẳng FFN
        self.feed_forward = FeedForwardNetwork(d_model, d_ff)
        
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, x, mask=None):
        # 1. Khối Self-Attention + Kết nối tắt Residual Connection + LayerNorm
        attn_out = self.attention(x, x, x, mask)
        x = self.norm1(x + self.dropout(attn_out))
        
        # 2. Khối FeedForward + Kết nối tắt Residual Connection + LayerNorm
        ff_out = self.feed_forward(x)
        x = self.norm2(x + self.dropout(ff_out))
        
        return x
 
if __name__ == "__main__":
    print("=== Khởi tạo khối Transformer Block ===")
    d_model = 64
    num_heads = 8
    d_ff = 256
    seq_len = 10
    batch_size = 2
    
    # Khởi tạo mô hình
    block = TransformerBlock(d_model=d_model, num_heads=num_heads, d_ff=d_ff)
    print(f"Tham số: d_model={d_model}, heads={num_heads}, d_ff={d_ff}")
    
    # Giả lập dữ liệu đầu vào (Batch, Seq Length, d_model)
    dummy_input = torch.randn(batch_size, seq_len, d_model)
    print(f"Kích thước tensor đầu vào: {dummy_input.shape}")
    
    # Khởi tạo mặt nạ Causal Mask giả lập cho bộ giải mã Decoder
    # Mặt nạ tam giác dưới (Lower Triangular Mask) chứa số 1, góc trên chứa số 0
    causal_mask = torch.tril(torch.ones(seq_len, seq_len)).unsqueeze(0).unsqueeze(1) # (1, 1, Seq, Seq)
    print(f"Kích thước mặt nạ Causal Mask: {causal_mask.shape}\n")
    
    # Lan truyền xuôi qua khối Transformer Block
    output = block(dummy_input, mask=causal_mask)
    print("=== Chạy Lan truyền xuôi qua mô hình thành công ===")
    print(f"Kích thước tensor đầu ra: {output.shape} (Bằng chính xác kích thước đầu vào)")
    print(f"Kiến trúc khối Transformer Block đã hoạt động đồng bộ.")
💡 Khác biệt giữa Batch Normalization và Layer Normalization
Trong khi mạng CNN sử dụng Batch Normalization (chuẩn hóa dữ liệu theo chiều dọc lô dữ liệu), Transformer lại sử dụng Layer Normalization (LayerNorm) (chuẩn hóa độc lập dữ liệu theo từng mẫu riêng biệt dọc theo chiều số đặc trưng). LayerNorm tỏ ra vượt trội hoàn toàn khi xử lý dữ liệu chuỗi có độ dài thay đổi liên tục, giúp ổn định hóa trị số kích hoạt của mạng cực kỳ tốt.

Tóm tắt bài học & Cầu nối kiến thức

🔑 Bài học đạt được:
  • Đạt được: Giải phẫu chi tiết cơ chế Self-Attention và Multi-Head Attention trong Transformer.
  • Đạt được: Hiểu rõ cấu trúc song song hóa khối Encoder-Decoder làm nền móng cho các siêu mô hình ngôn ngữ lớn (LLM).

Cầu nối bài tiếp theo: Sau khi đã hiểu cấu tạo phần cứng bên trong LLM, chúng ta sẽ bắt đầu học cách lập trình tương tác với các LLM đã được huấn luyện thông qua API và các kỹ thuật Prompt Engineering ở Bài số 11.

Tải file code thực hành minh họa bài học

File Python transformer_block.py — mã nguồn triển khai lớp chú ý đa luồng MultiHeadAttention và khối TransformerBlock (chạy python transformer_block.py, yêu cầu cài đặt thư viện torch):

Tải về transformer_block.py

📖 Tài liệu tham khảo

Bài viết liên quan trong series

Bài 9: Mạng tuần hoàn (RNN) & Attention Bài 11: Lập trình Prompt & Làm chủ API LLM Quay lại Lộ trình Kỹ Sư AI Thực Chiến

Bình luận