איך מודל יודע מה חשוב?
"Attention is all you need" המאמר משנת 2017 שעליו בנויים כל ה-LLMs היום. בפוסט הזה נצלול לרעיון מאפס, מהאינטואיציה הראשונית ועד הנוסחה המלאה, כולל הניואנסים המתמטיים שכמעט אף אחד לא מסביר.
- #attention
- #transformers
- #LLM
כל מה שאתם מכירים מ-LLM, ChatGPT, Claude, Gemini, Llama, מסתמך על רעיון אחד שפורסם ב-2017. הרעיון הזה כל כך פשוט שאחרי שתבינו אותו, תשאלו את עצמכם איך אף אחד לא חשב עליו קודם.
תרגיל קטן לפני שמתחילים
קראו את המשפט הבא:
“המורה צעקה על התלמידה כי היא הייתה עצבנית.”
מי הייתה עצבנית? המורה.
עכשיו נחליף מילה אחת בלבד, בסוף המשפט:
“המורה צעקה על התלמידה כי היא התחצפה.”
מי התחצפה? התלמידה.
עכשיו כדאי לעצור רגע ולנסות להבין מה קרה כאן. שינינו מילה אחת בסוף המשפט (מ”עצבנית” ל”התחצפה”), והפרשנות של מילה אחרת לגמרי, “היא”, התהפכה. אותה מילה בדיוק, באותו מקום בדיוק, מתייחסת פעם אחת למורה ופעם אחת לתלמידה.
אז איך בדיוק המוח שלנו ידע לעשות את זה?
הוא נתן משקל שונה לכל מילה במשפט. כש”עצבנית” הייתה שם, “המורה” קיבלה את רוב המשקל בפענוח של “היא”, כי עצבנות היא סיבה טבעית לצעקה. כשהחלפנו ל”התחצפה”, “התלמידה” קיבלה את המשקל, כי היא זו שעשתה את הפעולה. שינוי במילה אחת גרם לנו לתת משקל שונה מחדש לכל המילים במשפט.
זה בדיוק מה ש-attention עושה. וזה הרעיון המרכזי שמאחורי כל LLM שאתם מכירים.
אבל קודם, איך מחשב בכלל “מבין” מילים?
לפני שנדבר בכלל על איך המודל נותן משקל למילים, צריך להבין איך הוא מייצג אותן בכלל. מחשב לא יודע מה זה “תלמידה”, הוא יודע רק מספרים.
הפתרון נקרא embedding. כל מילה הופכת לרשימת מספרים, או בשפה קצת יותר מקצועית - “וקטור” במרחב רב-ממדי. החוכמה היא שמילים דומות מקבלות וקטורים שמצביעים לכיוון דומה.
תארו לעצמכם מרחב דו-ממדי (האמיתי הוא ממדים רבים, אבל לצורך האינטואיציה):
לחצו על מילה כדי להוסיף או להסיר אותה מהמרחב.
“שמח” ו”מאושר” יהיו קרובים אחד לשני. “שמח” ו”עצוב” יהיו רחוקים, אבל על אותו ציר (רגש). “שמח” ו”מכונית” יהיו במקומות לגמרי אחרים.
זה הקסם של embeddings: הם הופכים יחסים סמנטיים ליחסים גאומטריים. ועל הגאומטריה הזאת אפשר לעשות חשבון.
בחזרה לבעיה שאנחנו רוצים לפתור
עכשיו יותר קל לנו להתמודד עם הבעיה שאנחנו רוצים לפתור (לתת משקל למילים בתוך משפט) כי אפשר לייצג אותה בצורה מתמטית.
נתון משפט עם מילים , ולכל מילה יש וקטור אמבדינג (מה שראינו בפסקה הקודמת) . ואנחנו רוצים לחשב לכל מילה ייצוג חדש , וקטור, שלוקח בחשבון את כל שאר המילים, אבל עם משקל שונה לכל אחת.
במילים אחרות:
כאשר הוא המשקל שמילה נותנת למילה .
המשקלים שולטים על המיקום של y. נסו לשנות אותם.
הכל מסתכם בשאלה אחת: איך מחשבים את ?
כלי ראשון - Dot Product או “מד דמיון”
אם יש לי שני וקטורים, איך אני יכול למדוד עד כמה הם דומים? יש כלי פשוט ומדהים מעולמות האלגברה לינארית שנקרא dot product (מכפלה סקלרית):
והרעיון של dot product הוא גאומטרי:
- אם שני הוקטורים מצביעים לאותו כיוון, התוצאה גבוהה
- אם הם ניצבים, התוצאה אפס
- אם הם מנוגדים, התוצאה שלילית
לחצו על כפתור. אותו כיוון = חיובי. ניצב = אפס. הפוך = שלילי.
אז אולי הפתרון הוא פשוט: ? כלומר, “נבדוק כמה המילה דומה לי, ואם היא דומה ממש היא תקבל יותר משקל”?
זה רעיון לא רע, אבל יש לו בעיה.
למה דמיון פשוט לא מספיק
בחזרה למשפט שלנו: “המורה צעקה על התלמידה כי היא הייתה עצבנית.”
המילה “היא” צריכה להסתכל על “המורה” כדי להבין מי האובייקט שמרגיש עצבנות. אבל באותו משפט, אם נשאל “מה גרם לצעקה?”, היא הייתה צריכה לתת משקל גם ל”עצבנית”, המילה שמסבירה את הסיבה.
אותה מילה צריכה להיות מסוגלת לשאול שאלות שונות בהקשרים שונים. ו-dot product פשוט בין embeddings לא מאפשר את הגמישות הזאת.
צריך משהו חכם יותר.
הטריק החדש - Q, K, V
והנה הטריק החדש, במקום להשתמש ב-embedding ישירות, נלמד שלוש הטלות(ניחן לחשוב על זה כלמידת “וקטורים” חדשים) של כל מילה.
לכל מילה נחשב:
כאשר הן מטריצות נלמדות (אלו הפרמטרים שהמודל מאמן).
המילה "היא" משחקת שלושה תפקידים שונים בו-זמנית. כל תפקיד הוא וקטור אחר, נלמד מאותו x דרך מטריצה אחרת. הנה מה שכל אחד מהם "אומר":
כל ריבוע = מספר אחד בוקטור. גובה המילוי = גודל המספר.
"מחפשת שם עצם נקבה שכבר הוזכר במשפט"
"אני כינוי גוף יחיד, מין נקבה"
"הפנייה לסובייקט קודם"
ההסברים בעברית הם פרשנות אנושית. במציאות, מה ש-Q/K/V "אומרים" נלמד מהנתונים ולא ניתן לפענוח ישיר במילים.
כל מילה הופכת לשלושה וקטורים שונים, אחד לכל תפקיד.
האנלוגיה הכי טובה היא חיפוש בגוגל:
- כשאתם מקלידים שאילתת חיפוש, זה ה-Query
- לכל אתר באינדקס יש תיאור / מטא-טאגים, זה ה-Key
- התוכן של האתר עצמו, זה ה-Value
גוגל מוצא התאמה בין השאילתה שלכם (Q) לתיאורי האתרים (K), ומחזיר את התוכן (V) של ההתאמות הטובות ביותר.
Attention עושה בדיוק את זה, רק שכל מילה במשפט היא גם שואלת וגם נשאלת בו-זמנית.
הקסם פה הוא ש-Q, K, V הן הטלות שונות של אותה מילה. המודל יכול ללמוד ש”היא” שואלת שאלות מסוג אחד דרך , ומציעה מידע מסוג אחר דרך . ההפרדה הזאת היא מה שהופך את המנגנון לכל כך גמיש.
חישוב המשקלים
עכשיו, בשביל לחשב כמה משקל מילה נותנת למילה , נשווה את ה-Query של לכל ה-Keys:
מעולה. יש לנו ציון! אבל יש בעיה, הציון הזה יכול להיות כל מספר, חיובי, שלילי, ענק. אנחנו רוצים שהמשקלים יהיו “אמיתיים” ואנחנו צריכים לדרוש ש:
- כולם יהיו חיוביים
- מסתכמים ל-1 (כדי שזה יהיה ממוצע משוקלל)
הפתרון המתבקש: softmax.
softmax - מציונים להסתברויות
softmax לוקח רשימה של מספרים והופך אותם להתפלגות הסתברות:
זה לא קסם, זה פשוט מאוד. שני שלבים:
- מעריך: הופך כל מספר לחיובי (וגם מגדיל הבדלים)
- נירמול: מחלקים בסכום כך שהכל יסתכם ל-1
גררו את הסליידרים. הפסים מראים את ההסתברויות שיוצאות (סכומן 1).
אז המשקל הסופי שלנו:
הניואנס המתמטי שכולם מדלגים עליו
עד עכשיו זה הסיפור שהרוב מספרים. אבל יש פה ניואנס יפה שרוב ההסברים מדלגים עליו, ועליו אני רוצה להתעכב.
הנוסחה האמיתית של attention היא לא , אלא:
מאיפה הגיע ה-? ( הוא הממד של וקטורי ה-Query וה-Key.)
הנה האינטואיציה: נניח ש- ו- הם וקטורים אקראיים שכל רכיב שלהם נדגם מהתפלגות עם שונות 1. ה-dot product שלהם הוא סכום של מכפלות, ולכן השונות שלו היא , וסטיית התקן היא .
מה זה אומר? כשהוקטורים ארוכים, ה-dot products הופכים גדולים יותר בממוצע.
ולמה זה רע? כי softmax היא רגישה מאוד לגודל הקלט. אם תכניסו ל-softmax את [1, 2, 3] תקבלו פיזור יחסית רך. אבל אם תכניסו [10, 20, 30], כמעט כל המסה תהיה על האיבר השלישי.
גררו את הסקאלה למעלה. ראו איך softmax מתחדדת, פס אחד לוקח כמעט את כל המסה.
החלוקה ב- מנרמלת את הציונים חזרה לקנה מידה הגיוני. זה שומר על softmax “רכה” מספיק כדי שגרדיאנטים יזרמו באימון.
זה ניואנס קטן, אבל בלעדיו המודל לא היה מתאמן בכלל.
הנוסחה המלאה
עכשיו אפשר להציג את הנוסחה המפורסמת, וכל איבר בה יהיה הגיוני:
- ציוני דמיון בין כל זוג מילים (Query מול Key)
- נירמול לשמירה על softmax רכה
- המרת ציונים למשקלים שמסתכמים ל-1
כפל ב-- שילוב המידע (Values) לפי המשקלים
וזהו! הנוסחה כבר לא מאיימת, היא סיכום של כל מה שדיברנו עד עכשיו.
דוגמה למשקלי Attention במשפט
נחזור לדוגמה: “המורה צעקה על התלמידה כי היא הייתה עצבנית.”
לחצו על מילה בצד ימין כדי לראות אילו מילים היא מקבלת מהן הכי הרבה משקל.
הערה: המשקלים בדמו למעלה עוצבו ידנית לפי דפוסי attention שמודלים אמיתיים מייצרים על משפטים דומים, לא הוצאו ישירות ממודל.
מימוש ב-PyTorch
עכשיו אפשר לראות כמה זה פשוט בקוד:
import torch
import torch.nn.functional as F
import math
def attention(Q, K, V):
"""
Q, K, V: tensors of shape (batch, seq_len, d_k)
Returns: tensor of shape (batch, seq_len, d_k)
"""
d_k = Q.size(-1)
# 1. Score matrix: QK^T / sqrt(d_k)
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)
# 2. Apply softmax to get attention weights
weights = F.softmax(scores, dim=-1)
# 3. Multiply by V to get weighted values
output = torch.matmul(weights, V)
return output, weights
class SelfAttention(torch.nn.Module):
def __init__(self, d_model, d_k):
super().__init__()
self.W_Q = torch.nn.Linear(d_model, d_k)
self.W_K = torch.nn.Linear(d_model, d_k)
self.W_V = torch.nn.Linear(d_model, d_k)
def forward(self, x):
# x: (batch, seq_len, d_model)
Q = self.W_Q(x) # (batch, seq_len, d_k)
K = self.W_K(x) # (batch, seq_len, d_k)
V = self.W_V(x) # (batch, seq_len, d_k)
return attention(Q, K, V)
זה הכל. כל ה-LLMs שאתם מכירים בנויים מהבלוקים האלה, מוערמים זה על זה.
מה הלאה?
ראינו attention עם “ראש” אחד, אחד. אבל במציאות, מודלים משתמשים ב-Multi-Head Attention, מספר מקבילי של ראשי attention, כל אחד עם זוויות מבט אחרות על אותו משפט.
למה ראש אחד לא מספיק? למה כל ראש לומד “תפקיד” שונה? ואיך זה מתחבר לארכיטקטורת ה-Transformer המלאה?
על זה נדבר בפוסטים הבאים.
אם הפוסט הזה עזר לכם, שתפו אותו עם מישהו שעדיין חושב ש-ChatGPT ‘מבין’ אותו.