צייר לי 4 – חלק שני: האם זה נקניקייה?

האם זה נקניקייה? – בעשור השני של המאה העשרים ואחד עלתה קרנה של למידת המכונה ואלגוריתמי סיווג היו בין האלגוריתם הפשוטים למימוש ותוכנית הטלוויזיה סיליקון וואלי הביאה אותם אל המסך הקטן. בסצנה בלתי נשכחת מדגים המפתח אבטיפוס של אפליקציה שמזהה אוכל בעזרת מצלמת המכשיר הנייד מצליח לזהות נקניקייה. עדות דיגיטלית לכך ניתן למצוא בארכיון הווידאו ששרד מהתקופה.
מדריך ההייטקיסט לגלקסיה, מהדורה 7.983, 3021
אז איך בונים אלגוריתם שמזהה האם ציור מכיל את הספרה 4 או לא? לאחר שבפוסט הקודם עברנו על החלקים העיקריים של האלגוריתם אומנות גנרטיבי בסיסי – הצייר והאלגוריתם האבולוציוני כעת נשאר לעבור על החלק החסר והמעניין (בו גם נשתמש בלמידת מכונה) אלגוריתם הזיהוי כאשר הקלט של האלגוריתם הוא תמונה שחור לבן בגודל של 28 על 28 פיקסלים והפלט של האלגוריתם זה מספר בין 0 ל1 המייצג עד כמה האלגוריתם חושב שהציור דומה ל4.
בניית המודל
כאשר מפתחים מודל בשלב הראשון צריך להביא לו הרבה מקרים עבורם אנחנו כבר יודעים את התוצאה כדי שהוא יוכל להתאמן עליהם לפתח ״אינטואיציה״ לפתרון הבעייה. כך לדוגמא כדי לבנות אלגוריתם לחיזוי סיכון של החזר משכנתא נאסוף פרמטרים רבים מנתוני עבר שאנחנו חושבים שיש להם קשר לבעייה לדוגמא: משכורת, מספר ילדים, שנות לימוד, מספר כלי רכב במשק הבית וכו׳ אותם נחלק ועבור כל אחד מהם נוסיף את התוצאה – האם המשכנתא הוחזרה במלואה או לא.
במקרה שלנו אנחנו מעוניינים לבנות וקטור המייצג תמונה (נשטח את ה28×28 פיקסלים לוקטור של 784) כאשר 0 מייצג שחור ו 1 מייצג לבן וכל פיקסל יכול לקבל כל מספר בדרך. לאחר מכן נאסוף הרבה דוגמאות שונות (לדוגמא 10,000) מתוכם 2,000 יהיו של ציורים של 4 והשאר יהיו ציורים של ספרות אחרות או סתם שרבוטים חסרי פשר. נחלק את הדוגמאות לשתי קבוצות קבוצת האימון וקבוצת הבדיקה. נאמן את המודל על קבוצת האימון ובעזרת קבוצת הבדיקה נעריך עד כמה המודל שלנו מצליח לזהות את הספרה 4 בעולם האמיתי.
לצורך כך אנחנו צריכים הרבה ציורים של 4 ולא של 4 מאיפה נאסוף אותם?
יצירת המדגם
למזלי בגלל שבחרתי לכתוב אלגוריתם שמזהה 4 די פשוט ליצור נתונים פשוט נצייר את הספרה 4 (ואת כל שאר התווים) בהרבה פונטים שונים ואם קצת משחק של זוויות וכך נקבל את קבוצת מדגם (dataset בלע״ז) גדולה לאימון של המודל שלנו.
אני בחרתי להשתמש באוטומציה של דפדפן בפרויקט puppeteer המשתמש ב chromedriver כך שבעזרת קצת html+css וקוד js – שדווקא רץ בNode.js מחוץ לדפדפן אני יכול לייצר במהירות את התמונות הנדרשות. ראשית יצרתי קובץ html פשוט אליו הוספתי קובץ css המכיל 1317 פונטים חינמיים של גוגל (השתמשתי בסיפרייה honeysilvas/google-fonts כדי רשימה גדולה של גוגל פונטס ולהכין את הcss). לאחר מכן כתבתי סקריפט קצר שרץ בלולאה על הפונטים והתווים השונים (וגם משחק מעט בזווית שלהם) שומר את התוצאות בקובץ תמונה הרזולוציה נמוכה של 28×28. ובסוף כדי לשפר את המדגם יצרתי כמה אלפי תמונות שהם סתם שירבוטים של 3 קווים רנדומליים ותייגתי אותם כ״לא 4״.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// start static server on site folder on port 3030 before running this script const fs = require('fs'); const puppeteer = require('puppeteer'); const LETTERS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); const SIZE = 28; (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('http://localhost:3030'); // create results folder try { fs.mkdirSync(`out`); } catch(err) {} for (let i = 0; i < LETTERS.length; i++) { const l = LETTERS[i]; try { fs.mkdirSync(`out/${l}_${i}`); } catch(err) {} } for (let i = 0; i < 1320; i++) { const font_id = `font_${i}` await page.evaluate(`document.body.style.fontFamily = "${font_id}"`); for (let j = 0; j < LETTERS.length; j++) { const l = LETTERS[j]; await page.evaluate(`document.body.innerHTML = "${l}"`); const png_path = `out/${l}_${j}/${font_id}.png`; await page.screenshot({ path: png_path , clip: { x: 0, y: 0, width: SIZE, height: SIZE }}); console.log(png_path) } } await browser.close(); })(); |
לאחר מכן יצרתי 2 קבצים המתייגים את כל התמונות כאשר קובץ אחד מכיל את הרשימה של כל התמונות של ה4 והרשימה השנייה מכילה את כל התמונות שהם לא 4.
אימון המודל
את המודל עצמו אימנתי באופן דומה למודל שמאמן את MINST (דאטהסט המכיל 70,000 דוגמאות של הספרות 0-9 בכתב יד), בניסיון הראשון שלי ניסיתי פשוט להשתמש במודל הזה אבל במקרה שלי (פונט של שלוש קווים) היו ממש ממש גרועים.
השלב הראשון ביצירת המודל זה לכתוב פונקציה קטנה הממירה את התמונה לוקטור:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
from tensorflow.keras.utils import img_to_array, load_img from sklearn.model_selection import train_test_split # Importing the required Keras modules containing model and layers from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Conv2D, Dropout, Flatten, MaxPooling2D import tensorflow as tf import numpy as np SIZE = 28 def img2vec(path): vector = img_to_array(load_img(path, color_mode='grayscale', target_size=(SIZE, SIZE))) vector = vector.astype('float32') vector = vector/255 return vector |
כאן אני בעצם טוען את התמונה בשחור לבן ומעביר אותה למערך של מספרים שלמים (0-שחור, 255-לבן) את המערך אני הופך למערך של מספרים ממשיים שאותם אני מחלק ב255 על מנת שהוקטור שלי יחזיק מספרים בין 0-1.
בשלב הבא בונים מטריצה גדולה אליה טוענים את כל התמונות שיצרנו בדאטהסט:
1 2 3 4 5 6 7 8 9 10 11 12 |
with open('4_images_list.txt') as fin: images_of_4_filenames = [line.strip() for line in fin if line] with open('not_4_images_list.txt') as fin: images_of_not_4_filenames = [line.strip() for line in fin if line] number_of_vector = len(images_of_4_filenames) + len(images_of_not_4_filenames) X_MAT = np.ndarray(shape=(number_of_vector,SIZE,SIZE,1)) Y_MAT = np.ndarray(shape=(number_of_vector,)) i = 0 for filename in images_of_4_filenames + images_of_not_4_filenames: X_MAT[i] = img2vec(filename) Y_MAT[i] = 1 if filename in images_of_4_filenames else 0 i += 1 |
כעת נפריד בין קבוצת האימון לקבוצת הבדיקה באופן פסדו-רנדומלי (על מנת שנוכל לשחזר את החלוקה הזאת במודלים שונים)
1 |
x_train, x_test, y_train, y_test = train_test_split(X_MAT, Y_MAT, test_size=0.15, random_state=1432) |
נבנה את המודל ונתאמן עליו (האימון לוקח בין 5-20 דקות תלוי בחוזק של המחשב ובמספר האיטרציות שנבחרו)
1 2 3 4 5 6 7 8 9 10 11 12 |
model = Sequential() input_shape = (SIZE, SIZE, 1) model.add(Conv2D(SIZE, kernel_size=(3,3), input_shape=input_shape)) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Flatten()) model.add(Dense(128, activation=tf.nn.relu)) model.add(Dropout(0.2)) model.add(Dense(2,activation=tf.nn.sigmoid)) model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy']) model.fit(x=x_train,y=y_train, epochs=10) |
נבודק את ההצלחה שלו על קבוצת הבדיקה – מכיוון שלמודלים יש נטייה לעשות overfit ואז הם מתאימים את עצמם יותר מדי לקבוצת האימון ולא לומדים למקרה הכללי אזי חשוב מאוד לבדוק את ההצלחה של הזיהוי על קבוצה שהמודל לא התאמן עליה.
1 2 3 |
model.evaluate(x_test, y_test) # => 529/529 [==============================] - 2s 4ms/step # => - loss: 0.0057 - accuracy: 0.9975 |
ובמידה ואנחנו מרוצים מהמודל נשמור אותו
1 |
model.save("to_4_or_not_to_4_model.h5") |
נוח מאוד לבנות מודלים במחברות של jupyter שכן לא צריך לטעון את כל המידע מחדש בכל פעם שמשנים קונפיגורציה של המודל. (והכי נוח זה לעבוד בjupyter מתוך vs-code משלב את היתרונות של המחברת עם ההשלמות האוטומטיות והנוחות של editor מודרני).
שימוש במודל
כעת אפשר לממש את החוב הטכנולוגי – הפונקצייה שנשאר לנו מהפוסט הקודם:
1 2 3 4 5 6 7 8 9 10 11 |
import tensorflow as tf from tensorflow.keras.utils import img_to_array, load_img import pytesseract reconstructed_model = tf.keras.models.load_model("to_4_or_not_to_4_model.h5") def evluate_png(filename): arr = img_to_array(load_img(filename, color_mode='grayscale', target_size=(PIXEL_SIZE, PIXEL_SIZE))) arr = arr.astype('float32') arr = arr/255 arr = arr.reshape(1, PIXEL_SIZE, PIXEL_SIZE, 1) pred = reconstructed_model.predict(arr) return pred[0][1] |
בסך הכל אנחנו ממרים את התמונה לוקטור ומריצים את המודל על הוקטור (ותודה רבה לאלוהי הGPU על כפל המטריצות שהביא לנו את כל הטוב הזה). זהו עכשיו שהכל מוכן זה הזמן להריץ את הקוד של התמונות בלולאה, להזריק caffeine למחשב ללכת לישון ובבוקר לבדוק את התוצאות.
תוצאות
היתרון בשמירת התוצאות כsvg זה שניתן בקלות ליצור דף html פשוט שמציג את התוצאות ברזולוציה טובה (שכן svg הוא פורמט וקטורי, וכך לא צריך להוציא את העיניים על תמונות בגודל של 28 פיקסל…) בכל מקרה הרצתי את הסקריפט למשך הלילה הסתכלתי על התוצאות ובמבט ראשון הם היו מאכזבות:

האלגוריתם יצר אלפי תמונות אך רק בערך 10% מהם הזכירו את הספרה 4 כאשר חלקם היו ייצוג הפוך או מסובב של הספרה. לא התייאשתי והרצתי בדיקה של כלי OCR של גוגל בשם tesseract שיש לו את היכולת לזהות אות בודדת ולהגיד עד כמה הוא בטוח בזיהוי. בעזרת כלי זה בחרתי רק את התוצאות שגם בכלי זה קיבלו את הזיהוי של 4 ומיינתי אותם לפי הסדר:

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

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