فاز 5 · درس 10

مکانیسم اتنشن (Attention Mechanism): شکستن گلوگاه مدل‌های توالی به توالی

نوع
ساختنی
زمان
حدود 45 دقیقه
زبان‌ها
Python
پیش‌نیاز
فاز 5 · بخش 09 (مدل‌های توالی به توالی)
یک پرتو نور متمرکز زرد روی یک برگه‌ی کاغذی خاص در میان ردیفی از صفحات شناور در فضایی تاریک
فهرست مطالب
  1. صورت مسئله
  2. مفاهیم پایه
  3. ابعاد تنسورها (همان تله‌ای که گریبان‌گیر همه می‌شود)
  4. پیاده‌سازی
  5. گام 1: اتنشن جمعی (باداناو)
  6. گام 2: روش‌های dot و general لونگ
  7. گام 3: یک مثال عددی گام‌به‌گام
  8. گام 4: چرا این مفهوم پل ورود به ترنسفورمرها است؟
  9. استفاده‌ی عملی
  10. چرا اتنشن کلاسیک هنوز اهمیت دارد؟
  11. تله‌ی تفسیرپذیری بر اساس وزن‌های اتنشن
  12. ذخیره‌سازی پرامپت
  13. تمرین‌ها
  14. اصطلاحات کلیدی
  15. برای مطالعه‌ی بیشتر

در مدل‌های اولیه‌ی توالی به توالی (Seq2Seq)، فشرده‌سازی تمام بار معنایی جمله در یک وضعیت پنهان با طول ثابت، عملکرد مدل را در توالی‌های طولانی به‌شدت کاهش می‌داد. مکانیسم اتنشن (Attention Mechanism) این محدودیت ساختاری را کنار زد تا دکودر در هر گام به جای حدس‌زدن بر مبنای یک بردار ثابت، به کل توالی ورودی دسترسی پویا داشته باشد.

درک دقیق فرمول‌ها و ابعاد تنسورها در روش‌های باداناو و لونگ، پایه‌ی ورود به معماری‌های مدرن‌تر مثل ترنسفورمرها است؛ چرا که مفاهیم کلیدی Q/K/V ریشه در همین اتنشن کلاسیک دارند. پیاده‌سازی این درس محاسبات سبکی دارد و بدون نیاز به GPU ابری یا پردازنده‌های گرافیکی پرهزینه، روی همان CPU سیستم شخصی قابل پیاده‌سازی و آزمایش است.

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

صورت مسئله

درس 09 با یک شکست مستند و دقیق تمام شد: مدل انکودر-دکودرِ مبتنی بر GRU که روی یک مسئله‌ی ساده‌ی کپی‌کردن آموزش دیده بود، در طول توالی 5 به دقت 89% رسید، اما در طول 80 دقتش در حد حدس تصادفی پایین آمد. علت این افت نه باگ آموزش، بلکه کاملاً ساختاری است: هر ذره از اطلاعاتی که انکودر به دست می‌آورد باید در یک وضعیت پنهان (hidden state) با اندازه‌ی ثابت جا شود و دکودر هم هیچ‌چیز دیگری جز آن را نمی‌بیند.

باداناو، چو و بنجیو در سال 2014 راه‌حلی سه‌خطی برای این مشکل ارائه دادند: به‌جای ارسالِ صرفاً آخرین وضعیت انکودر به دکودر، همه‌ی وضعیت‌های پنهان انکودر را نگه دارید. در هر گام دکودر، میانگین وزن‌داری از وضعیت‌های انکودر حساب کنید که وزن‌هایش می‌گویند: «دکودر در همین لحظه چقدر باید به موقعیت i در انکودر نگاه کند؟» این میانگین وزن‌دار همان بافت (context) است و در هر گام دکودر تغییر می‌کند.

کل ایده همین است. ترنسفورمرها (Transformers) بسطش دادند؛ خود‌اتنشن (self-attention) آن را روی یک توالی واحد پیاده کرد و اتنشن چندسر (multi-head attention) اجرای هم‌زمان و موازی آن را ممکن ساخت. بااین‌حال نسخه‌ی سال 2014 همان موقع گلوگاه را شکست. وقتی این را درک کنید، جهش به سمت ترنسفورمرها صرفاً کاری مهندسی است، نه تحولی مفهومی.

مفاهیم پایه

اتنشن باداناو: دکودر از تمام وضعیت‌های انکودر کوئری می‌گیرد

در هر گام دکودر t:

  1. وضعیت پنهان قبلی دکودر، یعنی s_{t-1} را به‌عنوان کوئری (query) در نظر بگیرید.
  2. آن را در برابر تک‌تک وضعیت‌های پنهان انکودر h_1, ..., h_T امتیازدهی کنید: یک عدد اسکالر به ازای هر موقعیت در انکودر.
  3. روی امتیازها تابع سافت‌مکس (Softmax) بزنید تا وزن‌های اتنشن α_{t,1}, ..., α_{t,T} با مجموع 1 به دست آیند.
  4. بردار بافت را به‌صورت c_t = Σ α_{t,i} * h_i محاسبه کنید؛ یعنی میانگین وزن‌دار وضعیت‌های انکودر.
  5. دکودر c_t را همراه با توکن خروجی قبلی می‌گیرد و توکن بعدی را تولید می‌کند.

اصل ماجرا همین میانگین وزن‌دار است. وقتی دکودر می‌خواهد "Je" را به "I" ترجمه کند، به وضعیت انکودر روی کلمه‌ی "Je" وزن بالا و به بقیه وزن پایینی می‌دهد. وقتی نوبت "not" می‌رسد، به "pas" وزن بالا می‌دهد. بردار بافت در هر گام شکل تازه‌ای می‌گیرد.

ابعاد تنسورها (همان تله‌ای که گریبان‌گیر همه می‌شود)

اینجا جایی است که اجرای اولیه‌ی اتنشن همیشه خراب می‌شود. این بخش را باحوصله بخوانید.

مفهوم ابعاد (Shape) نکات
وضعیت‌های پنهان انکودر H (T_enc, d_h) اگر BiLSTM باشد: d_h = 2 * d_hidden
وضعیت پنهان دکودر s_{t-1} (d_s,) یک بردار
امتیاز اتنشن e_{t,i} اسکالر یک عدد به ازای هر موقعیت انکودر
وزن اتنشن α_{t,i} اسکالر حاصل اعمال سافت‌مکس روی تمام iها
بردار بافت c_t (d_h,) هم‌اندازه‌ی وضعیت انکودر

امتیازدهی جمعی یا باداناو (Additive score). فرمول: e_{t,i} = v_α^T * tanh(W_a * s_{t-1} + U_a * h_i).

  • s_{t-1} دارای ابعاد (d_s,) و h_i دارای ابعاد (d_h,) است.
  • W_a ابعاد (d_attn, d_s) و U_a ابعاد (d_attn, d_h) دارد.
  • حاصل جمع آن‌ها داخل تابع tanh دارای ابعاد (d_attn,) است.
  • v_α دارای ابعاد (d_attn,) است. ضرب داخلی با v_α خروجی را به یک عدد اسکالر تبدیل می‌کند. تمام کار v_α همین است. خبری از جادو نیست؛ همان تصویر کردن (projection) است که برداری با بعد اتنشن را به امتیازی اسکالر تبدیل می‌کند.

امتیازدهی ضربی یا لونگ (Multiplicative score). سه حالت دارد:

  • ضرب نقطه‌ای (dot): فرمول e_{t,i} = s_t^T * h_i. پیش‌شرط آن d_s == d_h است؛ یک قید سفت‌وسخت. اگر انکودر دوطرفه است، دور این حالت را خط بکشید.
  • حالت عمومی (general): فرمول e_{t,i} = s_t^T * W * h_i که در آن W دارای ابعاد (d_s, d_h) است. این حالت قید برابری ابعاد را حذف می‌کند.
  • الحاقی (concat): در اصل همان ساختار باداناو است و چون دو روش اول کم‌هزینه‌ترند، کمتر سراغش می‌روند.

یک تله‌ی مهم میان باداناو و لونگ. باداناو از s_{t-1} (وضعیت دکودر قبل از تولید کلمه‌ی جاری) استفاده می‌کند، اما لونگ به سراغ s_t (وضعیت دکودر بعد از تولید) می‌رود. جابه‌جا گرفتن این دو، خطاهای ریزی در گرادیان (gradient) ایجاد می‌کند که عیب‌یابی‌شان بسیار دشوار است. یکی از این دو مقاله را انتخاب کنید و پای قراردادش بمانید.

نقشه حرارتی اتنشنT را بکشید
ATTENTION HEATMAPEach query token scores every key token by dot product, divides by the temperature, then softmaxes the row so the weights sum to one. Darker cells take more attention. Lower temperature sharpens onto a single key; higher temperature spreads the focus.ThecatsatonthematThecatsatonthemat
هر توکن کوئری به تمام توکن‌های کلید از طریق ضرب داخلی امتیاز می‌دهد، بر دما تقسیم می‌کند و سپس روی سطر سافت‌مکس می‌زند تا مجموع وزن‌ها یک شود. سلول‌های تیره‌تر اتنشن بیشتری دریافت می‌کنند. دمای پایین‌تر تمرکز را روی یک کلید متمرکز می‌کند؛ دمای بالاتر تمرکز را پخش می‌سازد.

پیاده‌سازی

گام 1: اتنشن جمعی (باداناو)

import numpy as np


def additive_attention(decoder_state, encoder_states, W_a, U_a, v_a):
    projected_dec = W_a @ decoder_state
    projected_enc = encoder_states @ U_a.T
    combined = np.tanh(projected_enc + projected_dec)
    scores = combined @ v_a
    weights = softmax(scores)
    context = weights @ encoder_states
    return context, weights


def softmax(x):
    x = x - np.max(x)
    e = np.exp(x)
    return e / e.sum()

ابعاد تنسورهایتان را با جدول بالا تطبیق دهید. encoder_states دارای ابعاد (T_enc, d_h) است. projected_enc ابعاد (T_enc, d_attn) دارد. projected_dec دارای ابعاد (d_attn,) است و برادکست (broadcast) می‌شود. combined ابعاد (T_enc, d_attn) پیدا می‌کند. ابعاد scores برابر با (T_enc,)، ابعاد weights برابر با (T_enc,) و context دارای ابعاد (d_h,) است. آماده‌ی تحویل شد.

گام 2: روش‌های dot و general لونگ

def dot_attention(decoder_state, encoder_states):
    scores = encoder_states @ decoder_state
    weights = softmax(scores)
    return weights @ encoder_states, weights


def general_attention(decoder_state, encoder_states, W):
    projected = W.T @ decoder_state
    scores = encoder_states @ projected
    weights = softmax(scores)
    return weights @ encoder_states, weights

هرکدام فقط سه خط. دلیل ماندگاری مقاله‌ی لونگ همین بود: همان دقت در اکثر تسک‌ها، با کدی بسیار کمتر.

گام 3: یک مثال عددی گام‌به‌گام

فرض کنید سه وضعیت انکودر داریم (تقریباً معادل کلمات "cat" و "sat" و "mat") و وضعیت دکودر بیشترین شباهت را با کلمه‌ی اول دارد؛ در این حالت توزیع اتنشن روی موقعیت 0 متمرکز می‌شود. اگر وضعیت دکودر طوری تغییر کند که با کلمه‌ی آخر هماهنگ شود، تمرکز اتنشن به موقعیت 2 می‌رود. بردار بافت هم پابه‌پای آن تغییر جهت می‌دهد.

H = np.array([
    [1.0, 0.0, 0.2],
    [0.5, 0.5, 0.1],
    [0.1, 0.9, 0.3],
])

s_close_to_cat = np.array([0.9, 0.1, 0.2])
ctx, w = dot_attention(s_close_to_cat, H)
print("weights:", w.round(3))
weights: [0.464 0.305 0.231]

ردیف اول برنده شد. حالا وضعیت دکودر را طوری تغییر دهید که به سومین وضعیت انکودر نزدیک‌تر شود و جابه‌جا شدن وزن‌ها را ببینید. تمام اتنشن همین است: هم‌ترازی صریح (explicit alignment).

گام 4: چرا این مفهوم پل ورود به ترنسفورمرها است؟

مفاهیم بالا را به زبان Q/K/V برگردانید:

  • کوئری (Query) = وضعیت دکودر s_{t-1}
  • کلید (Key) = وضعیت‌های انکودر (همان چیزی که امتیاز را در مقایسه با آن حساب می‌کنیم)
  • مقدار (Value) = وضعیت‌های انکودر (همان چیزی که به آن وزن می‌دهیم و جمع می‌بندیم)

در اتنشن کلاسیک، کلید و مقدار یکی هستند. خود‌اتنشن آن‌ها را تفکیک می‌کند: می‌توانید یک توالی را با تصویر کردن‌های یادگرفته‌شده‌ی متفاوت برای K و V، در برابر خودش کوئری بزنید. اتنشن چندسر همین کار را به‌شکل موازی با تصویر کردن‌های یادگرفته‌شده‌ی گوناگون پیش می‌برد. ترنسفورمرها این کل مرحله را بارها روی هم می‌چینند و RNNها را یک‌جا کنار می‌گذارند.

ریاضیات همان است. ابعاد هم همان است. پرش آموزشی از اتنشن باداناو به اتنشن ضرب نقطه‌ای مقیاس‌شده (scaled dot-product attention)، بیشتر تغییر در نمادگذاری است تا مفهوم.

استفاده‌ی عملی

PyTorch و TensorFlow اتنشن را به‌صورت پیش‌فرض در خود دارند.

import torch
import torch.nn as nn

mha = nn.MultiheadAttention(embed_dim=128, num_heads=8, batch_first=True)
query = torch.randn(2, 5, 128)
key = torch.randn(2, 10, 128)
value = torch.randn(2, 10, 128)

output, weights = mha(query, key, value)
print(output.shape, weights.shape)
torch.Size([2, 5, 128]) torch.Size([2, 5, 10])

این یک لایه‌ی اتنشن ترنسفورمر است: یک بچ کوئری با 5 موقعیت، یک بچ کلید/مقدار با 10 موقعیت، هرکدام 128 بعدی، در قالب 8 سر. متغیر output همان کوئری‌های جدید تقویت‌شده با بافت است. weights هم ماتریس هم‌ترازی 5 در 10 است که می‌توانید بصری‌سازی کنید.

چرا اتنشن کلاسیک هنوز اهمیت دارد؟

  • کاربرد آموزشی: نسخه‌ی تک‌سر، تک‌لایه و مبتنی بر RNN همه‌ی مفاهیم را کاملاً ملموس و روشن می‌کند.
  • تسک‌های توالی روی دستگاه (On-device) در شرایطی که ترنسفورمرها جا نمی‌شوند.
  • مقالات سال‌های 2014 تا 2017: بدون دانستن قرارداد باداناو، آن‌ها را اشتباه خواهید خواند.
  • تحلیل موشکافانه‌ی هم‌ترازی در ترجمه‌ی ماشینی: وزن‌های خام اتنشن حتی در مدل‌های ترنسفورمر ابزاری برای تفسیرپذیری به حساب می‌آیند و خواندنشان نیازمند درک ماهیت آن‌هاست.

تله‌ی تفسیرپذیری بر اساس وزن‌های اتنشن

وزن‌های اتنشن به‌ظاهر کاملاً قابل‌تفسیرند؛ مجموعشان روی تمام موقعیت‌ها 1 می‌شود، می‌توان نمودارشان را کشید، و عدد بالا یعنی «مدل به این نقطه نگاه کرده است». داوران مقالات عاشق این تحلیل‌ها هستند.

اما ماجرا به این سادگی نیست. جین و والاس (Jain and Wallace, 2019) نشان دادند که در برخی تسک‌ها می‌توان توزیع وزن‌های اتنشن را جابه‌جا کرد یا متغیرهای دلخواه دیگری را جایگزینشان ساخت، بی‌آنکه پیش‌بینی‌های مدل تغییری کند. هرگز وزن‌های اتنشن را بدون آزمون حذف ویژگی (ablation) یا بررسی خلاف واقع (counterfactual)، به‌عنوان شواهد (evidence) استدلال مدل گزارش نکنید.

ذخیره‌سازی پرامپت

متن زیر را در فایل outputs/prompt-attention-shapes.md ذخیره کنید:

---
name: attention-shapes
description: Debug shape bugs in attention implementations.
phase: 5
lesson: 10
---

Given a broken attention implementation, you identify the shape mismatch. Output:

1. Which matrix has the wrong shape. Name the tensor.
2. What its shape should be, derived from (d_s, d_h, d_attn, T_enc, T_dec, batch_size).
3. One-line fix. Transpose, reshape, or project.
4. A test to catch regressions. Typically: assert `output.shape == (batch, T_dec, d_h)` and `weights.shape == (batch, T_dec, T_enc)` and `weights.sum(dim=-1) close to 1`.

Refuse to recommend fixes that silently broadcast. Broadcast-hiding bugs surface later as silent accuracy degradation, the worst kind of attention bug.

For Bahdanau confusion, insist the decoder input is `s_{t-1}` (pre-step state). For Luong, `s_t` (post-step state). For dot-product, flag dimension mismatch between query and key as the most common first-time error.

تمرین‌ها

  1. ساده. قابلیت ماسک کردن (masking) را در softmax پیاده کنید تا توکن‌های پدینگ (padding) در انکودر وزن اتنشن صفر بگیرند. این منطق را روی یک بچ با توالی‌هایی با طول متغیر بیازمایید.
  2. متوسط. قابلیت اتنشن چندسر را به حالت general لونگ بیفزایید. بعد d_h را به چند گروه n_heads تقسیم کنید، اتنشن را برای هر سر به‌طور مستقل اجرا نمایید و در پایان نتایج را الحاق (concatenate) کنید. مطمئن شوید که حالت تک‌سر با پیاده‌سازی قبلی شما یکی باشد.
  3. سخت. یک انکودر-دکودر GRU همراه با اتنشن باداناو را روی مسئله‌ی ساده‌ی کپی‌کردن درس 09 آموزش دهید. نمودار دقت بر حسب طول توالی را رسم کنید و آن را با مدل پایه‌ی بدون اتنشن بسنجید. باید ببینید که با بالا رفتن طول توالی فاصله‌ی عملکرد بیشتر می‌شود؛ امری که نشان می‌دهد اتنشن گلوگاه را از میان برداشته است.

اصطلاحات کلیدی

اصطلاح باور عامیانه معنای فنی دقیق
اتنشن (Attention) نگاه کردن به بخش‌های مختلف میانگین وزن‌دار از توالی مقادیر، که وزن‌هایش بر پایه‌ی شباهت کوئری و کلید به دست می‌آید.
کوئری، کلید، مقدار (Query, Key, Value) QKV سه بار تصویر کردن: کوئری می‌پرسد، کلید چیزی است که باید با آن تطبیق داده شود، و مقدار آنچه بازگردانده می‌شود.
اتنشن جمعی (Additive attention) اتنشن باداناو امتیازدهی پیش‌خور: v^T tanh(W q + U k).
اتنشن ضربی (Multiplicative attention) ضرب نقطه‌ای / حالت general لونگ امتیاز به‌صورت q^T k یا q^T W k محاسبه می‌شود. کم‌هزینه‌تر است و در اغلب تسک‌ها دقت یکسانی دارد.
ماتریس هم‌ترازی (Alignment matrix) همان تصویر جذاب وزن‌های اتنشن در قالب یک شبکه‌ی (T_dec, T_enc). با دیدنش متوجه می‌شوید مدل به چه بخش‌هایی توجه کرده است.

برای مطالعه‌ی بیشتر

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درساتنشن بهدانائو (Bahdanau) چه مشکلی را در مدل‌های seq2seq حل کرد؟
  2. پیش از درسبردار بافت (context vector) اتنشن در گام t دیکودر چیست؟
  3. حین درسدر اتنشن جمعی بهدانائو (additive)، بردار v_a چه نقشی ایفا می‌کند؟
  4. حین درسدر نوع اتنشن ضرب داخلی (dot) لونگ (Luong)، چه شرطی باید برقرار باشد؟
  5. حین درسکدام نگاشت Q/K/V اتنشن کلاسیک (بهدانائو/لونگ) را توصیف می‌کند؟
  6. پس از درسچرا گزارش وزن‌های خام اتنشن به‌عنوان «تفسیرپذیری» نامطمئن تلقی می‌شود؟
  7. پس از درسکدام گام اتنشن بهدانائو را به خود-اتنشن (self-attention) ترنسفورمر پیوند می‌دهد؟
  8. پس از درسیکی از کاربردهای عملی ماسک کردن (masking) در اتنشن چیست؟

سؤالات متداول

تفاوت اصلی اتنشن باداناو (Bahdanau) و لونگ (Luong) در چیست؟

اتنشن باداناو جمعی (Additive) است و از وضعیت دکودر در گام قبلی (s_{t-1}) استفاده می‌کند، در حالی که لونگ ضربی (Multiplicative) بوده و از وضعیت جاری دکودر (s_t) بهره می‌برد.

چرا ابعاد بردار بافت (Context Vector) با وضعیت انکودر یکی است؟

چون بردار بافت، میانگین وزن‌داری از وضعیت‌های پنهان انکودر است و ترکیب خطی بردارها بعد آن‌ها را تغییر نمی‌دهد.

آیا وزن‌های اتنشن مدرک قطعی برای چرایی تصمیم‌گیری مدل هستند؟

خیر؛ طبق پژوهش‌های جین و والاس، وزن‌های اتنشن در برخی تسک‌ها بدون تغییر پیش‌بینی مدل قابل تعویض هستند و نباید بدون آزمایش‌های کنترلی سند قطعیت استدلال تلقی شوند.

ما رو توی گوگل بیشتر ببین

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