第 4 步:构建、训练和评估模型

在本部分中,我们将努力构建、训练和评估我们的模型。在第 3 步中,我们选择使用 n-gram 模型或序列模型(通过我们的 S/W 比率)。现在该编写分类算法并进行训练了。为此,我们将结合使用 TensorFlowtf.keras API。

使用 Keras 构建机器学习模型的重点在于组装层、数据处理组件,就像我们组装乐高积木一样。通过这些层,我们可以指定要对输入执行的转换序列。由于我们的学习算法接受单个文本输入并输出单个分类,因此我们可以使用 Sequential 模型 API 创建层的线性堆栈。

线性图层堆栈

图 9:线性图层堆栈

输入层和中间层的构建方式有所不同,具体取决于我们构建的是 n-gram 还是序列模型。但无论模型类型如何,对于给定问题,最后一层都是相同的。

构建最后一层

如果只有 2 个类别(二元分类),我们的模型应输出单个概率得分。例如,如果给定输入样本输出 0.2,则表示“该样本属于第一类(类别 1)的置信度为 20%,属于第二类(类别 0)的概率为 80%”。如需输出这种概率得分,最后一层的激活函数应为 S 型函数训练 1 的 1 样本 需要 1 对图 1 进行求解,请 1 对 1 进行交叉训练。

如果类别超过 2 个(多类别分类),我们的模型应为每个类别输出一个概率得分。这些得分的总和应为 1。例如,输出 {0: 0.2, 1: 0.7, 2: 0.1} 表示“本示例的类别为 0 的置信度为 20%,类别为 1 的概率为 70%,类别 2 的概率为 10%”。如需输出这些得分,最后一层的激活函数应为 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

以下两部分介绍了为 n-gram 模型和序列模型创建剩余的模型层。

S/W 比率较小时,我们发现 n-gram 模型的效果优于序列模型。当有大量小型密集向量时,序列模型更好。这是因为嵌套关系是在密集空间中学习的,在许多样本上效果最好。

构建 n-gram 模型 [选项 A]

我们将独立处理令牌(不考虑字词顺序)的模型称为 n-gram 模型。简单的多层感知器(包括逻辑回归)、梯度提升机支持向量机模型都属于此类别,它们无法利用任何与文本排序相关的信息。

我们比较了上面提到的一些 n-gram 模型的性能,并发现多层感知器 (MLP) 通常比其他选项效果更好。MLP 易于定义和理解,提供良好的准确性,并且需要的计算相对较少。

以下代码在 tf.keras 中定义了一个两层 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 嵌入版本测试了序列模型,并观察到,如果我们冻结预训练嵌入的权重并仅训练网络的其余部分,则模型表现不佳。这可能是因为训练嵌入层的上下文可能与我们使用它的层不同。

根据维基百科数据训练的 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)。

  • 指标:如何使用指标来衡量模型的性能。我们在实验中使用准确率作为指标。
  • 损失函数:用于计算训练过程随后通过调整网络权重来尽量减少的损失值的函数。对于分类问题,交叉熵损失效果很好。
  • 优化工具:用于确定如何根据损失函数的输出更新网络权重的函数。我们在实验中使用了热门的 Adam 优化器。

在 Keras 中,我们可以使用 compile 方法将这些学习参数传递给模型。

学习参数
指标 准确性
损失函数 - 二元分类 二元交叉熵
损失函数 - 多类别分类 稀疏分类类别交叉熵
优化器 亚当

表 2:学习参数

实际训练是使用 fit 方法进行的。根据数据集的大小,大多数计算周期都将采用这种方法。在每次训练迭代中,来自训练数据的 batch_size 个样本都会用于计算损失,权重会根据此值更新一次。模型看到整个训练数据集后,训练过程会完成 epoch。在每个周期结束时,我们都会使用验证数据集来评估模型的学习效果。我们使用该数据集重复训练一定数量的周期。如果验证准确率在连续周期之间达到稳定状态,则表明模型已不再进行训练,那么我们可能会提前停止优化。

训练超参数
学习速率 1 - 3
周期数 1000
批次大小 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]

如需查看训练序列模型的代码示例,请点击此处