第2011篇:LoRA微调入门——用少量数据让开源模型专注特定领域
第2011篇:LoRA微调入门——用少量数据让开源模型专注特定领域
适读人群:想把开源LLM做领域定制的工程师 | 阅读时长:约21分钟 | 核心价值:理解LoRA微调的核心原理,掌握从数据准备到模型部署的完整工程流程
我们有个法律AI项目,一开始直接用通用LLM回答法律问题。效果还行,但有一个持续出现的问题:LLM对很多专业术语的用法不够准确,经常把"违约金"和"赔偿金"混用,把"原告"的诉求理解成了"被告"的立场。
换更强的模型能改善,但成本翻了三倍。
微调是更合理的解法。但全量微调一个7B模型,需要多张A100,光是显卡租金就是不小的开支,而且全量微调很容易"遗忘"原来的通用能力(灾难性遗忘)。
LoRA(Low-Rank Adaptation)是个很精妙的折中:只微调模型的一小部分参数,成本低几十倍,但效果能达到全量微调的80%以上。
LoRA的核心原理(工程师视角)
LoRA的思想是:
大模型的权重矩阵 W 在微调时的变化量 ΔW,可以用两个低秩矩阵 A 和 B 来近似:ΔW ≈ B × A
原来全量微调要更新W中的所有参数。如果W是一个4096×4096的矩阵,那就有1677万个参数。
LoRA把ΔW分解成 B(4096×r)× A(r×4096),其中r是秩(通常取8或16)。这样需要训练的参数数量从1677万降到了 4096×r×2 = 65536(当r=8时),减少了256倍。
推理时,LoRA的权重可以合并进原始权重:W' = W + B×A,不增加推理延迟。
数据准备:最关键也最容易被忽视的环节
好的微调数据胜过多10倍的普通数据。对于法律AI,我们构建了以下类型的训练数据:
格式:指令-输入-输出三元组
{
"instruction": "分析以下合同条款,识别其中的违约责任约定",
"input": "甲方如未能按时交付货物,应向乙方支付合同总额5%的违约金;如延迟超过30天,乙方有权解除合同并要求全额退款及同等金额的赔偿。",
"output": "违约责任分析:\n1. 延迟交货违约金:合同总额的5%(轻度违约)\n2. 严重违约处理:延迟超30天时,违约方(甲方)面临:\n - 合同解除风险\n - 全额退款义务\n - 等额赔偿(即合同总额的100%赔偿)\n3. 关键法律要点:本条款区分了一般违约(违约金)和根本违约(解除+赔偿),符合《民法典》第577条-580条的规定。"
}构建高质量数据集的方法:
# 用GPT-4生成初始数据,人工专家审核修正
# 最终我们用了1200条人工审核的数据
import json
def generate_training_data_with_gpt4(legal_text, openai_client):
"""
给定一段法律文本,生成多种角度的训练样本
"""
prompts = [
f"从律师视角分析以下合同条款的风险:\n{legal_text}",
f"以下合同条款是否存在歧义?请逐条分析:\n{legal_text}",
f"以下合同条款对甲方还是乙方更有利?原因是什么:\n{legal_text}",
]
samples = []
for prompt in prompts:
response = openai_client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "你是一位资深律师,专注于合同审查。"},
{"role": "user", "content": prompt}
]
)
samples.append({
"instruction": prompt,
"input": "",
"output": response.choices[0].message.content
})
return samples使用LLaMA-Factory进行LoRA微调
LLaMA-Factory是目前最成熟的开源微调框架,支持LoRA、QLoRA等多种方法:
# train_config.yaml
model_name_or_path: Qwen/Qwen2-7B-Instruct
finetuning_type: lora
# LoRA参数配置
lora_target: all # 对所有线性层应用LoRA
lora_rank: 16 # 秩,越大效果越好但参数越多
lora_alpha: 32 # LoRA的缩放因子,通常是rank的2倍
lora_dropout: 0.05 # dropout防止过拟合
# 训练数据
dataset: legal_qa_v1 # 数据集名称(放在data/目录下)
template: qwen # 使用Qwen模型的对话模板
# 训练超参数
per_device_train_batch_size: 4
gradient_accumulation_steps: 4 # 等效batch_size = 4*4 = 16
learning_rate: 5.0e-5
num_train_epochs: 3
warmup_ratio: 0.1
# 优化
bf16: true # 使用bfloat16节省显存
gradient_checkpointing: true # 梯度检查点,用时间换显存
# 输出
output_dir: ./output/legal_lora_v1
logging_steps: 50
save_steps: 500执行训练:
# 单机单卡训练(需要约18GB显存)
llamafactory-cli train train_config.yaml
# 如果显存不够,使用QLoRA(4bit量化)
# 在train_config.yaml中添加:
# quantization_bit: 4
# 显存需求降至约8GB模型合并与部署
训练完成后,将LoRA权重合并进基础模型:
# merge_lora.py
from llamafactory.model import load_model, load_tokenizer
from llamafactory.train.tuner import load_sft_peft_model
# 加载基础模型
tokenizer = load_tokenizer("Qwen/Qwen2-7B-Instruct")
model = load_model("Qwen/Qwen2-7B-Instruct")
# 加载并合并LoRA
model = load_sft_peft_model(
model,
"./output/legal_lora_v1",
is_trainable=False # 推理模式
)
# 合并权重(合并后不再有额外推理延迟)
model = model.merge_and_unload()
# 保存合并后的完整模型
model.save_pretrained("./legal_model_v1")
tokenizer.save_pretrained("./legal_model_v1")评估微调效果
用测试集评估,和基础模型对比:
@Service
public class ModelEvaluationService {
public EvaluationReport compareModels(
String baseModelUrl,
String fineTunedModelUrl,
List<EvaluationCase> testCases) {
List<CaseResult> results = testCases.stream().map(testCase -> {
String baseAnswer = callModel(baseModelUrl, testCase.getInput());
String fineTunedAnswer = callModel(fineTunedModelUrl, testCase.getInput());
double baseScore = scoreAnswer(testCase.getExpectedOutput(), baseAnswer);
double fineTunedScore = scoreAnswer(testCase.getExpectedOutput(), fineTunedAnswer);
return CaseResult.builder()
.input(testCase.getInput())
.expectedOutput(testCase.getExpectedOutput())
.baseAnswer(baseAnswer)
.fineTunedAnswer(fineTunedAnswer)
.baseScore(baseScore)
.fineTunedScore(fineTunedScore)
.improved(fineTunedScore > baseScore)
.build();
}).collect(Collectors.toList());
double baseAvgScore = results.stream().mapToDouble(CaseResult::getBaseScore).average().orElse(0);
double fineTunedAvgScore = results.stream().mapToDouble(CaseResult::getFineTunedScore).average().orElse(0);
return EvaluationReport.builder()
.totalCases(testCases.size())
.baseModelScore(baseAvgScore)
.fineTunedModelScore(fineTunedAvgScore)
.improvementRate((fineTunedAvgScore - baseAvgScore) / baseAvgScore)
.results(results)
.build();
}
}我们法律AI项目的微调结果:术语准确率从71%提升到了93%,合同风险识别的F1分数从0.68提升到了0.87。用了1200条训练样本,训练时间2小时,成本约80元(租用A100)。
