אז מה הקטע עם מודלי שפה גדולים?

מבוא

ההתפתחות המהותית ביותר במדעי המחשב בשנים האחרונות ואולי אפילו בעשורים האחרונים, היא הזינוק המטאורי שעבר תחום הבינה המלאכותית בעשור האחרון. זה אף פעם לא היה התחום שלי, אז כמו רוב האנושות ראיתי את הזינוק הזה במבט מהצד, כשהרגע הראשון שבו הבנתי שקורה פה משהו גדול ממש היה ב-1 בדצמבר 2022, כשניסיתי לבדוק את ChatGPT שהפך זמין לציבור. מנועי צ'אט שמבוססים על בינה מלאכותית לא היו המצאה חדשה, וכמה חודשים לפני כן כבר ראינו קסם AI אחר של ייצור תמונות, אבל כמה שעות עם ChatGPT שכנעו אותי שיש פה משהו חדש לגמרי, שמבחן טיורינג הפך לבלתי רלוונטי, ושלכו תדעו איפה זה ייגמר. גם כיום עדיין ממש לא ברור מה הגבול שאליו נוכל להגיע, ובנוסף להתלהבות מההישג הטכני המרהיב ומהדרכים השונות שבהן ההתקדמות הזו יכולה לשפר לנו את החיים אני מצטרף גם לחששות המתבקשים מפני שלל ההשלכות השליליות האפשריות של אותה התקדמות בדיוק.

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

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

מה זה LLM?

כשמדברים על AI בימינו מושג המפתח פה הוא "מודל שפה גדול", Large Language Model, ובקיצור LLM. מודלי שפה גדולים הם בינות מלאכותיות שנוצרות על ידי אימון על כמות אסטרונומית של מידע - תחשבו על כל מה שזמין פומבית באינטרנט וגם הרבה תוכן גנוב או מוגן בזכויות יוצרים. התוצאה של תהליך האימון הזה היא מה שמכונה מודל, שהוא... ובכן... בגדול הוא אוסף של מספרים. המון המון מספרים. למספרים הללו קוראים פרמטרים והם ה"מוח" של המודל, אבל הם עצמם לא מהווים אלגוריתם במובן המקובל. אף אחד לא ישב וכתב לבינה המלאכותית סדרת הוראות בסגנון "אם את רואה כלב, תעשי ככה ואם דיברו אלייך בנימוס תעשי ככה". האלגוריתם שאיתו עובדים מודלי שפה גדולים הוא פשוט למדי - ולמעשה, הוא חייב להיות פשוט למדי כי כל הרעיון בו הוא שיהיה קל להריץ אותו. עיקר החוכמה של מה שהאלגוריתם עושה מגיעה מהאופן שבו כיווננו את הפרמטרים.

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

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

אם מנסים לשים את האצבע על אירוע מחולל ספציפי אחד שאחראי לפריצת הדרך הגדולה שסללה את הדרך למודלים שמתבססים על הדברים הללו, האירוע הזה הוא פרסום המאמר Attention Is All You Need ב-2017 על ידי שמונה חוקרים (אשיש ואסואני, נועם שזיר, ניקי פרמר, ג'אקוב אוסקורייט, ליון ג'ונס, איידן גומז, לוקאש קייזר ואיליה פולוסוחין). בגדול, המאמר אמר את הדבר הבא, שכרגע ישמע כמו אוסף אקראי של תווים: עד כה המודלים המתקדמים ביותר בבינה מלאכותית התבססו על שיטה שנקראת RNN. השיטה הזו בתורה נעזרה בשלל דברים מתוחכמים שאחד מהם הוא מנגנון שנקרא Attention. המאמר מציע לוותר על המבנה המתוחכם של RNN לטובת מבנה פשוט הרבה יותר, שזוכה במאמר לשם "טרנספורמר" (Transformer) שהדבר העיקרי שנותן לו את כוחו הוא מנגנון ה-Attention המדובר. המנגנון הזה מצד אחד ניתן למימוש יעיל, ומצד שני הוא מנגנון חכם וחזק שמאפשר לקבל תוצאות מצוינות. הגישה החדשה הזו היא מה שאפשר אימונים של מודלי שפה מסדרי הגודל שאנו מכירים, ולכן את ההתפוצצות שראינו.

אז אני יכול לנסות לקרוא את המאמר של Attention Is All You ולהסביר מה הולך בו, אבל זה קצת כמו להיכנס לסרט באמצע בדיוק כשמגיע הטוויסט. כל מה שרואים זה מישהו פתאום מוריד מסיכה ואת כל שאר האנשים מסביב בוהים בו נדהמים, אבל למי שנכנס כרגע אין מושג מי בעצם האיש שמתחת למסיכה ולמה זה כל כך מעניין. אז בואו ננסה להציג את הנושא בצורה הבאה:

  1. נבין מה בעצם מודלי שפה עושים.

  2. נבין מאוד בגדול איך RNN ודומיו עבדו.

  3. נבין מה זה מנגנון ה-Attention המדובר ואיך זה עובד בטרנספורמר.

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

מה בעצם LLM-ים עושים?

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

עכשיו, אם נזין למודל את המשפט "חיית המחמד הטובה ביותר היא", מה בעצם התשובה ה"נכונה" פה? האם אנחנו מצפים שהוא יוציא את המילה "כלב"? או "חתול"? או משהו אחר? אולי בכלל אנחנו מצפים למילה "אין" כחלק מההמשך "אין מושג אובייקטיבי של חיית המחמד הטובה ביותר"? כל אלו הם אפשריים. אז צריך להבין שמודל לא מוציא את ההמשך ה"נכון". אין לו מושג כזה, של "נכון". מה שהמודל מוציא הוא דירוג כלשהו של מילים, ועם הדירוג הזה אפשר לעשות כל מני דברים. אפשר לקחת את המילה שמדורגת הכי גבוה, אבל אפשר גם להסתכל על עשרים המילים הגבוהות ביותר, או אפילו כל המילים האפשריות ולהשתמש בדירוג כדי להגדיר עליהן התפלגות - לכל מילה תהיה הסתברות כלשהי, ופשוט ההסתברות של "כלב" ושל "חתול" תהיה כנראה גבוהה יותר משל "פטרודקטיל" או של "מלבן". יותר מכך - כשמאמנים מודל שמיועד לצ'וטט עם אנשים, האימון הזה יצטרך לגרום למודל להתנהג כמו צ'אט (נניח, להגיד "אין מושג אובייקטיבי") ולא סתם לפעול בצורה שרוב הטקסטים שהוא קרא מכתיבים (להגיד "כלב" או "חתול" בלי פקפוק). כדי לגרום לצ'אט להתנהג כמו צ'אט אפשר לקחת מודל קיים ואז לאמן אותו עוד קצת באופן מיוחד - לדבר כזה קוראים Fine Tuning ואני אזכיר את זה בהמשך; כרגע אני מציין את זה רק כדי שנבין למה מה שנשמע כמו תיאור פשטני מדי של מודל, שלא לגמרי תואם את הנסיון האישי שאולי יש לנו, בעצם מספיק כדי להבין את עיקר "מה הולך פה".

אוקיי, אז הקלט למודל שפה הוא סדרה של מילים. רק שלא בדיוק, המודל לא עובד עם מילים. יש המון מילים, יותר מדי; ויש המון הטיות למילים; ויש המון ג'יברישים שאנשים יכולים להקליד למרות שהם לא מילים והמודל עדיין צריך להתמודד איתם. אז אחרי שנותנים למודל טקסט, לפני שהמודל מתחיל לעשות עליו את הקסמים שלו יש שלב של עיבוד. צריך לפרק את הטקסט שהמודל קיבל לחלקים נפרדים שכל אחד מהם נקרא טוקן (Token, "אסימון"? נעזוב את זה). מודל מגיע יחד עם מילון שמגדיר את כל הטוקנים שהוא מכיר; והמילון בנוי בצורה כזו שבה אפשר לפרק כל מחרוזת לסדרה של טוקנים, אפילו אם היא ג'יבריש כמו "דנחדא" שכנראה פשוט יתפרק לסדרת הטוקנים "ד", "נ", "ח", "ד", "א" (בהמשך כשאתאר עם מודל קונקרטי יהיה יותר ברור איך כל המנגנון הזה עובד).

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

עכשיו, מחרוזת של כמה תווים זה נחמד, אבל כשאנחנו באים לבצע חישובים, לא נוח לבצע אותם עם מחרוזות. הנסיון שלנו משלל תחומים מספר לנו שמאוד מועיל למדל מידע בעזרת מספרים, ולרוב לא באמצעות מספר בודד אלא באמצעות סדרה של כמה מספרים שמתייחסים אליה בתור אובייקט אחד. השם במתמטית ליצור כזה הוא וקטור. אם יש לי וקטור שהכניסות שלו הן מספרים ממשיים ויש בדיוק \(n\) כניסות, נהוג לכתוב אותו בתור \(x=\left(x_{1},\ldots,x_{n}\right)\) ולסמן \(x\in\mathbb{R}^{n}\), כלומר \(\mathbb{R}^{n}\) הוא הסימון לקבוצת "כל הוקטורים האפשריים שהכניסות שלהם הן מספרים ממשיים ויש \(n\) כאלו". הצעד שהמודל מבצע אחרי חלוקת הטקסט המקורי לטוקנים הוא להחליף כל טוקן בוקטור שכזה שמייצג אותו - חלק מהמודל מתאר בדיוק את אוסף הוקטורים הזה.

למה לייצג מילים עם וקטורים עדיף על פשוט לייצג אותם בתור, אה, מילים? כי זה נותן יותר גמישות לניואנסים, ומאפשר להתמקד על המשמעות של המילים במקום הצורה שלהן. למשל, בואו נסתכל על "אינטרנט" ו"מרשתת". אלו שתי מילים נרדפות, בעלות אותה משמעות, אבל הן לא נראות דומות בכלל. מצד שני, נניח שקודדנו את "אינטרנט" בעזרת הוקטור \(\left(1,7,3.14,42\right)\) ועכשיו אנחנו מסתכלים ורואים ש"מרשתת" מקודד באמצעות \(\left(0.97,7,3.2,41\right)\) - אנחנו רואים שזה וקטור קרוב אך לא זהה. כלומר, יש לנו אפשרות ליצור מעין כימות להבדל בין הוקטורים הללו. ולמה בעצם שיהיה הבדל אם אלו מילים נרדפות? ובכן, "אינטרנט" זו המילה היומיומית ו"מרשתת" זה משהו שלכו תדעו מי ומתי משתמש בו - אנחנו מצפים למצוא אותה יותר כשלטקסט יש אופי הומוריסטי, או פלצני, או רשמי. עכשיו, זה כמובן פשטני מדי להגיד את זה אבל אפשר לדמיין שהכניסה השלישית בוקטור אומרת מה "רמת הרשמיות" של המילה שמופיעה בו. כשיש \(3.2\) המילה יותר רשמית מאשר \(3.14\) . זו אינטואיציה טובה לכוח שנותנים לנו וקטורים, אבל מה שצריך להבין בנוגע לכל העסק הוא שקידוד מילים לא עובד בצורה מסודרת שכזו. זה לא שלכל כניסה בוקטור יש "משמעות" ואנחנו מסווגים את המילים על פי האופי שלהן בהקשרים השונים. אנחנו לא עושים שום דבר מזה. אנחנו פשוט אומרים למודל - תקשיב, מילים הולכות להיות מקודדות על ידי וקטורים עם 10 כניסות, יש לך 10 ממדים לשחק איתם ואת כל המספרים הממשיים לרשותך, תתפרע. הבחירה איך לקודד מה יכולה להיות חלק מתהליך האימון של המודל (או להתקבל מתוך תהליך אימון נפרד).

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

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

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

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

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

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

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

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

מאיפה הגיע ה-MLP?

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

הנה בעיה בסיסית: יש לנו פונקציה ממשית \(f:\mathbb{R}\to\mathbb{R}\) שלא ידועה לנו, אבל אנחנו יודעים את הערכים שלה בכל מני נקודות: יש לנו זוגות \(\left(x,y\right)\) כך ש-\(y=f\left(x\right)\) . נניח שיש לנו \(n\) זוגות כאלו, \(\left(x_{1},y_{1}\right),\ldots,\left(x_{n},y_{n}\right)\) . מה עושים? לא מזמן דיברתי בבלוג על משהו שנקרא אינטרפולציה פולינומית. הרעיון שם הוא למצוא פונקציה פשוטה במיוחד \(p\) כך ש-\(p\left(x_{i}\right)=y_{i}\) לכל הנקודות שיש לנו - הפונקציה הזו נקראת פולינום והפשטות שלה מתבטאת בכך שכל מה שאנחנו עושים הוא לקחת את \(x\), לכפול אותו בעצמו, לכפול את התוצאה בקבוע כלשהו, ולחבר כמה דברים שמתקבלים בצורה הזו. העניין הוא שזה לא תמיד עובד טוב בסיטואציות ריאליסטיות שבהן הנקודות \(\left(x_{i},y_{i}\right)\) הן לא בהכרח מדויקת - אולי הפונקציה \(f\) הזו היא "רועשת" ובנסיון להתאים את עצמנו לנקודות הספציפיות שלה אנחנו דווקא נפספס את מה שהיא עושה במקרים האחרים, שאנחנו עדיין לא מכירים (זו בעיה נפוצה מאוד במודלים באופן כללי שנקראת overfitting).

אז אפשר לקחת צעד אחד אחורה - בואו ננסה למצוא פונקציה פשוטה במיוחד \(p\) שלאו דווקא מקיימת \(p\left(x_{i}\right)=y_{i}\) עבור אף אחד מהזוגות שלנו, אבל גודל השגיאה שלה הוא הקטן ביותר ביחס לכל הפונקציות הפשוטות הרלוונטיות. פונקציה פשוטה במיוחד שכזו היא \(p\left(x\right)=w\cdot x\) כאשר \(w\) הוא מספר ממשי כלשהו - זו פונקציה שהיא בסך הכל קו ישר שעובר דרך ראשית הצירים. בהינתן \(p\) כזו בואו נסמן \(\tilde{y_{i}}=p\left(x_{i}\right)\), כלומר \(\tilde{y_{i}}\) הוא הערך החזוי שהפונקציה שלנו נותנת. עכשיו, יש כל מני דרכים להעריך את גודל השגיאה; אחת מקובלת מאוד היא להסתכל על \(\left(y_{i}-\tilde{y_{i}}\right)^{2}\) - ההעלאה בריבוע גם מבטיחה שנקבל מספר אי שלילי, וגם מאפשרת לתת משקל גדול יותר לשגיאות גדולות יותר. עכשיו אפשר לחשב את השגיאה הכוללת של המודל שלנו בעזרת ממוצע פשוט, \(\frac{1}{n}\sum^{n}_{i=1}\left(y_{i}-\tilde{y_{i}}\right)^{2}\) .

נניח שאנחנו רוצים לדעת מה ה-\(w\) האופטימלי, מה עושים? אנחנו רוצים לפתור בעיית מינימיזציה: נגדיר פונקציה עבור גודל השגיאה עבור \(w\),

\(E\left(w\right)=\frac{1}{n}\sum^{n}_{i=1}\left(y_{i}-\tilde{y_{i}}\right)^{2}=\frac{1}{n}\sum^{n}_{i=1}\left(y_{i}-w\cdot x_{i}\right)^{2}\) אנחנו רוצים למצוא את \(w\) שעבורו השגיאה מינימלית; יש לנו כלי חזק בשביל בעיות כאלו, חשבון דיפרנציאלי ואינטגרלי. יש לי פוסט שמסביר את הרעיון, אז כאן אפשר לגשת ישר לעניין - אנחנו גוזרים את \(E\) ומקבלים

\(E^{\prime}\left(w\right)=\frac{1}{n}\sum^{n}_{i=1}-2x_{i}\left(y_{i}-w\cdot x_{i}\right)\)

אנחנו רוצים למצוא את \(w\) שעבורו \(E^{\prime}\left(w\right)=0\), כלומר

\(\frac{1}{n}\sum^{n}_{i=1}-2x_{i}\left(y_{i}-w\cdot x_{i}\right)=0\)

אפשר לחלק ב-\(-\frac{2}{n}\) ששונה מאפס, ואז נישאר עם

\(\sum^{n}_{i=1}\left(y_{i}x_{i}-w\cdot x^{2}_{i}\right)=0\)

כלומר

\(\sum^{n}_{i=1}y_{i}x_{i}=\sum^{n}_{i=1}w\cdot x^{2}_{i}=w\left(\sum^{n}_{i=1}x^{2}_{i}\right)\)

ולכן

\(w=\frac{\sum^{n}_{i=1}y_{i}x_{i}}{\sum^{n}_{i=1}x^{2}_{i}}\)

וזה הפתרון שלנו! איזה יופי! קסם! הכל פתור!

כמובן, בעולם האמיתי העניינים לא יהיו פשוטים כל כך, לכן התחלתי עם מה שקל לנו לפתור במדויק עד הסוף. אבל מהר מאוד זה מסתבך. למשל, מה אם \(f\left(x\right)=x+3\) ? במקרה הזה פונקציה לינארית תעשה עבודה רעה מאוד למרות ש-\(f\) נראית בעצמה כמו פונקציה לינארית - קו ישר אלכסוני, פשוט לא כזה שעובר דרך ראשית הצירים (פונקציה כזו נקראת פונקציה אפינית ). אז נרצה שהמודל שלנו יהיה \(p\left(x\right)=wx+b\) ונחפש גם את \(w\) וגם את \(b\) שמביאים את השגיאה למינימום, והופס - יש לנו בעיית אופטימיזציה מרובת משתנים. גם את זה אנחנו יודעים, אבל זה כבר מערב נגזרות חלקיות וטכניקות יותר מסובכות ולא אכנס לזה כאן. אבל גם את זה אנחנו יודעים לפתור עד הסוף.

מה אם \(f\) עוד יותר מסובכת, ופונקציה לינארית לא מתארת אותה טוב? אפשר כאמור לנסות פולינומים, אבל אפשר גם לעשות משהו אחר - להגיד "אוקיי, דווקא היה כיף הקטע הזה עם פונקציות לינאריות, אולי פשוט נעשה הרבה פונקציות לינאריות?" אפשר למשל לחשוב על זה שנמצא שתי פונקציות לינאריות, \(p_{1}\left(x\right)=w_{1}x+b_{1}\) ו-\(p_{2}\left(x\right)=w_{2}x+b_{2}\) ואז נרכיב אותן, כלומר נגדיר \(p\left(x\right)=p_{2}\left(p_{1}\left(x\right)\right)\) . העניין הוא שחישוב פשוט יראה לנו שהתוצאה היא בעצמה פונקציה לינארית:

\(p_{2}\left(p_{1}\left(x\right)\right)=p_{2}\left(w_{1}x+b_{1}\right)=\)

\(=w_{2}\left(w_{1}x+b_{1}\right)+b_{2}=w_{2}w_{1}x+\left(w_{2}b_{1}+b_{2}\right)\)

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

אבל מה אם נעשה קצת יותר מזה - בין ההפעלה של הפונקציה הלינארית הראשונה וההפעלה של הפונקציה הלינארית השניה נדחוף פנימה רכיב לא לינארי?

יש שלל רכיבים לא לינאריים שבהם אפשר להשתמש וכרגיל בעניינים הללו, השאלה מה מתאים היא פחות תיאורטית ויותר פרקטית - מה עובד לנו טוב בקונטקסט הנוכחי שלנו. אז הנה דוגמא אחת לפונקציה כזו שהיא במקרה ממוזל גם זו שבה ישתמש המודל הקונקרטי שאציג בפוסט הבא. ראשית, בואו נגדיר פונקציה שנקראת סיגמואיד, \(\sigma\left(x\right)=\frac{1}{1+e^{-x}}\) . זו פונקציה שמוגדרת על כל \(\mathbb{R}\), ונעה באופן מונוטוני עולה בין 0 (אליו היא שואפת כש-\(x\) שואף ל-\(-\infty\) ) ו-1 (אליו היא שואפת כש-\(x\) שואפת אל \(\infty\) ). אם נכפול אותה ב-\(x\) נקבל פונקציה שערכיה נעים מ-\(-\infty\) אל \(\infty\) :

\(S\left(x\right)=\frac{x}{1+e^{-x}}\) הפונקציה הזו נקראת לפעמים SiLU(ראשי תיבות של Sigmoid Linear Unit). שימו לב לכך שהיא נחמדה למדי - קל לחשב אותה והתיאור שלה פשוט. בנוסף לכך, שימו לב שהיא לא תלויה בפרמטרים. הפונקציה הלינארית שמצאתי קודם, \(p\), הייתה תלויה בשני פרמטרים, \(w,b\) . לעומת זאת \(S\left(x\right)\) היא בדיוק מה שהגדרנו פה. גם במודל שאדבר עליו בפוסט הבא, זה כל מה שהיא תהיה. ה"מוח" של המודל, הפרמטרים שלו, מתאימים לפעולות הלינאריות שהמודל מבצע; הפעולות הלא לינאריות הן פונקציות נטולות פרמטרים כמו \(S\left(x\right)\) שלנו.

עכשיו בהחלט אפשר לדמיין אלגוריתם "דו שכבתי" שמקבל קלט \(x\), מחשב את \(p_{1}\left(x\right)=w_{1}x+b_{1}\), מפעיל את \(S\left(x\right)\) על התוצאה, ואז מפעיל את \(p_{2}\left(x\right)=w_{2}x+b_{2}\) על התוצאה של זה. כלומר, כל שכבה היא עצמה פונקציה אפינית עם שני פרמטרים, ובין שתי השכבות יש לנו הפעלה של פונקציה לא לינארית, מה שמבטיח שכל שכבה תהיה "עצמאית" במובן זה שאי אפשר לקפל את מה ששתי השכבות עושות לתוך שכבה בודדת.

אפשר כמובן להגדיל את זה עוד יותר - לא רק להוסיף שכבות אלא גם להגדיל את מה שקורה בכל שכבה, ולהגדיל את כמות הוקטורים שפועלים עליהם. הנה דוגמא פשוטה: נניח שיש לנו שני קלטים ממשיים, \(x^{\left(1\right)},x^{\left(2\right)}\) . אפשר להגדיר בעזרתם שלושה פלטים ממשיים,

\(y^{\left(1\right)}=S\left(w^{\left(1\right)}_{1}x^{\left(1\right)}+w^{\left(2\right)}_{1}x^{\left(2\right)}+b_{1}\right)\)

\(y^{\left(2\right)}=S\left(w^{\left(1\right)}_{2}x^{\left(1\right)}+w^{\left(2\right)}_{2}x^{\left(2\right)}+b_{2}\right)\)

\(y^{\left(3\right)}=S\left(w^{\left(1\right)}_{3}x^{\left(1\right)}+w^{\left(2\right)}_{3}x^{\left(2\right)}+b_{3}\right)\)

ו... אנחנו מתחילים להסתבך עם אינדקסים! אבל הרעיון ברור: יש לנו שלוש "קופסאות", כל אחת עם סט פרמטרים משלה. לקופסה ה-\(i\) יש את הפרמטרים \(w^{\left(1\right)}_{i},w^{\left(2\right)}_{i},b\), ומה שכל קופסה עושה הוא זהה: כופלים את הערכים \(x^{\left(1\right)},x^{\left(2\right)}\) במשקלים המתאימים, מוסיפים את ה-\(b\) המתאים ומפעילים \(S\) על הכל. עכשיו אפשר לקחת את הפלטים של שלוש הקופסאות הללו ולהשתמש בהם בתור קלטים לשכבה חדשה של קופסאות וכן הלאה.

יצור מהצורה הזו נקרא לפעמים רשת נוירונים. המקור לשם הזה הוא במאמר משנות ה-40 שניסה לתת מודל חישובי ששואב השראה מאופן פעילות הנוירונים במוח. הרעיון היה פשוט ויפה: רשת נוירונים היא גרף מכוון עם קשתות ממושקלות. כל צומת הוא "נוירון" שיכול להיות כבוי (0) או דלוק (1). בכל שלב נתון, מחשבים את האופן שבו הרשת מתפתחת לשלב הבא על ידי מעבר נוירון-נוירון; לכל נוירון, מסתכלים על כל הנוירונים הדולקים שנכנסים אליו (כלומר, יש קשת מכוונת מהם אליו) ומחברים את המשקולות שעל הקשתות שלהם; אם הסכום הכולל עובר סף מסוים (שתלוי בנוירון הספציפי), הוא יהיה דלוק בשלב הבא, ואחרת הוא יהיה כבוי. יש לרשת גם קשתות של קלט ראשוני שנכנסות את הנוירונים, וקשתות של פלט. הרעיון הוא "לאמן" את הרשת על ידי מציאת המשקלים המתאימים על הקשתות שיתאימו למשימה שהרשת מנסה לעשות (למשל, סיווג של קלטים ל"כן/לא").

רשת כזו מתאימה בדיוק למה שראינו לפני רגע, עבור המקרה שבו כל הקלטים הם 0 או 1, והפונקציה \(S\) היא פונקציה שמחזירה 0 עד סף מסוים, והחל ממנו היא מחזירה 1 (פונקציה כזו נקראת פונקציית הביסייד ). גם זו פונקציה לא לינארית, אבל בשימושים מודרניים הפונקציה \(S\) שהראיתי ופונקציות לא לינאריות אחרות, שלא מוציאות פלט דיסקרטי אלא רציף, הן שימושיות יותר.

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

\(x=\left(\begin{array}{c} x^{\left(1\right)}\\ x^{\left(2\right)} \end{array}\right)\) ו-

\(W=\left(\begin{array}{cc} w^{\left(1\right)}_{1} & w^{\left(2\right)}_{1}\\ w^{\left(1\right)}_{2} & w^{\left(2\right)}_{2}\\ w^{\left(1\right)}_{3} & w^{\left(2\right)}_{3} \end{array}\right)\) ו-

\(b=\left(\begin{array}{c} b_{1}\\ b_{2}\\ b_{3} \end{array}\right)\)

אז אפשר לכתוב

\(y=S\left(Wx+b\right)\)

כשהמוסכמה היא שכשאני מפעיל את \(S\) על וקטור, אני מפעיל את \(S\) בנפרד על כל כניסה של הוקטור. עכשיו קיבלנו רשת שבה כל שכבה מיוצגת על ידי מטריצה \(W\) ועל ידי וקטור \(b\) (מספר העמודות של המטריצה הוא כמספר הקלטים לשכבה; מספר השורות שלה, ומספר הכניסות של \(b\), הוא כמספר הפלטים של השכבה).

כשיש ליצור הזה רק פלט אחד, \(W\) היא בעצמה וקטור ו-\(b\) הוא סקלר בודד. במקרה הזה, ועם \(S\) שהיא פונקציית הביסייד, קוראים ליצור הזה פרספטרון (Perceptron; שוב יש כאן פיסת היסטוריה מעניינת שלא אכנס אליה). כשלוקחים את הדבר הזה ומרחיבים אותו - עוד שכבות, עוד פלטים - נהוג לפעמים לקרוא לזה "פרספטרון רב-שכבתי", Multilayer Perceptron, ובקיצור MLP. הזכרתי קודם, בתיאור של LLM-ים, שיש "שלב ה-MLP"? זה הדבר הזה, ככה בערך השלב הזה נראה.

אז מה הולך בשלב ה-Attention?

בואו נחזור שניה על מה ש-LLM עושה: הוא מקבל טקסט, ומוציא דירוג שמאפשר לנו לבחור את המילה הבאה שנוסיף לטקסט. הקלט הראשוני שלו, אחרי שמפרקים את הטקסט לטוקנים וממירים את הטוקנים לוקטורים, הוא לא וקטור בודד - הוא סדרה של וקטורים, \(\left(x_{1},x_{2},\ldots,x_{N}\right)\) כש-\(N\) הוא מספר הטוקנים הכולל בטקסט - זה יכול להיות מספר קטן אם סתם באנו לבקש להשלים את "החיה החמודה בטבע היא", וזה יכול להיות מספר עצום אם אנחנו מבקשים מה-LLM לקרוא מאמר שלם על "החיה החמודה הטבע היא כלבים" ודורשים ממנו למצוא פגמים לוגיים במאמר הזה. הדבר הראשון שה-LLM יעשה הוא "לקרוא את המאמר" על ידי בניית סדרת וקטורים ארוכה שמתארת את כל המאמר, ירוץ על כולה ואז יתחיל לייצר את התשובה שלו.

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

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

דרך אחת לעשות את זה נקראת RNN, ראשית תיבות של Recurrent Neural Network. הרעיון הבסיסי מאחוריה הוא פשוט: היא מורכבת מדברים שנראים כמו MLP-ים, אבל כאלו שמקבלים שני קלטים. אחד הוא וקטור הקלט, \(x_{i}\), אבל השני הוא וקטור "חבוי", \(h_{i-1}\) ("Hidden State" היא טרמינולוגיה מקובלת בתחום), שחושב בשלב הקודם. כלומר, אם \(f\) היא הפונקציה שמתארת שלב של ה-RNN, מה שקורה זה הדבר הבא:

\(f\left(x_{i},h_{i-1}\right)=\left(y_{i},h_{i}\right)\) כאשר \(y_{i}\) הוא הפלט על \(x_{i}\) ו-\(h_{i}\) הוא המידע החבוי שהולך לעבור לשלב הבא (כמובן, צריך שיהיה ערך התחלתי כלשהו \(h_{0}\) שלא תלוי בחישובים שבוצעו עד כה).

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

העניין הוא שהוקטור \(h_{i}\) הזה הוא... די מוגבל בכמות המידע שהוא יכול לכלול בתוכו? בסופו של דבר זה לא הולך להיות וקטור עצום כי אם הוא יהיה עצום גם הרכיבים של ה-RNN שמבצעים את החישובים יהיו עצומים, אבל הטקסט שקראנו קודם יכלו להיות עצום. איכשהו צריך יהיה לדחוס לתוך כל \(h\) כזה את כל המידע הרלוונטי על הכל שהופיע כבר בטקסט, וזה בוודאי לא הולך לעבוד. אז מידע שיכול להיות קריטי הולך לאיבוד. מצד שני, הרבה מידע שהוא ג'אנק ביחס למילה הנוכחית שאנחנו מתעסקים בה עשוי להישאר. למשל, אם אנחנו שואלים את ה-LLM"בהתחשב בזה שהצבע האהוב עלי הוא אדום ואני אוהב מותחני בילוש מתמטיים, מה הספר הבא שכדאי לי לקרוא ואיזו סימניה שווה לי להזמין?" אז העובדה שהצבע האהוב עלי אדום היא כנראה לא רלוונטית לספר הבא שכדאי לי לקרוא (אבל כן לסימניה ששווה לי להזמין). אבל המידע על האדום הזה יידחף ל-\(h\) ושם יתקוטט עם המידע על מותחני הבילוש המתמטיים למרות שבשלב שבו אנחנו מסתכלים על "ספר" המידע על האדום לא רלוונטי בכלל.

על הרקע הזה הגיע ב-2014 המאמר Neural Machine Translation by Jointly Learning to Align and Translate של Bahdanau, Cho, Bengio שהציע את המנגנון שאנחנו מכנים היום Attention(או ליתר דיוק Self-Attention אבל אני לא אכנס לזה הפעם). אני חושב שהדרך הטובה ביותר להבין מאיפה המנגנון הזה בא ומה הוא מנסה להשיג הוא בדיוק את מה שתיארתי: שבניתוח הטוקן הנוכחי, אנחנו נשלוף מהניתוח של הטוקנים הקודמים את המידע הרלוונטי לשלב הנוכחי. במאמר של 2014 המנגנון הזה לא הוצג בתור אלטרנטיבה לשימוש ב-RNN אלא בתור אמצעי חכם לשפר את ה-RNN-ים; המאמר של 2017 של Attention Is All You הלך צעד נוסף קדימה ואמר - עזבו אתכם מ-RNN-ים, אפשר להסתפק רק במנגנון המגניב של ה-Attention ואם זונחים את המבנה של ה-RNN-ים מקבלים יתרון נוסף - אפשר לטפל בכל הטקסט שקיבלנו בתור קלט באופן מקבילי, מה שיש לו השלכות דרסטיות על המהירות שבה אפשר לבצע אימון של מערכות כאלו. אבל לפני שאני אכנס להסברים הללו, בואו נבין סוף סוף מה זה Attention ואיך זה עובד.

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

הנה הרעיון הבסיסי. יש לנו סדרה \(\left(x_{1},\ldots,x_{N}\right)\) של וקטורים - הם יכולים להיות קידוד ישיר של \(N\) טוקנים, אבל אותו הדבר תקף גם אחרי שאנחנו כבר בשכבה עמוקה יותר של ה-LLM ולא נכון לחשוב על הוקטורים הללו בתור ייצוג של טוקנים אלא בתור ייצוג ביניים ("hidden state") של מידע חצי מעובד. מה שנעשה הוא לחשב מכל וקטור \(x_{i}\) שלושה פרטי מידע שונים, שגם הם מקודדים בתור וקטורים (כלומר, יכולים להכיל די הרבה אינפורמציה בו זמנית):

הנה דוגמת צעצוע שהיא כמובן מונפצת לגמרי - המטרה פה היא לתת אינטואיציה, לא להציג משהו ספציפי שבאמת קורה בתוך מודלים. בואו נחשוב ש-\(x\) הוא קידוד ישיר של הטוקן "חתול".

ההצגה הזו פשטנית מדי כי היא יוצרת את הרושם שהערך פשוט מכיל תשובות לשאלות של השאילתא ואילו המפתח בא לשקף בדיוק את השאלות שיש בשאילתא. זה לא המצב בפועל. מה שקורה בפועל הוא שהחישוב של \(q,k,v\) הללו תלוי בעצמו בפרמטרים שהם חלק מהאימון של המודל. אין לנו אלגוריתם שמייצר שאלות/תשובות בצורה מסודרת. מה שיש לנו, במימוש בעזרת LLM-ים שעליו אני מדבר, הוא מטריצות. יש שלוש מטריצות: \(W_{Q},W_{K},W_{V}\), ובהינתן \(x_{i}\) אנחנו בסך הכל מבצעים את החישוב

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

עכשיו מגיע החלק היפה. הוקטור \(x_{i}\) הולך להסתכל אחורה, אל כל הוקטורים עד אליו (וכולל הוא עצמו!) ולכל אחד מהם הוא הולך לשאול - כמה הוקטור הזה רלוונטי אלי? האינטואיציה מאחורי הבדיקה היא זו: השאילתא\(q_{i}\) מייצגת את המידע ש-\(x_{i}\) מחפש. לעומת זאת המפתח \(k_{j}\) עבור כל \(1\le j\le i\) מייצג את המידע שכל \(x_{j}\) יכול לתת. בהינתן השאילתא \(q_{i}\) והמפתח \(k_{j}\), איך נדע כמו \(k_{j}\) עונה טוב אל \(q_{i}\) ? האינטואיציה המתמטית היא שהתשובה טובה יותר ככל ששני הוקטורים הללו הם "באותו הכיוון", והכלי המתמטי שבו משתמשים כדי למדוד דבר כזה הוא מכפלה סקלרית. במכפלה סקלרית, אנחנו מקבלים מספר ששווה למכפלה של הגדלים של הוקטורים, כפול קוסינוס הזווית ביניהם; כלומר, אם הוקטורים הם באותו כיוון בדיוק ולכן הזווית ביניהם היא 0, נקבל את הערך הגדול ביותר האפשרי (קוסינוס של 0 הוא 1); אם הם בדיוק בכיוונים הפוכים עם זווית של 180 מעלות נקבל את הערך הקטן ביותר האפשרי כי נצמיד סימן מינוס לגדלים שלהם (קוסינוס של 180 מעלות הוא \(-1\) ) ואם הם בכיוונים בלתי תלויים לגמרי - אחד הולך למעלה, השני ימינה - אז הזווית ביניהם היא 90 מעלות ולכן נקבל 0 (קוסינוס של 90 מעלות הוא 0).

אם כך, מה שאנחנו עושים הוא לחשב, עבור \(i\) קונקרטי וכל \(1\le j\le i\), את סדרת המספרים \(q_{i}k_{j}\), כלומר

\(q_{i}\cdot k_{1},q_{i}\cdot k_{2},\ldots,q_{i}\cdot k_{i}\) הדבר הזה נותן לי סדרת מספרים ממשיים, אבל מה שבאמת הייתי רוצה לקבל הוא הסתברויות, כלומר סדרת מספרים \(p_{1},\ldots,p_{i}\) כך ש-\(0\le p_{j}\le1\) ו-\(\sum^{i}_{j=1}p_{j}=1\) . את האפקט הזה אפשר להשיג על ידי נרמול; השיטה המקובלת לבצע את הנירמול הזה היא בעזרת פונקציה לא לינארית שנקראת softmax. הנה ההגדרה הכללית שלה: בהינתן סדרת מספרים \(z_{1},\ldots,z_{N}\), מה ש-softmax עושה הוא לייצר מהם סדרת מספרים חדשה \(p_{1},\ldots,p_{N}\) כך ש-

\(\text{softmax}\left(z_{1},\ldots,z_{N},i\right)=p_{i}=\frac{e^{z_{i}}}{\sum^{N}_{j=1}e^{z_{j}}}\) כלומר, בסך הכל מחליפים כל \(z_{i}\) ב-\(e^{z_{i}}\) (מה שהופך מספר ממשי כלשהי למספר ממשי אי-שלילי, אבל בצורה מאוד לא לינארית) ואז מנרמלים למספר בין 0 ו-1 על ידי חלוקה בסכום הגדלים הכולל של כל המספרים בסדרה.

אם כן, הגענו לשלב שבו עבור הוקטור \(x_{i}\) מצאנו סדרת הסתברויות \(p_{1},\ldots,p_{i}\) שבמובן מסויים מתארות כמה כל וקטור עד \(x_{i}\) רלוונטי אליו. עכשיו מגיע השלב האחרון - אנחנו רוצים לאסוף את כל המידע הרלוונטי מכל הוקטורים; המידע הזה הוא מה ששמור בתוך וקטור הערך, \(v_{i}\), שחישבנו מכל אחד מהוקטורים הללו. אז מה שנעשה הוא לקחת צירוף לינארי שלהם כשהמקדמים הם ההסתברויות שחישנו (אולי אתם מכירים את הקונספט הזה בתור צירוף קמור ):

\(g_{i}=p_{1}v_{1}+p_{2}v_{2}+\ldots+p_{i}v_{i}\) ה-\(g_{i}\) הזה הוא התוצאה של חישוב ה-Attention עבור הוקטור \(x_{i}\) . מה עושים איתו? אפשר פשוט לשלוח אותו הלאה בתור התוצאה של השלב הזה; אפשר גם לחבר אותו עם \(x_{i}\) המקורי; אפשר לעשות כל מני דברים שתלויים במבנה של המודל עצמו. מה שחשוב הוא שיש לנו עכשיו מנגנון של איסוף מידע רלוונטי שבמקום ה-\(h\) של ה-RNN יודע לייצר \(g_{i}\) לכל וקטור - וחשוב מכך, יודע לייצר אותו בצורה פשוטה (הכל אלגברה לינארית בסיסית), ובעזרת פרמטרים של המודל עצמו (כלומר, משהו שאפשר לאמן ולשפר גם בלי להבין עד הסוף מה הולך שם).

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

איך כל זה מתחבר?

בואו נעשה סיכום ביניים קטן של איך LLM עובד: ראשית הוא לוקח את הטקסט ומפרק אותו לטוקנים; אחר כך הוא ממיר את הטוקנים לוקטורים; אחר כך הוא חוזר שוב ושוב על תהליך דו שלבי - קודם כל הוא מבצע Attention כדי להוסיף לוקטורים מידע שנשאב מהוקטורים הקודמים, ואז הוא מפעיל MLP על כל וקטור בנפרד. בסופו של דבר הוא ממיר את התוצאה של כל וקטור לא לטוקן בודד, אלא לדירוג מספרי של כל הטוקנים, מה שנקרא logits. הפרטים הקטנים של התהליך הזה תלויים בשני דברים: בארכיטקטורה של המודל - שהיא מה שקובע בדיוק מה קורה בכל שלב, כמה שלבים יהיו, מה הגודל של הוקטורים המעורבים וכו'; ובפרמטרים של המודל שהם המספרים הממשיים שנמצאים במטריצות שמשתמשים בהן בשלבי התרגום ההתחלתי מטוקנים לוקטורים, התרגום הסופי מוקטורים אל logits, וכל ה-MLP-ים ושכבות ה-Attention שבדרך. הפרמטרים הללו הם ה"מוח" של המודל, הם איפה שכל המידע שיש לו מאוחסן. וזה כמובן מעלה את השאלה - מאיפה הם מגיעים?!

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

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

בואו נדבר על שימוש מוכר אחד לבינה מלאכותית שעבד לא רע זמן רב לפני שהיו לנו LLM-ים: זיהוי כתב יד. כדי לאמן מודל לזהות כתב יד, אפשר לתת לו להתאמן על קבצי תמונות של אותיות בודדות בכתב יד. לצד כל קובץ יש מידע נוסף, "תיוג" של הקובץ, שבמקרה הזה אומר מה האות שמופיעה בתמונה. האלגוריתם שמבצע את אימון המודל מריץ אותו על התמונה ובודק מה הדירוג שהוא נתן לאות הנכונה. ככל שהדירוג גבוה יותר, כך ה-loss קטן יותר. עכשיו מחשבים את הגרדיאנט, משנים קצת את הפרמטרים של המודל על פי הערך של הגרדיאנט, וממשיכים לקובץ התמונה הבא (אפשר גם לחזור לתמונות קיימות; אפשר הרבה דברים). התקווה היא שבסופו של דבר, אוסף התיקונים הקטנים של הפרמטרים יצטבר למשהו גדול שבאמת יודע לזהות כתב יד ברמה סבירה של הצלחה - והפלא ופלא, זה אכן מה שקורה.

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

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

עכשיו, רגע, מה זאת אומרת הרצף שהמודל שלי חוזה? לא הרעיון הוא שקוראים טקסט ואז חוזים את המילה החדשה שצריך להוסיף לטקסט? איפה יש כאן רצף? ובכן, בואו ניזכר לרגע איך LLM עובד. בשלב הלפני אחרון, אחרי שעברנו את כל שכבות ה-MLP למיניהן, יש לנו ביד סדרה של וקטורים, \(x_{1},\ldots,x_{N}\), אחד לכל טוקן בקלט שלנו. בשלב האחרון אנחנו ממירים כל וקטור כזה אל logits. כשאנחנו משתמשים ב-LLM כדי ליצור טקסט, רוב ה-logits הללו לא מעניינים אותנו - רק האחרון, זה של \(x_{N}\), רלוונטי. אבל כשאנחנו בשלב האימון אנחנו יכולים להשתמש בכולם כדי לבצע את ההשוואה שדיברתי עליה. המילה "החתול" תייצר logits משלה שהערך שלהם יהיה גבוה יותר ככל ש"הקטן" יהיה בהם במקום גבוה יותר, וכן הלאה. במילים אחרות, לא צריך לעשות שום דבר מיוחד עם המודל כשרוצים לאמן אותו - האופן הרגיל שבו הוא עובד מתאים בדיוק לשיטת האימון של "תנסה לנבא את הטוקן הבא בתור".

בצורה הזו אפשר באמת להפיל על המודל כמויות עצומות של מידע בלי צורך לתייג אותו, אבל זה יוצר אצלנו צוואר בקבוק חדש: זמן. אם רוצים לאמן על כמויות אסטרונומיות של מידע, זה ייקח המון זמן. איך אפשר לחסוך זמן, בהינתן שלא נשנה את האלגוריתם שלנו? חישוב מקבילי. וכאן נכנס לתמונה יתרון גדול אחר של Attention על RNN-ים: אפשר לבצע חישוב Attention בצורה מקבילית יעילה. ליתר דיוק - אפשר להציג את רוב הסיפור בתור כפל מטריצות, וכפל מטריצות יעיל הוא בדיוק מה שיש לנו חומרה ייעודית מדהימה כדי לבצע ביעילות - GPU-ים, רכיבי חומרה סופר מתוחכמים שלכאורה מיועדים לשימוש בגרפיקה ממוחשבת, כלומר כדי לאפשר לגיימרים להרוג אחד את השני בתלת מימד בקצב של 60 פריימים לשניה; אלא שכמובן, מאז שהתגלה כמה GPU-ים הם שימושיים לבועת ה-AI, הגיימרים בצרות כי הביקוש העצום ל-GPU-ים גרם לקפיצת מחירים משמעותית גם בעולמות שלהם.

בואו נראה למה זה הכל כפל מטריצות. בשלב ה-Attention יש לנו סדרה \(x_{1},\ldots,x_{N}\) של \(N\) וקטורים; בואו נסמן את המימד שלהם ב-\(d\) . אפשר לדמיין שאנחנו מסדרים את הוקטורים הללו במטריצה כך שכל שורה היא אחד מהוקטורים; זו תהיה מטריצה מסדר \(N\times d\), עם \(N\) שורות ו-\(d\) עמודות. אסמן אותה ב-\(X\) . עכשיו, בשלב חישוב השאילתא, אנחנו מבצעים את החישוב \(q_{i}=W_{Q}x_{i}\) לכל \(1\le i\le N\) . כאן \(W_{Q}\) היא מטריצה מסדר \(d\times d\) (לצורך פשטות אני מניח שהגודל של כל וקטור שאילתא הוא גם \(d\), אבל הכל יעבוד בצורה דומה גם אם הוא לא). עכשיו, אם נסתכל על מכפלת המטריצות \(Q=XW_{Q}\), קיבלנו מטריצה חדשה מסדר \(N\times d\), שהשורה ה-\(i\) -ית שלה היא \(q_{i}\) . באופן דומה נגדיר גם \(K=XW_{K}\) ו-\(V=XW_{V}\) עבור המפתחות והערכים.

עכשיו מגיע השלב שבו אנחנו רוצים לחשב את המכפלות הפנימיות מהצורה \(q_{i}\cdot k_{j}\) . הכפל הזה הוא המכפלה של השורה ה-\(i\) ב-\(Q\) בשורה ה-\(j\) ב-\(K\) . כדי להשיג אפקט כזה על ידי כפל מטריצות, מבצעים שחלוף למטריצה השניה, כך שמי שקודם הייתה השורה ה-\(j\) של \(K\) עכשיו תהיה העמודה ה-\(j\) של \(K\), ולכן הכניסה ה-\(ij\) של כפל המטריצות \(QK^{T}\) תהיה שווה בדיוק למכפלה \(q_{i}\cdot k_{j}\) הזו.

דבר אחד שעושים בשלב הזה ולא סיפרתי עליו הוא לחלק את כל הכניסות ב-\(QK^{T}\) ב-\(\sqrt{d}\), מה שמבטיח שהערכים שעובדים אותם לא יהיו גדולים מדי (לכן קוראים לשלב הזה לפעמים scaled dot produt). דבר אחר שכן סיפרתי עליו אבל לא בא לידי ביטוי בחישוב שתיארתי כרגע הוא שנהוג לדרוש שהוקטור \(x_{i}\) יקח רק את המכפלות הפנימיות עם וקטורים שבאים לפניו ולא אחריו, אבל ב-\(QK^{T}\) יש כניסה לכל מכפלה מהצורה \(q_{i}\cdot k_{j}\) גם כאשר \(i\lt j\) . הדרך להתמודד עם זה היא לחבר אל \(QK^{T}\) "מסיכה" שמוחקת את הכניסות הבעייתיות - ברמה הפורמלית מחברים מטריצה \(M\) מסדר \(N\times N\) שהכניסות שלה מתחת וכולל האלכסון הראשי הן 0 וכל אלו שמעל הן \(-\infty\), כלומר משהו כזה:

\(M=\left(\begin{array}{cccc} 0 & -\infty & -\infty & -\infty\\ 0 & 0 & -\infty & -\infty\\ 0 & 0 & 0 & -\infty\\ 0 & 0 & 0 & 0 \end{array}\right)\) זה לא משנה את המכפלות הסקלריות שרצינו להשאיר, והופך ל-\(-\infty\) את אלו שלא רצינו להשאיר - ואחרי הפעלת ה-softmax ה-\(-\infty\) יהפוך ל-0, שמשמעותו היא שבצירוף הלינארי של ה-\(v\) -ים, מי שגדולים מ-\(i\) לא ישתתפו.

אפרופו הצירוף הלינארי הזה - כמובן שגם הוא בעצם מהווה כפל מטריצות. במטריצה \(QK^{T}\), הכניסה ה-\(ij\) מתאימה כאמור למכפלה \(q_{i}\cdot k_{j}\), ולכן השורה ה-\(i\) מתאימה לוקטור \(x_{i}\) ולכל המקדמים \(p_{1},\ldots,p_{i}\) שהוא נתן ליתר הוקטורים. עכשיו, מה יקרה כשנכפיל את השורה הזו במטריצה \(V\) שקיבלנו קודם?

באופן כללי, אם יש לי וקטור שורה \(v\) מסדר \(1\times n\) ומטריצה \(A\) מסדר \(n\times n\) ואני מסתכל על המכפלה \(vA\), אני מקבל וקטור שורה מסדר \(1\times n\) שהוא צירוף לינארי של השורות של \(A\), עם מקדמים שהם הערכים של \(v\) . לי קשה לראות את זה, אז אני מסתכל ברמה הנקודתית - הכניסה הראשונה של \(vA\) היא המכפלה של \(v\) בעמודה הראשונה של \(A\), כלומר בכל הכניסות הראשונות בוקטורים שהם שורות \(A\), כשכל כניסה כזו מוכפלת במקדם שמתאים למספר השורה. אותו הדבר יקרה גם עבור העמודה השניה, והשלישית, וכן הלאה. אז אם נחזור למקרה שלנו, כדי לקבל צירוף לינארי של כל וקטורי הערך, צריך לכפול את מה שקיבלנו ב-\(V\) . אז הנוסחה המלאה של מה שאנחנו עושים היא

\(\text{softmax}\left(\frac{QK^{T}}{\sqrt{d}}+M\right)V\) וזו הנוסחה שתראו בכל מקום שמסביר מה זה Attention(לפעמים בלי \(M\) ; למשל במאמר של Attention Is All You Need מדברים על האפשרות להשתמש במשהו כזה אבל זה לא נכנס במפורש לנוסחה).

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

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

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

חישוב MLP הוא כזכור עצמאי לגמרי מהשלבים הקודמים. הדבר היחיד שלא עצמאי הוא חישובי Attention. מנקודת מבטו של \(x\) החדש שלנו, מה קורה בשלב הזה? אנחנו מחשבים את \(q_{x}\), ואז כופלים את \(q_{x}\) בכל ה-\(k_{i}\) של כל הטוקנים הקודמים. העניין הוא שה-\(k_{i}\) הזה כבר חושב קודם. אנחנו לא צריכים לחשב אותו שוב בכל פעם, מספיק שנשמור אותו בצד. באופן דומה אחר כך נזדקק גם ל-\(v_{i}\) שחושב קודם וגם אותו שומרים בצד. האוסף הזה של ערכי ה-\(K\) וה-\(V\) ששמורים בצד נקרא ה-KV cache; שימו לב שאת ערכי ה-\(Q\) לא צריך לשמור כי השימוש בהם הוא חד פעמי - אנחנו משתמשים בהם רק בחישובים עבור הטוקן הנוכחי. זו גם הסיבה למה חשוב להקפיד ש-Attention של טוקן לא יתבסס על טוקנים מתקדמים יותר - אם זה כן יהיה המצב, כן נצטרך לחשב מחדש את ה-Attention של הטוקנים הישנים יותר ככל שאנחנו מרחיבים את הטקסט.

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

יש עוד דבר אחד שלא היה ברור לי כשהתחלתי ללמוד על הנושא, ולשמחתי התשובה היא פשוטה יחסית: איך בעצם גורמים ל-LLM, שהתפקיד שלו הוא לנבא את הטוקן הבא בטקסט, להתנהג כמו בצ'אט? כלומר, ממש לנהל איתי שיחה, לענות לי על שאלות וכדומה? הנה דוגמא פשוטה לבעייתיות פה. נניח שאני אומר ל-LLM"שלום, אני גדי" מה שאני מצפה שהוא יענה לי כשהוא מתנהג כמו צ'אט הוא "היי גדי! איך אני יכול לעזור לך?!" אבל מה שהוא אמור לענות אם הוא היה מחפש את הדבר הכי מתבקש לכתוב אחרי מה שכתבתי, זה "אלכסנדרוביץ'", או "החתול" או משהו בסגנון הזה - כלומר, הוא אמור להמשיך את הטקסט שלי, לא לכתוב תשובה משלו.

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

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

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

הרעיון הוא לקחת מודל קיים ולבצע לו אימון נוסף, מה שנקרא fine tuning. ראשית, מתחילים עם מודל שבאמת אומן על כמויות עצומות של מידע במשך שבועות - זה יהיה "מודל הבסיס" (base model) שלנו. השלב הבא הוא לקחת את המודל הזה ולהמשיך את תהליך האימון שלו - אבל רק על טקסטים שמתאימים למסגרת שלנו. פרט לכך הרעיון הוא אותו רעיון - בכל צעד משנים את הפרמטרים כך שפונקציית ה-loss שלנו תקטן ככל הניתן. כמובן, fine tuning שכזה אפשר לבצע בשלל הקשרים שונים, לא רק כדי ללמד מודל להשתמש נכון בפורמט של צ'אט; אבל זו דוגמת השימוש הכי מתבקשת לדעתי. בפוסט הבא אנסה טיפה להשוות מודל בסיס ומודל הצ'אט שנבנה מעליו ונראה מה הולך שם.

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