Skip to content
Tayakorn

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

LEVEL 3 · ระดับสูง

กราฟที่โน้ดคุยกัน

บทนี้คือแก่นทางเทคนิคของทั้งเล่ม และเราจะเข้าถึงมันด้วยแบบจำลองความคิดหนึ่งอันที่ทำให้ทุกอย่างที่เหลือกลายเป็นเรื่องเดาได้

เลิกมอง Transformer เป็นลำดับของคำ ให้มองเป็นกราฟ โทเค็นทุกตัวคือโน้ดหนึ่งโน้ด และการทำงานของทั้งสถาปัตยกรรมคือการสลับสองเฟสไปเรื่อย ๆ

เฟสชื่อทางเทคนิคเกิดอะไรขึ้น
สื่อสารmulti-head attentionโน้ดทุกตัวส่งข้อมูลถึงกันตามน้ำหนักที่คิดเอง
คิดMLP / feed-forwardโน้ดแต่ละตัวแปรรูปข้อมูลของตัวเองอย่างเป็นเอกเทศ ไม่คุยกับใคร

ชั้นหนึ่งชั้นคือสื่อสารหนึ่งครั้งแล้วคิดหนึ่งครั้ง ซ้อนกันหลายสิบชั้นก็คือสลับสื่อสาร-คิดหลายสิบรอบ เท่านั้นจริง ๆ

9.1 สามคำถามที่โน้ดทุกตัวต้องตอบ

ในเฟสสื่อสาร โน้ดแต่ละตัวสร้างเวกเตอร์สามตัวจากข้อมูลของตัวเอง และแต่ละตัวคือคำตอบของคำถามหนึ่งข้อ

  • Query — "ฉันกำลังมองหาข้อมูลแบบไหน"
  • Key — "ฉันมีข้อมูลลักษณะไหนอยู่กับตัว"
  • Value — "ถ้ามีใครสนใจฉัน เนื้อหาที่ฉันจะส่งให้คืออะไร"
เฟสสื่อสาร — โน้ดส่งข้อมูลถึงกันQโน้ดที่ถามหาอะไรอยู่Kทุกโน้ดมีอะไรอยู่กับตัวsoftmaxคะแนนกลายเป็นน้ำหนักรวมได้ 1Vถ่วงน้ำหนักแล้วบวกกันq @ k.T / sqrt(head)เฟสคิด — แต่ละโน้ดแปรรูปข้อมูลของตัวเองMLPMLPMLPMLPสังเกตว่าไม่มีเส้นเชื่อมสักเส้น — เฟสนี้ไม่มีการคุยกันเลย
FIG 9.1 จุดที่เน้นคือขั้นที่คะแนนกลายเป็นน้ำหนักรวมได้ 1 — หลังจากนั้น ผลลัพธ์เป็นเพียงผลรวมถ่วงน้ำหนักของ V ทุกตัว ไม่ใช่การเลือกมาตัวเดียว

จากนั้นกลไกทำสี่ขั้นตรงไปตรงมา: เอา Query ของโน้ดที่ถาม ไปจับคู่กับ Key ของทุกโน้ด ด้วยการคูณแบบจุด (q @ k.T) ได้คะแนนความเข้ากัน → หารด้วยรากของขนาดช่องเพื่อคุมสเกล → ผ่าน softmax ให้กลายเป็นน้ำหนักที่รวมกันได้ 1 → เอาน้ำหนักไปถ่วง Value ของทุกโน้ดแล้วบวกกัน

💡 อุปมาฐานข้อมูล และรอยที่มันรั่ว — Q คือคำค้น K คือดรรชนี V คือเนื้อหา ดีตรงที่สื่อว่าเป็นการค้นหา แต่รั่วตรงที่ ฐานข้อมูลจริงเลือกมาหนึ่งแถวที่ตรง ส่วน attention เอาทุกแถวมาผสมกันตามน้ำหนัก ไม่เคยเลือกแค่อันเดียว คำว่า "นุ่มนวล" ใน soft search ของบทที่ 7 หมายถึงตรงนี้พอดี

9.2 ลองพิสูจน์ด้วยตาเอง

อย่าเชื่อคำอธิบายข้างบน รันเองแล้วดู โค้ดนี้ใช้ numpy ล้วน ไม่มีไลบรารีเรียนรู้ของเครื่องเลย และครบทั้งกลไก

import numpy as np
np.set_printoptions(precision=2, suppress=True)

T, C, head = 4, 6, 3                                   # 4 โทเค็น · 6 ช่องข้อมูล · หัวขนาด 3
x = np.arange(T * C, dtype=np.float64).reshape(T, C) / 10.0

rng = np.random.default_rng(1337)
Wq = rng.normal(size=(C, head)) * 0.5
Wk = rng.normal(size=(C, head)) * 0.5
Wv = rng.normal(size=(C, head)) * 0.5

q, k, v = x @ Wq, x @ Wk, x @ Wv                       # สามคำถามของทุกโน้ด

scores = q @ k.T / np.sqrt(head)                       # แถว i = โน้ด i ถามหาใคร
print("scores (raw) =\n", scores)

mask = np.tril(np.ones((T, T), dtype=bool))
masked = np.where(mask, scores, -np.inf)               # ปิดอนาคตด้วยลบอนันต์
print("\nscores (after causal mask) =\n", masked)

e = np.exp(masked - masked.max(axis=-1, keepdims=True))
w = e / e.sum(axis=-1, keepdims=True)                  # softmax ทีละแถว
print("\nweights (after softmax) =\n", w)
print("row sums =", w.sum(axis=-1))

out = w @ v                                            # ถ่วงน้ำหนักแล้วบวก
print("\nout[0] == v[0] ?", np.allclose(out[0], v[0]))

ผลลัพธ์จริงที่ได้:

scores (raw) =
 [[-0.14 -0.27 -0.39 -0.52]
 [-0.1  -0.08 -0.06 -0.04]
 [-0.06  0.11  0.28  0.44]
 [-0.02  0.3   0.61  0.93]]

scores (after causal mask) =
 [[-0.14  -inf  -inf  -inf]
 [-0.1  -0.08  -inf  -inf]
 [-0.06  0.11  0.28  -inf]
 [-0.02  0.3   0.61  0.93]]

weights (after softmax) =
 [[1.   0.   0.   0.  ]
 [0.49 0.51 0.   0.  ]
 [0.28 0.33 0.39 0.  ]
 [0.15 0.2  0.28 0.38]]
row sums = [1. 1. 1. 1.]

out[0] == v[0] ? True

มีสามอย่างในผลลัพธ์นี้ที่คุ้มกับการหยุดดูให้ดี

หนึ่ง — ตารางกลายเป็นสามเหลี่ยม ครึ่งบนขวาเป็น -inf ทั้งหมด เพราะเราปิดไม่ให้โน้ดมองอนาคต (บทที่ 10 จะเล่าว่าทำไม)

สอง — -inf หายไปกลายเป็น 0 พอดี นี่ไม่ใช่ความบังเอิญ softmax ต้องยกกำลัง e ของทุกค่า และ e ยกกำลังลบอนันต์เท่ากับศูนย์เป๊ะ นี่คือเหตุผลว่าทำไมต้องเป็นลบอนันต์ ไม่ใช่เลขติดลบมาก ๆ ถ้าใช้ค่าอย่างลบหนึ่งพัน มันจะเหลือน้ำหนักจิ๋วหนึ่งค้างอยู่ ซึ่งแปลว่ายังมีข้อมูลจากอนาคตรั่วเข้ามา

สาม — แถวแรกได้น้ำหนัก [1, 0, 0, 0] และ out[0] เท่ากับ v[0] เป๊ะ โน้ดตัวแรกไม่มีใครให้มองนอกจากตัวเอง น้ำหนักจึงเทไปที่ตัวเองทั้งหมด และผลลัพธ์ที่ได้คือ Value ของตัวเองแบบไม่ผสมอะไร — นี่คือหลักฐานว่ากลไกทั้งหมดเป็นเพียงการถ่วงน้ำหนักแล้วบวกกัน ไม่มีเวทมนตร์อะไรเกินกว่านั้น

9.3 แล้วเฟสคิดล่ะ

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

การแยกสองหน้าที่นี้ออกจากกันเด็ดขาดคือความงามทางการออกแบบของ Transformer — รับส่งข้อมูลเป็นเรื่องของเฟสหนึ่ง แปรรูปข้อมูลเป็นเรื่องของเฟสสอง ไม่ปนกัน

Read the full book