创建自定义模型

句子转换器模型的结构

一个 Sentence Transformer 模型由一系列模块 (文档 ) 组成。最常见的架构是 Transformer 模块、Pooling 模块的组合,以及可选的 Dense 模块和/或 Normalize 模块。

  • Transformer: 该模块负责处理输入文本并生成上下文化的嵌入。

  • Pooling: 该模块通过聚合嵌入来减少Transformer模块输出的维度。常见的池化策略包括均值池化和CLS池化。

  • Dense: 该模块包含一个线性层,用于后处理从池化模块输出的嵌入。

  • Normalize: 该模块对前一层的嵌入进行归一化。

例如,流行的 all-MiniLM-L6-v2 模型也可以通过初始化构成该模型的3个特定模块来加载:

from sentence_transformers import models, SentenceTransformer

transformer = models.Transformer("sentence-transformers/all-MiniLM-L6-v2", max_seq_length=256)
pooling = models.Pooling(transformer.get_word_embedding_dimension(), pooling_mode="mean")
normalize = models.Normalize()

model = SentenceTransformer(modules=[transformer, pooling, normalize])

保存句子转换器模型

每当保存一个 Sentence Transformer 模型时,会生成三种类型的文件:

  • modules.json: 这个文件包含模块名称、路径和类型的列表,用于重建模型。

  • config_sentence_transformers.json: 这个文件包含 Sentence Transformer 模型的一些配置选项,包括保存的提示、模型的相似度函数以及模型作者使用的 Sentence Transformer 包版本。

  • 特定模块文件:每个模块都保存在一个单独的文件夹中,第一个模块保存在根文件夹中,所有后续模块保存在以模块索引和模型名称命名的子文件夹中(例如,1_Pooling2_Normalize)。大多数模块文件夹包含一个 config.json``(或 ``sentence_bert_config.json 用于 Transformer 模块)文件,该文件存储传递给该模块的关键字参数的默认值。因此,一个 sentence_bert_config.json 的内容如下:

    {
      "max_seq_length": 4096,
      "do_lower_case": false
    }
    

    意味着 Transformer 模块将以 max_seq_length=4096do_lower_case=False 进行初始化。

因此,如果我在前面的代码片段中对 model 调用 SentenceTransformer.save_pretrained("local-all-MiniLM-L6-v2") ,将生成以下文件:

local-all-MiniLM-L6-v2/
├── 1_Pooling
│   └── config.json
├── 2_Normalize
├── README.md
├── config.json
├── config_sentence_transformers.json
├── model.safetensors
├── modules.json
├── sentence_bert_config.json
├── special_tokens_map.json
├── tokenizer.json
├── tokenizer_config.json
└── vocab.txt

这包含一个 modules.json ,其内容如下:

[
  {
    "idx": 0,
    "name": "0",
    "path": "",
    "type": "sentence_transformers.models.Transformer"
  },
  {
    "idx": 1,
    "name": "1",
    "path": "1_Pooling",
    "type": "sentence_transformers.models.Pooling"
  },
  {
    "idx": 2,
    "name": "2",
    "path": "2_Normalize",
    "type": "sentence_transformers.models.Normalize"
  }
]

以及一个包含以下内容的 config_sentence_transformers.json

{
  "__version__": {
    "sentence_transformers": "3.0.1",
    "transformers": "4.43.4",
    "pytorch": "2.5.0"
  },
  "prompts": {},
  "default_prompt_name": null,
  "similarity_fn_name": null
}

此外,1_Pooling 目录包含 Pooling 模块的配置文件,而 2_Normalize 目录是空的,因为 Normalize 模块不需要任何配置。sentence_bert_config.json 文件包含 Transformer 模块的配置,该模块还在根目录中保存了许多与分词器和模型本身相关的文件。

加载 Sentence Transformer 模型

要从保存的模型目录加载 Sentence Transformer 模型,会读取 modules.json 以确定构成模型的模块。每个模块都使用相应模块目录中存储的配置进行初始化,之后使用加载的模块实例化 SentenceTransformer 类。

来自 Transformers 模型的句子转换器模型

当你使用纯 Transformers 模型(例如 BERT、RoBERTa、DistilBERT、T5)初始化一个 Sentence Transformer 模型时,Sentence Transformers 默认会创建一个 Transformer 模块和一个均值池化模块。这提供了一种简单的方式来利用预训练语言模型进行句子嵌入。

具体来说,这两个片段是相同的:

from sentence_transformers import SentenceTransformer

model = SentenceTransformer("bert-base-uncased")
from sentence_transformers import models, SentenceTransformer

transformer = models.Transformer("bert-base-uncased")
pooling = models.Pooling(transformer.get_word_embedding_dimension(), pooling_mode="mean")
model = SentenceTransformer(modules=[transformer, pooling])

高级:自定义模块

要创建自定义的 Sentence Transformer 模型,您可以通过子类化 PyTorch 的 torch.nn.Module 类并实现以下方法来实现自己的模块:

  • 一个接受 features 字典的 torch.nn.Module.forward() 方法,字典的键可能包括 input_idsattention_masktoken_type_idstoken_embeddingssentence_embedding,具体取决于模块在模型流水线中的位置。

  • 一个 save 方法,接受一个 save_dir 参数,并将模块的配置保存到该目录中。

  • 一个 load 静态方法,它接受一个 load_dir 参数,并根据该目录中的模块配置初始化模块。

  • (如果第一个模块) 一个 get_max_seq_length 方法,返回模块可以处理的最大序列长度。仅在模块处理输入文本时需要。

  • (如果第1个模块) 一个 tokenize 方法,接受输入列表并返回一个字典,字典的键如 input_idsattention_masktoken_type_idspixel_values 等。这个字典将被传递给模块的 forward 方法。

  • (可选) 一个 get_sentence_embedding_dimension 方法,返回该模块生成的句子嵌入的维度。仅在模块生成嵌入或更新嵌入维度时需要。

  • (可选) 一个 get_config_dict 方法,返回一个包含模块配置的字典。此方法可用于将模块配置保存到磁盘,并将模块配置保存到模型卡中。

例如,我们可以通过实现一个自定义模块来创建一个自定义池化方法。

# decay_pooling.py

import json
import os
import torch
import torch.nn as nn

class DecayMeanPooling(nn.Module):
    def __init__(self, dimension: int, decay: float = 0.95) -> None:
        super(DecayMeanPooling, self).__init__()
        self.dimension = dimension
        self.decay = decay

    def forward(self, features: dict[str, torch.Tensor], **kwargs) -> dict   [str, torch.Tensor]:
        token_embeddings = features["token_embeddings"]
        attention_mask = features["attention_mask"].unsqueeze(-1)

        # Apply the attention mask to filter away padding tokens
        token_embeddings = token_embeddings * attention_mask
        # Calculate mean of token embeddings
        sentence_embeddings = token_embeddings.sum(1) / attention_mask.sum(1)
        # Apply exponential decay
        importance_per_dim = self.decay ** torch.arange(sentence_embeddings.   size(1), device=sentence_embeddings.device)
        features["sentence_embedding"] = sentence_embeddings *    importance_per_dim
        return features

    def get_config_dict(self) -> dict[str, float]:
        return {"dimension": self.dimension, "decay": self.decay}

    def get_sentence_embedding_dimension(self) -> int:
        return self.dimension

    def save(self, save_dir: str, **kwargs) -> None:
        with open(os.path.join(save_dir, "config.json"), "w") as fOut:
            json.dump(self.get_config_dict(), fOut, indent=4)

    def load(load_dir: str, **kwargs) -> "DecayMeanPooling":
        with open(os.path.join(load_dir, "config.json")) as fIn:
            config = json.load(fIn)

        return DecayMeanPooling(**config)

备注

建议在 __init__forwardsaveloadtokenize 方法中添加 **kwargs,以确保这些方法与 Sentence Transformers 库的未来更新兼容。

这现在可以作为一个模块在 Sentence Transformer 模型中使用:

from sentence_transformers import models, SentenceTransformer
from decay_pooling import DecayMeanPooling

transformer = models.Transformer("bert-base-uncased", max_seq_length=256)
decay_mean_pooling = DecayMeanPooling(transformer.get_word_embedding_dimension(), decay=0.99)
normalize = models.Normalize()

model = SentenceTransformer(modules=[transformer, decay_mean_pooling, normalize])
print(model)
"""
SentenceTransformer(
    (0): Transformer({'max_seq_length': 256, 'do_lower_case': False}) with Transformer model: BertModel
    (1): DecayMeanPooling()
    (2): Normalize()
)
"""

texts = [
    "Hello, World!",
    "The quick brown fox jumps over the lazy dog.",
    "I am a sentence that is used for testing purposes.",
    "This is a test sentence.",
    "This is another test sentence.",
]
embeddings = model.encode(texts)
print(embeddings.shape)
# [5, 384]

您可以使用 SentenceTransformer.save_pretrained 保存此模型,结果将生成一个 modules.json 文件,内容如下:

[
  {
    "idx": 0,
    "name": "0",
    "path": "",
    "type": "sentence_transformers.models.Transformer"
  },
  {
    "idx": 1,
    "name": "1",
    "path": "1_DecayMeanPooling",
    "type": "decay_pooling.DecayMeanPooling"
  },
  {
    "idx": 2,
    "name": "2",
    "path": "2_Normalize",
    "type": "sentence_transformers.models.Normalize"
  }
]

为了确保可以导入 decay_pooling.DecayMeanPooling,你应该将 decay_pooling.py 文件复制到你保存模型的目录中。如果你将模型推送到 Hugging Face Hub,那么你也应该将 decay_pooling.py 文件上传到模型的仓库中。这样,每个人都可以通过调用 SentenceTransformer("your-username/your-model-id", trust_remote_code=True) 来使用你的自定义模块。

备注

使用存储在 Hugging Face Hub 上的远程代码的自定义模块时,要求您的用户在加载模型时将 trust_remote_code 指定为 True。这是一项安全措施,以防止远程代码执行攻击。

如果你在 Hugging Face Hub 上有你的模型和自定义建模代码,那么将你的自定义模块分离到一个单独的仓库中可能是有意义的。这样,你只需要维护一个自定义模块的实现,并且可以在多个模型中重用它。你可以通过更新 modules.json 文件中的 type 来实现这一点,将其路径包含到存储自定义模块的仓库中,例如 {repository_id}--{dot_path_to_module}。例如,如果 decay_pooling.py 文件存储在一个名为 my-user/my-model-implementation 的仓库中,并且模块名为 DecayMeanPooling,那么 modules.json 文件可能看起来像这样:

[
  {
    "idx": 0,
    "name": "0",
    "path": "",
    "type": "sentence_transformers.models.Transformer"
  },
  {
    "idx": 1,
    "name": "1",
    "path": "1_DecayMeanPooling",
    "type": "my-user/my-model-implementation--decay_pooling.DecayMeanPooling"
  },
  {
    "idx": 2,
    "name": "2",
    "path": "2_Normalize",
    "type": "sentence_transformers.models.Normalize"
  }
]

高级:自定义模块中的关键字参数传递

如果你想让你的用户能够通过 SentenceTransformer.encode 方法指定自定义的关键字参数,那么你可以将它们的名称添加到 modules.json 文件中。例如,如果我的模块在用户指定 task_type 关键字参数时应表现不同,那么你的 modules.json 可能看起来像:

[
  {
    "idx": 0,
    "name": "0",
    "path": "",
    "type": "custom_transformer.CustomTransformer",
    "kwargs": ["task_type"]
  },
  {
    "idx": 1,
    "name": "1",
    "path": "1_Pooling",
    "type": "sentence_transformers.models.Pooling"
  },
  {
    "idx": 2,
    "name": "2",
    "path": "2_Normalize",
    "type": "sentence_transformers.models.Normalize"
  }
]

然后,你可以在自定义模块的 forward 方法中访问 task_type 关键字参数:

from sentence_transformers.models import Transformer

class CustomTransformer(Transformer):
    def forward(self, features: dict[str, torch.Tensor], task_type: Optional[str] = None) -> dict[str, torch.Tensor]:
        if task_type == "default":
            # Do something
        else:
            # Do something else
        return features

这样,用户在调用 SentenceTransformer.encode 时可以指定 task_type 关键字参数:

from sentence_transformers import SentenceTransformer

model = SentenceTransformer("your-username/your-model-id", trust_remote_code=True)
texts = [...]
model.encode(texts, task_type="default")