代码示例 / 自然语言处理 / 使用 KerasHub 实现语义相似度

使用 KerasHub 实现语义相似度

作者: Anshuman Mishra
创建日期 2023/02/25
最后修改日期 2023/02/25
描述: 使用 KerasHub 中的预训练模型完成语义相似度任务。

ⓘ 本示例使用 Keras 3

在 Colab 中查看 GitHub 源代码


简介

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

本指南分为以下几个部分

  1. 设置、任务定义和建立基准。
  2. 使用 BERT 建立基准
  3. 保存和重新加载模型。
  4. 使用模型执行推理。使用 RoBERTa 提高准确率

设置

以下指南使用 Keras Core,可以在 tensorflowjaxtorch 中的任何一个中工作。KerasHub 内置了对 Keras Core 的支持,只需更改下面的 KERAS_BACKEND 环境变量即可更改您想要使用的后端。我们在下面选择了 jax 后端,这将使我们在下面的训练步骤特别快。

!pip install -q --upgrade keras-hub
!pip install -q --upgrade keras  # Upgrade to Keras 3.
import numpy as np
import tensorflow as tf
import keras
import keras_hub
import tensorflow_datasets as tfds

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


SNLI 数据集概述

数据集中的每个样本包含三个组成部分:hypothesis(假设)、premise(前提)和 label(标签)。 premise 代表提供给配对作者的原始标题,而 hypothesis 指的是配对作者创建的假设标题。标签由标注者分配,用于表示两个句子之间的相似度。

数据集包含三种可能的相似度标签值:Contradiction(矛盾)、Entailment(蕴含)和 Neutral(中性)。Contradiction 代表完全不相似的句子,而 Entailment 表示含义相似的句子。最后,Neutral 指的是无法在句子之间建立明确的相似或不相似关系的句子。

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

# Here's an example of how our training samples look like, where we randomly select
# four samples:
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

这是一个实用函数,将示例分割成适用于 model.fit()(x, y) 元组。默认情况下,keras_hub.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 建立基准。

我们使用 KerasHub 中的 BERT 模型为我们的语义相似度任务建立基准。keras_hub.models.BertClassifier 类将一个分类头连接到 BERT Backbone,将 Backbone 输出映射到适用于分类任务的 logit 输出。这显著减少了自定义代码的需求。

KerasHub 模型具有内置的分词功能,默认情况下根据所选模型处理分词。但是,用户也可以根据自己的特定需求使用自定义预处理技术。如果我们传递一个元组作为输入,模型将对所有字符串进行分词,并使用 "[SEP]" 分隔符将它们连接起来。

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

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

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

KerasHub 任务模型带有默认的编译设置。我们现在可以通过调用 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_hub.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):
    """Linear ramp up for `warmup` steps, then linear decay to zero at `total` steps."""

    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_hub.models.BertClassifier.from_preset(
    "bert_tiny_en_uncased", num_classes=3
)

# Get the total count of training batches.
# This requires walking the dataset to filter all -1 labels.
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)
Epoch 1/3
 6867/6867 ━━━━━━━━━━━━━━━━━━━━ 59s 8ms/step - accuracy: 0.5457 - loss: 0.9317 - val_accuracy: 0.7633 - val_loss: 0.5825
Epoch 2/3
 6867/6867 ━━━━━━━━━━━━━━━━━━━━ 55s 8ms/step - accuracy: 0.7291 - loss: 0.6515 - val_accuracy: 0.7809 - val_loss: 0.5399
Epoch 3/3
 6867/6867 ━━━━━━━━━━━━━━━━━━━━ 55s 8ms/step - accuracy: 0.7708 - loss: 0.5695 - val_accuracy: 0.7918 - val_loss: 0.5214

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

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

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

bert_classifier.evaluate(test_ds)
 614/614 ━━━━━━━━━━━━━━━━━━━━ 2s 3ms/step - accuracy: 0.7956 - loss: 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 - loss: 0.5128 - sparse_categorical_accuracy: 0.7956

[0.5245093703269958, 0.7890879511833191]

使用模型执行推理。

让我们看看如何使用 KerasHub 模型执行推理

# Convert to Hypothesis-Premise pair, for forward pass through model
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)>)

KerasHub 模型中的默认预处理器会自动处理输入分词,因此我们无需显式执行分词。

predictions = bert_classifier.predict(sample)


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


# Get the class predictions with maximum probabilities
predictions = softmax(predictions)
 1/1 ━━━━━━━━━━━━━━━━━━━━ 1s 711ms/step

使用 RoBERTa 提高准确率

现在我们已经建立了基准,我们可以尝试通过试验不同的模型来改进我们的结果。得益于 KerasHub,仅用几行代码就可以轻松地在同一数据集上微调 RoBERTa 检查点。

# Inittializing a RoBERTa from preset
roberta_classifier = keras_hub.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 - loss: 0.5509 - sparse_categorical_accuracy: 0.7740 - val_loss: 0.3292 - val_sparse_categorical_accuracy: 0.8789
 614/614 ━━━━━━━━━━━━━━━━━━━━ 56s 88ms/step - loss: 0.3307 - sparse_categorical_accuracy: 0.8784

[0.33771008253097534, 0.874796450138092]

RoBERTa base 模型的可训练参数明显多于 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]

我们希望本教程能帮助您了解使用 KerasHub 和 BERT 进行语义相似度任务的简便性和有效性。

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

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