文章目录
- 说明
- [Hugging Face和PEFT](#Hugging Face和PEFT)
-
- [Hugging Face组成部分](#Hugging Face组成部分)
- PEFT深入学习
- 虚拟环境准备
- 数据集准备和处理
- 模型微调和导出
说明
- 本文内容来自网络资料和作者实战经验总结,仅供学习和交流使用。
- 本地环境为Ubuntu 24 和单卡RTX 3090。
- peftLearn,项目中使用qwen3 0.6B,本文推荐使用qwen2.5-0.5B-Instruct获取更好的效果。
Hugging Face和PEFT
- Hugging Face 是一家专注于 人工智能(AI)技术开发和应用 的公司。它通过开源工具、社区支持和云服务,构建了一个完整的生态系统,涵盖了自然语言处理(NLP)、计算机视觉(CV)等领域。Hugging Face 的工具和平台在学术研究和工业界中都非常流行。
- Hugging Face的访问存在网络限制,国内平替为魔搭平台。
Hugging Face组成部分
- Model Hub(模型中心) :模型共享平台 ,Hugging Face 生态系统的核心。用户可以上传和下载数以千计的预训练模型,模型可以直接用于推理、微调和分布式训练,支持多个框架,包括 PyTorch 、TensorFlow 和 JAX。
-
数据集生态(Datasets Hub) : 数据集平台,用户可以访问超过 10,000 个标准化数据集,用于文本分类、翻译、问答等任务,数据集可以与模型无缝结合,支持自动分词和批处理。
-
Transformers(核心工具库):Hugging Face 的核心库,支持加载和训练预训练的 Transformer 模型,并完成推理和评估模型性能。
-
Tokenizers(分词工具库):用于将文本处理成模型可以理解的输入格式(tokens),支持多种分词算法(如 WordPiece、BPE 等),高效且支持多线程分词,适合处理大规模数据。
-
Accelerate:轻量级库,简化分布式训练和推理,适合需要在多 GPU 或 TPU 上运行的项目。
-
PEFT:Hugging Face的一个库,用于参数高效微调,通过只微调模型的一小部分参数来适配下游任务,从而大幅降低计算资源和存储需求。
PEFT深入学习
- PEFT 是一种 参数高效微调技术,旨在解决微调大型预训练模型时的高资源需求问题。传统的全量微调会调整模型的所有参数,这对存储、计算能力提出了很高的要求。而 PEFT 通过只更新少量参数,实现高效且经济的微调。
PEFT 的主要方法
- LoRA(Low-Rank Adaptation) :通过引入低秩矩阵,在不改变原始模型参数的基础上,微调少量的参数矩阵。
- 特点:适合硬件受限的场景。
- Prefix Tuning :在模型的输入序列中添加一组可训练的前缀向量,以适应下游任务。
- 特点:模型参数完全冻结,只调整前缀。
- P-Tuning v2 :在模型的每一层插入少量的提示向量,通过这些向量调整模型的行为。
- 特点:高效且适用于复杂任务。
- Prompt Tuning:直接优化输入的提示(Prompt),使模型生成符合需求的结果。
| 库 | 作用 | 关系 | 区别 |
|---|---|---|---|
| Transformers | 提供预训练模型和基础操作(如加载、微调和推理)。 | 是所有操作的基础,其他库都依赖于 Transformers 提供的模型和功能。 | 关注模型加载和基础训练,不涉及高效微调或强化学习。 |
| PEFT | 通过高效微调方法只调整少量参数来适配下游任务,降低资源需求。 | 基于 Transformers,微调时调用其模型和数据。 | 专注于减少训练资源和存储,适合多任务场景或硬件受限的环境。 |
| TRL | 使用强化学习优化模型性能,尤其是结合人类反馈的 RLHF 方法。 | 也依赖于 Transformers 提供的模型,但专注于强化学习优化流程。 | 专注于通过奖励函数和强化学习算法(如 PPO)提升模型性能,与 PEFT 的目标互补。 |
虚拟环境准备
- 创建虚拟环境安装必要依赖。
bash
conda create peft python=3.13.9
conda create -n peft python=3.13.9
conda activate peft
conda install ipykernel
pip install python -m ipykernel install --user --name peft --display-name peft
python -m ipykernel install --user --name peft --display-name peft
pip install datasets
pip install load_dotenv
pip show torch
pip install torch
pip install transformers
pip install accelerate
pip install ipywidgets
pip install tensorboard
- 主要依赖库和版本信息。
| 库 | 版本 |
|---|---|
| Python | 3.13.9 |
| pytorch | 2.9.1 |
| datasets | 4.4.1 |
| pandas | 2.3.3 |
| transformers | 4.57.3 |
| peft | 0.18.0 |
| wandb | 0.23.1 |
- 使用基础环境安装modelscope,并下载模型,推荐使用Qwen2.5-0.5B-Instruct
bash\
mkdir Qwen2.5-0.5B-Instruct
modelscope download --model Qwen/Qwen2.5-0.5B-Instruct README.md --local_dir ./Qwen2.5-0.5B-Instruct
- 模型加载和验证
python
import torch
# PyTorch深度学习框架,提供张量计算和神经网络构建工具
from transformers import AutoModelForCausalLM, AutoTokenizer
import os
# 指定使用的GPU设备(0,1表示使用第1和第2块GPU)
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
# 模型路径配置 - 可替换为其他模型路径
model_path = "/home/yang/models/Qwen2.5-0.5B-Instruct/"
# 加载分词器,负责文本与token之间的转换
tokenizer = AutoTokenizer.from_pretrained(model_path)
# 加载预训练模型
# dtype="auto": 自动选择最优数据类型(如float16减少显存)
# device_map="auto": 自动分配模型到可用GPU设备
model = AutoModelForCausalLM.from_pretrained(
model_path,
dtype="auto",
device_map="auto"
)
# 配置填充token - 确保批量处理时长度一致
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token # 使用结束符作为填充符
# 输入文本
input_text = "你好,你是谁!"
# 文本预处理:转换为模型输入格式
# return_tensors="pt": 返回PyTorch张量
# padding=True: 填充到相同长度
# truncation=True: 超长时截断
inputs = tokenizer(
input_text,
return_tensors="pt",
padding=True,
truncation=True).to(model.device)
# 确保attention_mask和pad_token_id在正确设备上
inputs["attention_mask"] = inputs["attention_mask"].to(model.device)
inputs["pad_token_id"] = tokenizer.pad_token_id
# 模型推理生成
# max_length=512: 限制生成最大长度
outputs = model.generate(
inputs["input_ids"],
attention_mask=inputs["attention_mask"],
pad_token_id=inputs["pad_token_id"],
max_length=512
)
# 调试信息:验证分词和填充效果
original_length = len(tokenizer.tokenize(input_text))
padded_length = len(inputs["input_ids"][0])
padding_count = padded_length - original_length
print(f"填充token: {tokenizer.pad_token}")
print(f"结束token: {tokenizer.eos_token}")
print(f"原始长度: {original_length}")
print(f"填充后长度: {padded_length}")
print(f"填充数量: {padding_count}")
print("=" * 50)
# 解码输出:将token转换为可读文本
# skip_special_tokens=True: 跳过特殊token(如[PAD]、[EOS])
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
print("模型回复:", response)
python
import torch
# PyTorch深度学习框架,提供张量计算和神经网络构建工具
from transformers import AutoModelForCausalLM, AutoTokenizer
# AutoModelForCausalLM: 自动加载因果语言模型(用于文本生成)
# AutoTokenizer: 自动加载对应的分词器(文本与token互转)
import os
# 指定使用的GPU设备
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
# 模型路径配置
model_path = "/home/yang/models/Qwen2.5-0.5B-Instruct/"
# 加载分词器
tokenizer = AutoTokenizer.from_pretrained(model_path)
# 加载预训练模型
# torch_dtype="auto": 自动选择最优数据类型(如float16减少显存)
# device_map="auto": 自动分配模型到可用GPU设备
model = AutoModelForCausalLM.from_pretrained(
model_path,
dtype="auto", # 官方示例中通常用 torch_dtype,更明确
device_map="auto"
)
# 配置填充token - 确保批量处理时长度一致
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token # 使用结束符作为填充符
# 输入文本
input_text = "你好,你是谁家的产品,哪个AI,叫什么名字?"
# 文本预处理:转换为模型输入格式
# return_tensors="pt": 返回PyTorch张量
# padding=True: 填充到相同长度
# truncation=True: 超长时截断
inputs = tokenizer(
input_text,
return_tensors="pt",
padding=True,
truncation=True
).to(model.device)
# 确保attention_mask和pad_token_id在正确设备上
inputs["attention_mask"] = inputs["attention_mask"].to(model.device)
inputs["pad_token_id"] = tokenizer.pad_token_id
# --- 核心优化:采用官方推荐的采样参数 ---
# 这里采用非思考模式的推荐参数,适合通用对话场景
# enable_thinking = False (默认)
sampling_params = {
"temperature": 0.7, # 控制随机性,值越大越随机
"top_p": 0.8, # 核采样,从累计概率超过0.8的token中采样
"top_k": 20, # 从概率最高的20个token中采样
"min_p": 0.0, # 最小概率阈值,过滤掉概率过低的token
"repetition_penalty": 1.05, # 存在惩罚,用于减少重复(官方建议在0-2之间调整)
"do_sample": True, # 启用采样,必须设置为True以上参数才生效
}
# 模型推理生成
# max_new_tokens: 限制新生成的token数量,比max_length更精确
# 注意:max_length = input_length + max_new_tokens
outputs = model.generate(
**inputs, # 直接传入包含input_ids, attention_mask等的字典
max_new_tokens=50, # 官方建议充足的输出长度,这里设置为512作为示例
**sampling_params # 解包并传入所有采样参数
)
# 解码输出:将token转换为可读文本
# skip_special_tokens=True: 跳过特殊token(如[PAD]、[EOS])
# 只解码新生成的部分,去除原始输入
response = tokenizer.decode(outputs[0][len(inputs["input_ids"][0]):], skip_special_tokens=True)
print("=" * 50)
print("模型回复:", response)
# --- 可选:针对特定任务的提示工程示例 ---
print("\n" + "=" * 50)
print("数学问题示例:")
math_prompt = "一个数的两倍比它本身多15,这个数是多少?请逐步推理,并将最终答案放在\\boxed{}内。"
math_inputs = tokenizer(math_prompt, return_tensors="pt").to(model.device)
math_outputs = model.generate(
**math_inputs,
max_new_tokens=256,
**sampling_params
)
math_response = tokenizer.decode(math_outputs[0][len(math_inputs["input_ids"][0]):], skip_special_tokens=True)
print("模型回复:", math_response)
数据集准备和处理
bash
mkdir chinese-medical
modelscope download --dataset alexhuangguo/chinese-medical README.md --local_dir ./chinese-medical
- 数据集处理脚本
python
# 更改"instruction"、"input"相互之间的对应关系,方便后续统一识别
import json
import argparse
def convert_json_format(input_file, output_file, num_entries=1000):
"""
读取输入JSON文件,将每条记录的字段重新排列,并写入输出JSON文件。
:param input_file: 输入JSON文件的路径
:param output_file: 输出JSON文件的路径
:param num_entries: 要处理的记录数量
"""
# 打开并读取输入文件的所有行
with open(input_file, 'r', encoding='utf-8') as infile:
lines = infile.readlines()
converted_data = []
# 遍历每一行数据,并进行字段重新排列
for line in lines[:num_entries]:
entry = json.loads(line)
new_entry = {
"instruction": entry["input"], # 将原input字段的内容移到instruction字段
"input": entry["instruction"], # 将原instruction字段的内容移到input字段
"output": entry["output"] # 保持output字段不变
}
converted_data.append(new_entry)
# 打开输出文件并逐条写入转换后的数据
with open(output_file, 'w', encoding='utf-8') as outfile:
json.dump(converted_data, outfile, ensure_ascii=False, indent=4)
if __name__ == "__main__":
# 创建ArgumentParser对象以解析命令行参数
parser = argparse.ArgumentParser(description="Convert JSON format of medical QA data.")
# 添加位置参数:输入文件路径
parser.add_argument("input_file", help="Path to the input JSON file")
# 添加位置参数:输出文件路径
parser.add_argument("output_file", help="Path to the output JSON file")
# 添加可选参数:要处理的记录数量,默认为3000
parser.add_argument("--num_entries", type=int, default=3000, help="Number of entries to process")
# 解析命令行参数
args = parser.parse_args()
# 调用convert_json_format函数进行实际的数据转换
convert_json_format(args.input_file, args.output_file, args.num_entries)
- 执行脚本代码
python
mkdir data
python convert_json.py /home/yang/dataset/chinese-medical/medicalQA.json ./data/medicalQA_top3k.json
模型微调和导出
分布式微调代码和执行脚本
train_distributed.py文件代码如下:
python
# 标准库
import os
import json
import datetime
from argparse import ArgumentParser
# 第三方库
import torch
import torch.distributed as dist
import pandas as pd
from torch.utils.tensorboard import SummaryWriter
# Hugging Face 生态
from datasets import Dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
Trainer,
DataCollatorForSeq2Seq,
)
# PEFT (Parameter-Efficient Fine-Tuning)
from peft import (
get_peft_model,
LoraConfig,
TaskType,
prepare_model_for_kbit_training,
)
from transformers import BitsAndBytesConfig
class CustomTrainer(Trainer):
"""
自定义训练器,继承自 Hugging Face 的 Trainer。
主要功能:
1. 在训练过程中自动记录损失、学习率、GPU内存等指标到 TensorBoard。
2. 定期记录梯度直方图和梯度范数,用于监控训练稳定性。
3. 处理并警告 NaN/Inf 损失值。
4. 在训练结束时正确关闭 TensorBoard 日志写入器。
"""
def __init__(self, *args, tensorboard_dir=None, **kwargs):
"""
初始化自定义训练器。
Args:
tensorboard_dir (str, optional): TensorBoard 日志的根目录。
"""
super().__init__(*args, **kwargs)
# 启用梯度检查点以减少显存占用,但会增加计算时间
self.model.gradient_checkpointing_enable()
# 创建 TensorBoard 写入器,日志目录包含时间戳以区分不同训练
timestamp = datetime.datetime.now().strftime('%Y%m%d-%H%M%S')
self.writer = SummaryWriter(f'{tensorboard_dir}/training_{timestamp}')
def compute_loss(self, model, inputs, return_outputs=False, **kwargs):
"""
计算损失并记录训练指标。覆盖父类的 compute_loss,增加详细的日志记录功能。
"""
outputs = model(**inputs)
loss = outputs.loss
# 按设定间隔记录日志
if self.state.global_step % self.args.logging_steps == 0:
try:
# --- 核心训练指标 ---
if not torch.isnan(loss) and not torch.isinf(loss):
self.writer.add_scalar('Training/Loss', loss.item(), self.state.global_step)
self.writer.add_scalar('Training/Learning_Rate', self.optimizer.param_groups[0]['lr'], self.state.global_step)
self.writer.add_scalar('Training/Progress', self.state.global_step / self.args.max_steps * 100, self.state.global_step)
self.writer.add_scalar('Training/Batch_Size', inputs['input_ids'].size(0), self.state.global_step)
# --- 系统资源指标 ---
if torch.cuda.is_available():
self.writer.add_scalar('System/GPU_Memory_Allocated', torch.cuda.memory_allocated() / 1024**2, self.state.global_step)
self.writer.add_scalar('System/GPU_Memory_Reserved', torch.cuda.memory_reserved() / 1024**2, self.state.global_step)
# --- 梯度监控(降低频率以避免性能开销) ---
if self.state.global_step % (self.args.logging_steps * 10) == 0:
for name, param in model.named_parameters():
if param.requires_grad and param.grad is not None:
grad_data = param.grad.data
if grad_data.numel() > 0 and not torch.all(grad_data == 0):
try:
self.writer.add_histogram(f'Gradients/{name}', grad_data.float().cpu().numpy(), self.state.global_step, bins='auto')
except Exception as e:
print(f"Warning: Failed to log histogram for {name}: {e}")
# 计算并记录总梯度范数,用于检测梯度爆炸
total_norm = self.get_grad_norm()
self.writer.add_scalar('Gradients/total_norm', total_norm, self.state.global_step)
except Exception as e:
print(f"Warning: Error during TensorBoard logging: {e}")
# 处理无效的损失值,防止训练中断
if torch.isnan(loss).any() or torch.isinf(loss).any():
print(f"Warning: NaN/Inf loss detected at step {self.state.global_step}: {loss.item()}")
loss = torch.where(torch.isnan(loss) | torch.isinf(loss), torch.full_like(loss, 0.0), loss)
return (loss, outputs) if return_outputs else loss
def get_grad_norm(self):
"""计算模型所有参数的梯度 L2 范数。"""
total_norm = 0.0
for p in self.model.parameters():
if p.requires_grad and p.grad is not None:
param_norm = p.grad.data.norm(2)
total_norm += param_norm.item() ** 2
return total_norm ** (1. / 2)
def on_evaluate(self, args, state, control, metrics=None, **kwargs):
"""评估回调,将验证集指标记录到 TensorBoard。"""
if metrics:
for key, value in metrics.items():
self.writer.add_scalar(f'Eval/{key}', value, state.global_step)
def on_train_end(self, args, state, control, **kwargs):
"""训练结束回调,关闭 TensorBoard 写入器。"""
self.writer.close()
def setup_distributed():
"""
初始化分布式训练环境。
通过环境变量 LOCAL_RANK 和 WORLD_SIZE 设置进程组和设备。
如果未在分布式环境中启动,则 local_rank 和 world_size 均为 -1。
Returns:
tuple[int, int]: (local_rank, world_size)
"""
local_rank = int(os.environ.get("LOCAL_RANK", -1))
world_size = int(os.environ.get("WORLD_SIZE", -1))
if local_rank != -1:
torch.cuda.set_device(local_rank)
dist.init_process_group(backend="nccl")
return local_rank, world_size
def preprocess_function(examples, tokenizer, max_length=512):
"""
数据预处理函数,将原始文本转换为模型输入格式。
1. 根据 'instruction', 'input', 'output' 字段构建提示词。
2. 使用 tokenizer 进行编码、填充和截断。
3. 创建 'labels',将填充 token 的位置设为 -100,使其不参与损失计算。
Args:
examples (dict): 包含 'instruction', 'input', 'output' 键的字典。
tokenizer (AutoTokenizer): 分词器实例。
max_length (int): 序列的最大长度。
Returns:
dict: 包含 'input_ids', 'attention_mask', 'labels' 的字典。
"""
prompts = []
for instruction, input_text, output in zip(examples['instruction'], examples['input'], examples['output']):
if input_text and input_text.strip():
prompt = f"Input: {input_text}\nInstruction: {instruction}\nOutput: {output}"
else:
prompt = f"Instruction: {instruction}\nOutput: {output}"
prompts.append(prompt)
tokenized_inputs = tokenizer(
prompts,
max_length=max_length,
truncation=True,
padding='max_length',
return_tensors="pt"
)
# 创建 labels,将 padding token 对应的 id 替换为 -100
labels = tokenized_inputs['input_ids'].clone()
labels[labels == tokenizer.pad_token_id] = -100
tokenized_inputs['labels'] = labels
return tokenized_inputs
def convert_json_to_dataset(json_file_path, save_directory, tokenizer):
"""
从 JSON 文件加载、清洗数据,并转换为 Hugging Face Dataset 格式。
处理后的数据集将保存到磁盘,避免重复处理。
Args:
json_file_path (str): JSON 数据文件路径。
save_directory (str): 处理后数据集的保存目录。
tokenizer (AutoTokenizer): 用于数据预处理的分词器。
Returns:
Dataset: 处理完成的 Hugging Face Dataset 对象。
"""
with open(json_file_path, 'r', encoding='utf-8') as file:
data = json.load(file)
# 数据清洗:确保 instruction 和 output 字段有效
cleaned_data = [
item for item in data
if isinstance(item.get('instruction'), str) and item['instruction'].strip() and
isinstance(item.get('output'), str) and item['output'].strip()
]
dataset = Dataset.from_pandas(pd.DataFrame(cleaned_data))
# 批量应用预处理函数
processed_dataset = dataset.map(
lambda x: preprocess_function(x, tokenizer),
batched=True,
remove_columns=dataset.column_names
)
# 设置数据格式为 PyTorch 张量
processed_dataset.set_format(type='torch', columns=['input_ids', 'attention_mask', 'labels'])
# 保存到磁盘
os.makedirs(save_directory, exist_ok=True)
processed_dataset.save_to_disk(save_directory)
return processed_dataset
def prepare_model_and_tokenizer(model_path, local_rank):
"""
加载预训练模型和分词器,并为低比特训练做准备。
- 加载分词器。
- 使用 8-bit 量化、float16 精度和设备映射加载模型,以优化显存。
- 调用 `prepare_model_for_kbit_training` 使模型适配量化训练。
Args:
model_path (str): 预训练模型的路径或名称。
local_rank (int): 当前进程的 GPU rank,用于设备映射。
Returns:
tuple: (model, tokenizer)
"""
# 定义量化配置
quantization_config = BitsAndBytesConfig(
load_in_8bit=True,
)
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
# 根据是否分布式训练设置设备映射
device_map = {'': local_rank} if local_rank != -1 else {'': torch.cuda.current_device()}
model = AutoModelForCausalLM.from_pretrained(
model_path,
trust_remote_code=True,
quantization_config=quantization_config, # 使用新的配置对象
dtype=torch.float16, # 使用新的 dtype 参数
device_map=device_map
)
# 为量化训练准备模型
model = prepare_model_for_kbit_training(model)
return model, tokenizer
def main():
"""
主训练流程。
1. 解析命令行参数。
2. 设置分布式训练环境。
3. 加载模型和分词器。
4. 配置训练参数和 LoRA。
5. 加载或预处理数据集。
6. 启动训练并保存最终模型。
"""
parser = ArgumentParser()
# --- 路径与目录 ---
parser.add_argument("--model_path", type=str, required=True, help="预训练模型路径")
parser.add_argument("--json_file_path", type=str, required=True, help="训练数据 JSON 文件路径")
parser.add_argument("--save_directory", type=str, default="./data/processed", help="处理后数据集的保存路径")
parser.add_argument("--output_dir", type=str, default="./output", help="模型输出目录")
parser.add_argument("--log_path", type=str, default="./logs", help="训练日志路径")
parser.add_argument("--tensorboard_dir", type=str, default="tensorboard", help="TensorBoard 日志目录")
# --- 分布式训练 ---
parser.add_argument("--local_rank", type=int, default=-1, help="本地进程 rank,通常由启动脚本设置")
# --- 训练超参数 ---
parser.add_argument("--batch_size", type=int, default=2, help="每个设备的训练批次大小")
parser.add_argument("--gradient_accumulation_steps", type=int, default=8, help="梯度累积步数")
parser.add_argument("--learning_rate", type=float, default=5e-6, help="学习率")
parser.add_argument("--max_steps", type=int, default=50000, help="最大训练步数")
parser.add_argument("--warmup_steps", type=int, default=100, help="学习率预热步数")
parser.add_argument("--logging_steps", type=int, default=10, help="日志记录间隔")
parser.add_argument("--save_steps", type=int, default=100, help="模型检查点保存间隔")
parser.add_argument("--train_epochs", type=int, default=10, help="训练轮数")
parser.add_argument("--max_grad_norm", type=float, default=0.5, help="梯度裁剪阈值")
parser.add_argument("--fp16", type=bool, default=True, help="是否使用混合精度训练")
# --- LoRA 配置 ---
parser.add_argument("--rank", type=int, default=8, help="LoRA 的 rank (r)")
parser.add_argument("--lora_alpha", type=int, default=32, help="LoRA 的 alpha 缩放因子")
parser.add_argument("--lora_dropout", type=float, default=0.05, help="LoRA 的 dropout 率")
args = parser.parse_args()
os.makedirs(args.tensorboard_dir, exist_ok=True)
# 1. 设置分布式环境
local_rank, _ = setup_distributed()
is_main_process = local_rank in [-1, 0]
# 2. 准备模型和分词器
model, tokenizer = prepare_model_and_tokenizer(args.model_path, local_rank)
# 3. 配置训练参数
training_args = TrainingArguments(
output_dir=args.output_dir,
per_device_train_batch_size=args.batch_size,
gradient_accumulation_steps=args.gradient_accumulation_steps,
learning_rate=args.learning_rate,
fp16=args.fp16,
logging_steps=args.logging_steps,
save_steps=args.save_steps,
max_steps=args.max_steps,
warmup_steps=args.warmup_steps,
logging_dir=args.log_path,
num_train_epochs=args.train_epochs,
save_strategy="steps",
save_total_limit=3,
dataloader_num_workers=2,
gradient_checkpointing=True,
max_grad_norm=args.max_grad_norm,
ddp_find_unused_parameters=False,
report_to="tensorboard",
local_rank=local_rank
)
# 4. 配置并应用 LoRA
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=args.rank,
lora_alpha=args.lora_alpha,
lora_dropout=args.lora_dropout,
target_modules=['q_proj', 'k_proj', 'v_proj', 'o_proj'], # 常见的注意力模块目标
bias="none",
)
model = get_peft_model(model, lora_config)
# 5. 加载或创建数据集
if os.path.exists(args.save_directory):
dataset = Dataset.load_from_disk(args.save_directory)
else:
dataset = convert_json_to_dataset(args.json_file_path, args.save_directory, tokenizer)
# 6. 初始化自定义训练器并开始训练
trainer = CustomTrainer(
model=model,
args=training_args,
train_dataset=dataset,
tensorboard_dir=args.tensorboard_dir,
data_collator=DataCollatorForSeq2Seq(tokenizer, pad_to_multiple_of=8, return_tensors="pt")
)
trainer.train()
# 7. 保存最终模型(仅主进程)
if is_main_process:
trainer.save_model(f"{args.output_dir}/final_model")
# 8. 清理分布式环境
if local_rank != -1:
dist.destroy_process_group()
if __name__ == "__main__":
main()
train_distributed.sh命令行执行脚本文件
python
#!/bin/bash
# 指定使用 bash 作为脚本的解释器
# 获取当前系统中可用的 GPU 数量
NUM_GPUS=$(nvidia-smi --query-gpu=gpu_name --format=csv,noheader | wc -l)
# 可以使用以下命令指定要使用的显卡
# export CUDA_VISIBLE_DEVICES="0,1"
# 使用 `torchrun` 启动分布式训练
# `torchrun` 是 PyTorch 用于分布式训练的工具,可以自动管理多卡训练
# --nproc_per_node=$NUM_GPUS:指定每个节点使用的 GPU 数量,$NUM_GPUS 是之前获取的 GPU 数量
# --nnodes=1:表示使用一个节点进行训练
# --node_rank=0:指定当前节点的 rank(编号),主节点通常是 0
# 指定训练时使用的各个参数
torchrun --nproc_per_node=$NUM_GPUS --nnodes=1 --node_rank=0 train_distributed.py \
--model_path "/home/yang/models/Qwen2.5-0.5B-Instruct/" \
--json_file_path "./data/medicalQA_top3k.json" \
--save_directory "./data/train_data_distribute" \
--output_dir "./output" \
--log_path "./log" \
--tensorboard_dir "tensorboard_dir" \
--batch_size 2 \
--train_epochs 5 \
--fp16 True \
--gradient_accumulation_steps 8 \
--learning_rate 5e-5 \
--max_steps -1 \
--warmup_steps 0 \
--logging_steps 10 \
--save_steps 100 \
--max_grad_norm 0.5 \
--rank 8 \
--lora_alpha 16 \
--lora_dropout 0.05
- 记得执行前添加执行权限。
bash
chmod +x train_distributed.sh
加载验证
lora_adapter_chat.py内容如下
python
import os
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel, PeftConfig
import warnings
# 关闭所有警告,避免干扰输出
warnings.filterwarnings('ignore')
class ChatBot:
def __init__(self, base_model_path, lora_model_path):
"""
初始化聊天机器人,加载基础模型和LoRA模型。
参数:
base_model_path (str): 基础模型路径,通常是预训练的语言模型路径。
lora_model_path (str): LoRA模型路径,用于微调。
"""
print("正在加载模型...")
# 加载tokenizer:用于将输入文本转换为模型可以理解的tokens(标记),并将模型的输出转换为可读的文本。
self.tokenizer = AutoTokenizer.from_pretrained(
base_model_path, # 基础模型路径
trust_remote_code=True # 允许加载远程的代码
)
# 加载基础模型:加载预训练的因果语言模型,用于生成文本。
self.base_model = AutoModelForCausalLM.from_pretrained(
base_model_path,
trust_remote_code=True, # 允许加载远程的代码
device_map="auto", # 自动选择设备(如GPU/CPU)
torch_dtype=torch.float16 # 使用16位精度来减少内存占用
)
# 加载LoRA模型:基于LoRA微调的模型,用于增强生成能力。
self.model = PeftModel.from_pretrained(
self.base_model, # 基础模型
lora_model_path, # LoRA微调后的模型路径
torch_dtype=torch.float16, # 使用16位精度
device_map="auto" # 自动选择设备
)
print("模型加载完成!")
def generate_response(self, instruction, input_text=""):
"""
根据给定的指令和输入文本生成回复。
参数:
instruction (str): 聊天机器人执行的指令。
input_text (str, optional): 用户输入的文本,默认为空。
返回:
response (str): 生成的回复。
"""
# 根据是否有输入文本构建不同的提示语(prompt)
if input_text:
prompt = f"Instruction: {instruction}\nInput: {input_text}\nOutput:"
else:
prompt = f"Instruction: {instruction}\nOutput:"
# 使用tokenizer将prompt转换为模型输入格式
inputs = self.tokenizer(prompt, return_tensors="pt", padding=True)
inputs = {k: v.to(self.model.device) for k, v in inputs.items()} # 将输入移动到与模型相同的设备
# 禁用梯度计算(推理时不需要计算梯度)
with torch.no_grad():
# 生成模型的输出(回复)
outputs = self.model.generate(
**inputs, # 输入的token ids
max_new_tokens=512, # 最多生成512个新tokens
temperature=0.1, # 控制生成的多样性,较低的温度使输出更确定性
top_p=0.1, # 控制样本的概率分布
repetition_penalty=1.1, # 重复惩罚,避免生成重复文本
pad_token_id=self.tokenizer.pad_token_id, # 填充token的ID
eos_token_id=self.tokenizer.eos_token_id # 结束token的ID
)
# 解码生成的tokens为文本,并去除特殊tokens
response = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
# 提取输出部分的回复文本,去除"Output:"标记
response = response.split("Output:")[-1].strip()
return response
def main():
"""
主函数:用于启动聊天机器人,接收用户输入,并生成回复。
"""
# 配置基础模型路径和LoRA模型路径
BASE_MODEL_PATH = "/home/yang/models/Qwen2.5-0.5B-Instruct/" # 基础模型路径
LORA_MODEL_PATH = "./output/final_model" # LoRA模型路径
# 初始化聊天机器人
chatbot = ChatBot(BASE_MODEL_PATH, LORA_MODEL_PATH)
# 欢迎信息和说明
print("\n=== 欢迎使用聊天机器人 ===")
print("输入 'quit' 或 'exit' 结束对话")
print("输入 'clear' 清空对话历史")
print("============================\n")
while True:
try:
# 获取用户输入的指令
instruction = input("\n请输入指令: ").strip()
# 检查是否退出对话
if instruction.lower() in ['quit', 'exit']:
print("\n感谢使用!再见!")
break
# 检查是否清空对话历史
if instruction.lower() == 'clear':
print("\n对话历史已清空!")
continue
# 获取用户输入的可选文本
input_text = input("请输入补充内容(可选,直接回车跳过): ").strip()
# 生成机器人回复
print("\n正在生成回复...")
response = chatbot.generate_response(instruction, input_text)
# 输出机器人的回复
print("\n机器人回复:")
print("-" * 50)
print(response)
print("-" * 50)
except KeyboardInterrupt:
print("\n\n检测到键盘中断,正在退出...")
break
except Exception as e:
print(f"\n发生错误: {str(e)}")
print("请尝试重新输入或输入 'exit' 退出")
continue
# 启动程序入口
if __name__ == "__main__":
main()
bash
python lora_adapter_chat.py
导出合并模型
merge.py
python
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
# 1. 加载原始基座模型(Qwen2.5-3B-Instruct)
base_model_path = "/home/yang/models/Qwen2.5-0.5B-Instruct/"
model = AutoModelForCausalLM.from_pretrained(base_model_path)
# 2. 加载微调后的 LoRA adapter(假设你使用的是 LoRA 库)
adapter_path = "output/final_model/"
# 根据使用的 LoRA 实现来加载 adapter
# 如,使用LoRA 模块并且是 Hugging Face 实现
from peft import PeftModel # 假设使用的是 `peft` 库,具体取决于的LoRA实现
# 加载微调后的 LoRA adapter
adapter = PeftModel.from_pretrained(model, adapter_path)
# 3. 合并 LoRA adapter 和基座模型
#使用的是 LoRA 库,通常调用 `PeftModel` 的方法会自动将 adapter 应用到基座模型上
# 4. 保存合并后的模型
final_model_path = "merge_model/Qwen2.5-0.5B-Instruct-peft"
adapter.save_pretrained(final_model_path)
# 也可以保存 tokenizer
tokenizer = AutoTokenizer.from_pretrained(base_model_path)
tokenizer.save_pretrained(final_model_path)
print(f"模型已保存至 {final_model_path}")
python
conda activate peft
python merge.py