راهنمای تصویریِ «Let’s build GPT: from scratch, in code, spelled out»
هر فصل به دقیقهای از ویدیوی ۱ ساعت و ۵۶ دقیقهایِ آندری کارپاتی وصل است.
ویدیو را پخش کن، و هرجا گیر کردی فصلِ متناظر را همینجا باز کن.
شهود
کامپیوتر فقط عدد میفهمد
شبکهی عصبی چیزی جز ضرب و جمعِ عدد نیست. حرف «م» برایش هیچ معنایی ندارد — پس قبل از هر کاری، هر حرف باید یک عدد بگیرد. کل این فصل همین یک کار است: متن ← عدد.
محرف
←
40عدد
←
🧮
شبکهی عصبی (فقط حساب میکند)
مثل شمارهی صندلی در کلاس: هر حرف یک صندلی ثابت دارد. «م» همیشه صندلی ۴۰ است.
در این تبدیل نه چیزی «یاد گرفته» میشود و نه چیزی گم میشود — فقط یک جدول ساده است.
مثال آسان · گام ۱
کتابی به اندازهی یک جمله
قبل از سعدی، با کوچکترین «کتاب» دنیا تمرین میکنیم — همان جملهای که خواندن را با آن یاد گرفتید. فقط ۱۱ حرف.
بابا·آب·داد
فاصله را با · نشان میدهیم — فاصله هم یک حرف است!
گام اول: حرفهای منحصربهفرد را پیدا کن. هر رنگ = یک حرف:
فاصله
آ
ا
ب
د
کل واژگان (vocabulary) ما: ۵ حرف. کتاب سعدی ۶۲ تا دارد — همین مفهوم، فقط بزرگتر.
مثال آسان · گام ۲
دفترچهی دوطرفه: حرف ⟷ عدد
حرفها را مرتب میکنیم و از صفر شماره میدهیم. دو دیکشنری میسازیم — مثل دفترچهتلفنی که از هر دو طرف کار میکند: stoi از حرف به عدد، itos از عدد به حرف.
همین دو خط، کلِ «توکنایزر» ماست. نه هوش مصنوعی، نه آموزش — فقط یک جدول.
مثال آسان · گام ۳
کل جمله، عدد میشود
encode() یعنی: هر حرف را در دفترچه نگاه کن و عددش را بنویس. دقت کن: حرفِ تکراری همیشه عدد یکسان میگیرد.
ب3
ا2
ب3
ا2
·0
آ1
ب3
·0
د4
ا2
د4
[3, 2, 3, 2, 0, 1, 3, 0, 4, 2, 4]
«ب» سه بار آمده — هر سه بار ۳. هیچ اطلاعاتی گم نشد: decode([3,2,3,2,0,1,3,0,4,2,4]) دوباره دقیقاً «بابا آب داد» میدهد.
مهمترین شهود
بازی «حدس حرف بعدی»
کل آموزش یک مدل زبانی همین بازی است: چند حرف ببین، حرف بعدی را حدس بزن. خودتان امتحان کنید — یادتان باشد کل کتاب ما «بابا آب داد» است:
بابا آ
زمینه (ورودی)
←
شبکهی عصبی
←
ب
حدسِ حرف بعدی
بابا آ←بقطعی! در کتاب ما بعد از «آ» همیشه «ب» آمده
بابا آب دا←داین هم قطعی
با←؟«با» دو بار آمده: یکبار بعدش «ب» بود (بابا)، یکبار «فاصله» (با·آب) — پس ۵۰٪–۵۰٪
مدل «معنا» یاد نمیگیرد — جدولِ احتمالِ «بعد از اینها معمولاً چه میآید» را یاد میگیرد.
روز اول کاملاً شانسی حدس میزند؛ با هر اشتباه، احتمالهایش را کمی اصلاح میکند. همین.
مثال آسان · گام ۴
از یک تکه، چند درس؟
مدل با پنجرهای محدود میخواند (اینجا block_size = 4). از همان ۴ حرف اولِ «بابا»، ۴ درس جداگانه ساخته میشود — زمینه هر بار یک حرف بلندتر:
ب←ابا ۱ حرف زمینه
با←ببا ۲ حرف
باب←ابا ۳ حرف
بابا←·با ۴ حرف — هدف: فاصله!
چرا زمینههای کوتاه هم تمرین میشوند؟ چون موقع تولید متن، مدل اول فقط ۱ حرف در دست دارد —
باید بتواند با هر طولی از زمینه حدس بزند.
پل
حالا همین کار — با کلیات سعدی
از اینجا به بعد هیچ مفهوم جدیدی نمیآید. فقط اعداد بزرگتر میشوند:
کتاب کوچک ما
کلیات سعدی
طول متن
11
1,191,008
حروف متمایز (واژگان)
5
62
اندازهی پنجره
4
8
کدِ لازم
دقیقاً همان دو دیکشنری
اگر اسلایدهای بعدی جایی گُنگ شد، برگرد به «بابا آب داد» — دقیقاً همان است.
مقیاس واقعی
متن خام
از کلیات سعدی شروع میکنیم — ۱٬۱۹۱٬۰۰۸ حرف
منت خدای را عز و جل که طاعتش موجب قربت است و به شکر اندرش مزید نعمت. هر نفسی که فرو میرود ممد حیات است و چون بر میآید مفرح ذات. از دست و زبان که برآید کز عهدهٔ شکرش به درآید؟ بنده همان به که ز تقصیر خویش عذر به درگاه خدای آورد
گام ۱
چه حرفهایی وجود دارند؟
هر حرف منحصربهفرد در کل متن را پیدا کن. سعدی فقط از ۶۲ حرف متمایز استفاده میکند — حروف، علائم نگارشی، ارقام، و دو حرف نامرئی.
نگه دار برای بزرگنمایی · بنفش = حروف خاص (نامرئی) · GPT-4 از ~۱۰۰٬۰۰۰ توکن استفاده میکند؛ ما از ۶۲
گام ۲
به هر حرف یک عدد اختصاص بده
همهی ۶۲ حرف را بر اساس کد یونیکدشان مرتب کن و به هر کدام یک شاخص بده. دو دیکشنری ساخته میشود: stoi (حرف ← عدد) و itos (عدد ← حرف).
حرف
شاخص
حرف
شاخص
↵
0
ز
28
·فاصله·
1
س
29
!
2
ش
30
،
8
ع
35
؟
10
غ
36
ا
17
ل
39
ب
18
م
40
ت
20
ن
41
ح
23
ه
42
خ
24
و
43
د
25
ک
48
ر
27
گ
49
ی
50
گام ۳
رمزگذاری و رمزگشایی یک کلمه
تابع encode() یک رشته را به اعداد صحیح تبدیل میکند. تابع decode() برمیگردد. فقط دو جستجوی دیکشنری است — کاملاً بدون اتلاف.
encode("سلام")
س29
ل39
ا17
م40
←
[29, 39, 17, 40]
⟷
decode([29, 39, 17, 40])
[29, 39, 17, 40]
→
سلام
هر حرف به دقیقاً یک عدد نگاشت میشود. هر عدد دقیقاً به یک حرف برمیگردد.
گام ۴
کل کتاب تبدیل به اعداد میشود
تابع encode() را روی همهی ۱٫۲ میلیون حرف اجرا کن. نتیجه یک تنسور تکی PyTorch از اعداد صحیح است — همان اطلاعات، شکل جدید.
۹۰٪ اول توکنها مدل را آموزش میدهند. ۱۰٪ آخر برای آزمایش تعمیم کنار گذاشته میشود — مدل هرگز روی این بخش آموزش نمیبیند، پس عملکرد خوب در اینجا یعنی واقعاً الگو را یاد گرفته.
توکن ۰توکن ۱٬۰۷۱٬۹۰۷توکن ۱٬۱۹۱٬۰۰۸
دادههای آموزشی — ۹۰٪
ارزیابی
۱٬۰۷۱٬۹۰۷ توکن۱۱۹٬۱۰۱ توکن
n = int(0.9 * len(data)) — یک برش ساده
گام ۶
پنجرهی زمینه (block_size = 8)
مدل نمیتواند کل کتاب را یکجا بخواند. در هر لحظه ۸ حرف میبیند. حرف نهم همان چیزی است که باید پیشبینی کند.
۹ حرف اول از train_data
م
ن
ت
·sp·
خ
د
ا
ی
←
·sp·
x[0]
x[1]
x[2]
x[3]
x[4]
x[5]
x[6]
x[7]
y
برش ۹ حرفی: منت خدای· — آغاز دیباچهی گلستان
گام ۷
یک بلاک = ۸ نمونه آموزشی
هر زیررشتهای درون بلاک یک نمونه آموزشی است. مدل یاد میگیرد حرف بعدی را از زمینههایی با طول ۱ تا ۸ پیشبینی کند.
حرف بعدی ← زمینه (فارسی راست به چپ)
گام ۸
۴ تکه تصادفی انتخاب کن
تابع get_batch() چهار موقعیت تصادفی در train_data انتخاب میکند و از هر کدام یک پنجرهی ۸ حرفی میگیرد. اینها یک دستهی آموزشی میشوند — همه به صورت موازی پردازش میشوند.
۴ ورودی ۸ حرفی تصادفی (نمونههای واقعی از متن سعدی):
۴ تکه از مکانهای کاملاً متفاوتی در مجموعه آموزشی ۱٫۱ میلیون توکنی میآیند.
نکتهی کلیدی
y همان x است — یک قدم جلوتر
جوابهای درست (y) چیز جدیدی نیستند: همان ردیف ورودیاند که یک حرف جابهجا شده. در جایگاه t، مدل زمینهی x[0..t] را میبیند و باید y[t] را حدس بزند — که دقیقاً همان x[t+1] است.
x
ر
ا
·
ع
ز
·
و
·
y
ا
·
ع
ز
·
و
·
ج
ستونها را عمودی مقایسه کن — بالایی و پایینی یکیاند. تنها حرفِ واقعاً تازه «ج» در انتهاست.
برای همین ساختن y «رایگان» است: x = data[i:i+8] و y = data[i+1:i+9] (این همان ردیف ۱ نارنجی اسلاید قبل است: «را عز و جل…»)
گام ۹
یک دسته = ۳۲ جفت آموزشی
هر یک از ۴ ردیف ۸ درس زمینه←هدف ارائه میدهد. پس هر فراخوانی get_batch() یکجا ۳۲ نمونهی «وقتی این را دیدی… این را پیشبینی کن» به مدل میدهد.
مدل بایگرام
سادهترین مدل ممکن: فقط حرفِ آخر
اولین مدل ما عمداً سادهلوح است: برای حدس حرف بعدی فقط به آخرین حرف نگاه میکند و بقیهی زمینه را دور میریزد. به این «بایگرام» میگویند: جفتِ (حرف فعلی ← حرف بعدی).
زمینه: «بابا آ» — اما مدل بایگرام فقط این را میبیند:
ب
ا
ب
ا
·
آ
←
ب
بله، این مدل ضعیف است — و عمدی است! ادامهی این مسیر دقیقاً دربارهی همین است که چطور از کل زمینه استفاده کنیم (attention). اول ساده، بعد قوی.
مدل بایگرام
کل «مغز» مدل: یک جدول ۵×۵
اگر فقط حرف آخر مهم است، مدل چیزی جز یک جدول لازم ندارد: هر سطر = حرف فعلی، هر ستون = شانسِ هر حرف بعدی. برای کتاب کوچک ما میشود جدول ایدهآل را با شمردن دستی ساخت:
فعلی ↓ / بعدی ←
·
آ
ا
ب
د
·
۰
۵۰٪
۰
۰
۵۰٪
آ
۰
۰
۰
۱۰۰٪
۰
ا
۳۳٪
۰
۰
۳۳٪
۳۳٪
ب
۳۳٪
۰
۶۷٪
۰
۰
د
۰
۰
۱۰۰٪
۰
۰
مثلاً سطر «ب»: در «بابا آب داد» بعد از ب دو بار «ا» آمد و یک بار فاصله ← ۶۷٪ و ۳۳٪. برای سعدی همین جدول ۶۲×۶۲ است — فقط ۳٬۸۴۴ عدد. کل مدل همین است!
مدل بایگرام · کد
nn.Embedding یعنی همین جدول
اسمش ترسناک است، کارش ساده: nn.Embedding(5, 5) همان جدول ۵×۵ را با اعداد تصادفی میسازد. وقتی عدد یک حرف را بدهی، فقط سطرِ همان شماره را برمیدارد:
self.token_embedding_table = nn.Embedding(vocab_size, vocab_size) logits = self.token_embedding_table(idx)# فقط یک جستجوی سطر
«ب» = عدد ۳ ← سطر ۳ بیرون میآید. همین. نه ضرب ماتریسی، نه جادو — جستجو در جدول. این اعداد اول تصادفیاند؛ آموزش بعداً درستشان میکند.
مدل بایگرام · سؤال خوب!
این اعداد (2.0, -1.0, …) از کجا میآیند؟
دو منبع دارند — و بهتنهایی هیچ معنایی ندارند؛ فقط در مقایسه با بقیهی اعدادِ همان سطر معنا پیدا میکنند:
روز اولکاملاً تصادفی! nn.Embedding اعداد را از یک توزیع زنگولهای حول صفر میکشد. torch.manual_seed(1337) فقط این تصادف را تکرارپذیر میکند تا هر بار اجرای کد، همان نتیجه تکرار شود.
هر گام آموزشهر عدد کمی هُل میخورد: نمرهی جوابِ درست بالا میرود، بقیه پایین. بعد از هزاران گام، اعداد «شکل» میگیرند — کسی آنها را ننوشته.
و چرا خودِ درصد را ذخیره نکنیم؟ چون فقط تفاوتِ نمرهها مهم است — هر دو سطر زیر یک نتیجه میدهند:
سطر دوم = سطر اول + ۳ به همه — خروجی یکسان، چون فاصلهی بین نمرهها همان است
درصدها قید دارند (بین ۰ و ۱۰۰، جمع = ۱۰۰) و با هُلدادنِ آزادانه خراب میشوند.
نمرهی آزاد (هر عددی، حتی منفی) + softmax = همیشه یک توزیع سالم. منفی یعنی «خیلی بعید»، نه «منفی»!
مدل بایگرام
لاجیت = نمرهی خام، نه احتمال
به اعداد داخل سطر «لاجیت» (logits) میگویند: نمرههای خام. هرچه بزرگتر، یعنی مدل آن حرف را محتملتر میداند. تابع softmax این نمرهها را به درصد واقعی تبدیل میکند:
سطرِ «ب» پس از کمی آموزش:
لاجیت: [ 1.0, -2.0, 2.0, -1.0, -1.0 ]
↓ softmax ↓
· ۲۵٪
آ ۱٪
ا ۶۷٪
ب ۳٪
د ۳٪
دارد به جدول دستی ما نزدیک میشود (ا: ۶۷٪، فاصله: ۳۳٪)! softmax فقط نرمالسازی است (eⁿ تقسیم بر مجموع) — ترتیب نمرهها را عوض نمیکند.
مدل بایگرام · کد
خروجی (4, 8, 5) — جعبهی سهبعدی
آدرسِ هر عدد سه بخش دارد: out[b, t, c] — کدام تکه؟ کدام جایگاه در آن تکه؟ نمرهی کدام حرف؟ از هر خانه یک بستهی ۵ نمرهای به عمق میرود؛ اینجا فقط بستهی یک خانه رسم شده، ولی هر ۳۲ خانه یکی دارند:
T: جایگاه ۰ (راست) تا ۷ (چپ) ←
B: تکهی ۰ تا ۳ ↓
1.0·
-2.0آ
2.0ا
-1.0ب
-1.0د
ب
ا
ب
ا
·
آ
ب
·
ب
ا
·
آ
ب
·
د
ا
ا
ب
ا
·
آ
ب
·
د
ب
ا
ب
ا
·
آ
ب
·
C: عمق — بستهی ۵ نمرهای داخل هر خانه
🖱 بگیر و بچرخان — هر جا خواستی نگه دار · دابلکلیک = برگشت به حالت اول
b — کدام تکهی تصادفی؟
همان ۴ تکهای که get_batch() از جاهای تصادفی متن برداشت. b=0 یعنی تکهی اول، b=3 یعنی چهارم.
t — کدام حرف داخل آن تکه؟
جایگاه ۰ تا ۷ درون تکه. هر خانه = یک حرف از آن تکه.
c — نمرههای حرفِ همان خانه
سطری از جدول که برای حرفِ خودِ آن خانه بیرون کشیده شد — نه یک سطرِ ثابت برای همه. فقط خانههایی که حرف یکسان دارند (مثلاً هر دو «ب») کپیِ همان سطر را میگیرند، چون مدل بایگرام چیز دیگری از زمینه نمیبیند.
مثال: out[0, 0, 2] یعنی «تکهی ۰، حرفِ اول (ب)، نمرهی حرف شمارهی ۲ (ا)» ← همان 2.0 در عمق بسته.
۴ × ۸ × ۵ = ۱۶۰ عدد در کل. برای سعدی هر بسته ۶۲تایی است: (4, 8, 62).
کد · نقشه
نقشهی کد: سه قطعه، دو مسیر
کل کلاس سه قطعه است. و دو مسیر از آن رد میشود: آموزش (با جوابها) و تولید (بدون جواب):
__init__جدول (۵×۵ یا ۶۲×۶۲) را با اعداد تصادفی میسازد — مغزِ مدل
forwardبرای هر خانه پیشبینی میکند؛ اگر targets بدهی، نمره (loss) هم حساب میکند
generateحلقهی نوشتن: پیشبینی ← درصد ← قرعهکشی ← بچسبان — به تعداد max_new_tokens بار
مسیر آموزش — m(xb, yb)
xb, yb → forward → logits, loss
loss میگوید حدسها چقدر بد بود — از همان برای اصلاح جدول استفاده میشود.
مسیر تولید — m.generate(idx, 100)
idx → forward → probs → حرف جدید
targets=None، پس loss در کار نیست — فقط حرفِ بعدی، صد بار پشت سر هم.
کد · forward — قلبِ ماجرا
forward یعنی چه؟ فقط یک جستجوی سطر — برای هر خانه
هیچ محاسبهای نیست. برای هر خانه، حرفِ همان خانه را در جدول پیدا میکند و سطرش را برمیدارد — همان سطر، خودِ حدس است. روی خانهها کلیک کن؛ و جدول را بین «روز اول» و «بعد از آموزش» عوض کن:
جدولِ مدل:
xb — دستهی (۴، ۸)؛ روی یک خانه کلیک کن
جستجوی سطر ←
روز اول جدول تصادفی است ← بلندترین ستون هم تصادفی است و ربطی به جوابِ درست ندارد ← میانگینِ «تعجب» روی ۳۲ خانه ≈ ln(۵) ≈ ۱٫۶۱. حرفِ یکسان ⟵ سطرِ یکسان ⟵ حدسِ یکسان. مدل بایگرام چیزی جز حرفِ همان خانه نمیبیند، پس همهی «ب»ها یک جواب میگیرند.
کد · forward — یک تابع، دو حالت
forward یک تابع است با یک دوراهی
خطِ اول همیشه اجرا میشود — همان جستجوی سطر. بعد یک if: آیا targets دادی؟ همین یک سؤال، دو مسیرِ کاملاً متفاوت میسازد. دکمه را عوض کن:
چه اتفاقی میافتد؟
برمیگرداند
چه کسی اینطور صدایش میزند؟
هسته یکی است: جستجو ← logits. سؤالِ targets فقط تعیین میکند که آیا حدسها را «تصحیح» هم بکنیم (loss) یا خام تحویل بدهیم. پیشفرضِ targets=None یعنی حالتِ عادی (تولید) رایگان است؛ فقط وقتِ آموزش targets میدهی.
کد · مسیر آموزش
مسیر آموزش: هر متغیر، یک شکل
forward همان جستجوی اسلاید قبل را برای هر ۳۲ خانه یکجا انجام میدهد ← ۳۲ حدس، کنار هم در جعبهی (۴،۸،۵). حالا برای گرفتنِ یک loss، فقط «بستهبندی» عوض میشود — هیچ عددی وسط راه عوض نمیشود:
xb — (4, 8) حرفها به شکل عدد
جستجوی جدول ←
logits — (4, 8, 5) جعبهی سهبعدی
.view(32, 5) ←
logits — (32, 5) فهرست بلند
yb — (4, 8) جوابهای درست
.view(32) ←
targets — (32,)
cross_entropy ←
1.61
loss — فقط یک عدد!
view جعبهی سهبعدی را به یک فهرست ۳۲ سطری باز میکند تا cross_entropy بتواند ۳۲ پیشبینی را یکجا با ۳۲ جواب مقایسه کند. کل مسیر در یک عدد خلاصه میشود: loss. (۱.۶۱ = نمرهی حدسِ شانسی برای واژگان ۵تایی)
کد · loss
cross_entropy: نمرهی امتحانِ ۳۲ سؤالی
برای هر خانه فقط یک چیز مهم است: به جوابِ درست چند درصد احتمال داده بودی؟ «تعجب» = −log(آن درصد). خوب بلد باشی، کم تعجب میکنی:
حرفِ خانه
جواب درست
احتمالی که مدل داد
تعجب (−log)
ب
ا
۶۷٪
0.40
ب
·
۳۳٪
1.10
ا
د
۳۳٪
1.10
د
ا
۱۰۰٪
0.00
loss = میانگین تعجب روی هر ۳۲ خانه: (0.40 + 1.10 + 1.10 + 0.00 + …) ÷ 32
خطکش: حدسِ کاملاً شانسی ← loss = ln(۵) ≈ 1.61 برای کتاب کوچک، ln(۶۲) ≈ 4.13 برای سعدی. اگر اولین print(loss) چیزی حدود ۴ نشان داد، همهچیز سرِ جایش است — مدل هنوز شانسی است. کمال مطلق: loss = 0.
مدل بایگرام · آموزش زنده
آموزش: سطر را به سمت واقعیت هُل بده
جدول تصادفی شروع میشود، پس حدسها شانسی است. هر گامِ آموزش سطرِ «آ» را کمی هُل میدهد: نمرهی جوابِ درست («ب») بالا، بقیه پایین. دکمه را بزن و loss را تماشا کن:
سطرِ «آ» — بعد از «آ» چه میآید؟ (در «بابا آب داد» جوابِ واقعی همیشه «ب» است)
0
گامِ آموزش
—
loss این سطر
هیچکس به مدل نگفت بعد از «آ» چه میآید — خودش از مقایسه با جوابِ درست فهمید. اینجا loss تا نزدیکِ ۰ میرود چون «آ» همیشه به «ب» میرسد. اما برای «ا» (که گاهی ←ا، گاهی ←ب، گاهی ←د میشود) loss هرگز به ۰ نمیرسد — خودِ داده نامطمئن است.
کد · generate — اجرای زنده
حلقهی generate را خودت اجرا کن
مدلِ آموزشدیدهی «بابا آب داد». دکمهی «گام بعدی» را بزن — همان کد، خط به خط، زنده:
متن تا الان (idx):
سطرِ حرفِ آخر — نمرهها / درصدها:
این همان حلقه است، فقط با جدولِ آموزشدیده — ستونها تیز شدهاند، پس قرعهکشی معمولاً حرفِ درست را درمیآورد. روز اول (جدول تصادفی) همین ستونها تخت بودند ← قرعهکشی تصادفی ← خروجی: آشغال. آموزش است که اینها را تیز کرد.
جمعبندی
کل فصل در یک نگاه
۱واژگان بساز— ۶۲ حرف متمایز در کل سعدی
۲حرف ⟷ عدد— stoi و itos؛ فقط یک جدول، بدون هوش
۳کل کتاب را encode کن— تنسور ۱٬۱۹۱٬۰۰۸ عددی، تقسیم ۹۰/۱۰
۴دستههای x و y بساز— y همان x است، یک قدم جلوتر؛ ۳۲ درس در هر گام
۷آموزش— حدس بزن، با y مقایسه کن (loss)، جدول را اصلاح کن، تکرار
۸تولید (generate)— حرف آخر ← درصدها ← قرعهکشی وزندار ← بچسبان ← تکرار
«کتاب را عدد کن، بازیِ حدسِ حرف بعدی بساز، و یک جدول را آنقدر اصلاح کن تا خوب حدس بزند.» قدمهای بعدی فقط مدل را قویتر میکنند (جدول ← ترنسفورمر) — دادهها و بازی همین میمانند.
بخش ۲ · پل
بایگرام کور است — بقیهی زمینه کجا رفت؟
تا اینجا مدل ما فقط حرفِ آخر را میدید. اما xb هشت جایگاه دارد و جایگاهِ هفتم، هفت حرفِ گذشته را در دسترس دارد که همه را دور میریزد. کلِ بقیهی ویدیو دقیقاً دربارهی همین است: چطور جایگاهها با هم حرف بزنند.
جایگاه ۵ میخواهد حرفِ بعدی را حدس بزند — چه چیزی در دسترس دارد؟
بایگرام (الان)
ب
ا
ب
ا
·
آ
۱ حرف از ۶
آنچه میخواهیم
ب
ا
ب
ا
·
آ
۶ حرف از ۶
ممنوع!
ب
ا
ب
ا
·
آ
ب
·
آینده — تقلب است
دو قید داریم و هر دو مهماند: هر جایگاه باید همهی گذشته را ببیند،
و هیچوقت آینده را — چون موقع تولیدِ متن، آینده هنوز وجود ندارد.
کارپاتی این را «causal» یا «autoregressive» مینامد. چهار نسخه لازم است تا به شکل نهایی برسیم.
بخش ۲ · نسخه ۱
سادهترین «حرف زدن با گذشته»: میانگین بگیر
ضعیفترین راهِ ممکنِ ترکیبِ گذشته: بردارهای همهی جایگاههای تا اینجا را میانگین بگیر. روی هر جایگاه کلیک کن تا ببینی چه چیزی وارد میانگین میشود:
x — ۸ جایگاه؛ یکی را انتخاب کن
میانگینِ بردارها ← xbow[b,t]
# نسخه ۱ — دو حلقهی تودرتو، کندترین راه ممکن
xbow = torch.zeros((B,T,C))
for b in range(B):
for t in range(T):
xprev = x[b, :t+1] # (t+1, C) — فقط تا اینجا
xbow[b,t] = torch.mean(xprev, 0)
میانگین ترتیب را نابود میکند — «بابا» و «اباب» یک میانگین دارند. عمداً بد است؛ فقط میخواهیم شکلِ کار درست شود. مشکل واقعی: این دو حلقه روی GPU فاجعهاند. اسلاید بعد همین را به یک ضرب ماتریسی تبدیل میکند.
بخش ۲ · ترفندِ کلیدی
ترفند: میانگینِ گذشته = یک ضربِ ماتریسی
کارپاتی اینجا ویدیو را نگه میدارد و یک اسباببازیِ ۳×۳ نشان میدهد. سه دکمه را به ترتیب بزن — a عوض میشود و c = a @ b معنیِ کاملاً متفاوتی پیدا میکند:
a (3×3)
@
b (3×2)
=
c (3×2)
بخش ۲ · نسخه ۲
نسخه ۲: دو حلقه ← یک خط
حالا همان ترفند را روی T = 8 اجرا میکنیم. ماتریسِ wei (وزنها) پایینمثلثیِ نرمالشده است: سطر t یعنی «برای ساختنِ خروجیِ جایگاه t، از هر جایگاهِ گذشته چقدر بردار؟»
wei — (8, 8)
هر سطر جمعش ۱ است ← میانگینِ وزندار
سطر ۰ — تنهاست
فقط 1.00 روی خودش. جایگاهِ اول گذشتهای ندارد، پس خروجیاش دقیقاً خودش است.
سطر ۷ — همه را میبیند
هشتتا 0.125 = ۱/۸. میانگینِ سادهی هر ۸ جایگاه.
بالای قطر — صفرِ خالص
هیچ سطری به راستِ قطر وزن نمیدهد. همان قیدِ «آینده ممنوع».
wei شکلِ (T,T) دارد ولی x شکلِ (B,T,C) — پایتورچ wei را روی هر B تکرار میکند (broadcasting) و B ضربِ موازی انجام میدهد. دقیقاً همان اعداد، ولی اینبار روی GPU. حالا سؤال: چرا به همین بسنده نکنیم؟
بخش ۲ · نسخه ۳
همان نتیجه، با راهِ عجیبِ -inf و softmax
نسخه ۳ دقیقاً همان اعداد نسخه ۲ را میدهد، ولی از راهی که در نگاه اول بیخود پیچیده است. دلیلش را انتهای اسلاید میبینی. سه مرحله را جلو ببر:
چرا اینهمه دردسر؟ چون در نسخه ۲ اعدادِ wei ثابت و دستی بودند.
اینجا خطِ wei = zeros فقط یک جای خالی است: در اسلایدهای بعد، بهجای صفر، عددی میآید که از خودِ داده محاسبه میشود —
و -infها تضمین میکنند هرچقدر هم آن اعداد یاد بگیرند، آینده همیشه صفر بماند.
بخش ۲ · موقعیت
مدل نمیداند هر حرف کجاست
تا اینجا ورودیِ مدل فقط «کدام حرف» بود. اما میانگینگیری (و بعداً توجه) نسبت به ترتیب بیتفاوت است — پس باید موقعیت را هم دستی به مدل بدهیم. یک جدولِ دومِ embedding، اینبار برای شمارهی جایگاه:
دو «ب» در «بابا» — همان حرف، دو جایگاهِ متفاوت
«ب» جایگاه ۰tok_emb[ب]+pos_emb[0]=x₀
«ب» جایگاه ۲tok_emb[ب]+pos_emb[2]=x₂
سمتِ چپ یکی است، سمتِ راست فرق دارد ← x₀ ≠ x₂. حالا مدل میتواند «اولین ب» را از «دومین ب» تشخیص دهد.
توجه کن که اینجا یک تغییرِ ساختاریِ دیگر هم افتاد: n_embd از vocab_size جدا شد. در بایگرام، جدول 62×62 بود و خروجی مستقیماً لاجیت. حالا 62×n_embd است و یک lm_head خطی در انتها آن را به ۶۲ لاجیت برمیگرداند.
بخش ۲ · گرانیگاه ویدیو
خودتوجهی: پرسش، کلید، مقدار
میانگینِ یکنواخت احمقانه است — جایگاههای مختلف باید بهشکل متفاوتی جالب باشند. راهحل: هر ژتون دو بردار منتشر میکند؛ query «دنبال چه میگردم» و key «چه دارم». شباهتشان (ضربِ نقطهای) وزن میشود.
نمایش wei:
کدام جایگاه پرسش میکند؟ (کلیک)
خروجی = جمعِ وزندارِ valueها
head_size = 16
key = nn.Linear(n_embd, head_size, bias=False)
query = nn.Linear(n_embd, head_size, bias=False)
value = nn.Linear(n_embd, head_size, bias=False)
k = key(x) # (B,T,16)
q = query(x) # (B,T,16)
wei = q @ k.transpose(-2,-1) # (B,T,T) ← شباهتِ هر پرسش با هر کلید
wei = wei.masked_fill(tril == 0, float('-inf'))
wei = F.softmax(wei, dim=-1)
out = wei @ value(x) # (B,T,16)
نکتهی ظریف: خروجی جمعِ وزندارِ x نیست، جمعِ وزندارِ value(x) است. x اطلاعاتِ خصوصیِ ژتون است؛ value یعنی «اگر به من توجه کردی، این را به تو میدهم». سه نقشِ جدا: چه میخواهم (q)، چه دارم (k)، چه میدهم (v).
بخش ۲ · «scaled» یعنی چه؟
چرا تقسیم بر √d؟
در مقاله اسمش «Scaled Dot-Product Attention» است. آن / sqrt(head_size) یک تزئین نیست — بدونش مدل در همان گام اول خفه میشود. لغزنده را بکش و به شکلِ softmax نگاه کن:
بزرگیِ لاجیتها
اگر q و k واریانسِ ۱ داشته باشند، q·k واریانسِ head_size پیدا میکند — یعنی با head_size=16 اعداد چهار برابر پخشتر میشوند. softmax روی اعدادِ پخش، به یک one-hot میل میکند: هر ژتون فقط به یک جایگاه نگاه میکند و گرادیان تقریباً صفر میشود. تقسیم بر √16 = 4 واریانس را به ۱ برمیگرداند.
بخش ۲ · شش یادداشت
شش نکته که کارپاتی جدا میگوید
بعد از ساختِ خودتوجهی، ویدیو شش یادداشتِ کوتاه دارد. هرکدام یک سوءتفاهمِ رایج را میبندد:
۱ · توجه = ارتباط، نه مکان
توجه را مثل یک گرافِ جهتدار ببین: هر گره به گرههای دیگر پیام میفرستد. هیچجای آن ذاتاً «دنباله» نیست — ما با tril آن را دنبالهای کردیم.
۲ · هیچ حسی از فاصله ندارد
اگر جای دو ژتونِ گذشته را عوض کنی، خروجی عوض نمیشود. برای همین pos_emb لازم است — برخلاف کانولوشن، مکان رایگان نیست.
۳ · بین نمونههای دسته خبری نیست
هر یک از B نمونه کاملاً مستقل پردازش میشود. wei شکلِ (B,T,T) دارد — یک ماتریسِ جدا برای هر نمونه.
۴ · encoder در برابر decoder
خطِ masked_fill را حذف کن ← همه با همه حرف میزنند ← encoder. نگهش داری ← decoder. تفاوت فقط همان یک خط است.
۵ · self در برابر cross
self: کلید، پرسش و مقدار همه از یک x. cross: پرسش از x، ولی کلید و مقدار از منبعِ دیگری (مثلاً خروجیِ یک encoder).
۶ · «scaled» ضروری است
تقسیم بر √head_size واریانسِ لاجیتها را نزدیک ۱ نگه میدارد؛ وگرنه softmax در گامِ اول به one-hot میرود. اسلاید قبل.
نقشهی راه
بخش ۳ — از یک سرِ توجه تا ترنسفورمر
اینجا مرزِ فعلیِ این سند است. تا این لحظه یک سرِ خودتوجهی داریم. آنچه باقی مانده، همهاش تکرار و پشتهکردن همین قطعه است — با چند ترفندِ آموزشپذیری:
1:19:11جاگذاریِ یک بلوکِ خودتوجهی در مدلهنوز ساخته نشده
1:21:59توجهِ چندسر — چند سرِ موازی، بعد بههم چسباندههنوز ساخته نشده
1:24:25لایهی feedforward — «فکر کردن» بعد از «حرف زدن»هنوز ساخته نشده
1:26:48اتصالاتِ باقیمانده (residual) — بزرگراهِ گرادیانهنوز ساخته نشده
1:32:04LayerNorm — و تفاوتش با مقالهی اصلیهنوز ساخته نشده
1:37:49بزرگکردنِ مدل و dropoutهنوز ساخته نشده
1:42:39encoder/decoder — و اینکه ما فقط decoder ساختیمهنوز ساخته نشده
1:46:22از nanoGPT تا ChatGPT — pretraining، SFT، RLHFهنوز ساخته نشده
خبرِ خوب: سختترین بخش گذشت. اگر خودتوجهی را فهمیدی، بقیه مهندسیِ ساختن است نه مفهومِ تازه. چندسر = همین قطعه، چهار بار موازی. بلوک = توجه + feedforward. ترنسفورمر = شش بلوک روی هم.