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

在本部分,我们将学习如何构建、训练和评估模型。在第 3 步中,我们根据 S/W 比率,选择使用 N 元语法模型或序列模型。现在,可以编写分类算法并对其进行训练了。为此,我们将使用 TensorFlowtf.keras API。

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

线性图层堆栈

图 9:线性层堆栈

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

构建最后一层

当我们只有 2 个类别(二元分类)时,我们的模型应输出单个概率得分。例如,对于给定的输入样本,输出 0.2 意味着“此样本属于第一类(类别 0)的置信度为 20%,有 80% 的置信度是第二类(类别 0)。要输出这样的概率得分,最后一层的激活函数应为 S 型函数,并且应使用交叉二进制函数(参见左侧图 10)。

当存在 2 个以上的类别(多类别分类)时,我们的模型应为每个类别输出一个概率得分。这些分数的总和应为 1。例如,输出 {0: 0.2, 1: 0.7, 2: 0.1} 表示“此样本属于类别 0 的置信度为 20%,样本属于类别 1 的置信度为 70%,类别 1 的置信度为 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 元语法模型和序列模型创建其余模型层。

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

构建 N 元语法模型 [选项 A]

我们将独立处理词元(不考虑字词顺序)的模型称为 N 元语法模型。简单的多层感知机(包括逻辑回归梯度提升机支持向量机模型)均属于此类别;它们无法使用任何有关文本排序的信息。

我们比较了上述某些 N 元语法模型的性能,并发现多层感知机 (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:学习参数

学习参数 价值
指标 精确度
损失函数 - 二元分类 binary_crossentropy
损失函数 - 多类别分类 sparse_categorical_crossentropy
优化器 adam

实际训练使用 fit 方法进行。此方法将用掉大部分计算周期,具体取决于数据集的大小。在每次训练迭代中,系统会使用训练数据中的 batch_size 个样本计算损失,并且权重会根据此值更新一次。一旦模型看到整个训练数据集,训练过程即会完成 epoch。在每个周期结束时,我们使用验证数据集来评估模型的学习情况。我们使用数据集重复训练预定的周期数。我们可以通过提前停止操作来优化这一点,当验证准确率在连续周期之间稳定下来,表明模型不再训练时。

训练超参数 价值
学习速率 1e-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]

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