ข้ามไปยังเนื้อหา
Tayakorn

คู่มือ Transformer — ลบทุกอย่าง เหลือแค่กราฟ

LEVEL 4 · ระดับเทพ

อ่านโค้ดจริงทั้งชั้น

ถึงจุดที่เราประกอบทุกอย่างเข้าด้วยกัน โค้ดข้างล่างนี้คือ Transformer block หนึ่งชั้นเต็มแบบ decoder-only โครงเดียวกับที่ใช้ในโมเดลจริง และรันได้บนเครื่องที่ไม่มีการ์ดจอ

11.1 เฟสสื่อสาร — บรรทัดที่ต้องจับตา

import torch
import torch.nn as nn
import torch.nn.functional as F

torch.manual_seed(1337)          # ตัวเลขทุกตัวในบทนี้มาจาก seed นี้

class CausalSelfAttention(nn.Module):
    def __init__(self, n_embd, n_head, block_size):
        super().__init__()
        self.n_head, self.n_embd = n_head, n_embd
        self.attn = nn.Linear(n_embd, 3 * n_embd, bias=False)   # Q, K, V ในก้อนเดียว
        self.proj = nn.Linear(n_embd, n_embd, bias=False)
        self.register_buffer("mask", torch.tril(torch.ones(block_size, block_size)))

    def forward(self, x):
        B, T, C = x.shape
        q, k, v = self.attn(x).split(C, dim=2)                  # แยกสามทางที่นี่
        hs = C // self.n_head
        q = q.view(B, T, self.n_head, hs).transpose(1, 2)       # แตกเป็นหลายหัว
        k = k.view(B, T, self.n_head, hs).transpose(1, 2)
        v = v.view(B, T, self.n_head, hs).transpose(1, 2)

        att = (q @ k.transpose(-2, -1)) * hs ** -0.5            # คะแนนความเข้ากัน
        att = att.masked_fill(self.mask[:T, :T] == 0, float("-inf"))
        att = F.softmax(att, dim=-1)
        y = att @ v                                             # ถ่วงน้ำหนักแล้วบวก
        y = y.transpose(1, 2).contiguous().view(B, T, C)
        return self.proj(y), att

เทียบกับโค้ด numpy ในบทที่ 9 แล้วจะเห็นว่าแกนกลางเหมือนกันทุกบรรทัด สี่บรรทัดที่ขึ้นต้นด้วย att คือกลไกเดียวกับที่คุณรันไปแล้วเป๊ะ ๆ ส่วนที่เพิ่มมามีสามอย่างเท่านั้น: ทำหลายประโยคพร้อมกัน (B) แตกเป็นหลายหัว และรวมการสร้าง Q, K, V ไว้ในชั้นเดียวเพื่อความเร็ว

จุดที่ควรสังเกตเป็นพิเศษคือ masked_fill — นั่นคือหน้ากากจากบทที่ 10 ที่ปรากฏเป็นโค้ดหนึ่งบรรทัด และคือทั้งหมดที่แยก GPT ออกจาก BERT

11.2 ประกอบเป็นชั้นเต็ม — สังเกตเครื่องหมายบวก

class Block(nn.Module):
    def __init__(self, n_embd, n_head, block_size):
        super().__init__()
        self.ln1 = nn.LayerNorm(n_embd)
        self.sa = CausalSelfAttention(n_embd, n_head, block_size)
        self.ln2 = nn.LayerNorm(n_embd)
        self.mlp = nn.Sequential(
            nn.Linear(n_embd, 4 * n_embd),                      # ขยาย 4 เท่า
            nn.GELU(),
            nn.Linear(4 * n_embd, n_embd),                      # บีบกลับ
        )

    def forward(self, x):
        delta, att = self.sa(self.ln1(x))
        x = x + delta                                           # residual เส้นที่ 1 — สื่อสาร
        x = x + self.mlp(self.ln2(x))                           # residual เส้นที่ 2 — คิด
        return x, att

ทั้งชั้นมีแค่นี้จริง ๆ สองเฟสจากบทที่ 9 อยู่ในสองบรรทัดสุดท้าย และแต่ละบรรทัดเป็น x = x + ... ไม่ใช่ x = ...

เครื่องหมายบวกนั้นคือหัวใจ และมันสำคัญกว่าที่หน้าตาบอก — ชั้นนี้ไม่ได้แทนที่ข้อมูลเดิม มันคำนวณส่วนแก้แล้วบวกทับลงไปบนของเดิม

11.3 พิสูจน์ว่า residual คือทางหลัก

นี่คือการทดลองที่ทำให้เชื่อได้ด้วยตาตัวเอง: ถ้าเราเซ็ตชั้นสุดท้ายของทั้งสองเฟสให้เป็นศูนย์ทั้งหมด ส่วนแก้ก็จะเป็นศูนย์ แล้ว x = x + 0 ต้องได้ x กลับมาเป๊ะ — ชั้นทั้งชั้นควรกลายเป็นทางผ่านที่ไม่ทำอะไรเลย

B, T, n_embd, n_head = 1, 5, 8, 2
block = Block(n_embd, n_head, block_size=T)
x = torch.randn(B, T, n_embd)
y, att = block(x)

print("params in one block =", sum(p.numel() for p in block.parameters()))
print("x.shape =", tuple(x.shape), " -> y.shape =", tuple(y.shape))
print("\nattention of head 0 (rows = who is asking):")
print(att[0, 0].detach().numpy().round(2))

with torch.no_grad():                                  # ปิดส่วนแก้ทั้งสองเส้น
    for p in block.sa.proj.parameters():
        p.zero_()
    for p in block.mlp[-1].parameters():
        p.zero_()
    y_zero, _ = block(x)
print("\nblock becomes identity?", torch.allclose(y_zero, x, atol=1e-6))

ผลลัพธ์จริง:

params in one block = 840
x.shape = (1, 5, 8)  -> y.shape = (1, 5, 8)

attention of head 0 (rows = who is asking):
[[1.   0.   0.   0.   0.  ]
 [0.41 0.59 0.   0.   0.  ]
 [0.31 0.35 0.34 0.   0.  ]
 [0.22 0.28 0.22 0.28 0.  ]
 [0.34 0.19 0.21 0.15 0.11]]

block becomes identity? True

คำว่า True บรรทัดสุดท้ายคือคำตอบข้อที่สองของปริศนาในบทที่ 1 ที่จับต้องได้ ค่าเริ่มต้นของชั้นคือ "ไม่ทำอะไร" ไม่ใช่ "ทำอะไรมั่ว ๆ" ดังนั้นการซ้อนชั้นเพิ่มเข้าไปจึงไม่เคยทำให้ของเดิมพัง อย่างแย่ที่สุดชั้นใหม่ก็เรียนรู้ที่จะเงียบ นี่คือเหตุผลที่โมเดลลึกเป็นร้อยชั้นเทรนขึ้นได้ และเป็นเหตุผลที่รูปทรงของกราฟการคำนวณมีทางตรงจากชั้นแรกถึงชั้นสุดท้ายให้สัญญาณวิ่งผ่านได้ตลอด

สังเกตตารางน้ำหนักด้วย: สามเหลี่ยมเหมือนบทที่ 9 และแถวแรกเป็น [1, 0, 0, 0, 0] — กลไกเดียวกัน ต่างแค่ตัวเลข

อ่านแบบเต็มเล่ม