מדריך לשימוש ב-API הפונקציונלי של Keras

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

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

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

להורדת הקוד אותו נפתח במדריך

 

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

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from tensorflow import keras
from keras.models import Sequential
from keras import layers
  • Numpy - היא ספרייה של פייתון שמאפשרת לעבוד עם מערכים ובפרט מערכים רב-מימדיים.

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

  • נבנה מודל למידת מכונה מבוסס TensorFlow וממשק Keras הידידותי.

 

מסד הנתונים

את מסד הנתונים המכיל נתונים אודות הרכב דוגמאות יין אדום הורדתי מ- Wine quality database. מסד הנתונים בפורמט CSV כולל 12 עמודות: 11 עמודות של מידע כמותי הכולל מדידות חומציות, סוכר, גופרית, ועמודה נוספת המכילה מידע על דירוג איכות דוגמאות היין כפי שקבעו טועמים אנושיים.

# import data and explore
wine = pd.read_csv('wine-quality.csv', sep=';')
wine.head()

CSV head of the wine quality data set

 

הכנת הנתונים ללמידת מכונה

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

הסרת דוגמאות חריגות:

from scipy.stats import zscore

z0 = wine.apply(zscore)

z = np.abs(zscore(wine))
z_in = (np.abs(zscore(wine)) < 3)
wine_clean = wine[z_in.all(axis=1)]

חלוקה לתכונות features ומטרות target (איכות היין ויתר התכונות המנבאות אותה):

x = wine_clean.iloc[:, :-1]
y = wine_clean.iloc[:, -1]

חלוקת ה-targets לשתי קטגוריות על פי איכות היין:

# divide the target (y) into 2 categories
bins = [3.0, 5.0, 8.0]
labels = ['bad','good']
wine_clean['quality_points'] = pd.cut(y, bins, labels=labels)

קידוד בינארי 1 או 0 על פי השייכות לקטגורית האיכות:

d = pd.get_dummies(wine_clean.loc[:,'quality_points'])

נשמיט את אחת מ-2 עמודות ה-targets, ונשאר רק עם 1, כדי לעשות סיווג בינארי:

# drop 1 of the categories
# so we have a binary classification
y = d.drop('bad', axis=1)

נעשה סטנדרטיזציה של הנתונים:

# standardize
x0 = x
x = x.apply(zscore) 

נפריד לקבוצות אימון ומבחן:

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(x, y, 
                                                    test_size=0.2, 
                                                    random_state=42)

 

בניית מודל Sequential

הדרך הפשוטה ביותר לבנות מודל Keras היא באמצעות API מסוג Sequential. לדוגמה:

def make_model(units1,units2,dropout):
  model = Sequential([
      layers.Input(shape=(11,), name='inputs'),
      layers.Dense(units1, activation='relu', name='dense_1'),
      layers.Dropout(dropout, name='droput_1'),
      layers.BatchNormalization(axis=1, name='norm_1'),
      layers.Dense(units2, activation='relu', name='dense_2'),
      layers.Dropout(dropout, name='droput_2'),
      layers.BatchNormalization(axis=1, name='norm_2'),
      layers.Dense(1, activation='sigmoid', name='binary_classifier')
  ])
  model._name = 'seq_model_wine_quality_classifier'
    
  return model


seq_model = make_model(256, 256, 0.1)
  • המשתנה seq_model מחזיק עכשיו את מבנה המודל.

מה סוג המודל?

type(seq_model)
keras.engine.sequential.Sequential

נציג את סיכום המודל:

seq_model.summary()

keras sequential model summary

הפונקציה הבאה של Keras מציגה את האופן בו השכבות מחוברות אחת לשנייה:

# display the network typology
keras.utils.plot_model(seq_model)

keras sequential model net typology

ניתן להציג בתרשים את מספר ה-inputs וה-outputs של כל שכבה על ידי הוספת פרמטר:

# display the inputs + outputs
keras.utils.plot_model(seq_model, show_shapes=True)

keras sequential model net typology with inputs and outputs

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

 

מודל פונקציונלי מאפס

הדרך הפשוטה ביותר לכתוב מודלים של Keras היא באמצעות ה-sequential API שראינו בחלקו הקודם של המדריך. דרך זו מאפשרת לבנות מודלים עם input ו- output אחד, ויישום של שכבה על גבי שכבה. אבל רוב המודלים בספריות מבוססים על ה- functional API שמאפשר לבנות מודלים בעלי מבנה מורכב: מספר קלטים inputs, מספר פלטים outputs, ופיצול של שכבות הביניים.

נתחיל מכתיבת המודל הכי פשוט באמצעות ה-API הפונקציונלי.

נגדיר שכבת קלט אותה נציב למשתנה inputs:

# define the inputs
inputs = layers.Input(shape=(11,), name='inputs')

נוסיף שכבה ונקרא לה על ה-inputs:

# define an additional layer
x = layers.Dense(units1, activation='relu', name='dense_1')(inputs)

נאתחל את המודל Model() על ידי העברת ה-inputs והשכבה האחרונה בתור outputs:

# instantiate the model
model = keras.Model(inputs=inputs, outputs=x)

עכשיו הכל ביחד:

# define the inputs
inputs = layers.Input(shape=(11,), name='inputs')

# define an additional layer
x = layers.Dense(units1, activation='relu', name='dense_1')(inputs)

# instantiate the model
model = keras.Model(inputs=inputs, outputs=x)

 

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

נגדיר את ה-inputs:

# define the inputs
inputs = layers.Input(shape=(11,), name='inputs')

נוסיף שכבה על גבי ה-inputs אותה נציב למשתנה x:

# define an additional layer
x = layers.Dense(units1, activation='relu', name='dense_1')(inputs)

נוסיף עוד שכבה על המשתנה x וגם אותה נציב למשתנה x:

# define yet another layer
x = layers.Dropout(dropout, name='droput_1')(x)

נחזור על התרגיל עבור כל השכבות החבויות ברשת:

# define all the latent layers in the same way
x = layers.BatchNormalization(axis=1, name='norm_1')(x)
x = layers.Dense(units2, activation='relu', name='dense_2')(x)
x = layers.Dropout(dropout, name='droput_2')(x)
x = layers.BatchNormalization(axis=1, name='norm_2')(x)

נגדיר את ה-outputs שהיא השכבה האחרונה שבמקרה זה תפקידה לסווג:

# define the outputs 
outputs = layers.Dense(1, activation='sigmoid', name='binary_classifier')(x)

נאתחל את המודל על ידי העברת ה-inputs וה-outputs לקונסטרקטור:

# define the model
model = keras.Model(inputs=[inputs], 
                    outputs=[outputs], 
                    name="func_model_wine_quality_classifier")

עכשיו הכל ביחד בתוך הפונקציה make_model():

def make_model(units1,units2,dropout):
  # define the inputs
  inputs = layers.Input(shape=(11,), name='inputs')
    
  # define an additional layer
  x = layers.Dense(units1, activation='relu', name='dense_1')(inputs)
  x = layers.Dropout(dropout, name='droput_1')(x)
  x = layers.BatchNormalization(axis=1, name='norm_1')(x)
  x = layers.Dense(units2, activation='relu', name='dense_2')(x)
  x = layers.Dropout(dropout, name='droput_2')(x)
  x = layers.BatchNormalization(axis=1, name='norm_2')(x)
  
  # define the outputs 
  outputs = layers.Dense(1, activation='sigmoid', name='binary_classifier')(x)
    
  # define the model
  model = keras.Model(inputs=[inputs], 
                    outputs=[outputs], 
                    name="func_model_wine_quality_classifier")

  return model

נקרא לפונקציה כדי שתיצור את המודל:

func_model = make_model(256, 256, 0.1)

מה סוג המודל?

print(type(func_model))
class 'keras.engine.functional.Functional'

נציג את סיכום המודל באמצעות:

func_model.summary()

keras functional model summary

ואת מבנה הקישורים כולל מספר ה-inputs וה-outputs של כל שכבה:

keras.utils.plot_model(func_model, show_shapes=True)

keras functional model net typology

 

ניתן לגשת לכל שכבות הרשת:

func_model.layers
[<keras.engine.input_layer.InputLayer at 0x7f72d4348640>,
 <keras.layers.core.dense.Dense at 0x7f72d4097250>,
 <keras.layers.regularization.dropout.Dropout at 0x7f72d756ab90>,
 <keras.layers.normalization.batch_normalization.BatchNormalization at 0x7f72d4097bb0>,
 <keras.layers.core.dense.Dense at 0x7f72d4096620>,
 <keras.layers.regularization.dropout.Dropout at 0x7f72d814eb00>,
 <keras.layers.normalization.batch_normalization.BatchNormalization at 0x7f72d4502230>,
 <keras.layers.core.dense.Dense at 0x7f72d40eb880>]

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

func_model.layers[:-1]
[<keras.engine.input_layer.InputLayer at 0x7f72d4348640>,
 <keras.layers.core.dense.Dense at 0x7f72d4097250>,
 <keras.layers.regularization.dropout.Dropout at 0x7f72d756ab90>,
 <keras.layers.normalization.batch_normalization.BatchNormalization at 0x7f72d4097bb0>,
 <keras.layers.core.dense.Dense at 0x7f72d4096620>,
 <keras.layers.regularization.dropout.Dropout at 0x7f72d814eb00>,
 <keras.layers.normalization.batch_normalization.BatchNormalization at 0x7f72d4502230>]

או רק את השכבה האחרונה:

func_model.layers[-1]
<keras.layers.core.dense.Dense at 0x7f72d40eb880>

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

func_model.get_layer('binary_classifier')
<keras.layers.core.dense.Dense at 0x7f72d40eb880>
  • את שם השכבה מוצאים מתוך הפלט שמייצרת המתודה: model.summary().

וכפי שאנחנו יכולים לקבל את השכבה, אנחנו יכולים לקבל את ה-input וה-output שלה:

func_model.get_layer('binary_classifier').input
<KerasTensor: shape=(None, 256) dtype=float32 (created by layer 'norm_2')>
func_model.get_layer('binary_classifier').output
<KerasTensor: shape=(None, 1) dtype=float32 (created by layer 'binary_classifier')>

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

 

כיצד להפוך מודל sequential ל-functional?

כדי להפוך מודל קיים מסוג sequential ל-functional מתחילים מהגדרת ה-inputs:

# 1. set the inputs
inputs = layers.Input(shape=(11,), name='inputs')

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

# 2. set the layers
x = inputs
for layer in seq_model.layers:
    x = layer(x)
  • שלב זה דורש לעיתים התייחסות נפרדת לשכבת ה-output.

ולבסוף, מאתחלים את המודל על ידי העברת הפרמטרים של ה-inputs וה-outputs:

# 3. instantiate the model
func_model = keras.Model(inputs=inputs, outputs=x)

עכשיו הכל ביחד:

# seq >> functional

# 1. set the inputs
inputs = layers.Input(shape=(11,), name='inputs')

# 2. set the layers
x = inputs
for layer in seq_model.layers:
    x = layer(x)

# 3. instantiate the model
func_model = keras.Model(inputs=inputs, outputs=x)

 

עד כה ראינו דוגמה מאוד פשוטה של המרת מודל מ-API פונקציונלי ל-sequential אבל בדרך כלל אנחנו נצטרך להיות קצת יותר מתוחכמים. לדוגמה, נשתמש בגישה זו כדי לעשות למידת העברה transfer learning במסגרתה ניקח מודל מאומן שמישהו אחר עשה, הרבה פעמים זה צוות המוביל בעולם בתחומו, ונאמץ אותו למשימה שלנו. במקרה מעין זה נשתמש במודל המאומן בתור עמוד השדרה ונתאים את שכבות הקלט והפלט לצרכינו. לדוגמה, שימוש במודל מאומן VGG16 לסיווג תמונות.

ראשית, נגדיר את שכבת ה-inputs:

# 1. set the inputs
inputs = layers.Input(shape=(11,), name='inputs')

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

# 2. adapt layers from the original model (those you need)
x = inputs
for layer in seq_model.layers[1:-1]:
    x = layer(x)

נוסיף שכבה אחת או יותר. פה אני מוסיף שכבה אחת:

# add a layer
x = layers.Dropout(0.1, name='new_dropout')(x)

נגדיר את שכבת ה-outputs:

# 3. set the outputs
outputs = layers.Dense(2, name='new_classifier')(x)

נאתחל את המודל על ידי זה שנעביר לתוכו את ה-inputs וה-outputs:

# 4. set the model
func_model = keras.Model(inputs=inputs, outputs=outputs)

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

# 0. freeze all of the trained layers
for layer in func_model.layers:
    layer.trainable = False
# you can also freeze only some layers
  • הקפאתי את כל המשקולות אבל לא חייבים. אפשר להקפיא רק חלק בהתאם לצורך.

נציג את סיכום המודל:

func_model.summary()

עכשיו הכל ביחד:

# extract the layers you need from an existing model

# 0. freeze all of the trained layers
for layer in func_model.layers:
    layer.trainable = False
# you can also freeze only some layers

# 1. set the inputs
inputs = layers.Input(shape=(11,), name='inputs')

# 2. set the layers (those you need)
x = inputs
for layer in func_model.layers[1:-1]:
    x = layer(x)

# add a layer
x = layers.Dropout(0.1, name='new_dropout')(x)

# 3. set the outputs
outputs = layers.Dense(2, name='new_classifier')(x)

# 4. set the model
func_model = keras.Model(inputs=inputs, outputs=outputs)

keras model summary transfer learning freezed weights

  • אפשר לראות ש-70,192 פרמטרים מתוך 71,426 אינם ניתנים לאימון מה שאומר שהמידע בהם ישמר ולא ישתנה כאשר נאמן את המודל שלנו על מנת שיתאים לצרכים שלנו.

 

מודלים מורכבים - מספר inputs ו-outputs

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

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

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

wine = pd.read_csv('wine-quality.csv', sep=';')
# wine.head()

from scipy.stats import zscore

z0 = wine.apply(zscore)

z = np.abs(zscore(wine))
z_in = (np.abs(zscore(wine)) < 3)
wine_clean = wine[z_in.all(axis=1)]

x = wine_clean.iloc[:, :-1]
y = wine_clean.iloc[:, -1]

# Divide the target (y) into 2 categories
bins = [3.0, 5.0, 8.0]
labels = ['bad','good']
wine_clean['quality_points'] = pd.cut(y, bins, labels=labels)

d = pd.get_dummies(wine_clean.loc[:,'quality_points'])

y = d.drop('bad', axis=1)

x0 = x
x = x.apply(zscore) # standardise

נכין את הנתונים עבור חיזוי רמת האלכוהול:

wine_clean2 = wine_clean
wine_clean2.columns = wine_clean2.columns.str.replace(' ', '_')

x2 = wine_clean.loc[:, ['residual_sugar','density']]
y2 = wine_clean.loc[:,'alcohol']

במקביל לנתונים לחיזוי טיב היין (מהם נשמיט את התכונה אלכוהול):

x1 = x.drop(['alcohol'], axis=1)
y1 = y

נחלק לקבוצות אימון ומבחן:

from sklearn.model_selection import train_test_split

X1_train, X1_test, y1_train, y1_test = train_test_split(x1, y1, 
                                                    test_size=0.2, 
                                                    random_state=42)

X2_train, X2_test, y2_train, y2_test = train_test_split(x2, y2, 
                                                    test_size=0.2, 
                                                    random_state=42)

נבנה את המודל. נתחיל בהגדרת ההיפר-פרמטרים:

# set the hyperparameters
units = 256
dropout = 0.5

במקרה זה יש לנו שני inputs במקום אחד. נגדיר את שניהם:

# set the inputs
inputs1 = layers.Input(shape=(10,), name='inputs1')
inputs2 = layers.Input(shape=(2,), name='inputs2')
  • שימו לב לשמות שכבות ה-inputs כי נצטרך אותם בשלב הרצת המודל.

נגדיר את שכבות הביניים:

# set the layers (features)
x1 = layers.Dense(units, activation='relu', name='dense_1')(inputs1)
x1 = layers.Dropout(dropout, name='droput_1')(x1)
x1 = layers.BatchNormalization(axis=1, name='norm_1')(x1)
x1 = layers.Dense(units, activation='relu', name='dense_2')(x1)
x1 = layers.Dropout(dropout, name='droput_2')(x1)
x1 = layers.BatchNormalization(axis=1, name='norm_2')(x1)

x2 = layers.Dense(units, activation='relu', name='dense_1_2')(inputs2)
  • הגדרתי שכבות ביניים נפרדות ל-inputs

נוסיף שכבה המחברת את שכבות הביניים:

x = layers.concatenate([x1, x2], name='concatenate')

נוסיף שני outputs נפרדים: מסווג שימצא את איכות היין ושני שעושה רגרסיה וחוזה את כמות האלכוהול

# set the outputs 
outputs1 = layers.Dense(1, activation='sigmoid', name='binary_classifier')(x)
outputs2 = layers.Dense(1, activation='linear', name='regressor')(x)
  • שימו לב לשמות שכבות ה-outputs כי נצטרך אותם בשלב הקומפילציה של המודל.

נאתחל את המודל על ידי זה שנעביר לו את שני ה-inputs ואת שני ה-outputs:

# instantiate the model
func_model_2 = keras.Model(inputs=[inputs1, inputs2], 
                outputs=[outputs1, outputs2], 
                name="func_model_wine_quality_classifier")

נציג סכימה של מבנה המודל:

keras model summary transfer learning freezed weights

  • אפשר לראות את הפיצול לשני inputs ולשני outputs
  • זו רק דוגמה. אפשר לבנות את הגרף איך שרוצים.

בשלב הקומפילציה נגדיר את ה-loss וה-metrics בתוך מילונים dictionaries בהם אתה נציב את שם שכבת ה-output בתור מפתח.

מה שמם של שכבות ה-output?

func_model_2.output
[<KerasTensor: shape=(None, 1) dtype=float32 (created by layer 'binary_classifier')>,
 <KerasTensor: shape=(None, 1) dtype=float32 (created by layer 'regressor')>]

לכל אחד משני ה-outputs נציב פונקצית loss ומדד metrics מתאים:

func_model_2.compile(optimizer=keras.optimizers.Adam(), 
                     loss = {
                         "binary_classifier" : keras.losses.BinaryCrossentropy(from_logits=False),
                         "regressor" : keras.losses.BinaryCrossentropy(from_logits=True),
                     },
                metrics={"binary_classifier" : "accuracy", "regressor" : "MSE"})
  • בשביל ה-output המסווג classifier נשתמש בפונקצית loss = BinaryCrossentropy(from_logits=False) ומדד דיוק metrics = "accuracy"
  • בשביל ה-output הרגרסור regressor נשתמש בפונקצית loss = BinaryCrossentropy(from_logits=True) ומדד metrics = "MSE"

בהגדרת הפונקציה fit() נשתמש גם כן במילונים dictionaries מכיוון שיש לנו מספר inputs ו- outputs:

history = func_model_2.fit({"inputs1":X1_train, "inputs2":X2_train}, 
                            {"binary_classifier":y1_train , "regressor":y2_train}, 
                            batch_size=32, 
                            epochs=26,
                            validation_data=({"inputs1":X1_test, "inputs2":X2_test}, 
                            {"binary_classifier":y1_test , "regressor":y2_test}))

 

יתרונות וחסרונות של מודל פונקציונליים באמצעות Keras

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

להורדת הקוד אותו נפתח במדריך

 

מדריכים נוספים בסדרה על למידת מכונה

שימוש ב-TensorBoard לניטור מודלים של למידת מכונה

דף צ'יטים לבניית מודלים של למידה עמוקה באמצעות Keras

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

 

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

 

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

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

 

 

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

 

= 6 + 6