TensorFlow.js: יצירת "מכונת הוראה אישית" באמצעות למידת העברה עם TensorFlow.js

1. לפני שמתחילים

השימוש במודל TensorFlow.js גדל באופן אקספוננציאלי בשנים האחרונות, ומפתחי JavaScript רבים רוצים עכשיו להשתמש במודלים חדשניים קיימים ולאמן אותם מחדש כדי שיעבדו עם נתונים מותאמים אישית הייחודיים לתעשייה שלהם. הפעולה של שימוש במודל קיים (שנקראת בדרך כלל מודל בסיס) והשימוש בו בדומיין דומה אך שונה, נקראת 'למידה העברה'.

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

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

דרישות מוקדמות

ה-Codelab הזה נכתב למפתחי אתרים שמכירים במידה מסוימת את המודלים המוכנים מראש של TensorFlow.js ואת השימוש הבסיסי ב-API, ורוצים להתחיל ללמוד בהעברה ב-inTensorFlow.js.

  • בשיעור ה-Lab הזה צפויה היכרות בסיסית עם TensorFlow.js, HTML5, CSS ו-JavaScript.

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

מה תלמדו

  • מהו TensorFlow.js ולמה כדאי להשתמש בו באפליקציית האינטרנט הבאה שלך.
  • איך יוצרים דף אינטרנט פשוט יותר ב-HTML/CSS /JS, משכפל את חוויית המשתמש ב-Teachable Machine.
  • איך משתמשים ב-TensorFlow.js כדי לטעון מודל בסיס שעבר אימון מראש, במיוחד ב-MobileNet, כדי ליצור תכונות של תמונות שאפשר להשתמש בהן בלמידת העברה.
  • איך לאסוף נתונים ממצלמת אינטרנט של משתמש עבור כמה סיווגים של נתונים שאתם רוצים לזהות.
  • איך ליצור ולהגדיר פרספקטיבה רב-שכבתית שלוקחת את תכונות התמונה ולומדת לסווג אובייקטים חדשים באמצעותן.

בואו נתחיל לפרוץ...

מה צריך להכין

  • עדיף לעקוב אחר חשבון Glitch.com, או להשתמש בסביבת מילוי בקשות באינטרנט שנוח לך לערוך ולהפעיל אותה בעצמך.

2. מה זה TensorFlow.js?

54e81d02971f53e8.png

TensorFlow.js היא ספריית למידת מכונה בקוד פתוח שיכולה לרוץ בכל מקום שבו JavaScript יכול. הוא מבוסס על ספריית TensorFlow המקורית שנכתבה ב-Python, ומטרתו ליצור מחדש את חוויית הפיתוח הזו ואת קבוצת ממשקי ה-API לסביבה העסקית של JavaScript.

איפה אפשר להשתמש בהן?

בגלל הניידות של JavaScript, עכשיו אפשר לכתוב בשפה אחת ולבצע למידת מכונה בקלות בכל הפלטפורמות הבאות:

  • צד הלקוח בדפדפן האינטרנט שמשתמש ב-JavaScript וניל
  • בצד השרת ואפילו מכשירים של IoT כמו Raspberry Pi שמשתמשים ב-Node.js
  • אפליקציות למחשב באמצעות Electron
  • אפליקציות נייטיב שמשתמשות ב-React Native

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

  • הפעלת WebGL בכרטיס הגרפי של המכשיר (GPU) – זו הדרך המהירה ביותר להפעיל מודלים גדולים יותר (בגודל של יותר מ-3MB) בעזרת האצת GPU.
  • הפעלת Web Assembly (WASM) במעבד (CPU) – כדי לשפר את הביצועים של המעבד (CPU) בכל המכשירים, כולל, לדוגמה, טלפונים ניידים מדור קודם. הדבר מתאים יותר לדגמים קטנים יותר (פחות מ-3MB) שבפועל יכולים לפעול מהר יותר במעבד עם WASM מאשר ב-WebGL עקב התקורה של העלאת תוכן למעבד גרפי.
  • הרצת המעבד (CPU) – החלופה לא אמורה להיות זמינה אף אחת מהסביבות האחרות. זאת האפשרות האיטית ביותר מבין השלושה, אבל היא תמיד כאן בשבילך.

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

כוחות-על בצד הלקוח

הרצה של TensorFlow.js בדפדפן האינטרנט במחשב הלקוח יכולה להוביל לכמה יתרונות שכדאי לשקול.

פרטיות

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

מהירות

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

פוטנציאל חשיפה והיקף חשיפה

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

עלות

אין שרתים, כך שהדבר היחיד שאתם צריכים לשלם עליו הוא CDN לאירוח קובצי HTML, CSS, JS ומודל. העלות של CDN היא הרבה יותר זולה מאשר החזקה של שרת (יכול להיות עם כרטיס גרפי מצורף) שפועל 24/7.

תכונות בצד השרת

מינוף ההטמעה של Node.js של TensorFlow.js מאפשר את התכונות הבאות.

תמיכה מלאה ב-CUDA

בצד השרת, כדי לשפר את המהירות של כרטיסי מסך, צריך להתקין את מנהלי ההתקנים של NVIDIA CUDA כדי לאפשר ל-TensorFlow לעבוד עם כרטיס המסך (בניגוד לדפדפן שמשתמש ב-WebGL - לא נדרשת התקנה). אבל עם תמיכה מלאה ב-CUDA, אפשר לנצל את היכולות הנמוכות יותר של הכרטיס הגרפי וכך לקצר את זמני האימון והסקת המסקנות. הביצועים דומים להטמעה של TensorFlow ב-Python, כי לשניהם יש אותו קצה עורפי של C++.

גודל הדגם

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

IOT

טכנולוגיית Node.js נתמכת במחשבים פופולריים עם לוח יחיד, כמו Raspberry Pi, וכתוצאה מכך אפשר להפעיל מודלים של TensorFlow.js גם במכשירים כאלה.

מהירות

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

עכשיו הבנת את היסודות של TensorFlow.js, איפה הוא יכול לפעול, וחלק מהיתרונות שלו, שנתחיל בו ולעשות איתו דברים מועילים!

3. העברת הלמידה

מה זה בעצם למידה בהעברה?

למידה העברה כוללת למידה של ידע שכבר למדו כדי ללמוד דבר שונה אבל דומה.

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

e28070392cd4afb9.png

בהתאם למקום שבו אתם נמצאים בעולם, יכול להיות שלא ראיתם בעבר עץ מהסוג הזה.

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

d9073a0d5df27222.png

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

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

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

באנימציה הזו, אפשר לראות את מספר השכבות העצום במודל MobileNet V1 הזה:

7d4e1e35c1a89715.gif

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

בואו נבחן ארכיטקטורה מסורתית של רשת עצבית מתקפלת (CNN) (בדומה ל-MobileNet) ונראה איך למידת העברה יכולה למנף את הרשת המאומנת הזו כדי ללמוד משהו חדש. התמונה הבאה מציגה את ארכיטקטורת המודל הטיפוסית של רשת CNN, שבמקרה הזה אומנה לזהות ספרות בכתב יד בין 0 ל-9:

baf4e3d434576106.png

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

369a8a9041c6917d.png

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

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

עכשיו תוכלו להוסיף כותרת סיווג חדשה שמנסה לחזות במקום זאת את a, b או c, כפי שמוצג בדוגמה הבאה:

db97e5e60ae73bbd.png

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

הפעולה הזו נקראת למידה בהעברה, והיא מה שעושים במסגרת Teachable Machine מאחורי הקלעים.

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

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

4. TensorFlow Hub – דגמים בסיסיים

חיפוש דגם בסיס מתאים לשימוש

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

c5dc1420c6238c14.png

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

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

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

הדבר הבא שצריך לבדוק הוא לבחור מודל בסיס נתון שמתעניינים בפורמט של TensorFlow.js. אם פותחים את הדף של אחד מהמודלים האלה של MobileNet v3 של התכונה, אפשר לראות בתיעוד של JS שהוא בצורת מודל תרשים על סמך קטע הקוד לדוגמה בתיעוד שבו נעשה שימוש ב-tf.loadGraphModel().

f97d903d2e46924b.png

חשוב גם לציין שאם מודל נמצא בפורמט השכבות במקום בפורמט התרשים, אתם יכולים לבחור אילו שכבות להקפיא ואילו לבטל את ההקפאה לצורך אימון. המודל הזה יכול להיות מאוד חזק כשיוצרים מודל למשימה חדשה, שנקראה בדרך כלל 'מודל ההעברה'. אבל בינתיים תשתמשו בסוג המודל של תרשים ברירת המחדל במדריך הזה, שעליו פרוסים רוב המודלים של TF Hub. למידע נוסף על עבודה עם מודלים של 'שכבות', מומלץ לעיין בקורס zero to hero TensorFlow.js.

היתרונות של למידת העברה

מהם היתרונות של שימוש בלמידת העברה במקום לאמן את כל ארכיטקטורת המודל מההתחלה?

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

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

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

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

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

5. הגדרת קוד

מה צריך להכין

  • דפדפן אינטרנט מודרני.
  • ידע בסיסי ב-HTML, ב-CSS, ב-JavaScript ובכלי הפיתוח ל-Chrome (צפייה בפלט במסוף).

שנתחיל בתכנות?

תבניות Boilerplate שאפשר להתחיל מהן נוצרו עבור Glitch.com או Codepen.io. תוכלו פשוט לשכפל כל אחת מהתבניות כמצב הבסיס ל-Code Lab הזה, בלחיצה אחת.

ב-Glitch, לוחצים על הלחצן remix this כדי לפצל את הקובץ וליצור קבוצת קבצים חדשה שאפשר לערוך.

לחלופין, ב-Codepen, לוחצים על fork" בפינה השמאלית התחתונה של המסך.

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

  • דף HTML (index.html)
  • גיליון סגנונות (style.css)
  • קובץ לכתיבת קוד JavaScript (script.js)

לנוחותכם, יש ייבוא נוסף בקובץ ה-HTML של ספריית TensorFlow.js. כך הוא נראה:

index.html

<!-- Import TensorFlow.js library -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs/dist/tf.min.js" type="text/javascript"></script>

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

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

6. תבנית סטנדרטית ל-HTML של אפליקציה

איפה מתחילים?

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

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

כדי להגדיר את התכונות שלמעלה, פותחים את index.html ומדביקים מעל הקוד הקיים את הקוד הבא:

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Transfer Learning - TensorFlow.js</title>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- Import the webpage's stylesheet -->
    <link rel="stylesheet" href="/style.css">
  </head>  
  <body>
    <h1>Make your own "Teachable Machine" using Transfer Learning with MobileNet v3 in TensorFlow.js using saved graph model from TFHub.</h1>
    
    <p id="status">Awaiting TF.js load</p>
    
    <video id="webcam" autoplay muted></video>
    
    <button id="enableCam">Enable Webcam</button>
    <button class="dataCollector" data-1hot="0" data-name="Class 1">Gather Class 1 Data</button>
    <button class="dataCollector" data-1hot="1" data-name="Class 2">Gather Class 2 Data</button>
    <button id="train">Train &amp; Predict!</button>
    <button id="reset">Reset</button>

    <!-- Import TensorFlow.js library -->
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@3.11.0/dist/tf.min.js" type="text/javascript"></script>

    <!-- Import the page's JavaScript to do some stuff -->
    <script type="module" src="/script.js"></script>
  </body>
</html>

פירוט הנתונים

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

  • הוספתם תג <h1> לכותרת הדף יחד עם תג <p> עם המזהה 'סטטוס', שבו מדפיסים מידע, תוך שימוש בחלקים שונים של המערכת כדי לצפות בפלט.
  • הוספת רכיב <video> עם המזהה 'מצלמת אינטרנט', שבו תעבד את הסטרימינג ממצלמת האינטרנט מאוחר יותר.
  • הוספת 5 רכיבי <button>. הראשון, עם המזהה 'enableCam', מפעיל את המצלמה. בשני הלחצנים הבאים יש מחלקה של dataCollector, שמאפשר לאסוף תמונות לדוגמה של האובייקטים שברצונכם לזהות. הקוד שכותבים מאוחר יותר מתוכנן כך שתוכלו להוסיף לחצנים רבים ככל האפשר, והם יפעלו באופן אוטומטי כמו שצריך.

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

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

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

  • בנוסף, הוספת 2 פעולות ייבוא של <script>. אחד ל-TensorFlow.js והשני ל-script.js שתגדירו בקרוב.

7. הוסף סגנון

ברירות מחדל של רכיבים

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

style.css

body {
  font-family: helvetica, arial, sans-serif;
  margin: 2em;
}

h1 {
  font-style: italic;
  color: #FF6F00;
}


video {
  clear: both;
  display: block;
  margin: 10px;
  background: #000000;
  width: 640px;
  height: 480px;
}

button {
  padding: 10px;
  float: left;
  margin: 5px 3px 5px 10px;
}

.removed {
  display: none;
}

#status {
  font-size:150%;
}

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

81909685d7566dcb.png

8. JavaScript: קבועים ומאזינים של מפתחות

הגדרה של קבועי מפתחות

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

script.js

const STATUS = document.getElementById('status');
const VIDEO = document.getElementById('webcam');
const ENABLE_CAM_BUTTON = document.getElementById('enableCam');
const RESET_BUTTON = document.getElementById('reset');
const TRAIN_BUTTON = document.getElementById('train');
const MOBILE_NET_INPUT_WIDTH = 224;
const MOBILE_NET_INPUT_HEIGHT = 224;
const STOP_DATA_GATHER = -1;
const CLASS_NAMES = [];

הנה פירוט לגבי הסיבות האלה:

  • STATUS פשוט מכיל הפניה לתג הפסקה שאליו תכתוב עדכוני סטטוס.
  • ב-VIDEO יש הפניה לרכיב הווידאו ב-HTML שמעבד את הפיד של מצלמת האינטרנט.
  • ENABLE_CAM_BUTTON, RESET_BUTTON ו-TRAIN_BUTTON תופסים הפניות DOM לכל לחצני המקשים מדף ה-HTML.
  • MOBILE_NET_INPUT_WIDTH ו-MOBILE_NET_INPUT_HEIGHT מגדירים את רוחב הקלט והגובה הצפויים של מודל MobileNet, בהתאמה. מאחסנים את הנתונים באופן קבוע ליד החלק העליון של הקובץ באופן הזה, אם תחליטו להשתמש בגרסה אחרת מאוחר יותר, כך יהיה קל יותר לעדכן את הערכים פעם אחת במקום להחליף אותם במקומות רבים ושונים.
  • הערך של STOP_DATA_GATHER הוא 1. ההגדרה הזו שומרת ערך מצב כדי לדעת מתי המשתמש הפסיק ללחוץ על לחצן כדי לאסוף נתונים מהפיד של מצלמת האינטרנט. מתן שם בעל משמעות למספר הזה הופך את הקוד לקריא יותר בהמשך.
  • CLASS_NAMES משמש כחיפוש ומכיל את השמות הקריאים לאנשים של החיזויים למחלקות האפשריות. המערך הזה יאוכלס מאוחר יותר.

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

הוספת פונקציות event listener

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

script.js

ENABLE_CAM_BUTTON.addEventListener('click', enableCam);
TRAIN_BUTTON.addEventListener('click', trainAndPredict);
RESET_BUTTON.addEventListener('click', reset);


function enableCam() {
  // TODO: Fill this out later in the codelab!
}


function trainAndPredict() {
  // TODO: Fill this out later in the codelab!
}


function reset() {
  // TODO: Fill this out later in the codelab!
}

ENABLE_CAM_BUTTON - מפעילה את הפונקציה EnableCam כשלוחצים עליה.

TRAIN_BUTTON – קריאות ל-trainAndPredict אם לוחצים עליהן.

RESET_BUTTON - השיחות מתאפסות כשלוחצים עליהן.

לבסוף, בקטע הזה מוצגים כל הלחצנים עם סיווג 'dataCollector'. באמצעות document.querySelectorAll(). הפעולה הזו מחזירה מערך של רכיבים שנמצאו במסמך שתואמים ל:

script.js

let dataCollectorButtons = document.querySelectorAll('button.dataCollector');
for (let i = 0; i < dataCollectorButtons.length; i++) {
  dataCollectorButtons[i].addEventListener('mousedown', gatherDataForClass);
  dataCollectorButtons[i].addEventListener('mouseup', gatherDataForClass);
  // Populate the human readable names for classes.
  CLASS_NAMES.push(dataCollectorButtons[i].getAttribute('data-name'));
}


function gatherDataForClass() {
  // TODO: Fill this out later in the codelab!
}

הסבר על הקוד:

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

שני האירועים מפעילים פונקציית gatherDataForClass שתגדירו בהמשך.

בשלב הזה, אפשר גם לדחוף את שמות המחלקות הקריאים לאנשים שנמצאו מהמאפיין data-name של לחצן ה-HTML למערך CLASS_NAMES.

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

script.js

let mobilenet = undefined;
let gatherDataState = STOP_DATA_GATHER;
let videoPlaying = false;
let trainingDataInputs = [];
let trainingDataOutputs = [];
let examplesCount = [];
let predict = false;

בואו נעבור עליהן.

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

עכשיו יש משתנה בשם gatherDataState. אם נעשה שימוש ב-'dataCollector' ללחוץ על הלחצן, והשינוי הזה יהיה המזהה הראשון של הלחצן, כפי שמוגדר ב-HTML, כך שתדעו איזה סוג של נתונים אתם אוספים באותו רגע. בהתחלה הוא מוגדר ל-STOP_DATA_GATHER כדי שלולאת איסוף הנתונים שתכתוב מאוחר יותר לא תאסוף נתונים כשלא מתבצעת לחיצה על לחצנים.

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

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

לאחר מכן מוגדר מערך סופי אחד, examplesCount,, כדי לעקוב אחרי מספר הדוגמאות שכלולות בכל מחלקה אחרי שמתחילים להוסיף אותן.

לבסוף, יש משתנה בשם predict ששולט בלולאת החיזוי. הערך הראשוני הוא false. לא ניתן יהיה לבצע חיזויים עד שתגדירו את הערך true מאוחר יותר.

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

9. טעינת מודל הבסיס של MobileNet

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

script.js

/**
 * Loads the MobileNet model and warms it up so ready for use.
 **/
async function loadMobileNetFeatureModel() {
  const URL = 
    'https://tfhub.dev/google/tfjs-model/imagenet/mobilenet_v3_small_100_224/feature_vector/5/default/1';
  
  mobilenet = await tf.loadGraphModel(URL, {fromTFHub: true});
  STATUS.innerText = 'MobileNet v3 loaded successfully!';
  
  // Warm up the model by passing zeros through it once.
  tf.tidy(function () {
    let answer = mobilenet.predict(tf.zeros([1, MOBILE_NET_INPUT_HEIGHT, MOBILE_NET_INPUT_WIDTH, 3]));
    console.log(answer.shape);
  });
}

// Call the function immediately to start loading.
loadMobileNetFeatureModel();

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

לאחר מכן אפשר לטעון את המודל באמצעות await tf.loadGraphModel(). חשוב לזכור להגדיר את המאפיין המיוחד fromTFHub כ-true, כשטוענים מודל מהאתר הזה של Google. זהו מקרה מיוחד רק לגבי שימוש במודלים שמתארחים ב-TF Hub, שבו יש להגדיר את המאפיין הנוסף הזה.

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

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

אפשר להשתמש ב-tf.zeros() שעטוף בתוך tf.tidy() כדי לוודא שהכלים של Tenors מושלכים בצורה נכונה, בגודל 1 ובגודל וברוחב הנכונים שהגדרתם בקבועים בהתחלה. לבסוף, מציינים גם את ערוצי הצבעים. במקרה הזה הם 3, כי המודל מצפה לתמונות RGB.

בשלב הבא, רושמים את הצורה של ה-tensor שתתקבל שתוצג באמצעות answer.shape(), כדי לעזור לכם להבין את גודל התמונה שהמודל הזה מייצר.

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

אם אתם צופים בתצוגה המקדימה בזמן אמת כרגע, לאחר כמה רגעים תוכלו לראות שטקסט הסטטוס משתנה מ'בהמתנה לטעינת TF.js'. כדי להפוך ל-"MobileNet v3 enabled בהצלחה!" כפי שמוצג בהמשך. צריך לוודא שהאימות פועל לפני שממשיכים.

a28b734e190afff.png

אפשר גם לבדוק את הפלט של המסוף כדי לראות את הגודל המודפס של תכונות הפלט שהמודל הזה מפיק. אחרי שהרצת אפסים במודל MobileNet, תופיע צורה של [1, 1024] מודפסת. הפריט הראשון הוא גודל הקבוצה של 1, ואפשר לראות שהוא למעשה מחזיר 1,024 תכונות שאפשר להשתמש בהן כדי לסווג אובייקטים חדשים.

10. הגדרת הראש החדש של המודל

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

script.js

let model = tf.sequential();
model.add(tf.layers.dense({inputShape: [1024], units: 128, activation: 'relu'}));
model.add(tf.layers.dense({units: CLASS_NAMES.length, activation: 'softmax'}));

model.summary();

// Compile the model with the defined optimizer and specify a loss function to use.
model.compile({
  // Adam changes the learning rate over time which is useful.
  optimizer: 'adam',
  // Use the correct loss function. If 2 classes of data, must use binaryCrossentropy.
  // Else categoricalCrossentropy is used if more than 2 classes.
  loss: (CLASS_NAMES.length === 2) ? 'binaryCrossentropy': 'categoricalCrossentropy', 
  // As this is a classification problem you can record accuracy in the logs too!
  metrics: ['accuracy']  
});

בואו נבחן את הקוד. מתחילים מהגדרה של מודל tf.reCAPTCHA שאליו תוסיפו שכבות מודל.

בשלב הבא מוסיפים שכבה צפופה כשכבת הקלט למודל הזה. צורת הקלט הזו היא 1024, כי הפלט מהפיצ'רים של MobileNet v3 הוא בגודל הזה. גיליתם זאת בשלב הקודם אחרי שהעברתם נתונים דרך המודל. בשכבה הזו יש 128 נוירונים שמשתמשים בפונקציית ההפעלה של ReLU.

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

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

עכשיו מדפיסים model.summary() כדי להדפיס במסוף את הסקירה הכללית של המודל החדש שהוגדר.

לסיום, הרכב את המודל כדי שיהיה מוכן לאימון. כאן כלי האופטימיזציה מוגדר ל-adam, וההפסד יהיה binaryCrossentropy אם CLASS_NAMES.length שווה ל-2, או שייעשה שימוש ב-categoricalCrossentropy אם יש 3 מחלקות או יותר לסיווג. נדרשים גם מדדי דיוק כדי שיהיה אפשר לעקוב אחריהם ביומנים מאוחר יותר למטרות ניפוי באגים.

אתם אמורים לראות במסוף הזה משהו כזה:

22eaf32286fea4bb.png

לתשומת ליבכם: יש יותר מ-130 אלף פרמטרים שניתנים לאימון. אבל מכיוון שזו שכבה פשוטה וצפופה של נוירונים רגילים, היא תאמן די מהר.

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

11. הפעלת מצלמת האינטרנט

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

script.js

function hasGetUserMedia() {
  return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
}

function enableCam() {
  if (hasGetUserMedia()) {
    // getUsermedia parameters.
    const constraints = {
      video: true,
      width: 640, 
      height: 480 
    };

    // Activate the webcam stream.
    navigator.mediaDevices.getUserMedia(constraints).then(function(stream) {
      VIDEO.srcObject = stream;
      VIDEO.addEventListener('loadeddata', function() {
        videoPlaying = true;
        ENABLE_CAM_BUTTON.classList.add('removed');
      });
    });
  } else {
    console.warn('getUserMedia() is not supported by your browser');
  }
}

קודם כול צריך ליצור פונקציה בשם hasGetUserMedia() כדי לבדוק אם הדפדפן תומך ב-getUserMedia(). לשם כך, צריך לבדוק אם קיימים מאפייני מפתח של ממשקי API לדפדפן.

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

אם הוא תומך בכך, עליך להגדיר כמה מגבלות עבור השיחה של getUserMedia(), למשל: וידאו בסטרימינג בלבד והעדפה שwidth של הסרטון תהיה בגודל 640 פיקסלים, ו-height יהיה 480 פיקסלים. למה? ובכן, אין הרבה טעם לצלם סרטון גדול יותר מזה שיהיה צורך לשנות את גודלו ל-224 על 224 פיקסלים כדי להזין אותו במודל MobileNet. אפשר גם לחסוך חלק ממשאבי המחשוב על ידי בקשה לרזולוציה נמוכה יותר. רוב המצלמות תומכות ברזולוציה הזו.

בשלב הבא, צריך להתקשר אל navigator.mediaDevices.getUserMedia() עם נתוני constraints שצוינו למעלה, ואז להמתין עד שהשדה stream יוחזר. אחרי ש-stream מוחזר, אפשר לגרום לרכיב VIDEO להפעיל את stream על ידי הגדרתו כערך srcObject שלו.

כדאי גם להוסיף eventListener ברכיב VIDEO כדי לדעת מתי stream נטען ומושמע בהצלחה.

אחרי הפעלת ה-Steam, אפשר להגדיר את videoPlaying כ-TRUE ולהסיר את ENABLE_CAM_BUTTON כדי למנוע לחיצה חוזרת על ידי הגדרת המחלקה שלו ל-"removed".

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

b378eb1affa9b883.png

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

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

עכשיו הגיע הזמן למלא את הפונקציה הריקה הנוכחית, שנקראת gatherDataForClass().. זה מה שהקצית כפונקציית הגורם המטפל באירועים ללחצני dataCollector בתחילת ה-Codelab.

script.js

/**
 * Handle Data Gather for button mouseup/mousedown.
 **/
function gatherDataForClass() {
  let classNumber = parseInt(this.getAttribute('data-1hot'));
  gatherDataState = (gatherDataState === STOP_DATA_GATHER) ? classNumber : STOP_DATA_GATHER;
  dataGatherLoop();
}

קודם כול, בודקים את המאפיין data-1hot בלחצן הנוכחי שעליו לוחצים על ידי שליחת קריאה ל-this.getAttribute() עם שם המאפיין, במקרה הזה data-1hot בתור הפרמטר. מכיוון שזו מחרוזת, אפשר להשתמש בפונקציה parseInt() כדי להמיר אותה למספר שלם ולהקצות את התוצאה הזו למשתנה בשם classNumber.

לאחר מכן מגדירים את המשתנה gatherDataState בהתאם. אם הערך של gatherDataState הנוכחי שווה ל-STOP_DATA_GATHER (שהגדרתם להיות -1), המשמעות היא שאתם לא אוספים נתונים כרגע, אבל מדובר באירוע mousedown שהופעל. מגדירים את gatherDataState בתור classNumber שמצאתם עכשיו.

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

לסיום, מתחילים את השיחה ל-dataGatherLoop(),, שמבצעת בפועל את הקלטת נתוני הכיתה.

13. איסוף נתונים

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

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

בואו נעבור על זה:

script.js

function dataGatherLoop() {
  if (videoPlaying && gatherDataState !== STOP_DATA_GATHER) {
    let imageFeatures = tf.tidy(function() {
      let videoFrameAsTensor = tf.browser.fromPixels(VIDEO);
      let resizedTensorFrame = tf.image.resizeBilinear(videoFrameAsTensor, [MOBILE_NET_INPUT_HEIGHT, 
          MOBILE_NET_INPUT_WIDTH], true);
      let normalizedTensorFrame = resizedTensorFrame.div(255);
      return mobilenet.predict(normalizedTensorFrame.expandDims()).squeeze();
    });

    trainingDataInputs.push(imageFeatures);
    trainingDataOutputs.push(gatherDataState);
    
    // Intialize array index element if currently undefined.
    if (examplesCount[gatherDataState] === undefined) {
      examplesCount[gatherDataState] = 0;
    }
    examplesCount[gatherDataState]++;

    STATUS.innerText = '';
    for (let n = 0; n < CLASS_NAMES.length; n++) {
      STATUS.innerText += CLASS_NAMES[n] + ' data count: ' + examplesCount[n] + '. ';
    }
    window.requestAnimationFrame(dataGatherLoop);
  }
}

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

לאחר מכן, צריך לארוז את הקוד ב-tf.tidy() כדי להשליך את כל רכיבי הטנזור שנוצרו בקוד הבא. התוצאה של ביצוע הקוד tf.tidy() מאוחסנת במשתנה שנקרא imageFeatures.

עכשיו אפשר להשיג פריים ממצלמת האינטרנט VIDEO באמצעות tf.browser.fromPixels(). הטנזור שנוצר שמכיל את נתוני התמונה מאוחסן במשתנה שנקרא videoFrameAsTensor.

לאחר מכן, משנים את גודל המשתנה videoFrameAsTensor כך שיהיה בצורה הנכונה לקלט של מודל MobileNet. משתמשים בקריאה tf.image.resizeBilinear() עם הסמן שרוצים לשנות את עיצובו כפרמטר הראשון, ולאחר מכן בצורה שמגדירה את הגובה והרוחב החדשים כפי שהוגדרו על ידי הקבועים שכבר יצרתם. לסיום, מגדירים ליישר את הפינות עם הערך true על ידי העברת הפרמטר השלישי כדי למנוע בעיות בהתאמה במהלך שינוי הגודל. התוצאה של שינוי הגודל הזה שמורה במשתנה שנקרא resizedTensorFrame.

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

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

לאחר מכן, מנרמלים את נתוני התמונה. נתוני התמונה תמיד נמצאים בטווח של 0 עד 255 כשמשתמשים ב-tf.browser.frompixels(), לכן אפשר לחלק את sizedTensorFrame ב-255 כדי להבטיח שכל הערכים הם בין 0 ל-1, וזה מה שמודל MobileNet מצפה לקבל כקלט.

לסיום, בקטע tf.tidy() של הקוד, לוחצים על הארגומנט המנורמל הזה דרך המודל שנטען באמצעות קריאה ל-mobilenet.predict(), ומעבירים אליה את הגרסה המורחבת של normalizedTensorFrame באמצעות expandDims(), כך שהיא תהיה קבוצה של 1, כי המודל מצפה לקבוצה של קלט לעיבוד.

אחרי שהתוצאה חוזרת, אפשר מיד לקרוא לפונקציה squeeze() בתוצאה המוחזרת כדי לדחוס אותה בחזרה לטנזור דו-ממדי, ואז להחזיר אותו ולהקצות אותו למשתנה imageFeatures שמקליט את התוצאה מ-tf.tidy().

עכשיו, אחרי שיש לכם את ה-imageFeatures ממודל MobileNet, אפשר להקליט אותם על ידי דחיפתם למערך trainingDataInputs שהגדרתם קודם.

אפשר גם להקליט מה הקלט הזה מייצג על ידי דחיפת הערך הנוכחי של gatherDataState גם למערך trainingDataOutputs.

הערה: המשתנה gatherDataState היה מוגדר למזהה המספרי של המחלקה הנוכחית. הנתונים שמתועדים במערכת לגבי קליק על הלחצן בפונקציה gatherDataForClass() שהוגדרה בעבר.

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

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

בסוף, קוראים לפונקציה window.requestAnimationFrame() עם dataGatherLoop שמועבר כפרמטר, כדי לקרוא לפונקציה הזו שוב באופן רקורסיבי. כך תמשיך לדגום את הפריימים מהסרטון עד שיזוהה ה-mouseup של הלחצן, והפרמטר gatherDataState יוגדר לערך STOP_DATA_GATHER,. בשלב הזה תסתיים לולאת איסוף הנתונים.

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

541051644a45131f.gif

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

14. אימון וחיזוי

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

script.js

async function trainAndPredict() {
  predict = false;
  tf.util.shuffleCombo(trainingDataInputs, trainingDataOutputs);
  let outputsAsTensor = tf.tensor1d(trainingDataOutputs, 'int32');
  let oneHotOutputs = tf.oneHot(outputsAsTensor, CLASS_NAMES.length);
  let inputsAsTensor = tf.stack(trainingDataInputs);
  
  let results = await model.fit(inputsAsTensor, oneHotOutputs, {shuffle: true, batchSize: 5, epochs: 10, 
      callbacks: {onEpochEnd: logProgress} });
  
  outputsAsTensor.dispose();
  oneHotOutputs.dispose();
  inputsAsTensor.dispose();
  predict = true;
  predictLoop();
}

function logProgress(epoch, logs) {
  console.log('Data for epoch ' + epoch, logs);
}

קודם כול, כדי להפסיק את ההצגה של חיזויים נוכחיים, צריך להגדיר את predict לערך false.

בשלב הבא כדאי לערבב את מערכי הקלט והפלט באמצעות tf.util.shuffleCombo() כדי לוודא שהסדר לא גורם לבעיות באימון.

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

משתמשים בפונקציה tf.oneHot() עם המשתנה outputsAsTensor יחד עם מספר המחלקות המקסימלי לקידוד, שהוא רק CLASS_NAMES.length. הפלט היחיד המקודד חם מאוחסנים עכשיו בטנזור חדש שנקרא oneHotOutputs.

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

כדי לעשות את זה, יש פונקציה מעולה בספרייה של TensorFlow.js בשם tf.stack(),

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

בשלב הבא לוחצים על await model.fit() כדי לאמן את ראש המודל בהתאמה אישית. כאן מעבירים את המשתנה inputsAsTensor יחד עם ה-oneHotOutputs, כדי לייצג את נתוני האימון שישמשו לקלט ולפלט לדוגמה, בהתאמה. באובייקט ההגדרה של הפרמטר השלישי, מגדירים את shuffle לערך true, משתמשים ב-batchSize מתוך 5, כאשר epochs מוגדר לערך 10, ולאחר מכן מציינים callback בשביל onEpochEnd לפונקציה logProgress שתגדירו בקרוב.

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

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

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

לולאת חיזוי ליבה

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

בודקים את הקוד:

script.js

function predictLoop() {
  if (predict) {
    tf.tidy(function() {
      let videoFrameAsTensor = tf.browser.fromPixels(VIDEO).div(255);
      let resizedTensorFrame = tf.image.resizeBilinear(videoFrameAsTensor,[MOBILE_NET_INPUT_HEIGHT, 
          MOBILE_NET_INPUT_WIDTH], true);

      let imageFeatures = mobilenet.predict(resizedTensorFrame.expandDims());
      let prediction = model.predict(imageFeatures).squeeze();
      let highestIndex = prediction.argMax().arraySync();
      let predictionArray = prediction.arraySync();

      STATUS.innerText = 'Prediction: ' + CLASS_NAMES[highestIndex] + ' with ' + Math.floor(predictionArray[highestIndex] * 100) + '% confidence';
    });

    window.requestAnimationFrame(predictLoop);
  }
}

קודם כל צריך לבדוק שהערך של predict נכון, כדי שהחיזויים יבוצעו רק אחרי אימון המודל והוא יהיה זמין לשימוש.

בשלב הבא אפשר לקבל את תכונות התמונה של התמונה הנוכחית, בדיוק כמו שעשיתם בפונקציה dataGatherLoop(). למעשה, ניתן להשיג פריים ממצלמת האינטרנט באמצעות tf.browser.from pixels(), לנרמל אותה, לשנות את הגודל שלה לגודל של 224 על 224 פיקסלים, ולאחר מכן להעביר את הנתונים דרך מודל MobileNet כדי לקבל את תכונות התמונה שיתקבלו.

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

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

אפשר גם לקבל את דירוגי הסמך של החיזויים באותו אופן על ידי קריאה ישירה ל-arraySync() במשתנה prediction.

עכשיו יש לך את כל מה שצריך כדי לעדכן את הטקסט בSTATUS בנתונים של prediction. כדי לקבל את המחרוזת הקריאה לבני אדם עבור הכיתה, אתם יכולים לחפש את highestIndex במערך CLASS_NAMES, ולאחר מכן לשלוף את ערך הסמך מה-predictionArray. כדי להפוך אותו לקריא יותר באחוזים, פשוט מכפילים ב-100 ו-math.floor() את התוצאה.

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

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

15. הטמעת לחצן האיפוס

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

script.js

/**
 * Purge data and start over. Note this does not dispose of the loaded 
 * MobileNet model and MLP head tensors as you will need to reuse 
 * them to train a new model.
 **/
function reset() {
  predict = false;
  examplesCount.length = 0;
  for (let i = 0; i < trainingDataInputs.length; i++) {
    trainingDataInputs[i].dispose();
  }
  trainingDataInputs.length = 0;
  trainingDataOutputs.length = 0;
  STATUS.innerText = 'No data collected';
  
  console.log('Tensors in memory: ' + tf.memory().numTensors);
}

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

עכשיו צריך לעבור על כל נתוני trainingDataInputs שהוקלטו כרגע ולוודא שdispose() מכל טנזור שכלול בו כדי לפנות שוב זיכרון, כי קולט האשפה של JavaScript לא מוחק את Tensors.

לאחר מכן, אפשר להגדיר בבטחה את אורך המערך ל-0 גם במערכים של trainingDataInputs וגם במערכים של trainingDataOutputs כדי למחוק גם אותם.

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

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

16. אני רוצה לנסות

הגיע הזמן להתנסות בגרסה משלכם של Teachable Machine!

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

bf1ac3cc5b15740.gif

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

17. מזל טוב

מעולה! השלמת את הדוגמה הראשונה של הלמידה הראשונה להעברה באמצעות TensorFlow.js בשידור חי בדפדפן.

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

סיכום

ב-Codelab הזה למדת:

  1. מהי למידה מסוג העברה ומה היתרונות שלה על פני אימון מודל מלא.
  2. איך לקבל מודלים לשימוש חוזר מ-TensorFlow Hub.
  3. איך מגדירים אפליקציית אינטרנט שמתאימה ללמידה בהעברה.
  4. איך לטעון מודל בסיס ולהשתמש בו כדי ליצור תכונות של תמונות.
  5. איך לאמן ראש חיזוי חדש שיכול לזהות אובייקטים מותאמים אישית מתמונות במצלמת אינטרנט.
  6. איך להשתמש במודלים שמתקבלים כדי לסווג נתונים בזמן אמת.

מה השלב הבא?

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

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

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

משתפים את היצירות שלכם איתנו

אתם יכולים להרחיב בקלות את מה שיצרתם היום גם לתרחישים יצירתיים אחרים, ואנחנו ממליצים לכם לחשוב מחוץ למסגרת ולהמשיך להאקינג.

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

אתרים שאפשר לשלם עליהם