考虑到数据量(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):
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 虚拟环境(推荐),然后安装所需库。
bash# 创建并激活虚拟环境 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
python
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
函数)
python
import json
import os
from datasets import DatasetDict, Dataset
from sklearn.model_selection import train_test_split
from config import DATA_PATH, PROMPT_TEMPLATE
def 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
python
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():
# 初始化 Accelerate
accelerator = 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_data
dataset_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))
# 加载基础模型和 Tokenizer
print(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 token
print("模型和 Tokenizer 加载完成。")
# 配置 LoRA
lora_config = LoraConfig(
r=LORA_R,
lora_alpha=LORA_ALPHA,
target_modules=TARGET_MODULES,
lora_dropout=LORA_DROPOUT,
bias="none",
task_type="CAUSAL_LM", # 这是一个因果语言模型任务
)
# 在模型上应用 LoRA
model = 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_dataset
if not os.path.exists(DATA_PATH):
create_mock_dataset(num_samples=84) # 创建84条模拟数据用于演示
training_function()
inference.py
python
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 Accelerator
def 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()
# 加载 Tokenizer
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_ID, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token
model.eval() # 设置模型为评估模式
# 格式化 Prompt
full_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__":
# 示例 Prompt
instruction = "请根据以下灾情概述生成一份详细的本地灾情分析报告,重点分析人员伤亡和经济损失。"
input_text = "时间:2024年5月1日\n地点:我市中心城区\n事件类型:地震\n初步影响:部分建筑倒塌,造成人员伤亡,交通拥堵。"
# 可以从命令行参数获取 Prompt
if len(sys.argv) > 1:
instruction = sys.argv[1]
if len(sys.argv) > 2:
input_text = sys.argv[2]
else:
input_text = "" # 如果只提供了 instruction
generate_report(instruction, input_text)
evaluate.py
python
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 os
def 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 加载完成。")
# 加载 Tokenizer
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_ID, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token
model.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 生成或准备数据。")
return
dataset_dict = load_and_process_data()
if 'test' not in dataset_dict or len(dataset_dict['test']) == 0:
print("错误:数据集中没有 'test' 分割或测试集为空。请检查 dataset_prep.py。")
return
test_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 sacrebleu
except Exception as e:
print(f"加载评估指标失败: {e}. 跳过自动化评估。")
rouge = None
# bleu = None
generated_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:
# fallback
generated_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 的并行和优化功能。bashaccelerate launch train_lora.py
训练过程会显示进度条和日志。根据数据量和硬件,这可能需要数小时甚至更长时间。
-
评估模型: 训练完成后,运行评估脚本。
bashpython evaluate.py
脚本会在测试集上生成报告,计算自动化指标(如果成功加载),并打印前几个生成示例供您人工检查。
-
模型推理: 使用微调后的模型进行新报告的生成。
bashpython 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 和人工审核才能构建一个真正实用的灾情分析报告生成系统。