4단계: 모델 빌드, 학습, 평가

이 섹션에서는 모델을 빌드, 학습, 평가합니다. 3단계에서는 S/W 비율을 사용하여 N-그램 모델 또는 시퀀스 모델을 사용하기로 선택했습니다. 이제 분류 알고리즘을 작성하고 학습할 차례입니다. 이를 위해 tf.keras API와 함께 TensorFlow를 사용합니다.

Keras를 사용하여 머신러닝 모델을 빌드하는 작업은 Lego 블록을 조립하는 것처럼 여러 레이어로 데이터를 조합하는 데이터 처리 빌딩 블록입니다. 이러한 레이어를 사용하면 입력에서 수행할 변환 시퀀스를 지정할 수 있습니다. 학습 알고리즘은 단일 텍스트 입력을 사용하고 단일 분류를 출력하므로 Sequential 모델 API를 사용하여 레이어의 선형 스택을 만들 수 있습니다.

레이어의 선형 스택

그림 9: 선형 스택 스택

입력 레이어와 중간 레이어는 n-gram 또는 시퀀스 모델 빌드 여부에 따라 다르게 구성됩니다. 그러나 모델 유형과 관계없이 마지막 레이어는 특정 문제에 대해 동일합니다.

마지막 레이어 생성

클래스가 2개 (이진 분류)만 있을 때 모델은 하나의 확률 점수를 출력합니다. 예를 들어 특정 입력 샘플에 대해 0.2을 출력하면 '이 샘플이 첫 번째 클래스(클래스 1)에 있다는 20% 의 신뢰도, 두 번째 클래스(클래스 0)에 있다는 80% 의 신뢰도'를 의미합니다. 이러한 확률 점수를 출력하려면 마지막 레이어의 활성화 함수시그모이드 함수여야 하고 모델 학습에 사용되는 손실 함수 1) {/1) {/1

세 개 이상의 클래스 (다중 클래스 분류)가 있는 경우 모델은 클래스당 하나의 확률 점수를 출력합니다. 이러한 점수의 합계는 1이어야 합니다. 예를 들어 {0: 0.2, 1: 0.7, 2: 0.1}을 출력하면 '이 샘플이 클래스 0에 있고 70% 가 클래스 1에 있다는 20% 의 신뢰도, 10% 는 이 클래스가 클래스 2에 있다는 신뢰도를 의미합니다.' 이러한 점수를 출력하려면 마지막 레이어의 활성화 함수가 소프트맥스여야 하고 모델 학습에 사용되는 손실 함수는 범주형 교차 엔트로피여야 합니다. 그림 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

다음 두 섹션에서는 N-그램 모델 및 시퀀스 모델의 나머지 모델 레이어를 만드는 방법을 살펴봅니다.

S/W 비율이 작으면 n-gram 모델이 시퀀스 모델보다 더 우수한 성능을 나타내는 것으로 확인되었습니다. 시퀀스 모델은 작은 벡터가 많으면 더 좋습니다. 이는 임베딩 관계가 밀집된 공간에서 학습되기 때문이며 대부분의 샘플에서 발생합니다.

N-그램 모델 빌드 [옵션 A]

토큰을 독립적으로 처리하는 모델 (단어 순서를 고려하지 않음)을 N-그램 모델이라고 합니다. 간단한 다층 퍼셉트론 (로지스틱 회귀 포함), 경사 부스팅 머신벡터 머신 모델이 모두 이 카테고리에 속하며, 텍스트 정렬에 대한 정보를 활용할 수 없습니다.

앞서 언급한 일부 N-그램 모델의 성능을 비교한 결과, 일반적으로 다중 레이어 퍼셉트론 (MLP)의 성능이 다른 옵션보다 더 좋다는 점을 확인했습니다. MLP는 정의와 이해가 단순하고 정확성이 높으며 비교적 적은 연산이 필요합니다.

다음 코드는 tf.keras의 2레이어 MLP 모델을 정의하여 학습 샘플에 과적합을 방지하기 위해 정규화용 드롭아웃 레이어를 추가합니다.

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는 여러 코퍼스 (주로 위키백과)에 대해 교육을 받았습니다. GloVe 임베딩 버전을 사용하여 시퀀스 모델 학습을 테스트한 결과 사전 학습된 임베딩의 가중치를 고정하고 네트워크의 나머지 부분만 학습시킨 경우 모델이 잘 작동하지 않는 것을 확인했습니다. 임베딩 레이어가 학습된 컨텍스트가 Google에서 사용 중인 컨텍스트와 다를 수 있기 때문일 수 있습니다.

Wikipedia 데이터에서 학습된 GloVe 임베딩은 IMDb 데이터 세트의 언어 패턴과 일치하지 않을 수 있습니다. 추론된 관계에 업데이트가 필요할 수 있습니다. 즉, 임베딩 가중치에 상황별 조정이 필요할 수 있습니다. 이 작업은 다음 두 단계로 이루어집니다.

  1. 첫 번째 실행에서는 임베딩 레이어 가중치가 고정되어 있으므로 네트워크의 나머지 부분을 학습할 수 있습니다. 이 실행이 끝나면 모델 가중치가 초기화되지 않은 값보다 훨씬 더 좋은 상태에 도달합니다. 두 번째 실행에서는 임베딩 레이어도 학습하여 네트워크의 모든 가중치를 미세하게 조정합니다. 이를 미세 조정된 임베딩을 사용하는 것으로 지칭합니다.

  2. 미세 조정된 임베딩의 정확성이 더 높습니다. 그러나 이는 네트워크를 학습시키는 데 필요한 컴퓨팅 성능이 증가함에 따라 비용이 발생합니다. 샘플 수가 충분하면 임베딩을 처음부터 정확히 배울 수 있습니다. S/W > 15K의 경우 처음부터 처음부터 세부 조정된 임베딩을 사용하는 것과 동일한 정확성을 얻게 됩니다.

CNN, sepCNN, RNN (LSTM & GRU), CNN-RNN, 스택된 RNN과 같은 다양한 시퀀스 모델을 모델 아키텍처를 다르게 비교했습니다. 또한 데이터 효율성과 컴퓨팅 효율성이 뛰어난 컨볼루셔널 네트워크 변형인 sepCNN이 다른 모델보다 성능이 우수한 것으로 확인되었습니다.

다음 코드는 4레이어 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 참조).

  • 측정항목: 측정항목을 사용하여 모델의 성능을 측정하는 방법 실험에서는 측정항목으로 정확성을 사용했습니다.
  • 손실 함수: 학습 프로세스에서 네트워크 가중치를 조정하여 최소화하려고 시도하는 손실 값을 계산하는 데 사용되는 함수입니다. 분류 문제의 경우 교차 엔트로피 손실이 적합합니다.
  • 최적화 도구: 손실 함수의 출력에 따라 네트워크 가중치가 업데이트되는 방식을 결정하는 함수입니다. 실험에서 인기 있는 Adam 옵티마이저를 사용했습니다.

Keras에서 compile 메서드를 사용하여 이러한 학습 매개변수를 모델에 전달할 수 있습니다.

학습 매개변수
측정항목 accuracy
손실 함수 - 이진 분류 바이너리_교차엔트로피
손실 함수 - 다중 클래스 분류 희소_카테고리_교차 엔트로피
옵티마이저 Adam

표 2: 학습 매개변수

실제 학습은 fit 메서드를 사용하여 이루어집니다. 데이터 세트의 크기에 따라 대부분의 컴퓨팅 주기가 소비되는 방법입니다. 각 학습 반복에서 학습 데이터의 batch_size 수는 손실을 계산하는 데 사용되며 이 값을 기준으로 가중치가 한 번 업데이트됩니다. 모델이 전체 학습 데이터 세트를 확인하면 학습 프로세스는 epoch를 완료합니다. 각 에포크가 끝나면 검증 데이터 세트를 사용하여 모델이 얼마나 잘 학습하고 있는지 평가합니다. 데이터 세트를 사용하여 미리 정해진 세대 동안 학습을 반복합니다. 연속성 에포크 간의 검증 정확성이 안정화되면 조기에 중단하여 모델이 더 이상 학습하지 않음을 보여줌으로써 이를 최적화할 수 있습니다.

학습 초매개변수
학습률 1e~3
에포크 1000
배치 크기 512
조기 중단 매개변수: val_loss, engagement: 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]

여기에서 시퀀스 모델 학습을 위한 코드 예시를 확인하세요.