小白学大模型:训练400倍加速的嵌入模型

在大模型开发中,嵌入模型一直是研究和应用的核心。然而,随着模型规模的不断扩大,其对计算资源的需求也日益增加,尤其是在 CPU 上的运行效率往往成为瓶颈。

▎https://huggingface.co/blog/static-embeddings

本文介绍一种全新的静态嵌入模型训练方法,能够在 CPU 上实现比当前最先进的嵌入模型快 100 到 400 倍的速度提升,同时保留了大部分的性能表现。

小白学大模型:训练400倍加速的嵌入模型


新发布的两大高效模型

基于这一创新方法,我们训练了两个极其高效的嵌入模型:

1. sentence-transformers/static-retrieval-mrl-en-v1:专为英语检索任务设计,性能卓越。

2. sentence-transformers/static-similarity-mrl-multilingual-v1:适用于多语言相似性任务,覆盖广泛语言。

与常见的同类模型(如 all-mpnet-base-v2 和 multilingual-e5-small)相比,这两个模型在 CPU 上的运行速度提升了 100 到 400 倍,同时在各种基准测试中至少达到了它们 85% 的性能表现。


什么是嵌入(Embeddings)?

在自然语言处理(NLP)的世界里,嵌入(Embeddings)是最重要的工具之一。它几乎可以解决所有类型的自然语言任务,是连接人类语言和机器理解的桥梁。

小白学大模型:训练400倍加速的嵌入模型

简单来说,嵌入是一种将复杂对象(如文本、图像、音频等)转换为数值表示的方法。这些数值表示可以被计算机轻松处理和分析,从而帮助我们解决各种问题。

小白学大模型:训练400倍加速的嵌入模型

现代的嵌入模型通常包含几个关键步骤,这些步骤被称为“推理”(Inference)。

  • 分词器(Tokenizer):将文本切分成一个个“词”或“子词”,这些“词”是模型能够理解的基本单位。
  • 编码器(Encoder):这是模型的核心部分,通常是一个带有注意力机制的语言模型。它会根据上下文计算每个“词”的嵌入向量。
  • 池化器(Pooler):将所有“词”的嵌入向量合并成一个整体的文本嵌入向量。

例如,单词“bank”在不同的上下文中(如“河岸”或“银行”)会有不同的嵌入向量,因为编码器会根据上下文来调整它的含义。

虽然带有大量注意力层的大模型能够生成高质量的嵌入向量,但它们的计算成本非常高。在推理过程中,编码器几乎占据了所有计算时间,导致速度非常慢。

这就引出了一个问题:有没有一种方法可以在不牺牲太多质量的情况下,大幅提升嵌入模型的速度呢?

静态嵌入是一种不依赖于复杂注意力模型的嵌入方法。它通过预先计算好的“词”嵌入向量来实现快速推理。

小白学大模型:训练400倍加速的嵌入模型

早在 Transformer 架构出现之前,静态嵌入就已经被广泛使用了。常见的静态嵌入模型包括 GLoVe 和 word2vec。最近,Model2Vec 这种技术可以将预训练的嵌入模型转换为静态嵌入模型,进一步提升了效率。

在静态嵌入模型中,编码器的步骤非常简单:只需要根据输入的“词”查找预先计算好的嵌入向量即可。这样一来,推理速度可以提升几个数量级,而质量损失却非常小。


重新定义静态嵌入模型

通过引入对比学习损失函数(Contrastive Learning Loss Function)和嵌套表示学习(Matryoshka Representation Learning),成功地让静态嵌入模型在速度和性能上实现了双重突破。

对比学习是一种强大的训练技术,它通过将相似的样本拉近、不相似的样本推远,来优化嵌入向量的表示。在我们的方法中,对比学习损失函数是提升静态嵌入模型性能的核心。

小白学大模型:训练400倍加速的嵌入模型

除了对比学习,我们还引入了嵌套表示学习(Matryoshka Representation Learning)。这是一种非常巧妙的技术,它允许我们使用嵌入向量的截断版本,从而进一步提升模型的推理速度。

小白学大模型:训练400倍加速的嵌入模型

想象一下,一个嵌入向量就像一个俄罗斯套娃(Matryoshka),我们可以根据需要选择使用完整的向量,或者只使用其中的一部分。通过这种方式,我们可以在不牺牲太多性能的情况下,显著减少计算量,让模型运行得更快。

训练细节

借助 Sentence Transformers 这一强大的工具,我们可以将训练过程拆解为几个关键组件。
① 数据集(Dataset)
② 损失函数(Loss Function)
③ 训练参数(Training Arguments,可选)
④ 评估器(Evaluator,可选)
⑤ 训练器(Trainer)

模型选择

我们选择了 StaticEmbedding 模块。这个模块的核心优势在于它的高效性,它通过避免填充(padding)和高效的前向传播方法来计算和池化嵌入向量。简单来说,它就像一个经过优化的 PyTorch EmbeddingBag,本质上是一个高效的嵌入表(即嵌入向量的查找表),并结合了平均池化操作。

SentenceTransformer(
  (0): StaticEmbedding(
    (embedding): EmbeddingBag(30522, 1024, mode='mean')
  )
)

嵌入模型的应用场景大致可以分为两类:检索任务和通用任务(如分类、聚类、语义相似性等)。为了满足这些不同的需求,我们决定分别训练两个模型:一个专注于检索的单语模型,以及一个面向通用相似性任务的多语种模型。

  • English Retrieval

在检索任务中,模型需要快速准确地从大量文本中找到与查询最相关的文档。然而,多语言检索的训练数据相对有限,因此我们决定专注于英语检索模型。

from sentence_transformers import SentenceTransformer
from sentence_transformers.models import StaticEmbedding
from tokenizers import Tokenizer

tokenizer = Tokenizer.from_pretrained("google-bert/bert-base-uncased")
static_embedding = StaticEmbedding(tokenizer, embedding_dim=1024)

model = SentenceTransformer(modules=[static_embedding])
  • Multilingual Similarity

与检索模型不同,通用相似性模型需要处理多种语言,并且要适应各种任务(如分类、聚类、语义相似性等)。

from sentence_transformers import SentenceTransformer
from sentence_transformers.models import StaticEmbedding
from tokenizers import Tokenizer

tokenizer = Tokenizer.from_pretrained("google-bert/bert-base-multilingual-uncased")
static_embedding = StaticEmbedding(tokenizer, embedding_dim=1024)

model = SentenceTransformer(modules=[static_embedding])

损失函数的选择

在 Sentence Transformers 中,损失函数必须与训练数据的格式相匹配。根据数据的不同格式,我们可以选择不同的损失函数。我们的数据主要有以下几种格式:

  • (anchor, positive) 对:没有标签
  • (anchor, positive, negative) 三元组:没有标签
  • (anchor, positive, negative_1, ..., negative_n) 元组:没有标签

此外还有三种优秀的损失函数:

  • MultipleNegativesRankingLoss (MNRL)

MNRL 是一种广泛使用的损失函数,也被称为“批内负样本损失”或 InfoNCE 损失。它的核心思想是:给定一个锚点(例如一个问题),模型需要从所有正样本和负样本(例如所有答案)中,为锚点分配最高的相似性分数给对应的正样本。

如果提供额外的负样本,它们将作为“批内负样本”,帮助模型更好地“挑选”正确的正样本。负样本越难区分,模型的性能就会越强。因此,较大的批量大小会增加批内负样本的数量,从而提升模型性能(在一定范围内)。

  • CachedMultipleNegativesRankingLoss (CMNRL)

CMNRL 是 MNRL 的扩展版本,它通过 GradCache 技术实现了在不增加内存消耗的情况下,任意增加批量大小。这种方法特别适合内存有限的场景。

如果你的硬件已经能够支持足够大的批量大小(例如使用 MNRL),那么可以直接使用 MNRL,以节省 CMNRL 带来的额外 20% 训练时间成本。

  • GISTEmbedLoss (GIST)

GIST 是 MNRL 的另一种扩展版本,它通过一个引导的 Sentence Transformer 模型来过滤潜在的假负样本。假负样本可能会对模型性能产生负面影响,而真正的难负样本(与正样本非常接近但不完全相同)则有助于提升性能。因此,这种过滤需要非常谨慎地处理。

在我们的项目中,我们选择了 MultipleNegativesRankingLoss (MNRL) ,原因如下:

1. 硬件限制:我们的静态嵌入模型非常轻量级,可以在单个 RTX 3090(24GB 内存)上轻松处理 2048 个样本的批量大小。因此,我们不需要使用 CMNRL 来扩展批量大小。

2. 训练效率:由于我们的模型训练速度非常快,使用 GIST 的引导机制会显著降低训练速度。因此,我们选择了简单高效的 MNRL。

from sentence_transformers import SentenceTransformer
from sentence_transformers.losses import MultipleNegativesRankingLoss

# Prepare a model to train
tokenizer = Tokenizer.from_pretrained("google-bert/bert-base-uncased")
static_embedding = StaticEmbedding(tokenizer, embedding_dim=1024)
model = SentenceTransformer(modules=[static_embedding])

# Initialize the MNRL loss given the model
loss = MultipleNegativesRankingLoss(model)

MatryoshkaLoss 是 Sentence Transformers 提供的一种损失修饰器,它可以在标准损失函数的基础上,让模型学习如何在嵌入向量的不同维度上存储信息。具体来说,MatryoshkaLoss 会要求模型在以下几种维度上同时优化:

  • 完整的嵌入维度:模型需要在标准的嵌入维度上表现良好。
  • 截断的嵌入维度:模型还需要在截断后的维度上保持性能。
from sentence_transformers import SentenceTransformer
from sentence_transformers.losses import MultipleNegativesRankingLoss, MatryoshkaLoss

# Prepare a model to train
tokenizer = Tokenizer.from_pretrained("google-bert/bert-base-uncased")
static_embedding = StaticEmbedding(tokenizer, embedding_dim=1024)
model = SentenceTransformer(modules=[static_embedding])

# Initialize the MNRL loss given the model
base_loss = MultipleNegativesRankingLoss(model)
loss = MatryoshkaLoss(model, base_loss, matryoshka_dims=[1024, 768, 512, 256, 128, 64, 32])

训练参数选择

训练轮数:num_train_epochs = 1:我们选择了仅训练 1 个轮次。这听起来可能有些意外,但考虑到我们有足够的数据,重复训练相同的样本并不会带来太多额外的收益。

批量大小:per_device_train_batch_size/per_device_eval_batch_size = 2048:我们选择了 2048 作为训练和评估的批量大小。这个大小能够在我们的硬件(RTX 3090)上轻松运行,同时也能充分利用 GPU 的计算能力。

学习率:learning_rate = 2e-1:我们选择了一个相对较高的学习率 2e-1。这比传统的嵌入模型训练(通常使用 2e-5)要大得多。

预热比例:warmup_ratio = 0.1:我们选择了 10% 的预热比例。预热策略可以帮助模型在训练初期逐渐适应高学习率,从而避免过早收敛或震荡。

bf16 = True:如果硬件支持,我们推荐使用 bf16(混合精度训练)。bf16 不仅可以加速训练,还能减少内存占用。

batch_sampler = BatchSamplers.NO_DUPLICATES:对于使用批内负样本的损失函数(如 MNRL),避免批次内的重复样本是非常重要的。重复样本可能会导致假负样本的出现,从而削弱模型的性能。

多数据集采样器:multi_dataset_batch_sampler = MultiDatasetBatchSamplers.PROPORTIONAL:在多数据集训练时,不同数据集的大小可能不同。我们选择了 按比例采样 的策略,确保每个数据集的样本都能被充分利用,即使这会导致数据分布不完全均匀。

run_name = "static-retrieval-mrl-en-v1"
# or 
# run_name = "static-similarity-mrl-multilingual-v1"

args = SentenceTransformerTrainingArguments(
    # Required parameter:
    output_dir=f"models/{run_name}",
    # Optional training parameters:
    num_train_epochs=1,
    per_device_train_batch_size=2048,
    per_device_eval_batch_size=2048,
    learning_rate=2e-1,
    warmup_ratio=0.1,
    fp16=False,  # Set to False if you get an error that your GPU can't run on FP16
    bf16=True,  # Set to True if you have a GPU that supports BF16
    batch_sampler=BatchSamplers.NO_DUPLICATES,  # MultipleNegativesRankingLoss benefits from no duplicate samples in a batch
    multi_dataset_batch_sampler=MultiDatasetBatchSamplers.PROPORTIONAL,
    # Optional tracking/debugging parameters:
    eval_strategy="steps",
    eval_steps=1000,
    save_strategy="steps",
    save_steps=1000,
    save_total_limit=2,
    logging_steps=1000,
    logging_first_step=True,
    run_name=run_name,  # Used if `wandb`, `tensorboard`, or `neptune`, etc. is installed
)

选择评估器

训练损失虽然重要,但它更多地反映了模型在训练数据上的拟合程度。然而,我们真正关心的是模型在实际任务中的表现,比如检索任务中的 NDCG(归一化折损累计增益)、MAP(平均精度均值)、MRR(平均倒数排名),或者语义相似性任务中的 Spearman 相关性,以及三元组任务中的 Triplet 准确率。

在选择评估器时,我们特别关注了 NanoBEIREvaluator。这是一个专门为检索任务设计的评估器,能够快速运行在 NanoBEIR 数据集 上的信息检索基准测试。

简单高效:NanoBEIREvaluator 能够快速运行,适合在训练过程中频繁评估模型性能。

指标丰富:它提供了多种信息检索指标,如 NDCG、MAP 和 MRR,能够全面反映模型在检索任务中的表现。

与 MTEB Leaderboard 对齐:NanoBEIR 数据集是 MTEB Leaderboard 中检索任务的简化版本,使用 NanoBEIREvaluator 可以让我们更好地与社区中的其他模型进行对比。

from sentence_transformers import SentenceTransformer
from sentence_transformers.evaluation import NanoBEIREvaluator

# Load an example pre-trained model to finetune further
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")

# Initialize the NanoBEIR Evaluator
evaluator = NanoBEIREvaluator()

# Run it on any Sentence Transformer model
evaluator(model)

模型表现

GPU速度

小白学大模型:训练400倍加速的嵌入模型

CPU速度

小白学大模型:训练400倍加速的嵌入模型

Matryoshka 维度

小白学大模型:训练400倍加速的嵌入模型


模型总结

在过去的探索中,我们从创意构思到模型完成,经历了多个步骤,并详细记录了两个最终模型——static-retrieval-mrl-en-v1(英语检索模型)和 static-similarity-mrl-multilingual-v1(多语言相似性模型)的训练和评估过程。这些模型不仅在性能上表现出色,还在推理速度上展现了巨大的优势。

在推理速度方面,静态嵌入模型的表现更是令人惊叹:

  • 在 GPU 上,静态嵌入模型比传统模型快 10-25倍。
  • 在 CPU 上,静态嵌入模型比传统模型快 100-400倍。

通过使用 Matryoshka 损失函数,我们能够在不损失太多性能的情况下,显著减小嵌入向量的维度。这不仅进一步提升了模型的推理速度,还为下游任务提供了更高的灵活性。

  • static-similarity-mrl-multilingual-v1 在英语语义相似性任务(STS)中,将嵌入向量缩小 4倍,仅导致 0.56% 的性能下降。
  • static-retrieval-mrl-en-v1 在英语检索任务中,将嵌入向量缩小 2倍,仅导致 1.47% 的性能下降。

本文转自:Coggle数据科学,转载此文目的在于传递更多信息,版权归原作者所有。如不支持转载,请联系小编demi@eetrend.com删除。

最新文章