1. 从报错到解决:一个真实的故事

“No sentence-transformers model found with name ../sbert-base-chinese-nli. Creating a new one with me.”

如果你在本地跑代码时,屏幕上突然跳出这行红字,心里是不是咯噔一下?我太懂这种感觉了。当时我正在做一个中文文本相似度匹配的项目,兴致勃勃地从 Hugging Face 上把 sbert-base-chinese-nli 模型整个文件夹拖了下来,心想这下本地运行总该没问题了吧。结果,一运行 SentenceTransformer 的加载代码,这个错误就毫不客气地弹了出来。它说找不到模型,还说要“为我创建一个新的”。这听起来很贴心,但实际上意味着它没认出来我辛辛苦苦下载的这一堆文件,准备用默认配置重新初始化一个,这完全不是我想要的那个预训练好的中文模型。

这个错误的核心,其实在于 sentence-transformers 库加载模型的“规矩”。这个库并不是简单地把 Hugging Face 模型仓库里的所有文件一股脑儿读进去就完事了。它有一套自己的模型组织结构预期。当你从 Hugging Face 下载一个标准的 Transformers 模型(比如 BERT)时,你得到的是模型的核心权重(pytorch_model.bin 或 model.safetensors)和对应的 Transformers 配置文件(config.json)。但是,一个 sentence-transformers 模型 是一个“组合模型”,它通常由多个模块(modules)串联而成,比如一个 Transformer 编码器(负责把文本变成向量),后面跟着一个池化层(Pooling,负责把一串向量聚合成一个句子向量),有时还会有归一化层(Normalize)。

因此,sentence-transformers 库在加载一个本地模型文件夹时,会寻找一个名为 modules.json 的“地图”文件。这个文件告诉库:“我这个模型由哪几部分组成,它们分别是什么类型,文件在哪里。” 如果没有这个 modules.json,库就懵了,它不知道该如何组装这些文件,于是就会抛出那个“找不到模型”的错误,并试图用默认方式重建。我们接下来要做的所有事情,本质上就是为这个下载好的模型文件夹,补上这张关键的“组装说明书”和相关的配置,让它能被正确识别和加载。

2. 错误根源深度剖析:库的预期 vs. 你的文件

为什么直接从 Hugging Face 下载的模型会不被识别?我们来把问题掰开揉碎了看。当你访问 uer/sbert-base-chinese-nli 这个仓库时,你会发现它本质上是一个标准的 BERT 模型,经过了在自然语言推理(NLI)任务上的微调,使其能够生成高质量的句子向量。Hugging Face 的 transformers 库可以直接加载它,因为它有标准的 config.json 和模型权重文件。

然而,sentence-transformers 库是在 transformers 之上的一层封装,专门为生成句子嵌入(Sentence Embeddings)优化。它定义了自己的模型保存格式。一个完整的 sentence-transformers 模型包应该包含:

  1. 模块定义文件 (modules.json): 这是最重要的文件,定义了模型的流水线结构。
  2. 每个模块的配置: 比如 Transformer 模块的 config.json,Pooling 模块的 config.json。
  3. 模型权重文件: 如 pytorch_model.bin。
  4. 分词器相关文件: 如 vocab.txt, tokenizer_config.json 等。
  5. 可选的 sentence-transformers 专属配置: 如 sentence_bert_config.json。

你从 Hugging Face 下载的原始包,通常只包含第3、4项以及一个面向 transformers 库的 config.json。它缺少了 sentence-transformers 库所依赖的第1项和第2项(针对池化等模块的配置)。因此,当你用 SentenceTransformer(‘../sbert-base-chinese-nli’) 去加载时,库的加载逻辑大致是这样的:

def _load_model(self, model_path):
    # 1. 检查是否存在 modules.json
    if os.path.exists(os.path.join(model_path, ‘modules.json’)):
        # 按照 sentence-transformers 格式加载
        return self._load_sbert_model(model_path)
    else:
        # 2. 尝试作为 transformers 模型加载并自动添加默认池化层
        try:
            # 但这里可能因为其他配置缺失而失败,或产生非预期行为
            return self._load_auto_model(model_path)
        except Exception as e:
            # 3. 如果都失败了,就报出我们看到的那个错误
            logger.warning(f“No sentence-transformers model found with name {model_path}. Creating a new one with me”)
            # 然后尝试用默认配置创建一个“新”模型,这通常不是我们想要的。

所以,我们的目标非常明确:在已有的模型文件基础上,补全 sentence-transformers 库所期望的配置文件,特别是 modules.json,让加载流程能顺利走到第一步。

3. 手把手修复:构建完整的模型文件夹结构

好了,理论讲完,我们开始动手。假设你的模型文件夹路径是 ./models/sbert-base-chinese-nli。打开这个文件夹,你应该能看到类似以下这些从 Hugging Face 下载的文件:

sbert-base-chinese-nli/
├── config.json               (原始的BERT配置文件)
├── pytorch_model.bin         (或 model.safetensors)
├── vocab.txt
├── tokenizer_config.json
└── (可能还有其他文件,如 special_tokens_map.json)

我们的任务就是为这个文件夹添加缺失的部分。请严格按照以下步骤和内容操作,我踩过的坑你就别踩了。

3.1 第一步:创建池化层(Pooling)配置

sentence-transformers 默认在 Transformer 编码器后添加一个池化层,将序列中所有词的向量聚合成一个句子向量。我们需要为这个池化层创建一个配置。

  1. 在 sbert-base-chinese-nli 文件夹内,新建一个子文件夹,命名为 1_Pooling。这个命名是约定俗成的,1 表示它在模块流水线中的顺序。
  2. 在 1_Pooling 文件夹内,新建一个 config.json 文件。
  3. 将以下内容复制到这个 config.json 中:
{
  “word_embedding_dimension”: 768,
  “pooling_mode_cls_token”: false,
  “pooling_mode_mean_tokens”: true,
  “pooling_mode_max_tokens”: false,
  “pooling_mode_mean_sqrt_len_tokens”: false
}

参数解释:

  • word_embedding_dimension: 768: 这与原始 config.json 里的 hidden_size 一致,表示BERT模型输出的每个词向量的维度是768。
  • pooling_mode_mean_tokens: true: 这是最常用、效果也通常很好的池化策略。它取所有输出词向量的平均值作为句子向量。我们这里就启用它。
  • 其他 pooling_mode_* 选项设为 false,表示我们不使用CLS标签、最大值或按长度平方根归一化的均值等池化方式。

3.2 第二步:修正外部的BERT配置文件

现在,回到 sbert-base-chinese-nli 主文件夹。你会发现这里已经有一个从 Hugging Face 下载的 config.json。我们需要确保这个文件的内容是完整且正确的。有时下载的配置可能缺少某些字段,导致加载时出警告。最稳妥的方法是将其替换或修改为以下内容:

{
  “architectures”: [“BertModel”],
  “attention_probs_dropout_prob”: 0.1,
  “gradient_checkpointing”: false,
  “hidden_act”: “gelu”,
  “hidden_dropout_prob”: 0.1,
  “hidden_size”: 768,
  “initializer_range”: 0.02,
  “intermediate_size”: 3072,
  “layer_norm_eps”: 1e-12,
  “max_position_embeddings”: 512,
  “model_type”: “bert”,
  “num_attention_heads”: 12,
  “num_hidden_layers”: 12,
  “pad_token_id”: 0,
  “position_embedding_type”: “absolute”,
  “transformers_version”: “4.6.0”,
  “type_vocab_size”: 2,
  “use_cache”: true,
  “vocab_size”: 21128
}

关键点检查:请确认 hidden_size 是 768,vocab_size 是 21128(这是中文BERT的标准词表大小)。这个配置文件定义了BERT模型本身的结构。

3.3 第三步:创建核心的模块映射文件 (modules.json)

这是让 sentence-transformers 库“开窍”的关键一步。在 sbert-base-chinese-nli 主文件夹内,新建一个文件,命名为 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”
  }
]

这个文件就像一份组装说明书:

  • 第0号模块 (idx: 0): 类型是 Transformer,path 为空字符串 (“”),这意味着它使用主目录下的配置文件 (config.json) 和模型权重。这就是我们的 BERT 编码器本体。
  • 第1号模块 (idx: 1): 类型是 Pooling,path 指向我们刚刚创建的 1_Pooling 文件夹。库会去那个文件夹里找 config.json 来配置池化层。
  • 第2号模块 (idx: 2): 类型是 Normalize,path 指向 2_Normalize。这是一个可选但强烈推荐的步骤。它对句子向量进行L2归一化(即让向量长度变为1),这在计算余弦相似度时非常重要,能直接提升相似度计算的效果和稳定性。我们接下来就创建它。

3.4 第四步:(可选但推荐)添加归一化层

为了完整性,我们把 2_Normalize 也加上。在 sbert-base-chinese-nli 主文件夹内,新建一个子文件夹,命名为 2_Normalize。这个文件夹里通常不需要单独的 config.json,因为归一化操作没有需要配置的参数。但为了保持结构清晰,你可以放一个空的 config.json 文件,或者不创建任何文件。sentence-transformers 库识别到这个模块类型后会自动应用归一化。

3.5 第五步:添加Sentence-BERT专用配置

最后,我们还可以添加一个可选的全局配置文件。在 sbert-base-chinese-nli 主文件夹内,新建一个文件,命名为 sentence_bert_config.json。

内容如下:

{
  “max_seq_length”: 512,
  “do_lower_case”: false
}
  • max_seq_length: 512: 指定模型处理文本的最大长度,这与BERT的 max_position_embeddings 对应。
  • do_lower_case: false: 对于中文模型,这个通常是 false,因为中文分词不涉及大小写转换。

4. 最终文件结构检查与验证

完成以上所有步骤后,你的 sbert-base-chinese-nli 文件夹应该看起来像这样:

sbert-base-chinese-nli/
├── 1_Pooling/                 # 【新增】池化层配置文件夹
│   └── config.json            # 【新增】池化层配置
├── 2_Normalize/               # 【新增】归一化层文件夹(可为空)
├── config.json                # 【修正】原始的BERT模型结构配置
├── modules.json               # 【新增】核心模块定义文件
├── sentence_bert_config.json  # 【新增】Sentence-BERT全局配置
├── pytorch_model.bin          # (原有)模型权重
├── vocab.txt                  # (原有)词表
├── tokenizer_config.json      # (原有)分词器配置
└── (其他原有文件,如 special_tokens_map.json)

现在,是见证奇迹的时刻。打开你的Python解释器或Jupyter Notebook,运行以下测试代码:

from sentence_transformers import SentenceTransformer

# 指定你修改后的模型路径
model_path = ‘./models/sbert-base-chinese-nli’

# 加载模型
model = SentenceTransformer(model_path)

# 尝试编码一些句子
sentences = [‘今天天气真好’, ‘这是一个测试句子’]
embeddings = model.encode(sentences)

print(“模型加载成功!”)
print(f“句子向量维度:{embeddings.shape}”) # 应该是 (2, 768)

如果一切顺利,你将不会再看到那个恼人的警告,而是成功加载模型并输出句子向量。恭喜你,问题解决了!

5. 进阶讨论与避坑指南

在实际操作中,你可能还会遇到一些变体或问题,这里我分享几点经验:

关于 config_sentence_transformers.json 文件: 原始文章提到了这个文件。它通常包含更详细的sentence-transformers训练元数据,比如使用的损失函数、训练批次大小等。对于仅仅加载和使用预训练模型来说,这个文件不是必需的。我们的 modules.json 和 sentence_bert_config.json 已经提供了足够的信息。如果你是从别人那里打包好的完整 sentence-transformers 模型,可能会有这个文件,但自己从 Hugging Face 转换时,可以忽略它。

模型权重格式问题: 新版本的 Hugging Face 模型可能使用 model.safetensors 格式而不是 pytorch_model.bin。这完全没问题,sentence-transformers 和 transformers 库都能自动识别。如果你的文件夹里是 .safetensors 文件,什么都不用改。

使用 sentence-transformers 官方工具进行转换: 其实,sentence-transformers 库提供了一个更官方的命令行工具来做这件事。如果你不想手动创建这些文件,可以尝试以下方法(确保已安装 sentence-transformers):

# 首先,使用 transformers 库下载模型到本地缓存
from transformers import AutoModel, AutoTokenizer
model = AutoModel.from_pretrained(‘uer/sbert-base-chinese-nli’)
tokenizer = AutoTokenizer.from_pretrained(‘uer/sbert-base-chinese-nli’)

# 然后,使用 sentence-transformers 的转换函数(在代码中)
from sentence_transformers import SentenceTransformer
# 这种方法有时能自动处理格式,但不如手动配置可靠

不过,根据我和很多社区开发者的经验,手动配置(即本文的方法)是最可靠、最让你心里有底的方式,尤其当你需要定制化某些模块时。

错误排查清单:

  1. 检查路径: 确保 SentenceTransformer 加载的路径是文件夹的路径,而不是某个具体文件的路径。
  2. 检查JSON格式: 确保你添加的所有 .json 文件都是有效的JSON格式。一个多余的逗号或缺少引号都会导致解析失败。可以用在线JSON校验工具检查。
  3. 检查模块顺序: modules.json 里的 idx 顺序很重要,必须是 0, 1, 2… 依次递增,并且类型要匹配。
  4. 文件编码: 确保所有文本文件(.json, .txt)以 UTF-8 无BOM 格式保存。在Windows上用记事本编辑时要特别注意。
  5. 权限问题: 确保你的Python进程有权限读取该文件夹下的所有文件。

这个过程虽然有点繁琐,但一旦你成功一次,就会对 sentence-transformers 的模型组织方式有更深的理解。以后再遇到类似问题,或者需要自定义模型结构(比如添加不同的池化层、拼接多个Transformer输出),你都能游刃有余。记住,核心就是那个 modules.json 文件,它定义了整个模型的组装蓝图。

更多推荐