代码示例 / 自然语言处理 / 使用 KerasNLP 进行语义相似性

使用 KerasNLP 进行语义相似性

作者: Anshuman Mishra
创建日期: 2023/02/25
最后修改日期: 2023/02/25
描述: 使用 KerasNLP 的预训练模型进行语义相似性任务。

在 Colab 中查看 GitHub 源代码


介绍

语义相似性是指确定两个句子在意义上的相似程度的任务。我们已经在这个例子中看到如何使用 SNLI(斯坦福自然语言推断)语料库来预测句子的语义相似性,使用 HuggingFace Transformers 库。在本教程中,我们将学习如何使用KerasNLP(Keras API 的扩展)来完成相同的任务。此外,我们还将发现 KerasNLP 如何有效减少样板代码并简化构建和使用模型的过程。有关 KerasNLP 的更多信息,请参考KerasNLP 的官方文档

本指南分为以下部分:

  1. 设置、任务定义及建立基线。
  2. 使用 BERT 建立基线
  3. 保存和重新加载模型。
  4. 使用模型进行推断
  5. 使用 RoBERTa 提高准确性

设置

以下指南使用Keras Core来在tensorflowjaxtorch中工作。对 Keras Core 的支持已经集成到 KerasNLP中,只需更改下面的KERAS_BACKEND环境变量即可更改要使用的后端。我们在下面选择jax后端,这将使我们的训练步骤特别快速。

!pip install -q --upgrade keras-nlp
!pip install -q --upgrade keras  # 升级到 Keras 3.
import numpy as np
import tensorflow as tf
import keras
import keras_nlp
import tensorflow_datasets as tfds

要加载 SNLI 数据集,我们使用 tensorflow-datasets 库,该库总共包含超过 550,000 个样本。然而,为了确保这个例子运行快速,我们仅使用 20% 的训练样本。


SNLI 数据集概述

数据集中每个样本包含三个部分:hypothesispremiselabelpremise 表示提供给这对作者的原始标题,而 hypothesis 是由这对作者创建的假设标题。标签由注释员分配,以指示两个句子之间的相似性。

该数据集包含三个可能的相似性标签值:矛盾、蕴含和中立。矛盾代表完全不同的句子,而蕴含表示相似意义的句子。最后,中立指的是无法在它们之间建立明显相似性或差异性的句子。

snli_train = tfds.load("snli", split="train[:20%]")
snli_val = tfds.load("snli", split="validation")
snli_test = tfds.load("snli", split="test")

# 这是我们训练样本的示例,我们随机选择了四个样本:
sample = snli_test.batch(4).take(1).get_single_element()
sample
{'hypothesis': <tf.Tensor: shape=(4,), dtype=string, numpy=
 array([b'A girl is entertaining on stage',
        b'A group of people posing in front of a body of water.',
        b"The group of people aren't inide of the building.",
        b'The people are taking a carriage ride.'], dtype=object)>,
 'label': <tf.Tensor: shape=(4,), dtype=int64, numpy=array([0, 0, 0, 0])>,
 'premise': <tf.Tensor: shape=(4,), dtype=string, numpy=
 array([b'A girl in a blue leotard hula hoops on a stage with balloon shapes in the background.',
        b'A group of people taking pictures on a walkway in front of a large body of water.',
        b'Many people standing outside of a place talking to each other in front of a building that has a sign that says "HI-POINTE."',
        b'Three people are riding a carriage pulled by four horses.'],
       dtype=object)>}

预处理

在我们的数据集中,我们发现一些样本缺失或标签错误,使用值 -1 来表示。为了确保我们模型的准确性和可靠性,我们只需从数据集中过滤掉这些样本。

def filter_labels(sample):
    return sample["label"] >= 0

这是一个实用函数,将示例拆分为适合 (x, y) 的元组。 for model.fit()。默认情况下,keras_nlp.models.BertClassifier会在训练期间使用"[SEP]"标记对原始字符串进行标记化和打包。因此,这种标签拆分就是我们所需进行的所有数据准备。

def split_labels(sample):
    x = (sample["hypothesis"], sample["premise"])
    y = sample["label"]
    return x, y


train_ds = (
    snli_train.filter(filter_labels)
    .map(split_labels, num_parallel_calls=tf.data.AUTOTUNE)
    .batch(16)
)
val_ds = (
    snli_val.filter(filter_labels)
    .map(split_labels, num_parallel_calls=tf.data.AUTOTUNE)
    .batch(16)
)
test_ds = (
    snli_test.filter(filter_labels)
    .map(split_labels, num_parallel_calls=tf.data.AUTOTUNE)
    .batch(16)
)

使用BERT建立基线。

我们使用KerasNLP中的BERT模型为我们的语义相似性任务建立基线。keras_nlp.models.BertClassifier类在BERT骨干网络上附加了分类头,将骨干输出映射到适合分类任务的logit输出。这大大减少了对自定义代码的需求。

KerasNLP模型具有内置的标记化能力,默认情况下根据所选模型处理标记化。不过,用户也可以根据特定需求使用自定义预处理技术。如果我们将元组作为输入,模型将对所有字符串进行标记化,并使用"[SEP]"分隔符将它们连接起来。

我们使用这个带有预训练权重的模型,并且可以使用from_preset()方法使用我们自己的预处理器。对于SNLI数据集,我们将num_classes设置为3。

bert_classifier = keras_nlp.models.BertClassifier.from_preset(
    "bert_tiny_en_uncased", num_classes=3
)

请注意,BERT Tiny模型只有4,386,307个可训练参数。

KerasNLP任务模型提供编译默认值。我们现在可以通过调用fit()方法来训练我们刚刚实例化的模型。

bert_classifier.fit(train_ds, validation_data=val_ds, epochs=1)
 6867/6867 ━━━━━━━━━━━━━━━━━━━━ 61s 8ms/step - loss: 0.8732 - sparse_categorical_accuracy: 0.5864 - val_loss: 0.5900 - val_sparse_categorical_accuracy: 0.7602

<keras.src.callbacks.history.History at 0x7f4660171fc0>

我们的BERT分类器在验证集上达到了约76%的准确率。现在,让我们评估它在测试集上的表现。

评估训练模型在测试数据上的表现。

bert_classifier.evaluate(test_ds)
 614/614 ━━━━━━━━━━━━━━━━━━━━ 2s 3ms/step - loss: 0.5815 - sparse_categorical_accuracy: 0.7628

[0.5895748734474182, 0.7618078589439392]

我们的基线BERT模型在测试集上的准确率也达到了约76%。现在,让我们尝试通过重新编译模型并稍微提高学习率来提高其性能。

bert_classifier = keras_nlp.models.BertClassifier.from_preset(
    "bert_tiny_en_uncased", num_classes=3
)
bert_classifier.compile(
    loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    optimizer=keras.optimizers.Adam(5e-5),
    metrics=["accuracy"],
)

bert_classifier.fit(train_ds, validation_data=val_ds, epochs=1)
bert_classifier.evaluate(test_ds)
 6867/6867 ━━━━━━━━━━━━━━━━━━━━ 59s 8ms/step - accuracy: 0.6007 - loss: 0.8636 - val_accuracy: 0.7648 - val_loss: 0.5800
 614/614 ━━━━━━━━━━━━━━━━━━━━ 2s 3ms/step - accuracy: 0.7700 - loss: 0.5692

[0.578984260559082, 0.7686278820037842]

仅仅调整学习率不足以提升性能,准确率仍保持在76%左右。让我们再试一次,这次使用keras.optimizers.AdamW和学习率调度。

class TriangularSchedule(keras.optimizers.schedules.LearningRateSchedule):
    """前 `warmup` 步骤线性上升,然后在 `total` 步骤线性衰减到零。"""

    def __init__(self, rate, warmup, total):
        self.rate = rate
        self.warmup = warmup
        self.total = total

    def get_config(self):
        config = {"rate": self.rate, "warmup": self.warmup, "total": self.total}
        return config

    def __call__(self, step):
        step = keras.ops.cast(step, dtype="float32")
        rate = keras.ops.cast(self.rate, dtype="float32")
        warmup = keras.ops.cast(self.warmup, dtype="float32")
        total = keras.ops.cast(self.total, dtype="float32")

        warmup_rate = rate * step / self.warmup
        cooldown_rate = rate * (total - step) / (total - warmup)
        triangular_rate = keras.ops.minimum(warmup_rate, cooldown_rate)
        return keras.ops.maximum(triangular_rate, 0.0)


bert_classifier = keras_nlp.models.BertClassifier.from_preset(
    "bert_tiny_en_uncased", num_classes=3
)

# 获取训练批次的总数。
# 这需要遍历数据集以过滤所有 -1 标签。
epochs = 3
total_steps = sum(1 for _ in train_ds.as_numpy_iterator()) * epochs
warmup_steps = int(total_steps * 0.2)

bert_classifier.compile(
    loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    optimizer=keras.optimizers.AdamW(
        TriangularSchedule(1e-4, warmup_steps, total_steps)
    ),
    metrics=["accuracy"],
)

bert_classifier.fit(train_ds, validation_data=val_ds, epochs=epochs)
第 1 轮/共 3 轮
 6867/6867 ━━━━━━━━━━━━━━━━━━━━ 59s 8ms/step - 准确率: 0.5457 - 损失: 0.9317 - 验证准确率: 0.7633 - 验证损失: 0.5825
第 2 轮/共 3 轮
 6867/6867 ━━━━━━━━━━━━━━━━━━━━ 55s 8ms/step - 准确率: 0.7291 - 损失: 0.6515 - 验证准确率: 0.7809 - 验证损失: 0.5399
第 3 轮/共 3 轮
 6867/6867 ━━━━━━━━━━━━━━━━━━━━ 55s 8ms/step - 准确率: 0.7708 - 损失: 0.5695 - 验证准确率: 0.7918 - 验证损失: 0.5214

<keras.src.callbacks.history.History at 0x7f45645b3370>

成功!利用学习率调度器和 AdamW 优化器,我们的验证准确率提高到了约 79%。

现在,让我们评估我们的最终模型在测试集上的表现如何。

bert_classifier.evaluate(test_ds)
 614/614 ━━━━━━━━━━━━━━━━━━━━ 2s 3ms/step - 准确率: 0.7956 - 损失: 0.5128

[0.5245093703269958, 0.7890879511833191]

我们的 Tiny BERT 模型在测试集上获得了约 79% 的准确率, 这得益于使用学习率调度器。这是对我们之前结果的显著改进。微调预训练的 BERT 模型在自然语言处理任务中可以成为一个强大的工具,甚至像 Tiny BERT 这样的小模型也能取得令人印象深刻的结果。

现在暂时保存我们的模型,继续学习如何进行推断。


保存和重新加载模型

bert_classifier.save("bert_classifier.keras")
restored_model = keras.models.load_model("bert_classifier.keras")
restored_model.evaluate(test_ds)
 614/614 ━━━━━━━━━━━━━━━━━━━━ 2s 3ms/step - 损失: 0.5128 - 稀疏分类准确率: 0.7956

[0.5245093703269958, 0.7890879511833191]

使用模型进行推断。

让我们看看如何使用 KerasNLP 模型进行推断

# 转换为假设-前提对,以便通过模型前向传递
sample = (sample["hypothesis"], sample["premise"])
sample
(<tf.Tensor: shape=(4,), dtype=string, numpy=
 array([b'A girl is entertaining on stage',
        b'A group of people posing in front of a body of water.',
        b"The group of people aren't inide of the building.",
        b'The people are taking a carriage ride.'], dtype=object)>,
 <tf.Tensor: shape=(4,), dtype=string, numpy=
 array([b'A girl in a blue leotard hula hoops on a stage with balloon shapes in the background.',
        b'A group of people taking pictures on a walkway in front of a large body of water.',
        b'Many people standing outside of a place talking to each other in front of a building that has a sign that says "HI-POINTE."',
        b'Three people are riding a carriage pulled by four horses.'],
       dtype=object)>)

KerasNLP 模型中的默认预处理器会自动处理输入标记化,因此我们不需要显式地进行标记化。

predictions = bert_classifier.predict(sample)


def softmax(x):
    return np.exp(x) / np.exp(x).sum(axis=0)


# 获取具有最大概率的类别预测
predictions = softmax(predictions)
 1/1 ━━━━━━━━━━━━━━━━━━━━ 1s 711ms/step

使用 RoBERTa 提高准确率

现在我们已经建立了一个基线,我们可以尝试通过实验不同模型来提高我们的结果。得益于 KerasNLP,在相同数据集上微调 RoBERTa 检查点只需几行代码。

# 从预设初始化 RoBERTa
roberta_classifier = keras_nlp.models.RobertaClassifier.from_preset(
    "roberta_base_en", num_classes=3
)

roberta_classifier.fit(train_ds, validation_data=val_ds, epochs=1)

roberta_classifier.evaluate(test_ds)
 6867/6867 ━━━━━━━━━━━━━━━━━━━━ 2049s 297ms/step - 损失: 0.5509 - 稀疏分类准确率: 0.7740 - 验证损失: 0.3292 - 验证稀疏分类准确率: 0.8789
 614/614 ━━━━━━━━━━━━━━━━━━━━ 56s 88ms/step - 损失: 0.3307 - 稀疏分类准确率: 0.8784

[0.33771008253097534, 0.874796450138092]

RoBERTa 基础模型的可训练参数远远超过 BERT Tiny 模型,几乎多了 30 倍,参数数量达到 124,645,635。 因此,在 P100 GPU 上训练大约花费了 1.5 小时。然而,性能提升显著,验证和测试分割的准确率均提高至 88%。借助 RoBERTa,我们能够在 P100 GPU 上拟合最大批量大小为 16。

尽管使用了不同的模型,但使用 RoBERTa 进行推断的步骤与使用 BERT 的步骤相同!

predictions = roberta_classifier.predict(sample)
print(tf.math.argmax(predictions, axis=1).numpy())
 1/1 ━━━━━━━━━━━━━━━━━━━━ 4s 4s/step
[0 0 0 0]

我们希望本教程能够帮助您展示使用不同模型进行推断的简单性和有效性 使用 KerasNLP 和 BERT 进行语义相似性任务。

在本教程中,我们演示了如何使用预训练的 BERT 模型建立基准,并通过只需几行代码训练更大规模的 RoBERTa 模型来提高性能。

KerasNLP 工具箱提供了一系列模块化构建块,用于文本预处理,包括预训练的最先进模型和低级 Transformer 编码器层。我们相信,这使得实验自然语言解决方案变得更加便捷和高效。