مدلهای زبانی مدرن و معماریهای پیشرفته ترنسفورمر، تسلط امروز خود را مدیون مکانیزم خوداتنشن (Self-Attention) هستند. اگر تلاش کرده باشید معماری شبکههای RNN یا LSTM را برای دنبالههای طولانی به کار ببرید، حتماً با مشکل فراموشی اطلاعات در گامهای دور یا کندی پردازش ترتیبی روبهرو شدهاید؛ جایی که یک بردار با اندازهی ثابت به گلوگاهی سخت برای یادگیری روابط میان کلمات دور تبدیل میشود.
خوداتنشن با حذف گامهای ترتیبی و فراهمکردن امکان توجه همزمان و موازی همهی کلمات به یکدیگر، این گلوگاه را از میان برداشت. در این درس، به جای تکیه بر لایههای آماده و سربسته، فرمول مشهور اتنشن ضرب داخلی مقیاسشده (Scaled Dot-Product Attention) را با بردارهای پرسوجو (Query)، کلید (Key) و مقدار (Value) به کمک کتابخانهی NumPy از صفر پیادهسازی میکنید تا طرز کار واقعی این موتور محرک هوش مصنوعی را عمیقاً درک کنید.
پیادهسازی این درس کاملاً متکی بر محاسبات برداری پایه در CPU شخصی است و برای اجرای کدهای ریاضی آن نیازی به GPU یا ابزارهای ابری نخواهید داشت؛ هرچند که در ادامه، معادل صنعتی آن در PyTorch را نیز مرور خواهیم کرد.
اتنشن مثل یک جدول جستوجو است که در آن هر کلمه میپرسد «چه کسی برای من اهمیت دارد؟» — و پاسخ را یاد میگیرد.
اهداف یادگیری
- پیادهسازی خوداتنشن (self-attention) با ضرب داخلی مقیاسشده از صفر و فقط با استفاده از NumPy؛ شامل تصویر کردن (projection) پرسوجو/کلید/مقدار و مجموع وزنی سافتمکس
- ساخت یک لایهی اتنشن چندسر (multi-head attention) که سرها را جدا میکند، اتنشن موازی را محاسبه میکند و نتایج را به هم پیوند میزند
- ردگیری نحوهی ثبت روابط توکنها در ماتریس اتنشن و توضیح اینکه چرا مقیاسبندی با جلوی اشباع سافتمکس را میگیرد
- اعمال ماسک علی (causal masking) برای تبدیل اتنشن دوطرفه به اتنشن خودبازگشتی (سبک دیکودر)
صورت مسئله
شبکههای عصبی بازگشتی (RNN) توالیها را توکن به توکن پردازش میکنند. وقتی به توکن 50 میرسید، اطلاعات توکن 1 از 50 مرحله فشردهسازی رد شده است. وابستگیهای دوربرد در یک وضعیت پنهان (hidden state) با اندازهی ثابت فشرده و خرد میشوند؛ گلوگاهی که هیچ گیتبندیای در LSTM هم نمیتواند آن را کامل حل کند.
مقالهی اتنشن بهدنوا (Bahdanau) در سال 2014 راهحل را نشان داد: به دیکودر اجازه دهید به تکتک موقعیتهای انکودر نگاه کند و ببیند کدام موقعیتها برای گام فعلی مهم هستند. اما این سازوکار هنوز روی یک RNN سوار بود. مقالهی سال 2017 با عنوان «Attention Is All You Need» سؤالی هوشمندانهتر پرسید: چه میشود اگر اتنشن تنها سازوکار مدل باشد؟ بدون هیچ بازگشتی. بدون کانولوشن. فقط اتنشن.
خوداتنشن به هر موقعیت در یک توالی اجازه میدهد در یک گام موازی، به تمام موقعیتهای دیگر توجه کند. همین ویژگی است که ترنسفورمرها را سریع، مقیاسپذیر و دستنیافتنی کرده است.
مفاهیم پایه
شباهت با جستوجو در پایگاه داده
اتنشن را مثل یک جستوجوی نرم در پایگاه داده در نظر بگیرید:
Traditional database:
Query: "capital of France" --> exact match --> "Paris"
Attention:
Query: "capital of France" --> similarity to ALL keys --> weighted blend of ALL values
هر توکن سه بردار تولید میکند:
- پرسوجو یا Query (Q): «من دنبال چه میگردم؟»
- کلید یا Key (K): «من چه اطلاعاتی دارم؟»
- مقدار یا Value (V): «اگر انتخاب شوم، چه اطلاعاتی ارائه میدهم؟»
ضرب داخلی میان یک پرسوجو و همهی کلیدها، امتیازهای اتنشن را میسازد. امتیاز بالا یعنی «این کلید با پرسوجوی من همخوانی دارد». این امتیازها به مقدارها وزن میدهند و خروجی نهایی، مجموع وزنی این مقدارهاست.
محاسبهی Q، K و V
امبدینگ هر توکن از طریق سه ماتریس وزن یادگرفتنی تصویر میشود:
Input embeddings (sequence of n tokens, each d-dimensional):
X = [x1, x2, x3, ..., xn] shape: (n, d)
Three weight matrices:
Wq shape: (d, dk)
Wk shape: (d, dk)
Wv shape: (d, dv)
Projections:
Q = X @ Wq shape: (n, dk) each token's query
K = X @ Wk shape: (n, dk) each token's key
V = X @ Wv shape: (n, dv) each token's value
تصویر مفهومی برای یک توکن:
Wq
x_i ------[*]------> q_i "What am I looking for?"
|
| Wk
+----[*]------> k_i "What do I contain?"
|
| Wv
+----[*]------> v_i "What do I offer?"
ماتریس اتنشن
وقتی Q، K و V را برای همهی توکنها داشته باشید، امتیازهای اتنشن یک ماتریس را تشکیل میدهند:
Scores = Q @ K^T shape: (n, n)
k1 k2 k3 k4 k5
+-----+-----+-----+-----+-----+
q1 | 2.1 | 0.3 | 0.1 | 0.8 | 0.2 | <- how much q1 attends to each key
+-----+-----+-----+-----+-----+
q2 | 0.4 | 1.9 | 0.7 | 0.1 | 0.3 |
+-----+-----+-----+-----+-----+
q3 | 0.2 | 0.6 | 2.3 | 0.5 | 0.1 |
+-----+-----+-----+-----+-----+
q4 | 0.9 | 0.1 | 0.4 | 1.7 | 0.6 |
+-----+-----+-----+-----+-----+
q5 | 0.1 | 0.3 | 0.2 | 0.5 | 2.0 |
+-----+-----+-----+-----+-----+
Each row: one token's attention over the entire sequence
ببینید چطور هر بار یک پرسوجو کلیدها را پیمایش میکند: هر سطر به تمام توکنها امتیاز میدهد، سافتمکس این امتیازها را به وزن تبدیل میکند، و بردار بافتار (context vector) حاصل ترکیب وزنی مقدارهاست.
چرا مقیاسبندی میکنیم؟
حاصلضربهای داخلی با افزایش بُعد بزرگتر میشوند. اگر باشد، ضرب داخلی میتواند به اعدادی در حدود چند ده برسد و ورودی سافتمکس را به مناطقی ببرد که گرادیان در آنها ناپدید میشود. راهحل ساده است: تقسیم بر .
این کار مقادیر را در محدودهای نگه میدارد که سافتمکس گرادیانهای مفیدی تولید کند.
تبدیل امتیازها به وزن با سافتمکس
سافتمکس امتیازهای خام را در هر سطر به یک توزیع احتمالی تبدیل میکند:
Raw scores for q1: [2.1, 0.3, 0.1, 0.8, 0.2]
|
softmax
|
Attention weights: [0.52, 0.09, 0.07, 0.14, 0.08] (sums to ~1.0)
حالا هر توکن مجموعهای از وزنها دارد که نشان میدهد چقدر باید به تکتک توکنهای دیگر توجه کند.
مجموع وزنی مقادیر
خروجی نهایی برای هر توکن، مجموع وزنی همهی بردارهای مقدار است:
output_i = sum( attention_weight[i][j] * v_j for all j )
For token 1:
output_1 = 0.52 * v1 + 0.09 * v2 + 0.07 * v3 + 0.14 * v4 + 0.08 * v5
پایپلاین کامل
فرمول در یک خط:
پیادهسازی از صفر
گام 1: پیادهسازی سافتمکس از صفر
سافتمکس لاجیتهای خام را به احتمال تبدیل میکند. برای پایداری عددی (numerical stability)، بیشینهی مقدار را از لاجیتها کم کنید.
import numpy as np
def softmax(x):
shifted = x - np.max(x, axis=-1, keepdims=True)
exp_x = np.exp(shifted)
return exp_x / np.sum(exp_x, axis=-1, keepdims=True)
logits = np.array([2.0, 1.0, 0.1])
print(f"logits: {logits}")
print(f"softmax: {softmax(logits)}")
print(f"sum: {softmax(logits).sum():.4f}")
گام 2: اتنشن ضرب داخلی مقیاسشده
تابع اصلی. ماتریسهای Q، K و V را میگیرد و خروجی اتنشن را همراه با ماتریس وزنها برمیگرداند.
def scaled_dot_product_attention(Q, K, V):
dk = Q.shape[-1]
scores = Q @ K.T / np.sqrt(dk)
weights = softmax(scores)
output = weights @ V
return output, weights
گام 3: کلاس خوداتنشن با تصویر کردنهای یادگرفتنی
یک ماژول کامل خوداتنشن با ماتریسهای وزن Wq، Wk و Wv که با مقیاسبندی خاویر (Xavier) مقداردهی اولیه شدهاند.
class SelfAttention:
def __init__(self, d_model, dk, dv, seed=42):
rng = np.random.default_rng(seed)
scale = np.sqrt(2.0 / (d_model + dk))
self.Wq = rng.normal(0, scale, (d_model, dk))
self.Wk = rng.normal(0, scale, (d_model, dk))
scale_v = np.sqrt(2.0 / (d_model + dv))
self.Wv = rng.normal(0, scale_v, (d_model, dv))
self.dk = dk
def forward(self, X):
Q = X @ self.Wq
K = X @ self.Wk
V = X @ self.Wv
output, weights = scaled_dot_product_attention(Q, K, V)
return output, weights
گام 4: اجرا روی یک جمله
برای یک جمله امبدینگهای فرضی بسازید و وزنهای اتنشن را ببینید.
sentence = ["The", "cat", "sat", "on", "the", "mat"]
n_tokens = len(sentence)
d_model = 8
dk = 4
dv = 4
rng = np.random.default_rng(42)
X = rng.normal(0, 1, (n_tokens, d_model))
attn = SelfAttention(d_model, dk, dv, seed=42)
output, weights = attn.forward(X)
print("Attention weights (each row: where that token looks):\n")
print(f"{'':>6}", end="")
for token in sentence:
print(f"{token:>6}", end="")
print()
for i, token in enumerate(sentence):
print(f"{token:>6}", end="")
for j in range(n_tokens):
w = weights[i][j]
print(f"{w:6.3f}", end="")
print()
گام 5: مصورسازی اتنشن با هیتمپ متنی (ASCII)
وزنهای اتنشن را روی کاراکترها نگاشت کنید تا تصویری سریع از وضعیت آن داشته باشید.
def ascii_heatmap(weights, tokens, chars=" ░▒▓█"):
n = len(tokens)
print(f"\n{'':>6}", end="")
for t in tokens:
print(f"{t:>6}", end="")
print()
for i in range(n):
print(f"{tokens[i]:>6}", end="")
for j in range(n):
level = int(weights[i][j] * (len(chars) - 1) / weights.max())
level = min(level, len(chars) - 1)
print(f"{' ' + chars[level] + ' '}", end="")
print()
ascii_heatmap(weights, sentence)
استفاده از ابزارهای واقعی
ماژول nn.MultiheadAttention در PyTorch دقیقاً همان کاری را میکند که ساختیم؛ به اضافهی تقسیم سرها و تصویر کردن خروجی:
import torch
import torch.nn as nn
d_model = 8
n_heads = 2
seq_len = 6
mha = nn.MultiheadAttention(embed_dim=d_model, num_heads=n_heads, batch_first=True)
X_torch = torch.randn(1, seq_len, d_model)
output, attn_weights = mha(X_torch, X_torch, X_torch)
print(f"Input shape: {X_torch.shape}")
print(f"Output shape: {output.shape}")
print(f"Attention weight shape: {attn_weights.shape}")
print(f"\nAttn weights (averaged over heads):")
print(attn_weights[0].detach().numpy().round(3))
تفاوت کلیدی این است: اتنشن چندسر چندین تابع اتنشن را به طور موازی اجرا میکند که هر کدام تصویرهای Q، K و V اختصاصی خود را با اندازهی دارند، سپس نتایج را به هم پیوند میزند. این کار به مدل اجازه میدهد همزمان به انواع مختلفی از روابط توجه کند.
خروجی این درس
این درس این خروجی را تولید میکند:
outputs/prompt-attention-explainer.md- پرامپتی برای توضیح اتنشن با تکیه بر استعارهی جستوجو در پایگاه داده
تمرینها
- تابع
scaled_dot_product_attentionرا طوری تغییر دهید که یک ماتریس ماسک اختیاری بگیرد و پیش از اعمال سافتمکس، برخی موقعیتها را برابر با منفی بینهایت بگذارد (ماسک علی/دیکودر به همین شیوه کار میکند). - اتنشن چندسر را از صفر پیادهسازی کنید: Q، K و V را به بخشهای
n_headsتقسیم کنید، اتنشن را روی هر بخش اجرا کنید، نتایج را به هم پیوند بزنید، و در نهایت از یک ماتریس وزن نهایی Wo عبور دهید. - دو جملهی مختلف با طول یکسان بردارید، آنها را به یک نمونهی واحد از SelfAttention بدهید، و الگوهای اتنشن آنها را مقایسه کنید. چه چیزی تغییر میکند؟ چه چیزی ثابت میماند؟
اصطلاحات کلیدی
| اصطلاح | در گفتوگوهای عامیانه | معنای دقیق فنی |
|---|---|---|
| پرسوجو / Query (Q) | «بردار سؤال» | تصویری یادگرفتنی از ورودی که نشان میدهد این توکن دنبال چه اطلاعاتی میگردد |
| کلید / Key (K) | «بردار برچسب» | تصویری یادگرفتنی که نشان میدهد این توکن چه اطلاعاتی در خود دارد و در برابر پرسوجوها سنجیده میشود |
| مقدار / Value (V) | «بردار محتوا» | تصویری یادگرفتنی حامل محتوای اصلی که بر اساس امتیازهای اتنشن تجمیع میشود |
| اتنشن ضرب داخلی مقیاسشده (Scaled dot-product attention) | «فرمول اتنشن» | فرمول - مقیاسبندی جلوی اشباع سافتمکس را در ابعاد بالا میگیرد |
| خوداتنشن (Self-attention) | «توکن به خودش و بقیه نگاه میکند» | نوعی از اتنشن که در آن Q، K و V همگی از یک توالی میآیند و به هر موقعیت اجازه میدهند به تمام موقعیتهای دیگر توجه کند |
| وزنهای اتنشن (Attention weights) | «میزان توجه» | توزیع احتمالی روی موقعیتها که حاصل اعمال سافتمکس روی ضربهای داخلی مقیاسشده است |
| اتنشن چندسر (Multi-head attention) | «اتنشن موازی» | اجرای چند تابع اتنشن با تصویر کردنهای مختلف و سپس پیوند زدن نتایج به هم برای رسیدن به بازنماییهای غنیتر |
برای مطالعهی بیشتر
- Attention Is All You Need (Vaswani et al., 2017) - مقالهی اصلی ترنسفورمر
- The Illustrated Transformer (Jay Alammar) - بهترین راهنمای تصویری برای درک ساختار کلی ترنسفورمر
- The Annotated Transformer (Harvard NLP) - پیادهسازی خطبهخط ترنسفورمر در PyTorch همراه با توضیحات
