Nemotron 3 Embed 部署完相似度分数偏低、归一化没做还是分词出错?

文章导读
相似度分数偏低但又不是全 0,通常说明向量已经算出来了,问题出在“比较口径”上。这时先别去动分词器:多数情况是模型输出的向量没有归一化、文本在 tokenizer 阶段被截断,或者向量库的度量方式和你的假设不一致,这三件事里的一件。按“打印范数—看切分结果—手算余弦—换距离度量”四步走,每一步都能在本地用几十行代码验证,不必等线上流量来复现。
📋 目录
  1. Ⅰ 打印一条文本输出向量的范数
  2. Ⅱ 把中文长句拆开看切分与截断结果
  3. Ⅲ 手算一对样本的余弦相似度,与库返回分数对齐
  4. Ⅳ 换一种距离度量再跑同一批样本
  5. Ⅴ 把定位结论写成固定检查顺序
A A

相似度分数偏低但又不是全 0,通常说明向量已经算出来了,问题出在“比较口径”上。这时先别去动分词器:多数情况是模型输出的向量没有归一化、文本在 tokenizer 阶段被截断,或者向量库的度量方式和你的假设不一致,这三件事里的一件。按“打印范数—看切分结果—手算余弦—换距离度量”四步走,每一步都能在本地用几十行代码验证,不必等线上流量来复现。

分数偏低不是单一原因。先打印一条文本输出向量的 L2 范数:接近 1 说明模型侧已归一化,明显偏离就要在入库前手动归一化,或确认向量库对所选度量是否内部归一化;范数正常就把长句的 token 数与截断位置打印出来,排除关键内容在进入模型前被切掉;再用同一对文本手算余弦,与向量库返回值逐条比对,差值集中在库侧是度量口径问题,两侧一致则说明这对文本本来就该这么低。检查可全部在本地完成,结论需结合所用模型说明与向量库版本确认。

打印一条文本输出向量的范数

这一步在模型服务进程里或本地推理脚本里执行,目的是确认“归一化到底做了没有”,而不是靠猜。取一条你线上实际用过的中文文本,走完整推理链路后拿到向量,打印它的形状和 L2 范数。

import numpy as np
import torch
from transformers import AutoTokenizer, AutoModel

tok = AutoTokenizer.from_pretrained(MODEL_DIR)
model = AutoModel.from_pretrained(MODEL_DIR)

text = '这里放你线上一条真实的中文查询'
inputs = tok(text, return_tensors='pt', truncation=False)
with torch.no_grad():
    out = model(**inputs)

# 池化方式按模型的说明选:cls 或 mean,两处不要混用
emb = out.last_hidden_state[:, 0, :].squeeze().cpu().numpy()
# emb = out.last_hidden_state.mean(dim=1).squeeze().cpu().numpy()

print('shape:', emb.shape)
print('L2 norm:', np.linalg.norm(emb))
print('max / min:', emb.max(), emb.min())
print('是否有 nan:', np.isnan(emb).any())

判定口径建议这样看:L2 范数与 1 的偏差在 1e-2 量级内,视为模型侧已归一化;范数明显小于 0.5 或远大于 2,视为未归一化或归一化被跳过;出现 nan 则先修推理链路,不要继续往下比分数。

两种结论各对应不同的下一步。若未归一化:确认入库前是否执行过 emb = emb / np.linalg.norm(emb),没有就补上;同时看向量库的度量配置,部分库在 COSINE 度量下会对向量做内部归一化,部分库原样使用,需要按你部署的版本确认,否则同一份向量在两种库里分数会不一样。若已归一化:模型侧排除,直接进第 2 步,别在这里反复调参数。

把中文长句拆开看切分与截断结果

服务正常、范数也正常,但长查询的检索质量明显差,先怀疑文本没被完整送进模型。检查方法是把 tokenizer 的中间结果单独打印出来,而不是只看最终向量。

ids_full = tok(text, truncation=False)['input_ids']
print('完整 token 数:', len(ids_full))
print('尾部 token 还原:', tok.decode(ids_full[-12:]))

MAX_LEN = 512  # 换成你配置文件里的实际值
ids_trunc = tok(text, max_length=MAX_LEN, truncation=True)['input_ids']
print('截断后 token 数:', len(ids_trunc))
print('被切掉的部分:', tok.decode(ids_full[len(ids_trunc):]))

截断位置的确认方式:比较“完整 token 数”和配置文件里的 max_length,两者相等就说明这条文本正好被吃满,尾部内容已经丢了;再把 decode 出来的尾部文本与原文结尾对照,中文可能会有空格或字被拆开的差异,只要语义还在即可,出现明显缺字就是截断。另一个常见原因是中文被拆得过碎:词表以英文为主的模型处理中文时 token 数会明显膨胀,同样长度的句子更容易触顶,这种情况下要结合你的 max_length 一起看,而不是改分词器了事。

截断前后向量对照:把同一条长句分别在“完整 token”和“只保留前一半 token”两种输入下各算一次向量,计算两者余弦。如果这个余弦低到你直觉上不该这么低,说明尾部信息对语义贡献不小,线上分数偏低很可能就是截断造成的。做完这一步再回到第 3 步。

手算一对样本的余弦相似度,与库返回分数对齐

取一组你确认语义相近的中文样本对,在本地手算余弦,再与向量库同一次查询返回的分数并排看。手算用纯 numpy,不要经过任何库的封装。

Nemotron 3 Embed 部署完相似度分数偏低、归一化没做还是分词出错?
def cos(a, b):
    a = np.asarray(a, dtype=np.float64)
    b = np.asarray(b, dtype=np.float64)
    denom = np.linalg.norm(a) * np.linalg.norm(b)
    return float(np.dot(a, b) / (denom + 1e-12))

print('手算余弦:', cos(emb_a, emb_b))

把结果填成下面这样一张表,差值一栏是定位关键。表中数值仅为填写格式示意,实际以你环境打印结果为准。

样本对手算余弦向量库返回分数差值判断方向
A(近义短句)待填待填待填差值大 → 库侧口径问题
B(长句含截断)待填待填待填差值大且 B 更小 → 回到第 2 步
C(不相干文本)待填待填待填两侧都低 → 属正常

偏差出现的位置决定处理动作:手算与库返回基本一致,说明分数计算没问题,偏低是数据本身的问题,要么换更强的向量模型,要么检查这段文本是不是业务里本来就不相似的语料;手算明显高于库返回,说明库在计算前对向量做了额外处理,或者你用错了度量字段,回到第 4 步。

换一种距离度量再跑同一批样本

内积和余弦的区别只在“是否除以模长”。向量未归一化时,内积分数会被模长放大,长文本向量模长往往更大,于是排序被长度带偏,看起来就是“分数乱但又不为 0”。同一批样本改用余弦再跑一次,看排序和分数区间是否变化。

# 向量库度量配置示意,字段名以你使用的库和版本为准
# 例:metric_type / distance / distance_strategy
# IP(内积 / DOT)   -> 未归一化时分数受模长影响
# COSINE             -> 部分库内部自动归一化,部分库原样计算

# 不依赖向量库的本地对照
for a, b in sample_pairs:
    ip  = float(np.dot(a, b))
    cs  = cos(a, b)
    print('IP:', round(ip, 4), ' COSINE:', round(cs, 4))

同批样本的分数对照建议记成两列,重点看三件事:TOP1 结果是否变化、近义样本与不相干样本的分差是否被拉大、长句样本是否系统性偏高。如果 IP 下长句明显占优而余弦下回归正常,基本可以判定是度量方式与“未归一化向量”不匹配;如果两种度量下排序一致,说明度量不是主因,问题仍在切分或数据本身。

把定位结论写成固定检查顺序

把这四步固定成排查顺序,下次遇到分数偏低可以直接照跑,不用重新想从哪下手。

顺序动作通过标准跳过会漏掉什么
1打印一条向量范数无 nan,且明确知道是否已归一化会把度量不匹配误判成分词问题,改错方向
2打印 token 数与截断位置完整性可解释,尾部内容确认未丢关键语义长查询的低分会被误认为模型能力不足
3手算余弦并与库返回值比对差值为 0 或可解释分不清是计算错还是语料本身不相似
4换度量跑同批样本两种度量下结论一致,或差异可用归一化解释会一直在模型和分词上打转,忽略库配置

实践上建议把第 1 步和第 2 步的打印写进服务启动时的自检,日志里保留一条范数和 token 数,出问题时不必再改代码复现。第 3、4 步属于定位手段,不必常驻,但要留一份可运行的比对脚本,遇到“分数偏低”的反馈时先跑脚本,再决定要不要动模型配置。