▶ ویدیو · ۱:۵۶:۲۰
شهود

کامپیوتر فقط عدد می‌فهمد

شبکه‌ی عصبی چیزی جز ضرب و جمعِ عدد نیست. حرف «م» برایش هیچ معنایی ندارد — پس قبل از هر کاری، هر حرف باید یک عدد بگیرد. کل این فصل همین یک کار است: متن ← عدد.

محرف
40عدد
🧮
شبکه‌ی عصبی
(فقط حساب می‌کند)
مثل شماره‌ی صندلی در کلاس: هر حرف یک صندلی ثابت دارد. «م» همیشه صندلی ۴۰ است.
در این تبدیل نه چیزی «یاد گرفته» می‌شود و نه چیزی گم می‌شود — فقط یک جدول ساده است.
مثال آسان · گام ۱

کتابی به اندازه‌ی یک جمله

قبل از سعدی، با کوچک‌ترین «کتاب» دنیا تمرین می‌کنیم — همان جمله‌ای که خواندن را با آن یاد گرفتید. فقط ۱۱ حرف.

بابا·آب·داد
فاصله را با · نشان می‌دهیم — فاصله هم یک حرف است!

گام اول: حرف‌های منحصربه‌فرد را پیدا کن. هر رنگ = یک حرف:

فاصله
آ
ا
ب
د
کل واژگان (vocabulary) ما: ۵ حرف. کتاب سعدی ۶۲ تا دارد — همین مفهوم، فقط بزرگ‌تر.
مثال آسان · گام ۲

دفترچه‌ی دوطرفه: حرف ⟷ عدد

حرف‌ها را مرتب می‌کنیم و از صفر شماره می‌دهیم. دو دیکشنری می‌سازیم — مثل دفترچه‌تلفنی که از هر دو طرف کار می‌کند: stoi از حرف به عدد، itos از عدد به حرف.

حرفعدد
فاصله0
آ1
ا2
ب3
د4
stoi = {' ':0, 'آ':1, 'ا':2, 'ب':3, 'د':4}
itos = {0:' ', 1:'آ', 2:'ا', 3:'ب', 4:'د'}
همین دو خط، کلِ «توکنایزر» ماست. نه هوش مصنوعی، نه آموزش — فقط یک جدول.
مثال آسان · گام ۳

کل جمله، عدد می‌شود

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). از همان ۴ حرف اولِ «بابا»، ۴ درس جداگانه ساخته می‌شود — زمینه هر بار یک حرف بلندتر:

بابا ۱ حرف زمینه
باببا ۲ حرف
بابابا ۳ حرف
بابا·با ۴ حرف — هدف: فاصله!
چرا زمینه‌های کوتاه هم تمرین می‌شوند؟ چون موقع تولید متن، مدل اول فقط ۱ حرف در دست دارد —
باید بتواند با هر طولی از زمینه حدس بزند.
پل

حالا همین کار — با کلیات سعدی

از اینجا به بعد هیچ مفهوم جدیدی نمی‌آید. فقط اعداد بزرگ‌تر می‌شوند:

کتاب کوچک ماکلیات سعدی
طول متن111,191,008
حروف متمایز (واژگان)562
اندازه‌ی پنجره48
کدِ لازمدقیقاً همان دو دیکشنری
اگر اسلایدهای بعدی جایی گُنگ شد، برگرد به «بابا آب داد» — دقیقاً همان است.
مقیاس واقعی

متن خام

از کلیات سعدی شروع می‌کنیم — ۱٬۱۹۱٬۰۰۸ حرف

منت خدای را عز و جل که طاعتش موجب قربت است و به شکر اندرش مزید نعمت. هر نفسی که فرو می‌رود ممد حیات است و چون بر می‌آید مفرح ذات.
از دست و زبان که برآید
کز عهدهٔ شکرش به در‌آید؟
بنده همان به که ز تقصیر خویش
عذر به درگاه خدای آورد
گام ۱

چه حرف‌هایی وجود دارند؟

هر حرف منحصربه‌فرد در کل متن را پیدا کن. سعدی فقط از ۶۲ حرف متمایز استفاده می‌کند — حروف، علائم نگارشی، ارقام، و دو حرف نامرئی.

نگه دار برای بزرگ‌نمایی  ·  بنفش = حروف خاص (نامرئی)  ·  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 از اعداد صحیح است — همان اطلاعات، شکل جدید.

۹ حرف اول ← ۹ عدد صحیح اول (رنگ‌بندی مطابق)
منت خدای
40, 41, 20, 1, 24, 25, 17, 50, 1, …
tensor([40, 41, 20, 1, 24, 25, 17, 50, 1, ...,
                                   ×1,191,008 total])
شکل: torch.Size([1191008])، نوع: torch.int64
گام ۵

تقسیم: آموزش در برابر ارزیابی

۹۰٪ اول توکن‌ها مدل را آموزش می‌دهند. ۱۰٪ آخر برای آزمایش تعمیم کنار گذاشته می‌شود — مدل هرگز روی این بخش آموزش نمی‌بیند، پس عملکرد خوب در اینجا یعنی واقعاً الگو را یاد گرفته.

توکن ۰توکن ۱٬۰۷۱٬۹۰۷توکن ۱٬۱۹۱٬۰۰۸
داده‌های آموزشی — ۹۰٪
ارزیابی
۱٬۰۷۱٬۹۰۷ توکن۱۱۹٬۱۰۱ توکن
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)  # فقط یک جستجوی سطر
row 0 (·): [ 0.1, -0.4, 1.1, -0.2, 0.0]
row 1 (آ): [-0.7, 0.2, 0.3, 1.5, -1.0]
row 2 (ا): [ 0.9, -1.2, 0.1, 0.8, 0.7]
row 3 (ب): [ 1.0, -2.0, 2.0, -1.0, -1.0] ◄ idx = 3
row 4 (د): [-0.3, 0.5, 2.2, 0.1, -0.9]
«ب» = عدد ۳ ← سطر ۳ بیرون می‌آید. همین. نه ضرب ماتریسی، نه جادو —
جستجو در جدول. این اعداد اول تصادفی‌اند؛ آموزش بعداً درستشان می‌کند.
مدل بایگرام · سؤال خوب!

این اعداد (2.0, -1.0, …) از کجا می‌آیند؟

دو منبع دارند — و به‌تنهایی هیچ معنایی ندارند؛ فقط در مقایسه با بقیه‌ی اعدادِ همان سطر معنا پیدا می‌کنند:

روز اول کاملاً تصادفی! nn.Embedding اعداد را از یک توزیع زنگوله‌ای حول صفر می‌کشد. torch.manual_seed(1337) فقط این تصادف را تکرارپذیر می‌کند تا هر بار اجرای کد، همان نتیجه تکرار شود.
هر گام آموزش هر عدد کمی هُل می‌خورد: نمره‌ی جوابِ درست بالا می‌رود، بقیه پایین. بعد از هزاران گام، اعداد «شکل» می‌گیرند — کسی آن‌ها را ننوشته.
و چرا خودِ درصد را ذخیره نکنیم؟ چون فقط تفاوتِ نمره‌ها مهم است — هر دو سطر زیر یک نتیجه می‌دهند:
[ 1.0, -2.0, 2.0, -1.0, -1.0 ] — softmax → [ 25%, 1%, 67%, 3%, 3% ]
[ 4.0,  1.0, 5.0,  2.0,  2.0 ] — softmax → [ 25%, 1%, 67%, 3%, 3% ]
سطر دوم = سطر اول + ۳ به همه — خروجی یکسان، چون فاصله‌ی بین نمره‌ها همان است
درصدها قید دارند (بین ۰ و ۱۰۰، جمع = ۱۰۰) و با هُل‌دادنِ آزادانه خراب می‌شوند.
نمره‌ی آزاد (هر عددی، حتی منفی) + 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 است، یک قدم جلوتر؛ ۳۲ درس در هر گام
۵مدل بایگرام = جدول ۶۲×۶۲— nn.Embedding؛ سطرِ حرف فعلی = نمره‌ی حرف‌های بعدی
۶لاجیت ← softmax ← احتمال— خروجی (B=4, T=8, C=62)
۷آموزش— حدس بزن، با 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 = torch.tril(torch.ones(T, T)) wei = wei / wei.sum(1, keepdim=True) xbow2 = wei @ x # (T,T) @ (B,T,C) → (B,T,C) torch.allclose(xbow, xbow2) # True — همان اعداد!
wei شکلِ (T,T) دارد ولی x شکلِ (B,T,C) — پای‌تورچ wei را روی هر B تکرار می‌کند (broadcasting) و B ضربِ موازی انجام می‌دهد.
دقیقاً همان اعداد، ولی این‌بار روی GPU. حالا سؤال: چرا به همین بسنده نکنیم؟
بخش ۲ · نسخه ۳

همان نتیجه، با راهِ عجیبِ ‎-inf و softmax

نسخه ۳ دقیقاً همان اعداد نسخه ۲ را می‌دهد، ولی از راهی که در نگاه اول بی‌خود پیچیده است. دلیلش را انتهای اسلاید می‌بینی. سه مرحله را جلو ببر:

wei — (8, 8)
tril = torch.tril(torch.ones(T, T)) wei = torch.zeros((T, T)) wei = wei.masked_fill(tril == 0, float('-inf')) wei = F.softmax(wei, dim=-1) xbow3 = wei @ x
چرا این‌همه دردسر؟ چون در نسخه ۲ اعدادِ wei ثابت و دستی بودند.
اینجا خطِ wei = zeros فقط یک جای خالی است: در اسلایدهای بعد، به‌جای صفر، عددی می‌آید که از خودِ داده محاسبه می‌شود — و -inf‌ها تضمین می‌کنند هرچقدر هم آن اعداد یاد بگیرند، آینده همیشه صفر بماند.
بخش ۲ · موقعیت

مدل نمی‌داند هر حرف کجاست

تا اینجا ورودیِ مدل فقط «کدام حرف» بود. اما میانگین‌گیری (و بعداً توجه) نسبت به ترتیب بی‌تفاوت است — پس باید موقعیت را هم دستی به مدل بدهیم. یک جدولِ دومِ embedding، این‌بار برای شماره‌ی جایگاه:

دو «ب» در «بابا» — همان حرف، دو جایگاهِ متفاوت
‏«ب» جایگاه ۰ tok_emb[ب] + pos_emb[0] = x₀
‏«ب» جایگاه ۲ tok_emb[ب] + pos_emb[2] = x₂
سمتِ چپ یکی است، سمتِ راست فرق دارد ← x₀ ≠ x₂. حالا مدل می‌تواند «اولین ب» را از «دومین ب» تشخیص دهد.
self.token_embedding_table = nn.Embedding(vocab_size, n_embd) self.position_embedding_table = nn.Embedding(block_size, n_embd) # جدید # ... tok_emb = self.token_embedding_table(idx) # (B,T,C) pos_emb = self.position_embedding_table(torch.arange(T)) # (T,C) x = tok_emb + pos_emb # (B,T,C) — broadcast روی B
توجه کن که اینجا یک تغییرِ ساختاریِ دیگر هم افتاد: 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. ترنسفورمر = شش بلوک روی هم.