מעבדת Deep Learning עם Tensorflow: מבוא לקלסיפיקציה נומרית

תוכן עניינים

בתרגול זה נצלול לעולם של סיווג נומרי (Numeric Classification) באמצעות ספריית TensorFlow. אנו נתמקד בבעיה קלאסית של סיווג בינארי שאינו לינארי – בעיית המעגלים (Circles).
 

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


הכנה וסביבת עבודה ב-Google Colab

לצורך מעבדת זו – נשתמש בסביבת Google Colab.

 

Google Colaboratory (Colab) הוא שירות מחשוב ענן מבוסס Jupyter Notebooks, המאפשר כתיבה והרצה של קוד Python דרך הדפדפן. Colab אידיאלי לעבודה עם TensorFlow ו-PyTorch כיוון שהוא מספק גישה חינמית ליחידות עיבוד גרפיות (GPU) וטנסוריות (TPU).

 

  1. פתיחת מחברת חדשה:
     

    • פתחו את Colab דרך דפדפן האינטרנט שלכם והתחילו מחברת חדשה (New notebook).
  2.  

  3. שינוי סוג המנוע (Runtime Type):
     

    • כדי לוודא שאנו משתמשים בכוח המחשוב הנדרש עבור אימון מודלים, נחליף את סוג ה-Runtime.
    • בסרגל העליון, אתרו את הכפתור Connect (נמצא בצד ימין, כאשר הממשק באנגלית) ולחצו על החץ הקטן לידו.
    • בתפריט שנפתח בחרו Change runtime type.
    • תחת "Hardware accelerator" (מאיץ חומרה), בחרו ב-T4 GPU.
    • לחצו על Save.
    • לחצו על Connect באותו הסרגל.
    • ודאו כי סביבת ההרצה מחוברת ופעילה.
  4.  

  5. הוספת תאי קוד (Code Cells):
     

    • מחברת Colab מורכבת מתאים. אנו נשתמש בתאי קוד (Code Cells).
    • באזור העורך המרכזי, לחצו על + Code (או קיצור דרך: Ctrl + M, B ב-Win, או Cmd + M, B ב-Mac) כדי להוסיף תא חדש.
    • כל קטע קוד בתרגול זה יש להעתיק לתא קוד נפרד ולהריץ אותו (באמצעות לחיצה על כפתור ההפעלה בצד התא או Shift + Enter).
    • מומלץ (אך לא חובה) להוסיף גם תאי-טקסט בין תאי-הקוד השונים, על מנת ליצור סדר וארגון.

הכנת הדאטה

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

 

  1. יצירת דאטה מעגלי:
     

    • נשתמש בספריית Scikit-Learn כדי ליצור 1000 דוגמאות של נקודות המסווגות לשני מעגלים.
    • העתיקו והריצו את הקוד:
      
      from sklearn.datasets import make_circles
      
      # Make 1000 examples
      n_samples = 1000
      
      # Create circles
      X, y = make_circles(n_samples, noise=0.03, random_state=42)
                      
    • הפונקציה יוצרת דאטה-סט של 1000 נקודות המאורגנות ב 2 מעגלים, זה בתוך זה. הדאטה מופיע בשני מערכים:
       

      • מערך X – הקואורדינטות של הנקודות. כל אלמנט במערך זה הוא מערך בעצמו ומכיל מיקום X ומיקום Y.
      • מערך y – הלייבלים, הסיווג של כל נקודה. 0 או 1. אדום או כחול.
    • בחנו את הערכים הראשונים בשני ה Arrays שהתקבלו ע״י X[0], y[0].
    • בחנו את המבנה של X ו-y באמצעות הפקודה X.shape, y.shape.
  2.  

  3. ארגון ב-DataFrame לצורך ניתוח חזותי:
     

    • כדי להקל על העבודה והניתוח, נמיר את הנתונים ל-DataFrame של Pandas.
    • הריצו את הקוד:
      
      import pandas as pd
      circles = pd.DataFrame({"X0":X[:, 0], "X1":X[:, 1], "label":y})
      circles.head()
                      
    • הנתונים עדיין לא ברורים לנו מצפיה בטבלה. נראה שהמחשה ויזואלית תעזור לנו יותר!
  4.  

  5. ויזואליזציה של הנתונים:
     

    • כעת, נצייר את הנתונים כדי להבין את האתגר. נשתמש ב-Matplotlib.
    • הריצו:
      
      import matplotlib.pyplot as plt
      plt.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.RdBu);
                      

בניית מודל 1: מודל נאיבי

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

 

  • יצירת המודל (או שימוש באחד מוכן…)
  • קומפילציה
  • אימון
  • אוולואציה
  • התאמה ושיפור
  • אוולואציה
  • וחוזר חלילה

 
עם התקדמות התהליך תכירו את סל הכלים שלנו לשיפור המודל.
 

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

 

  1. ייבוא הספריה – Tensorflow:
     

    • הוסיפו מסגרת קוד חדשה והריצו את הקוד הבא:
      
      # imnport TensorFlow
      import tensorflow as tf
      tf.__version__
      
  2. הגדרת המודל (Model 1):
     

    • נתחיל ממודל בעל שכבה בודדת.
    • לצורך השוואה מדויקת יותר בין המודלים במעבדה זו – נאפס את ה״רעש״ בתחילת כל אימון לערך קבוע. לצורך כך נשתמש בפקודת ה seed בשורה הראשונה.
    • הוסיפו מסגרת קוד חדשה והריצו את הקוד הבא:
      
      # Set the random seed
      tf.random.set_seed(42)
      
      # 1. Create the model using the Sequential API
      model_1 = tf.keras.Sequential([
          tf.keras.layers.Dense(1)
      ])
      
      # 2. Compile the model
      model_1.compile(loss=tf.keras.losses.BinaryCrossentropy(),
                      optimizer=tf.keras.optimizers.SGD(),
                      metrics=["accuracy"])
      
      # 3. Fit the model
      model_1.fit(X, y, epochs=5)
                      
    • שימו לב לפלט ממהלך האימון. בכל Epoch (מחזור אימון) מוצג הפער (Loss) הנוכחי בין חיזוי המודל לבין הערך האמיתי (y), וכן מטריקת ה accuracy – דיוק, כפי שביקשנו בפקודת הקומפילציה.
  3. הערכת המודל:
     

    • הריצו את הפקודה evaluate ובדקו את ציון הדיוק (Accuracy) שהמודל השיג:
      
      model_1.evaluate(X, y)
                      
    • ערך ה Accuracy קטן מאוד. לצורך ההשואה – כאשר ערך זה נקבע על 0.5 – זה אומר שהמודל פשוט ״מנחש״, ולכן פוגע ב 50% מהמקרים.
  4. נסיון שיפור:
     

    • ננסה לשפר את אותו המודל ע״י אימון למשך זמן ארוך יותר. בתא-קוד חדש, הריצו:
      
      # Let's try to improve our model by training for longer...
      model_1.fit(X, y, epochs=200, verbose=0)
      model_1.evaluate(X, y)
                      
    • לא נראה שיש שיפור משמעותי.
  5. הבנת מבנה המודל:
     

    • כדי שנוכל להגיע לשיפור אמיתי – חשוב קודם להבין את המבנה הפנימי של המודל. הריצו את השורה הבאה
      
      # Understanding the model structure
      model_1.summary()
                      
    • בטבלה שמופיעה ניתן לראות סיכום של שכבות המודל, וכמה פרמטרים (=משקלים) מכילה כל שכבה. בתוך המשקלים הללו שמורות התבניות אותן המודל מנסה ללמוד.

מודל 2 – הוספת שכבה נסתרת (Hidden Layer)

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

 

  1. הגדרת המודל (Model 2):
     

    • בתא-קוד חדש, צרו ואמנו מודל זהה לגרסה הקודמת, אך בעל שכבה נוספת. אמנו אותו למשך 100 Epochs:
      
      # Set the random seed
      tf.random.set_seed(42)
      
      # 1. Create the model using the Sequential API
      model_2 = tf.keras.Sequential([
          tf.keras.layers.Dense(1),
          tf.keras.layers.Dense(1)
      ])
      
      # 2. Compile the model
      model_2.compile(loss=tf.keras.losses.BinaryCrossentropy(),
                      optimizer=tf.keras.optimizers.SGD(),
                      metrics=["accuracy"])
      
      # 3. Fit the model
      model_2.fit(X, y, epochs=100, verbose=0)
                      
  2. הערכה חזותית ונומרית:
     

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

טכניקות התאמה ושיפור

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

 

  • בשלב הבניה – הוספת שכבות נוספות או הגדלת מספר היחידות בכל אחת מהשכבות הנסתרות.
  • בשלב הקומפילציה – בחירה ב Optimizer מסוג אחר, כגון Adam.
  • בשלב האימון – אימון המודל למשך יותר Epochs.

מודל 3 – שימוש בסל הטריקים לשיפור הביצועים

ה Optimizer הוא ה״נווט״, הרכיב שמכוון את ערכי המשקלים במודל במהלך האימון. שינוי סוג האופטימייזר עשוי להביא אותנו לביצועים טובים בהרבה. בנוסף – נעמיק את המודל שלנו למבנה של 3 שכבות רחבות יותר (100, 10 ו-1 נוירונים).

 

  1. הגדרת המודל וקומפילציה (Model 3):
     

    • בתא-קוד חדש, צרו מודל נוסף (model_3) בעל 3 שכבות Dense ברוחב 100, 10 ו-1, וכוונו את ה Optimizer ל-Adam:
      
      # Set the random seed
      tf.random.set_seed(42)
      
      # 1. Create the model using the Sequential API
      model_3 = tf.keras.Sequential([
          tf.keras.layers.Dense(100),
          tf.keras.layers.Dense(10),
          tf.keras.layers.Dense(1)
      ])
      
      # 2. Compile the model with Adam optimizer
      model_3.compile(loss=tf.keras.losses.BinaryCrossentropy(),
                      optimizer=tf.keras.optimizers.Adam(),
                      metrics=["accuracy"])
      
      # 3. Fit the model
      model_3.fit(X, y, epochs=100, verbose=0)
      
    • העריכו את ביצועי המודל. האם יש שיפור?
    • נראה שעדיין לא… כדי להבין מה לא בסדר במודל נחזור למוטו שלנו – "המחשה, המחשה, המחשה".
  2. הצגת גבול קבלת ההחלטה – Decision Boundary.
     

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

     

    • הריצו את הקוד הבא בתא-קוד חדש:
      
      import numpy as np
      
      def plot_decision_boundary(model, X, y):
        """
        Plot the decision boundary created by a model predicting on X.
        """
        # Define the axis boundaries of the plot and create a meshgrid
        x_min, x_max = X[:, 0].min() - 0.1, X[:, 0].max() + 0.1
        y_min, y_max = X[:, 1].min() - 0.1, X[:, 1].max() + 0.1
      
        xx, yy = np.meshgrid(np.linspace(x_min, x_max, 250),
                             np.linspace(y_min, y_max, 250))
      
        # Create X value (we're gonna make predictions on these)
        x_in = np.c_[xx.ravel(), yy.ravel()] # stack 2D arrays together
      
        # Make predictions
        y_pred = model.predict(x_in)
      
        # Check for multi-class
        if len(y_pred[0]) > 1:
          print("Doing multi-class classification")
          # If so, we have to reshape our predictions to get them ready for plotting
          y_pred = np.argmax(y_pred, axis=1).reshape(xx.shape)
        else:
          print("Doing binary classification")
          y_pred = np.round(y_pred).reshape(xx.shape)
      
        # Plot the decision boundary
        plt.contourf(xx, yy, y_pred, cmap=plt.cm.RdYlBu, alpha=0.7)
        plt.scatter(X[:,  0],  X[:,  1], c=y, s=40, cmap=plt.cm.RdYlBu)
        plt.xlim(xx.min(), xx.max())
        plt.xlim(yy.min(), yy.max())
      
    • והפעילו את הפונקציה על המודל האחרון שיצרנו, בתא-קוד חדש:
      
      # Check out the predictions our model is making
      plot_decision_boundary(model=model_3, X=X, y=y)
      
    • בינגו! נראה שעלינו על משהו – גבול קבלת ההחלטה נשאר ליניארי, למרות כל השינויים שביצענו. זו תופעת קריסת הליניאריות והפתרון פשוט – אקטיבציה!.

מודל 4 – החתיכה החסרה: חוסר-ליניאריות

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

 

  1. הגדרת המודל (Model 4):
     

    • ישנן מספר פונקציות אקטיבציה שנוכל לבחור, כגון: ReLU, Sigmoid או Tanh. כדי לסמלץ את ההרכב הנכון של השכבות ופונקציות האקטיבציה – מומלץ לפנות לכלי Tensorflow Playground, ואז לשחזר את המבנה האופטימלי בקוד שלנו.
    • הרגישו חופשי להתנסות במבנים שונים ובאקטיבציות שונות לשיפור הביצועים. הקוד הבא מציג מודל בן 3 שכבות צרות, בעלות פונקציות אקטיבציה. זו הצעה אפשרית להכנסת אי-ליניאריות למודל:
      
      # Set the random seed
      tf.random.set_seed(42)
      
      # 1. Create the model using the Sequential API
      model_4 = tf.keras.Sequential([
          tf.keras.layers.Dense(4, activation="relu"),
          tf.keras.layers.Dense(4, activation="relu"),
          tf.keras.layers.Dense(1, activation="sigmoid")
      ])
      
      # 2. Compile the model
      model_4.compile(loss="binary_crossentropy",
                      optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
                      metrics=["accuracy"])
      
      # 3. Fit the model
      history = model_4.fit(X, y, epochs=100, verbose=0)
                      
    • התנסו במבנים שונים ובצעו הערכה מחזורית.
  2. הערכה והמחשה:
     

    • בצעו אוולואציה ובדקו – האם עכשיו יש שיפור?
    • נראה שהכנסת אקטיבציה משפרת באופן דרמטי את הביצועים גם כאשר הורדנו משמעותית את מספר הנוירונים בשכבות הביניים!
    • בתא-קוד חדש, בצעו המחשה של ה-Decision-Boundary, שמים לב לשינוי?
    • שימו לב: השילוב של פונקציות ליניאריות ולא-ליניאריות הוא אחד מיסודות המפתח ברשתות נוירונים!

שיטות הערכה

עד כה ביצענו Evaluation לאחר כל מודל שאימנו, אבל עבדנו לא נכון!

 

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

 

נתקן את המצב הזה עכשיו, ותחילה נחלק את הדאטה-סט המקורי שלנו לסט-אימון (training-set) וסט-הערכה (test-set).
 

  1. פיצול סט-בחינה וסט-אימון:
     

    • בתא קוד חדש, הריצו:
      
      # Split into train and test sets
      X_train, y_train = X[:800], y[:800]
      X_test, y_test = X[800: ], y[800: ]
      
      X_train.shape, y_train.shape, X_test.shape, y_test.shape
      
    • יחס החלוקה המקובל בין סט האימון לסט הבחינה הוא 80%-20% לטובת סט האימון. האם זה מתבטא בחלוקה שביצענו?
  2. אימון מודל חדש על סט-האימון
     

    • בתא-קוד חדש צרו עותק של המודל האחרון שלנו, והפעם אמנו אותו רק על סט האימון.
    • הקפידו לשמור את הערך המוחזר מפקודת ה-fit למשתנה בשם history. זה יישמש אותנו בהמשך להמחשה של מהלך האימון:
  3. הערכת על דאטה סט חדש:
     

    • כעת, כשהמודל מאומן, בצעו הערכה שלו על סט-הבחינה שלנו.
    • בואו נמחיש ויזואלית את הביצועים של המודל מול סט-האימון לבין סט-הבחינה:
      
      # Plot the decision boundaries for the training and test sets
      plt.figure(figsize=(12, 6))
      plt.subplot(1, 2, 1)
      plt.title("Train")
      plot_decision_boundary(model_5, X_train, y_train)
      
      plt.subplot(1, 2, 2)
      plt.title("Test")
      plot_decision_boundary(model_5, X_test, y_test)
      
    • הפרדה זו של סטים שונים לאימון ולהערכה היא קריטית למניעת תופעות כמו Overfitting – כאשר מודל מתחיל ״לשנן״ את הדוגמאות מהאימון במקום להסיק תבניות. תופעה זו נראית באופן מובהק כאשר ביצועי המודל טובים מאוד על סט האימון, אבל פחות טובים באופן משמעותי על סט-הבחינה.
  4. עקומת ה Loss:
     

    • נציג כעת את עקומת ה-Loss, המכילה מידע חשוב על מהלך האימון. עקומה זו מציגה את דיוק המודל ביחס לאורך האימון. מידע זה חשוב לגלות איך מתקדם האימון והאם נדרשת התאמה כלשהי של קצב הלימוד, האקטיבציה או הרגולריזציה – על מנת לפתור בעיות Overfitting ואחרות.
    • בתא-קוד חדש הריצו:
      
      # Plot the loss curves
      pd.DataFrame(history.history).plot()
      plt.title("Model_5 loss curves")
      
    • שימו לב לגרף שהתקבל. מה אתם לומדים ממנו על המודל האחרון?
  5. Confusion Matrix
     

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

      • אחוז הפגיעה של המודל ב״כחול״.
      • אחוז הפגיעה של המודל אדום״.
      • אחוז הטעות של המודל מסוג 1 (המודל אמר ״כחול״ על מה שאמור להיות אדום)
      • אחוז הטעות של המודל מסוג 2 (המודל אמר ״אדום״ על מה שאמור להיות כחול)
    • בתא קוד חדש – הריצו את הקוד הבא:
      
      # Create a confusion matrix
      import itertools
      from sklearn.metrics import confusion_matrix
      
      # Make predictions
      y_preds = model_5.predict(X_test)
      
      figsize = (10, 10)
      cm = confusion_matrix(y_test, tf.round(y_preds))
      cm_norm = cm.astype("float") / cm.sum(axis=1)[:, np.newaxis] # normalize our confusion matrix
      n_classes = cm.shape[0]
      
      # Let's prettify it
      fig, ax = plt.subplots(figsize=figsize)
      
      # Create a matrix plot
      cax = ax.matshow(cm, cmap=plt.cm.Blues)
      fig.colorbar=cax
      
      # Create classes
      classes = False
      
      if classes:
        labels = classes
      else:
        labels = np.arange(cm.shape[0])
      
      # Label the axis
      ax.set(title="Confusion Matrix",
             xlabel="Predicted Label",
             ylabel="True Label",
             xticks=np.arange(n_classes),
             yticks=np.arange(n_classes),
             xticklabels=labels,
             yticklabels=labels)
      
      # Set x axis to bottom
      ax.xaxis.set_label_positio=["bottom"]
      ax.xaxis.tick_bottom()
      
      # Adjust label size
      ax.xaxis.label.set_size(20)
      ax.yaxis.label.set_size(20)
      ax.title.set_size(20)
      
      
      
      # Set threshold for different colors
      threshold = (cm.max() + cm.min()) / 2.
      
      # Plot the text on each cell
      for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
        plt.text(j, i, f"{cm[i, j]} ({cm_norm[i, j]*100:.1f}%)",
                 horizontalalignment="center",
                 color="white" if cm[i, j] > threshold else "black",
                 size=15)
      
    • התבוננו במטריצה שהתקבלה ושימו לב לאחוזי הפגיעה / הטעויות השונות

תרגול פתוח

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

 

  1. חקירת Learning Rate:
     

    • חזרו למודל 4, אך נסו לשנות את ערך ה-Learning Rate לאחד מהערכים: 0.1 (גבוה מדי) או 0.0001 (נמוך מדי).
    • אמנו את המודל ובחנו את עקומות ה-Loss.
  2. פיזור דאטה שונה:
     

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

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