גלו את יסודות עיבוד השפה הטבעית (NLP) עם המדריך המקיף שלנו למימוש מודלי N-gram מאפס. למדו את התיאוריה, הקוד ויישומים פרקטיים.
בניית היסודות של NLP: צלילת עומק למימוש מודלי שפה מסוג N-gram
בעידן הנשלט על ידי בינה מלאכותית, החל מהעוזרים החכמים שבכיסנו ועד לאלגוריתמים המתוחכמים המניעים מנועי חיפוש, מודלי שפה הם המנועים הבלתי נראים המניעים רבים מהחידושים הללו. הם הסיבה שהטלפון שלכם יכול לחזות את המילה הבאה שתרצו להקליד, וכיצד שירותי תרגום יכולים להמיר שפה אחת לאחרת בשטף. אבל איך המודלים האלה באמת עובדים? לפני עלייתן של רשתות נוירונים מורכבות כמו GPT, היסודות של הבלשנות החישובית נבנו על גישה סטטיסטית פשוטה להפליא אך רבת עוצמה: מודל N-gram.
מדריך מקיף זה מיועד לקהל עולמי של מדעני נתונים, מהנדסי תוכנה וחובבי טכנולוגיה סקרנים. נצא למסע חזרה ליסודות, נסיר את המסתורין מהתיאוריה שמאחורי מודלי שפת N-gram ונספק הדרכה מעשית, שלב אחר שלב, כיצד לבנות אחד כזה מאפס. הבנת N-grams אינה רק שיעור היסטוריה; זהו צעד חיוני בבניית בסיס איתן בעיבוד שפה טבעית (NLP).
מהו מודל שפה?
בבסיסו, מודל שפה (LM) הוא התפלגות הסתברות על פני רצף של מילים. במילים פשוטות יותר, המשימה העיקרית שלו היא לענות על שאלה בסיסית: בהינתן רצף של מילים, מהי המילה הבאה הסבירה ביותר?
חשבו על המשפט: "התלמידים פתחו את ___ שלהם."
מודל שפה מאומן היטב יקצה הסתברות גבוהה למילים כמו "ספרי", "מחשבי" או "ראשי", והסתברות נמוכה ביותר, כמעט אפסית, למילים כמו "פוטוסינתזה", "פילים" או "כביש מהיר". על ידי כימות הסבירות של רצפי מילים, מודלי שפה מאפשרים למכונות להבין, ליצור ולעבד שפה אנושית באופן קוהרנטי.
היישומים שלהם נרחבים ומשולבים בחיינו הדיגיטליים היומיומיים, וכוללים:
- תרגום מכונה: הבטחת משפט פלט שוטף ונכון דקדוקית בשפת היעד.
- זיהוי דיבור: הבחנה בין ביטויים דומים פונטית (לדוגמה, "recognize speech" לעומת "wreck a nice beach").
- חיזוי טקסט והשלמה אוטומטית: הצעת המילה או הביטוי הבאים בזמן ההקלדה.
- תיקון שגיאות כתיב ודקדוק: זיהוי וסימון רצפי מילים שאינם סבירים סטטיסטית.
היכרות עם N-grams: מושג הליבה
N-gram הוא פשוט רצף עוקב של 'n' פריטים מדגימה נתונה של טקסט או דיבור. ה'פריטים' הם בדרך כלל מילים, אך הם יכולים להיות גם תווים, הברות או אפילו פונמות. ה-'n' ב-N-gram מייצג מספר, מה שמוביל לשמות ספציפיים:
- יוניגרם (n=1): מילה בודדת. (לדוגמה, "The", "quick", "brown", "fox")
- ביגרם (n=2): רצף של שתי מילים. (לדוגמה, "The quick", "quick brown", "brown fox")
- טריגרם (n=3): רצף של שלוש מילים. (לדוגמה, "The quick brown", "quick brown fox")
הרעיון הבסיסי מאחורי מודל שפת N-gram הוא שאנו יכולים לחזות את המילה הבאה ברצף על ידי התבוננות ב-'n-1' המילים שבאו לפניה. במקום לנסות להבין את המורכבות הדקדוקית והסמנטית המלאה של משפט, אנו מניחים הנחה מפשטת שמפחיתה באופן דרמטי את קושי הבעיה.
המתמטיקה מאחורי N-grams: הסתברות ופישוט
כדי לחשב באופן פורמלי את ההסתברות של משפט (רצף מילים W = w₁, w₂, ..., wₖ), אנו יכולים להשתמש בכלל השרשרת של ההסתברות:
P(W) = P(w₁) * P(w₂|w₁) * P(w₃|w₁, w₂) * ... * P(wₖ|w₁, ..., wₖ₋₁)
נוסחה זו קובעת שההסתברות של הרצף כולו היא מכפלת ההסתברויות המותנות של כל מילה, בהינתן כל המילים שבאו לפניה. למרות שגישה זו נכונה מתמטית, היא אינה מעשית. חישוב ההסתברות של מילה בהינתן היסטוריה ארוכה של מילים קודמות (למשל, P(word | "The quick brown fox jumps over the lazy dog and then...")) ידרוש כמות גדולה באופן בלתי אפשרי של נתוני טקסט כדי למצוא מספיק דוגמאות לביצוע הערכה אמינה.
הנחת מרקוב: פישוט מעשי
זה המקום שבו מודלי N-gram מציגים את המושג החשוב ביותר שלהם: הנחת מרקוב. הנחה זו קובעת שההסתברות של מילה תלויה רק במספר קבוע של מילים קודמות. אנו מניחים שההקשר המיידי מספיק, ושאנו יכולים להתעלם מההיסטוריה הרחוקה יותר.
- עבור מודל ביגרם (n=2), אנו מניחים שההסתברות של מילה תלויה רק במילה הקודמת היחידה:
P(wᵢ | w₁, ..., wᵢ₋₁) ≈ P(wᵢ | wᵢ₋₁) - עבור מודל טריגרם (n=3), אנו מניחים שהיא תלויה בשתי המילים הקודמות:
P(wᵢ | w₁, ..., wᵢ₋₁) ≈ P(wᵢ | wᵢ₋₁, wᵢ₋₂)
הנחה זו הופכת את הבעיה לפתירה מבחינה חישובית. איננו צריכים עוד לראות את ההיסטוריה המלאה המדויקת של מילה כדי לחשב את ההסתברות שלה, אלא רק את n-1 המילים האחרונות.
חישוב הסתברויות N-gram
כאשר הנחת מרקוב בתוקף, כיצד אנו מחשבים את ההסתברויות הפשוטות הללו? אנו משתמשים בשיטה הנקראת אומדן נראות מרבית (MLE), שזו דרך מהודרת לומר שאנו מקבלים את ההסתברויות ישירות מהספירות בטקסט האימון שלנו (קורפוס).
עבור מודל ביגרם, ההסתברות של מילה wᵢ המופיעה אחרי מילה wᵢ₋₁ מחושבת כך:
P(wᵢ | wᵢ₋₁) = Count(wᵢ₋₁, wᵢ) / Count(wᵢ₋₁)
במילים: ההסתברות לראות את מילה B אחרי מילה A היא מספר הפעמים שראינו את הזוג "A B" חלקי מספר הפעמים שראינו את מילה "A" בסך הכל.
הבה נשתמש בקורפוס זעיר כדוגמה: "The cat sat. The dog sat."
- ספירת ("The") = 2
- ספירת ("cat") = 1
- ספירת ("dog") = 1
- ספירת ("sat") = 2
- ספירת ("The cat") = 1
- ספירת ("The dog") = 1
- ספירת ("cat sat") = 1
- ספירת ("dog sat") = 1
מהי ההסתברות של "cat" אחרי "The"?
P("cat" | "The") = Count("The cat") / Count("The") = 1 / 2 = 0.5
מהי ההסתברות של "sat" אחרי "cat"?
P("sat" | "cat") = Count("cat sat") / Count("cat") = 1 / 1 = 1.0
מימוש שלב-אחר-שלב מאפס
כעת בואו נתרגם את התיאוריה הזו למימוש מעשי. נתאר את השלבים באופן אגנוסטי לשפת תכנות, אם כי ההיגיון מתמפה ישירות לשפות כמו פייתון.
שלב 1: עיבוד מקדים של נתונים וטוקניזציה
לפני שנוכל לספור משהו, עלינו להכין את קורפוס הטקסט שלנו. זהו שלב קריטי המעצב את איכות המודל שלנו.
- טוקניזציה: תהליך פיצול גוף טקסט ליחידות קטנות יותר, הנקראות טוקנים (במקרה שלנו, מילים). לדוגמה, "The cat sat." הופך ל-["The", "cat", "sat", "."].
- המרת כל האותיות לקטנות: זוהי פרקטיקה סטנדרטית להמיר את כל הטקסט לאותיות קטנות. הדבר מונע מהמודל להתייחס ל-"The" ו-"the" כשתי מילים שונות, מה שעוזר לאחד את הספירות ולהפוך את המודל לחזק יותר.
- הוספת טוקני התחלה וסיום: זוהי טכניקה חיונית. אנו מוסיפים טוקנים מיוחדים, כמו <s> (התחלה) ו-</s> (סיום), לתחילת וסוף כל משפט. למה? הדבר מאפשר למודל לחשב את ההסתברות של מילה בתחילת משפט (לדוגמה, P("The" | <s>)) ועוזר להגדיר את ההסתברות של משפט שלם. המשפט לדוגמה שלנו "the cat sat." יהפוך ל-["<s>", "the", "cat", "sat", ".", "</s>"].
שלב 2: ספירת N-grams
ברגע שיש לנו רשימה נקייה של טוקנים לכל משפט, אנו עוברים על הקורפוס שלנו כדי לקבל את הספירות. מבנה הנתונים הטוב ביותר לכך הוא מילון או hash map, כאשר המפתחות הם ה-N-grams (מיוצגים כטאפלים) והערכים הם התדירויות שלהם.
עבור מודל ביגרם, נצטרך שני מילונים:
unigram_counts: מאחסן את התדירות של כל מילה בודדת.bigram_counts: מאחסן את התדירות של כל רצף של שתי מילים.
תעברו בלולאה על המשפטים שעברו טוקניזציה. עבור משפט כמו ["<s>", "the", "cat", "sat", "</s>"], תצטרכו:
- להגדיל את הספירה עבור יוניגרמים: "<s>", "the", "cat", "sat", "</s>".
- להגדיל את הספירה עבור ביגרמים: ("<s>", "the"), ("the", "cat"), ("cat", "sat"), ("sat", "</s>").
שלב 3: חישוב הסתברויות
כאשר מילוני הספירה שלנו מלאים, אנו יכולים כעת לבנות את מודל ההסתברות. אנו יכולים לאחסן את ההסתברויות הללו במילון אחר או לחשב אותן בזמן אמת.
כדי לחשב את P(word₂ | word₁), תצטרכו לשלוף את bigram_counts[(word₁, word₂)] ואת unigram_counts[word₁] ולבצע את החילוק. פרקטיקה טובה היא לחשב מראש את כל ההסתברויות האפשריות ולאחסן אותן לשליפה מהירה.
שלב 4: יצירת טקסט (יישום מהנה)
דרך מצוינת לבדוק את המודל שלכם היא לגרום לו ליצור טקסט חדש. התהליך עובד כך:
- התחילו עם הקשר ראשוני, למשל, טוקן ההתחלה <s>.
- חפשו את כל הביגרמים שמתחילים ב-<s> ואת ההסתברויות המשויכות להם.
- בחרו באקראי את המילה הבאה על סמך התפלגות הסתברות זו (למילים עם הסתברויות גבוהות יותר יש סיכוי גבוה יותר להיבחר).
- עדכנו את ההקשר שלכם. המילה שנבחרה הופכת לחלק הראשון של הביגרם הבא.
- חזרו על תהליך זה עד שאתם יוצרים טוקן סיום </s> או מגיעים לאורך הרצוי.
הטקסט שנוצר על ידי מודל N-gram פשוט עשוי לא להיות קוהרנטי לחלוטין, אך הוא יפיק לעתים קרובות משפטים קצרים סבירים מבחינה דקדוקית, מה שמדגים שהוא למד קשרים בסיסיים בין מילה למילה.
אתגר הדלילות והפתרון: החלקה
מה קורה אם המודל שלנו נתקל בביגרם במהלך הבדיקה שהוא מעולם לא ראה במהלך האימון? לדוגמה, אם קורפוס האימון שלנו מעולם לא הכיל את הביטוי "the purple dog", אז:
Count("the", "purple") = 0
המשמעות היא ש-P("purple" | "the") יהיה 0. אם ביגרם זה הוא חלק ממשפט ארוך יותר שאנו מנסים להעריך, ההסתברות של המשפט כולו תהפוך לאפס, מכיוון שאנו מכפילים את כל ההסתברויות יחד. זוהי בעיית ההסתברות-אפס, ביטוי של דלילות נתונים. זה לא ריאלי להניח שקורפוס האימון שלנו מכיל כל צירוף מילים חוקי אפשרי.
הפתרון לכך הוא החלקה. הרעיון המרכזי של החלקה הוא לקחת כמות קטנה של מסת הסתברות מה-N-grams שראינו ולהפיץ אותה ל-N-grams שמעולם לא ראינו. זה מבטיח שלאף רצף מילים לא תהיה הסתברות של אפס בדיוק.
החלקת לפלס (הוסף-אחד)
טכניקת ההחלקה הפשוטה ביותר היא החלקת לפלס, הידועה גם כהחלקת הוסף-אחד. הרעיון אינטואיטיבי להפליא: העמידו פנים שראינו כל N-gram אפשרי פעם אחת יותר ממה שראינו בפועל.
נוסחת ההסתברות משתנה מעט. אנו מוסיפים 1 לספירה של המונה. כדי להבטיח שההסתברויות עדיין יסתכמו ל-1, אנו מוסיפים את גודל אוצר המילים כולו (V) למכנה.
P_laplace(wᵢ | wᵢ₋₁) = (Count(wᵢ₋₁, wᵢ) + 1) / (Count(wᵢ₋₁) + V)
- יתרונות: פשוט מאוד למימוש ומבטיח אפס הסתברויות אפסיות.
- חסרונות: לעתים קרובות הוא נותן יותר מדי הסתברות לאירועים שלא נראו, במיוחד עם אוצר מילים גדול. מסיבה זו, ביצועיו לרוב גרועים בפועל בהשוואה לשיטות מתקדמות יותר.
החלקת הוסף-k
שיפור קל הוא החלקת הוסף-k, שבה במקום להוסיף 1, אנו מוסיפים ערך שברי קטן 'k' (למשל, 0.01). זה ממתן את ההשפעה של הקצאה מחדש של יותר מדי מסת הסתברות.
P_add_k(wᵢ | wᵢ₋₁) = (Count(wᵢ₋₁, wᵢ) + k) / (Count(wᵢ₋₁) + k*V)
אף על פי שזה טוב יותר מהוסף-אחד, מציאת ה-'k' האופטימלי יכולה להיות אתגר. קיימות טכניקות מתקדמות יותר כמו Good-Turing smoothing ו-Kneser-Ney smoothing והן סטנדרטיות בערכות כלים רבות של NLP, ומציעות דרכים מתוחכמות הרבה יותר להעריך את ההסתברות של אירועים שלא נראו.
הערכת מודל שפה: פרפלקסיות
איך נדע אם מודל ה-N-gram שלנו טוב? או אם מודל טריגרם טוב יותר ממודל ביגרם למשימה הספציפית שלנו? אנו זקוקים למדד כמותי להערכה. המדד הנפוץ ביותר עבור מודלי שפה הוא פרפלקסיות (perplexity).
פרפלקסיות היא מדד לאיכות החיזוי של מודל הסתברותי על דגימה. באופן אינטואיטיבי, ניתן לחשוב עליה כעל מקדם ההסתעפות הממוצע המשוקלל של המודל. אם למודל יש פרפלקסיות של 50, זה אומר שבכל מילה, המודל מבולבל כאילו היה צריך לבחור באופן אחיד ועצמאי מבין 50 מילים שונות.
ציון פרפלקסיות נמוך יותר הוא טוב יותר, מכיוון שהוא מציין שהמודל פחות "מופתע" מנתוני המבחן ומקצה הסתברויות גבוהות יותר לרצפים שהוא אכן רואה.
פרפלקסיות מחושבת כהסתברות ההפוכה של סט המבחן, מנורמלת לפי מספר המילים. היא מוצגת לעתים קרובות בצורתה הלוגריתמית לחישוב קל יותר. מודל עם כוח חיזוי טוב יקצה הסתברויות גבוהות למשפטי המבחן, מה שיביא לפרפלקסיות נמוכה.
מגבלות של מודלי N-gram
למרות חשיבותם הבסיסית, למודלי N-gram יש מגבלות משמעותיות שהובילו את תחום ה-NLP לעבר ארכיטקטורות מורכבות יותר:
- דלילות נתונים: גם עם החלקה, עבור N גדול יותר (טריגרמים, 4-גרמים וכו'), מספר צירופי המילים האפשריים מתפוצץ. נהיה בלתי אפשרי שיהיו מספיק נתונים כדי להעריך באופן מהימן הסתברויות עבור רובם.
- אחסון: המודל מורכב מכל ספירות ה-N-gram. ככל שאוצר המילים ו-N גדלים, הזיכרון הנדרש לאחסון ספירות אלה יכול להפוך לעצום.
- חוסר יכולת ללכוד תלויות ארוכות-טווח: זהו הפגם הקריטי ביותר שלהם. למודל N-gram יש זיכרון מוגבל מאוד. מודל טריגרם, למשל, אינו יכול לקשר מילה למילה אחרת שהופיעה יותר משני מיקומים לפניה. חשבו על המשפט הזה: "הסופר, שכתב מספר רומנים רבי-מכר וחי במשך עשרות שנים בעיירה קטנה במדינה נידחת, דובר ___ שוטפת." מודל טריגרם המנסה לחזות את המילה האחרונה רואה רק את ההקשר "דובר שוטפת". אין לו ידע על המילה "הסופר" או המיקום, שהם רמזים חיוניים. הוא אינו יכול ללכוד את הקשר הסמנטי בין מילים מרוחקות.
מעבר ל-N-grams: שחר מודלי השפה הנוירוניים
מגבלות אלו, ובמיוחד חוסר היכולת להתמודד עם תלויות ארוכות-טווח, סללו את הדרך לפיתוח מודלי שפה נוירוניים. ארכיטקטורות כמו רשתות נוירונים רקורנטיות (RNNs), רשתות זיכרון ארוך קצר-טווח (LSTMs), ובמיוחד הטרנספורמרים הדומיננטיים כיום (המהווים בסיס למודלים כמו BERT ו-GPT) תוכננו כדי להתגבר על בעיות ספציפיות אלו.
במקום להסתמך על ספירות דלילות, מודלים נוירוניים לומדים ייצוגים וקטוריים צפופים של מילים (embeddings) הלוכדים קשרים סמנטיים. הם משתמשים במנגנוני זיכרון פנימיים כדי לעקוב אחר הקשר על פני רצפים ארוכים הרבה יותר, מה שמאפשר להם להבין את התלויות המורכבות וארוכות הטווח הטמונות בשפה האנושית.
סיכום: עמוד תווך יסודי ב-NLP
בעוד שה-NLP המודרני נשלט על ידי רשתות נוירונים רחבות היקף, מודל ה-N-gram נותר כלי חינוכי הכרחי ובסיס יעיל באופן מפתיע למשימות רבות. הוא מספק מבוא ברור, פרשני ויעיל מבחינה חישובית לאתגר הליבה של מידול שפה: שימוש בדפוסים סטטיסטיים מהעבר כדי לחזות את העתיד.
על ידי בניית מודל N-gram מאפס, אתם מרוויחים הבנה עמוקה, מעקרונות ראשונים, של הסתברות, דלילות נתונים, החלקה והערכה בהקשר של NLP. ידע זה אינו רק היסטורי; הוא סלע היסוד הרעיוני שעליו בנויים גורדי השחקים המתנשאים של הבינה המלאכותית המודרנית. הוא מלמד אתכם לחשוב על שפה כרצף של הסתברויות – פרספקטיבה שהיא חיונית לשליטה בכל מודל שפה, לא משנה כמה הוא מורכב.