Image

Smart Fresh Bear

climb a mountain


Image

הרפתקת קוד טעימה

October 21, 2022 posted by Aviad Shalom

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

אבל כמו ישראלי טוב, ברגע שהיה לי מינימום מידע לפתירת בעיית הml הראשונה שלי - התחלתי.

אז מה הבעיה?

הבעיה שהוצעה לי לפתור ע״י ידידתי, מתכנתת אחרת שחובבת UI ובישולים, היא כזאת:
בהינתן URL של אתר מתכונים, תחזיר לי JSON עם מצרכים בלבד והוראות הכנה בלבד.

למה? כי ידידתי בנתה אפליקציית ״ספר מתכונים״ מאוד יפה, אבל, היה ממש נחמד אם הייתה לה יכולת לעשות לייבא (import) בהינתן URL. לכן היא רצתה שאחשוף לה API שיעזור לה עם זה.

אז על מה אני אכתוב פה?

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


הכלים והיכולות שהיו לי באותו זמן:


  • סיימתי 3 קורסים בקורסרה מסדרת Deep Learning של Andrew NG.
  • פייטון ברמה סבירה.
  • יכולתי לתפקד כאיש DEVOPS בשקל וחצי, לא ארחיב יותר בפוסט הזה, רק אוסיף לסקרני הסטאק השירות שלי רץ על AmazonLightSail ושהשתמשתי בשני סרבסים שכתובים בפייטון אחד עם פלטפורמת Django והשני עם Flask והשתמשתי בApache server כWebserver.
  • וכמובן כמה שנים טובות של ניסיון בBack End.


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

bllaaaahhha

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

שילוב של הבעיות הבאות סיפקו לי היוריסטיקה התחלתית לפתרון הבעיה:

  • בעיית spam classifier.
  • בעיית זיהוי חתולים קלאסית.
  • מתודולוגיית חלון רץ.

כאשר:

  • בעיית spam classifier שנלמדה בקורס הראשון מראה איך עושים וקטוריזציה לטקסט (להפוך טקסט לרשימת מספרים סדורה).
  • ב-״בעיית זיהוי חתולים קלאסית״ =: בהינתן תמונה כלשהי, החזר ״כן״ אם זה חתול או ״לא״ אם זה לא. הבעיה היא בעית דגל שכמעט כל קורס או Tutorial בתחום הרשת נוירונים אוהב לקחת ולהראות כמה חזקה יכולה להיות רשת נוירונים עמוקה גם עם כמה אלפי דוגמאות אימון(!), במקרה שלי זה סיפק לי את blackbox שאני צריך בשביל ה"מתודולגיית חלון" רץ (הסעיף הבא). לא השתמשתי באף תשתית מוכרת(TensorFlow,SkLearn etc), לקחתי את הקוד מהקורס Improving Deep Neural Networks: Hyperparameter Tuning, Regularization and Optimization״ והתאמתי אותו לצרכים שלי.
  • מתודולגיית חלון רץ - זו בעצם שיטה להשתמש במכונה (אותו blackbox שהזכרתי) שיודעת לעבד קלט בגודל קטן בשביל לעבור על קלט גדול בחלקים ולאחר מכן לשלב את הפלטים בשביל להסיק משהו על הקלט הגדול. טוב פה אני מניח שדוגמא ויזואלית יכולה מאוד לעזור. running window
    דמיינו את הריבוע הקטן זז לאורך התמונה ולאט לאט יוצר מיפוי של אזורים בהם יש תמרור ואיזורים שבהם אין, זה דוגמא לחלון רץ
    נניח וקיבלנו ב״מתנה״ מודל שיודע לזהות תמרור בודד, נקרא לו ״בנימין״, והוא מצפה לקלט *רק* של 20x20 פיקסלים. עכשיו מישהו מבקש ממכם לממש מערכת זיהוי תמרורים בתמונה מרובת אובייקטים, משמע ״בהינתן תמונה של רחוב עם כביש וכל מיני אובייקטים, תחזיר לי את כל האזורים בהם יש תמרורים״ איך הייתם ממשים את זה? בפתרון ה״נאיבי״ הייתם מבקשים מבנימין לסרוק בחלקים את התמונה וכל פעם שהייתם נתקלים בתמרור הייתם מוסיפים את זה לאוסף התמרורים שנמצאו עד כה. (כמובן יש פה מורכבות בנוגע לאיחוד הממצאים, לכן ״נאיבי״). אם הצלחתם להבין מהי מתודולגיית חלון רץ על רחוב עם תמרורים עכשיו תנסו להשליך את זה על אתר עם המון טקסט ו״מרכיבים״ ו״הוראות הכנה״. באמצעות חלונית בגודל קבוע נעבור על האתר ונפעיל את המודל שאימנו עליה, כל פעם נוריד שורה מלמעלה ונוסיף שורה מלמטה - זה לפחות היה הרעיון הראשוני.


טקסט ל-וקטור

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

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

הטבלה נראת ככה:



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

זכרתי את *רוב* החלקים החשובים של להפוך טקסט לוקטור והם מהקורס Intro To Machine Learning:

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

אבל מאיפה המילים הללו מגיעות בכלל? המילים שיקבלו ייצוג בינארי בוקטור הם למעשה k המילים הכי נפוצות בדוגמאות (עשיתי ספירה נפרדת ל״הוראות הכנה״ וספירה נפרדת ל״מרכיבים״, כיוון שזה שני מודלים שונים)

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

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

פה אני אציין ואודה לHebrew NLP שחושפים REST API נחמד שמאפשר להמיר מילה לשורש שלה ועוד הרבה דברים אחרים בתחום הNLP שאני עדיין לא יודע.

אז ״להמליח את השניצל ולחמם״ יתן לנו את ״מלח״, ״חמם״

״להיזהר לא לשים יותר מדי מלח״ ייתן לנו ״זהר״, ״מלח״

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


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

הבדיקה הראשונית שלי גרמה לי להיות סופר אופטימי, כשבדקתי את ה-classifier קיבלתי דיוק לא רע של סביב ה80% על test set, ויחסית ל-600 דוגמאות הרגשתי שאני בכיוון טוב - אז אספנו עוד דוגמאות, ולאט לאט הגענו לדיוק בtest set של 97%!



כמה אתגרים שקפצו ואיך הם נפתרו

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

ציפיתי שהוא ייתן לי רצף תשובות באופן הבא(ירוק- אזור רלווטי לפי המודל, אדום - אזור - לא רלוונטי לפי המודל):

Image Image

אבל! קיבלתי משהו כזה:


Image Image
פה אפשר לראות שהמודל תייג יותר מידי דברים כחלק מהמתכון, ואני לא מאשים אותו, הפיסות טקסט שהכניס היו בהחלט מאוד ״מתכוניות״

לבעיה מעלה מצאתי שתי סיבות עיקריות:

1- קלאסיפייר לא היה מדויק מספיק, בהיבט של ״מה כן מתכון״ הוא היה ״נדיב״ בכך שסיווג פסקאות כמתכון מתי שהם לא. במילים אחרות היו לי הרבה false positives

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

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

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

 איך?

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

  • כמו ירידות שורות ביחס לאורך הטקסט
  • כמה ספרות (digits) יש בטקסט? כמה מאורך 1,2,3,4 (תחשבו על מספרים המייצגים מידות לעומת שנים)
  • כמה מקפים ״-״ יש?
  • כמה סימני קריאה (פחות סביר שיהיה הרבה סימני קריאה בטקסט שמתאר פעולה והם יותר יאפיינו אזורים כמו אזורי התגובות - ושוב גם אם לא, אני רוצה לסמוך על הרשת שתבין את הקשר לבעיה בעצמה.
  • כמה סימני שאלה? וכו׳

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

פה היו לי כמה כיוונים:

  • בגלל שהשתמשתי בhtml2text, ספרייה שממירה html לטקסט, אני למעשה איבדתי מידע שיכולתי להשתמש בו ולהפוך את המודל למדוייק יותר, בהינתן זה, אופציה אחת הייתה לעשות backtracking לא קטן בתהליך הפיתוח שלי ולאסוף את הדוגמאות מחדש עם תגי ה-html המיוחסים להם ולאמן את המודל עם מידע הנוסף הזה (כמובן שיש פה מורכבות של ״איך״ שאני לא נכנס אלי פה בכוונה - כי לא הלכתי על זה בסוף).
  • כיוון שני זה לתת לאלגוריתם רמז ״עבה״ מאד של איפה כדאי לו להתחיל לחפש. זה כמובן יהפוך את הפתרון לקצת יותר אלגוריתמי וקצת פחות מבוסס deep learning. אבל לפחות מהטעימה הקטנה שלי בתעשייה הדברים האלה קוראים הרבה.

איך זה יעבוד?

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

איך זה מומש?

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

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

אחרי הרפורמות שהזכרתי למעלה 90 אחוז מהאתרים ״תוכלסו״ בהצלחה.


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


דיאגרמה קטנה שעושה קצת סדר

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

אפילוג:

בניתי סביב הAPI הזה אתר פשוט ואפליקציה.

האתר הפשוט, הוא פשוט UI עם שדה טקסט שאפשר להעתיק-להדביק לשם URLים.

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

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

יותר ממוזמנים להכנס ולשחק עם זה בגרסת ה web או אם יש לכם android כלשהו תוכלו ישר לשתף את מתכון חופר עם האפליקציה וישר לקבל את התכלוס

מה הלאה?

1- להשתמש בספריית NN אמיתית ולא שיעורי בית שיש לי מקורסרה(למרות שזה עדיין מדהים אותי שכל הדבר הזה עובד!)

2- לראות שיש לי גרסה יציבה בארץ עם 100-200 משתמשים מרוצים.

3- לעבור לאנגלית בשביל לתפוס קהל גדול יותר.

4- לתת ערך נוסף לאפליקציה ע״י שינוי המרכיבים (ישרת, צמחונים, טבעונים, אנשים עם אלרגיות וכל דייטה מסויימת)

4- לנהל משתמשים.


רציתי גם להגיד תודה

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