
在微调中,数据质量远比数量重要。我们的原始数据来自公司内部的研报库和人工撰写的摘要(已脱敏)。经过清洗,最终保留1.2万条高质量样本。
我们采用 Alpaca-style 的指令微调格式,每条数据包含三个字段:
{
"instruction": "请根据以下研报内容,生成一段不超过300字的摘要,并提取目标价、评级和EPS预测。",
"input": "研报全文(约8000字)...",
"output": "摘要文本... \n【关键指标】目标价:15.8元;评级:买入;EPS预测:2025年1.24元,2026年1.52元。"
}为了提高训练稳定性,我们将instruction固定,只在input和output上变化。同时,我们要求摘要严格遵循三段式结构:行业背景 → 公司核心逻辑 → 风险提示,在输出中对格式做了明确约束。
由于原始摘要风格不统一,我们做了一轮人工复核,并利用大模型(GPT-4)对20%的低质量样本进行重写,然后人工验收。同时,使用datasets库进行去重和长度过滤:
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 条我们使用Qwen2.5的tokenizer,并设置了合理的最大长度(max_length=4096),因为研报较长,需要截断或分段。我们选择保留开头和结尾(因为结论通常在末尾),中间部分用[省略]标记占位。
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在实际训练中,我们使用 transformers 的 DataCollatorForSeq2Seq 来处理动态padding和label遮蔽。
我们对比了 Llama 3-8B、Mistral-7B、Qwen2.5-7B 和 ChatGLM3-6B,在中文金融文本上的零样本摘要质量上,Qwen2.5-7B表现最佳(ROUGE-L 0.38 vs Llama 0.34)。同时它原生支持中文,且上下文窗口达32K,能覆盖大部分研报。
transformers + peft + bitsandbytes + accelerateq_proj, k_proj, v_proj, o_proj完整训练脚本(关键部分):
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()gradient_accumulation_steps提高到8,并开启gradient_checkpointing(添加 model.gradient_checkpointing_enable())成功降低到52G。我们在验证集上计算了ROUGE-1/2/L,以及关键指标提取的精确率和召回率。
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%。但我们也发现,摘要偶尔会“照抄”原文长句,导致可读性下降。因此我们引入了人工评估维度:流畅度和信息冗余度,并据此对训练数据进行第二轮清洗。
我们邀请5位研究员对200条摘要进行盲测打分(1~5分)。微调模型平均分4.2,GPT-4为4.5,但微调模型的合规性(不包含敏感预测)优于GPT-4。典型的Bad Case是模型会遗漏“风险提示”部分,我们通过在后训练中增加负样本(缺少风险提示的摘要)进行对抗性训练,问题显著改善。
训练完成后,我们需要将LoRA权重与基座合并,并部署为内部API服务。
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")为了提高吞吐,我们使用 vLLM 框架(支持PagedAttention和连续批处理)。
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。
我们设计了一个反馈闭环:研究员在使用摘要服务时,可对结果打分(👍/👎),低分数据定期回流到训练集中,每两周触发一次增量微调。增量微调采用LoRA的“续训”模式,无需从头开始。
# 增量微调时,加载已有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%,同时所有数据留在内网,满足合规要求。
output必须严格统一格式,否则训练会震荡temperature=0.1~0.2,降低随机性大模型微调看似高深,实则遵循“数据-训练-评估-部署”的标准工程流程。它的本质是将通用智能转化为企业专属智能。本项目从立项到上线历时6周,团队仅3人,证明了即使资源有限的团队也能高效完成微调落地。
给读者的三点建议:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。