使用peft进行qwen小模型微调实战

文章目录

说明

  • 本文内容来自网络资料和作者实战经验总结,仅供学习和交流使用。
  • 本地环境为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组成部分

  1. Model Hub(模型中心)模型共享平台 ,Hugging Face 生态系统的核心。用户可以上传和下载数以千计的预训练模型,模型可以直接用于推理、微调和分布式训练,支持多个框架,包括 PyTorchTensorFlowJAX

  1. 数据集生态(Datasets Hub)数据集平台,用户可以访问超过 10,000 个标准化数据集,用于文本分类、翻译、问答等任务,数据集可以与模型无缝结合,支持自动分词和批处理。

  2. Transformers(核心工具库):Hugging Face 的核心库,支持加载和训练预训练的 Transformer 模型,并完成推理和评估模型性能。

  3. Tokenizers(分词工具库):用于将文本处理成模型可以理解的输入格式(tokens),支持多种分词算法(如 WordPiece、BPE 等),高效且支持多线程分词,适合处理大规模数据。

  4. Accelerate:轻量级库,简化分布式训练和推理,适合需要在多 GPU 或 TPU 上运行的项目。

  5. PEFT:Hugging Face的一个库,用于参数高效微调,通过只微调模型的一小部分参数来适配下游任务,从而大幅降低计算资源和存储需求。

PEFT深入学习

  • PEFT 是一种 参数高效微调技术,旨在解决微调大型预训练模型时的高资源需求问题。传统的全量微调会调整模型的所有参数,这对存储、计算能力提出了很高的要求。而 PEFT 通过只更新少量参数,实现高效且经济的微调。

PEFT 的主要方法

  1. LoRA(Low-Rank Adaptation) :通过引入低秩矩阵,在不改变原始模型参数的基础上,微调少量的参数矩阵。
    • 特点:适合硬件受限的场景。
  2. Prefix Tuning :在模型的输入序列中添加一组可训练的前缀向量,以适应下游任务。
    • 特点:模型参数完全冻结,只调整前缀。
  3. P-Tuning v2 :在模型的每一层插入少量的提示向量,通过这些向量调整模型的行为。
    • 特点:高效且适用于复杂任务。
  4. 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
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
相关推荐
缘友一世6 小时前
Unsloth高效微调实战:基于DeepSeek-R1-Distill-Llama-8B与医疗R1数据
llm·模型微调·unsloth·deepseek
core5122 天前
实战:使用 Qwen-Agent 调用自定义 MCP 服务
agent·qwen·mcp
core51210 天前
不借助框架实现Text2SQL
sql·mysql·ai·大模型·qwen·text2sql
盼小辉丶10 天前
Transformer实战(27)——参数高效微调(Parameter Efficient Fine-Tuning,PEFT)
深度学习·transformer·模型微调
core51213 天前
LangChain实现Text2SQL
langchain·大模型·qwen·text2sql
缘友一世13 天前
模型微调DPO算法原理深入学习和理解
算法·模型微调·dpo
武子康16 天前
AI研究-129 Qwen2.5-Omni-7B 要点:显存、上下文、并发与成本
人工智能·深度学习·机器学习·ai·大模型·qwen·全模态
菠菠萝宝17 天前
【Java手搓RAGFlow】-3- 用户认证与权限管理
java·开发语言·人工智能·llm·openai·qwen·rag
宁渡AI大模型19 天前
从生成内容角度介绍开源AI大模型
人工智能·ai·大模型·qwen