当前位置: 首页 > news >正文

Embedding 模型微调实战:从 22% 到 97.9% 的踩坑记录

我们有一个基于 RAG(检索增强生成)的 AI 客服系统,核心逻辑是:用户提交新工单时,先把它向量化,然后去历史工单库里找相似的,再把检索结果喂给 LLM 生成回复。

向量化用的是 Embedding 模型,部署在本地 Ollama 上。系统跑着还行,但总感觉检索质量一般,于是决定用自己的工单数据微调一下。

测试的三个模型:

模型ChromaDB 集合类型
mxbai-embed-largedocs基线
mxbai-mnrl-finetuneddocs_mxbai_finetuned微调版
bge-m3docs_bge基线

数据准备:构建 Triplet 训练集

微调 Embedding 模型用的是 Triplet 结构:每条训练样本由三部分组成 —Anchor(锚点)、Positive(正例)、Negative(负例)

  • Anchor:一个工单
  • Positive:与 Anchor 存在关联关系的工单(重复工单、相关工单等)
  • Negative:与 Anchor 无关的工单

关系数据来自 Mantis 的mantis_bug_relationship_table,总共有 5715 个有关系的工单对。

# 只用重复工单关系(rel-types 0=related_to, 1=duplicate_of) python finetune/train_mxbai.py --generate-data --rel-types 0 1 --negatives-per-positive 1 # 全部关系类型(5715 个工单对) python finetune/train_mxbai.py --generate-data --rel-types 0 1 2 # GPU 显存不够时 python finetune/train_mxbai.py \ --generate-data \ --rel-types 0 1 2 \ --batch-size 8 \ --grad-accum 4 # 查看数据量 wc -l data/finetune/triplets_train.jsonl data/finetune/triplets_eval.jsonl

注意:评估集data/finetune/triplets_eval.jsonl是固定的,不能被覆盖。跑不同实验时要用--data-base data/finetune/triplets_dup指定不同路径。


模型结构与微调策略

为什么冻结前 22 层?

mxbai-embed-large 基于 BERT-large,共 24 层 Transformer。越底层学到的知识越通用:

层数学到的内容
1–6基础语法、词形、位置信息(跨语言通用)
7–16语义组合、句子结构
17–23任务相关的高层语义 ←微调的目标
24最终 Embedding 表示
Pooling聚合层

--freeze-layers 22只训练最顶部的 2 层 + Pooling,大约只更新 3.5% 的参数(~12M / 335M)。这样既避免了灾难性遗忘(Catastrophic Forgetting),又节省了 GPU 显存。


损失函数

余弦相似度

sim(a,b)=a⋅b∥a∥∥b∥

符号含义
a, b两个文本向量(模型的 Embedding 输出)
a · b点积:各分量乘积之和
‖a‖向量 a 的长度(范数)
sim(a, b)取值范围 −1 到 1;1 = 完全相同,0 = 完全无关

TripletLoss

有显式负例时使用(--train-jsonl数据带negative列):

L=max(0, d(a,p)−d(a,n)+m)

符号含义
L损失值;训练过程中模型会最小化它
aAnchor — 查询工单
pPositive — 语义相似的工单
nNegative — 语义无关的工单
d(a, p)Anchor 与 Positive 的距离(欧氏距离)— 越小越好
d(a, n)Anchor 与 Negative 的距离(欧氏距离)— 越大越好
m最小间距(= 0.5):d(a, n) 必须比 d(a, p) 大出这个值,否则产生损失

MultipleNegativesRankingLoss(MNRL)

只需 Anchor + Positive,用--use-mnrl开启:

L=−1NN∑i=1logesim(ai,pi)/τN∑j=1esim(ai,pj)/τ

符号含义
NBatch 大小(每步的训练样本数)
i当前 Anchor 的序号
jBatch 中其他所有样本的序号 — 它们自动充当负例
aᵢ, pᵢ第 i 个 Anchor 和对应的 Positive
e^(·)指数函数(让所有值为正)
τ(Tau)温度系数;越小区分越清晰
log自然对数;损失最小化意味着 sim(aᵢ, pᵢ) 在 Batch 中最大

关键:Batch 中其他所有 Positive 都作为负例(In-Batch Negatives)。N 越大,负例越多,训练信号越强。gradient_accumulation_steps不会增大 N,只有batch_size才算。


训练命令

mxbai — 最佳方案(MNRL + 大 Batch)

source ~/venv/bin/activate PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \ python finetune/run_mxbai_experiments.py --run --preset mnrl_large_batch

等价于:

python finetune/train_mxbai.py \ --output-dir finetune/models/mxbai-mnrl-large-batch \ --use-mnrl \ --freeze-layers 22 \ --batch-size 32 \ --grad-accum 1 \ --lr 2e-6 \ --epochs 1

结果:TripletEvaluator cosine_accuracy 97.9%(基线:88.9%)


mxbai — 备选方案(TripletLoss,聚焦重复工单)

python finetune/train_mxbai.py \ --output-dir finetune/models/mxbai-duplicate-triplet \ --generate-data \ --rel-types 1 4 \ --neg-strategy random \ --negatives-per-positive 1 \ --freeze-layers 22 \ --batch-size 8 \ --grad-accum 4 \ --lr 5e-6 \ --epochs 1

结果:96.0%


bge-m3 — 最佳方案(TripletLoss,聚焦重复工单)

source ~/venv/bin/activate PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \ python finetune/run_bge_m3_experiments.py --run --preset duplicate_triplet

等价于:

python finetune/train_bge_m3.py \ --output-dir finetune/models/bge-m3-duplicate-triplet \ --generate-data \ --rel-types 1 4 \ --neg-strategy random \ --negatives-per-positive 1 \ --freeze-layers 22 \ --batch-size 4 \ --grad-accum 8 \ --lr 3e-6 \ --epochs 1

结果:96.4%(基线:94.5%)


训练监控(TensorBoard)

tensorboard --logdir=finetune/models/ --host 0.0.0.0 --port 6006 # 浏览器访问:http://<gpu-server-ip>:6006

模型调试与对比

# 检查架构、NaN、Embedding 坍缩 python finetune/debug_model.py --model-a finetune/models/mxbai-mnrl-large-batch # 基线 vs 微调版对比 + 运行 TripletEvaluator python finetune/debug_model.py \ --compare --eval \ --model-a mixedbread-ai/mxbai-embed-large-v1 \ --model-b finetune/models/mxbai-mnrl-large-batch \ --eval-jsonl data/finetune/triplets_eval.jsonl # 清理旧 checkpoint rm -rf finetune/models/mxbai-finetuned/checkpoint-*

HuggingFace → Ollama(GGUF 导出)

Ollama 只能运行模型,不能训练。HuggingFace 的 PyTorch 格式需要先转成 GGUF 才能用 Ollama 部署。

Step 1:转换为 GGUF(fp32 → f16)

# 需要安装 llama.cpp python llama.cpp/convert_hf_to_gguf.py finetune/models/mxbai-mnrl-large-batch \ --outfile finetune/models/mxbai-mnrl-finetuned.gguf \ --outtype f16

Step 2:注册到 Ollama

echo 'FROM finetune/models/mxbai-mnrl-finetuned.gguf' > Modelfile ollama create mxbai-mnrl-finetuned -f Modelfile

Step 3:对比 HuggingFace 与 Ollama 的输出(精度验证)

python finetune/compare_hf_vs_ollama.py \ --hf-model finetune/models/mxbai-mnrl-large-batch \ --ollama-model mxbai-mnrl-finetuned:latest \ --ollama-host http://localhost:11434

预期结果:平均向量对齐度 > 0.990(GGUF f16 对方向的保真度很好)。

Step 4:写入 ChromaDB

python -m src.createemb \ --embedding-model mxbai-mnrl-finetuned:latest \ --collection docs_mxbai_finetuned

端到端评估(Recall@K / MRR)

Recall@K— 查询中,正确工单出现在前 K 个结果里的比例:

Recall@K=1|Q|∑q∈Q1[相关工单在 Top-K 中]

符号含义
Q全部测试查询集合
|Q|测试查询的数量
K看前 K 个结果(例如 K=5:看前 5 名)
1[...]正确工单在前 K 名则为 1,否则为 0
结果解读Recall@5 = 0.5 表示:50% 的查询里正确工单在前 5 名内

MRR(Mean Reciprocal Rank)— 第一个正确结果排名的倒数均值:

MRR=1|Q|∑q∈Q1rankq

符号含义
rank_q查询 q 的第一个正确结果排名(第 1 名最好)
1/rank_q倒数:第 1 名 → 1.0;第 2 名 → 0.5;第 5 名 → 0.2;未找到 → 0
结果解读MRR = 1.0:每次都排第 1;MRR = 0.5:平均排第 2

两个指标均基于mantis_bug_relationship_table中的真实关系对。

# 训练前跑基线 python tools/eval_recall.py --issues 200 --output-json results/recall_baseline.json # 三个模型对比(相同 seed = 相同查询工单) python tools/eval_recall.py --model bge-m3 --collection docs_bge --issues 200 --seed 42 --output-json results/eval_bge.json python tools/eval_recall.py --model mxbai-embed-large --collection docs --issues 200 --seed 42 --output-json results/eval_mxbai.json python tools/eval_recall.py --model mxbai-mnrl-finetuned --collection docs_mxbai_finetuned --issues 200 --seed 42 --output-json results/eval_mxbai_ft.json

手动搜索测试结果

查询 1:"Bestellung ist ohne Wareneingang abgeschlossen"

排名mxbai-baseline (docs)mxbai-finetuned (docs_mxbai_finetuned)bge-m3 (docs_bge)
#118534 "GOODS_RECEIPT-Telegramme" 0.985 ❌43165 "Bestellung ist ohne Wareneingang abgeschlossen"0.980 ✓431651.000 ✓
#231337 0.98318534 0.95135104 0.931

微调版 mxbai 和 bge-m3 都将 Issue 43165(与查询完全一致)排在第 1 位;mxbai 基线漏掉了它。


查询 2:"Login Fehler"

排名mxbai-baseline (docs)mxbai-finetuned (docs_mxbai_finetuned)bge-m3 (docs_bge)
#141258 "Login fehlgeschlagen"0.948 ✓33252 "Fehlermeldung beim Kommissionieren" 0.957 ❌41258 "Login fehlgeschlagen"0.977 ✓
#233252 "Kommissionieren" 0.946 ❌19454 "Fehler" 0.952 ❌30743 "Fehler bei der Anmeldung" 0.947 ✓
#3–5"Fehler" 0.933 ❌"Fehler" 0.952(×4)❌456 "Fehlermeldung beim Anmelden" 0.944 ✓

mxbai-finetuned 的前 5 条里完全没有 Issue 41258。4 条结果相似度完全相同(0.952),这是 Hub-Node 问题的典型症状:Issue 33252 的向量成了"磁铁",把大量无关查询都吸过去了。bge-m3 在两次查询中都稳定返回语义相关结果。


评估结果

TripletEvaluator(离线,cosine_accuracy)

两个测试集:full-type-random(全部关系类型,712 个 Triplet)和dup-random(只有重复工单关系,586 个 Triplet)。随机基线:50%。

Cosine Accuracy(TripletEvaluator 指标):Triplet 中 Anchor 与 Positive 的相似度高于 Negative 的比例:

cosine_accuracy=1NN∑i=11[sim(ai,pi)>sim(ai,ni)]

符号含义
N测试集中 Triplet 总数
1[...]条件为真时为 1,否则为 0
aᵢ, pᵢ, nᵢ第 i 个 Triplet 的 Anchor、Positive、Negative
结果百分比,0%–100%;随机基线 = 50%
MXBAI
模型full-type-randomdup-random
mxbai-base88.9%89.8%
triplet_baseline77.4%80.0%
duplicate_triplet95.7%96.9%
hard_negative_triplet88.8%88.9%
mnrl_large_batch97.9%98.0%
BGE-M3
模型full-type-randomdup-random
bge-m3-base94.9%94.5%
duplicate_triplet95.4%96.4%
mnrl_batch1695.4%95.9%

注:微调版 bge-m3 尚未转换为 GGUF 格式,也没有对应的 ChromaDB 集合,因此不在端到端评估中。

端到端 RAG 评估(200 个工单,seed=42,threshold=0.00)

仅对已部署为 Ollama GGUF 并写入 ChromaDB 的模型:

Recall@K(全部 K 值)

模型K=1K=3K=5K=10MRREmbedding 失败数
mxbai-embed-large(基线)0.2100.2800.3150.3900.26174
mxbai-mnrl-finetuned0.2500.3050.3300.3650.28970
bge-m3(基线)0.3450.4500.5050.5400.4131

按关系类型的 Recall@10

关系类型mxbai-基线mxbai-finetunedbge-m3
related_to0.4800.4830.880
duplicate_of0.4360.375 ↓0.537
parent_of0.3700.500 ↑0.600

关键发现:mxbai-finetuned 在duplicate_of(重复工单)上的表现比基线下降了(0.375 vs 0.436),而这正是微调的目标场景。这是 Hub-Node 问题在核心场景上的直接危害。


踩坑记录:为什么准确率从 22% 涨到 97.9%

这是本文最有价值的部分。第一次跑训练的结果远低于随机基线(50%),说明模型不只是"没学好",而是主动学错了。

阶段一:22% —— 三个 Bug 同时存在(4 月 30 日)

Bug 1:TripletLoss 的 margin 默认值是 5.0,应该用 0.5

# 错误写法(sentence-transformers 默认 margin = 5.0) loss = TripletLoss(model) # 正确写法(commit 9fb67fd,5 月 5 日修复) loss = TripletLoss(model, triplet_margin=0.5)

归一化向量的欧氏距离范围是 0 到 2。Loss 归零的条件是 d(a,n) − d(a,p) > m,而 margin=5.0 要求距离差大于 5.0——但最大差值只有 2.0,这个条件永远无法满足,Loss 始终为正值,梯度方向持续错误,模型越训越糟。

Bug 2:模型以 bf16 格式保存,加载后出现 NaN

# 错误写法:直接以训练精度保存 model.save_pretrained(config.output_dir) # 正确写法(commit 085f4db,5 月 5 日修复) model.float() # bf16 → fp32 model.save_pretrained(config.output_dir)

bf16 只有 3 位尾数精度。序列化时的舍入误差,在反序列化后变成了 NaN 值。NaN 向量算出来的余弦相似度也是 NaN,NaN > NaN永远是False,TripletEvaluator 每条都判错 → 准确率趋近 0%。

Bug 3:没有冻结层,灾难性遗忘

不加--freeze-layers时,335M 个参数全部参与更新。结合 Bug 1 产生的错误梯度,底层通用语言知识被大幅覆盖。


阶段二:44% —— 修了 NaN 和 margin,其他问题还在(5 月 5 日后)

修复 Bug 1 和 Bug 2 后,模型终于输出了有效的 Embedding。44% 略低于随机基线(50%),说明模型没有完全崩溃,但由于全量参数训练和训练数据噪声,还是损失了不少通用知识。

此时还未解决:

  • 全部 335M 参数可训练(无层冻结)
  • 训练数据包含大量"无意义模板文本"(如"Auslieferung der aktuellen Serverklassen")
  • MNRL 模式下 TripletEvaluator 根本没有运行(_trainer.py里有一个判断写错了)

阶段三:97.9% —— 三项改进后的质变(5 月 6 日起)

改进 1:冻结前 22 层(commit 9d15a94,5 月 5 日)

只训练最顶部的 2 层 + Pooling,底层 22 层的通用语言知识完整保留,灾难性遗忘消失。

改进 2:过滤训练数据中的模板文本(commit d02c6c0,5 月 6 日)

像"Auslieferung der aktuellen Serverklassen"这类工单的 Summary 对模型来说没有任何语义区分度,加进去只会污染训练。在dataset.py里加了LOW_QUALITY_PREFIXES/LOW_QUALITY_PHRASES黑名单过滤。

改进 3:切换到 MNRL + batch_size=32

TripletLoss 每个 Anchor 只有 1 个负例,而 MNRL 在 batch_size=32 时,每个 Anchor 有 31 个负例,训练信号强得多。需要注意:gradient_accumulation_steps不增加 MNRL 的负例数量,只有batch_size才算。


Bug 汇总表

时间问题影响修复方法
初始TripletLoss margin = 5.0(默认值)梯度方向持续错误改为 margin = 0.5
初始模型以 bf16 保存NaN 值 → 准确率 ≈ 0%model.float()后再保存
初始无层冻结灾难性遗忘--freeze-layers 22
初始训练集含模板文本训练数据噪声高dataset.py加质量过滤
初始MNRL 模式不运行 TripletEvaluatorMNRL 结果无法测量修正_trainer.py中的判断

最终结论

离线指标(TripletEvaluator)

mxbai-mnrl-large-batch 以 97.9%/98.0% 拿下最高分,明显高于 bge-m3 基线(94.9%)。mxbai 微调效果提升很大;bge-m3 本身基线已经很强,微调的增益有限。
在生产库上为mxbai-mnrl-finetuned嵌入了专用chroma collection, mxbai-finetuned实测结果不如bge-m3。所以嵌入模型更换成品模型比微调收益大且更稳定。但是我不认为折腾这么些是浪费时间,至少知道了嵌入模型微调具体是怎么操作的。

端到端 RAG

bge-m3(无微调)在真实检索场景中全面胜出

  • Recall@5:0.505 vs 0.315 / 0.330(绝对提升 +0.190 / +0.175)
  • Recall@10:0.540 vs 0.390 / 0.365
  • MRR:0.413 vs 0.261 / 0.289(绝对提升 +0.152 / +0.124)
  • Embedding 失败数:1 vs 74 / 70,因为 bge-m3 上下文窗口 8192 token,mxbai 只有 512 token,长工单直接截断

微调对 mxbai 的提升极为有限(Recall@5 仅 +0.015,MRR +0.028),同时让 duplicate_of 召回率从 0.436 下降到 0.375——这恰恰是微调专门针对的场景,说明 Hub-Node 问题已实质性地损害了模型的核心价值。

手动搜索测试(见上一节)显示mxbai-mnrl-large-batch的表现不稳定:一个查询排名正确,另一个查询的正确答案完全不在前 5 名,且 4 条结果相似度完全相同,确认存在 Hub-Node 问题。行为依赖具体查询,生产环境不够可靠。

当前生产建议

bge-m3(docs_bge)是当前最稳的选择,在定量和定性两个维度均优于其他模型。mxbai-mnrl-finetuned因 Hub-Node 不稳定不建议上生产。


已知问题

  • docs_mxbai_finetuned里的 HNSW Hub-Node 问题:Block 类型的 Embedding 在相同查询下可能返回不同结果。原因:微调压缩了 Embedding 分布,放大了 HNSW 的局部最优问题。docsdocs_bge不受影响。
  • bf16 → fp32 必须在保存前执行:_trainer.py第 207 行model.float()不能删。bf16 序列化会产生 NaN,加载后模型完全损坏。

参考文献

[1] Schroff, F., Kalenichenko, D., & Philbin, J. (2015).FaceNet: A Unified Embedding for Face Recognition and Clustering.CVPR 2015. arXiv:1503.03832
— TripletLoss 的原始论文:L=max(0, d(a,p)−d(a,n)+m),margin 参数的来源。

[2] Karpukhin, V., Oğuz, B., Min, S., Lewis, P., Wu, L., Edunov, S., & Yih, W. (2020).Dense Passage Retrieval for Open-Domain Question Answering.EMNLP 2020. arXiv:2004.04906
— 将 In-Batch Negatives 确立为检索训练标准;解释了为什么 batch_size 而非 gradient_accumulation 决定 MNRL 的训练信号强度。

[3] Voorhees, E. M. (1999).The TREC-8 Question Answering Track Report.TREC 1999.
— MRR(Mean Reciprocal Rank)指标的来源论文。

[4] Malkov, Y. A., & Yashunin, D. A. (2018).Efficient and Robust Approximate Nearest Neighbor Search Using Hierarchical Navigable Small World Graphs.IEEE TPAMI. arXiv:1603.09320
— ChromaDB 默认索引算法 HNSW 的原始论文;Hub-Node 问题的理论根源。

[5] Kirkpatrick, J., Pascanu, R., Rabinowitz, N., et al. (2017).Overcoming Catastrophic Forgetting in Neural Networks.PNAS 114(13). arXiv:1612.00796
— 灾难性遗忘的理论分析,--freeze-layers 22策略的学术依据。


作者:Rest探路者
出处:Rest探路者 - 博客园
本文版权归作者和博客园共有,欢迎转载,但未经作者同意请保留此段声明,请在文章页面明显位置给出原文连接
Github:cjy513203427 (Chen, Jinyao) · GitHub

免责声明:本内容来自平台创作者,博客园系信息发布平台,仅提供信息存储空间服务。

好文要顶 关注我 收藏该文 微信分享

Rest探路者
粉丝 - 126 关注 - 9

+加关注

1

0

« 上一篇: 重温星际2强化学习之QLearning(一)

posted @ 2026-05-13 20:36 Rest探路者 阅读(244) 评论(2) 收藏 举报

http://www.jsqmd.com/news/1078901/

相关文章:

  • 基于QUBO模型的量子计算在信用评分卡组合优化中的应用研究
  • scikit-learn工业级建模实战:从数据加载到上线部署的26个关键节点
  • 研究技术软件工程研究方法的实证研究与案例研究对比
  • 分层设计的记忆系统
  • 多模态RAG实战:让AI真正看懂PDF中的文字、表格与流程图
  • 25元打造AI智能眼镜:OpenGlass开源项目技术解析与实现指南
  • AI 建议加索引后查询仍变慢:从联合索引、回表与分页排序看慢 SQL 排查
  • 安装 Envoy Gateway
  • 知识库文档清洗:垃圾进垃圾出
  • AI模型访问控制机制与能力评估实践指南
  • C++大成之路:右值引用 move 语义
  • 抖音账号与手机号关联验证:合规路径、技术实现与风险规避指南
  • 9 年 IDEA 老用户,终于把它彻底卸载了!
  • SMD贴片式网络变压器专业厂家的核心能力解码:技术壁垒与行业实践
  • ESPHome:用配置文件搞定智能硬件开发
  • 不用注册就能用的 Web 应用合集
  • 协同线程与协同函数
  • 【JetBrains认证工程师亲授】:Ubuntu下IntelliJ IDEA免sudo安装+全局命令行启动+Shell集成三步到位(实测11种发行版兼容)
  • 【软工方法论22】代码重构原则与实践
  • 还在用 SSMS 手动导入 Excel?这款插件让 SQL Server 数据导入效率提升 10 倍(支持 Upsert + 大数据流式导入)
  • V 语言精选资源库
  • Kubernetes Pod 完全指南:从入门到实战,轻松掌握容器编排核心
  • 【题目讲解】 算法系列之定长类滑动窗口解析(上)
  • 拆解RAG分层架构:文档解析、切片、向量检索、问答逻辑解耦(原理+案例+Java代码)
  • 截断流Witt代数的模表示:基于p-特征与高度的简单模分类与构造
  • Go语言的sync.RWMutex读写锁升级与降级在并发访问模式变化中的限制
  • 2026 洗衣液十大名牌最新资讯汇总 主流品牌定位与家用场景指南
  • 分类评估指标实战指南:从混淆矩阵到业务价值落地
  • 高维点集密度分析:Jensen不等式与凸性原理的应用
  • 配置wsl记录(坎坷版)