عنوان

سیر تحول شبکه‌های عصبی برای داده‌های ترتیبی؛ از RNN و LSTM تا Transformer و یادگیری توجه

در مقالهٔ قبلی دیدیم یک شبکهٔ عصبی چطور یاد می‌گیره: یک جواب می‌ده، اشتباهش را اندازه می‌گیره، سهم پارامترها را با backpropagation حساب می‌کنه و وزن‌ها را تغییر می‌ده. اما تا اینجا بیشتر طوری حرف زدیم که انگار ورودی یک چیز ثابت است؛ مثلاً ویژگی‌های یک خانه یا پیکسل‌های یک عکس.

حالا فرض کن ورودی، جمله‌ایه که کلمه‌هایش یکی‌یکی می‌آیند، یا قیمت بیت‌کوین در روزهای پشت سر هم. اینجا فقط اینکه «چه عددهایی داریم» کافی نیست. اینکه هر عدد چه زمانی آمده و به کدام عدد قبل از خودش وصل است، بخشی از خودِ مسئله است.

یک شبکه چطور می‌تواند گذشته را همراه خودش ببرد؟ چطور می‌فهمد کدام قسمت از گذشته برای جوابِ الان مهم‌تر است؟ و اصلاً چرا از RNN و LSTM رسیدیم به Attention و Transformer؟ داستان این مقاله همین مسیر است.

اول باید بفهمیم دادهٔ ترتیبی چه فرقی دارد

دو جملهٔ «سگ، مرد را گاز گرفت» و «مرد، سگ را گاز گرفت» تقریباً کلمه‌های یکسانی دارند، اما ترتیب کلمه‌ها معنی را عوض می‌کند. در سری زمانی هم اگر قیمت‌ها به‌ترتیب ۱۰۰، ۱۱۰، ۱۲۰ باشند، تصویر متفاوتی از ترتیب ۱۲۰، ۱۱۰، ۱۰۰ می‌سازند.

ما معمولاً یک توالی را با چند ورودی پشت سر هم نشان می‌دهیم. هر ورودی یک گام زمانی دارد؛ حتی اگر آن گام به‌جای یک روز، یک کلمه از جمله باشد.

نمایش یک توالی

sequence (x₁, x₂, …, xₜ)
prediction f(x₁, x₂, …, xₜ)

اینجا t جایگاه فعلی در توالی است. پیش‌بینی ممکن است «کلمهٔ بعدی چیست؟» یا «قیمت فردا چقدر می‌شود؟» باشد. در هر دو حالت، گذشته می‌تواند روی جواب اثر بگذارد.

شبکهٔ چندلایهٔ معمولی با این داده‌ها چه می‌کرد؟

اگر ۳۰ قیمت روزانه داشته باشیم، می‌توانیم همان ۳۰ عدد را کنار هم بگذاریم و به یک شبکهٔ چندلایهٔ معمولی یا MLP بدهیم. شبکه یاد می‌گیرد از جایگاه هر عدد استفاده کند و قیمت روز بعد را حدس بزند. پس این جمله که «MLP اصلاً ترتیب را نمی‌فهمد» دقیق نیست: اگر جایگاه‌ها را ثابت و مشخص نگه داریم، می‌تواند از ترتیب آن‌ها الگو یاد بگیرد.

محدودیتش جای دیگری است. معمولاً باید از قبل تصمیم بگیریم چند گام ورودی داریم. برای پنجرهٔ ۳۰ روزه یک شکل ورودی تعریف کرده‌ایم؛ اگر فردا بخواهیم جمله‌های با طول متفاوت یا تاریخچه‌های خیلی بلند را به همان شبکه بدهیم، مسئله پیچیده‌تر می‌شود. خودِ شبکه هم مفهوم «حالت فعلی را بگیر و با دادهٔ بعدی به‌روز کن» ندارد.

می‌توانیم پنجره را بزرگ‌تر کنیم یا راه‌های دیگری برای نمایش داده بسازیم، اما یک سؤال طبیعی پیش می‌آید: آیا می‌شود مدل یک حافظهٔ کوچک داشته باشد که با آمدن هر ورودی، آن را به‌روز کند؟

RNN؛ یک حافظه که همراه توالی حرکت می‌کند

ایدهٔ RNN یا Recurrent Neural Network این است که مدل در هر گام فقط ورودی تازه را نبیند. یک hidden state هم از گام قبلی دریافت کند؛ چیزی شبیه خلاصه‌ای فشرده از آنچه تا حالا دیده است. مدل با ورودی جدید و این خلاصه، حالت بعدی را می‌سازد.

هستهٔ یک RNN ساده

hₜ tanh(Wₓxₜ + Wₕhₜ₋₁ + b)
ŷₜ Wᵧhₜ + c

در این فرمول، xₜ ورودیِ الان، hₜ₋₁ حافظهٔ گام قبل و hₜ حافظهٔ تازه است. ماتریس‌های Wₓ و Wₕ در تمام گام‌ها همان پارامترها هستند؛ یعنی برای هر روز یا هر کلمه شبکهٔ جدیدی نمی‌سازیم. tanh هم یک تابع فعال‌سازی غیرخطی است که خروجی‌اش بین منفی یک و یک قرار می‌گیرد.

مثلاً مدل کلمهٔ «هوا» را می‌بیند و حالتش تغییر می‌کند؛ بعد «امروز» را می‌بیند و حالت تازه‌ای می‌سازد؛ بعد «بارانی» را. اگر قرار باشد کلمهٔ بعدی را پیش‌بینی کند، حالت فعلی باید چیزی از کلمه‌های قبلی در خودش نگه داشته باشد.

این حافظه شبیه یک فایل آرشیوی دقیق نیست که بتوانیم هر ورودی قدیمی را بی‌کم‌وکاست از آن بیرون بکشیم. یک نمایش فشرده و یادگرفتنی است. خودِ مدل با آموزش تعیین می‌کند چه چیزی در آن باقی بماند.

یک RNN خیلی کوچک با عدد

برای اینکه حرکت حافظه را ببینیم، فعلاً تابع فعال‌سازی را کنار می‌گذاریم و یک RNN خطیِ آموزشی می‌سازیم. این مثال فقط برای دیدن ایدهٔ انتقال حالت است؛ RNN واقعی معمولاً غیرخطی است و حالتش می‌تواند یک بردار بزرگ باشد.

حافظهٔ سادهٔ مثال

h₀ 0
hₜ 0.5 × hₜ₋₁ + xₜ
(x₁, x₂, x₃) (2, 0, 1)

در گام اول، حافظه صفر است و ورودی ۲ می‌رسد. در گام دوم، ورودی جدید صفر است، اما بخشی از مقدار قبلی هنوز باقی می‌ماند. در گام سوم هم ورودی ۱ به همان حافظهٔ باقی‌مانده اضافه می‌شود.

سه گام حرکت حافظه

h₁ 0.5 × 0 + 2 = 2
h₂ 0.5 × 2 + 0 = 1
h₃ 0.5 × 1 + 1 = 1.5

پس در h₃ هنوز ردّی از x₁ وجود دارد. اگر فرمول را باز کنیم، می‌بینیم ورودی اول با ضریب ۰.۲۵، ورودی دوم با ضریب ۰.۵ و ورودی سوم با ضریب ۱ حضور دارد. این فقط رفتار همین وزن فرضی است؛ در شبکهٔ واقعی وزن‌ها در آموزش تعیین می‌شوند و رابطه می‌تواند خیلی پیچیده‌تر باشد.

ردّ ورودی‌های قدیمی در حالت فعلی

رابطهٔ اصلی

h₃ = 0.5 × (0.5 × x₁ + x₂) + x₃
0.25 × x₁ + 0.5 × x₂ + x₃

جایگذاری مقادیر

0.25 × 2 + 0.5 × 0 + 1
1.5

همین ایده یک جهش بود: مدل با رسیدن دادهٔ تازه، خلاصهٔ گذشته را به‌روز می‌کند و می‌تواند با توالی‌هایی با طول‌های مختلف کار کند. اما یک مشکل جدی هنوز مانده است.

RNN چطور یاد می‌گیرد و چرا گذشته‌های دور سخت‌اند؟

در مقالهٔ قبلی backpropagation را برای شبکه‌ای با چند لایه دیدیم. حالا RNN را در ذهن باز کن: یک نسخه از همان سلول در گام اول، یک نسخه در گام دوم، تا گام آخر؛ همه با وزن‌های مشترک. خطا از خروجی به عقب برمی‌گردد و از میان این گام‌ها عبور می‌کند. به این کار Backpropagation Through Time یا BPTT می‌گویند.

اگر جوابِ الان به اتفاقی در ۱۰۰ گام قبل وابسته باشد، سیگنال یادگیری باید از ۱۰۰ اتصال زمانی رد شود. در هر اتصال مشتقِ محلی در مشتق‌های قبلی ضرب می‌شود. اگر بسیاری از این عامل‌ها کوچک‌تر از یک باشند، حاصل‌ضرب می‌تواند خیلی کوچک شود؛ اگر بزرگ‌تر از یک باشند، ممکن است خیلی بزرگ شود.

تصویر ساده از کم‌شدن سیگنال

local_derivative 0.5
steps 10
gradient_factor 0.5¹⁰ ≈ 0.00098

این فرمول، تحلیل کامل RNN نیست؛ یک تصویر از اثرِ تکرار ضرب‌هاست. نتیجه‌اش می‌تواند vanishing gradient باشد: مدل خطای گام آخر را می‌بیند، اما وقتی این خطا به ورودیِ خیلی قدیمی می‌رسد، سیگنالش ضعیف شده است. حالت برعکس، یعنی بزرگ‌شدن بیش از حد گرادیان، exploding gradient نام دارد.

فرض کن جمله با «او کلید را زیر گلدان گذاشت» شروع می‌شود و چند جمله بعد می‌پرسیم «کلید کجاست؟». یک RNN ساده ممکن است با طولانی‌شدن فاصله، نگه‌داشتن و یادگرفتن این ارتباط را سخت‌تر پیدا کند. RNN هیچ قانون مطلقی ندارد که بگوید «بعد از ده کلمه همه‌چیز را فراموش می‌کنم»؛ مسئله این است که آموزشِ وابستگی‌های دور برایش دشوار می‌شود.

LSTM؛ مدل خودش تصمیم می‌گیرد چه چیزی بماند

LSTM یا Long Short-Term Memory برای همین دشواری طراحی شد. ایده‌اش این است که به‌جای یک مسیر ساده برای تغییر حافظه، یک cell state داشته باشیم که اطلاعات بتوانند در آن حرکت کنند، و چند دروازه که مشخص کنند چه چیزی فراموش، چه چیزی اضافه و چه چیزی آشکار شود. فرمولی که پایین می‌بینیم شکل متداولِ امروزی LSTM است؛ دروازهٔ فراموشی بعد از مقالهٔ اولیهٔ LSTM به آن افزوده شد.

این دروازه‌ها صفر و یکِ قطعی نیستند. خروجی تابع sigmoid را می‌گیرند؛ معمولاً عددی بین صفر و یک. صفر یعنی «تقریباً عبور نده» و یک یعنی «تقریباً کامل عبور بده». خودِ وزن‌های دروازه‌ها هم با همان backpropagation یاد گرفته می‌شوند.

چهار بخش اصلی در LSTM متداول

fₜ sigmoid(Wf × [hₜ₋₁, xₜ] + bf)
iₜ sigmoid(Wi × [hₜ₋₁, xₜ] + bi)
gₜ tanh(Wg × [hₜ₋₁, xₜ] + bg)
oₜ sigmoid(Wo × [hₜ₋₁, xₜ] + bo)

fₜ دروازهٔ فراموشی است، iₜ اجازهٔ نوشتن اطلاعات تازه را می‌دهد، gₜ محتوای تازهٔ پیشنهادی است و oₜ مشخص می‌کند چه بخشی از حافظه در خروجی دیده شود. علامت [hₜ₋₁, xₜ] یعنی دو بردار قبلی و جدید را کنار هم گذاشته‌ایم.

به‌روزرسانی حافظه و خروجی LSTM

cₜ fₜ ⊙ cₜ₋₁ + iₜ ⊙ gₜ
hₜ oₜ ⊙ tanh(cₜ)

علامت ⊙ ضرب عضو‌به‌عضو است. اگر fₜ نزدیک یک باشد، بخش زیادی از حافظهٔ قبلی باقی می‌ماند. اگر iₜ نزدیک صفر باشد، محتوای تازه به‌سختی وارد حافظه می‌شود. و اگر oₜ کوچک باشد، آنچه در حافظه هست الزاماً همه‌اش در خروجی فعلی دیده نمی‌شود.

یک مثال عددیِ تک‌بعدی کمک می‌کند. فرض کن حافظهٔ قبلی ۰.۸ بوده، دروازهٔ فراموشی ۰.۹، دروازهٔ ورود ۰.۲ و محتوای تازهٔ پیشنهادی ۰.۵ است:

یک گام به‌روزرسانی حافظهٔ LSTM

رابطهٔ اصلی

cₜ = fₜ × cₜ₋₁ + iₜ × gₜ
0.9 × 0.8 + 0.2 × 0.5
0.72 + 0.10
0.82

مدل در این گام بیشترِ حافظهٔ قدیمی را نگه داشته و فقط کمی اطلاعات تازه به آن اضافه کرده. این اعداد را ما برای مثال انتخاب کردیم؛ در یک LSTM واقعی دروازه‌ها از ورودی و حالت قبلی محاسبه می‌شوند. ویژگی مهم این مسیرِ جمعیِ حافظه است: در شرایط مناسب، سیگنال یادگیری می‌تواند آسان‌تر از یک زنجیرهٔ ضربیِ ساده عبور کند. بااین‌حال LSTM تضمین نمی‌کند هر وابستگیِ بسیار دوری را بی‌نقص یاد بگیرد.

GRU؛ نسخه‌ای جمع‌وجورتر از ایدهٔ دروازه‌ها

GRU یا Gated Recurrent Unit یک معماری بازگشتی دیگر است که با دروازه‌ها تصمیم می‌گیرد چه مقدار از حالت قبلی و چه مقدار از محتوای تازه را به حالت بعدی ببرد. در صورت رایجِ آن، دو دروازهٔ update و reset داریم و برخلاف LSTM، حالت پنهان و حافظهٔ جداگانهٔ cell state را به همان شکل نداریم.

طرح کلی GRU

zₜ sigmoid(Wz × xₜ + Uz × hₜ₋₁ + bz)
rₜ sigmoid(Wr × xₜ + Ur × hₜ₋₁ + br)
h̃ₜ tanh(Wh × xₜ + Uh × (rₜ ⊙ hₜ₋₁) + bh)
hₜ (1 − zₜ) ⊙ hₜ₋₁ + zₜ ⊙ h̃ₜ

اینجا قرارداد ما این است که zₜ سهم حالت تازه را تعیین کند؛ در بعضی متن‌ها تعریف دروازه را برعکس می‌نویسند، اما ایده یکی است. GRU را نمی‌شود همیشه «بهتر» یا «بدتر» از LSTM دانست. هر دو راهی هستند برای کنترل جریان اطلاعات در طول زمان، و انتخاب بینشان به داده، هزینهٔ محاسبه و آزمایش بستگی دارد.

تا اینجا مدل حافظه‌دار شده. ولی هنوز در بعضی کارها یک مشکل دیگر داریم: اگر قرار باشد از کل یک توالی برای تولید یک توالی دیگر استفاده کنیم، اطلاعات گذشته دقیقاً کجا نگه‌داری می‌شوند؟

Encoder–Decoder؛ وقتی یک توالی باید به توالی دیگری تبدیل شود

در ترجمه، ورودی یک جمله به یک زبان است و خروجی جمله‌ای به زبان دیگر. طول این دو جمله هم لزوماً برابر نیست. یک راه قدیمی این بود که یک RNN یا LSTM به نام encoder جملهٔ ورودی را بخواند و یک نمایش نهایی بسازد. سپس decoder با آن نمایش، خروجی را قدم‌به‌قدم تولید کند.

طرح سادهٔ مدل توالی به توالی

input_sequence → encoder → context_vector
context_vector → decoder → output_sequence

این روش کار می‌کرد، اما در نسخهٔ سادهٔ آن، تمام اطلاعات جملهٔ ورودی باید از یک بردار با طول ثابت عبور می‌کرد. برای جمله‌های بلند، این بردار می‌توانست گلوگاه شود. دقت کن این توصیف مربوط به آن معماریِ encoder–decoder قدیمی است؛ هر decoder یا هر مدل جدیدی قرار نیست چنین گلوگاهی داشته باشد.

مثلاً اگر جملهٔ ورودی می‌گوید «کتابی که هفتهٔ پیش از کتابخانه گرفتم روی میز است»، decoder شاید هنگام تولید کلمهٔ مربوط به «کتابخانه» بخواهد دوباره به همان قسمت جمله نگاه کند. چرا مجبورش کنیم فقط از یک خلاصهٔ نهایی استفاده کند؟

Attention؛ دسترسی دوباره به بخش‌های مختلف ورودی

ایدهٔ Attention این بود که decoder در هر گامِ تولید، بتواند به حالت‌های مختلف encoder نگاه کند و برای هر بخش یک وزنِ وابسته به نیاز همان گام بسازد. پس یک خلاصهٔ ثابت برای همهٔ خروجی‌ها نداریم؛ در هر گام، خلاصهٔ تازه‌ای از بخش‌های مرتبط ورودی ساخته می‌شود.

اگر مدل اکنون می‌خواهد ترجمهٔ «کتابخانه» را تولید کند، ممکن است وزنِ مربوط به آن قسمت ورودی بیشتر شود. وقتی نوبت «میز» می‌رسد، الگوی وزن‌ها عوض می‌شود. این وزن‌ها از یک تابعِ امتیازدهیِ قابل‌آموزش و سپس softmax به دست می‌آیند؛ برچسب‌گذاری دستیِ کلمه‌های مهم نیستند.

Attention در یک decoder بازگشتی

eₜⱼ score(decoder_stateₜ₋₁, encoder_stateⱼ)
αₜⱼ softmax(eₜⱼ across j)
contextₜ Σⱼ αₜⱼ × encoder_stateⱼ

αₜⱼ می‌گوید هنگام ساخت خروجیِ گام t، حالتِ ورودیِ جایگاه j چه سهمی در بردار زمینه داشته باشد. مجموع وزن‌ها برابر یک است. «توجه» در اینجا یک استعارهٔ مفید برای وزن‌دهی محاسباتی است؛ لازم نیست آن را شبیه توجه آگاهانهٔ انسان تصور کنیم.

و یک نکتهٔ تاریخی مهم: Attention قبل از Transformer در مدل‌های مبتنی بر RNN استفاده می‌شد. Transformer از هیچ، Attention را اختراع نکرد؛ معماری‌ای ساخت که Attention را به جزء اصلی پردازش توالی تبدیل کرد.

یک مثال عددی؛ توجه دقیقاً چه چیزی را محاسبه می‌کند؟

فرض کن مدل برای جواب فعلی‌اش فقط دو بخش از ورودی دارد. به بخش اول امتیاز ۲ و به بخش دوم امتیاز ۱ داده است. softmax امتیازها را به وزن‌های مثبت تبدیل می‌کند که جمعشان یک است.

تبدیل دو امتیاز به وزن توجه

score₁ 2
score₂ 1
α₁ e² ÷ (e² + e¹) ≈ 0.731
α₂ e¹ ÷ (e² + e¹) ≈ 0.269

حالا فرض کن اطلاعاتی که این دو بخش حمل می‌کنند در یک مثال تک‌بعدی به‌ترتیب ۱۰ و ۴ باشد. خروجی Attention میانگینِ وزن‌دارِ آن‌هاست:

خروجی Attention در مثال دو بخشی

رابطهٔ اصلی

context = α₁ × value₁ + α₂ × value₂
0.731 × 10 + 0.269 × 4
7.31 + 1.076
8.386

اگر سؤالِ مدل عوض شود و امتیازهای دو بخش جابه‌جا شوند، وزن‌ها و خروجی هم عوض می‌شوند. بنابراین Attention یک خلاصهٔ ثابت از ورودی نمی‌سازد؛ بر اساس نیاز هر گام، ترکیب تازه‌ای می‌سازد. در مدل واقعی امتیازها، وزن‌ها و valueها برداری و قابل‌آموزش‌اند.

از Attention بازگشتی تا Self-Attention

در مثال قبلی، decoder به حالت‌های encoder نگاه می‌کرد. به این نوع رابطه معمولاً cross-attention می‌گویند: یک بخش مدل از نمایش‌های بخش دیگر اطلاعات می‌گیرد.

اما یک کلمهٔ جمله ممکن است برای فهم خودش هم به کلمه‌های دیگر همان جمله نیاز داشته باشد. در عبارت «بانک کنار رودخانه»، معنی «بانک» با نگاه به «رودخانه» روشن‌تر می‌شود. وقتی جایگاه‌های یک توالی به جایگاه‌های همان توالی نگاه می‌کنند، اسمش self-attention است.

برای هر جایگاه سه نمایش می‌سازیم: Query یا چیزی که آن جایگاه دنبال آن می‌گردد، Key یا مشخصه‌ای که هر جایگاه برای سنجش ارتباط ارائه می‌دهد، و Value یا اطلاعاتی که در صورت توجه از آن جایگاه برداشته می‌شود. این‌ها معمولاً با ضرب نمایش ورودی در سه ماتریسِ قابل‌آموزش ساخته می‌شوند. اسم‌های Query و Key و Value کمک می‌کنند ایده را تصور کنیم، اما در عمل همه‌شان بردارهای عددی‌اند.

ساخت Query و Key و Value

Q XW_Q
K XW_K
V XW_V

برای اینکه بفهمیم Query یک جایگاه چقدر با Key جایگاه دیگر جور است، ضرب داخلی می‌گیریم. امتیازها را بر ریشهٔ بُعدِ Key تقسیم می‌کنیم، از softmax رد می‌کنیم و با Valueها ترکیب می‌کنیم.

Scaled Dot-Product Attention

Attention(Q, K, V) softmax(QKᵀ ÷ √dₖ) V

تقسیم بر √dₖ کمک می‌کند وقتی بُعد بردارها بزرگ می‌شود، امتیازها بیش از حد بزرگ نشوند و softmax خیلی زود به مقادیر تقریباً صفر و یک نچسبد. این فرمول برای یک مجموعه از Queryها هم‌زمان نوشته شده است؛ هر سطر خروجی، ترکیبی از Valueها برای یک جایگاه است.

چرا چند Head داریم؟

اگر فقط یک مجموعه از Query، Key و Value داشته باشیم، فقط یک شیوهٔ یادگرفتنی برای ساخت ارتباط‌ها داریم. در multi-head attention چند مجموعهٔ مستقل از این تبدیل‌ها ساخته می‌شود. خروجی headها به هم وصل می‌شود و دوباره از یک تبدیل خطی می‌گذرد.

چند سر توجه

headᵢ Attention(XW_Qᵢ, XW_Kᵢ, XW_Vᵢ)
output Concat(head₁, …, headₕ) W_O

ممکن است یک head به رابطهٔ نزدیک و یک head به وابستگی دور حساس شود، اما نباید از قبل فرض کنیم هر head یک نقش انسانی و ثابت مثل «فاعل‌یاب» دارد. نقش‌های واقعی از آموزش درمی‌آیند و تفسیرشان همیشه ساده نیست.

Transformer؛ تغییر اصلی دقیقاً چه بود؟

تا اینجا Attention می‌توانست در کنار RNN استفاده شود. مقالهٔ Attention Is All You Need در سال ۲۰۱۷ نشان داد برای کار ترجمه می‌شود معماری encoder–decoder را بدون لایهٔ بازگشتی ساخت و ارتباط بین جایگاه‌ها را عمدتاً با Attention یاد گرفت. این همان Transformer اصلی است.

هر لایهٔ encoder در آن معماری، self-attention و سپس یک شبکهٔ پیش‌خور دارد. decoder هم self-attention پوشیده یا masked، توجه به خروجی encoder، و شبکهٔ پیش‌خور دارد. اتصال‌های باقیمانده و نرمال‌سازی لایه هم بخشی از طراحی هستند. پس نام مقاله به معنی «مدل فقط یک تابع Attention دارد و هیچ جزء دیگری ندارد» نیست.

در RNN، محاسبهٔ حالتِ گام t به حالتِ t−1 وابسته است. این وابستگی، پردازش گام‌ها را در طول توالی ترتیبی می‌کند. در self-attention، امتیازهای میان جایگاه‌های یک توالی را می‌توان در زمان آموزش به شکل ماتریسی محاسبه کرد. این یک دلیل مهم برای موازی‌سازی بهتر Transformer است. البته هنگام تولید خودرگرسیو، خروجی‌ها هنوز معمولاً یکی‌یکی تولید می‌شوند.

اگر همه‌چیز هم‌زمان دیده شود، ترتیب از کجا می‌آید؟

RNN ترتیب را از مسیر حرکتش دریافت می‌کرد: اول گام یک، بعد گام دو. self-attention خالص اگر هیچ اطلاعات جایگاهی به ورودی ندهیم، به‌تنهایی نمی‌داند چه چیزی اول بوده و چه چیزی آخر. بنابراین Transformer به نمایش ورودی، اطلاعات موقعیت اضافه می‌کند؛ در مقالهٔ اصلی از positional encoding استفاده شده بود.

ورودی همراه با جایگاه

input_at_positionₜ token_embeddingₜ + positional_encodingₜ

برای سری زمانی هم این نکته مهم است: اگر مدل قرار است از ترتیب روزها یا فاصلهٔ زمانی آن‌ها استفاده کند، باید به شکلی اطلاعات ترتیب و موقعیت را دریافت کند. شکل دقیقِ نمایش زمان در مدل‌های مختلف یکسان نیست.

Mask چرا لازم است؟

وقتی مدل کلمهٔ بعدی یا نقطهٔ بعدیِ سری زمانی را پیش‌بینی می‌کند، نباید جوابِ آینده را در ورودی همان لحظه ببیند. در self-attentionِ علی یا causal، برای هر جایگاه دسترسی به جایگاه‌های بعد از خودش بسته می‌شود. به این محدودیت causal mask می‌گویند.

فرض کن قرار است قیمت روز چهارم را با روزهای اول تا سوم پیش‌بینی کنیم. اگر هنگام آموزش، مدل اجازه داشته باشد به قیمت روز چهارم نگاه کند، آزمایشمان معنایی ندارد. mask این نشت اطلاعات آینده را در محاسبهٔ Attention محدود می‌کند. البته هنگام آماده‌سازی داده هم باید مراقب نشتِ آینده از راه‌های دیگر باشیم؛ mask به‌تنهایی همهٔ خطاهای داده را حل نمی‌کند.

پس Transformer همهٔ مشکل‌ها را حل کرد؟

نه. self-attention معمولی میان همهٔ جفت‌های جایگاه امتیاز می‌سازد. برای توالی با طول T، ماتریس امتیازها تقریباً T × T خانه دارد؛ یعنی هزینهٔ حافظه و محاسبهٔ Attention متعارف با طول توالی سریع رشد می‌کند. روش‌های جدیدتر برای کم‌کردن این هزینه طراحی شده‌اند، اما این محدودیت برای فهم معماری اصلی مهم است.

Transformer همچنین باید جایگاه را به‌صورت صریح وارد مدل کند، معمولاً به دادهٔ زیاد و تنظیم مناسب وابسته است و «توانایی توجه» به معنی تضمین پیش‌بینی درست نیست. اگر از گذشتهٔ قیمت بیت‌کوین هیچ سیگنال قابل‌اتکایی برای فردا وجود نداشته باشد، Attention نمی‌تواند اطلاعاتِ وجودنداشته را بسازد.

همین‌طور LSTM یک‌شبه منسوخ نشد. برای بعضی مسئله‌ها، حجم داده‌ها و محدودیت‌های سخت‌افزاری، مدل بازگشتی هنوز می‌تواند انتخاب خوبی باشد. بهتر است معماری‌ها را ابزارهایی برای فرض‌ها و هزینه‌های متفاوت ببینیم، نه یک مسابقه با یک برندهٔ همیشگی.

این مسیر چه ربطی به سری زمانی و مقالهٔ بعدی ما دارد؟

در سری زمانی هم با یک توالی روبه‌رو هستیم: قیمت، دما، مصرف برق یا هر عددی که در زمان ثبت شده. RNN با یک حالتِ در حال به‌روزرسانی سراغ آن می‌رود؛ LSTM و GRU جریان اطلاعات را با دروازه‌ها کنترل می‌کنند؛ Attention اجازه می‌دهد برای یک پیش‌بینی، بخش‌های مختلف گذشته وزن‌های متفاوت بگیرند؛ و Transformer این سازوکار را در مرکز معماری قرار می‌دهد.

اما از اینجا هنوز مستقیم به «مدلی که روی هزاران سری زمانیِ جدید هم جواب بدهد» نرسیده‌ایم. برای آن باید دربارهٔ پیش‌آموزش روی داده‌های متنوع و ایدهٔ Foundation Model حرف بزنیم. در مقالهٔ بعدی، TimesFM را دقیقاً از همین زاویه باز می‌کنیم: مدل چه چیزی از سری‌های زمانی متعدد یاد گرفته، ورودی را چطور به تکه‌ها یا patchها تبدیل می‌کند و چگونه از گذشته برای آینده پیش‌بینی می‌سازد.

جمع‌بندی

داستان از یک نیاز ساده شروع شد: داده‌های ترتیبی را نمی‌شود همیشه مثل یک فهرستِ بی‌زمان از ویژگی‌ها دید. شبکهٔ چندلایه می‌تواند با پنجرهٔ ثابت از ترتیب الگو یاد بگیرد، اما RNN یک راه طبیعی برای نگه‌داشتن حالت و به‌روزرسانی آن در طول توالی داد.

وقتی یادگیری ارتباط‌های دور برای RNN ساده سخت شد، LSTM و GRU با دروازه‌ها کنترل بهتری بر جریان اطلاعات فراهم کردند. وقتی یک خلاصهٔ ثابت از ورودی برای ساخت خروجی کافی نبود، Attention به مدل اجازه داد در هر گام به بخش‌های مرتبط ورودی رجوع کند. و Transformer نشان داد می‌شود پردازش توالی را بدون بازگشتِ RNN، با self-attention، اطلاعات جایگاهی و اجزای مکملش ساخت.

اگر بخواهیم همهٔ این مسیر را در یک جمله نگه داریم: مدل اول یاد گرفت گذشته را با خودش حمل کند، بعد یاد گرفت چه چیزی را نگه دارد، و بعد یاد گرفت هنگام ساخت هر جواب به کدام بخشِ اطلاعاتِ در دسترس بیشتر رجوع کند.

منابع

این مقاله با روایت و مثال‌های عددیِ تألیفی نوشته شده است. منابع زیر برای بررسی تاریخچه، فرمول‌ها و ادعاهای معماری آن استفاده شده‌اند:

  1. Jeffrey L. Elman، Finding Structure in Time، Cognitive Science، ۱۹۹۰. برای ایدهٔ حالت بازگشتی و حافظهٔ پویا در توالی.
  2. Yoshua Bengio، Patrice Simard و Paolo Frasconi، Learning Long-Term Dependencies with Gradient Descent Is Difficult، IEEE Transactions on Neural Networks، ۱۹۹۴. برای دشواری آموزش وابستگی‌های دور و جریان گرادیان.
  3. Sepp Hochreiter و Jürgen Schmidhuber، Long Short-Term Memory، Neural Computation، ۱۹۹۷. برای ایدهٔ حافظه و دروازه‌های LSTM.
  4. Felix A. Gers، Jürgen Schmidhuber و Fred Cummins، Learning to Forget: Continual Prediction with LSTM، Neural Computation، ۲۰۰۰. برای دروازهٔ فراموشی در شکل متداول LSTM.
  5. Kyunghyun Cho و همکاران، Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation، EMNLP، ۲۰۱۴. برای GRU و معماری بازگشتیِ encoder–decoder.
  6. Ilya Sutskever، Oriol Vinyals و Quoc V. Le، Sequence to Sequence Learning with Neural Networks، NeurIPS، ۲۰۱۴. برای مدل توالی به توالی با encoder و decoder.
  7. Dzmitry Bahdanau، Kyunghyun Cho و Yoshua Bengio، Neural Machine Translation by Jointly Learning to Align and Translate، ICLR، ۲۰۱۵. برای Attention در کنار مدل بازگشتی و مسئلهٔ بردار زمینهٔ ثابت.
  8. Ashish Vaswani و همکاران، Attention Is All You Need، NeurIPS، ۲۰۱۷. برای self-attention، Query/Key/Value، چندسر توجه، جایگاه، mask و معماری Transformer.