Transformer 有一個看似矛盾的特性:它可以在訓練時同時計算一整排 token,生成文字時卻又得一個 token 接一個 token。要理解這件事,先看三個設計如何分工:位置編碼提供順序線索;causal mask 限制每個位置可讀取的內容;attention、前饋網路與殘差區塊則逐層混合和整理表示。
本文先用一組四維向量手算正弦位置編碼,再從完整 encoder-decoder 圖認識 block。接著釐清遮罩和 cross-attention 的工作,最後比較 teacher forcing 的平行計算、推論時的逐步生成,以及 post-norm 和 pre-norm。文末程式是一個手設參數的 CPU 機制例,不是訓練好的翻譯模型,也不是效能測試。
位置編碼:讓表示帶上順序
Self-attention 可以讓一個位置直接讀取多個其他位置,但「能彼此連接」本身沒有告訴模型誰先誰後。Transformer 的一種做法,是把位置向量加到 token embedding 上。兩者維度必須相同,因此相加後仍是同一個 d_model 寬度:
輸入表示 = token embedding × √d_model + positional encoding
原始 Transformer 論文使用固定的正弦與餘弦函數。對位置 pos、模型寬度 d_model,每一對特徵的角度如下:
PE(pos, 2i) = sin(pos / 10000^(2i / d_model))
PE(pos, 2i + 1) = cos(pos / 10000^(2i / d_model))
偶數欄放 sin,緊接的奇數欄放同頻率的 cos;i 遞增時,分母改變,各組函數的變化速度也不同。位置 0 時角度是 0,所以每對值都是 [sin(0), cos(0)] = [0, 1]。
同一份位置編碼也可以改用矩陣看:每列是一個位置,每欄是一個 encoding dimension,顏色表示該位置與欄位的 sin/cos 值。色條範圍是 −1 到 1,因此深淺不是「這個 token 對另一個 token 的注意力」;它還沒有經過 query-key 配對或 softmax。
以程式中的第一個 embedding 為例,d_model=4,所以縮放因子是 √4=2。位置 0 的 encoding 是 [0, 1, 0, 1]:
embedding = [0.20, −0.10, 0.30, 0.40]
embedding × 2 = [0.40, −0.20, 0.60, 0.80]
PE(position=0) = [0.00, 1.00, 0.00, 1.00]
相加後 = [0.40, 0.80, 0.60, 1.80]
這說明同一個 token 放在不同位置時,送入後續 attention 的向量會不同。表中的小數是本篇程式實際使用的 sin/cos 值,四捨五入到四位:
位置 pos | 欄 0 sin(pos) | 欄 1 cos(pos) | 欄 2 sin(pos/100) | 欄 3 cos(pos/100) |
|---|---|---|---|---|
| 0 | 0.0000 | 1.0000 | 0.0000 | 1.0000 |
| 1 | 0.8415 | 0.5403 | 0.0100 | 1.0000 |
| 2 | 0.9093 | −0.4161 | 0.0200 | 0.9998 |
| 3 | 0.1411 | −0.9900 | 0.0300 | 0.9996 |
這是原始 Transformer 採用的其中一種位置表示,不代表所有 Transformer 都必須用固定正弦表。正弦值也沒有被「訓練成位置標籤」;它們是依位置索引和公式算出,與 token embedding 相加。
從整體架構看每一層在做什麼
位置資訊進入表示後,encoder 可在來源序列的位置間做 self-attention;decoder 先對已提供的目標 prefix 做 masked self-attention,再用 cross-attention 讀 encoder 輸出,接著各自透過 positionwise feed-forward network(FFN)。圖中的 Add & norm 是殘差相加後再做正規化,虛線框內的 × n 表示重複堆疊同類 block。頂端 FC 是輸出投影;這張圖沒有畫 softmax,因此不應把它說成已輸出的詞彙機率。
一個常見的 Transformer block 可用兩個子層來理解:attention 在位置之間交換資訊;FFN 則在每一個位置各自處理特徵。FFN 通常是兩個線性層加一個非線性函數,例如:
FFN(x) = W₂ · ReLU(W₁x + b₁) + b₂
同一層的 FFN 權重會套用到每個位置,但不同層有自己的權重。FFN 的線性變換在特徵維度上運算,不直接混合不同 token;跨位置的資訊仍由 attention 帶入。為了能和輸入相加,子層最後的特徵寬度要回到 d_model,即使 FFN 中間層較寬。
Causal mask:限制 decoder 可以讀到哪裡
自回歸生成不能讓預測位置讀到未來 token。以長度 4 的目標輸入來說,query 位置 i 可讀取 key 位置 j ≤ i,而 j > i 的未來位置要封鎖。若依常用分數矩陣慣例,列是 query、欄是 key,causal mask 會封鎖對角線右側:
key 0 key 1 key 2 key 3
query 0 可見 遮住 遮住 遮住
query 1 可見 可見 遮住 遮住
query 2 可見 可見 可見 遮住
query 3 可見 可見 可見 可見
對角線可見,因為它代表目前餵進 decoder 的 token。避免洩漏還需要把訓練標籤向右移一格:若要預測 [詞₁, 詞₂, EOS],decoder 輸入是 [BOS, 詞₁, 詞₂]。例如第二列以 詞₁ 為輸入,允許讀 BOS 與詞₁,但其監督目標是詞₂;它沒有讀到作為目標的詞₂本身。只加 mask、卻把未位移的正確答案直接放在同一輸入位置,仍可能洩漏答案。
Causal mask 與 cross-attention 解決不同問題。前者在 decoder 的 self-attention 中禁止讀取未來目標位置;後者讓 decoder 當前表示從 encoder 的來源表示取資訊。Cross-attention 通常要遮住 source padding,但不會因此替 decoder self-attention 自動封鎖未來 target。
Padding mask 和訓練 loss 也不同:
src_key_padding_mask- 讓 encoder attention 忽略 source 端 PAD keys
- 不會替 decoder cross-attention 自動遮 PAD;後者需
memory_key_padding_mask
tgt_key_padding_mask- 忽略 target 端作為 key/value 的 PAD 位置;可與 causal mask 一起用
- 不代表 PAD query 的輸出必為零
ignore_index- 訓練時計算 loss 時排除不作監督的 PAD labels
- 不會替 attention 建立 causal 或 padding mask
因此,「PAD token 被 attention mask 遮住」不等於「PAD label 不會計入 loss」;反過來,loss 忽略 PAD 也不會阻止模型在 attention 中讀到 PAD。這些是不同張量和不同階段的控制。本篇原型沒有實作 padding、cross-attention 或 loss。
Transformer 訓練可以平行,生成仍按序
訓練時,完整正確目標序列已知,先做位移,形成 decoder inputs;每個目標位置都能同時建立 query,再用 causal mask 限制各列可讀範圍。於是,同一層中的多個位置可以用矩陣運算一起算,不必像標準 RNN 那樣等待前一個 hidden state 才能算下一個位置。遮罩保留了每個位置的條件依賴,所以「平行計算」不等於「偷看未來答案」。
但 decoder 的多層網路仍按層次傳遞:第 2 層要等第 1 層輸出。推論時,下一個 token 又依賴先前選出的 token;因此自回歸生成在 token 時間軸上仍逐步進行。實作可以用 KV cache 重用先前結果來減少重算,但這不是本文原型的內容,也不會讓下一個未知 token 在前一個 token 選定前就已知。
以標準 dense self-attention 來說,長度 T 會形成 T × T 的 query-key 分數矩陣;平行處理這些位置不代表計算和記憶體成本固定不變。此處只談資料依賴和運算安排,沒有量測 Transformer 與 RNN 的速度、記憶體或品質,也不把理論複雜度當成所有硬體上的效能保證。
殘差與 LayerNorm:在子層外再加一道整理
Attention 或 FFN 子層的輸出會和其輸入做殘差相加,再做 LayerNorm。以原始論文採用的 post-norm 表示:
post-norm: LayerNorm(x + Sublayer(x))
相加前兩個張量形狀必須相同。另一種常見安排是 pre-norm:
pre-norm: x + Sublayer(LayerNorm(x))
兩者把 LayerNorm 放在不同位置,不是把公式換個寫法;每層實際輸入及梯度路徑會不同。原始論文與 D2L 圖採用 post-norm 示意,PyTorch TransformerEncoderLayer/TransformerDecoderLayer 可依 norm_first 選 post- 或 pre-norm。看到「Transformer 有 residual + LayerNorm」還不夠,若要重現特定模型,也要核對是哪種順序。
PyTorch LayerNorm(d_model) 在每一個 token 的最後 d_model 個特徵上計算均值與變異數,不跨 batch 或序列位置;使用母體變異數(除以特徵數)並加 eps,再套上可學的 γ、β。仿射縮放與平移後,輸出均值不保證還是 0、變異數也不保證正好為 1。這點與 BatchNorm 使用 running statistics 的做法不同。
本篇手設原型只比較一個 causal self-attention 子層的 post-norm 與 pre-norm。相同示例參數下,兩種輸出的最大絕對差為 1.6793967211269867;手算 LayerNorm 與 PyTorch 輸出的最大差為 2.220446049250313e−16。這些數字只核對此 toy 算例,不表示哪種架構普遍較好。
從教材程式搬到自己的模型時,兩個地方要額外確認
eval()不等於自動套上 causal mask。 D2L 的 PyTorch DecoderBlock 範例只在 training 分支建立 causal valid lengths;若你在 eval 模式把一整段 target prefix 一次送入,仍須確認實際呼叫提供了正確遮罩。若是一次只餵一個新 token且 state 僅含過去內容,前提不同。不要只因呼叫了model.eval()就假定任何輸入方式都防止未來洩漏。- 增量解碼要用絕對位置 offset。 D2L 教材的逐步示例在每次 forward 取 position encoding 前綴;它的簡化呼叫路徑沒有依已快取位置調整 offset。若自己把整段輸入拆成逐 token/chunk 呼叫,應檢查每塊是否加上正確絕對位置。這是對該教材程式路徑的靜態閱讀限制,不是本篇做過的完整翻譯模型實測。
可檢查的小型機制原型
以下程式使用四個手設 embedding、d_model=4、兩個 head、float64 與 PyTorch nn.MultiheadAttention。所有 Q/K/V 與輸出投影參數都明確覆寫;程式實際測一層 causal self-attention、位置偏移、prefix 重算與 LayerNorm/residual。已核環境為 Python 3.10.12、PyTorch 2.3.1+cu118,但只在 CPU 執行,float64、單一 thread、dropout 0、eval 並明確傳 mask。+cu118 是 PyTorch build 標記,不代表這次用了 GPU。
可以先準備已有 PyTorch 2.3.1 的 Python 環境,把整段存成 transformer_position_mask_residual.py 後執行 python3 transformer_position_mask_residual.py。本機既有環境已跑通;新虛擬環境安裝與 pip 安裝流程沒有在本次驗證。
四個 token 的每個 PE 向量都是四維;causal mask 是 4 × 4。對 torch.nn.MultiheadAttention 2.3 API,布林 attn_mask=True 代表封鎖該 query-key 配對,False 代表可見;這和 raw F.scaled_dot_product_attention(F 指 torch.nn.functional)的布林遮罩慣例相反,不能把一種 API 的遮罩直接搬給另一種。原型採明確 MHA mask,沒有直接呼叫或獨立測試 raw SDPA API;MHA 內部仍可能依條件選用其實作。
以 query 位置 2 為例,它可以讀 key 0、1、2,但不能讀 key 3。兩個 head 對這列的實際權重如下:
- 0.385728
- 0.319920
- 0.294352
- 0
- 1
- 0.241557
- 0.384838
- 0.373606
- 0
- 1
將同一個完整序列一次送入 masked MHA,和逐個 prefix 從頭重算、取最後位置輸出相比,最大差是 1.1102230246251565e−16;把最後一個未來 embedding 大幅改動,前三個位置的輸出最大改變為 0。這些是有明確參數的單一小例。prefix 每一步都是從頭重算,不是 KV cache,也不是速度比較。
張量形狀在此例為 (batch=1, sequence=4, d_model=4);切成 2 個 head 後每 head 寬度是 2。這不是完整 Transformer:沒有 FFN、cross-attention、source/target padding、tokenizer、vocabulary head、teacher-forcing loss 或訓練更新。因此程式沒有驗證翻譯品質、語料效果、GPU 效能或 KV cache 加速。完整數值輸出與驗證界線保留在本機研究紀錄;本文的程式碼區塊本身可直接另存重跑。
完整可重跑程式
"""Small, hand-set Transformer mechanics example for DS46.
CPU/float64 only. Demonstrates absolute sinusoidal positions, an explicit
causal mask, post-norm vs pre-norm residual forms, and prefix recomputation.
No training, corpus, cache implementation, or performance benchmark.
"""
from __future__ import annotations
import json
import math
import platform
import torch
from torch import nn
def sinusoidal_position_encoding(positions: torch.Tensor, d_model: int) -> torch.Tensor:
"""Return sinusoidal encodings for explicit absolute position indices."""
if d_model <= 0 or d_model % 2:
raise ValueError("this example requires a positive even d_model")
positions = torch.as_tensor(positions, dtype=torch.float64, device="cpu")
if positions.ndim != 1:
raise ValueError("positions must be a one-dimensional list of absolute indices")
if torch.any(positions < 0):
raise ValueError("positions must be nonnegative")
feature_pairs = torch.arange(0, d_model, 2, dtype=torch.float64, device="cpu")
denominators = torch.pow(10000.0, feature_pairs / d_model)
angles = positions[:, None] / denominators[None, :]
result = torch.empty((positions.numel(), d_model), dtype=torch.float64, device="cpu")
result[:, 0::2] = torch.sin(angles)
result[:, 1::2] = torch.cos(angles)
return result
def upper_triangle_causal_mask(length: int) -> torch.Tensor:
"""For nn.MultiheadAttention, True means that query-key pair is blocked."""
if length < 1:
raise ValueError("sequence length must be positive")
return torch.triu(torch.ones((length, length), dtype=torch.bool), diagonal=1)
def rounded(tensor: torch.Tensor, digits: int = 10):
values = tensor.detach().cpu().to(torch.float64)
if values.ndim == 0:
return round(float(values.item()), digits)
if values.ndim == 1:
return [round(float(x), digits) for x in values.tolist()]
return [rounded(row, digits) for row in values]
def main() -> None:
torch.set_num_threads(1)
dtype = torch.float64
d_model, num_heads, seq_len = 4, 2, 4
head_dim = d_model // num_heads
assert d_model % num_heads == 0
# Four fixed token embeddings. These are illustrative inputs, not token IDs.
embeddings = torch.tensor(
[[[
0.20, -0.10, 0.30, 0.40,
], [
-0.30, 0.50, 0.10, -0.20,
], [
0.40, 0.20, -0.50, 0.10,
], [
-0.10, -0.40, 0.20, 0.30,
]]], dtype=dtype, device="cpu")
# Explicit absolute position IDs. Match the common embedding scale + PE form.
position_ids = torch.arange(seq_len, dtype=torch.long, device="cpu")
pe = sinusoidal_position_encoding(position_ids, d_model)
positioned = embeddings * math.sqrt(d_model) + pe.unsqueeze(0)
# Verify a later chunk receives its absolute positions, not positions reset to 0.
chunk_positions = torch.tensor([2, 3], dtype=torch.long, device="cpu")
chunk_pe = sinusoidal_position_encoding(chunk_positions, d_model)
chunk_pe_from_full = pe[2:4]
reset_chunk_pe = sinusoidal_position_encoding(torch.tensor([0, 1]), d_model)
offset_error = float(torch.max(torch.abs(chunk_pe - chunk_pe_from_full)).item())
reset_error = float(torch.max(torch.abs(chunk_pe - reset_chunk_pe)).item())
assert offset_error == 0.0
assert reset_error > 1e-3
# One explicit causal self-attention layer; all parameters are overwritten.
mha = nn.MultiheadAttention(
embed_dim=d_model,
num_heads=num_heads,
dropout=0.0,
bias=True,
batch_first=True,
device="cpu",
dtype=dtype,
)
w_q = torch.tensor([
[0.40, -0.10, 0.20, 0.30],
[0.10, 0.50, -0.20, 0.10],
[-0.30, 0.20, 0.40, 0.10],
[0.20, -0.40, 0.10, 0.50],
], dtype=dtype)
w_k = torch.tensor([
[0.30, 0.20, -0.10, 0.40],
[-0.20, 0.40, 0.30, 0.10],
[0.10, -0.30, 0.50, 0.20],
[0.40, 0.10, 0.20, -0.20],
], dtype=dtype)
w_v = torch.tensor([
[0.50, 0.10, -0.20, 0.30],
[0.20, -0.40, 0.30, 0.10],
[-0.10, 0.30, 0.40, 0.20],
[0.30, 0.20, 0.10, -0.50],
], dtype=dtype)
b_q = torch.tensor([0.05, -0.02, 0.03, 0.01], dtype=dtype)
b_k = torch.tensor([-0.03, 0.04, 0.02, -0.01], dtype=dtype)
b_v = torch.tensor([0.01, 0.02, -0.04, 0.03], dtype=dtype)
w_o = torch.tensor([
[0.60, -0.20, 0.10, 0.30],
[-0.10, 0.50, 0.20, -0.30],
[0.20, 0.10, 0.70, -0.20],
[0.30, -0.40, 0.10, 0.50],
], dtype=dtype)
b_o = torch.tensor([0.02, -0.01, 0.03, -0.04], dtype=dtype)
with torch.no_grad():
mha.in_proj_weight.copy_(torch.cat([w_q, w_k, w_v], dim=0))
mha.in_proj_bias.copy_(torch.cat([b_q, b_k, b_v], dim=0))
mha.out_proj.weight.copy_(w_o)
mha.out_proj.bias.copy_(b_o)
mha.eval()
causal_mask = upper_triangle_causal_mask(seq_len)
with torch.no_grad():
full_output, full_weights = mha(
positioned,
positioned,
positioned,
attn_mask=causal_mask,
need_weights=True,
average_attn_weights=False,
)
# Recompute each prefix from scratch. This is not a KV-cache implementation.
prefix_last_outputs = []
for end in range(1, seq_len + 1):
prefix = positioned[:, :end, :]
prefix_mask = upper_triangle_causal_mask(end)
prefix_output, _ = mha(
prefix,
prefix,
prefix,
attn_mask=prefix_mask,
need_weights=False,
)
prefix_last_outputs.append(prefix_output[0, -1])
prefix_last_outputs = torch.stack(prefix_last_outputs, dim=0)
# Change only a future token; earlier query outputs must remain unchanged.
changed_embeddings = embeddings.clone()
changed_embeddings[0, 3] += torch.tensor([9.0, -4.0, 3.0, 1.0], dtype=dtype)
changed_positioned = changed_embeddings * math.sqrt(d_model) + pe.unsqueeze(0)
changed_output, _ = mha(
changed_positioned,
changed_positioned,
changed_positioned,
attn_mask=causal_mask,
need_weights=False,
)
prefix_max_diff = float(torch.max(torch.abs(full_output[0] - prefix_last_outputs)).item())
earlier_future_change_max_diff = float(
torch.max(torch.abs(full_output[:, :3] - changed_output[:, :3])).item()
)
future_attention_max = float(
torch.max(torch.abs(full_weights.masked_select(causal_mask[None, None, :, :]))).item()
)
weight_row_sums = full_weights.sum(dim=-1)
assert prefix_max_diff < 1e-12
assert earlier_future_change_max_diff < 1e-12
assert future_attention_max == 0.0
assert torch.max(torch.abs(weight_row_sums - 1.0)).item() < 1e-12
# Compare the post-norm and pre-norm residual forms; these are not interchangeable.
eps = 1e-5
gamma = torch.tensor([1.10, 0.90, 1.20, 0.80], dtype=dtype)
beta = torch.tensor([0.10, -0.20, 0.05, 0.30], dtype=dtype)
layer_norm = nn.LayerNorm(d_model, eps=eps, elementwise_affine=True, device="cpu", dtype=dtype)
with torch.no_grad():
layer_norm.weight.copy_(gamma)
layer_norm.bias.copy_(beta)
with torch.no_grad():
postnorm_residual_sum = positioned + full_output
postnorm = layer_norm(postnorm_residual_sum)
prenorm_input = layer_norm(positioned)
prenorm_attention, _ = mha(
prenorm_input,
prenorm_input,
prenorm_input,
attn_mask=causal_mask,
need_weights=False,
)
prenorm = positioned + prenorm_attention
last_dim_mean = postnorm_residual_sum.mean(dim=-1, keepdim=True)
last_dim_var = postnorm_residual_sum.var(dim=-1, unbiased=False, keepdim=True)
manual_postnorm = (
(postnorm_residual_sum - last_dim_mean)
/ torch.sqrt(last_dim_var + eps)
* gamma
+ beta
)
manual_layernorm_diff = float(torch.max(torch.abs(postnorm - manual_postnorm)).item())
pre_post_difference = float(torch.max(torch.abs(prenorm - postnorm)).item())
assert manual_layernorm_diff < 1e-12
assert pre_post_difference > 1e-6
result = {
"scope": "Four hand-set token embeddings; sinusoidal absolute positions; one hand-parameterized causal MHA layer; LayerNorm/residual arithmetic. No training, corpus, KV cache, GPU, latency, or quality benchmark.",
"environment": {
"python": platform.python_version(),
"torch": torch.__version__,
"device": "cpu",
"dtype": "float64",
"torch_threads": torch.get_num_threads(),
"batch_first": True,
"mha_dropout": 0.0,
"mha_eval": True,
"mha_bool_attn_mask_true_means": "blocked",
"need_weights": True,
"average_attn_weights": False,
},
"dimensions": {
"batch": 1,
"sequence_length": seq_len,
"d_model": d_model,
"num_heads": num_heads,
"head_dim": head_dim,
"embeddings": list(embeddings.shape),
"position_encoding": list(pe.shape),
"positioned_embeddings": list(positioned.shape),
"causal_mask": list(causal_mask.shape),
"full_output": list(full_output.shape),
"per_head_weights": list(full_weights.shape),
"postnorm": list(postnorm.shape),
"prenorm": list(prenorm.shape),
},
"position_encoding": {
"absolute_position_ids": position_ids.tolist(),
"formula": "PE[pos,2i]=sin(pos/10000^(2i/d_model)); PE[pos,2i+1]=cos(pos/10000^(2i/d_model))",
"embedding_scale": "sqrt(d_model)",
"pe_rows": rounded(pe),
"positioned_input_first_token": rounded(positioned[0, 0]),
"offset_check_positions": chunk_positions.tolist(),
"offset_pe_max_diff_vs_full_sequence_rows_2_3": offset_error,
"incorrect_reset_to_positions_0_1_max_diff": reset_error,
},
"causal_mask": {
"true_means": "block this query-key pair for torch.nn.MultiheadAttention 2.3",
"matrix": causal_mask.tolist(),
"diagonal_is_visible": True,
"full_sequence_output": rounded(full_output[0]),
"per_head_attention_weights": rounded(full_weights[0]),
"per_query_key_weight_sums": rounded(weight_row_sums[0]),
"max_weight_on_blocked_future_positions": future_attention_max,
"full_masked_pass_vs_independent_prefix_recomputation_max_diff": prefix_max_diff,
"earlier_output_change_after_perturbing_future_token_max_diff": earlier_future_change_max_diff,
"prefix_recomputation_note": "Each prefix is rerun from scratch at its original position IDs; this is not cached decoding or a speed comparison.",
},
"residual_and_layernorm": {
"post_norm_formula": "LayerNorm(x + Sublayer(x))",
"pre_norm_formula": "x + Sublayer(LayerNorm(x))",
"chosen_demo_path": "post_norm",
"residual_sum": rounded(postnorm_residual_sum[0]),
"epsilon": eps,
"normalized_axis": -1,
"variance_convention": "biased variance (unbiased=False) across the final d_model feature axis for each token",
"gamma": rounded(gamma),
"beta": rounded(beta),
"postnorm_output": rounded(postnorm[0]),
"postnorm_feature_means_after_affine": rounded(postnorm.mean(dim=-1)[0]),
"postnorm_feature_variances_after_affine_biased": rounded(postnorm.var(dim=-1, unbiased=False)[0]),
"manual_layernorm_max_diff": manual_layernorm_diff,
"pre_vs_post_norm_max_diff_same_demo_parameters": pre_post_difference,
},
"checks": "PASS: explicit positions retain absolute offsets; MHA future-mask weights are zero; full causal sequence rows match same-token prefix recomputation; changing a future token leaves earlier outputs unchanged; post-norm LayerNorm matches manual final-feature-axis biased-variance computation; pre-norm and post-norm outputs differ.",
}
print(json.dumps(result, ensure_ascii=False, indent=2, allow_nan=False))
if __name__ == "__main__":
main()
來源與延伸閱讀
- D2L:Self-Attention and Positional Encoding(位置編碼與曲線/熱圖),The Transformer Architecture(encoder/decoder、residual、LayerNorm 與教材程式)。兩章內容授權 CC BY-SA 4.0;圖 1–3 為上述 D2L 原圖/作者程式輸出轉檔與手機裁切改作,改作圖依同授權。封面也採同一完整 Transformer 原圖等比置於白底 1600×900 畫布,未裁切或加圖元,依 CC BY-SA 4.0。
- TensorFlow:Neural machine translation with a Transformer(causal self-attention、cross-attention 與四層 compact 圖)。頁面自有圖授權 CC BY 4.0;圖 4–6 完整原圖白底轉檔,圖元未改。
- Vaswani et al., Attention Is All You Need, 2017,方法 §3、訓練 §5。此文只連結原論文,沒有重用其圖。
- PyTorch 2.3:MultiheadAttention、Transformer、LayerNorm API 文件;另見 SDPA 布林遮罩定義。
留言0
登入後就能留言。
登入