微调灾情分析报告生成模型
考虑到数据量(84篇报告)对于14B模型来说非常有限,直接进行传统微调会面临严重的过拟合问题。我们将采用 QLoRA (Quantized LoRA) 技术,它是一种高效的参数微调方法,允许在量化后的模型上进行 LoRA 微调,极大地降低了计算资源需求,并减轻了过拟合风险。同时,我们将构建代码,展示如何加载模型、准备数据(模拟真实场景下的数据格式)、进行微调、保存模型以及进行基本的测试和推理。
重要提示:
- 数据是核心: 提供的代码是技术框架,但模型的最终效果强烈依赖于您实际准备的训练数据的质量、数量和多样性。84篇报告的数据量是主要瓶颈,模型很可能出现泛化能力不足和“幻觉”问题。人工审核和RAG是必要的补充手段。
- 计算资源: 尽管使用 QLoRA,训练 14B 模型仍需要至少一块显存较大的高性能 GPU(推荐 NVIDIA A100 40GB/80GB 或 A6000 48GB)。
- 数据准备复杂性: 代码中的数据加载部分将使用一个模拟的、简化格式的数据集。实际的数据准备(从PDF/Word提取文本、结构化、构建Prompt-Response对)是整个项目中最耗时、最复杂的部分,需要大量人工或半自动化的工作,这部分复杂性无法直接包含在核心微调代码中。 手册中会详细描述这部分的思路。
- 评估限制: 自动化评估指标(如 ROUGE, BLEU)对生成报告的准确性、结构、逻辑等评估能力有限。人工评估和 RAG 辅助校验是必不可少的。
灾情分析报告生成模型微调技术开发手册
项目目标: 基于现有灾情报告,微调 14B 模型,使其能够生成部门专用的灾情分析报告。
核心技术: QLoRA 微调 + Hugging Face 生态系统。
1. 前期准备
- 硬件要求: 至少一块高性能 GPU,推荐显存 ≥ 40GB。
- 软件要求:
- Python 3.8+
- PyTorch
- CUDA toolkit (与 PyTorch 版本兼容)
- git
- 数据: 整理好的 84 篇灾情分析报告及 24 篇综合报告。
2. 数据准备 (手动/半自动化阶段)
这是项目中最关键也是最耗时的部分。您的目标是将原始报告转换为 Prompt-Response 对,供模型训练使用。
-
步骤:
- 文本提取: 从 PDF、Word 等格式报告中提取纯文本。
- 工具:
PyMuPDF
,python-docx
,pdfminer.six
。对于扫描件需要 OCR (PaddleOCR, Tesseract)。
- 工具:
- 结构化与信息抽取: 识别报告的各个部分(标题、时间、地点、事件概述、影响分析、应对措施、建议等)。提取关键信息。
- 方法:正则表达式、关键词匹配、基于规则的解析。高级方法可使用 LayoutLM 等模型辅助。
- 构建 Prompt-Response 对: 这是训练数据的核心。需要定义模型的输入 (Prompt) 和期望的输出 (Response)。
- 策略示例 (推荐组合使用):
- 完整报告生成:
- Prompt: “请根据以下灾情概述生成一份详细的[地区名称]灾情分析报告:\n[灾情概述核心信息,例如:\n时间:XXXX年XX月XX日\n地点:[详细地点]\n事件类型:[类型]\n初步影响:[简要描述]]”
- Response: [对应报告的完整文本]
- 章节生成:
- Prompt: “请根据以下信息,撰写一份[地区名称]灾情分析报告的’灾害影响分析’章节:\n[灾害影响相关的详细信息,如:\n人员伤亡:XX人死亡,XX人受伤\n经济损失:直接经济损失XX万元\n基础设施破坏:[详细描述]]”
- Response: [对应报告中’灾害影响分析’章节的文本]
- 特定信息填充:
- Prompt: “请填写以下灾情分析报告模板的关键信息:\n报告标题:\n灾害类型:\n发生时间:\n发生地点:\n受灾人数:\n死亡人数:\n直接经济损失:”
- Response: [填写好的关键信息列表或句子]
- 完整报告生成:
- 格式: 建议使用 JSONL (JSON Lines) 格式,每行一个训练样本,例如:
{"prompt": "...", "response": "..."}
或{"text": "### Instruction:\\n请根据以下信息生成报告...\\n### Input:\\n[灾情信息]\\n### Response:\\n[报告内容]"}
(Alpaca/ShareGPT 风格)。后一种格式有利于模型区分指令、输入和输出。
- 策略示例 (推荐组合使用):
- 数据清洗与标准化: 清理提取文本中的错误、噪声;统一命名实体(地名、单位名);核对关键数据(如果可能);删除重复或低质量样本。
- 数据集划分: 将准备好的 Prompt-Response 对数据集划分为训练集、验证集和测试集(例如 80%-10%-10%)。
- 文本提取: 从 PDF、Word 等格式报告中提取纯文本。
-
输出格式示例 (JSONL):
{"text": "### Instruction:\n请根据以下灾情概述生成一份详细的本地灾情分析报告:\n### Input:\n时间:2023年8月15日\n地点:我市XX区XX镇\n事件类型:洪涝\n初步影响:大量农田被淹,部分房屋受损,交通中断。\n### Response:\n## XX市2023年8月15日洪涝灾情分析报告\n\n**摘要**\n2023年8月15日,我市XX区XX镇突发洪涝灾害,主要原因是...\n\n**一、灾情背景**\n...")} {"text": "### Instruction:\n请根据以下信息,撰写一份本地灾情分析报告的'灾害影响分析'章节:\n### Input:\n时间:2022年7月\n地点:我市山区\n事件类型:森林火灾\n核心数据:过火面积约100公顷,无人员伤亡,直接经济损失估算XX万元。\n### Response:\n## 灾害影响分析\n\n本次森林火灾对我市山区造成了显著影响。主要体现在以下几个方面:\n\n1. **生态环境影响:** 过火面积约100公顷,部分林地生态系统遭到破坏...\n2. **经济损失:** 初步估算直接经济损失达XX万元,主要包括林业资源损失...\n3. **社会影响:** 未造成人员伤亡,但对周边居民生活造成短期影响...\n"} ...
3. 环境搭建
-
克隆代码仓库 (可选,但推荐): 使用 Hugging Face
transformers
和peft
库进行 QLoRA 微调通常不需要克隆特定仓库,直接通过 pip 安装即可。但参考他人实现的 QLoRA 训练脚本可能有用。这里我们基于库函数直接构建。 -
安装依赖: 创建 Python 虚拟环境(推荐),然后安装所需库。
# 创建并激活虚拟环境 python -m venv venv_disaster_report source venv_disaster_report/bin/activate # Linux/macOS # venv_disaster_report\Scripts\activate # Windows# 安装 PyTorch (选择适合你CUDA版本的命令,参考 PyTorch 官网) # Example for CUDA 11.8 pip install torch==2.1.0 torchvision==0.16.0 torchaudio==2.1.0 --index-url https://download.pytorch.org/whl/cu118# 安装其他依赖 pip install transformers datasets peft accelerate bitsandbytes trl evaluate rouge_score nltk # trl 库用于更方便的SFT,evaluate用于评估 pip install -U accelerate # 确保 accelerate 是最新版本
-
配置 Accelerate: 运行
accelerate config
命令,根据你的硬件配置(单卡、多卡)进行设置。对于单卡 QLoRA,通常选择no
分布式训练,选择你使用的显卡 ID。
4. 微调代码实现 (使用 QLoRA 和 Hugging Face Trainer)
我们将创建几个 Python 文件:
config.py
: 存储模型路径、数据路径、训练参数等配置。dataset_prep.py
: 模拟数据加载和预处理(实际中需要替换为您的数据处理逻辑)。train_lora.py
: 主训练脚本。inference.py
: 加载微调模型并进行推理。evaluate.py
: 对模型进行评估(自动化指标和示例生成)。requirements.txt
: 列出依赖库。
requirements.txt
torch>=2.1.0
transformers
datasets
peft
accelerate
bitsandbytes
trl>=0.7.0 # Use a recent version of trl
evaluate
rouge_score
nltk
(根据实际安装的 PyTorch 版本调整第一行)
config.py
import os# 模型配置
# 您需要指定一个开源的14B模型,例如 Llama-2-13b-chat-hf 或类似的模型ID
# 注意:如果使用 Llama 系列模型,可能需要 Hugging Face 账号并同意其许可协议
# 如果无法直接访问,可以尝试使用国内镜像或下载模型权重到本地
BASE_MODEL_ID = "meta-llama/Llama-2-13b-chat-hf" # 示例,请替换为您能访问的模型ID
# BASE_MODEL_ID = "/path/to/your/downloaded/llama2-13b" # 如果下载到本地# 数据路径配置
DATA_PATH = "./disaster_reports_data.jsonl" # 模拟的训练数据文件路径# 微调输出路径
OUTPUT_DIR = "./lora_adapters"# QLoRA 配置
LORA_R = 64 # LoRA 的秩,可以尝试 8, 16, 32, 64,越大表达能力越强,但也更容易过拟合
LORA_ALPHA = 128 # LoRA 的缩放因子,通常是 r 的两倍
LORA_DROPOUT = 0.05 # LoRA 层的 Dropout 比率
# 需要应用 LoRA 的模型层,通常是 Attention 层的 q_proj, k_proj, v_proj, o_proj
# 对于 Llama 模型,这些层的名称通常是 'q_proj', 'k_proj', 'v_proj', 'o_proj'
# 需要根据您实际使用的模型结构确认这些层名
TARGET_MODULES = ["q_proj", "k_proj", "v_proj", "o_proj"] # 示例,请查阅模型文档确认# 训练参数配置
MICRO_BATCH_SIZE = 4 # 每个设备的微批量大小,受限于显存,通常设小
GRADIENT_ACCUMULATION_STEPS = 8 # 梯度累积步数,模拟更大的批量大小:MICRO_BATCH_SIZE * GRADIENT_ACCUMULATION_STEPS
BATCH_SIZE = MICRO_BATCH_SIZE * GRADIENT_ACCUMULATION_STEPS # 模拟的实际批量大小
NUM_EPOCHS = 5 # 训练轮数,数据量小,不宜过多
LEARNING_RATE = 3e-4 # 学习率
SAVE_STEPS = 100 # 每隔多少步保存一次检查点
LOG_STEPS = 20 # 每隔多少步记录一次日志
EVAL_STEPS = 100 # 每隔多少步在验证集上评估一次
MAX_SEQ_LENGTH = 1024 # 输入序列的最大长度,根据您的数据平均长度调整
# 量化配置
LOAD_IN_4BIT = True
BITSANDBYTES_CONFIG = {"load_in_4bit": LOAD_IN_4BIT,"bnb_4bit_quant_type": "nf4", # nf4 或 fp4"bnb_4bit_use_double_quant": True,"bnb_4bit_compute_dtype": "torch.bfloat16", # 如果硬件支持,bfloat16 通常更好
}
# 训练使用的 dtype
# 根据硬件支持选择 torch.float16 (fp16) 或 torch.bfloat16 (bf16)
# A100/A6000 支持 bf16,消费级卡通常只支持 fp16
TRAIN_DTYPE = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16# Prompt 模板 (与数据准备中的格式一致)
PROMPT_TEMPLATE = "### Instruction:\n{}\n### Input:\n{}\n### Response:\n{}"
# 对于只包含 'text' 键的数据,直接使用
# PROMPT_TEMPLATE = "{}"# 评估配置
MAX_NEW_TOKENS = 1024 # 生成报告的最大长度
NUM_BEAMS = 1 # Beam Search 的束数,1表示贪婪搜索,增加可以提高生成质量但更慢
DO_SAMPLE = False # 是否使用采样,True 生成更具创意,False 生成更确定
dataset_prep.py
(这个脚本是用来生成一个模拟的 disaster_reports_data.jsonl
文件,以便 train_lora.py
可以运行。您需要根据您的实际数据格式和内容来替换 load_and_process_data
函数)
import json
import os
from datasets import DatasetDict, Dataset
from sklearn.model_selection import train_test_split
from config import DATA_PATH, PROMPT_TEMPLATEdef create_mock_dataset(num_samples=100):"""创建一个模拟的灾情报告数据集(Prompt-Response 对)。在实际应用中,您需要替换此函数来加载和处理您的真实报告数据。"""mock_data = []for i in range(num_samples):instruction = f"请根据以下灾情概述生成一份详细的本地灾情分析报告,重点分析影响和建议。"input_text = f"时间:202{i%3}年{i%12+1}月{i%28+1}日\n地点:我市XX区YY镇\n事件类型:{'洪涝' if i%2==0 else '干旱'}\n初步影响:样本报告 {i} 的简要描述。"response = f"## 本地灾情分析报告 ({i})\n\n**摘要**\n本报告详细分析了202{i%3}年{i%12+1}月{i%28+1}日发生在我市XX区YY镇的{'洪涝' if i%2==0 else '干旱'}灾害。\n\n**一、灾情背景**\n...\n\n**二、灾害影响分析**\n...\n\n**三、应急响应情况**\n...\n\n**四、面临的挑战与建议**\n...\n"# 使用Prompt模板格式化数据text = PROMPT_TEMPLATE.format(instruction, input_text, response)mock_data.append({"text": text}) # 注意:使用 'text' 作为键,方便后续加载# 将模拟数据保存为 JSONL 文件with open(DATA_PATH, "w", encoding="utf-8") as f:for entry in mock_data:json.dump(entry, f, ensure_ascii=False)f.write("\n")print(f"模拟数据集已生成到 {DATA_PATH},共 {num_samples} 条样本。")def load_and_process_data():"""加载并处理数据集。在实际应用中,您需要修改此函数来读取您的 JSONL 文件,并可能进行额外的处理,例如 tokenization 或格式检查。"""if not os.path.exists(DATA_PATH):print(f"错误:未找到数据文件 {DATA_PATH}。请先运行 create_mock_dataset 或准备您的真实数据文件。")# 为演示目的,如果文件不存在,先创建模拟数据create_mock_dataset(num_samples=84) # 模拟您的84篇报告数量# 使用 Hugging Face datasets 库加载 JSONL 文件# 注意 'text' 键是我们模拟数据中使用的格式dataset = Dataset.from_json(DATA_PATH)# 将数据集划分为训练集和测试集# 数据量非常小,划分比例要谨慎。这里简单按比例划分# 实际应用中,如果数据极少,可能需要更复杂的交叉验证或不用单独验证集# 或者人工保留少量测试样本train_test = dataset.train_test_split(test_size=0.1, seed=42) # 10% 用于测试# 如果需要验证集# train_val_test = dataset.train_test_split(test_size=0.2, seed=42)# train_test = train_val_test['train'].train_test_split(test_size=0.5, seed=42) # 10% test, 10% validation# dataset_dict = DatasetDict({# 'train': train_test['train'],# 'validation': train_test['test'], # 将这个小的测试集作为验证集# 'test': Dataset.from_list([]) # 测试集单独准备或人工评估# })dataset_dict = DatasetDict({'train': train_test['train'],'test': train_test['test']})print("数据集加载并划分完成:")print(dataset_dict)return dataset_dict# 如果直接运行此脚本,则生成模拟数据
if __name__ == "__main__":create_mock_dataset(num_samples=84) # 生成84条模拟数据# load_and_process_data() # 可以测试加载功能
train_lora.py
import os
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from accelerate import Accelerator
from datasets import load_dataset
from trl import SFTTrainer # 使用 trl 的 SFTTrainer 更方便处理 Prompt 格式
from config import * # 导入配置def training_function():# 初始化 Accelerateaccelerator = Accelerator()# 加载数据 (使用 dataset_prep.py 中的函数)# 在实际中,请确保 DATA_PATH 指向您准备好的 JSONL 文件# 并且 dataset_prep.load_and_process_data 能够正确加载您的数据print(f"正在加载数据集来自 {DATA_PATH}...")# dataset = load_dataset("json", data_files=DATA_PATH) # 如果没有 train/test split 在文件中# dataset_dict = dataset['train'].train_test_split(test_size=0.1) # 如果是单个文件,这里划分# dataset_dict = dataset_dict.rename_column("text", "input") # 如果您的 Prompt-Response 在一个字段# 假设 load_and_process_data 返回一个 DatasetDict 包含 'train' 和 'test'from dataset_prep import load_and_process_datadataset_dict = load_and_process_data()train_dataset = dataset_dict['train']# eval_dataset = dataset_dict['validation'] if 'validation' in dataset_dict else None # 如果有验证集eval_dataset = dataset_dict['test'] if 'test' in dataset_dict else None # 这里使用测试集作为验证集print("数据集加载完成。")print("训练集大小:", len(train_dataset))if eval_dataset:print("验证集大小:", len(eval_dataset))# 加载基础模型和 Tokenizerprint(f"正在加载基础模型: {BASE_MODEL_ID}")model = AutoModelForCausalLM.from_pretrained(BASE_MODEL_ID,load_in_4bit=LOAD_IN_4BIT,quantization_config=torch.quantization.QConfig(activation=torch.quantization.HistogramObserver.with_args(dtype=torch.quint8),weight=torch.quantization.MinMaxObserver.with_args(dtype=torch.qint8, qscheme=torch.per_tensor_symmetric)), # 示例量化配置,实际bitsandbytes会处理device_map={"": accelerator.process_index}, # 自动分配设备torch_dtype=TRAIN_DTYPE # 使用混合精度)model = prepare_model_for_kbit_training(model) # 准备进行 k-bit 训练tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_ID, trust_remote_code=True)tokenizer.pad_token = tokenizer.eos_token # 设置 padding token 为 eos tokenprint("模型和 Tokenizer 加载完成。")# 配置 LoRAlora_config = LoraConfig(r=LORA_R,lora_alpha=LORA_ALPHA,target_modules=TARGET_MODULES,lora_dropout=LORA_DROPOUT,bias="none",task_type="CAUSAL_LM", # 这是一个因果语言模型任务)# 在模型上应用 LoRAmodel = get_peft_model(model, lora_config)model.print_trainable_parameters() # 打印可训练的参数量# 配置训练参数training_arguments = TrainingArguments(output_dir=OUTPUT_DIR,num_train_epochs=NUM_EPOCHS,per_device_train_batch_size=MICRO_BATCH_SIZE,gradient_accumulation_steps=GRADIENT_ACCUMULATION_STEPS,learning_rate=LEARNING_RATE,logging_steps=LOG_STEPS,save_steps=SAVE_STEPS,evaluation_strategy="steps" if eval_dataset else "no", # 如果有验证集,按步数评估eval_steps=EVAL_STEPS if eval_dataset else None,save_total_limit=3, # 最多保存3个检查点dataloader_num_workers=4, # 数据加载工作进程数fp16=(TRAIN_DTYPE == torch.float16),bf16=(TRAIN_DTYPE == torch.bfloat16),group_by_length=False, # 数据量小,不需要按长度分组report_to="none", # 可以设置为 "tensorboard", "wandb" 等run_name="disaster-report-finetune", # wandb 等报告名称optim="paged_adamw_8bit", # QLoRA 推荐的优化器# gradient_checkpointing=True, # 显存不足时开启,会减慢速度# disable_tqdm=True, # 命令行下禁用进度条remove_unused_columns=False, # SFTTrainer 需要保留未使用的列)# 使用 SFTTrainer 进行训练# SFTTrainer 封装了数据处理(如 tokenization, formatting using prompt template)# 需要指定 text_field,它对应于您的数据集中包含完整 prompt-response 文本的列名trainer = SFTTrainer(model=model,train_dataset=train_dataset,eval_dataset=eval_dataset,peft_config=lora_config,dataset_text_field="text", # 数据集中包含文本的列名max_seq_length=MAX_SEQ_LENGTH,tokenizer=tokenizer,args=training_arguments,packing=False, # 数据量小,不进行 packing)# 开始训练print("开始训练...")trainer.train()# 保存最终的 LoRA adapter 权重trainer.save_model(OUTPUT_DIR)print(f"训练完成。LoRA adapter 权重已保存到 {OUTPUT_DIR}")if __name__ == "__main__":# 先确保模拟数据存在,实际中跳过这步,直接使用您的数据from dataset_prep import create_mock_datasetif not os.path.exists(DATA_PATH):create_mock_dataset(num_samples=84) # 创建84条模拟数据用于演示training_function()
inference.py
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
import sys
from config import BASE_MODEL_ID, OUTPUT_DIR, PROMPT_TEMPLATE, MAX_NEW_TOKENS, NUM_BEAMS, DO_SAMPLE, LOAD_IN_4BIT, TRAIN_DTYPE
from accelerate import Acceleratordef generate_report(prompt_instruction, prompt_input):"""加载微调后的模型并根据输入生成报告。"""accelerator = Accelerator()device = accelerator.device# 加载基础模型 (使用与训练时相同的量化配置)print(f"正在加载基础模型: {BASE_MODEL_ID}")model = AutoModelForCausalLM.from_pretrained(BASE_MODEL_ID,load_in_4bit=LOAD_IN_4BIT,device_map={"": device}, # 自动分配设备torch_dtype=TRAIN_DTYPE # 使用训练时的 dtype)print("基础模型加载完成。")# 加载 LoRA adapter 权重print(f"正在加载 LoRA adapters 来自: {OUTPUT_DIR}")model = PeftModel.from_pretrained(model, OUTPUT_DIR)print("LoRA adapters 加载完成。")# 可以选择将 LoRA 权重合并到基础模型中,以提高推理速度(但会增加显存)# model = model.merge_and_unload()# 加载 Tokenizertokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_ID, trust_remote_code=True)tokenizer.pad_token = tokenizer.eos_tokenmodel.eval() # 设置模型为评估模式# 格式化 Promptfull_prompt = PROMPT_TEMPLATE.format(prompt_instruction, prompt_input, "") # 训练时 response 部分是空的给模型生成inputs = tokenizer(full_prompt, return_tensors="pt").to(device)print("\n--- 生成报告 ---")print("Prompt:")print(full_prompt)with torch.no_grad(): # 推理时不需要计算梯度outputs = model.generate(inputs=inputs.input_ids,attention_mask=inputs.attention_mask,max_new_tokens=MAX_NEW_TOKENS,num_beams=NUM_BEAMS,do_sample=DO_SAMPLE,temperature=0.7 if DO_SAMPLE else 1.0, # 采样温度,仅在 do_sample=True 时有效top_k=50 if DO_SAMPLE else None, # Top-k 采样,仅在 do_sample=True 时有效top_p=0.95 if DO_SAMPLE else None, # Top-p 采样,仅在 do_sample=True 时有效pad_token_id=tokenizer.eos_token_id, # 使用 eos_token_id 作为 pad_token_id)# 解码生成的 tokens# 注意:生成的文本包含 Prompt 本身,需要截取generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)# 查找 Response 部分的开始标记并截取response_start_tag = "### Response:\n"response_start_index = generated_text.find(response_start_tag)if response_start_index != -1:generated_report_content = generated_text[response_start_index + len(response_start_tag):].strip()else:# 如果没有找到 Response 标记,可能是模型生成格式有问题,返回全部生成内容(去除Prompt)# 查找 Prompt 结束的位置,这取决于您的 Prompt 模板prompt_end_marker = "### Response:\n" # 假设 Prompt 模板以这个结束prompt_end_index = generated_text.find(prompt_end_marker)if prompt_end_index != -1:generated_report_content = generated_text[prompt_end_index + len(prompt_end_marker):].strip()else:# Fallback: 简单去除原始 prompt 部分,这可能不准确# 需要更鲁棒的方法或依赖模型生成正确的格式generated_report_content = generated_text.replace(full_prompt, "").strip()print("\n--- 生成结果 ---")print(generated_report_content)if __name__ == "__main__":# 示例 Promptinstruction = "请根据以下灾情概述生成一份详细的本地灾情分析报告,重点分析人员伤亡和经济损失。"input_text = "时间:2024年5月1日\n地点:我市中心城区\n事件类型:地震\n初步影响:部分建筑倒塌,造成人员伤亡,交通拥堵。"# 可以从命令行参数获取 Promptif len(sys.argv) > 1:instruction = sys.argv[1]if len(sys.argv) > 2:input_text = sys.argv[2]else:input_text = "" # 如果只提供了 instructiongenerate_report(instruction, input_text)
evaluate.py
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
from datasets import load_dataset
from evaluate import load as load_metric # 使用 Hugging Face evaluate 库加载评估指标
from tqdm import tqdm
from config import BASE_MODEL_ID, OUTPUT_DIR, PROMPT_TEMPLATE, MAX_NEW_TOKENS, NUM_BEAMS, DO_SAMPLE, LOAD_IN_4BIT, TRAIN_DTYPE
from accelerate import Accelerator
import osdef evaluate_model():"""在测试集上评估模型性能,并打印一些生成示例。"""accelerator = Accelerator()device = accelerator.device# 加载基础模型 (使用与训练时相同的量化配置)print(f"正在加载基础模型: {BASE_MODEL_ID}")model = AutoModelForCausalLM.from_pretrained(BASE_MODEL_ID,load_in_4bit=LOAD_IN_4BIT,device_map={"": device},torch_dtype=TRAIN_DTYPE)print("基础模型加载完成。")# 加载 LoRA adapter 权重print(f"正在加载 LoRA adapters 来自: {OUTPUT_DIR}")model = PeftModel.from_pretrained(model, OUTPUT_DIR)print("LoRA adapters 加载完成。")# 加载 Tokenizertokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_ID, trust_remote_code=True)tokenizer.pad_token = tokenizer.eos_tokenmodel.eval() # 设置模型为评估模式# 加载测试数据集from dataset_prep import load_and_process_data, DATA_PATH# 确保数据文件存在if not os.path.exists(DATA_PATH):print(f"错误:未找到数据文件 {DATA_PATH}。请先运行 dataset_prep.py 生成或准备数据。")returndataset_dict = load_and_process_data()if 'test' not in dataset_dict or len(dataset_dict['test']) == 0:print("错误:数据集中没有 'test' 分割或测试集为空。请检查 dataset_prep.py。")returntest_dataset = dataset_dict['test']print(f"加载测试集,共 {len(test_dataset)} 条样本。")# 初始化评估指标 (例如 ROUGE)# ROUGE 适合评估文本内容的相似度# BLEU 更适合评估生成文本的精确度,但对于长文本生成可能不适用# 这里仅作示例,这些指标对报告结构、事实准确性评估能力有限try:rouge = load_metric("rouge")# bleu = load_metric("bleu") # BLEU 需要安装 sacrebleu: pip install sacrebleuexcept Exception as e:print(f"加载评估指标失败: {e}. 跳过自动化评估。")rouge = None# bleu = Nonegenerated_responses = []reference_responses = []print("\n--- 开始在测试集上生成和评估 ---")# 迭代测试集样本进行生成for i, example in tqdm(enumerate(test_dataset), total=len(test_dataset), desc="Generating on test set"):# 解析 Prompt 和 Response (假设使用 PROMPT_TEMPLATE 格式)# 需要根据您实际数据格式进行解析# 这是一个基于 PROMPT_TEMPLATE 的简单解析示例text = example['text']instruction_start = text.find("### Instruction:\n") + len("### Instruction:\n")input_start = text.find("### Input:\n")response_start = text.find("### Response:\n")if instruction_start != -1 and input_start != -1 and response_start != -1:prompt_instruction = text[instruction_start:input_start].strip()prompt_input = text[input_start + len("### Input:\n"):response_start].strip()reference_response = text[response_start + len("### Response:\n"):].strip()# 构建用于推理的 Prompt (Response 部分留空)full_prompt_for_inference = PROMPT_TEMPLATE.format(prompt_instruction, prompt_input, "")inputs = tokenizer(full_prompt_for_inference, return_tensors="pt").to(device)with torch.no_grad():outputs = model.generate(inputs=inputs.input_ids,attention_mask=inputs.attention_mask,max_new_tokens=MAX_NEW_TOKENS,num_beams=NUM_BEAMS,do_sample=DO_SAMPLE,temperature=0.7 if DO_SAMPLE else 1.0,top_k=50 if DO_SAMPLE else None,top_p=0.95 if DO_SAMPLE else None,pad_token_id=tokenizer.eos_token_id,)generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)# 提取生成的 Response 部分gen_response_start_tag = "### Response:\n"gen_response_start_index = generated_text.find(gen_response_start_tag)if gen_response_start_index != -1:generated_response = generated_text[gen_response_start_index + len(gen_response_start_tag):].strip()else:# fallbackgenerated_response = generated_text.replace(full_prompt_for_inference, "").strip()print(f"Warning: Could not find '{gen_response_start_tag}' in generated text for sample {i}. Returning raw generated text after prompt.")print(f"Generated raw: {generated_text}")generated_responses.append(generated_response)reference_responses.append(reference_response)# 打印前几个生成示例,方便人工评估if i < 5: # 打印前5个示例print(f"\n--- 示例 {i+1} ---")print("Prompt Instruction:", prompt_instruction)print("Prompt Input:", prompt_input)print("--- Reference Response ---")print(reference_response)print("--- Generated Response ---")print(generated_response)else:print(f"Warning: Sample {i} format does not match PROMPT_TEMPLATE. Skipping.")print(f"Sample text: {text}")# 计算自动化评估指标if rouge:print("\n--- 自动化评估结果 (ROUGE) ---")# ROUGE 需要输入是列表形式results = rouge.compute(predictions=generated_responses, references=reference_responses, use_stemmer=True)print(results)# print("ROUGE-1 F1:", results["rouge1"].mid.fmeasure)# print("ROUGE-2 F1:", results["rouge2"].mid.fmeasure)# print("ROUGE-L F1:", results["rougeL"].mid.fmeasure)# if bleu:# print("\n--- 自动化评估结果 (BLEU) ---")# # BLEU 的 reference 需要是列表的列表(每个 prediction 对应一个或多个参考)# # 这里假设每个 prediction 只有一个参考# bleu_results = bleu.compute(predictions=generated_responses, references=[[ref] for ref in reference_responses])# print(bleu_results)print("\n--- 自动化评估完成 ---")print("\n**重要提示:自动化指标仅供参考,请务必进行详细的人工评估!**")print(f"可以查看生成的 {len(generated_responses)} 个示例报告,并与参考报告进行对比。")if __name__ == "__main__":# 在运行评估前,请确保已经运行 train_lora.py 完成训练,并且生成了 lora_adapters 目录if not os.path.exists(OUTPUT_DIR):print(f"错误:未找到微调模型目录 {OUTPUT_DIR}。请先运行 train_lora.py 进行训练。")else:evaluate_model()
5. 执行步骤
-
安装依赖: 按照
requirements.txt
安装所有库。确保 PyTorch 的 CUDA 版本与你的环境匹配。 -
配置 Accelerate: 运行
accelerate config
进行设置。 -
准备数据:
- 真实数据: 投入大量精力,将您的 84 篇报告转换为
disaster_reports_data.jsonl
文件,格式如dataset_prep.py
中create_mock_dataset
函数生成的那样(包含text
键,内容使用PROMPT_TEMPLATE
格式化)。确保数据质量高。 - 模拟数据 (仅用于测试代码流程): 如果只是想跑通代码,可以先运行
python dataset_prep.py
生成一个包含 84 条模拟数据的disaster_reports_data.jsonl
文件。
- 真实数据: 投入大量精力,将您的 84 篇报告转换为
-
配置
config.py
: 仔细检查并修改config.py
中的BASE_MODEL_ID
、DATA_PATH
、OUTPUT_DIR
、LORA_R
、TARGET_MODULES
、训练参数等配置,尤其是BASE_MODEL_ID
和TARGET_MODULES
要与您选择的 14B 模型匹配。 -
开始训练: 运行训练脚本。使用
accelerate launch
来利用 Accelerate 的并行和优化功能。accelerate launch train_lora.py
训练过程会显示进度条和日志。根据数据量和硬件,这可能需要数小时甚至更长时间。
-
评估模型: 训练完成后,运行评估脚本。
python evaluate.py
脚本会在测试集上生成报告,计算自动化指标(如果成功加载),并打印前几个生成示例供您人工检查。
-
模型推理: 使用微调后的模型进行新报告的生成。
python inference.py "请生成一份关于最近XX市地震的详细报告" "时间:2024年5月1日\n地点:我市中心城区\n强度:5.5级\n影响:部分建筑受损,交通受阻。" # 或者直接运行不带参数,使用代码中的默认示例 Prompt # python inference.py
这会加载微调后的模型,并根据您提供的 Prompt 生成报告内容。
6. 进一步优化与提升 (超越基础微调)
考虑到数据量限制,以下是提高模型实用性的关键方向:
- 数据扩充: 不遗余力地搜集更多高质量的灾情报告及相关文档。
- 数据增强: 在专家指导下,对现有数据进行有意义的增强,生成更多训练样本。
- RAG 集成: 强烈推荐实现检索增强生成。将所有原始报告、政策文件等作为知识库。在生成报告时,先检索相关信息,然后将检索结果作为上下文输入给模型。这将大幅提高生成报告的事实准确性和时效性。这需要额外的开发工作(向量数据库、检索模块)。
- 更复杂的 Prompt Engineering: 设计更精细的 Prompt 模板或 Few-Shot 示例,更明确地指导模型生成特定结构和内容的报告。
- 人工后处理: 对模型生成的报告进行人工校对和修正,确保准确性和专业性,尤其是在初期阶段。
- 持续学习/微调: 随着新的灾情发生和报告产生,定期更新训练数据并对模型进行增量微调。
- 领域词表与知识图谱 (高级): 构建灾情领域的专业词表,甚至构建简单的知识图谱,辅助模型理解和生成。
7. 总结
代码本身只是工具,数据质量和数量是决定模型效果的关键。对于有限的数据集,微调后的模型很可能无法独立生成完全准确和专业的报告,它更像是一个智能助手,能够理解灾情信息并按照报告风格组织语言。结合 RAG 和人工审核才能构建一个真正实用的灾情分析报告生成系统。