首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >大模型微调企业——决定微调成败的基石

大模型微调企业——决定微调成败的基石

原创
作者头像
用户12502927
修改2026-07-29 17:44:52
修改2026-07-29 17:44:52
1450
举报

第一阶段:数据工程——决定微调成败的基石

在微调中,数据质量远比数量重要。我们的原始数据来自公司内部的研报库和人工撰写的摘要(已脱敏)。经过清洗,最终保留1.2万条高质量样本。

1.1 数据格式设计

我们采用 Alpaca-style 的指令微调格式,每条数据包含三个字段:

代码语言:javascript
复制
{
  "instruction": "请根据以下研报内容,生成一段不超过300字的摘要,并提取目标价、评级和EPS预测。",
  "input": "研报全文(约8000字)...",
  "output": "摘要文本... \n【关键指标】目标价:15.8元;评级:买入;EPS预测:2025年1.24元,2026年1.52元。"
}

为了提高训练稳定性,我们将instruction固定,只在inputoutput上变化。同时,我们要求摘要严格遵循三段式结构:行业背景 → 公司核心逻辑 → 风险提示,在输出中对格式做了明确约束。

1.2 数据增强与去重

由于原始摘要风格不统一,我们做了一轮人工复核,并利用大模型(GPT-4)对20%的低质量样本进行重写,然后人工验收。同时,使用datasets库进行去重和长度过滤:

代码语言:javascript
复制
from datasets import Dataset
import pandas as pd

df = pd.read_json("raw_data.jsonl", lines=True)
# 过滤 input 长度 < 500 字或 output 长度 < 50 字的样本
df = df[(df['input'].str.len() >= 500) & (df['output'].str.len() >= 50)]
# 基于input的SimHash去重(简化示例)
df = df.drop_duplicates(subset=['input'])
# 最终训练集 1.2w 条,验证集 1500 条
1.3 Tokenization 关键配置

我们使用Qwen2.5的tokenizer,并设置了合理的最大长度(max_length=4096),因为研报较长,需要截断或分段。我们选择保留开头和结尾(因为结论通常在末尾),中间部分用[省略]标记占位。

代码语言:javascript
复制
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B", trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token

def tokenize_fn(example):
    # 拼接 instruction + input
    text = example['instruction'] + "\n" + example['input'] + "\n" + example['output']
    # 注意:训练时只计算 output 部分的 loss,需设置 labels
    # 这里仅做演示,实际使用 tokenizer 的 padding 和 truncation
    tokenized = tokenizer(text, max_length=4096, truncation=True, padding="max_length")
    return tokenized

在实际训练中,我们使用 transformersDataCollatorForSeq2Seq 来处理动态padding和label遮蔽。


第二阶段:基座模型选型与训练环境
2.1 为什么选择 Qwen2.5-7B

我们对比了 Llama 3-8B、Mistral-7B、Qwen2.5-7B 和 ChatGLM3-6B,在中文金融文本上的零样本摘要质量上,Qwen2.5-7B表现最佳(ROUGE-L 0.38 vs Llama 0.34)。同时它原生支持中文,且上下文窗口达32K,能覆盖大部分研报。

2.2 训练资源配置
  • 硬件:单台 A100-80G(训练用) + V100-32G(调试用)
  • 框架:transformers + peft + bitsandbytes + accelerate
  • LoRA 参数:rank=16,alpha=32,dropout=0.05,只作用于 q_proj, k_proj, v_proj, o_proj

完整训练脚本(关键部分)

代码语言:javascript
复制
import torch
from transformers import AutoModelForCausalLM, TrainingArguments, Trainer
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from datasets import load_dataset

# 加载模型(4bit量化以节省显存)
model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen2.5-7B",
    torch_dtype=torch.bfloat16,
    device_map="auto",
    load_in_4bit=True,
    bnb_4bit_compute_dtype=torch.bfloat16,
)
model = prepare_model_for_kbit_training(model)

# LoRA配置
lora_config = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM"
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()  # 约 0.2% 的参数可训练

# 训练参数
training_args = TrainingArguments(
    output_dir="./qwen_finetune",
    per_device_train_batch_size=2,
    per_device_eval_batch_size=2,
    gradient_accumulation_steps=8,   # 有效batch=16
    num_train_epochs=3,
    learning_rate=2e-4,
    warmup_ratio=0.03,
    logging_steps=10,
    eval_steps=100,
    save_steps=500,
    report_to="wandb",
    fp16=False,
    bf16=True,
    optim="paged_adamw_8bit",
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_train,
    eval_dataset=tokenized_val,
    data_collator=DataCollatorForSeq2Seq(tokenizer, padding=True),
)
trainer.train()
2.3 训练中的三个“坑”与解决办法
  • 显存溢出:Qwen2.5-7B + 4bit量化 + batch_size=2 时,A100-80G显存占用约68G。我们将gradient_accumulation_steps提高到8,并开启gradient_checkpointing(添加 model.gradient_checkpointing_enable())成功降低到52G。
  • Loss不下降:前500步loss在2.3左右徘徊,发现是学习率过高(1e-4),调低至2e-4并增加warmup后正常收敛。
  • 过拟合:验证集loss在第2个epoch后上升,我们将epoch从5减到3,并增加weight_decay=0.01。

第三阶段:评估与迭代——不只是ROUGE
3.1 自动化指标

我们在验证集上计算了ROUGE-1/2/L,以及关键指标提取的精确率和召回率。

代码语言:javascript
复制
from rouge_score import rouge_scorer
import json

scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=True)

def evaluate_predictions(preds, refs):
    scores = []
    for p, r in zip(preds, refs):
        scores.append(scorer.score(r, p))
    avg = {k: sum(s[k].fmeasure for s in scores)/len(scores) for k in scores[0]}
    return avg

微调后,ROUGE-L从基座的0.38提升到0.47,关键指标提取准确率从78%跃升至94%。但我们也发现,摘要偶尔会“照抄”原文长句,导致可读性下降。因此我们引入了人工评估维度:流畅度信息冗余度,并据此对训练数据进行第二轮清洗。

3.2 人工评估与Bad Case分析

我们邀请5位研究员对200条摘要进行盲测打分(1~5分)。微调模型平均分4.2,GPT-4为4.5,但微调模型的合规性(不包含敏感预测)优于GPT-4。典型的Bad Case是模型会遗漏“风险提示”部分,我们通过在后训练中增加负样本(缺少风险提示的摘要)进行对抗性训练,问题显著改善。


第四阶段:部署与推理优化

训练完成后,我们需要将LoRA权重与基座合并,并部署为内部API服务。

4.1 合并权重

代码语言:javascript
复制
from peft import PeftModel

base_model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen2.5-7B",
    torch_dtype=torch.bfloat16,
    device_map="auto",
)
lora_model = PeftModel.from_pretrained(base_model, "./qwen_finetune/checkpoint-1500")
merged_model = lora_model.merge_and_unload()
merged_model.save_pretrained("./merged_model")
4.2 推理服务(FastAPI + vLLM)

为了提高吞吐,我们使用 vLLM 框架(支持PagedAttention和连续批处理)。

代码语言:javascript
复制
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from vllm import LLM, SamplingParams

app = FastAPI()
llm = LLM(model="./merged_model", tensor_parallel_size=1, dtype="bfloat16")

class SummarizeRequest(BaseModel):
    report_text: str

class SummarizeResponse(BaseModel):
    summary: str
    metrics: dict

sampling_params = SamplingParams(
    temperature=0.2,
    top_p=0.9,
    max_tokens=512,
    stop=["<|im_end|>"]
)

@app.post("/summarize", response_model=SummarizeResponse)
async def summarize(req: SummarizeRequest):
    prompt = f"请根据以下研报内容,生成摘要和关键指标。\n{req.report_text}\n"
    outputs = llm.generate([prompt], sampling_params)
    generated_text = outputs[0].outputs[0].text
    # 解析摘要和指标(正则提取)
    # ...
    return SummarizeResponse(summary=summary, metrics=extracted_metrics)

压测结果:单卡A100可支持并发16请求,平均延迟2.8秒,满足SLA。

4.3 持续学习与在线反馈

我们设计了一个反馈闭环:研究员在使用摘要服务时,可对结果打分(👍/👎),低分数据定期回流到训练集中,每两周触发一次增量微调。增量微调采用LoRA的“续训”模式,无需从头开始。

代码语言:javascript
复制
# 增量微调时,加载已有LoRA checkpoint,再训练
model = PeftModel.from_pretrained(base_model, "./lora_checkpoint_v1")
model.train()  # 继续用新数据训练

成果与数据对比

指标

微调前(Qwen2.5基座+提示词)

微调后(LoRA)

GPT-4 API

ROUGE-L

0.38

0.47

0.51

关键指标准确率

78%

94%

95%

合规性(敏感词触发率)

5.2%

1.1%

2.3%

单次推理成本(元)

0.12(云端GPU)

0.008(自建)

0.35

月总成本(万)

6.2

0.9(含运维)

15+

微调后模型在摘要质量上接近GPT-4,但成本仅为GPT-4的2.3%,同时所有数据留在内网,满足合规要求。


可复用的方法论与避坑指南
5.1 数据永远是第一优先级
  • 至少准备5000条高质量样本,多样性比数量重要
  • 每个样本的output必须严格统一格式,否则训练会震荡
  • 建议用10%的数据做人工校验,建立“黄金标准集”
5.2 超参数调优经验
  • LoRA rank:16~32在7B模型上效果最佳,过大容易过拟合
  • 学习率:1e-4 ~ 3e-4,配合warmup_ratio=0.03
  • 训练epoch:2~3轮即可,超过5轮会过拟合
  • 使用bf16混合精度(A100支持)比fp16更稳定
5.3 避免中文“胡说八道”技巧
  • 在训练数据中显式加入“如果原文未提及,则输出’无相关信息‘”的示例
  • 推理时设置temperature=0.1~0.2,降低随机性
  • 后处理阶段加入正则表达式校验(如目标价必须带“元”)

未来演进方向
  • 多模态微调:研报中的图表(K线图、财务表格)用多模态模型处理
  • RLHF对齐:收集研究员偏好数据,用DPO方法进一步优化摘要风格
  • 小模型蒸馏:将7B模型蒸馏到3B,部署在边缘节点降低延迟

结语:微调不是终点,而是持续进化的起点

大模型微调看似高深,实则遵循“数据-训练-评估-部署”的标准工程流程。它的本质是将通用智能转化为企业专属智能。本项目从立项到上线历时6周,团队仅3人,证明了即使资源有限的团队也能高效完成微调落地。

给读者的三点建议

  1. 不要盲目追求大模型:7B~13B在多数垂直场景已足够,成本可控
  2. 重视评估体系:自动指标 + 人工盲测缺一不可
  3. 建立反馈机制:持续收集线上数据,定期迭代模型

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 第一阶段:数据工程——决定微调成败的基石
    • 1.1 数据格式设计
    • 1.2 数据增强与去重
    • 1.3 Tokenization 关键配置
  • 第二阶段:基座模型选型与训练环境
    • 2.1 为什么选择 Qwen2.5-7B
    • 2.2 训练资源配置
    • 2.3 训练中的三个“坑”与解决办法
  • 第三阶段:评估与迭代——不只是ROUGE
    • 3.1 自动化指标
    • 3.2 人工评估与Bad Case分析
  • 第四阶段:部署与推理优化
    • 4.1 合并权重
    • 4.2 推理服务(FastAPI + vLLM)
    • 4.3 持续学习与在线反馈
  • 成果与数据对比
  • 可复用的方法论与避坑指南
    • 5.1 数据永远是第一优先级
    • 5.2 超参数调优经验
    • 5.3 避免中文“胡说八道”技巧
  • 未来演进方向
  • 结语:微调不是终点,而是持续进化的起点
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档