CH·4從零打造 LLM/ 從零實作 GPT 模型

Build a Large Language Model (From Scratch) · Chapter 4

從零實作一個能生成文字的 GPT 模型Implementing a GPT model from scratch to generate text

前一章我們刻好了 multi-head attention。這一章把其餘積木補齊,照 GPT-2(124M)的架構組裝成一台完整的 GPT——可以跑 forward pass、輸出 logits、逐字生成文字。章節中的關鍵 tensor 都以真實數值的 array 呈現,與書中執行輸出一致(暖身區的示意值會另行標明)。

00

本章定位:還缺哪些積木?

attention 只是引擎,要組成一台能吐字的 GPT 還需要一批「看似配角、缺一不可」的零件。這是本章的積木清單——它會隨你往下讀自動打勾(也可以點擊跳到該節):

🔰 先建立心態

GPT 的架構沒有想像中複雜。它的「深」不是來自千奇百怪的結構,而是同一個 transformer block 重複 12 次。理解一個 block,就理解了整台模型的主體。另外:本章做完的模型還不會講人話——權重是隨機的,訓練(pretraining)是第 5 章的事,所以結尾生成出亂碼是預期結果。

🔰 先分清楚兩種模式:訓練 vs 推論(本頁會隨時標註)

模型有兩種「使用模式」,千萬別混在一起:🏋️ 訓練 training=forward 算預測 → 算 loss → backward 算梯度 → 更新權重,靠大量資料把權重調好(第 5 章的主題);💬 推論 inference=權重固定不動、只做 forward,一次吐一個字。本章主要在搭兩種模式共用的架構;但凡講到「梯度」(LayerNorm 的動機、shortcut、前向×反向)都是在講訓練,而 4.7 的文字生成是推論。各節標題旁會掛上對應的標籤。

暖身:tensor 就是「多層的 array」

模型看不懂文字,只吃數字。一個字(token)→ 一串數字(vector);一句話 → 一疊向量(matrix);一批句子 → 再疊一層(3 維 tensor)。以單字 moves 為例:

# 一個 token:一條向量(GPT-2 用 768 個數字描述一個字;以下數值為示意,非書中輸出) "moves" → token ID 6100 → tok_emb[6100] = [ 0.0189, -0.4471, 0.7671, …, 0.1029 ] # 768 個數字 # 一句話(4 個 token):一個 2 維矩陣,形狀 [4, 768] "Every effort moves you" → [[ …768 個… ], # Every [ …768 個… ], # effort [ …768 個… ], # moves [ …768 個… ]] # you # 一批(2 句):3 維 tensor,形狀 [2, 4, 768] ← 之後看到方括號數字就是在說這個
4.1

Coding an LLM architecture(實作 LLM 架構)

input:token IDs
[2, 4]
→GPTModel(本節先搭骨架)embedding → 12×block → final LN → head→output:logits
[2, 4, 50257]

「參數(parameters)」就是模型裡可被訓練調整的權重(weights)。一層 2048×2048 的權重矩陣就有 2048×2048 = 4,194,304 個參數。我們瞄準 GPT-2 small(約 124M 參數):它的預訓練權重公開、筆電跑得動;GPT-3 架構幾乎相同,只是規模放大到 175B。

所有超參數集中在一份設定字典。點任一行看說明:

👆 點一行 config,看它的意思

佔位骨架 DummyGPTModel

在刻好每個零件前,先用「什麼都不做、原樣回傳」的假模組把骨架搭起來,看清楚資料怎麼從輸入流到輸出:

class DummyGPTModel(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])   # token 查表成向量
        self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"]) # 位置查表成向量
        self.drop_emb = nn.Dropout(cfg["drop_rate"])
        self.trf_blocks = nn.Sequential(                                  # 12 個(假的)block
            *[DummyTransformerBlock(cfg) for _ in range(cfg["n_layers"])])
        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):
        tok_embeds = self.tok_emb(in_idx)
        pos_embeds = self.pos_emb(torch.arange(in_idx.shape[1], device=in_idx.device))
        x = tok_embeds + pos_embeds     # 「是什麼字」+「在第幾位」
        x = self.drop_emb(x)
        x = self.trf_blocks(x)
        x = self.final_norm(x)
        return self.out_head(x)         # 投影回 50257 維 → logits

動畫:一個英文字如何走進模型

在餵整個 batch 之前,先看單一個字的完整旅程:文字 → 查 vocabulary 得到整數 ID → 到 embedding 表取出那一列向量。選一個字、按播放:

選字:
moves
tokenizer
查 vocab
→
6100
nn.Embedding
查表取列
→
→
768 維向量(示意值)

實際餵資料:兩句話的 batch

用 tiktoken 把兩句話編碼成一個 batch。滑過下面的字或數字,看文字 ↔ token ID 的對應(注意兩句開頭都是 Every → 同一個 ID 6109):

滑過任何一個字或 ID 試試 →

輸出形狀 [2, 4, 50257]

把 batch 餵進模型(torch.manual_seed(123)),輸出 logits 的形狀是 torch.Size([2, 4, 50257])。點下面三個維度,看每一維在 array 裡對應什麼:

torch.Size( [ 2, 4, 50257 ] )
👆 點一個維度
關鍵:輸入幾個 token,輸出就有幾個對應向量——模型對每一個位置都給出「下一個 token 是誰」的 50257 個分數(logits,尚未過 softmax)。生成時我們只取最後一個位置的那條。
常見疑問:那前面位置的 logits 要拿去 argmax 嗎?💬 推論時不用——前面位置的字已經固定(它們就是輸入本身),只對最後一格做 argmax,早期位置的 logits 直接丟棄。那幹嘛算它們?① transformer 是一次矩陣運算算完所有位置,各位置 logits 是同一次計算順便掉出來的;② 真正的主場在🏋️ 訓練:每個位置都是一筆「預測下一個字」的訓練樣本——4 個 token 一次 forward 就得到 4 筆監督訊號(第 5 章的 loss 正是對所有位置一起算),這是 GPT 訓練效率高的關鍵。

深入:為什麼能一次算 4 個位置,又不會「偷看未來」?(causal mask)🏋️ 訓練

雖然整句 token 是一起送進 transformer 做一次矩陣運算,但 causal mask 讓每個位置的 hidden state 只能利用自己與前面的 token——所以它產生的 logits 從未看過未來。點一個位置,看它的視野和它在回答的問題:

🔰 考場比喻

把 4 個位置想成同一間考場的 4 個學生:大家同時開考(一次矩陣運算),但監考老師(causal mask)規定——第 1 位只能看自己的考卷、第 2 位能看第 1~2 位、第 3 位能看前三位、第 4 位能看全部。沒有任何人能偷看「未來的答案」。這就是為什麼訓練時整句平行輸入,學到的能力和推論時「一步一步往後生成」完全一致。

點一個位置(query):
4.2

Normalizing activations with layer normalization

input:x
[2, 4, 768]
→LayerNorm(768)每個 token 的 768 維 → mean 0 / var 1,再乘 scale 加 shift→output:x′
[2, 4, 768](形狀不變)

深層網路常遇到 vanishing / exploding gradient(梯度消失/爆炸),訓練極不穩定。Layer normalization 的核心想法:把一層的輸出調整成 mean 0、variance 1(unit variance),加速收斂、穩定訓練。

🔰 白話說明

像考試「調分」:把一組高低不齊的數字拉成「平均 0、散開程度一致」的標準形狀。它是每個樣本自己調自己的(沿特徵維),跟 batch 裡其他樣本無關——這是它和 batch normalization 最大的差別,也是它不怕 batch size 變動、適合 LLM 的原因。

互動①:訊號穿過 30 層,會發生什麼事?

把一個訊號丟進 30 層的網路(真實矩陣相乘)。第 l 顆點=訊號過完第 l 層時的大小——精確說是該層輸出向量的 RMS(均方根:√mean(x²),24 個數字「典型有多大」;不用平均值是因為正負會抵銷)。點縮小到看不見=訊號消失、點變紅=數值爆掉;滑鼠停在點上可看實際數值。

🔰 s 是什麼?(不是 activation,也不是單一權重)

模擬中每層算的是 y = (s·W)·x:W 是那一層的權重矩陣(幾百個不同的數字,每層都不一樣),s(scale)是把整個 W 一起放大/縮小的倍率——代表該層權重的「整體大小」。點的大小才是 activation(訊號值):s 決定 activation 每過一層被放大或縮小多少,走到第 l 層時量級 ≈ s^l。可怕之處在於要連乘 30 次:s=0.9 看似無害,0.9³⁰ ≈ 0.04;s=1.1 也看似無害,1.1³⁰ ≈ 17.4。真實網路裡沒有一顆叫 s 的旋鈕——權重大小是初始化與訓練自然形成的,沒人能保證它剛好停在 1.0(Xavier/He initialization 就是想把每層的等效 s 調到 ≈ 1)。

每層把訊號乘上的倍率 s = 0.70
無 LayerNorm
每層加 LN

互動②:那 loss 會怎樣?(瀏覽器裡真的訓練 300 步)🏋️ 訓練

同一個 6 層小網路,只差在初始權重的倍率 s(就是互動①那顆旋鈕,訓練開始時權重的整體大小):s=0.3(太小)、s=3.0(太大)、s=3.0 但每層加 LayerNorm。在你的瀏覽器裡實際跑 300 步訓練(真實 forward + backward + 權重更新),把 loss 畫出來:

把結論記成三句話:梯度 → 0 ⇒ 權重不再更新 ⇒ loss 卡住不動(注意:不是變 0!);梯度 → ∞ ⇒ 數值溢位 ⇒ loss 變 NaN,訓練直接炸掉;加 LayerNorm ⇒ 每層訊號被校準回同一量級 ⇒ loss 才能順利下降。這正是 GPT 在每個子層前都放 LayerNorm(Pre-LN)的原因。

先看 dim 往哪個方向算

書中的例子:torch.randn(2,5) 過一層 Linear(5,6)+ReLU,得到一個 2×6 的輸出(ReLU 把負值壓成 0,所以沒有負數)。切換 dim,看平均值是「沿哪個方向」算的:

一步步做正規化(數值即時計算,和書中輸出一致)

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)              # keepdim 保形狀好做廣播
        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
unbiased=False:為什麼分母用 n 而不是 n−1?統計學上從「樣本」估計母體變異數時,慣例會把分母改成 n−1(稱為 Bessel's correction):因為用樣本自己的平均當中心,會系統性低估散佈程度,除以 n−1 可以修正這個偏差;反之分母用 n,得到的就是所謂「有偏(biased)」的變異數估計。這裡刻意選 n,理由有二:① 對 LLM 來說這個 n 是 embedding 維度(768),n 和 n−1 的差別約 0.13%,實務上可忽略;② 相容性才是重點——原始 GPT-2 是用 TensorFlow 實作的,其變異數預設就是除以 n;我們採一模一樣的算法,之後把 OpenAI 的預訓練權重載進來時,LayerNorm 的行為才會和那些權重當初被訓練時完全一致。
4.3

Implementing a feed forward network with GELU activations

input:x
[2, 3, 768]
→FeedForwardLinear 768→3072 · GELU · Linear 3072→768→output
[2, 3, 768](形狀不變)

LLM 不用傳統 ReLU,改用更平滑的 GELU(Gaussian error linear unit)。精確定義是 GELU(x) = x·Φ(x)(Φ 是標準常態的 CDF),實務用便宜的近似式(原始 GPT-2 也用它):

GELU(x) ≈ 0.5·x·(1 + tanh[ √(2/π)·(x + 0.044715·x³) ])
滑鼠移到曲線上逐點比較。ReLU 在 0 有尖角、負值一律歸零(梯度也歸零,神經元可能「死掉」);GELU 平滑、負值仍有小的非零輸出,訓練更好調。

互動:GELU 的好處——神經元不會「死掉」

🔰 先懂情境

一個神經元收到的輸入值 z(=前一層輸出乘上權重的加總)在訓練過程會不斷漂移——因為權重每一步都在變。下圖把 24 個神經元照它們的 z 值排在數線上,z = 0 是 ReLU 的生死線:一旦漂到左邊(負區),ReLU 的輸出是 0、梯度也恰為 0 → 權重從此不再更新 → 這個神經元永遠困在負區(dying ReLU)。GELU 在負區仍保留一點梯度,還能被訓練慢慢拉回右邊。

模擬訓練中的漂移:把所有 z 平移 0.0
z = −40(ReLU 生死線)z = +4
ReLU
GELU

FeedForward:768 → 3072 → 768

transformer block 裡的前饋網路=兩個 Linear 夾一個 GELU。先放大 4 倍到更寬的空間探索特徵、過非線性、再收縮回原維度。點「下一步」看形狀流動(用書中的輸入 [2, 3, 768]):

class FeedForward(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.layers = nn.Sequential(
            nn.Linear(cfg["emb_dim"], 4 * cfg["emb_dim"]),  # 768 → 3072(放大 4 倍)
            GELU(),
            nn.Linear(4 * cfg["emb_dim"], cfg["emb_dim"]),  # 3072 → 768(收縮回來)
        )
為什麼進出同維度?進去 768、出來也 768,這種一致性才能把模組一路往上疊,中間不必調整維度——這是 transformer「可堆疊」的關鍵設計。
🔁

補充:前向傳播 × 反向傳播🏋️ 訓練

input:x
[2]
→小型網路Linear+tanh(3 節點)→ Linear→output:ŷ 與 loss
(backward 再沿原路回傳梯度)

下一節會用「各層梯度」證明 shortcut 的威力——先花兩分鐘搞懂梯度從哪來。forward pass 把輸入算成預測、算出 loss;backward pass(backpropagation)用連鎖律從 loss 往回,算出每個權重「該往哪調」。

前向:activation(往右流) 反向:gradient(往左流) 數值皆即時計算的真值
4.4

Adding shortcut connections(加入捷徑連接)🏋️ 為了訓練

input:x→任意子層Linear+GELU / attention / FFN …→+x 走捷徑
直接加回
→output = 子層(x) + x
(形狀相同才能相加)

Shortcut connection(又稱 skip / residual connection,源自電腦視覺的 ResNet)作法樸素到不可思議:把某一層的輸入,直接加到那一層的輸出上。這等於替反向傳播的梯度開一條「抄近路」的旁路,繞過中間運算、避免梯度層層縮小。

書中用一個 5 層小網路(每層 Linear+GELU)實測。forward 裡可選擇要不要加捷徑:

def forward(self, x):
    for layer in self.layers:
        layer_output = layer(x)
        if self.use_shortcut and x.shape == layer_output.shape:
            x = x + layer_output      # 加回輸入=捷徑
        else:
            x = layer_output
    return x

切換開關,比較各層權重的平均絕對梯度(右側是書中實際的 print 輸出):

4.5

Connecting attention and linear layers in a transformer block

input:x
[2, 4, 768]
→TransformerBlockLN → attention → +x · LN → FFN → +x→output
[2, 4, 768](內容重新編碼)

把 MultiHeadAttention、LayerNorm、FeedForward、dropout、shortcut 全部組進一個 transformer block——GPT-2 small 把它重複 12 次。兩個子模組分工明確:

  • multi-head attention:「跨位置」的資訊交流——讓每個 token 看到並吸收其他 token 的資訊。
  • feed forward:「逐位置」的加工——對每個位置的向量獨立做非線性變換。

逐行走一遍 forward(注意兩個子層都是同一個範式:存 shortcut → LayerNorm → 子層 → dropout → 加回):

目前形狀:[2, 4, 768]
👆 按「下一行」逐行執行 TransformerBlock.forward
Pre-LayerNorm vs Post-LayerNorm:本實作把 LayerNorm 放在進子層之前(Pre-LN,現代 GPT 的做法,訓練較穩定);原始 transformer 論文放在子層之後(Post-LN),較容易訓練不穩。
形狀不變 ≠ 內容不變:輸入 [2, 4, 768]、輸出仍是 [2, 4, 768],但每個位置的向量已被重新編碼成整合了整句資訊的 context vector——每過一個 block,token 的表徵就「更懂上下文」。
4.6

Coding the GPT model(實作完整 GPT)

input:token IDs
[batch, tokens]
→GPTModeltok+pos embedding → dropout → 12×TransformerBlock → final LN → linear head→output:logits
[batch, tokens, 50257]

把骨架裡的假模組換成真貨,就是完整的 GPTModel:embedding → 12× TransformerBlock → final LayerNorm → linear output head。輸出形狀依然 [2, 4, 50257]——複習一下這三個數字:2=batch(一次餵了 2 句話)、4=每句 4 個 token、50257=每個 token 位置對詞彙表全部 50257 個字各給一個分數(logits)。也就是「2 句 × 每句 4 個位置 × 每個位置一整排『下一個字是誰』的評分」(回 4.1 的維度互動)。

self.trf_blocks = nn.Sequential(
    *[TransformerBlock(cfg) for _ in range(cfg["n_layers"])])   # 這次是真的 12 層
self.final_norm = LayerNorm(cfg["emb_dim"])
self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)

參數量:163M 與 124M 的差別(weight tying)

total_params = sum(p.numel() for p in model.parameters()) # Total number of parameters: 163,009,536

祕密在 weight tying(權重綁定):原始 GPT-2 讓輸出層直接重用 token embedding 的權重。看形狀就懂——兩層一模一樣、互為反向操作:

tok_emb.weight → [50257, 768]
token ID → 768 維向量
≠
out_head.weight → [50257, 768]
768 維向量 → 50257 個分數
163,009,536 − (50257 × 768) = 163,009,536 − 38,597,376 = 124,412,160 # ≈ 124M ✓

我們的實作採「兩層各自獨立」(163M),因為經驗上訓練效果較好、現代 LLM 也多如此;tying 的好處是省記憶體。163M 個 32-bit float(每個 4 bytes)合計 621.83 MB——「小」模型的權重也相當可觀。

🔰 weight tying 為什麼「合理」,不只是省參數

兩層做的事互為鏡像:token embedding 把「字」查成「向量」;輸出層把「向量」變回「每個字的分數」。若共用同一份權重,第 t 個字的 logit 就等於 h · e_t——拿目前的表徵 h 去和每個字的 embedding 做內積(相似度):「h 長得最像哪個字的 embedding,哪個字分數就最高」。這在語意上非常自然:同一套「字的座標系」同時負責讀入與輸出。

假設模型此刻的表徵 h(示意 6 維):
  • 省多少:50257 × 768 ≈ 38.6M 個參數(float32 約 147 MB)——tying 後 163M → 124M、權重檔 622 MB → 約 475 MB。
  • 代價:輸入與輸出被迫共用同一套表徵,少了一份可以分工的權重。實務上兩派都有:原始 GPT-2 綁(省參數);本書實作不綁——經驗上各自獨立的訓練表現通常略好。
  • 之後會再遇到:載入 OpenAI 的 GPT-2 預訓練權重時,因為官方是綁的,要把同一份 embedding 權重同時填進 tok_emb 與 out_head。

動手感受規模:參數計算器

拖動滑桿即時計算總參數與記憶體。preset 是書中列的四種 GPT-2 尺寸(medium 1024/24 層、large 1280/36、XL 1600/48)。計算器用的公式如下(帶入 GPT-2 small 的數值驗算):

# 記 E = emb_dim(768)、V = vocab_size(50257)、C = context_length(1024)、L = n_layers(12)

Token embedding      = V × E                       = 50257×768          =  38,597,376
Positional embedding = C × E                       = 1024×768           =     786,432

# 每個 transformer block:
  attention  = 3E²(Q,K,V,無 bias)+ (E²+E)(輸出投影含 bias)
             = 3×768² + (768²+768)                 = 1,769,472+590,592  =   2,360,064
  feed forward = (E·4E + 4E) + (4E·E + E)          # 768→3072 再 3072→768,皆含 bias
             = (768×3072+3072) + (3072×768+768)                         =   4,722,432
  2× LayerNorm = 2×(E + E)(scale 與 shift)        = 4×768              =       3,072
  → 每 block 小計                                                        =   7,085,568

Transformer ×L       = 12 × 7,085,568                                   =  85,026,816
Final LayerNorm      = 2E                          = 2×768              =       1,536
── 到這裡的小計(=weight tying 後的總數)                                 124,412,160
Output head(不綁時)= V × E                        = 50257×768          =  38,597,376
── 全部(我們的實作)                                                     163,009,536

# 記憶體:163,009,536 × 4 bytes (float32) ≈ 621.83 MB
preset 標示為 weight tying 後的官方參數量(點擊會自動打開 tying,計算結果即與標籤一致)。本章原文沿用 GPT-2 論文的說法,稱 medium / large / XL 為 345M / 762M / 1,542M——兩套數字的差異來自後來的參數計數校正。
n_heads 不影響參數量(只把 emb_dim 切成幾份)
weight tying(共用 token embedding)
總參數
163.0M
記憶體(float32)≈ 621.83 MB
4.7

Generating text(生成文字)💬 推論

input:idx
[1, n]
→model → 取 [:, -1, :] → softmax → argmax只看最後一個位置的預測→output:idx
[1, n+1](接回再跑,迴圈)

生成式模型一次產生一個 token、接龍式寫句子。單步機制:模型輸出 logits → 只取最後一個位置那條 50257 維向量 → softmax 轉機率 → argmax 挑最大 → 把新 token ID 接回輸入 → 再跑一輪。

def generate_text_simple(model, idx, max_new_tokens, context_size):
    for _ in range(max_new_tokens):
        idx_cond = idx[:, -context_size:]        # 超長就裁掉前面
        with torch.no_grad():                    # 推論不用算梯度,省時省記憶體
            logits = model(idx_cond)
        logits = logits[:, -1, :]                # (batch, n_token, vocab) → 只取最後一格
        probas = torch.softmax(logits, dim=-1)
        idx_next = torch.argmax(probas, dim=-1, keepdim=True)  # greedy:挑機率最高
        idx = torch.cat((idx, idx_next), dim=1)  # 接回序列尾巴
    return idx
softmax 其實可以省:softmax 是單調函數,不改變大小順序——logits 最大值的位置=softmax 後最大值的位置,直接對 logits 做 argmax 結果相同。這裡保留它,是為了把「logits → 機率」的過程完整展示。

實跑:模型還沒訓練,會發生什麼事?

把 "Hello, I am" 編碼成 [15496, 11, 314, 716]、模型切到 model.eval()(關掉 dropout),生成 6 個新 token。按「生成下一個 token」逐步看 ID array 長大——這些是書中用 seed 123 跑出的真實輸出:

亂碼是預期結果!模型權重還是隨機初始化的,當然語無倫次。這精準凸顯了:架構只是骨架,訓練才是賦予語言能力的靈魂——下一章 Pretraining 就來解決它。
▣

本章小結

  • Layer normalization 沿特徵維(dim=-1)把輸出調成 mean 0 / var 1,穩定訓練;scale/shift 可訓練、eps 防除零、unbiased=False 相容 GPT-2 權重。
  • GELU 平滑、負值保留非零梯度,比 ReLU 更適合深層 LLM。
  • FeedForward:768 → 3072 → 768,「膨脹—非線性—收縮」探索更豐富的表徵。
  • Shortcut connection 把輸入加回輸出;5 層實測:無捷徑第 1 層梯度 0.0002(消失)、有捷徑 0.22(穩定),差三個數量級。
  • Transformer block=attention(跨位置)+ feed forward(逐位置),Pre-LayerNorm+雙捷徑;形狀不變、內容重新編碼。
  • GPTModel=embedding + 12×block + final norm + output head;我們 163M,GPT-2 靠 weight tying 記為 124M,權重約 622 MB。
  • 生成=取最後一格 logits → softmax → argmax(greedy)→ 接回輸入循環;未訓練 → 亂碼。
✓

知識檢核

// 六題,作答後立即看解析

從零打造 LLM · 第 4 章互動教材 | 講解為原創文字,程式碼與數值對應 Sebastian Raschka《Build a Large Language Model (From Scratch)》Ch.4 的公開程式與執行輸出,非原文翻譯
頁面中的計算皆為瀏覽器即時運算 · 快捷鍵 T 切換主題