Chương 3·Bài 3.9

Attention và Transformer

Trong câu “Con mèo không nhảy lên bàn vì nó quá cao”, từ “nó” chỉ cái bàn; đổi “cao” thành “mệt” thì “nó” chỉ con mèo. Nghĩa của một từ phụ thuộc vào các từ khác trong câu, và từ nào quan trọng thì thay đổi theo từng câu. Mean pooling ở bài trước lấy trung bình đều mọi từ; attention cho mỗi từ tự chọn lấy thông tin từ những từ nào, với tỉ lệ bao nhiêu. Bài này tính attention bằng tay trên ví dụ nhỏ, rồi cài một khối encoder của Transformer.

Từ trung bình cố định tới trung bình theo câu hỏi

Attention nhận query (câu hỏi), key (thông tin dùng để so khớp) và value (thông tin muốn lấy). Điểm giống nhau giữa query và key quyết định trọng số của value tương ứng. Hình dung truy vấn trong CP: key dùng để tìm, value là dữ liệu cần trả. Khác hash map, attention thường trả tổ hợp mềm của nhiều value thay vì chọn một địa chỉ chính xác.

Đặt QQ shape (Lq, dk), KK (Lk, dk), VV (Lk, dv). Tính

S=QK⊤/dk,A=softmax⁡(S),O=AV.S=QK^\top/\sqrt{d_k},\qquad A=\operatorname{softmax}(S),\qquad O=AV.

Softmax theo chiều key: mỗi hàng AA có tổng 1, nói một query phân phối trọng lượng cho các key ra sao. Query và key cần cùng dkd_k để nhân vô hướng; value có thể có độ rộng dvd_v khác. Hệ số 1/dk1/\sqrt{d_k} giữ thang score khi độ rộng tăng; mục riêng ngay sau ví dụ tính tay giải thích con số này.

Hỏi nhanh

Trong một câu, mọi key giống hệt nhau, còn các value thì khác nhau. Output của một query bất kỳ là gì?

Một attention trên hai key

Lấy một query q=[1]q=[1], hai key k1=[1]k_1=[1], k2=[0]k_2=[0], nên dk=1d_k=1 và score là [1,0][1,0]. Softmax cho a1=e/(e+1)≈0.731a_1=e/(e+1)\approx0.731, a2=1/(e+1)≈0.269a_2=1/(e+1)\approx0.269. Với v1=[2,0]v_1=[2,0], v2=[0,4]v_2=[0,4], kết quả là [2a1,4a2]≈[1.462,1.076][2a_1,4a_2]\approx[1.462,1.076]. Key quyết định hệ số, value cung cấp nội dung.

Khi trọng số đã có, đầu ra chỉ là trung bình có trọng số: (0.5,0.3,0.2)(0.5, 0.3, 0.2) trên các value 2, 4, 10 cho 1+1.2+2=4.21 + 1.2 + 2 = 4.2. Nếu mọi key giống hệt nhau thì mọi score bằng nhau, và đầu ra là trung bình cộng các value, query là gì cũng vậy.

Score (1, 0) qua softmax thành (0.731, 0.269), rồi trộn hai value.q = [1]k₁ = [1]score 10.731× v = [2, 0]k₂ = [0]score 00.269× v = [0, 4]softmaxoutput = [1.462, 1.076]
Score (1, 0) qua softmax thành (0.731, 0.269), rồi trộn hai value.
Python
import numpy as np
import torch
from torch import nn

torch.set_num_threads(1)
torch.manual_seed(41)
Q = np.array([[1.]])
K = np.array([[1.], [0.]])
V = np.array([[2., 0.], [0., 4.]])
scores = Q @ K.T / np.sqrt(Q.shape[-1])
shifted = scores - scores.max(axis=-1, keepdims=True)
weights = np.exp(shifted)
weights /= weights.sum(axis=-1, keepdims=True)
output = weights @ V
print('weights:', np.round(weights, 6).tolist())
print('output:', np.round(output, 6).tolist())
tq, tk, tv = [torch.tensor(a) for a in [Q, K, V]]
torch_output = (tq @ tk.T / np.sqrt(Q.shape[-1])).softmax(dim=-1) @ tv
print('match:', np.allclose(output, torch_output.numpy()))

Trừ max trước exp không đổi softmax vì mọi số mũ cùng chia một hệ số; nó tránh exp của số quá lớn. Đây là cùng tư duy phép số ổn định như loss bài 3.3. Dòng match xác nhận phép tự cài trùng với phép tensor của PyTorch.

Hỏi nhanh

scores có shape (B, Lq, Lk). Trong weights = scores.softmax(dim=...), phải chọn dim nào để mỗi query phân trọng số cho các key?

Score quá lớn làm softmax bão hòa

Score q⊤k=∑i=1dkqikiq^\top k=\sum_{i=1}^{d_k}q_ik_i là tổng của dkd_k tích. Nếu các thành phần của qq và kk độc lập, trung bình 0, phương sai 1, mỗi tích có phương sai 1, nên score có phương sai dkd_k và độ lệch chuẩn dk\sqrt{d_k}. Score càng chênh nhau, softmax càng dồn trọng số vào một key. Khi phân phối gần one-hot, softmax bão hòa: gradient tới các key còn lại gần bằng 0, giống tanh bão hòa ở bài 3.3. Chia cho dk\sqrt{d_k} đưa độ lệch chuẩn về khoảng 1 ở mọi độ rộng.

Thí nghiệm dưới lấy 2000 query ngẫu nhiên, mỗi query 8 key, đo độ lệch chuẩn của score và trung bình trọng số lớn nhất trong mỗi hàng:

Python
rng = np.random.default_rng(0)

def mean_top_weight(scores):
    shifted = scores - scores.max(axis=-1, keepdims=True)
    weights = np.exp(shifted)
    weights /= weights.sum(axis=-1, keepdims=True)
    return weights.max(axis=-1).mean()

for d_k in [4, 64, 1024]:
    q = rng.standard_normal((2000, 1, d_k))   # 2000 query
    k = rng.standard_normal((2000, 8, d_k))   # 8 key cho mỗi query
    raw = (q * k).sum(axis=-1)                # score chưa chia, shape (2000, 8)
    scaled = raw / np.sqrt(d_k)
    print(f'd_k={d_k:<4}  std {raw.std():5.2f} -> {scaled.std():4.2f}   '
          f'max weight {mean_top_weight(raw):.3f} -> {mean_top_weight(scaled):.3f}')

Không chia, độ lệch chuẩn tăng đúng như dk\sqrt{d_k} (2, 8, 32) và key mạnh nhất chiếm trung bình 0.524, 0.878 rồi 0.968 trọng số, trong khi chia đều cho 8 key chỉ là 0.125. Có chia, độ lệch chuẩn ở quanh 1 và trọng số lớn nhất giữ quanh 0.35 ở cả ba độ rộng: thang score không còn phụ thuộc kích thước model.

Lập luận dựa trên giả định độc lập lúc khởi tạo; trong quá trình học, model vẫn có thể làm score lớn khi thật sự cần một phân phối nhọn. Đẩy tới cùng, nhân mọi score với c→∞c \to \infty thì softmax thành one-hot và đầu ra chỉ còn value của key có score lớn nhất.

Không chia √d_k, một key chiếm gần hết trọng số khi d_k lớn.00.1250.510.520.34d_k = 40.880.36d_k = 640.970.36d_k = 1024không chiachia √d_kchia đều: 1/8trọng số lớn nhất (TB)
Không chia √d_k, một key chiếm gần hết trọng số khi d_k lớn.

Hỏi nhanh

Các thành phần của qq và kk độc lập, trung bình 0, phương sai 1, với dk=256d_k = 256. Độ lệch chuẩn của score trước và sau khi chia cho dk\sqrt{d_k} là bao nhiêu?

Self-attention học cách tạo câu hỏi

Trong self-attention, QQ, KK, VV đều được tạo từ cùng chuỗi XX nhưng qua ba phép chiếu học được: Q=XWQQ=XW_Q, K=XWKK=XW_K, V=XWVV=XW_V. Cùng token có ba vai trò khác nhau. Query của một vị trí hỏi nên lấy thông tin gì; key của các vị trí giúp so khớp; value mang dữ liệu được trộn. Ba ma trận được học từ loss, nên từng phần tử của chúng không mang một quy tắc ngữ nghĩa đặt sẵn.

Đại lượng một head Shape có batch Ý nghĩa
XX (B,L,d) Vector token đầu vào
QQ, KK (B,L,dk) Dùng so khớp
VV (B,L,dv) Nội dung cần trộn
score, weights (B,L,L) Mỗi query nhìn mọi key
OO (B,L,dv) Biểu diễn đã nhận ngữ cảnh

Multi-head attention dùng nhiều bộ chiếu, tạo nhiều cách trộn song song, ghép các kết quả rồi chiếu lại. Mỗi head có softmax riêng, nên một query có HH phân phối trọng số cùng lúc: một head có thể dồn vào chủ ngữ, head khác vào từ đứng kề. Nhiều head không bảo đảm mỗi head ứng với một khái niệm con người đặt tên được; chúng chỉ là thêm cách tương tác để model học.

Với embed_dim dd và số head HH của lớp PyTorch trong bài, cần dd chia hết cho HH, mỗi head rộng d/Hd/H. Ba phép chiếu WQ,WK,WVW_Q,W_K,W_V và phép chiếu ra WOW_O đều là d×dd\times d, nên lớp có 4d2+4d4d^2+4d tham số kể cả bias, không phụ thuộc HH: với d=512d=512 là 4⋅5122+4⋅512=1 050 6244\cdot512^2+4\cdot512=1\,050\,624 cho cả 8 lẫn 16 head.

Hỏi nhanh

Bỏ hai phép chiếu WQW_Q, WKW_K, dùng thẳng Q=K=XQ = K = X. Ma trận score XX⊤XX^\top bị ràng buộc gì?

Mask bỏ vị trí nào?

Có hai loại mask, giải quyết hai lý do khác nhau và có thể dùng cùng lúc:

  • Padding mask bỏ các key PAD.
  • Causal mask bỏ key tương lai để vị trí tt không nhìn t+1t+1 trở đi, cần khi mô hình dự đoán token tiếp theo. Encoder đọc toàn câu thường không dùng causal mask; decoder sinh từng token (tự hồi quy) cần nó.

BERT là encoder, mỗi token nhìn cả hai phía; GPT là decoder, mỗi token chỉ nhìn bên trái. Causal mask viết bằng torch.tril(torch.ones(T, T)).bool() hay ~torch.triu(torch.ones(T, T), diagonal=1).bool() đều như nhau: vị trí ii nhìn jj khi j≤ij \le i.

Padding mask bỏ cột PAD, causal mask bỏ tương lai.padding mask123PADcausal mask1234cả hai123PADhàng: query · cột: key · ô tô: được đọc
Padding mask bỏ cột PAD, causal mask bỏ tương lai.

Ta đưa score của vị trí cấm về −∞-\infty trước softmax, nên trọng số đó bằng 0. Nếu một hàng bị cấm mọi key, softmax không còn phân phối hợp lệ; tránh câu rỗng hoặc chọn quy tắc rõ cho trường hợp ấy. Nếu mask sau softmax mà không chuẩn hóa lại, tổng weights không còn 1 và không còn cùng phép attention.

Ví dụ score [1,2,3][1, 2, 3] với key thứ ba là PAD: softmax chỉ còn trên [1,2][1, 2], cho (0.269,0.731,0)(0.269, 0.731, 0). Với mask đúng, thêm bao nhiêu PAD thì biểu diễn của token thật cũng không đổi.

Quy ước True/False của mask khác nhau giữa các hàm:

  • Trong nn.MultiheadAttention, key_padding_mask=True nghĩa là bỏ key; attn_mask=True nghĩa là cấm cặp query-key. Causal mask vì vậy thường viết torch.triu(torch.ones(L, L, dtype=torch.bool), diagonal=1), True ở các ô trên đường chéo, tức key tương lai. Code bài 3.8 có valid_mask True ở token thật, nên truyền key_padding_mask=~valid_mask.
  • F.scaled_dot_product_attention dùng quy ước ngược lại: True nghĩa là được tham gia attention.

Đọc tài liệu của đúng hàm đang gọi; không chuyển mask nguyên xi chỉ vì tên tham số giống nhau.

Score −∞ trước softmax cho trọng số 0; tổng vẫn bằng 1.102score10−∞sau mask0.7310.2690softmaxkey 3cấm trước softmax: hai key còn lại tự chia lại tổng 1
Score −∞ trước softmax cho trọng số 0; tổng vẫn bằng 1.

Hỏi nhanh

Causal mask cho MHA được viết torch.triu(torch.ones(L, L, dtype=torch.bool), diagonal=0) thay vì diagonal=1. Chuyện gì xảy ra?

Hỏi nhanh

Một hàng weights là [0.5, 0.3, 0.2], trong đó key thứ ba là PAD. Code đặt weight của PAD về 0 sau softmax. Để khớp với cách đặt score bằng −∞-\infty trước softmax, weight của key đầu phải bằng bao nhiêu?

Một encoder block nhỏ

Một encoder block ghép bốn thành phần:

  • Attention trộn thông tin giữa các token, cho output cùng shape với input.
  • Residual (kết nối tắt) cộng input vào output, giữ một đường thông tin và gradient đi thẳng.
  • LayerNorm (bài 3.6) chuẩn hóa theo feature trong từng token.
  • Feed-forward network (FFN) là một MLP nhỏ áp riêng cho từng vị trí, biến đổi feature sau khi đã trộn ngữ cảnh.

Ta cài kiểu post-norm: norm(x + attention(x)), rồi norm(x + FFN(x)). Nhiều model dùng pre-norm, x + attention(norm(x)): đường residual đi thẳng không qua norm nào, nên mạng rất sâu học ổn định hơn và ít phụ thuộc warmup. Vị trí norm là một lựa chọn kiến trúc, phải giữ đúng khi nạp checkpoint.

Encoder block: attention, cộng residual, LayerNorm; FFN, cộng residual, LayerNorm.xattention+ xLayerNormFFN+LayerNormresidualresidualpost-norm: norm(x + attention(x)) rồi norm(x + FFN(x)); shape (B, L, d) giữ nguyên
Encoder block: attention, cộng residual, LayerNorm; FFN, cộng residual, LayerNorm.
Python
class TinyEncoder(nn.Module):
    def __init__(self, width=4, heads=2):
        super().__init__()
        self.attention = nn.MultiheadAttention(width, heads, batch_first=True, dropout=0.)
        self.norm1 = nn.LayerNorm(width)
        self.ffn = nn.Sequential(nn.Linear(width, 8), nn.ReLU(), nn.Linear(8, width))
        self.norm2 = nn.LayerNorm(width)

    def forward(self, x, padding_mask=None, causal_mask=None):
        attended, weights = self.attention(x, x, x,
            key_padding_mask=padding_mask, attn_mask=causal_mask,
            need_weights=True, average_attn_weights=False)
        x = self.norm1(x + attended)
        x = self.norm2(x + self.ffn(x))
        return x, weights

block = TinyEncoder()
block.eval()
x = torch.randn(2, 3, 4)
padding_mask = torch.tensor([[False, False, True], [False, False, False]])
with torch.no_grad():
    encoded, attn = block(x, padding_mask=padding_mask)
print('output:', tuple(encoded.shape))
print('weights:', tuple(attn.shape))
print('PAD key max weight:', round(attn[0, :, :, 2].abs().max().item(), 6))
print('weight row sums:', torch.allclose(attn.sum(dim=-1), torch.ones(2, 2, 3), atol=1e-6))

Đầu ra (2,3,4) giữ batch, độ dài và độ rộng; weights (2,2,3,3) gồm batch, head, query, key vì không lấy mean qua head. Cột PAD của mẫu đầu bằng 0 cho cả hai head. Hàng query tại PAD vẫn có thể sinh vector: key padding mask chỉ bỏ key. Khi gộp thành vector câu hoặc tính token loss, còn phải dùng valid mask để bỏ query PAD ở bước đó.

Hai head có phân phối riêng; cột PAD bằng 0.head 10.210.790.000.780.220.000.740.260.00t1t2PADt1t2PADhead 20.440.560.000.250.750.000.450.550.00t1t2PADt1t2PADhàng: query · cột: key · cột PAD bằng 0 ở cả hai head
Hai head có phân phối riêng; cột PAD bằng 0.
Một head của MiniLM: mỗi khung là một query, cột là trọng số lên từng token.

Hỏi nhanh

Gọi self.attention(x, x, x, key_padding_mask=padding_mask) với mặc định average_attn_weights=True, cùng x có shape (2, 3, 4) và 2 head. weights có shape gì?

Thử causal mask bằng perturbation

Một phép kiểm có ý nghĩa hơn chỉ nhìn ma trận mask là thay token tương lai và xem quá khứ có đổi không. Với ba vị trí, cấm phần tam giác trên. Ta sửa token cuối rồi so đầu ra hai token đầu, giữ eval và dropout=0:

Python
causal = torch.triu(torch.ones(3, 3, dtype=torch.bool), diagonal=1)
one_sequence = x[1:2].clone()
changed = one_sequence.clone()
changed[:, 2, :] += torch.tensor([10., -3., 7., 2.])
with torch.no_grad():
    before, _ = block(one_sequence, causal_mask=causal)
    after, _ = block(changed, causal_mask=causal)
print('past unchanged:', torch.allclose(before[:, :2], after[:, :2], atol=1e-6))
print('last position changed:', not torch.allclose(before[:, 2], after[:, 2], atol=1e-6))

Dòng đầu xác nhận hai vị trí đầu không dùng token cuối. Dòng thứ hai là đối chứng: vị trí cuối phải đổi, nếu không thì dòng đầu có thể True chỉ vì block bỏ qua phần input bị sửa. Cách kiểm này có cùng tinh thần tạo input đối kháng nhỏ trong CP: thay phần bị cấm rồi xem output có chịu ảnh hưởng. Với model nhiều layer, chạy cùng test trên toàn model, vì chỉ một khâu trộn cả chuỗi (pooling, chuẩn hóa theo chiều độ dài) cũng đủ làm tương lai lọt vào.

Hỏi nhanh

Vẫn block có causal mask ấy, nhưng lần này sửa token ở vị trí 0 thay vì token cuối. Output ở những vị trí nào được phép thay đổi?

Thứ tự cần thông tin vị trí

Phép kiểm sau dùng block không có thông tin vị trí và không mask, ở chế độ eval với dropout=0. Khi ấy self-attention và FFN có tính equivariant theo hoán vị: đảo thứ tự token đầu vào thì các vector đầu ra đảo theo đúng cách đó. Một mean pooling đặt sau block còn bất biến (invariant): đảo token không đổi vector câu. Block vẫn tạo vector riêng ở mỗi vị trí, nhưng không tự biết token nào đứng thứ hai.

Nếu có mask, phải hoán vị mask cùng hàng query và cột key; một causal mask giữ cố định đã mang thông tin thứ tự nên phá tính chất này.

Python
permutation = torch.tensor([2, 0, 1])
with torch.no_grad():
    original, _ = block(one_sequence)
    permuted, _ = block(one_sequence[:, permutation])
print('equivariant without positions:', torch.allclose(
    permuted, original[:, permutation], atol=1e-6))

True ở đây là tính chất toán học, không phải lỗi thư viện. Positional encoding cộng vào mỗi token một vector cho biết nơi nó xuất hiện. Ba cách thường gặp là bảng vị trí học được, sin/cos cố định và relative position. Bản sin/cos dùng PEp,2i=sin⁡(p/10 0002i/d)PE_{p,2i}=\sin(p/10\,000^{2i/d}), PEp,2i+1=cos⁡(p/10 0002i/d)PE_{p,2i+1}=\cos(p/10\,000^{2i/d}); với d=4d=4, hai tần số là 1 và 0.01. Đoạn sau kiểm nó:

Python
positions = torch.arange(3, dtype=torch.float32).unsqueeze(1)
frequencies = torch.exp(-np.log(10000.) * torch.arange(0, 4, 2) / 4)
pe = torch.zeros(3, 4)
pe[:, 0::2] = torch.sin(positions * frequencies)
pe[:, 1::2] = torch.cos(positions * frequencies)
with torch.no_grad():
    ordered, _ = block(one_sequence + pe)
    swapped, _ = block(one_sequence[:, permutation] + pe)
print('position 0:', pe[0].tolist())
print('equivariant with fixed slots:', torch.allclose(swapped, ordered[:, permutation], atol=1e-6))

Khi đảo token nhưng giữ bảng vị trí tại các slot 0, 1, 2, ta đổi quan hệ token–vị trí, nên dòng cuối thành False: block giờ phân biệt được thứ tự. Sin/cos chỉ cung cấp tín hiệu; model dùng tín hiệu ấy đến đâu là do huấn luyện.

Positional encoding: chiều đầu dao động nhanh, chiều sau chậm.vị trí p = 0 … 47tần số caotần số thấpxanh: dươngcam: âm
Positional encoding: chiều đầu dao động nhanh, chiều sau chậm.

Relative position không gắn vị trí tuyệt đối vào token mà đưa khoảng cách có hướng j−ij-i vào score, chẳng hạn qi⊤(kj+rj−i)/dkq_i^\top(k_j+r_{j-i})/\sqrt{d_k} với rj−ir_{j-i} là vector học được. Mọi cặp cách nhau cùng một khoảng dùng chung một vector, hợp với dữ liệu mà quan hệ phụ thuộc khoảng cách hơn vị trí tuyệt đối, như chuỗi thời gian. Bảng vị trí học được thì là một nn.Embedding(max_len, d), nên chuỗi dài hơn max_len cần quy tắc rõ từ trước: bảng 512 vị trí không có hàng cho token thứ 600.

Với TT token, khoảng cách j−ij - i nhận 2T−12T - 1 giá trị, từ −(T−1)-(T-1) tới T−1T-1. Sin/cos tính được ở mọi vị trí, và dịch một khoảng cố định là một phép quay trên từng cặp chiều, nên model dễ đọc ra khoảng cách tương đối. Cùng công thức sin/cos còn dùng để đưa một con số như bước khử nhiễu tt vào mạng (bài 5.8).

Hỏi nhanh

Bảng vị trí học được là nn.Embedding(512, d). Tập test có một câu dài 600 token. Nếu không xử lý gì, chuyện gì xảy ra?

Kiểm graph và đọc attention đúng mức

Để bảo đảm encoder nhỏ có đường gradient, ta nối một loss kiểm với mục tiêu ngẫu nhiên có shape đúng. Không dùng chỉ bình phương đầu ra LayerNorm rồi kết luận gradient lớn hay nhỏ, vì norm đã cố định một số thống kê đầu ra.

Python
block.train()
block.zero_grad(set_to_none=True)
encoded, _ = block(x, padding_mask=padding_mask)
target = torch.randn_like(encoded)
valid = ~padding_mask
loss = ((encoded - target).square().mean(dim=-1) * valid).sum() / valid.sum()
loss.backward()
print('finite loss:', torch.isfinite(loss).item())
print('all parameter gradients finite:', all(p.grad is not None and torch.isfinite(p.grad).all()
                                             for p in block.parameters()))

Hai True xác nhận loss hữu hạn và mọi tham số đều nhận gradient hữu hạn, kể cả khi có mask PAD: block sẵn sàng để huấn luyện.

Attention weights là hệ số trộn tại một layer/head; chúng không tự là lời giải thích nhân quả cho dự đoán cuối, vì model còn residual, FFN, nhiều head và layer khác. Muốn biết token ảnh hưởng kết quả thế nào, cần perturbation hoặc ablation: xóa hay thay token rồi đo dự đoán đổi bao nhiêu.

Hỏi nhanh

Trong model phân loại cảm xúc, một head ở lớp cuối dồn 0.9 trọng số vào từ “không”. Kết luận nào có cơ sở?

Chi phí của attention

Attention mọi cặp tốn khoảng O(BL2d)O(BL^2d) tính toán ở phần score và trộn, cùng O(BHL2)O(BHL^2) bộ nhớ cho score nếu lưu đủ ma trận; các phép chiếu thêm O(BLd2)O(BLd^2). Tăng LL gấp đôi thì phần score gấp bốn. Padding cũng trả theo L2L^2: một câu rất dài kéo cả batch lên độ dài của nó, nên xếp các câu dài gần nhau vào cùng batch (bucketing), cắt hoặc chia cửa sổ văn bản dài trước khi nghĩ tới phần cứng lớn hơn.

Độ dài gấp 16 thì số ô của ma trận score gấp 256.L = 1280.02 triệu ôL = 2560.07 triệu ôL = 5120.26 triệu ôL = 10241.05 triệu ôL = 20484.19 triệu ôsố ô score L × L của một head, một câu: L gấp đôi thì gấp bốn
Độ dài gấp 16 thì số ô của ma trận score gấp 256.

Bậc hai đến từ việc softmax buộc tạo đủ ma trận L×LL\times L: exp⁡(qi⊤kj)\exp(q_i^\top k_j) không viết được thành tích vô hướng ϕ(qi)⊤ϕ(kj)\phi(q_i)^\top\phi(k_j) với ϕ\phi hữu hạn chiều.

Linear attention thay hẳn nó bằng ϕ(qi)⊤ϕ(kj)\phi(q_i)^\top\phi(k_j) với một feature map ϕ\phi hữu hạn chiều; khi đó có thể đổi thứ tự nhân ma trận như bài toán nhân chuỗi ma trận quen thuộc trong CP, và chi phí còn tuyến tính theo LL, cỡ O(Ld2)O(Ld^2). Với mask nhân quả, chỉ cần giữ hai tổng chạy St=∑j≤tϕ(kj)vj⊤S_t = \sum_{j \le t}\phi(k_j)v_j^\top và zt=∑j≤tϕ(kj)z_t = \sum_{j \le t}\phi(k_j), nên sinh mỗi token tốn bộ nhớ không đổi. ϕ\phi phải dương, như 1+ELU⁡(x)1 + \operatorname{ELU}(x), để trọng số không âm và mẫu số không bằng 0.

Với dữ liệu ít, model phức tạp chưa chắc thắng baseline masked mean; so trên cùng validation trước khi chuyển sang model pretrained ở chương 4.

Hỏi nhanh

Một batch 32 câu: 31 câu dài khoảng 100 token, một câu dài 2000 token. Cả batch được pad lên 2000. Cách rẻ nhất để giảm lãng phí là gì?

Khi đi thi

  • In một mask 3×3 và đọc quy ước của đúng hàm đang gọi: có hàm coi True là cấm, có hàm coi True là được đọc.
  • Tính bộ nhớ score B⋅H⋅L2B\cdot H\cdot L^2 trước khi chọn max_length; gấp đôi LL là gấp bốn bộ nhớ.
  • Shape đúng chưa đủ: kiểm thêm mỗi hàng weights cộng lại bằng 1.

Tóm tắt

  • Attention là trung bình có trọng số của value, trọng số lấy từ softmax của score query-key; chia dk\sqrt{d_k} giữ softmax khỏi bão hòa.
  • Self-attention học ba phép chiếu Q, K, V; multi-head tạo nhiều cách trộn với cùng số tham số.
  • Encoder block: attention, residual, LayerNorm, FFN.
  • Padding mask bỏ PAD, causal mask bỏ tương lai; thông tin vị trí cho model biết thứ tự.
  • Chi phí bậc hai theo độ dài quyết định ngữ cảnh dài được bao nhiêu.

Kiểm tra cuối bài

Câu 1

Một đội tự cài multi-head self-attention:

Q = self.q_proj(x).view(B, T, self.num_heads, self.head_dim).transpose(1, 2)
K = self.k_proj(x).view(B, T, self.num_heads, self.head_dim).transpose(1, 2)
scores = Q @ K.transpose(-2, -1)          # dòng A
attn = F.softmax(scores, dim=-1)

Khi chạy, trọng số attention rất nhọn: một vị trí nhận khoảng 99% trọng số. Dòng A thiếu phép nào?

Đề thiNOAI 2026 Singapore · Chung kết · Câu 17

Câu 2

Hai cấu hình self-attention cùng embed_dim=512: một head rộng 512, hoặc 8 head, mỗi head rộng 64. Lợi ích chính của 8 head là gì?

Phỏng theoCMU 10-601 · Exam 3 Practice · Q2.5

Câu 3

Code cũ dùng nn.MultiheadAttention với causal = torch.triu(torch.ones(L, L, dtype=torch.bool), diagonal=1). Đội chuyển sang F.scaled_dot_product_attention, nơi mask boolean True nghĩa là được tham gia attention. Phải truyền gì để giữ đúng phép tính?

Câu 4

Chuỗi có 4 vị trí, dùng left padding: vị trí 0 là PAD, token thật ở các vị trí 1, 2, 3. Model dùng cả causal mask lẫn key padding mask. Query ở vị trí 2 được đọc những key nào?

Câu 5

Key padding mask đã bỏ các key PAD, nhưng output tại một query PAD vẫn khác 0. Muốn gộp thành vector của cả câu, ta phải làm gì?

Câu 6

Model phân loại cảm xúc gồm embedding, hai encoder block không có mask, mean pooling rồi linear, chạy ở chế độ eval. Đội quên cộng positional encoding. Hai câu “phim hay , không chán” và “phim chán , không hay” có cùng tập token. Logit của hai câu thế nào?

Phỏng theoNOAI 2026 Singapore · Vòng sơ loại · Câu 225

Câu 7

Score có dạng qi⊤(kj+rj−i)/dq_i^\top(k_j + r_{j-i})/\sqrt{d}, trong đó rj−ir_{j-i} là vector học được theo khoảng cách có hướng từ token ii tới token jj. Chuỗi có 128 token, đánh số từ 0 tới 127. Cần học bao nhiêu vector rr khác nhau?

Phỏng theoUSA-NA-AIO 2026 · Vòng 2 · Ngày 1 · Part 1.2.2

Câu 8

QQ, KK, VV đều có TT hàng, dd cột, và đã tính xong. Với self-attention chuẩn không mask, tính O=softmax⁡(QK⊤/d)VO = \operatorname{softmax}(QK^\top/\sqrt d)V tốn chi phí bậc nào?

Đề thiUSA-NA-AIO 2026 · Vòng 2 · Ngày 1 · Part 1.1.1

Câu 9

Linear attention thay exp⁡(qi⊤kj/d)\exp(q_i^\top k_j/\sqrt d) bằng ϕ(qi)⊤ϕ(kj)\phi(q_i)^\top\phi(k_j), với ϕ:Rd→Rℓ\phi:\mathbb R^d\to\mathbb R^\ell đã tính sẵn cho mọi token. Bỏ qua mẫu số chuẩn hóa, output là ϕ(Q)ϕ(K)⊤V\phi(Q)\phi(K)^\top V, trong đó ϕ(Q)\phi(Q), ϕ(K)\phi(K) có shape T×ℓT\times\ell và VV có shape T×dT\times d. Tính theo thứ tự nào thì chi phí tuyến tính theo TT?

Phỏng theoUSA-NA-AIO 2026 · Vòng 2 · Ngày 1 · Part 1.1.3–1.1.4

Câu 10

Một đội train encoder với batch 8, 12 head, float32, ngữ cảnh 512 token. Ma trận score của một lớp chiếm khoảng 0.1 GB. Đội muốn tăng ngữ cảnh lên 4096 token, giữ nguyên batch và model. Khi ấy bộ nhớ score của một lớp khoảng bao nhiêu?

Phỏng theoNOAI 2026 Singapore · Vòng sơ loại · Câu 247

Đọc thêm