ขั้นตอนที่ 4: สร้าง ฝึก และประเมินโมเดล

ในส่วนนี้ เราจะไปสร้าง ฝึก และประเมินโมเดลของเรา ในขั้นตอนที่ 3 เราเลือกใช้โมเดล n-gram หรือโมเดลลำดับ โดยใช้อัตราส่วน S/W ตอนนี้ก็ถึงเวลาเขียนอัลกอริทึมการจัดประเภทและฝึกอัลกอริทึมแล้ว เราจะใช้ TensorFlow กับ tf.keras API สำหรับเรื่องนี้

การสร้างโมเดลแมชชีนเลิร์นนิงด้วย Keras เป็นเรื่องของการประกอบเลเยอร์ องค์ประกอบที่ใช้ประมวลผลข้อมูล เหมือนกับที่เราประกอบตัวต่อเลโก้ เลเยอร์เหล่านี้ช่วยให้เราระบุลำดับการแปลงที่เราต้องการ ดำเนินการกับอินพุตของเรา เนื่องจากอัลกอริทึมการเรียนรู้ของเราอินพุตข้อความเดียวและเอาต์พุตการแยกประเภทเดียว เราจึงสามารถสร้างสแต็กเลเยอร์เชิงเส้นโดยใช้ API ของโมเดลตามลำดับ

การซ้อนเลเยอร์แบบเชิงเส้น

รูปที่ 9: กลุ่มเลเยอร์เชิงเส้น

เลเยอร์อินพุตและเลเยอร์ขั้นกลางจะสร้างขึ้นแตกต่างกัน ขึ้นอยู่กับว่าเรากำลังสร้างโมเดล n-gram หรือโมเดลลำดับ แต่ไม่ว่าโมเดลจะเป็นประเภทใด เลเยอร์สุดท้ายก็จะเหมือนกันสำหรับปัญหาที่ระบุ

การสร้างเลเยอร์สุดท้าย

เมื่อมีเพียง 2 คลาส (การจัดประเภทแบบไบนารี) โมเดลของเราควรแสดงคะแนนความน่าจะเป็นรายการเดียว ตัวอย่างเช่น เอาต์พุต 0.2 สำหรับตัวอย่างอินพุตหมายถึง "ความมั่นใจ 20% ว่าตัวอย่างนี้อยู่ในคลาสแรก (คลาส 1), 80% ที่อยู่ในกลุ่มที่สอง (คลาส 0)" หากต้องการแสดงผลคะแนนความน่าจะเป็นดังกล่าว ฟังก์ชันการเปิดใช้งาน ของเลเยอร์สุดท้ายควรเป็น ฟังก์ชัน Sigmoid และ ฟังก์ชันการขาดหาย ที่ใช้ในการฝึกโมเดล (ดูรูปที่ 10 ทางด้านซ้าย)

เมื่อมีมากกว่า 2 คลาส (การจัดประเภทแบบหลายคลาส) โมเดลของเราควรแสดงคะแนนความน่าจะเป็น 1 รายการต่อคลาส ผลรวมของคะแนนเหล่านี้ควรเป็น 1. ตัวอย่างเช่น เอาต์พุต {0: 0.2, 1: 0.7, 2: 0.1} หมายถึง "ความมั่นใจ 20% ว่าตัวอย่างนี้อยู่ในคลาส 0, 70% ว่าอยู่ในระดับ 1 และ 10% ว่าอยู่ในระดับ 2" หากต้องการแสดงผลคะแนนเหล่านี้ ฟังก์ชันการเปิดใช้งานของเลเยอร์สุดท้ายควรเป็น softmax และฟังก์ชันการสูญเสียที่ใช้ในการฝึกโมเดลควรเป็นแบบข้ามเอนโทรปีเชิงหมวดหมู่ (ดูรูปที่ 10 ด้านขวา)

เลเยอร์สุดท้าย

รูปที่ 10: เลเยอร์สุดท้าย

โค้ดต่อไปนี้จะกำหนดฟังก์ชันที่ใช้จำนวนคลาสเป็นอินพุตและแสดงผลจำนวนหน่วยเลเยอร์ที่เหมาะสม (1 หน่วยสำหรับการจัดประเภทไบนารี หรือ 1 หน่วยสำหรับแต่ละคลาส) และฟังก์ชันการเปิดใช้งานที่เหมาะสม

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

2 ส่วนต่อไปนี้จะอธิบายการสร้างเลเยอร์โมเดลที่เหลือสำหรับโมเดล n-gram และโมเดลลำดับ

เมื่ออัตราส่วน S/W มีน้อย เราพบว่าโมเดล n-gram มีประสิทธิภาพดีกว่าโมเดลลำดับ โมเดลลำดับจะดียิ่งขึ้นเมื่อมีเวกเตอร์ ขนาดเล็กและหนาแน่นจำนวนมาก นั่นเป็นเพราะความสัมพันธ์ของการฝังฝังอยู่ในพื้นที่ที่หนาแน่น ซึ่งจะเกิดขึ้นได้ดีที่สุดเมื่อเทียบกับตัวอย่างหลายๆ ตัวอย่าง

สร้างโมเดล n-gram [ตัวเลือก A]

เราเรียกโมเดลที่ประมวลผลโทเค็นอย่างอิสระ (ไม่คำนึงถึงลำดับคำ) เป็นโมเดล n-gram เปอร์เซปรอนหลายชั้นแบบเรียบง่าย (รวมถึงโมเดลการถดถอยแบบโลจิสติก เครื่องเร่งการไล่ระดับสีและเครื่องเวกเตอร์ที่รองรับ) ทั้งหมดนี้จัดอยู่ในหมวดหมู่นี้ โดยไม่สามารถใช้ข้อมูลเกี่ยวกับการจัดลำดับข้อความได้

เราเปรียบเทียบประสิทธิภาพของโมเดล n-gram บางรายการที่กล่าวถึงข้างต้นและสังเกตเห็นว่าโดยทั่วไปแล้ว Perceptrons แบบหลายชั้น (MLP) จะทำงานได้ดีกว่าตัวเลือกอื่นๆ MLP นั้นง่ายต่อการกำหนดและทำความเข้าใจ ให้ความแม่นยำที่ดี และต้องใช้การคำนวณค่อนข้างน้อย

โค้ดต่อไปนี้กำหนดโมเดล MLP แบบ 2 เลเยอร์ใน tf.keras โดยเพิ่มเลเยอร์แบบเลื่อนลงสำหรับการกำหนดมาตรฐาน 2 เลเยอร์เพื่อป้องกันไม่ให้มีการปรับมากเกินไปกับตัวอย่างการฝึก

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

สร้างโมเดลลำดับ [ตัวเลือก B]

เราหมายถึงโมเดลที่สามารถเรียนรู้จากโทเค็นต่อกันในรูปแบบลำดับ ซึ่งรวมถึงคลาสของโมเดล CNN และ RNN ข้อมูลจะได้รับการประมวลผลล่วงหน้าเป็นเวกเตอร์ของลำดับสำหรับโมเดลเหล่านี้

โดยทั่วไปโมเดลลำดับจะมีพารามิเตอร์ที่ต้องเรียนรู้มากกว่า เลเยอร์แรกในโมเดลเหล่านี้คือเลเยอร์ที่ฝัง ซึ่งจะเรียนรู้ความสัมพันธ์ระหว่างคำในพื้นที่เวกเตอร์ที่หนาแน่น การเรียนรู้ความสัมพันธ์ของคำใช้ได้ผลดีที่สุด เมื่อเทียบกับหลายๆ ตัวอย่าง

คำในชุดข้อมูลหนึ่งๆ มักจะไม่ซ้ำกับคำในชุดข้อมูลนั้น เราจึงสามารถเรียนรู้ความสัมพันธ์ระหว่างคำในชุดข้อมูลโดยใช้ชุดข้อมูลอื่นๆ ในการทำเช่นนั้น เราสามารถโอนการฝังที่เรียนรู้จากชุดข้อมูลอื่นไปยังเลเยอร์การฝังของเราได้ การฝังเหล่านี้เรียกว่าการฝังที่ฝึกไว้แล้วล่วงหน้า การใช้การฝังก่อนการฝึกจะช่วยให้โมเดลเริ่มต้นกระบวนการเรียนรู้ได้อย่างรวดเร็ว

มีการฝังก่อนการฝึกที่ได้รับการฝึกด้วยคลังข้อมูลขนาดใหญ่ เช่น GloVe GloVe ได้รับการฝึกมาแล้วในหลายคลัง (ส่วนใหญ่คือ Wikipedia) เราได้ทดสอบการฝึกโมเดลลำดับโดยใช้การฝัง GloVe เวอร์ชันหนึ่ง และสังเกตเห็นว่าหากเราตรึงน้ำหนักของการฝังที่ฝึกไว้แล้วล่วงหน้าและฝึกเฉพาะเครือข่ายที่เหลือ โมเดลจะทำงานได้ไม่ดีนัก ซึ่งอาจเป็นเพราะบริบทที่มีการฝึกเลเยอร์การฝังอาจแตกต่างจากบริบทที่เราใช้

การฝัง GloVe ที่ได้รับการฝึกบนข้อมูล Wikipedia อาจไม่สอดคล้องกับรูปแบบภาษาในชุดข้อมูล IMDb ของเรา ความสัมพันธ์ที่อนุมานอาจต้องมีการอัปเดต เช่น น้ำหนักของการฝังอาจต้องปรับตามบริบท โดยแบ่งเป็น 2 ขั้น ดังนี้

  1. ในการเรียกใช้ครั้งแรก เมื่อน้ำหนักของเลเยอร์ที่ฝังถูกหยุดนิ่ง เราก็ปล่อยให้เครือข่ายที่เหลือได้เรียนรู้ ในตอนท้ายของการเรียกใช้นี้ น้ำหนักของโมเดลจะไปถึงสถานะที่ดีกว่าค่าเริ่มต้นมาก ในการเรียกใช้ครั้งที่ 2 เราอนุญาตให้เลเยอร์ที่ฝังเรียนรู้ด้วย ทำการปรับเปลี่ยนน้ำหนักทั้งหมดในเครือข่ายอย่างละเอียด เราเรียกกระบวนการนี้ว่าการใช้การฝังที่ได้รับการปรับแต่งอย่างละเอียด

  2. การฝังที่ปรับแต่งมาอย่างดีจะช่วยให้มีความถูกต้องแม่นยำมากขึ้น อย่างไรก็ตาม การดำเนินการนี้ก็มาพร้อมกับกำลังการประมวลผลที่เพิ่มขึ้นซึ่งจำเป็นต่อการฝึกเครือข่าย หากมีตัวอย่างมากพอ เราก็สามารถเรียนรู้การฝัง ตั้งแต่ต้นได้เหมือนกัน เราสังเกตเห็นว่าการใช้งาน S/W > 15K ตั้งแต่ต้นให้ให้ความแม่นยำอย่างมีประสิทธิภาพพอๆ กับการใช้การฝังที่มีการปรับแต่ง

เราเปรียบเทียบโมเดลลำดับต่างๆ เช่น CNN, sepCNN, RNN (LSTM และ GRU), CNN-RNN และ RNN แบบซ้อน โดยแตกต่างกันไปตามสถาปัตยกรรมโมเดล เราพบว่า sepCNN ซึ่งเป็นตัวแปรเครือข่ายคอนโวลูชัน (Convolution Network) ที่มักประหยัดข้อมูลและคำนวณได้ดีที่สุด มีประสิทธิภาพดีกว่าโมเดลอื่นๆ

โค้ดต่อไปนี้จะสร้างโมเดล 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

ฝึกโมเดล

เมื่อได้สร้างสถาปัตยกรรมโมเดลแล้ว เราจะต้องฝึกโมเดลต่อไป การฝึกจะเกี่ยวข้องกับการทำการคาดการณ์ตามสถานะปัจจุบันของโมเดล คำนวณความไม่ถูกต้องในการคาดการณ์ และอัปเดตน้ำหนักหรือพารามิเตอร์ของเครือข่ายเพื่อลดข้อผิดพลาดนี้ และทำให้โมเดลคาดการณ์ได้ดีขึ้น เราจะทำกระบวนการนี้ซ้ำจนกว่าโมเดลจะรวมเข้าด้วยกัน และไม่สามารถเรียนรู้ได้อีก มีพารามิเตอร์หลัก 3 อย่างที่ต้องเลือกสำหรับกระบวนการนี้ (ดูตาราง 2)

  • เมตริก: วิธีวัดประสิทธิภาพของโมเดลโดยใช้เมตริก เราใช้ความแม่นยำเป็นเมตริกในการทดสอบ
  • ฟังก์ชันที่สูญเสียไป: ฟังก์ชันที่ใช้ในการคำนวณค่าการสูญเสียที่กระบวนการฝึกพยายามย่อขนาดลงโดยการปรับน้ำหนักเครือข่าย สำหรับปัญหาการจัดประเภท การสูญเสียครอสเอนโทรปีทำงานได้ดี
  • ตัวเพิ่มประสิทธิภาพ: ฟังก์ชันที่ตัดสินใจว่าจะอัปเดตน้ำหนักของเครือข่ายอย่างไรตามเอาต์พุตของฟังก์ชันการสูญเสีย เราใช้เครื่องมือเพิ่มประสิทธิภาพ Adam ที่ได้รับความนิยมในการทดสอบของเรา

ใน Keras เราสามารถส่งพารามิเตอร์การเรียนรู้เหล่านี้ไปยังโมเดลโดยใช้เมธอดcompile

ตารางที่ 2: พารามิเตอร์การเรียนรู้

พารามิเตอร์การเรียนรู้ ค่า
เมตริก ความแม่นยำ
ฟังก์ชันสูญหาย - การจัดประเภทแบบไบนารี binary_crossentropy
ฟังก์ชันสูญเสียไป - การแยกประเภทแบบหลายคลาส sparse_categorical_crossentropy
ผู้เชี่ยวชาญด้านประสิทธิภาพ adam

การฝึกจริงจะเกิดขึ้นโดยใช้วิธี Fit นี่คือวิธีการที่จะใช้รอบการประมวลผลส่วนใหญ่ ทั้งนี้ขึ้นอยู่กับขนาดของชุดข้อมูล ในการทำซ้ำการฝึกแต่ละครั้ง ระบบจะใช้จำนวนตัวอย่าง batch_size จากข้อมูลการฝึกของคุณเพื่อคำนวณการลดลง และน้ำหนักจะได้รับการอัปเดตครั้งเดียวตามค่านี้ กระบวนการฝึกจะทำให้ epoch เสร็จสมบูรณ์เมื่อโมเดลเห็นชุดข้อมูลการฝึกทั้งหมดแล้ว ในตอนท้ายของแต่ละ Epoch เราจะใช้ชุดข้อมูลการตรวจสอบเพื่อประเมินว่าโมเดลเรียนรู้ได้ดีเพียงใด เราทำการฝึกซ้ำโดยใช้ชุดข้อมูล สำหรับจำนวน Epoch ที่กำหนดไว้ล่วงหน้า เราอาจเพิ่มประสิทธิภาพการทำงานในส่วนนี้ด้วยการหยุดตั้งแต่เนิ่นๆ เมื่อความถูกต้องของการตรวจสอบเสถียรระหว่าง 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]

โปรดดูตัวอย่างโค้ดสำหรับการฝึกโมเดลลำดับที่นี่