RAG/搜索召回之二——Embedding模型bge-m3微调
今天介绍Embedding模型bge-m3的微调。在做向量召回的时候我们会使用bge-m3这样的模型把query和doc转化成embedding。但在一些专业领域尤其是术语比较多的领域bge-m3的效果可能就不好。这时候比较有效的方法就是使用少量的高质量样本进行lora微调。LoRALow-Rank Adaptation是目前大模型微调最主流的方法之一它的核心思想非常简单冻结原始模型参数只训练一小部分新增的低秩矩阵。假设我们有一个 Embedding 模型例如bge-m3ransformer 中一个线性层通常是y W x yWxyWx其中W 是模型参数x 是输入y 是输出。例如W的维度是[4096,4096]参数量为4096*4096约1677万参数。整个模型有几百个这样的矩阵如果全量微调需要更新所有的参数需要保存梯度、保存 Adam Optimizer 状态、更新全部参数显存需求通常会达到模型参数量的数倍。LoRA不更新W只在原来的参数上学习一个变化矩阵ΔW这样W ′ W Δ W WWΔWW′WΔW。而Δ W B A ΔWBAΔWBAq其中A为的维度[r,d]B的维度为[d,r]。当r比较小的时候通常为8、16参数量就能大幅减少。这个r就是所谓的秩。下面介绍使用LoRA对bge-m3微调过程。1 准备高质量的微调样本我这边针对的领域是“新能源电池”。样本不用特别多几百条到几千条就行。格式如下{”query”: ”800V对电池性能有何影响”, ”pos”: [”在新能源电池的快充系统场景中800V 架构降低线束电流有利于高功率快充但绝缘监控与电弧防护要求更高。该点常作为材料选型、电芯设计或系统集成时的判断依据。”], ”neg”: [”在新能源电池的性能指标低温场景中低温下电解液粘度升高、电荷转移阻抗增大表现为可用容量与功率下降。该点常作为材料选型、电芯设计或系统集成时的判断依据。”, ”在新能源电池的正极材料结构场景中三元正极 NCM811 中镍含量约 80%可提高能量密度但对空气和水更敏感加工需严格控湿。该点常作为材料选型、电芯设计或系统集成时的判断依据。”], ”pos_ids”: [”6497b91f95f0cc53”], ”neg_ids”: [”246887d6a13e5f72”, ”349a5a7d6496d616”], ”seed_id”: ”3aabe3694a2d”, ”topic”: ”快充”, ”aspect”: ”系统”}必须包含的字段query是查询词pos为正样本只要一条就行neg为难负样本通常包含多条。注意这里的负样本一定要是难负样本如果是一些随机的样本效果应该就不太行。我这里的训练集合是780条测试集合是100条。2 环境我的机器是MacBook Pro M3 16G。python环境python3.10安装所需的库torch2.1.0 transformers4.40.0 sentence-transformers3.0.0 peft0.11.0 accelerate0.30.0 datasets2.19.0 pyyaml6.0 numpy1.24.0 tqdm4.66.0 scikit-learn1.3.03 微调网络我们采用PEFT库的get_peft_model函数把普通的Transformer模型改造成LoRA模型。然后在LoRA模型用infoNce进行微调。首先定义一个数据集class TripletJsonlDataset(Dataset): def __init__(self, path: Path, num_negatives: int 2): self.rows read_jsonl(path) self.num_negatives num_negatives self._pos_pool [r[”pos”][0] for r in self.rows if r.get(”pos”)] def __len__(self) - int: return len(self.rows) def _sample_random_negs(self, need: int, forbid_text: str) - List[str]: ”””Sample other rows positives as random negatives (never the current pos).””” if need 0 or not self._pos_pool: return [] out: List[str] [] tries 0 while len(out) need and tries need * 20: tries 1 cand random.choice(self._pos_pool) if cand forbid_text or cand in out: continue out.append(cand) return out def __getitem__(self, idx: int) - Dict[str, Any]: row self.rows[idx] pos row[”pos”][0] negs list(row.get(”neg”) or [])[: self.num_negatives] if len(negs) self.num_negatives: negs negs self._sample_random_negs( self.num_negatives - len(negs), forbid_textpos ) return { ”query”: row[”query”], ”pos”: pos, ”negs”: negs, }这块就是从数据集中读取query、pos和negs字段。如果negs的数量比需要的少还会随机从别的样本中随机采样进行补充。从bge-m3模型构建LoRA微调模型def build_model(model_name: str, lora_cfg: Dict[str, Any], device: str): tokenizer AutoTokenizer.from_pretrained(model_name) base AutoModel.from_pretrained(model_name) peft_config LoraConfig( task_typeTaskType.FEATURE_EXTRACTION, rint(lora_cfg[”lora_r”]), lora_alphaint(lora_cfg[”lora_alpha”]), lora_dropoutfloat(lora_cfg[”lora_dropout”]), target_moduleslist(lora_cfg[”target_modules”]), bias”none”, ) model get_peft_model(base, peft_config) model.print_trainable_parameters() model.to(device) return tokenizer, model相关配置文件model_name: BAAI/bge-m3 train_file: data/train.jsonl eval_file: data/eval.jsonl output_dir: outputs/lora # LoRA lora_r: 16 lora_alpha: 32 lora_dropout: 0.05 target_modules: - query - value # Training (Mac M3 16G friendly) max_seq_length: 512 train_batch_size: 2 eval_batch_size: 4 gradient_accumulation_steps: 8 num_epochs: 2 learning_rate: 1.0e-4 warmup_ratio: 0.05 weight_decay: 0.01 seed: 42 num_negatives: 2 # Device: auto | mps | cpu device: auto这里只微调query和valuekey和dense都没有微调如果显存足够可以考虑也开启。Loss定义def info_nce_loss( q: torch.Tensor, p: torch.Tensor, negs: torch.Tensor, temperature: float 0.02, ) - torch.Tensor: ””” q: [B, D], p: [B, D], negs: [B, N, D] ””” pos_logit torch.sum(q * p, dim-1, keepdimTrue) / temperature # [B, 1] neg_logit torch.einsum(”bd,bnd-bn”, q, negs) / temperature # [B, N] logits torch.cat([pos_logit, neg_logit], dim1) # [B, 1N] labels torch.zeros(q.size(0), dtypetorch.long, deviceq.device) return F.cross_entropy(logits, labels)Encoder把文本调用bge-m3得到embeddingdef encode( model: torch.nn.Module, tokenizer, texts: List[str], device: str, max_length: int, ) - torch.Tensor: batch tokenizer( texts, paddingTrue, truncationTrue, max_lengthmax_length, return_tensors”pt”, ) batch {k: v.to(device) for k, v in batch.items()} outputs model(**batch) # BGE-style: CLS token; keep compatible if pooler absent if hasattr(outputs, ”last_hidden_state”): cls outputs.last_hidden_state[:, 0] else: cls outputs[0][:, 0] return F.normalize(cls, p2, dim-1)训练代码def train(cfg: Dict[str, Any]) - None: set_seed(int(cfg.get(”seed”, 42))) device pick_device(str(cfg.get(”device”, ”auto”))) print(f”device{device}”) train_path ROOT / cfg[”train_file”] eval_path ROOT / cfg[”eval_file”] out_dir ROOT / cfg[”output_dir”] out_dir.mkdir(parentsTrue, exist_okTrue) tokenizer, model build_model(cfg[”model_name”], cfg, device) train_ds TripletJsonlDataset(train_path, num_negativesint(cfg.get(”num_negatives”, 2))) eval_rows read_jsonl(eval_path) loader DataLoader( train_ds, batch_sizeint(cfg[”train_batch_size”]), shuffleTrue, collate_fncollate_keep, num_workers0, ) optimizer torch.optim.AdamW( [p for p in model.parameters() if p.requires_grad], lrfloat(cfg[”learning_rate”]), weight_decayfloat(cfg.get(”weight_decay”, 0.01)), ) steps_per_epoch math.ceil(len(loader) / int(cfg[”gradient_accumulation_steps”])) total_steps steps_per_epoch * int(cfg[”num_epochs”]) warmup_steps int(total_steps * float(cfg.get(”warmup_ratio”, 0.05))) scheduler get_linear_schedule_with_warmup(optimizer, warmup_steps, total_steps) max_length int(cfg[”max_seq_length”]) accum int(cfg[”gradient_accumulation_steps”]) best_acc -1.0 global_step 0 for epoch in range(int(cfg[”num_epochs”])): model.train() running 0.0 optimizer.zero_grad(set_to_noneTrue) pbar tqdm(loader, descf”epoch {epoch1}/{cfg[num_epochs]}”) for step, batch in enumerate(pbar, start1): q encode(model, tokenizer, batch[”query”], device, max_length) p encode(model, tokenizer, batch[”pos”], device, max_length) # negs: list of lists - encode flat then reshape flat_negs [n for group in batch[”negs”] for n in group] n_emb encode(model, tokenizer, flat_negs, device, max_length) bsz len(batch[”query”]) n_per len(batch[”negs”][0]) n_emb n_emb.view(bsz, n_per, -1) loss info_nce_loss(q, p, n_emb) / accum loss.backward() running loss.item() * accum if step % accum 0 or step len(loader): torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() scheduler.step() optimizer.zero_grad(set_to_noneTrue) global_step 1 pbar.set_postfix(lossf”{running / step:.4f}”) acc eval_pair_accuracy(model, tokenizer, eval_rows, device, max_length) print(f”epoch{epoch1} eval_pair_acc{acc:.4f}”) ckpt out_dir / f”epoch-{epoch1}” ckpt.mkdir(parentsTrue, exist_okTrue) model.save_pretrained(ckpt) tokenizer.save_pretrained(ckpt) meta {”epoch”: epoch 1, ”eval_pair_acc”: acc, ”global_step”: global_step} (ckpt / ”train_meta.json”).write_text(json.dumps(meta, indent2), encoding”utf-8”) if acc best_acc: best_acc acc best_dir out_dir / ”best” best_dir.mkdir(parentsTrue, exist_okTrue) model.save_pretrained(best_dir) tokenizer.save_pretrained(best_dir) (best_dir / ”train_meta.json”).write_text( json.dumps(meta, indent2), encoding”utf-8” ) print(f”saved best adapter - {best_dir}”) print(f”done. best_eval_pair_acc{best_acc:.4f}”)训练日志trainable params: 1,572,864 || all params: 569,327,616 || trainable%: 0.2763 epoch 1/2: 100%|█████████████████████████████████████████| 390/390 [02:2600:00, 2.66it/s, loss0.4972] epoch1 eval_pair_acc0.9600 saved best adapter - ./outputs/lora/best epoch 2/2: 100%|█████████████████████████████████████████| 390/390 [02:1900:00, 2.79it/s, loss0.2856] epoch2 eval_pair_acc0.9700 saved best adapter - ./outputs/lora/best done. best_eval_pair_acc0.97004 评估微调效果 baseline { “MRR10”: 0.6589007936507936, “nDCG10”: 0.7273867753390105, “Recall10”: 0.94} lora { “MRR10”: 0.7681666666666669, “nDCG10”: 0.8272662245965128, “Recall10”: 1.0}从上面可以看出微调后效果有明显的提升。学AI大模型的正确顺序千万不要搞错了2026年AI风口已来各行各业的AI渗透肉眼可见超多公司要么转型做AI相关产品要么高薪挖AI技术人才机遇直接摆在眼前有往AI方向发展或者本身有后端编程基础的朋友直接冲AI大模型应用开发转岗超合适就算暂时不打算转岗了解大模型、RAG、Prompt、Agent这些热门概念能上手做简单项目也绝对是求职加分王给大家整理了超全最新的AI大模型应用开发学习清单和资料手把手帮你快速入门学习路线:✅大模型基础认知—大模型核心原理、发展历程、主流模型GPT、文心一言等特点解析✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑✅开发基础能力—Python进阶、API接口调用、大模型开发框架LangChain等实操✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经以上6大模块看似清晰好上手实则每个部分都有扎实的核心内容需要吃透我把大模型的学习全流程已经整理好了抓住AI时代风口轻松解锁职业新可能希望大家都能把握机遇实现薪资/职业跃迁这份完整版的大模型 AI 学习资料已经上传CSDN朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】