זיהוי ספרות שכתב אדם על ידי בינה מלאכותית - פיתוח המודל

מחבר:
בתאריך:

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

זיהוי ספרות כתובות ביד באמצעות בינה מלאכותית

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

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

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

רשת נאורונית שכוללת שכבה חבויה

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

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

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

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

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

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

 

המודל

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

 

כלים ושיטות

המודל נכתב באמצעות Keras, ספרייה של למידת מכונה שכתובה ב-Python. את המודל עצמו לקחתי ממדריך מצוין של Jason Brownlee.

את התוצאה של המודל ייצאתי לקובץ json באמצעות ספריית TensorFlow.js.

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

את הקוד המלא העליתי ל-Github.

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

 

פיתוח המודל

נתחיל מייבוא הספריות שאותם נצטרך לצורך פיתוח המודל:

import numpy
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.layers import Flatten
from keras.layers.convolutional import Conv2D
from keras.layers.convolutional import MaxPooling2D
from keras.utils import np_utils
from keras import backend as K
K.set_image_dim_ordering('th')

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

# fix random seed for reproducibility
seed = 7
numpy.random.seed(seed)

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

ערכי ה-x הם התמונות של הספרות, וערכי ה-y הם שמם של הספרות, כשאנחנו מפצלים את סט התמונות לקבוצת אימון (train) ולקבוצת בקרה (test).

# load data
(X_train, y_train), (X_test, y_test) = mnist.load_data()

את התמונות נעצב מחדש כדי שיתאימו להזנה לרשת נוירונית CNN:

# reshape to be [samples][pixels][width][height]
X_train = X_train.reshape(X_train.shape[0], 1, 28, 28).astype('float32')
X_test = X_test.reshape(X_test.shape[0], 1, 28, 28).astype('float32')

הערך הראשון הוא מספר הדוגמאות (60,000 לאימון ו-10,000 לבדיקה).

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

שני הערכים האחרונים הם רוחב וגובה שהם 28 פיקסלים בהתאם למה שאנחנו מקבלים מ-MNIST. את הערכים נהפוך לסוג float32 כדי להגביל את דיוק הפיקסלים ולהפחית את הדרישות מהמחשב שמריץ את התוכנה.

 

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

# normalize inputs from 0-255 to 0-1
X_train = X_train / 255
X_test = X_test / 255

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

בדוגמה שלנו, אפשריות 10 ספרות (סט שנע בין 0 ל-9), ולפיכך נשתמש במערך של 10 פריטים כדי לייצג כל ספרה.

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

[1,0,0,0,0,0,0,0,0,0]

ובשביל לקודד את הספרה 1 רק הפריט השני משמאל יקבל את הערך 1.

[0,1,0,0,0,0,0,0,0,0]

לקידוד עצמו אחראית הפונקציה to_categorical של ספריית numpy:

# one hot encode outputs
y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)
num_classes = y_test.shape[1]

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

def plot_images(images, labels):
  # Create figure with 2x2 sub-plots.
  fig, axes = plt.subplots(2, 2)
  fig.subplots_adjust(hspace=0.3, wspace=0.3)

  # plot 4 images
  for i, ax in enumerate(axes.flat):
    # Plot image
    ax.imshow(images[i].reshape([28,28]), cmap=plt.get_cmap('gray'))
    
    # Plot label
    for idx, val in enumerate(labels[i]):
      if(val == 1):
        ax.set_xlabel('Label : %d' % idx)
        
  plt.show()
  
plot_images(X_train[0:4], y_train[0:4])

זו התוצאה, הצגת 4 התמונות הראשונות בסט התמונות:

דוגמה ל-4 התמונות הראשונות של סט הנתונים mnist

 

אימון המודל

אימון המודל יעשה בתוך פונקציה:

def baseline_model():
  # create model
  model = Sequential()
  model.add(Conv2D(32, (5, 5), input_shape=(1, 28, 28), activation='relu'))
  model.add(MaxPooling2D(pool_size=(2, 2)))
  model.add(Dropout(0.2))
  model.add(Flatten())
  model.add(Dense(128, activation='relu'))
  model.add(Dense(num_classes, activation='softmax'))
  
  # Compile model
  model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
  return model

השכבה החבויה היא שכבת ה-convolution שנתנה לשיטה את שמה. שכבה זו מריצה 32 פילטרים בגודל 5*5 פיקסלים על כל תמונה, ודוחסת את המידע לתוך פונקצית אקטיבציה מסוג relu. מה שאומר שכל ריבוע של 25 פיקסלים יניב ערך יחיד שיעבור לשכבה הבאה. התוצאה היא שהשכבה הבאה רואה פחות פרטים, ויותר מהתמונה הכללית. צורת העבודה הזו מפחיתה מהעומס החישובי המוטל על המחשב.

השכבה הבאה היא שכבת דגימה (pooling) שתפקידה לצמצם כל ריבוע פיקסלים שממדיו 2*2 פיקסלים שנמסר לה על ידי השכבה הקודמת לריבוע בודד. שכבה זו אינה כוללת פונקצית אקטיבציה.

שכבת ה-dropout שומטת באקראי 20% מהמידע מהשכבה הקודמת. המטרה היא למנוע מהמערכת להתאים אתה עצמה יתר על המידה לדוגמאות.

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

שכבת ה-dense מכילה 128 פיקסלים בלבד (במקום ה-784, 28*28 איתם התחלנו) ובה כל נוירון מחובר לכל אחד מהנוירונים בשכבה הבאה.

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

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

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

 

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

# build the model
model = baseline_model()

# Fit the model
model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=10, batch_size=200, verbose=2)

אחרי ריצת אימון אחת בלבד קיבלתי 97% דיוק בזיהוי תמונות בקבוצת הבקרה, ולאחר 10 ריצות אימון הגעתי ללמעלה מ-99% אחוזי דיוק בזיהוי.

 

ייצוא המודל

את המודל ייצאתי באמצעות tensorflow.js וקיבלתי שני קבצים בהם אני משתמש בתור ה-api של האפליקציה כפי שהסברתי במדריך הקודם זיהוי ספרות שכתב אדם באמצעות בינה מלאכותית.

 

לסיכום

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

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

 

אהבתם? לא אהבתם? דרגו!

0 הצבעות, ממוצע 0 מתוך 5 כוכבים

 

הוסף תגובה חדשה

 

= 5 + 8