1.引言
1.1.RoBERTa模型开发背景
BERT模型自发布以来,就以其卓越的性能和广泛的应用领域,在NLP领域引起了巨大的轰动。BERT通过预训练大量文本数据,学习到了丰富的语言表示,并在多个NLP任务上取得了显著的效果提升。然而,随着研究的深入和应用的广泛,人们发现BERT在某些任务上仍存在一些局限性,这为RoBERTa的研发提供了契机。
为了克服BERT的局限性,Facebook AI团队在研发RoBERTa时,进行了一系列有针对性的改进。首先,他们使用了更大规模的训练数据集,包括CommonCrawl和OpenWebText等,使得模型能够学习到更多的语言知识和模式。其次,他们对数据预处理进行了优化,去除了一些无用的标点符号和空格,进一步提高了模型的学习效率。此外,RoBERTa还采用了动态遮蔽策略,每次训练时都会生成新的遮蔽模式,这有助于模型学习更丰富的语言表征。最后,RoBERTa移除了BERT中的下一句预测任务,这一改进使得模型在多个任务上取得了更好的性能。
RoBERTa的研发不仅提高了模型本身的性能,还为NLP领域的研究人员提供了新的工具和思路。由于其出色的性能,RoBERTa很快被广泛应用于各种NLP任务中,包括文本分类、命名实体识别、问答系统等。此外,RoBERTa的发布还推动了NLP技术的进一步发展和应用,为人工智能领域的进步做出了重要贡献。
1.2.RoBERTa模型概述
1.2.1.RoBERTa模型的结构和工作原理
-
结构:
- RoBERTa模型的核心是Transformer模型,它由多个编码器和解码器堆叠而成。但在RoBERTa中,主要关注的是编码器部分,因为RoBERTa是一个预训练语言模型,主要用于生成文本的嵌入表示。
- 每个编码器由多层的自注意力机制和前馈神经网络组成。这些层允许模型捕捉输入序列中的上下文关系,并提取语义信息。
-
工作原理:
- 输入:RoBERTa的输入是一段文本序列,通常是经过分词处理的句子。分词后的文本会被转化为词嵌入向量,同时还会添加位置编码向量来表示单词在句子中的位置信息。
- 处理:这些向量会作为输入送入Transformer的编码器部分进行处理。通过多层自注意力机制和前馈神经网络的堆叠,模型能够学习到丰富的语义表示。
- 输出:模型的输出是文本序列的嵌入表示,这些表示可以被用于各种NLP任务,如文本分类、命名实体识别等。
1.2.2.RoBERTa模型的优势
- 性能提升:通过更大规模的数据集、更长的训练时间、动态遮蔽策略以及移除下一句预测任务等优化手段,RoBERTa在多个NLP任务上取得了比BERT更高的性能。
- 通用性强:作为一个预训练语言模型,RoBERTa学习到的语言表示具有很强的通用性,可以很容易地迁移到各种NLP任务中。
- 易于使用:RoBERTa提供了预训练的模型权重,用户可以直接在自己的任务上进行微调,无需从头开始训练。
1.2.3.RoBERTa模型的劣势
- 计算资源需求大:由于RoBERTa使用了更大的数据集和更长的训练时间,因此训练RoBERTa模型需要更多的计算资源,包括GPU和内存。
- 模型复杂度高:RoBERTa模型的结构相对复杂,包含多个编码器和自注意力层,这使得模型的参数量较大,对硬件资源的要求也更高。
- 对长文本的处理能力有限:虽然RoBERTa通过自注意力机制能够捕捉长文本中的上下文关系,但对于非常长的文本序列,RoBERTa的处理能力仍然有限,可能无法充分利用所有的信息。
综上所述,RoBERTa模型在NLP领域具有显著的优势,但也存在一些劣势。在实际应用中,需要根据具体任务和资源条件来选择合适的模型。
1.3.句子嵌入技术
句子嵌入技术是一种自然语言处理(NLP)中用于将文本数据转换为数值向量的方法。这种技术的核心目标是在保留文本语义信息的同时,将高维的文本数据映射到低维的连续向量空间中。
-
语义保留:句子嵌入技术能够将句子中的语义信息编码到向量中,使得语义相近的句子在向量空间中的距离较近。
-
降维:通过将文本转换为固定大小的向量,句子嵌入技术解决了传统文本表示方法中的高维稀疏问题。
-
计算效率:句子嵌入使得文本的存储和计算更加高效,因为向量运算比原始文本处理要快得多。
-
应用广泛:句子嵌入向量可以作为特征输入到各种机器学习模型中,支持文本分类、情感分析、信息检索、机器翻译等多种NLP任务。
-
预训练模型:存在多种预训练的句子嵌入模型,如BERT、GPT、Sentence-BERT等,这些模型在大量文本数据上训练,能够捕捉丰富的语言特征。
-
跨语言能力:一些句子嵌入模型能够处理多种语言,为跨语言的文本分析提供了可能。
-
可定制性:根据不同的应用需求,可以对句子嵌入模型进行微调,以更好地适应特定领域的文本数据。
-
深度学习集成:现代的句子嵌入技术通常与深度学习架构结合,利用神经网络的强大能力来学习文本的复杂表示。
句子嵌入技术是连接人类语言和机器学习的桥梁,为文本数据的自动化分析和理解提供了强大的工具。随着技术的进步,我们可以期待句子嵌入在准确性和应用范围上都将持续提升。
1.4.主要内容
BERT和RoBERTa模型在语义文本相似性任务中表现出色,它们能够评估两个输入句子的相似度。然而,当面对一个包含大量句子的集合,并需要在这个集合中寻找最相似的句对时,计算成本会显著增加。例如,对于一个包含10000个句子的集合,逐对比较将需要进行( \frac{n(n-1)}{2} )次推理,其中n是句子的数量。在V100 GPU上,这可能意味着需要65小时的计算时间。
为了解决这一时间效率问题,一种有效的方法是利用模型输出的聚合或特定标记(如BERT中的[CLS]标记)作为句子的嵌入表示。通过这种方式,可以大幅减少寻找相似句子对所需的时间,从65小时降低到仅需5秒。
尽管直接使用RoBERTa生成的句子嵌入可能质量不高,但通过微调模型,比如使用孪生网络结构,可以显著提升嵌入的质量,使其能够捕捉到句子的深层语义。这种微调后的RoBERTa模型可以应用于多种任务,包括:
- 大规模语义相似性比较。
- 文本聚类。
- 基于语义的搜索和信息检索。
本文将展示如何通过孪生网络对RoBERTa进行微调,使其能够生成具有丰富语义信息的句子嵌入,并在语义搜索和聚类任务中应用这些嵌入。这种微调技术最初在Sentence-BERT项目中提出。通过这种方法,我们能够提高模型在各种语义相关任务中的性能和应用范围。
2.构建RoBERTa模型
2.1.设置
# 升级 keras-nlp 和 keras 库到最新版本(如果可用)
!pip install -q --upgrade keras-nlp
!pip install -q --upgrade keras
# 设置 Keras 的后端为 TensorFlow
import os
os.environ["KERAS_BACKEND"] = "tensorflow"
# 导入所需的库
import keras
import keras_nlp
import tensorflow as tf
import tensorflow_datasets as tfds
import sklearn.cluster as cluster
# 设置全局混合精度策略为 mixed_float16,以提高训练速度和减少内存使用
# 注意:这需要在支持混合精度的硬件上运行,并且 TensorFlow 和 Keras 的版本需要支持
keras.mixed_precision.set_global_policy("mixed_float16")
2.2.构建模型理论基础
微调模型采用孪生网络结构
孪生网络是一种神经网络结构,由两个或多个共享权重的子网络构成,用于生成输入特征向量并比较它们的相似度。
在我们的案例中,子网络是以RoBERTa模型为基础,通过添加池化层来生成输入句子的嵌入向量。这些嵌入向量随后将相互比较,以便学习生成具有实际语义的嵌入。
我们采用的池化策略包括平均池化、最大池化和CLS池化,其中平均池化效果最佳,这将在我们的示例中使用。
利用回归目标函数进行微调
在构建回归目标函数的孪生网络时,该网络的任务是预测两个输入句子嵌入的余弦相似度,该相似度反映了嵌入向量之间的角度关系。余弦相似度越高,表示句子在语义上越相似。
2.3.准备数据集
我们将采用STSB数据集对模型进行微调,该数据集包含了一系列被标记的句子对,标记范围从0到5,反映了句子对之间的语义相似度。为了使余弦相似度的输出范围与数据集标签一致,我们将在数据预处理阶段将标签除以2.5再减去1,以匹配余弦相似度的[-1, 1]范围。
TRAIN_BATCH_SIZE = 6
VALIDATION_BATCH_SIZE = 8
TRAIN_NUM_BATCHES = 300
VALIDATION_NUM_BATCHES = 40
AUTOTUNE = tf.data.experimental.AUTOTUNE
def change_range(x):
return (x / 2.5) - 1
def prepare_dataset(dataset, num_batches, batch_size):
dataset = dataset.map(
lambda z: (
[z["sentence1"], z["sentence2"]],
[tf.cast(change_range(z["label"]), tf.float32)],
),
num_parallel_calls=AUTOTUNE,
)
dataset = dataset.batch(batch_size)
dataset = dataset.take(num_batches)
dataset = dataset.prefetch(AUTOTUNE)
return dataset
stsb_ds = tfds.load(
"glue/stsb",
)
stsb_train, stsb_valid = stsb_ds["train"], stsb_ds["validation"]
stsb_train = prepare_dataset(stsb_train, TRAIN_NUM_BATCHES, TRAIN_BATCH_SIZE)
stsb_valid = prepare_dataset(stsb_valid, VALIDATION_NUM_BATCHES, VALIDATION_BATCH_SIZE)
代码定义了用于处理和准备STSB(Semantic Textual Similarity Benchmark)数据集的函数和流程,用于训练和验证模型。
-
定义批量大小:
TRAIN_BATCH_SIZE
和VALIDATION_BATCH_SIZE
分别定义了训练和验证批次的样本数量。
-
定义批次数量:
TRAIN_NUM_BATCHES
和VALIDATION_NUM_BATCHES
分别定义了训练和验证过程中要使用的批次总数。
-
定义自动调整:
AUTOTUNE
用于tf.data.experimental.AUTOTUNE
,它允许TensorFlow自动调整数据加载过程中的资源。
-
定义范围变化函数:
change_range
函数将标签值的范围从[0, 5]转换为[-1, 1],这是通过除以2.5再减1实现的。
-
定义数据集准备函数:
prepare_dataset
函数接收原始数据集、批次数量和批量大小,然后执行以下操作:- 使用
map
函数处理数据集中的每个元素,将句子对和转换后的标签组合在一起。 - 使用
batch
函数将数据分批。 - 使用
take
函数限制每个epoch的批次数量。 - 使用
prefetch
函数提高性能,允许模型在训练时异步预读取数据。
- 使用
-
加载和划分数据集:
- 使用
tfds.load
加载 “glue/stsb” 数据集,它被划分为训练集和验证集。
- 使用
-
准备训练和验证数据集:
- 调用
prepare_dataset
函数分别准备训练集和验证集,传入相应的批次数量和批量大小。
- 调用
代码的最终目标是为模型的训练和验证准备经过预处理和优化的数据流,确保数据以高效和适当的格式提供给模型。通过这种方式,可以提高训练过程的效率,并确保模型能够接收到正确范围的标签值。
让我们来看看数据集中两个句子及其相似性的例子。
# 遍历stsb_train数据集中的一批数据
for x, y in stsb_train.take(1): # 使用.take(1)来仅获取一批数据,因为外部已经有一个break
# 遍历这一批数据中的每一个样本
for i, example in enumerate(x):
# 输出第一个句子
print(f"句子1 : {
example[0].numpy().decode('utf-8')} ") # 假设example[0]是bytes类型,需要解码为字符串
# 输出第二个句子
print(f"句子2 : {
example[1].numpy().decode('utf-8')} ") # 同样假设example[1]是bytes类型
# 输出相似度得分(注意y[i]是一个列表,因为我们之前将y封装在一个列表中)
print(f"相似度 : {
y[0][i].numpy()} \n") # 如果y是一个列表的列表,我们只需要y[0]来获取相似度列表
# 因为我们只想看一个批次的样本,所以在这里添加break
break
```python
句子 1 : b"A young girl is sitting on Santa's lap."
句子 2 : b"A little girl is sitting on Santa's lap"
相似度 : [0.9200001]
2.4.构建编码器模型
我们将创建一个编码器模型,用于生成能够表示句子语义的嵌入向量。该模型包括以下几个主要组件:
- 一个预处理层,负责将句子分词,并为它们生成填充掩码,以处理不同长度的句子。
- 一个骨干网络模型,它负责生成句子中每个分词的上下文感知表示。
- 一个均值池化层,通过
keras.layers.GlobalAveragePooling1D
实现,用于将骨干模型生成的分词表示合并为单一的句子嵌入向量。在计算均值时,该层会考虑填充掩码,以确保填充分词不会影响嵌入结果。 - 一个归一化层,用于将嵌入向量标准化,这对于基于余弦相似度的相似性度量是必要的。
通过这种方式,编码器模型能够将输入的句子转换成固定大小的嵌入向量,这些向量可以用于后续的相似性比较或其他下游任务。
# 加载预定义的 Roberta 预处理器和主干网络
# 从 "roberta_base_en" 预设中加载 Roberta 预处理器
preprocessor = keras_nlp.models.RobertaPreprocessor.from_preset("roberta_base_en")
# 从 "roberta_base_en" 预设中加载 Roberta 主干网络
backbone = keras_nlp.models.RobertaBackbone.from_preset("roberta_base_en")
# 定义一个输入层,用于接收字符串类型的句子,并命名为 "sentence"
inputs = keras.Input(shape=(1,), dtype="string", name="sentence")
# 使用预处理器处理输入句子
x = preprocessor(inputs)
# 使用主干网络处理预处理器输出的张量
h = backbone(x)
# 注意:在 Keras 中,GlobalAveragePooling1D 通常不接受额外的 mask 参数
# 因此在应用 GlobalAveragePooling1D 时,我们不需要 x["padding_mask"]
# 应用全局平均池化层,并命名为 "pooling_layer"
embedding = keras.layers.GlobalAveragePooling1D(name="pooling_layer")(h)
# 应用单位归一化层,对嵌入进行归一化,并沿着轴 1 进行操作
n_embedding = keras.layers.UnitNormalization(axis=1)(embedding)
# 创建模型,输入为句子,输出为归一化后的嵌入
roberta_normal_encoder = keras.Model(inputs=inputs, outputs=n_embedding)
# 显示模型的摘要信息
roberta_normal_encoder.summary()
代码展示了如何构建一个用于生成句子嵌入的编码器模型,具体使用了RoBERTa模型作为骨干网络。
-
加载预处理器和骨干网络:
- 使用
keras_nlp.models.RobertaPreprocessor
和keras_nlp.models.RobertaBackbone
从预设 “roberta_base_en” 加载RoBERTa的预处理器和骨干网络。
- 使用
-
定义输入层:
- 创建一个输入层
inputs
,用于接收形状为(1,)的字符串类型句子,并命名为 “sentence”。
- 创建一个输入层
-
处理输入句子:
- 利用加载的预处理器
preprocessor
对输入层的输出进行处理。
- 利用加载的预处理器
-
生成上下文表示:
- 使用骨干网络
backbone
处理预处理器输出的张量,生成每个分词的上下文表示。
- 使用骨干网络
-
应用全局平均池化:
- 应用
keras.layers.GlobalAveragePooling1D
层进行全局平均池化,生成句子的嵌入向量,并命名为 “pooling_layer”。注意,这里没有使用额外的掩码参数。
- 应用
-
归一化嵌入:
- 使用
keras.layers.UnitNormalization
层对嵌入向量进行单位归一化处理,操作沿轴1进行。
- 使用
-
创建编码器模型:
- 构建一个Keras模型
roberta_normal_encoder
,输入为句子,输出为归一化后的嵌入。
- 构建一个Keras模型
-
显示模型摘要:
- 使用
roberta_normal_encoder.summary()
显示模型的摘要信息,以便于理解模型结构和各层的参数。
- 使用
代码段的目的是构建一个能够将输入句子转换为固定大小的归一化嵌入向量的模型,这些嵌入向量可以用于后续的相似性比较或其他需要句子级特征表示的任务。通过使用预训练的RoBERTa模型,编码器能够捕捉到丰富的语义信息,并有效地将这些信息编码到低维空间中。
Model: "functional_1"
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓
┃ Layer (type) ┃ Output Shape ┃ Param # ┃ Connected to ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩
│ sentence │ (None, 1) │ 0 │ - │
│ (InputLayer) │ │ │ │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ roberta_preprocess… │ [(None, 512), │ 0 │ sentence[0][0] │
│ (RobertaPreprocess… │ (None, 512)] │ │ │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ roberta_backbone │ (None, 512, 768) │ 124,05… │ roberta_preprocesso… │
│ (RobertaBackbone) │ │ │ roberta_preprocesso… │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ pooling_layer │ (None, 768) │ 0 │ roberta_backbone[0]… │
│ (GlobalAveragePool… │ │ │ roberta_preprocesso… │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ unit_normalization │ (None, 768) │ 0 │ pooling_layer[0][0] │
│ (UnitNormalization) │ │ │ │
└─────────────────────┴───────────────────┴─────────┴──────────────────────┘
Total params: 124,052,736 (473.22 MB)
Trainable params: 124,052,736 (473.22 MB)
Non-trainable params: 0 (0.00 B)
2.5.建立基于孪生模型的回归模型
我们将构建一个带有回归目标的孪生网络。孪生网络由两个或多个子网络构成,对于我们的孪生模型,理论上需要两个独立的编码器来处理输入的两个句子。但实际情况下,我们只有一个编码器模型,因此我们将采用让两个句子依次通过同一编码器的方式,以此实现两个句子的嵌入表示,同时保持权重的共享。
一旦模型处理了两个句子并生成了归一化的嵌入向量,我们将通过将这两个嵌入向量进行点积运算来计算它们之间的余弦相似度。这种相似度度量方式反映了句子对在语义上接近程度的量化指标。通过这种方式,孪生网络能够学习如何评估两个输入句子在语义层面的相似性。
import tensorflow as tf
from tensorflow import keras
class RegressionSiamese(keras.Model):
def __init__(self, encoder, **kwargs):
super().__init__(**kwargs) # 先调用父类的初始化方法
# 定义输入层
self.input_layer = keras.Input(shape=(2,), dtype="string", name="sentences")
# 使用 Lambda 层或自定义层来处理字符串,因为标准的 Input 层不直接处理字符串
# 这里我们假设 encoder 已经能够处理字符串输入
# 假设 encoder 返回一个张量,我们可以直接调用它来获取句子的编码
sen1 = keras.layers.Lambda(lambda x: x[:, 0])(self.input_layer)
sen2 = keras.layers.Lambda(lambda x: x[:, 1])(self.input_layer)
# 使用 encoder 对两个句子进行编码
u = encoder(sen1)
v = encoder(sen2)
# 计算余弦相似度
# 注意:matmul 可能会因为维度不匹配而失败,我们需要确保 u 和 v 的形状适合矩阵乘法
# 这里假设 u 和 v 的最后一个维度是它们的嵌入维度
cosine_similarity_scores = keras.layers.Dot(axes=-1