عنوان
در مقالهٔ قبلی دیدیم یک شبکهٔ عصبی چطور یاد میگیره: یک جواب میده، اشتباهش را اندازه میگیره، با backpropagation سهم وزنها را پیدا میکنه و وزنها را کمی تغییر میده.
اما آنجا بیشتر طوری حرف زدیم که انگار ورودی یک چیز ثابت است؛ مثلاً مشخصات یک خانه یا پیکسلهای یک تصویر. حالا فرض کن ورودی یک جمله است که کلمههایش پشت سر هم میآیند، یا قیمت بیتکوین در روزهای متوالی.
اینجا فقط اینکه «چه عددهایی داریم» مهم نیست؛ ترتیبشان هم بخشی از مسئله است. قیمتهای ۱۰۰، ۱۱۰ و ۱۲۰ با همان سه قیمت در ترتیب برعکس، یک داستان نمیگویند. جملهٔ «سگ، مرد را گاز گرفت» هم با «مرد، سگ را گاز گرفت» کلمههای تقریباً یکسانی دارد، اما معنایش عوض میشود.
این مقاله داستان تلاش مدلها برای فهم همین ترتیب است: اول با یک MLP ساده، بعد با حافظهٔ RNN، بعد با دروازههای LSTM و GRU و در نهایت با Attention و Transformer.
یک توالی را میتوانیم اینطور بنویسیم:
نمایش یک توالی
xₜ میتواند قیمت روز t، یک کلمه، دمای یک ساعت یا بردار چند ویژگی در یک لحظه باشد. هدف هم ممکن است پیشبینی قیمت فردا باشد یا حدسزدن کلمهٔ بعدی.
سؤال اصلی این است: مدل چطور میفهمد کدام قسمت گذشته برای جواب فعلی مهم است؟
گاهی گفته میشود MLP ترتیب را نمیفهمد. این جمله دقیق نیست. اگر جایگاه ورودیها را ثابت نگه داریم، MLP میتواند یاد بگیرد که عدد اول پنجره «قدیمیتر» و عدد آخر «جدیدتر» است.
فرض کن قیمت شش روز اینهاست:
10, 12, 11, 14, 15, 13
میخواهیم مدل هر بار سه روز گذشته را ببیند و روز بعد را حدس بزند. پس از یک سری، چند نمونهٔ آموزشی میسازیم:
| ورودی، از قدیمی به جدید | جواب واقعی روز بعد |
|---|---|
| ۱۰، ۱۲، ۱۱ | ۱۴ |
| ۱۲، ۱۱، ۱۴ | ۱۵ |
| ۱۱، ۱۴، ۱۵ | ۱۳ |
به این روش پنجرهٔ لغزان میگویند. هر ردیف را به یک MLP میدهیم؛ مدل پیشبینی میکند، loss میگیریم و وزنها را با backpropagation تغییر میدهیم. دقیقاً همان منطق مقالهٔ قبلی.
پس MLP میتواند از سری زمانی استفاده کند. محدودیتش این است که شکل ورودی از قبل ثابت است. اگر پنجرهٔ ما ۳۰ روزه باشد، مدل برای همان ۳۰ جایگاه ساخته شده است. خود MLP هم حالتی ندارد که با رسیدن هر روز جدید، همراه توالی جلو برود و بهروز شود.
همین سؤال طبیعی ما را به RNN میرساند: آیا مدل میتواند یک حافظهٔ داخلی داشته باشد که با هر ورودی تازه تغییر کند؟
در RNN، هر گام فقط ورودی جدید را نمیبیند؛ وضعیت یا حافظهٔ گام قبل را هم میگیرد.
هستهٔ یک RNN ساده
xₜ ورودی فعلی، hₜ₋₁ حالت قبلی و hₜ حالت تازه است. وزنهای Wₓ و Wₕ در همهٔ گامها مشترکاند؛ برای هر روز یا هر کلمه شبکهٔ جدیدی نمیسازیم.
مثلاً در جملهٔ زیر، مدل کلمهها را یکییکی میبیند:
هوا → امروز → بارانی → است
هر کلمه hidden state را عوض میکند. پس حالتِ بعد از «بارانی» باید چیزی از «هوا» و «امروز» هم در خودش داشته باشد.
این حافظه یک آرشیو دقیق از گذشته نیست؛ یک بردار فشرده و یادگرفتنی است. مدل خودش در آموزش میفهمد چه چیزهایی را نگه دارد.
برای دیدن این حرکت، فعلاً activation را کنار میگذاریم:
حافظهٔ سادهٔ مثال
سه گام حرکت حافظه
در h₃ هنوز اثر x₁ وجود دارد. اگر فرمول را باز کنیم:
رابطهٔ اصلی
جایگذاری مقادیر
ایدهٔ RNN همین است: گذشته را در یک state فشرده کن، سپس با ورودی تازه آن را بهروز کن.
برای آموزش RNN، شبکه را در طول زمان باز میکنیم: یک نسخه از همان سلول در گام اول، یکی در گام دوم و همینطور تا آخر. وزنها میان همهٔ نسخهها مشترکاند.
گرادیان از loss به عقب برمیگردد و از گامهای زمانی عبور میکند. نام این فرایند Backpropagation Through Time یا BPTT است.
اگر پاسخ امروز به اتفاقی در صد گام قبل وابسته باشد، gradient باید از صد اتصال زمانی عبور کند. در مسیر، مشتقهای محلی در هم ضرب میشوند. اگر بسیاری از آنها کوچکتر از یک باشند، سیگنال یادگیری کوچک میشود:
تصویر ساده از vanishing gradient
به این وضعیت vanishing gradient میگوییم. حالت مخالف، یعنی خیلی بزرگشدن حاصلضرب، exploding gradient است. در نتیجه، RNN ساده ممکن است یادگرفتن وابستگیهای دور را سخت پیدا کند.
LSTM یا Long Short-Term Memory آمد تا مدل کنترل دقیقتری روی حافظه داشته باشد. بهجای یک مسیر ساده برای h، یک حافظهٔ جدا به نام cell state یا c دارد و چند دروازه تصمیم میگیرند چه چیزی بماند، چه چیزی وارد شود و چه چیزی در خروجی دیده شود.
چهار محاسبهٔ اصلی در LSTM
هر باکس وزنهای خودش را دارد. f میگوید چقدر از حافظهٔ قبلی بماند، i میگوید چقدر از پیشنهاد تازه وارد شود، g خودِ محتوای پیشنهادی است و o تصمیم میگیرد چه بخشی از حافظه در خروجی دیده شود.
بهروزرسانی حافظه و خروجی LSTM
⊙ یعنی ضرب عضوبهعضو. مثلاً اگر حافظهٔ قبلی 0.8 باشد:
رابطهٔ اصلی
مدل در این گام بیشتر حافظهٔ قبلی را نگه داشته و فقط کمی اطلاعات تازه وارد کرده است. عددهای دروازهها را کسی دستی تعیین نمیکند؛ با forward pass ساخته و با BPTT یاد گرفته میشوند.
مسیر جمعیِ c کمک میکند اطلاعات و gradient در بعضی شرایط بهتر از یک زنجیرهٔ ضربی ساده حرکت کنند. اما LSTM هم حافظهٔ نامحدود و بینقص نیست.
GRU هم یک مدل بازگشتیِ دروازهدار است، اما از LSTM سادهتر است. بهجای cell state جدا، بیشتر کار را با hidden state انجام میدهد.
طرح کلی GRU
z دروازهٔ update است: تصمیم میگیرد چه مقدار از حالت تازه جای حالت قدیمی را بگیرد. r دروازهٔ reset است: تعیین میکند هنگام ساخت candidate، چقدر از گذشته وارد شود.
LSTM و GRU رقیبهایی با یک برندهٔ همیشگی نیستند. هر دو برای کنترل جریان اطلاعات در طول زمان ساخته شدهاند؛ انتخاب واقعی به داده، اندازهٔ مدل و آزمایش بستگی دارد.
برای پیشبینی قیمت فردا شاید یک عدد خروجی بخواهیم. اما در ترجمه، ورودی یک توالی است و خروجی هم توالی دیگری با طول احتمالاً متفاوت.
I love cats
↓
J’aime les chats
ایدهٔ Encoder–Decoder این بود:
Encoder: جملهٔ ورودی را بخوان و خلاصه کن
Decoder: با آن خلاصه، جملهٔ خروجی را کلمهبهکلمه بساز
در نسخهٔ سادهٔ اولیه، Encoder بعد از خواندن کل ورودی فقط حالت آخرش را به Decoder میداد:
Seq2Seq ساده بدون Attention
برای ورودی کوتاه شاید جواب بدهد. اما برای جملهٔ بلند، Decoder مجبور میشود تمام جزئیات را از یک بردار ثابت بیرون بکشد. انگار کل یک کتاب را در یک برگه خلاصه کردهایم و بعد از Decoder میخواهیم با همان برگه کتاب را ترجمه کند.
همین گلوگاه ما را به Attention میرساند.
Attention گفت چرا Decoder فقط به h₃ دسترسی داشته باشد؟ همهٔ حالتهای Encoder را نگه داریم و Decoder در هر گام تصمیم بگیرد کدامها مهمترند.
Attention در مدل بازگشتی Encoder–Decoder
sₜ₋₁ وضعیت فعلی Decoder است. hⱼ حالت مربوط به جایگاه j در Encoder. وزن αₜⱼ میگوید هنگام ساخت خروجیِ گام t، هر بخش ورودی چقدر سهم دارد.
مثلاً هنگام تولید chats، Decoder ممکن است بیشتر به بخشی از ورودی نگاه کند که نمایندهٔ cats است. هنگام تولید فعل، وزن توجه به love بیشتر شود.
پس Attention یک خلاصهٔ ثابت نمیسازد؛ برای هر گام Decoder، یک context تازه میسازد.
scoreها میتوانند هر عددی باشند. Softmax آنها را به وزنهای مثبت تبدیل میکند که جمعشان یک است:
دو score و وزنهای Attention
این وزنها شبیه احتمالاند، اما لازم نیست آنها را «احتمال درستبودن یک کلمه» بدانیم. اینجا معنای اصلیشان سهم هر نمایش در ساختن context است.
نکتهٔ تاریخی مهم این است که Attention پیش از Transformer و کنار RNNها استفاده میشد. Transformer، Attention را اختراع نکرد؛ آن را از یک افزونه به مرکز معماری تبدیل کرد.
Transformer یک نوع معماری شبکهٔ عصبی برای دادههای ترتیبی است. همانطور که RNN یک معماری بود، Transformer هم یک معماری است؛ فقط روش جابهجایی اطلاعات در آن فرق میکند.
RNN اطلاعات را از یک زنجیره رد میکرد:
x₁ → h₁ → h₂ → h₃
Transformer گفت لازم نیست اطلاعات از همهٔ گامهای وسط عبور کند. اجازه بدهیم هر جایگاه مستقیماً جایگاههای دیگر را بررسی کند. این ایده، Self-Attention است.
یک Transformer فقط Attention نیست. در هر block، Attention ارتباط بین توکنها را تعیین میکند و یک MLP اطلاعات هر توکن را پردازش میکند.
Attention = اطلاعات کدام توکنها با هم ترکیب شوند؟
MLP = این اطلاعات ترکیبشده چطور پردازش شوند؟
فرض کن سه نمایش ورودی داریم:
x₁, x₂, x₃
برای هرکدام سه نسخهٔ متفاوت میسازیم:
ساخت Query و Key و Value
W_Q، W_K و W_V سه لایهٔ خطی با وزنهای یادگرفتنی هستند. اینها همان ضرب ورودی در وزناند که در MLP دیدهایم؛ فقط برای همهٔ توکنها با وزن مشترک اجرا میشوند.
x₂ → WQ → q₂
x₂ → WK → k₂
x₂ → WV → v₂
میتوانیم این سه را شهودی اینطور بخوانیم:
Query: من دنبال چه نوع اطلاعاتی هستم؟
Key: من چه نوع اطلاعاتی برای پیدا شدن دارم؟
Value: اگر مهم بودم، چه اطلاعاتی منتقل کنم؟
برای ساختن نمایش جدید توکن دوم، q₂ را با Key همهٔ توکنها مقایسه میکنیم:
سه score برای توکن دوم
بعد Softmax وزنها را میسازد و Valueها با همان وزنها ترکیب میشوند:
ساخت نمایش contextual برای توکن دوم
z₂ نسخهٔ جدید x₂ است؛ حالا فقط معنی خام خودش را ندارد و از توکنهای دیگر اطلاعات گرفته است.
در شکل ماتریسی، تمام این کارها برای همهٔ توکنها همزمان نوشته میشود:
Scaled Dot-Product Attention
dₖ تعداد عددهای هر Key و Query است. تقسیم بر √dₖ کمک میکند scoreها در ابعاد بزرگ بیشازحد بزرگ نشوند و Softmax خیلی زود به صفر و یکهای افراطی نچسبد.
تفاوت اصلی فقط این است که Q، K و V از کجا میآیند.
Encoder Self-Attention:
Q, K, V همگی از Encoder
Decoder Masked Self-Attention:
Q, K, V همگی از Decoder، اما آینده Mask میشود
Cross-Attention:
Q از Decoder؛ K و V از Encoder
در Encoder، کل جملهٔ ورودی از اول در دسترس است. بنابراین هر توکن میتواند همهٔ توکنهای ورودی را ببیند.
q₁ → k₁, k₂, k₃
q₂ → k₁, k₂, k₃
q₃ → k₁, k₂, k₃
هیچ Maskی وجود ندارد. برای فهمیدن یک جمله، دیدن همهٔ جمله تقلب نیست؛ همان کاری است که Encoder باید انجام دهد.
Decoder خروجی را کلمهبهکلمه میسازد. فرض کن ورودیِ جابهجاشدهٔ آن این باشد:
<START> | J’aime | les
مدل باید بهترتیب اینها را پیشبینی کند:
J’aime | les | chats
اگر جایگاه دوم بتواند les یا chats آینده را ببیند، مدل در آموزش تقلب میکند. پس causal mask چنین محدودیتی میسازد:
جایگاه ۱ → فقط ۱ را میبیند
جایگاه ۲ → ۱ و ۲ را میبیند
جایگاه ۳ → ۱، ۲ و ۳ را میبیند
خانههای ممنوعِ score را قبل از Softmax برابر −∞ میکنیم. چون e⁻∞ = 0، وزن توجه آنها بعد از Softmax صفر میشود.
نمونهٔ Mask برای جایگاه دوم
پس جایگاه دوم هیچ اطلاعاتی از جایگاه سوم نمیگیرد.
بعد از اینکه Decoder ترجمهٔ قبلی خودش را بررسی کرد، باید دوباره به جملهٔ اصلی رجوع کند. Cross-Attention همین پل است.
برای تولید chats در ترجمهٔ I love cats داریم:
Query از Decoder:
الان برای ساختن کلمهٔ بعدی دنبال چه اطلاعاتی هستم؟
Key و Value از Encoder:
کدام بخش جملهٔ انگلیسی مرتبط است و چه اطلاعاتی منتقل کند؟
Cross-Attention
اگر Decoder برای ساختن chats بیشترین ارتباط را با نمایش مربوط به cats پیدا کند، وزن آن Value بیشتر میشود. هنگام ساختن فعل، ممکن است الگوی وزنها به سمت love جابهجا شود.
Cross-Attention در Transformer اصلی سال ۲۰۱۷ هم وجود داشت. مقاله آن را encoder-decoder attention نامیده بود؛ نام رایج امروزیاش Cross-Attention است.
یک Attention فقط یک نوع نگاه یاد میگیرد. Transformer چند کپی همشکل از Attention را موازی اجرا میکند:
Multi-Head Attention
همهٔ headها از نظر فرمول یکی هستند، اما وزنهایشان یکی نیست.
Head 1: WQ₁, WK₁, WV₁
Head 2: WQ₂, WK₂, WV₂
درست مثل نورونهای یک لایه: فرمول مشترک است، اما وزنها متفاوتاند. اگر وزن headها دقیقاً برابر باشند، خروجیشان هم یکی میشود و داشتن چند head بیفایده است.
مدل ممکن است یاد بگیرد headهای مختلف از زاویههای متفاوتی به رابطهها نگاه کنند، اما لازم نیست از قبل به آنها نقش انسانی و ثابت بدهیم.
Self-Attention بهتنهایی ترتیب را نمیفهمد. اگر به مدل نگوییم چه چیزی اول یا آخر بوده، توالی زیر فقط مجموعهای از توکنها به نظر میرسد:
I love cats
cats love I
پس به embedding هر جایگاه، اطلاعات موقعیت اضافه میکنیم:
ورودی همراه با جایگاه
در مقالهٔ اصلی، positional encoding با سینوس و کسینوس ساخته شد. هدف مهمتر از فرمول است: هر توکن علاوه بر معنی خودش، جایگاهش را هم حمل میکند.
در سری زمانی نیز این بخش مهم است. مدل باید بفهمد مشاهدهٔ مربوط به دیروز با مشاهدهٔ ده روز قبل فرق دارد؛ گاهی حتی فاصلهٔ واقعی زمانها هم باید وارد مدل شود.
Transformer فقط Attention نیست. پس از هر Attention، خروجی هر توکن وارد یک Feed-Forward Network میشود؛ همان یک MLP کوچک و مشترک برای تمام جایگاهها.
Feed-Forward Network در Transformer
این MLP روی هر توکن جداگانه اجرا میشود:
z₁ → MLP → u₁
z₂ → MLP → u₂
z₃ → MLP → u₃
پس نقشها تقسیم شدهاند:
Attention: اطلاعات بین توکنها را ترکیب میکند
MLP: اطلاعات ترکیبشدهٔ هر توکن را پردازش میکند
برای اینکه شبکههای عمیق بهتر آموزش ببینند، ورودی هر زیرلایه را به خروجی آن اضافه میکنیم:
Residual Connection
بعد Layer Normalization مقدارهای بردار را پایدارتر میکند:
Layer Normalization بهشکل ساده
γ و β پارامترهای یادگرفتنیاند. در تصویرهای Transformer این دو مرحله را معمولاً با عبارت Add & Norm میبینی.
یک Encoder block در Transformer اصلی چنین است:
Embedding + Position
↓
Multi-Head Self-Attention
↓
Add & Norm
↓
Feed-Forward MLP
↓
Add & Norm
یک Decoder block اینطور است:
Shifted target tokens + Position
↓
Masked Multi-Head Self-Attention
↓
Add & Norm
↓
Cross-Attention با خروجیهای Encoder
↓
Add & Norm
↓
Feed-Forward MLP
↓
Add & Norm
↓
Linear + Softmax
↓
احتمال توکن بعدی
چند Encoder block روی هم Encoder را میسازند و چند Decoder block هم Decoder را. هر block نمایشها را یک لایه عمیقتر میکند.
در آموزش ترجمه، جملهٔ ورودی و ترجمهٔ درست هر دو موجودند.
Source: I love cats
Target: J’aime les chats
Encoder سه نمایش contextual میسازد:
z₁, z₂, z₃
Decoder ورودیِ جابهجاشده را میگیرد:
<START> | J’aime | les
و احتمال جوابهای درست را برای همهٔ جایگاهها میسازد. loss معمولاً با cross-entropy محاسبه میشود:
Loss برای یک توکن درست
اگر P(chats) زیاد باشد، loss کم است. اگر مدل به chats احتمال کمی بدهد، loss زیاد میشود.
Backpropagation از loss به عقب میرود:
Loss
↓
Softmax و Linear نهایی
↓
Decoder MLP و Attentionها
↓
Cross-Attention
↓
Encoder Self-Attention و MLPها
↓
Embeddingها و وزنهای WQ, WK, WV, WO
Attention weightها خودِ پارامتر نیستند؛ در هر forward pass از Q و K ساخته میشوند. چیزهایی که یاد گرفته میشوند وزنهای لایههای خطیاند، مثل WQ، WK و WV.
Self-Attention معمولی برای هر جفت جایگاه score میسازد. برای توالی با طول T، ماتریس score اندازهای تقریباً برابر T × T دارد. پس با زیادشدن طول توالی، هزینهٔ حافظه و محاسبه سریع بالا میرود.
همچنین Attention نمیتواند اطلاعاتی بسازد که در داده وجود ندارد. اگر در گذشتهٔ یک سری مالی سیگنال قابلاعتمادی دربارهٔ فردا نباشد، معماری بهتر بهتنهایی معجزه نمیکند.
RNN و LSTM هم یکشبه بیاستفاده نشدند. آنها برای بعضی دادهها، حجمهای کوچکتر و محدودیتهای سختافزاری هنوز انتخابهای معقولیاند.
در سری زمانی، توکنها لازم نیست کلمه باشند. میتوانند قیمتهای روزانه، بازدهها، حجم معاملات یا تکههایی از یک پنجرهٔ زمانی باشند.
RNN:
گذشته را در state حمل میکند
LSTM و GRU:
تصمیم میگیرند چه چیز از گذشته بماند
Attention:
برای جواب فعلی به بخشهای مختلف گذشته وزن متفاوت میدهد
Transformer:
این ارتباط مستقیم میان بخشهای توالی را هستهٔ معماری میکند
اما هنوز یک پله تا TimesFM فاصله داریم. حالا Transformer را میفهمیم؛ مقالهٔ بعدی دربارهٔ این است که یک مدل چطور روی تعداد زیادی سری زمانی پیشآموزش میبیند و به یک Foundation Model برای پیشبینی تبدیل میشود.
مسیر تاریخ را میتوانیم اینطور نگه داریم:
MLP با پنجرهٔ لغزان
↓
RNN: گذشته را با hidden state حمل کن
↓
LSTM و GRU: جریان حافظه را با دروازهها کنترل کن
↓
Seq2Seq: یک توالی را به توالی دیگر تبدیل کن
↓
Attention: برای هر پاسخ به بخش مناسب ورودی رجوع کن
↓
Transformer: ارتباط بین توکنها را با Self-Attention و MLP بساز
اگر همهٔ این مقاله را در یک جمله جمع کنیم:
مدل اول یاد گرفت گذشته را با خودش حمل کند، بعد یاد گرفت چه چیزی را نگه دارد، و در نهایت یاد گرفت برای جواب فعلی مستقیماً به کدام بخش از اطلاعات موجود رجوع کند.
این مقاله با روایت و مثالهای عددیِ تألیفی نوشته شده است. منابع زیر برای تاریخچه، فرمولها و ادعاهای معماری استفاده شدهاند: