שלב 4: בניית, אימון והערכה של המודל

בחלק הזה נעבוד על פיתוח, אימון והערכה של המודל שלנו. בשלב 3 בחרנו להשתמש במודל n-gram או במודל רצף, תוך שימוש ביחס S/W שלנו. זה הזמן לכתוב את אלגוריתם הסיווג שלנו ולאמן אותו. לשם כך נשתמש ב-TensorFlow עם ה-API של tf.keras.

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

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

איור 9: מקבץ שכבות לינארי

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

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

כשיש לנו רק 2 סיווגים (סיווג בינארי), המודל שלנו צריך להפיק ציון הסתברות אחד. לדוגמה, המשמעות של פלט 0.2 לדוגמת קלט נתונה היא "20% ודאות שהדגימה הזו נמצאת במחלקה הראשונה (מחלקה 1), 80% שהיא נמצאת במחלקה השנייה (מחלקה 0)." כדי להפיק ציון הסתברות כזה, פונקציית ההפעלה של השכבה האחרונה צריכה להיות פונקציית sigmoid, ומומלץ להשתמש בפונקציית ההפסדים כדי לאמן את המודל. (ראו איור 10, בצד ימין).

כשיש יותר מ-2 מחלקות (סיווג לכמה כיתות), המודל שלנו צריך להפיק ציון הסתברות אחד לכל כיתה. הסכום של הציונים האלה צריך להיות 1. לדוגמה, פלט {0: 0.2, 1: 0.7, 2: 0.1} פירושו "ודאות של 20% שהדגימה הזו נמצאת במחלקה 0, 70% שהדגימה ברמה 1 ו-10% היא בקבוצה 2". כדי להפיק את הפלט של הציונים האלה, פונקציית ההפעלה של השכבה האחרונה צריכה להיות softmax, ופונקציית האובדן שמשמשת לאימון המודל צריכה להיות קרוס-אנטרופיה קטגורית. (ראו איור 10, בצד ימין).

השכבה האחרונה

איור 10: השכבה האחרונה

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

def _get_last_layer_units_and_activation(num_classes):
    """Gets the # units and activation function for the last network layer.

    # Arguments
        num_classes: int, number of classes.

    # Returns
        units, activation values.
    """
    if num_classes == 2:
        activation = 'sigmoid'
        units = 1
    else:
        activation = 'softmax'
        units = num_classes
    return units, activation

בשני הקטעים הבאים מוסבר איך ליצור את שכבות המודל שנותרו למודלים של n-gram ולמודלים של רצף.

כשהיחס S/W קטן, גילינו שמודלים של n-gram מניבים ביצועים טובים יותר ממודלים של רצף. מודלים של רצף טובים יותר כשיש מספר גדול של וקטורים קטנים וצפופים. הסיבה לכך היא שקשרי הטמעה נלמדים בשטח צפוף, והדבר קורה בצורה הטובה ביותר מתוך דוגמאות רבות.

בניית מודל n-gram [אפשרות א']

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

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

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

from tensorflow.python.keras import models
from tensorflow.python.keras.layers import Dense
from tensorflow.python.keras.layers import Dropout

def mlp_model(layers, units, dropout_rate, input_shape, num_classes):
    """Creates an instance of a multi-layer perceptron model.

    # Arguments
        layers: int, number of `Dense` layers in the model.
        units: int, output dimension of the layers.
        dropout_rate: float, percentage of input to drop at Dropout layers.
        input_shape: tuple, shape of input to the model.
        num_classes: int, number of output classes.

    # Returns
        An MLP model instance.
    """
    op_units, op_activation = _get_last_layer_units_and_activation(num_classes)
    model = models.Sequential()
    model.add(Dropout(rate=dropout_rate, input_shape=input_shape))

    for _ in range(layers-1):
        model.add(Dense(units=units, activation='relu'))
        model.add(Dropout(rate=dropout_rate))

    model.add(Dense(units=op_units, activation=op_activation))
    return model

מודל רצף של מבנים [אפשרות ב']

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

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

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

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

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

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

  2. הטמעות מכווננות מניבות דיוק משופר. עם זאת, כתוצאה מכך יש צורך בכוח מחשוב מוגבר שנדרש כדי לאמן את הרשת. בגלל כמות מספיקה של דגימות, נוכל באותה מידה ללמוד הטמעה מאפס. שמנו לב שעבור S/W > 15K, התחלה של מודל חדש מניב ביעילות את אותו רמת דיוק כמו שימוש בהטמעה מכווננת עדינה.

השווינו בין מודלים שונים של רצף כמו CNN, sepCNN, RNN (LSTM ו-GRU), CNN-RNN ו-RNN בערימה, עם ארכיטקטורות שונות של המודלים. גילינו ש-sepCNN, וריאנט של רשת קונבולוציה שלעיתים קרובות יעיל יותר בנתונים וחסכוני במחשוב, מניב ביצועים טובים יותר ממודלים אחרים.

הקוד הבא בונה מודל sepCNN בן ארבע שכבות:

from tensorflow.python.keras import models
from tensorflow.python.keras import initializers
from tensorflow.python.keras import regularizers

from tensorflow.python.keras.layers import Dense
from tensorflow.python.keras.layers import Dropout
from tensorflow.python.keras.layers import Embedding
from tensorflow.python.keras.layers import SeparableConv1D
from tensorflow.python.keras.layers import MaxPooling1D
from tensorflow.python.keras.layers import GlobalAveragePooling1D

def sepcnn_model(blocks,
                 filters,
                 kernel_size,
                 embedding_dim,
                 dropout_rate,
                 pool_size,
                 input_shape,
                 num_classes,
                 num_features,
                 use_pretrained_embedding=False,
                 is_embedding_trainable=False,
                 embedding_matrix=None):
    """Creates an instance of a separable CNN model.

    # Arguments
        blocks: int, number of pairs of sepCNN and pooling blocks in the model.
        filters: int, output dimension of the layers.
        kernel_size: int, length of the convolution window.
        embedding_dim: int, dimension of the embedding vectors.
        dropout_rate: float, percentage of input to drop at Dropout layers.
        pool_size: int, factor by which to downscale input at MaxPooling layer.
        input_shape: tuple, shape of input to the model.
        num_classes: int, number of output classes.
        num_features: int, number of words (embedding input dimension).
        use_pretrained_embedding: bool, true if pre-trained embedding is on.
        is_embedding_trainable: bool, true if embedding layer is trainable.
        embedding_matrix: dict, dictionary with embedding coefficients.

    # Returns
        A sepCNN model instance.
    """
    op_units, op_activation = _get_last_layer_units_and_activation(num_classes)
    model = models.Sequential()

    # Add embedding layer. If pre-trained embedding is used add weights to the
    # embeddings layer and set trainable to input is_embedding_trainable flag.
    if use_pretrained_embedding:
        model.add(Embedding(input_dim=num_features,
                            output_dim=embedding_dim,
                            input_length=input_shape[0],
                            weights=[embedding_matrix],
                            trainable=is_embedding_trainable))
    else:
        model.add(Embedding(input_dim=num_features,
                            output_dim=embedding_dim,
                            input_length=input_shape[0]))

    for _ in range(blocks-1):
        model.add(Dropout(rate=dropout_rate))
        model.add(SeparableConv1D(filters=filters,
                                  kernel_size=kernel_size,
                                  activation='relu',
                                  bias_initializer='random_uniform',
                                  depthwise_initializer='random_uniform',
                                  padding='same'))
        model.add(SeparableConv1D(filters=filters,
                                  kernel_size=kernel_size,
                                  activation='relu',
                                  bias_initializer='random_uniform',
                                  depthwise_initializer='random_uniform',
                                  padding='same'))
        model.add(MaxPooling1D(pool_size=pool_size))

    model.add(SeparableConv1D(filters=filters * 2,
                              kernel_size=kernel_size,
                              activation='relu',
                              bias_initializer='random_uniform',
                              depthwise_initializer='random_uniform',
                              padding='same'))
    model.add(SeparableConv1D(filters=filters * 2,
                              kernel_size=kernel_size,
                              activation='relu',
                              bias_initializer='random_uniform',
                              depthwise_initializer='random_uniform',
                              padding='same'))
    model.add(GlobalAveragePooling1D())
    model.add(Dropout(rate=dropout_rate))
    model.add(Dense(op_units, activation=op_activation))
    return model

אימון המודל

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

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

ב-Keras, אנחנו יכולים להעביר את הפרמטרים של הלמידה האלו למודל באמצעות שיטת compile.

טבלה 2: פרמטרים של למידה

פרמטר למידה ערך
המדד דיוק
פונקציית אובדן – סיווג בינארי binary_crossentropy
פונקציית הפסד – סיווג מרובה-מחלקות sparse_categorical_crossentropy
אליפות ביעילות adam

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

היפר-פרמטר לאימון ערך
קצב למידה 1e-3
תקופות של זמן מערכת 1,000
גודל הקבוצה 512
עצירה מוקדמת פרמטר: val_loss, סבלנות: 1

טבלה 3: אימון היפר-פרמטרים

קוד Keras הבא מיישם את תהליך האימון באמצעות הפרמטרים שנבחרו בטבלאות 2 ו-3 למעלה:

def train_ngram_model(data,
                      learning_rate=1e-3,
                      epochs=1000,
                      batch_size=128,
                      layers=2,
                      units=64,
                      dropout_rate=0.2):
    """Trains n-gram model on the given dataset.

    # Arguments
        data: tuples of training and test texts and labels.
        learning_rate: float, learning rate for training model.
        epochs: int, number of epochs.
        batch_size: int, number of samples per batch.
        layers: int, number of `Dense` layers in the model.
        units: int, output dimension of Dense layers in the model.
        dropout_rate: float: percentage of input to drop at Dropout layers.

    # Raises
        ValueError: If validation data has label values which were not seen
            in the training data.
    """
    # Get the data.
    (train_texts, train_labels), (val_texts, val_labels) = data

    # Verify that validation labels are in the same range as training labels.
    num_classes = explore_data.get_num_classes(train_labels)
    unexpected_labels = [v for v in val_labels if v not in range(num_classes)]
    if len(unexpected_labels):
        raise ValueError('Unexpected label values found in the validation set:'
                         ' {unexpected_labels}. Please make sure that the '
                         'labels in the validation set are in the same range '
                         'as training labels.'.format(
                             unexpected_labels=unexpected_labels))

    # Vectorize texts.
    x_train, x_val = vectorize_data.ngram_vectorize(
        train_texts, train_labels, val_texts)

    # Create model instance.
    model = build_model.mlp_model(layers=layers,
                                  units=units,
                                  dropout_rate=dropout_rate,
                                  input_shape=x_train.shape[1:],
                                  num_classes=num_classes)

    # Compile model with learning parameters.
    if num_classes == 2:
        loss = 'binary_crossentropy'
    else:
        loss = 'sparse_categorical_crossentropy'
    optimizer = tf.keras.optimizers.Adam(lr=learning_rate)
    model.compile(optimizer=optimizer, loss=loss, metrics=['acc'])

    # Create callback for early stopping on validation loss. If the loss does
    # not decrease in two consecutive tries, stop training.
    callbacks = [tf.keras.callbacks.EarlyStopping(
        monitor='val_loss', patience=2)]

    # Train and validate model.
    history = model.fit(
            x_train,
            train_labels,
            epochs=epochs,
            callbacks=callbacks,
            validation_data=(x_val, val_labels),
            verbose=2,  # Logs once per epoch.
            batch_size=batch_size)

    # Print results.
    history = history.history
    print('Validation accuracy: {acc}, loss: {loss}'.format(
            acc=history['val_acc'][-1], loss=history['val_loss'][-1]))

    # Save model.
    model.save('IMDb_mlp_model.h5')
    return history['val_acc'][-1], history['val_loss'][-1]

כאן אפשר למצוא דוגמאות קוד לאימון של מודל הרצף.