דלג לתוכן
9/30פרק 9 מתוך 30

Attention ובלוק ה-Transformer, נגזרים מממוצע

מתחילים מהממוצע כסיכום הזול ביותר של context, מודדים איפה הוא נכשל, וגוזרים ממנו את נוסחת ה-attention.

בעמוד הזה

אתם מגיעים לכאן עם tokenizer מפרק 7, טבלת embedding מפרק 8, והיעד שמגיע איתם: בהינתן ה-tokens עד כה, לשים הסתברות על ה-token הבא.

מה שחסר הוא האמצע. כדי לחזות את token tt המודל צריך וקטור אחד שמסכם את כל מה שלפניו, ושום דבר שבניתם לא מייצר אחד כזה. ה-embedding של token t1t-1 אינו זה — זה מודל bigram, והוא לא יכול לדעת שהמשפט התחיל בשאלה. גם שרשור של כל ה-embeddings הקודמים אינו זה: מספרם משתנה בכל צעד, ומטריצת משקלים קבועה לא יכולה לקבל קלט באורך משתנה.

אז: וקטור אחד בגודל קבוע, שמסכם מספר משתנה של וקטורים. זו כל הבעיה, ו-attention הוא מה שמקבלים כשפותרים אותה בדרך העצלנית ביותר האפשרית ואז מתקנים את שני הדברים שנשברים.

התשובה שהייתה לתחום, ולמה אנחנו לא בונים אותה

קישור למקטע: התשובה שהייתה לתחום, ולמה אנחנו לא בונים אותה

מ-1997 ועד בערך 2017 הסיכום היה מצב רקורנטי: לשמור וקטור h\mathbf{h} ולעדכן אותו בכל token, ht=f(ht1,xt)\mathbf{h}_t = f(\mathbf{h}_{t-1}, \mathbf{x}_t). גודל קבוע, קלט משתנה, בדיוק הצורה הנכונה.

הוא נכשל בשלוש דרכים, והארכיטקטורה של הפרק הזה עונה על שלושתן. ביצוע backpropagation דרך TT צעדים מכפיל TT יעקוביאנים, ולכן ה-gradient נעלם או מתפוצץ — המחלה שפרק 5 מדד בתוך צומת tanh\tanh יחיד. ה-LSTM1 תוכנן בדיוק נגד זה והרחיב את הטווח השימושי מעשרות צעדים למאות, בלי לשנות את העובדה שמידע מ-token 5 מגיע ל-token 500 רק אם שרד 495 עדכונים סדרתיים. כל המקור היה צריך להיכנס לווקטור אחד: בתרגום sequence-to-sequence2 encoder דוחס את הקלט למצב הסופי שלו. Bahdanau, Cho ו-Bengio נתנו שם לצוואר הבקבוק הזה ותיקנו אותו ב-2014, שלוש שנים לפני ה-transformer, בכך שאפשרו ל-decoder לקחת סכום משוקלל של כל מצבי ה-encoder עם משקלים שחישב בעצמו.3 כל מה שמתחת הוא הרעיון הזה, מיושם על ידי רצף על עצמו, כשהרקורסיה נמחקה. וגם העדכון סדרתי מעצם בנייתו: ht\mathbf{h}_t צריך את ht1\mathbf{h}_{t-1}, ו-GPU עם עשרת אלפים ליבות לא יכול לעשות עם זה כלום. הארכיטקטורה שניצחה אינה בהכרח חכמה יותר; היא זו שהצעד היקר שלה הוא כפל מטריצות.

ההטיה האינדוקטיבית הקלאסית האחרת, קונבולוציה — להחליק מסנן קטן אחד על כל הקלט, כך שתכונה שמזוהה בכל מקום תזוהה בכל מקום — גם היא לא נבנית כאן; היא כמעט בדיוק נכונה לתמונות ומואצלת לקורס ראייה. לא רקורסיה ולא קונבולוציה מופיעות שוב אחרי הדף הזה, ולכן אף אחת מהן לא מקבלת פרק: פרק 1 הבטיח שההשמטות יוצהרו, לא יוסתרו בשקט.

הפונקציה הברורה ביותר שמקבלת מספר משתנה של וקטורים ומחזירה וקטור אחד היא הממוצע:

ct=1ti=1txi\mathbf{c}_t = \frac{1}{t}\sum_{i=1}^{t} \mathbf{x}_i

כל מספר קלטים, גודל פלט קבוע, דיפרנציאבילי, חינם. טבלת embedding ועוד הממוצע הזה ועוד שכבה ליניארית אל אוצר המילים הם מודל שפה שלם בחמש-עשרה שורות. הוא גם נוראי, והדרך שבה הוא נוראי היא כל הגזירה.

הקורפוס למטה הוא מגה-בייט אחד של שייקספיר, 1,115,394 תווים, דרך byte-level BPE tokenizer מהסוג שנבנה בפרק 7 עם אוצר מילים של 1024: 459,760 tokens באורך 2.43 תווים כל אחד, בחלוקה 90/10. כל מודל הוא ברוחב 128, רואה 128 tokens, ומתאמן במשך 3000 צעדי AdamW ב-10310^{-3} עם batch של 64. Perplexity נמדדת על הפיצול המוחזק בצד.4

מודלפרמטריםvalidation perplexity
ה-token הנוכחי בלבד, בלי context כלל263,16859.71
בתוספת הממוצע האחיד של כל מה שלפניו263,168248.07
בתוספת embeddings מיקום נלמדים279,552245.93
ממוצע אחיד נוסף ל-token במקום להחליף אותו263,16860.45

קראו את השורה השנייה פעמיים. מיצוע ה-context לא עוזר קצת; הוא הופך את המודל לגרוע פי ארבעה מאשר התעלמות מוחלטת מה-context. שתי סיבות, שתיהן ניתנות להוכחה ולא אמפיריות בלבד.

הממוצע לא יכול לראות סדר. חיבור הוא קומוטטיבי, ולכן ערבוב החלון משאיר את הסיכום ללא שינוי — לא בקירוב:

order.pyPYTHON
A = torch.tril(torch.ones(T, T))
A = A / A.sum(1, keepdim=True)          # rows of the averaging matrix
y = x[torch.randperm(T)]                # the same tokens, shuffled
print((A[-1] @ x - A[-1] @ y).abs().max().item())
TEXT
2.9802322387695312e-08

רעש נקודה צפה על סכום שסודר מחדש: שני הסיכומים הם אותו וקטור. מודל שהמבט היחיד שלו על ה-context הוא ממוצע לא יכול להבחין בין the dog bit the man לבין the man bit the dog. השורה השלישית מוכיחה שאי אפשר לתקן זאת על ידי הוספת מיקומים לקלטים — embedding מיקום נלמד על כל token לפני המיצוע קנה 2.14 נקודות מתוך 188. המיקומים נכנסים לסכום, והסכום שוכח אותם.

והממוצע מטביע את ההווה. במיקום 100 ה-token הנוכחי הוא מאית מהסיכום. לזה יש תיקון זול שכבר בבעלותכם: לשמור את ה-token ולהוסיף לו את הסיכום — חיבור residual, מפרק 6, והשורה הרביעית מראה מה הוא עושה. אחרי תיקון הדילול, הממוצע האחיד לא תורם שום דבר בכלל: 60.45 מול baseline של 59.71. כל token נמצא שם, במשקל שווה, ומשקל שווה הוא אותו דבר כמו אין מידע.

הבעיה אינה המיצוע. היא המשקלים.

הממוצע הוא כפל מטריצות, וה-mask הוא softmax

קישור למקטע: הממוצע הוא כפל מטריצות, וה-mask הוא softmax

מיצוע על prefix גדל נראה כמו לולאה. הוא כפל אחד במטריצה משולשת-תחתונה שהשורות שלה מסתכמות לאחד — וגם, בדיוק, softmax:

mechanics.pyPYTHON
loop = torch.stack([x[:t + 1].mean(0) for t in range(T)])   # the obvious version

A = torch.tril(torch.ones(T, T))
A = A / A.sum(1, keepdim=True)
mat = A @ x                                                  # the same thing

S = torch.zeros(T, T).masked_fill(torch.tril(torch.ones(T, T)) == 0, float("-inf"))
soft = F.softmax(S, dim=-1) @ x                              # and the same thing again
TEXT
loop vs matmul   max |diff| = 5.960464477539063e-08
loop vs softmax  max |diff| = 5.960464477539063e-08

the averaging matrix A (rows sum to 1, upper triangle is zero):
  1.000 0.000 0.000 0.000 0.000 0.000
  0.500 0.500 0.000 0.000 0.000 0.000
  0.333 0.333 0.333 0.000 0.000 0.000
  0.250 0.250 0.250 0.250 0.000 0.000
  0.200 0.200 0.200 0.200 0.200 0.000
  0.167 0.167 0.167 0.167 0.167 0.167

שלושה רכיבים בעלי שם של transformer נמצאים עכשיו על המסך. המשולש הוא ה-causal mask, שנכפה על ידי היעד: אם מיקום tt היה יכול לראות את מיקום t+1t{+}1 התשובה הייתה בתוך הקלט — הדליפה שפרק 6 אמר לכם לבקר, רק בתוך הארכיטקטורה. ה-softmax הוא הדרך שבה ה-mask מיושם: הצבת ערכים אסורים ל--\infty שולחת אותם בדיוק לאפס ומנרמלת את מה שנשאר, כך ש-masking ונרמול הם פעולה אחת. (השתמשו ב--\infty, לא ב--1e9: זה הערך שה-masking משמעו, הוא שורד המרה ל-float16 כ--\infty, והוא חוסך לכם להחליט אם הקבוע שבחרתם גדול מספיק לטווח שבמקרה אתם נמצאים בו — שזו קופסת הנקודה הצפה של פרק 2 ששואלת שאלה שאין צורך לענות עליה.) וה-scores הם הפרמטר החופשי. הממוצע האחיד הוא מה שמקבלים כשכל score מותר הוא אותו מספר; שימו שם מספרים כלשהם וה-softmax יהפוך אותם למשקלים תקינים.

שאר הפרק הזה הוא שאלה אחת: מאיפה המספרים האלה מגיעים?

הם לא יכולים להיות פרמטרים רגילים. מטריצת T×TT \times T נלמדת תהיה זהה לכל משפט — היא תוכל לקודד "הסתכל ארבעה tokens אחורה" אבל לעולם לא "הסתכל על שם העצם שהכינוי הזה מתייחס אליו". המשקל שמקשר את מיקום tt למיקום ii חייב להיות תלוי במה שנמצא בשני המיקומים, כי רלוונטיות היא יחס, לא תכונה: המילה it אינה רלוונטית מעצמה, היא רלוונטית למשהו.

הפונקציה הזולה ביותר שמקבלת שני וקטורים ומחזירה מספר היא המכפלה הסקלרית מפרק 1. תנו score למיקום ii עבור מיקום tt כ-xtxi\mathbf{x}_t \cdot \mathbf{x}_i והמנגנון עובד — רע, בשתי דרכים שמכריחות את כל השאר. המכפלה הסקלרית של וקטור עם עצמו היא הנורמה בריבוע שלו, ולכן כל token היה עושה attend בעיקר לעצמו. והיחס היה סימטרי: אם it עושה attend חזק ל-animal, אז animal עושה attend חזק ל-it, וזה שקר לגבי שפה, שבה שם תואר צריך את שם העצם שלו הרבה יותר מששם העצם צריך את שם התואר.

לכן תנו לכל token שני תפקידים, כשתי מפות ליניאריות נלמדות שלו: מה המיקום הזה מחפש, qt=Wqxt\mathbf{q}_t = W_q\mathbf{x}_t, ה-query; ומה הוא מציע כדי שימצאו אותו לפיו, ki=Wkxi\mathbf{k}_i = W_k\mathbf{x}_i, ה-key. תנו score qtki\mathbf{q}_t \cdot \mathbf{k}_i והסימטריה נעלמת, כי WqWkW_q \neq W_k: token יכול לפרסם דבר אחד ולחפש דבר אחר.

דבר אחד עדיין שגוי. הסכום המשוקלל היה על ה-xi\mathbf{x}_i עצמם, מה שמכריח את הדבר שמועתק להיות הדבר שמותאם. התאמה רוצה את התכונות שמזהות token; העתקה רוצה את התכונות שמועילות בהמשך. לכן לומדים מפה שלישית, vi=Wvxi\mathbf{v}_i = W_v\mathbf{x}_i, ה-value, וסוכמים אותם.

הנוסחה היא עכשיו הנהלת חשבונות:

Attention(Q,K,V)=softmax ⁣(QKdk+M)V\mathrm{Attention}(Q, K, V) = \mathrm{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}} + M\right)V

כאשר MM הוא ה-causal mask, אפס על האלכסון ומתחתיו ו--\infty מעליו. בקוד זה שלושים שורות, שעשרים מהן הן צורות:

attention.pyPYTHON
class Head(nn.Module):
    """One head of causal self-attention."""

    def __init__(self, d_model, d_head, block):
        super().__init__()
        self.q = nn.Linear(d_model, d_head, bias=False)      
        self.k = nn.Linear(d_model, d_head, bias=False)      
        self.v = nn.Linear(d_model, d_head, bias=False)      
        self.d_head = d_head
        self.register_buffer("mask", torch.tril(torch.ones(block, block)).bool())

    def forward(self, x):
        T = x.shape[1]
        q, k, v = self.q(x), self.k(x), self.v(x)
        s = q @ k.transpose(-2, -1) / math.sqrt(self.d_head)          
        s = s.masked_fill(~self.mask[:T, :T], float("-inf"))          
        w = F.softmax(s, dim=-1)                                      
        return w @ v                                                  

לתת score, לעשות mask, לנרמל, לערבב. כל השאר הוא projection.

החלוקה בשורש הריבועי, וממה היא מגינה

קישור למקטע: החלוקה בשורש הריבועי, וממה היא מגינה

כמעט כל הסבר של dk\sqrt{d_k} אומר "כדי למנוע מה-softmax להגיע לרוויה", וזה נכון ולא מסביר דבר. הטיעון הוא שתי שורות של שונות מפרק 2. אם הערכים של q\mathbf{q} ו-k\mathbf{k} בלתי תלויים עם ממוצע אפס ושונות אחת, לכל מכפלה qjkjq_j k_j יש שונות אחת, ושונויות של דברים בלתי תלויים מסתכמות:

Var(qk)=j=1dkVar(qjkj)=dk\mathrm{Var}(\mathbf{q}\cdot\mathbf{k}) = \sum_{j=1}^{d_k}\mathrm{Var}(q_j k_j) = d_k

אז ל-scores יש סטיית תקן dk\sqrt{d_k}. נמדד על עשרים אלף זוגות אקראיים:

TEXT
     d     Var(q.k)         std   sqrt(d)
     4        3.975       1.994     2.000
    16       16.071       4.009     4.000
    64       64.249       8.016     8.000
   256      253.065      15.908    16.000
  1024     1015.562      31.868    32.000

למה זה משנה: ה-softmax רגיש לסקאלה באופן ששכבה ליניארית אינה. הכפלת הקלט של שכבה ליניארית מכפילה את הפלט שלה; הכפלת scores בעשר לפני softmax הופכת תערובת רכה לבחירה קשיחה. שורה אחת של 64 scores, עם ובלי החלוקה:

dkd_kהמשקל הגדול ביותר, ללא חלוקהאנטרופיהtokens אפקטיבייםהמשקל הגדול ביותר, מחולקאנטרופיהtokens אפקטיביים
40.2052.94419.00.0813.75842.9
160.4381.6925.40.0753.84946.9
640.4890.8742.40.0853.67339.4
2560.99990.00071.00.1433.54734.7
10241.00000.00001.00.1323.64438.3

"tokens אפקטיביים" הוא האקספוננט של האנטרופיה: על כמה מיקומים השורה באמת ממוצעת. ללא חלוקה, ב-dk=256d_k = 256, head שאותחל זה עתה עושה attend בדיוק ל-token אחד מתוך 64, שנבחר רק על ידי ההגרלה האקראית.

זה רע קדימה וגרוע יותר אחורה, בצורה שפרק 5 כבר מדד על tanh\tanh. ל-softmax שהתחייב לערך אחד כמעט אין נגזרת: האלכסון של היעקוביאן שלו הוא wi(1wi)w_i(1-w_i), אפס בשני הקצוות. על פני אלפיים שורות אקראיות:

dkd_kiwi(1wi)\sum_i w_i(1-w_i) ללא חלוקהמחולקשורות ברוויה (המשקל הגדול ביותר מעל 0.99)
40.84270.95680.2 % → 0.0 %
640.29400.960917.9 % → 0.0 %
2560.14060.960949.1 % → 0.0 %
10240.06810.961170.4 % → 0.0 %

ב-dk=1024d_k = 1024, שבע שורות מתוך עשר קפואות עוד לפני שהאימון מתחיל, ו-head שמתחיל קפוא לא יכול ללמוד על מה להסתכל. עם חלוקה, הכמות שטוחה ב-0.96 בכל רוחב ושום דבר לא מגיע לרוויה.

עכשיו החלק שאף אחד לא מפרסם: האם זה משנה את ה-perplexity הסופית? מחקו את החלוקה ואמנו, בארבעה רוחבי head:

רוחב headללא חלוקהמחולק ב-dk\sqrt{d_k}מחולק ב-dkd_k
ארבעה heads, dk=32d_k = 3237.2938.0737.89
head אחד, dk=128d_k = 12848.5146.1045.99
head אחד, dk=256d_k = 25665.3747.53
head אחד, dk=512d_k = 51267.0649.15
head אחד, dk=1024d_k = 102476.6959.17

שתי השורות הראשונות מגיעות מתקציב 3000 הצעדים שלמעלה; שלוש האחרונות הן ריצה קצרה יותר — 1500 צעדים, batch של 32, head אחד, בלי נרמול לפני ה-projections — כאשר שתי הגרסאות תחת הגדרות זהות.

ב-dk=32d_k = 32 החלוקה לא שווה כלום והריצה בלעדיה מקדימה במעט. זו לא רשות לוותר עליה, כי ב-256 היא שווה 18 נקודות perplexity וב-1024 היא שווה 17. המנגנון נראה ב-scores עצמם:

dkd_kscore std באתחולאחרי 1500 צעדים, ללא חלוקהאחרי 1500 צעדים, מחולקשורות ברוויה, ללא חלוקהמחולק
25610.49121.672.1391.9 %0.8 %
51215.13836.852.6698.7 %1.3 %
102421.155147.463.4499.9 %16.5 %

ה-head ללא החלוקה לא מתאושש. הוא בורח: סטיית התקן של ה-scores שלו עולה מ-21 באתחול ל-5147, אנטרופיית ה-attention יורדת לאפס, ו-99.9 % מהשורות שמות יותר מ-0.99 ממשקלן על token יחיד. ברגע ש-head הוא בורר קשיח ה-gradient שלו כמעט אפס ושום דבר לא מושך אותו חזרה, ולכן הקריסה יציבה. ה-head המחולק יושב על סטיית תקן score של 3.44 אחרי אותו אימון, תערובת רכה שעדיין אפשר לשנות.

Vaswani et al. אומרים בדיוק את זה ולא יותר — הם חושדים שהמכפלות "grow large in magnitude for large values of dkd_k" ומחלקים.5 המילה large נושאת משקל, והטבלאות אומרות איפה גדול מתחיל: כלום ב-32, הכול עד 256.

יותר מדעה אחת, ושני השלישים שאף אחד לא מדבר עליהם

קישור למקטע: יותר מדעה אחת, ושני השלישים שאף אחד לא מדבר עליהם

head אחד הוא שורת softmax אחת לכל מיקום, ולכן הוא מחזיק תשובה אחת ל"מה רלוונטי כאן". חיזוי המילה אחרי the ב-the animal that crossed the wet street צריך את המשבצת התחבירית, את הנושא ואת ה-token הקודם בבת אחת, והתפלגות הסתברות אחת לא יכולה להיות מרוכזת בשלושה מקומות. לכן מריצים כמה heads במקביל, כל אחד ברוחב dmodel/hd_{\text{model}}/h, משרשרים, ומערבבים עם עוד מטריצה WoW_o: חילקתם את הרוחב, לא הוספתם לו.

Attention גם עושה בדיוק דבר אחד — הוא מעביר מידע בין מיקומים. כל פעולה בקוד למעלה ליניארית לאורך ציר התכונות, ופרק 5 הוכיח מהי ערימה של מפות ליניאריות. לכן כל block נושא גם MLP קטן שמוחל על כל מיקום בנפרד, מרחיב את הרוחב פי ארבע וחוזר, עם GELU באמצע. כדאי לזכור את חלוקת העבודה: attention מערבב בין מיקומים, רשת ה-feed-forward מחשבת בתוך מיקום.

הסולם המלא, כל שורה מוסיפה רכיב אחד לשורה שמעליה:

מודלפרמטריםvalidation perplexity
ממוצע אחיד, נוסף279,55260.45
attention head אחד, מחליף את ה-token328,70455.47
attention head אחד, נוסף328,70446.10
ארבעה heads במקום אחד345,21643.21
בתוספת רשת feed-forward476,92839.87
בתוספת LayerNorm — ה-block המלא477,69638.07

משקלים נלמדים מנצחים משקלים אחידים ב-14 נקודות perplexity, וזה כל הטיעון של הפרק בשורה אחת. ארבעה heads קונים עוד 3 תמורת 16,512 פרמטרים נוספים. ואותו head שווה עוד 9 נקודות כשהוא נוסף ולא מחליף: attention מכניס מידע פנימה, הוא לא מחליט מהו מיקום.

עכשיו איפה הפרמטרים באמת יושבים, מה שמפתיע אנשים שראו רק את התרשים:

רוחבheadsattentionfeed-forwardסך הכול ל-block
128465,664 (33.2 %)131,712 (66.6 %)197,888
768122,360,064 (33.3 %)4,722,432 (66.6 %)7,085,568
40963267,112,960 (33.3 %)134,238,208 (66.7 %)201,367,552

שני שלישים מכל transformer block הם רשת ה-feed-forward, בכל סקאלה, כי ל-attention יש ארבע מטריצות d×dd \times d ול-MLP יש שקול של שמונה. כל מה שמודל יודע, רוב הפרמטרים שמחזיקים אותו נמצאים ב-MLP פר-מיקום.

LayerNorm נבנה ונמדד בפרק 6, והפרק הזה משתמש בו כפי שהושאר שם; חיבורי residual נקראו בשם ועברו ablation שם, ונבנים כאן. שורות ה"נוסף, לא מחליף" למעלה הן חיבורי residual, ששווים 188 נקודות perplexity עבור הממוצע ו-9 עבור head אחד. LayerNorm7 מנרמל כל דוגמה על פני התכונות שלה, ופרק 6 נתן את הסיבות לכך שהוא, ולא BatchNorm, שרד כאן — אין תלות ב-batch, אין סטטיסטיקות רצות, זהה באימון וב-inference, אדיש לאורך רצף — וכל אחת מהן הופכת לדרישה כשמייצרים token אחד בכל פעם למשתמש אחד, שזה המקום שאליו פרק 13 מגיע. הוא עולה 768 פרמטרים וקונה 1.8 נקודות perplexity.

block.pyPYTHON
class Block(nn.Module):
    def forward(self, x):
        x = x + self.att(self.ln1(x))     
        x = x + self.ff(self.ln2(x))      
        return x

הסתכלו איפה הנרמול יושב: על הקלט של כל תת-שכבה, כשנתיב ה-residual מהקלט לפלט לעולם אינו מנורמל. זה pre-norm. המאמר מ-2017 עושה את ההפך, x = LayerNorm(x + Att(x))post-norm, שמניח LayerNorm על נתיב ה-residual עצמו.

Xiong et al. הסבירו את ההבדל דרך ה-gradient באתחול, שברשת post-norm מסוקלל רע עם העומק — הסיבה שה-transformer המקורי היה צריך learning-rate warmup כדי להתאמן בכלל.8 שנים-עשר blocks, 1000 צעדים, learning rate 3×1033 \times 10^{-3}:

TEXT
gradient norm per block at initialisation, before any step
  pre-norm    block 1 0.0498 ... block 12 0.0657   ratio last/first  1.32
  post-norm   block 1 0.0977 ... block 12 0.1613   ratio last/first  1.65

  pre-norm,  no warmup          perplexity   37.82
  pre-norm,  200-step warmup    perplexity   37.62
  post-norm, no warmup          perplexity  308.05
  post-norm, 200-step warmup    perplexity   37.88

Post-norm בלי warmup גרוע פי שמונה, ו-post-norm עם warmup תואם בדיוק ל-pre-norm. Warmup אינו פרקטיקה טובה כללית כאן; הוא טלאי לסידור ספציפי של הנרמול, והזזת ה-LayerNorm מסירה את הצורך בו. זו הסיבה שכמעט כל מודל מאז 2019 הוא pre-norm, וזו הסיבה שיש לקרוא את התרשים מ-2017 כהיסטוריה ולא כמפרט.

מחקו את embeddings המיקום והמודל עדיין מתאמן; הוא פשוט לא יכול לדעת איפה משהו נמצא, וזו סימטריה ולא כשל אימון. שום דבר ב-attention score לא מזכיר את tt או ii עצמם, ולכן פרמוטציה של הקלט מפרמטת את הפלט: self-attention הוא permutation-equivariant. זו עיוורון-הסדר של הממוצע בתחפושת טובה יותר — ה-causal mask משחזר חלק מהסדר, כי כל מיקום רואה prefix אחר, אבל בתוך prefix כל הסידורים דומים.

ארבע דרכים להזריק מיקום, מאומנות על חלונות של 64 tokens ומוערכות ב-64, 128 ו-256 — מעבר לכל אורך שראו:

מיקומיםperplexity ב-64ב-128ב-256
בלי בכלל48.7952.6357.52
embeddings אבסולוטיים נלמדים38.63108.47181.94
סינוסים קבועים42.9695.26152.25
RoPE44.1250.5284.84
ALiBi44.9543.5142.49

Embeddings אבסולוטיים נלמדים — וקטור אחד לכל מיקום, נוסף ל-token — מנצחים באורך שעליו אומנו ואז נופלים מצוק, כי מיקום 100 מעולם לא היה ב-batch וה-embedding שלו עדיין הווקטור האקראי שבו התחיל. סינוסים, הבחירה המקורית, מחושבים ולא נלמדים, מסינוסים וקוסינוסים בתדרים המרווחים גיאומטרית; המאמר מ-2017 קיווה שזה יעשה אקסטרפולציה, והטבלה אומרת שלא — הפונקציה מוגדרת במיקום 200, אבל המודל מעולם לא למד לקרוא אותה שם. RoPE9 לא מוסיף כלום ובמקום זאת מסובב query ו-key בזווית פרופורציונלית למיקום, בפרוסות דו-ממדיות; מכיוון שסיבוב שני הצדדים של מכפלה סקלרית באותה מידה משאיר אותה ללא שינוי, ה-score בסוף תלוי רק ב-tit - i, כך שהמיקום הופך יחסי בחינם ואין טבלה שיכולה להיגמר. הוא מתדרדר, אבל הוא מתדרדר. ALiBi10 הוא התוצאה הפשוטה והמוזרה ביותר כאן: קנס ליניארי על ה-score פרופורציונלי למרחק, עם שיפוע שונה לכל head. ה-perplexity שלו משתפרת כשהחלון גדל מעבר לאורך האימון, מ-44.95 ל-42.49, כי הקנס מוגדר בכל מרחק וכל head ממשיך לעשות את מה שאומן לעשות.

הלקח שורד את הטבלה: ארכיטקטורה שלא יכולה לייצג משהו היא בעיה שונה מזו שמעולם לא למדה את הטווח הזה, והשנייה היא זו שנושכת. זו גם המכונה שמאחורי כל הכרזת "הרחבנו את ה-context ל-128K" — כמעט תמיד מדובר בסקיילינג מחדש של קידוד רוטרי, וזו הסיבה שפרק 16 אומר שמגבלת ה-context זזה ולא נעלמת.

Dropout עובר בירושה באותה דרך: הוא מופיע על משקלי ה-attention אחרי ה-softmax, על הפלט של כל תת-שכבה לפני תוספת ה-residual, ועל סכום ה-embedding, ועושה בדיוק את מה שפרק 6 תיאר. בריצות pretraining גדולות הוא לעיתים קרובות מוגדר לאפס, כי מודל שרואה כל token פעם אחת אינו במצב של overfit.

לשני tensors בשכבה יש צורה n×nn \times n, כאשר nn הוא מספר ה-tokens: ה-scores והמשקלים אחרי ה-softmax. כל השאר — כל projection, כל ה-MLP — ליניארי ב-nn.

שכבת attention אחת, ברוחב 512, 8 heads, batch של אחד, float32, על GPU של מחשב נייד. קראו את שתי עמודות המילישניות רק לפי היחסים שלהן: הן זמן קיר על כרטיס מחשב נייד של 8 GB שמוריד תדר מ-1,785 MHz לפחות מ-300 MHz כשהוא מתחמם, כך שריצה קרה של אותו קוד חוזרת מהר פי שבעה עד עשרה וריצה עמוסה איטית עוד יותר. עמודות המגה-בייט הן ספירות בייטים של allocator ואינן זזות.

TEXT
  tokens   ms total    ms x4   ms projections   attn matrix MB    peak MB    MB x4
     128      2.246        -            1.324              0.5       14.6        -
     256      2.855     1.27            2.113              2.0       19.2     1.31
     512      5.761     2.02            3.105              8.0       34.4     1.79
    1024     16.414     2.85            4.008             32.0       89.1     2.59
    2048     51.573     3.14            9.989            128.0      296.1     3.32
    4096    225.432     4.37           20.176            512.0     1100.1     3.72
    8192    832.838     3.69           40.106           2048.0     4300.1     3.91
   16384   OUT OF MEMORY                                 8192.0

fitted exponent (log-log slope, last four rows):  time ~ n^1.91   memory ~ n^1.87

עמודות x4 הן היחס לשורה שמעל, והכפלה של nn מתכנסת בדיוק ל-4 גם בזמן וגם בזיכרון — 3.91 בצעד האחרון מול 4 תיאורטי. עמודת ה-projections היא הבקרה: 4.0 ms ב-1024 tokens עד 40.1 ms ב-8192, פקטור של עשר עבור פקטור של שמונה. ליניארי, כפי שהובטח.

ואז השורה האחרונה. שכבת attention אחת, רצף אחד, בלי מודל סביבה, נגמר לה הזיכרון על GPU של 8 GB ב-16,384 tokens — מטריצת ה-score לבדה תהיה 8 GB, בהיותה 8 heads כפול 16,384 כפול 16,384 כפול 4 בייטים. לא המודל; tensor ביניים אחד בשכבה אחת.

זו העובדה הפיזית שמתחת לשלושה פרקים מאוחרים יותר. זו הסיבה של-context window יש בכלל מגבלה, שפרק 16 הופך למחיר. זו הסיבה ש-FlashAttention קיים, מחשב את אותה תוצאה באריחים בלי לאחסן את המטריצה לעולם — אופטימיזציית זיכרון לפני שהיא אופטימיזציית מהירות.11 וזה החשבון שמאחורי המחיר של prompt ארוך, שפרק 24 משלם בלולאת agent — עניין נפרד מהממצא האחר של אותו פרק, שמודל גם משתמש ב-context ארוך גרוע יותר, שאותו הוא מודד ומסרב להאשים בנוסחה הזאת.

הצגת פרטים

שתי הגרסאות שמכווצות cache, נקראות כאן ומשולמות בפרק 13.

Generation שומר ב-cache את ה-keys וה-values של ה-tokens שכבר עובדו — key אחד ו-value אחד לכל token, לכל head לכל שכבה. Multi-query attention12 שומר hh query projections אבל projection יחיד של key ו-value שמשותף לכל ה-heads, ומחלק את ה-cache הזה ב-hh. Grouped-query attention13 עושה אינטרפולציה: heads מקובצים, כל קבוצה חולקת key ו-value אחד, כך ש-g=hg = h הוא attention רגיל ו-g=1g = 1 הוא multi-query. כמעט כל מודל פתוח מאז 2023 משתמש בו עם 4 או 8 קבוצות. אף אחד מהם לא קיים בשביל איכות; שניהם קיימים בשביל גודל ה-cache הזה, ופרק 13 עושה את החשבון שהופך אותו ל"איזה מודל נכנס ל-GPU שלכם".

המאמר מ-2017 מתאר encoder-decoder: stack אחד שקורא את המקור עם attention לא ממוסך, שני שמייצר את היעד בצורה causal, וסוג שלישי של attention באמצע שבו ה-queries של ה-decoder פוגשים את ה-keys של ה-encoder. זה נכון לתרגום, שבו קלט ופלט הם שני רצפים.

מה שניצח היה חצי ה-decoder-only — stack אחד, causal לכל אורכו, קלט ופלט באותו רצף — והסיבה אינה אלגנטיות. "לחזות את ה-token הבא" רץ על כל טקסט, ולכן סט האימון הוא האינטרנט ולא קורפוס מקביל, והכול הופך למשימה האחת הזאת: תרגום הוא מסמך שמכיל מקור ואז יעד, שאלה ותשובתה הן מסמך, שיחה עם tool call באמצע היא מסמך. פרק 11 עוסק באופן שבו האחרון מיוצר. Encoders לא נעלמו — אחד רואה את כל הקלט בבת אחת, וזה מה שרוצים כשהמשימה היא לייצג טקסט ולא להמשיך אותו, וזו הסיבה שה-retrieval embeddings של פרק 19 מגיעים מ-encoders ולא מהמודל שמנהל את הצ'אט.

לאחר שה-block מוגדר, גודל המודל הוא חשבון. לכל block, עם רוחב dd והרחבה פי ארבע: 4d2+4d4d^2 + 4d עבור Wq,Wk,Wv,WoW_q, W_k, W_v, W_o עם biases על כל הארבעה, כפי שיש ל-GPT-2 — הטבלה למעלה משאירה את ה-bias מחוץ לשלושה מהם, ולכן 2,304 פחות לכל block ב-d=768d = 768; 8d2+5d8d^2 + 5d עבור ה-MLP; 4d4d עבור שני LayerNorms — 12d2+13d12d^2 + 13d, בתוספת טבלת token של V×dV \times d, ועבור מיקומים אבסולוטיים, nctx×dn_{\text{ctx}} \times d. עבור הצורה של GPT-2 small — d=768d = 768, 12 blocks, אוצר מילים של 50,257, context של 1024, שכבת הפלט חולקת את משקלי ה-embedding:

TEXT
  token embeddings     50,257 x 768 = 38,597,376
  position embeddings   1,024 x 768 =    786,432
  one block                             7,087,872
  12 blocks                            85,054,464
  final LayerNorm         2 x 768 =        1,536
  total (weights tied)                124,439,808

וזה הגודל שפורסם למודל הזה. הנוסחה אינה קירוב; היא המודל. שימו לב גם שכמעט שליש ממודל קטן הוא טבלת ה-embedding, ולכן גודל אוצר המילים הוא החלטה ארכיטקטונית ולא החלטת preprocessing — ה-trade-off שפרק 7 הציב.

Perplexity היא מספר על קורפוס. מה ש-head אחד עושה זו שאלה אחרת, ומודל שאומן על מגה-בייט של שייקספיר הוא הכלי הלא נכון בשבילה: הדבר ההוגן לומר על מפת ה-attention של מודל בן 500,000 פרמטרים הוא שהיא לרוב אינה ניתנת לפירוש. לכן: שפה שבה לשאלה יש תשובה נכונה.

האיור הקלאסי הוא the animal did not cross the street because it was too tired, שבו it הוא החיה, מול …because it was too wet, שבו מילה אחת מזיזה את הרפרנט לרחוב. אלה Winograd schemas14 — זוגות משפטים זהים חוץ ממילה אחת, שבה המילה הזו קובעת למה כינוי גוף מתייחס.

הם גם ניתנים לפתרון ברמאות, וזה החלק שהמדריכים מדלגים עליו. אם שני המועמדים הם חיה ומקום, tired ו-wet מזהים את הרפרנט לפי קטגוריה, ומודל שיודע רק אילו מילים נוכחות מצליח בלי לדעת דבר על סדר. נמדד על הגרסה הזאת של המשימה, עם זוגות חיה/מקום מוחזקים בצד:

TEXT
uniform causal average           held-out referent accuracy 100.0 %
one transformer block            held-out referent accuracy  91.7 %

שק המילים מנצח את ה-transformer. כל הדגמה שבנויה על המשפט הזה לא מוכיחה דבר על attention.

אז סוגרים את החור: שולפים את שני המועמדים ממאגר אחד של שישה-עשר שמות עצם, שכל אחד מהם יכול להופיע בכל אחת משתי המשבצות, ומפצלים את שמות התואר לפי תפקיד במקום לפי קטגוריה — ארבעה שהופכים את it לחוצה (tired, scared, slow, weak), ארבעה שהופכים אותו לנחצה (wet, wide, busy, steep).

TEXT
the {x} did not cross the {y} because it was too {adj} , so the {ref} waited .

אמנו כחיזוי רגיל של token הבא, תנו score למיקום אחד — המילה אחרי so the — ובנו את הסט המוחזק בצד מזוגות שמות עצם שסדרם ההפוך היה באימון, כך שכל דבר שיודע אילו שני שמות עצם נוכחים אבל לא מי בא ראשון חייב לענות הפוך.

מודלפרמטריםמוחזק בצדנותן שם לעצם האחר
ה-token הנוכחי בלבד5,7965.2 %5.2 %
ממוצע causal אחיד5,79627.9 %50.0 %
head אחד של attention נלמד18,08435.4 %64.6 %
ארבעה heads22,24475.0 %15.6 %
transformer block אחד55,71692.7 %4.2 %
שני transformer blocks105,508100.0 %0.0 %

ניחוש בין שני שמות העצם הנוכחים הוא 50 %. הממוצע האחיד נוחת על 27.9 % ועונה עם שם העצם הלא נכון בזוג בדיוק חצי מהזמן — החתימה של משהו שיודע אילו מילים שם ולא דבר על הסדר שלהן, כפי שמבחן הערבוב חזה שלושה סעיפים קודם.

עכשיו המפה: ה-attention במיקום שצריך לתת שם לרפרנט, ממוצע על פני ארבעת ה-heads של כל block, עבור שני המשפטים שנבדלים במילה אחת. ממוצע אחיד היה שם 0.067 על כל אחד מחמישה-עשר ה-tokens הנראים.

TEXT
the animal did not cross the street because it was too tired , so the animal waited .
  blk 1  the:0.00 animal:0.70 did:0.00 not:0.00 cross:0.00 the:0.00 street:0.06
         because:0.00 it:0.00 was:0.00 too:0.00 tired:0.00 ,:0.05 so:0.00 the:0.19
  blk 2  the:0.00 animal:0.00 did:0.00 not:0.00 cross:0.00 the:0.00 street:0.00
         because:0.00 it:0.00 was:0.00 too:0.00 tired:1.00 ,:0.00 so:0.00 the:0.00

the animal did not cross the street because it was too wet , so the street waited .
  blk 1  the:0.00 animal:0.70 did:0.00 not:0.00 cross:0.00 the:0.00 street:0.06
         because:0.00 it:0.00 was:0.00 too:0.00   wet:0.00 ,:0.05 so:0.00 the:0.19
  blk 2  the:0.00 animal:0.00 did:0.00 not:0.00 cross:0.03 the:0.00 street:0.49
         because:0.00 it:0.00 was:0.00 too:0.20   wet:0.03 ,:0.00 so:0.00 the:0.25

Block 1 זהה בשני המשפטים — 0.70 על שם העצם הראשון, בלי קשר לשם התואר. זה לא כישלון אלא הוכחה: בשכבה הראשונה ה-query במיקום הוא פונקציה של ה-token והאינדקס של אותו מיקום עצמו, ו-the במיקום 14 הוא אותו token בשני המשפטים. head בשכבה ראשונה לא יכול להתנות במילה שעדיין לא שלף. לכן block 1 עושה את הדבר המועיל היחיד שזמין לו וגורר את שם העצם הראשון קדימה.

Block 2 הוא המקום שבו המשפטים מתפצלים, ואותה שורה על פני כל שמונת שמות התואר מראה את הכלל שהמודל מצא:

שם תוארblock 2 על animalעל streetעל שם התוארתשובה
tired, scared, slow, weak0.0000.0001.000animal
wet, wide, busy, steep0.0000.4910.00–0.03street

עבור שם תואר של חוצה, ה-block השני מבלה את כל משקלו על שם התואר, כי התשובה כבר נמצאת ב-residual stream — block 1 שם אותה שם — וכל מה שהוא צריך הוא אישור. עבור שם תואר של נחצה הוא הולך ושולף את שם העצם האחר במקום. זה מעגל דו-קפיצי: head אחד מזיז מועמד קדימה, head בשכבה מאוחרת יותר קורא token שמחליט אם לשמור אותו. קומפוזיציה על פני שכבות היא המנגנון, וזו הסיבה ש-block אחד הגיע ל-92.7 % ושניים הגיעו ל-100 %.

זו גם הצורה של המעגל המתועד ביותר במודלים אמיתיים. Induction heads — head של token קודם שמזין head בשכבה הבאה שמשלים את התבנית [A][B] … [A] → [B] — הם מה שעבודת הפרשנות של Anthropic מזהה מאחורי חלק גדול מ-in-context learning, והם נוצרים ברגע ניתן לזיהוי במהלך pretraining. הפרק הזה לא מנסה את הניתוח הזה: הוא מואצל, עם שני המאמרים במקורות, כי קריאת מעגלים מתוך מודל אמיתי היא תחום מחקר ולא סעיף.

ולבסוף, המימוש. שלושים השורות למעלה, עם משקליהן מועתקים מ-PyTorch עצמו:

TEXT
ours vs nn.MultiheadAttention           max |diff| = 1.7881393432617188e-07
ours vs F.scaled_dot_product_attention  max |diff| = 1.7881393432617188e-07

1.8×1071.8 \times 10^{-7} על פלטים שגודל הממוצע שלהם הוא 0.159: אותו חשבון בסדר אחר, בדיוק float32.

יש לכם את הארכיטקטורה שכל מודל בשאר הקורס בנוי ממנה, והיא קטנה מהמוניטין שלה: ממוצע משוקלל שמשקליו נלמדים, MLP פר-מיקום שמחזיק שני שלישים מהפרמטרים, שני נרמולים ושתי תוספות, בערימה.

מה שאין לכם הוא מודל שיודע משהו, ו-stacking לא יתקן זאת בפני עצמו. שני blocks על הקורפוס הזה מגיעים ל-training perplexity של 14.49 ול-validation perplexity של 40.57, מול 18.77 ו-38.07 של block אחד — יותר קיבולת, טוב יותר על מה שראה, גרוע יותר על מה שלא ראה, הטבלה של פרק 6 עם transformer בתוכה. המרחק בין המודל הזה לבין אלה שפרקים 14 עד 30 מדברים איתם אינו ארכיטקטוני. זה אותו block, חוזר יותר פעמים, על הרבה יותר טקסט.

מה שהופך את זה לבעיית חשבונאות, והחשבונאות מוזרה מכפי שהיא נראית. כמה טקסט, ומאיפה מישהו משיג אותו? כמה חישוב, ואיך מעריכים אותו לפני שהכסף הוצא? בהינתן תקציב קבוע, עדיף להגדיל את המודל או להראות לו יותר נתונים — והאם יש תשובה נכונה, או רק אופנה? פרק 10 עונה על שלוש השאלות במדידה, ומצמיד מחיר לצורה השימושית הזולה ביותר של השאלה: כמה עולה, היום, לאמן מודל כמו GPT-2 מאפס?


שלושה הסברים של החומר הזה טובים מזה במה שהם מיועדים לו, והפרק הזה נכתב כדי להיקרא לצידם. The Illustrated Transformer של Jay Alammar הוא התמונה הטובה ביותר של זרימת הנתונים שצוירה אי פעם. The Annotated Transformer של Harvard NLP הוא המאמר מ-2017 עם קוד רץ משולב שורה אחר שורה. Let's build GPT: from scratch, in code, spelled out של Andrej Karpathy בונה את אותו מודל בשידור חי בשעתיים, וסולם ה-ablations למעלה הוא אותו שלד שנמדד על קורפוס אחר. לשאלת הפרשנות שהפרק הזה רק נוגע בה, המקורות הראשוניים הם Elhage et al., A Mathematical Framework for Transformer Circuits (2021) ו-Olsson et al., In-context Learning and Induction Heads (2022), שניהם מקבוצת הפרשנות של Anthropic.

  1. Hochreiter, S. and Schmidhuber, J. Long Short-Term Memory. Neural Computation 9(8), pp. 1735–1780 (1997).

  2. Sutskever, I., Vinyals, O. and Le, Q. V. Sequence to Sequence Learning with Neural Networks. arXiv:1409.3215 (2014). ה-encoder-decoder שבו וקטור context יחיד הוא צוואר הבקבוק.

  3. Bahdanau, D., Cho, K. and Bengio, Y. Neural Machine Translation by Jointly Learning to Align and Translate. arXiv:1409.0473 (2014). Attention, שלוש שנים לפני ה-transformer.

  4. Perplexity היא האקספוננט של ה-cross-entropy הממוצע לכל token, מפרק 8. כל מספר כאן משתמש באותו tokenizer ובאותו validation split, וזה התנאי היחיד שבו בכלל מותר להשוות שתי perplexities.

  5. Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., Kaiser, Ł. and Polosukhin, I. Attention Is All You Need. arXiv:1706.03762 (2017). סעיף 3.2.1 הוא המשפט האחד על dk\sqrt{d_k} שהפרק הזה מקדיש סעיף למדידתו.

  6. Shazeer, N., Mirhoseini, A., Maziarz, K., Davis, A., Le, Q., Hinton, G. and Dean, J. Outrageously Large Neural Networks: The Sparsely-Gated Mixture-of-Experts Layer. arXiv:1701.06538 (2017).

  7. Ba, J. L., Kiros, J. R. and Hinton, G. E. Layer Normalization. arXiv:1607.06450 (2016). הוצג ונמדד בפרק 6; משמש כאן ללא שינוי.

  8. Xiong, R., Yang, Y., He, D., Zheng, K., Zheng, S., Xing, C., Zhang, H., Lan, Y., Wang, L. and Liu, T.-Y. On Layer Normalization in the Transformer Architecture. arXiv:2002.04745 (2020). ניתוח ה-gradient שמאחורי pre-norm, והטיעון ש-warmup הוא תסמין.

  9. Su, J., Lu, Y., Pan, S., Murtadha, A., Wen, B. and Liu, Y. RoFormer: Enhanced Transformer with Rotary Position Embedding. arXiv:2104.09864 (2021).

  10. Press, O., Smith, N. A. and Lewis, M. Train Short, Test Long: Attention with Linear Biases Enables Input Length Extrapolation. arXiv:2108.12409 (2021). תוצאת האקסטרפולציה ששוחזרה למעלה.

  11. Dao, T., Fu, D. Y., Ermon, S., Rudra, A. and Ré, C. FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness. arXiv:2205.14135 (2022).

  12. Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019).

  13. Ainslie, J., Lee-Thorp, J., de Jong, M., Zemlyanskiy, Y., Lebrón, F. and Sanghai, S. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023).

  14. Levesque, H. J., Davis, E. and Morgenstern, L. The Winograd Schema Challenge. KR (2012). הבנייה שמאחורי משפט ה-animal / street שכל מדריך attention משתמש בו.


נוצר על ידי

David Vicente Campos

מייסד NeuraLIA Labs ושותף-מייסד MyRealFood

אני מהנדס מחשבים, בוגר אוניברסיטת לאון. הייתי שותף בהקמת MyRealFood, שם, כסמנכ״ל טכנולוגיות, בניתי את האפליקציה שמיליוני אנשים השתמשו בה כדי לאכול בריא יותר, והקמתי את NeuraLIA Labs, שם אני בונה מוצרי בינה מלאכותית. כאן אני כותב על מה שהייתי צריך להבין לאורך הדרך, כפי שהייתי רוצה שמישהו היה מסביר לי בזמנו.

עוד על המחבר

פורסם על ידי NeuraLIA Labs.

פוסטים חדשים ישירות לתיבת הדואר

חדשות AI, מדריכים ועדכוני מוצר — מייל קצר כשאנחנו מפרסמים משהו ששווה את הזמן שלך.

תוכן הקורס

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev10 דקות קריאה

מודל ה-AI Jev נבנה להחלטות, לא לפרוזה

Jev של TypeSafe AI מושך תשומת לב כי הוא מתייחס לאינטליגנציית תוכנה כאל בעיית הסתברות: לבחור את ההסתעפות הנכונה, להצמיד ביטחון, ולהימנע מתשלום ל-LLM כדי שיכתוב טקסט כשהקוד צריך החלטה.

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering10 דקות קריאה

הנדסת הקשר לסוכני AI ארוכי־טווח

סוכנים שרצים לאורך זמן לא נכשלים רק כי החלון קטן. הם נכשלים כשקבצים, פלטי כלים והיסטוריה מיושנת דוחקים החוצה את המשימה שהסוכן היה אמור להשלים.

מוכנים לתת ל-LIA לבחור?

בנו עם כל מודלי ה-AI במקום אחד — התחילו בחינם עוד היום.