【LLM训练系列04】手把手教你Qlora微调

大模型向量数据库云安全

导入包


            
              
import os  
os.environ["CUDA\_VISIBLE\_DEVICES"]="7"  
  
from dataclasses import dataclass, field  
import json  
import math  
import logging  
import os  
from typing import Dict, Optional, List  
import torch  
from torch.utils.data import Dataset  
import transformers  
from transformers import Trainer, GPTQConfig, deepspeed  
from transformers.trainer_pt_utils import LabelSmoother  
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training  
from accelerate.utils import DistributedType  
import warnings  
import datasets  
warnings.filterwarnings("ignore")  

          

数据集加载

IGNORE_TOKEN_ID 是一个常量,通常用于在训练过程中忽略某些特定的标签或输入。它的作用是告诉模型在计算损失时不考虑这些特定的标签或输入。


            
              
IGNORE_TOKEN_ID = LabelSmoother.ignore_index  
IGNORE_TOKEN_ID  

          

            
              
def preprocess_function(  
    examples: Dict,  
    tokenizer: transformers.PreTrainedTokenizer,  
    max_len: int,  
    system_message: str = "You are a helpful assistant."  
) -> Dict:  
    """  
    预处理函数,用于处理对话数据并转换为模型输入格式  
      
    参数:  
        examples: 包含对话数据的字典  
        tokenizer: 分词器  
        max\_len: 序列最大长度  
        system\_message: 系统提示信息  
    """  
    # 定义角色标记  
    roles = {"user": "<|im\_start|>user", "assistant": "<|im\_start|>assistant"}  
      
    # 获取特殊token的ID  
    # im\_start = tokenizer.im\_start\_id  # 对话开始标记  
    # im\_end = tokenizer.im\_end\_id      # 对话结束标记  
      
    im_start = tokenizer('<|im\_start|>').input_ids[0]  
    im_end = tokenizer('<|im\_end|>').input_ids[0]  
    nl_tokens = tokenizer('\n').input_ids  # 换行符的token ID  
      
      
      
      
      
    # 预处理各种角色标记  
    _system = tokenizer('system').input_ids + nl_tokens  
    _user = tokenizer('user').input_ids + nl_tokens  
    _assistant = tokenizer('assistant').input_ids + nl_tokens  
  
    # 存储批处理结果  
    input_ids_list = []  
    labels_list = []  
      
    # 处理每个对话样本  
    for source in examples["conversations"]:  
        # 确保对话以用户输入开始  
        if roles[source[0]["from"]] != roles["user"]:  
            source = source[1:]  
              
        input_id, target = [], []  
          
        # 添加系统消息  
        system = [im_start] + _system + tokenizer(system_message).input_ids + [im_end] + nl_tokens  
        input_id += system  
        # 系统消息在训练时不计算损失  
        target += [im_start] + [IGNORE_TOKEN_ID] * (len(system)-3) + [im_end] + nl_tokens  
          
        # 处理对话中的每个回合  
        for sentence in source:  
            role = roles[sentence["from"]]  
            # 构建输入序列  
            _input_id = tokenizer(role).input_ids + nl_tokens + \  
                tokenizer(sentence["value"]).input_ids + [im_end] + nl_tokens  
            input_id += _input_id  
              
            # 构建目标序列:用户输入部分用IGNORE\_TOKEN\_ID标记,不计算损失  
            if role == '<|im\_start|>user':  
                _target = [im_start] + [IGNORE_TOKEN_ID] * (len(_input_id)-3) + [im_end] + nl_tokens  
            # 助手回复部分需要计算损失  
            elif role == '<|im\_start|>assistant':  
                _target = [im_start] + [IGNORE_TOKEN_ID] * len(tokenizer(role).input_ids) + \  
                    _input_id[len(tokenizer(role).input_ids)+1:-2] + [im_end] + nl_tokens  
            else:  
                raise NotImplementedError  
            target += _target  
              
        # padding到最大长度  
        input_id += [tokenizer.pad_token_id] * (max_len - len(input_id))  
        target += [IGNORE_TOKEN_ID] * (max_len - len(target))  
          
        # 截断到最大长度  
        input_ids_list.append(input_id[:max_len])  
        labels_list.append(target[:max_len])  
      
    # 构建attention mask:padding位置为0,其他位置为1  
    attention_masks = [[1 if token != tokenizer.pad_token_id else 0 for token in seq] for seq in input_ids_list]  
      
    return {  
        "input\_ids": input_ids_list,  
        "labels": labels_list,  
        "attention\_mask": attention_masks  
    }  
  
def make_supervised_data_module(  
    tokenizer: transformers.PreTrainedTokenizer,  
    data_args,  
    max_len: int,  
) -> Dict:  
    """  
    创建用于有监督微调的数据集  
      
    参数:  
        tokenizer: 分词器  
        data\_args: 数据相关的参数配置  
        max\_len: 序列最大长度  
      
    返回:  
        包含训练集和验证集的字典  
    """  
    # 加载训练数据集  
    train_dataset = datasets.load_dataset('json', data_files=data_args.data_path)['train']  
      
    # 使用map函数进行批处理  
    train_dataset = train_dataset.map(  
        lambda x: preprocess_function(x, tokenizer, max_len),  
        batched=True,  # 启用批处理  
        remove_columns=train_dataset.column_names,  # 移除原始列  
        num_proc=data_args.preprocessing_num_workers if hasattr(data_args, 'preprocessing\_num\_workers') else None,  # 并行处理  
        load_from_cache_file=not data_args.overwrite_cache if hasattr(data_args, 'overwrite\_cache') else True,  # 缓存控制  
        desc="处理训练数据集",  # 进度条描述  
    )  
      
    # 设置数据集格式为PyTorch张量  
    train_dataset.set_format(type='torch', columns=['input\_ids', 'labels', 'attention\_mask'])  
      
    # 处理验证数据集(如果提供)  
    eval_dataset = None  
    if data_args.eval_data_path:  
        eval_dataset = datasets.load_dataset('json', data_files=data_args.eval_data_path)['train']  
        eval_dataset = eval_dataset.map(  
            lambda x: preprocess_function(x, tokenizer, max_len),  
            batched=True,  
            remove_columns=eval_dataset.column_names,  
            num_proc=data_args.preprocessing_num_workers if hasattr(data_args, 'preprocessing\_num\_workers') else None,  
            load_from_cache_file=not data_args.overwrite_cache if hasattr(data_args, 'overwrite\_cache') else True,  
            desc="处理验证数据集",  
        )  
        eval_dataset.set_format(type='torch', columns=['input\_ids', 'labels', 'attention\_mask'])  
    return dict(train_dataset=train_dataset, eval_dataset=eval_dataset)  

          

这个函数的作用主要是为对话模型准备训练和验证数据。具体来说,它将原始的对话数据转换成模型可以接受的输入格式,并且为有监督微调(supervised fine-tuning)做好准备。下面我会用口语化的方式来解释这个函数的作用:


1. **preprocess\_function** 函数

这个函数的主要任务是对话数据进行预处理,将其转换为模型能够理解的格式。具体来说,它做了以下几件事:

  • 角色标记 :对话中通常有不同的角色,比如用户和助手。函数会为每个角色添加特定的标记(比如<|im_start|>user<|im_start|>assistant),这样模型就知道是谁在说话。
  • 系统消息 :在对话开始之前,通常会有一个系统消息(比如“你是一个有帮助的助手”),这个消息会被添加到对话的开头。
  • 分词和编码 :使用分词器(tokenizer)将对话内容转换为模型能理解的 token ID。比如,用户的输入和助手的回复都会被转换成 token 序列。
  • 损失计算 :在训练过程中,模型需要知道哪些部分是需要计算损失的。用户的输入部分不会计算损失(用IGNORE_TOKEN_ID标记),而助手的回复部分会计算损失。
  • 填充和截断 :为了保证所有对话的长度一致,函数会对对话进行填充(padding)或截断(truncation),使其长度不超过max_len
  • 注意力掩码 :生成一个注意力掩码(attention mask),告诉模型哪些部分是有效的(1),哪些是填充的(0)。

最终,这个函数会返回处理后的对话数据,包括input_ids(输入的 token ID 序列)、labels(目标的 token ID 序列)和attention_mask(注意力掩码)。


2. **make\_supervised\_data\_module** 函数

这个函数的作用是为有监督微调准备数据集,包括训练集和验证集。具体来说,它做了以下几件事:

  • 加载数据 :从指定的 JSON 文件中加载训练数据和验证数据(如果有的话)。
  • 批处理 :使用map函数对数据进行批处理,调用preprocess_function对每条对话进行预处理。
  • 并行处理 :如果配置了并行处理参数(preprocessing_num_workers),函数会并行处理数据,加快处理速度。
  • 缓存控制 :如果配置了缓存控制参数(overwrite_cache),函数会决定是否重新生成缓存文件。
  • 格式转换 :将处理后的数据集转换为 PyTorch 张量格式,方便模型直接使用。

最终,这个函数会返回一个字典,包含训练集和验证集(如果有的话)。


这两个函数的作用可以简单概括为:

  • preprocess_function:将对话数据转换为模型能理解的格式,包括角色标记、分词、填充、截断和损失计算。
  • make_supervised_data_module:加载和处理训练数据及验证数据,为有监督微调做好准备。

如果你要做公众号笔记,可以用更口语化的方式来解释:

想象一下,你有一个对话数据集,里面有用户和助手的对话。但是,模型并不懂人类的语言,它只能理解数字(token ID)。所以,我们需要把这些对话转换成模型能理解的格式。preprocess_function就是干这个事的,它会给对话加上角色标记,把文字变成数字,还会告诉模型哪些部分需要学习(计算损失)。

然后,make_supervised_data_module会帮我们把数据集准备好,包括训练数据和验证数据。它会批量处理数据,让模型可以快速学习。最后,我们把这些数据交给模型,让它变得更聪明!

3. 使用Belle多轮对话数据

数据集链接: https://huggingface.co/datasets/BelleGroup/train\_3.5M\_CN

内容:

包含约350万条由BELLE项目生成的中文指令数据。

样例


            
              
{  
    "id":"66623028",  
    "conversations":[  
        {  
            "from":"human",  
            "value":"归纳介绍三种不同类型的电影题材"  
        },  
        {  
            "from":"assistant",  
            "value":"1. 动作片:动作片通常强调动作和战斗场面,故事情节紧凑,充满了紧张和兴奋的氛围。主角一般是具有超凡实力或特殊技能的英雄人物,往往会与反派角色进行一系列激烈的格斗,追逐和爆破等酷炫场面。例如《速度与激情》和《复仇者联盟》等。\n2. 爱情片:爱情片是以爱情为主题的电影,着重呈现角色之间的感情纠葛,展现了爱情的美好和悲壮。情节与角色之间的发展令人深思,增加了观众的感性体验。这种电影的故事情节主要以人类与人类之间的情感变化为主题。例如《泰坦尼克号》和《让子弹飞》等。\n3. 科幻片:科幻片是以未来科技和未知世界为主题的电影,通常包括时间旅行、外星人、虚拟现实、未来社会等各种奇幻的元素。这种电影描绘了一种比现实更加宏伟、更加神秘和惊奇的世界。例如《星际穿越》和《发条橙》等。"  
        }  
    ]  
}  
{  
    "id":"18205437",  
    "conversations":[  
        {  
            "from":"human",  
            "value":"根据给定的食材,编写一道美食菜谱。\n鸡腿肉、蘑菇、洋葱"  
        },  
        {  
            "from":"assistant",  
            "value":"鸡肉炒蘑菇洋葱\n食材:\n- 鸡腿肉 400克,去骨去皮切成小块\n- 蘑菇 200克,切片\n- 洋葱 1个,切丝\n- 生姜 适量,切丝\n- 酱油 2勺\n- 料酒 1勺\n- 盐 适量\n- 白胡椒粉 适量\n- 植物油 适量\n做法:\n1. 在一个碗里放入切好的鸡腿肉块,加入酱油、料酒和适量的白胡椒粉,搅拌均匀,腌制10分钟。\n2. 热锅凉油,加入姜丝翻炒,放入腌好的鸡肉块翻炒至变色。\n3. 加入洋葱丝和蘑菇,翻炒2-3分钟至蘑菇变软。\n4. 加入适量的盐和少量的水,盖上锅盖焖煮5分钟直至鸡肉熟透。\n5. 最后翻炒几下,使汤汁变浓稠即可。\n温馨提示:\n1. 在腌制鸡肉时,如果想让肉质更嫩滑,可以加入少量的淀粉和蛋清。\n2. 可以根据个人口味加入其他调料,如蒜末、辣椒等。\n3. 洋葱和蘑菇可以用其他蔬菜代替,如青椒、胡萝卜等。"  
        },  
        {  
            "from":"human",  
            "value":"太好了,我会试着照着你教的做一次,但是我最近买了一部新相机,想让你推荐一些好的拍照地点。"  
        },  
        {  
            "from":"assistant",  
            "value":"当地的风景区、公园、景点等等都是比较适合拍照的地点。如果你想要一些特别的地方,可以考虑一些建筑群,街道巷子、咖啡馆、书店等具有文艺气息的地方。"  
        },  
        {  
            "from":"human",  
            "value":"嗯,这些地方都很不错,但是我还想找一些可以体验当地文化和风俗的地点,你有什么好的建议吗?"  
        },  
        {  
            "from":"assistant",  
            "value":"如果你想要更深入地了解当地文化和风俗,可以考虑参加一些游学活动,比如参观当地的博物馆、参加民俗节庆、尝试当地美食等等。此外,也可以去一些小城镇,感受那里的古朴风情。"  
        }  
    ]  
}  

          

字段:

  • id: 数据id
  • conversations: 数据内容,以对话形式给出,包括多轮和单轮对话的数据

我们以Belle对轮对话数据为例,下面我们看看数据集什么样子


            
              
{'conversations': [{'from': 'user',  
   'value': '总结下面这段文本的摘要,随着科技的飞速发展,我们的生活方式也在悄然改变。智能手机、人工智能、物联网等科技产品的出现,为我们的日常生活带来了更多便利和舒适。比如,我们可以通过智能手机随时随地地获取信息,控制家庭设备,同时感受着人工智能为我们带来的智能化之便。但是,科技进步带来的便利也会对生活形成某种影响,比如,冲击传统行业和职业,改变人们的生产和消费模式。'},  
  {'from': 'assistant',  
   'value': '科技的不断进步已经改变了我们的生活方式,给我们带来了更多的便利和舒适。但是,科技的发展也带来了一定的负面影响,如对传统行业产生冲击,改变人们的生产和消费模式等。因此,我们应该在享受科技带来的便利的同时,也要注意保护传统行业的发展和照顾一些可能被科技发展边缘化的人们。'}],  
 'id': 'identity\_0'}  

          

为了方便演示,我们这里使用267条数据


            
              
import json  
with open("Belle\_sampled\_qwen.json",'r',encoding="utf-8") as f:  
    data=json.load(f)  

          

构建Dataset


            
              
ds=datasets.Dataset.from_json("Belle\_sampled\_qwen.json")  
ds  

          

            
              
Dataset({  
    features: ['conversations', 'id'],  
    num_rows: 267  
})  

          

加载分词器,我们尝试对数据集进行处理


            
              
tokenizer=transformers.AutoTokenizer.from_pretrained("/home/jovyan/codes/llms/Qwen2.5-0.5B-Instruct/")  
tokenizer  

          

picture.image

处理完的数据是这个格式


            
              
ds  
Dataset({  
    features: ['input\_ids', 'labels', 'attention\_mask'],  
    num_rows: 267  
})  

          

picture.image

参数设置


            
              
@dataclass  
class ModelArguments:  
    model_name_or_path: Optional[str] = field(default="/home/jovyan/codes/llms/Qwen2.5-0.5B-Instruct/")  
  
  
@dataclass  
class DataArguments:  
    data_path: str = field(  
        default="Belle\_sampled\_qwen.json", metadata={"help": "Path to the training data."}  
    )  
    eval_data_path: str = field(  
        default=None, metadata={"help": "Path to the evaluation data."}  
    )  
    lazy_preprocess: bool = False  
  
  
@dataclass  
class TrainingArguments(transformers.TrainingArguments):  
    cache_dir: Optional[str] = field(default=None)  
    optim: str = field(default="adamw\_torch")  
    model_max_length: int = field(  
        default=8192,  
        metadata={  
            "help": "Maximum sequence length. Sequences will be right padded (and possibly truncated)."  
        },  
    )  
    use_lora: bool = False  
      
  
@dataclass  
class LoraArguments:  
    lora_r: int = 64  
    lora_alpha: int = 16  
    lora_dropout: float = 0.05  
    lora_target_modules: List[str] = field(  
        default_factory=lambda: ["q\_proj", "o\_proj", "k\_proj", "v\_proj", "gate\_proj", "up\_proj", "down\_proj"]  
    )  
    lora_weight_path: str = ""  
    lora_bias: str = "none"  
    q_lora: bool = False  

          

1. 模型参数


            
              
model_args=ModelArguments(  
    model_name_or_path="/home/jovyan/codes/llms/Qwen2.5-0.5B-Instruct/"  
)  
model_args  
ModelArguments(model_name_or_path='/home/jovyan/codes/llms/Qwen2.5-0.5B-Instruct/')  
数据参数  
  
data_args=DataArguments(  
    data_path="Belle\_sampled\_qwen.json"  
)  
data_args  

          

输出如下:


            
              
DataArguments(data_path='Belle\_sampled\_qwen.json', eval_data_path=None, lazy_preprocess=False)  

          

2. 训练参数


            
              
training_args = TrainingArguments(  
    output_dir="output/qwen2.5-0.5b-belle-qlora",  
    overwrite_output_dir=True,  
    report_to="none",  
    num_train_epochs=1,  
    per_device_train_batch_size=2,  
    gradient_accumulation_steps=1,  
    per_device_eval_batch_size=2,  
    logging_steps=10,  
    evaluation_strategy="no",  
    save_strategy="no",  
    model_max_length=128,  
    fp16=True,  
    learning_rate=2e-4,  
    ddp_find_unused_parameters=False,  
    save_total_limit=3,  
    load_best_model_at_end=True,  
    use_lora=False  
)  

          

输出如下:


            
              
training_args  
TrainingArguments(output_dir='output/qwen2.5-0.5b-belle-qlora', overwrite_output_dir=True, do_train=False, do_eval=False, do_predict=False, eval_strategy=<IntervalStrategy.NO: 'no'>, prediction_loss_only=False, per_device_train_batch_size=2, per_device_eval_batch_size=2, per_gpu_train_batch_size=None, per_gpu_eval_batch_size=None, gradient_accumulation_steps=1, eval_accumulation_steps=None, eval_delay=0, torch_empty_cache_steps=None, learning_rate=0.0002, weight_decay=0.0, adam_beta1=0.9, adam_beta2=0.999, adam_epsilon=1e-08, max_grad_norm=1.0, num_train_epochs=1, max_steps=-1, lr_scheduler_type=<SchedulerType.LINEAR: 'linear'>, lr_scheduler_kwargs={}, warmup_ratio=0.0, warmup_steps=0, log_level='passive', log_level_replica='warning', log_on_each_node=True, logging_dir='output/qwen2.5-0.5b-belle-qlora/runs/Dec20\_07-26-30\_gomate-0', logging_strategy=<IntervalStrategy.STEPS: 'steps'>, logging_first_step=False, logging_steps=10, logging_nan_inf_filter=True, save_strategy=<IntervalStrategy.NO: 'no'>, save_steps=500, save_total_limit=3, save_safetensors=True, save_on_each_node=False, save_only_model=False, restore_callback_states_from_checkpoint=False, no_cuda=False, use_cpu=False, use_mps_device=False, seed=42, data_seed=None, jit_mode_eval=False, use_ipex=False, bf16=False, fp16=True, fp16_opt_level='O1', half_precision_backend='auto', bf16_full_eval=False, fp16_full_eval=False, tf32=None, local_rank=0, ddp_backend=None, tpu_num_cores=None, tpu_metrics_debug=False, debug=[], dataloader_drop_last=False, eval_steps=None, dataloader_num_workers=0, dataloader_prefetch_factor=None, past_index=-1, run_name='output/qwen2.5-0.5b-belle-qlora', disable_tqdm=False, remove_unused_columns=True, label_names=None, load_best_model_at_end=True, metric_for_best_model='loss', greater_is_better=False, ignore_data_skip=False, fsdp=[], fsdp_min_num_params=0, fsdp_config={'min\_num\_params': 0, 'xla': False, 'xla\_fsdp\_v2': False, 'xla\_fsdp\_grad\_ckpt': False}, fsdp_transformer_layer_cls_to_wrap=None, accelerator_config=AcceleratorConfig(split_batches=False, dispatch_batches=None, even_batches=True, use_seedable_sampler=True, non_blocking=False, gradient_accumulation_kwargs=None, use_configured_state=False), deepspeed=None, label_smoothing_factor=0.0, optim=<OptimizerNames.ADAMW_TORCH: 'adamw\_torch'>, optim_args=None, adafactor=False, group_by_length=False, length_column_name='length', report_to=[], ddp_find_unused_parameters=False, ddp_bucket_cap_mb=None, ddp_broadcast_buffers=None, dataloader_pin_memory=True, dataloader_persistent_workers=False, skip_memory_metrics=True, use_legacy_prediction_loop=False, push_to_hub=False, resume_from_checkpoint=None, hub_model_id=None, hub_strategy=<HubStrategy.EVERY_SAVE: 'every\_save'>, hub_token=None, hub_private_repo=False, hub_always_push=False, gradient_checkpointing=False, gradient_checkpointing_kwargs=None, include_inputs_for_metrics=False, eval_do_concat_batches=True, fp16_backend='auto', evaluation_strategy='no', push_to_hub_model_id=None, push_to_hub_organization=None, push_to_hub_token=None, mp_parameters='', auto_find_batch_size=False, full_determinism=False, torchdynamo=None, ray_scope='last', ddp_timeout=1800, torch_compile=False, torch_compile_backend=None, torch_compile_mode=None, dispatch_batches=None, split_batches=None, include_tokens_per_second=False, include_num_input_tokens_seen=False, neftune_noise_alpha=None, optim_target_modules=None, batch_eval_metrics=False, eval_on_start=False, use_liger_kernel=False, eval_use_gather_object=False, cache_dir=None, model_max_length=128, use_lora=False)  

          

3. Lora参数


            
              
lora_args=LoraArguments(  
    lora_r=16,  
    lora_alpha=32,  
    lora_target_modules=['q\_proj', 'o\_proj', 'k\_proj', 'v\_proj', 'gate\_proj', 'up\_proj', 'down\_proj']  
)  

          

输出如下:


            
              
lora_args  
LoraArguments(lora_r=16, lora_alpha=32, lora_dropout=0.05, lora_target_modules=['q\_proj', 'o\_proj', 'k\_proj', 'v\_proj', 'gate\_proj', 'up\_proj', 'down\_proj'], lora_weight_path='', lora_bias='none', q_lora=False)  

          

加载模型

1. 加载Base模型


            
              
# Set RoPE scaling factor  
config = transformers.AutoConfig.from_pretrained(  
    model_args.model_name_or_path,  
    cache_dir=training_args.cache_dir,  
    trust_remote_code=True,  
)  
config.use_cache = False  
config  

          

我们可以看到Qwen2.5-0.5B-Instruct模型的配置:


            
              
Qwen2Config {  
  "\_name\_or\_path": "/home/jovyan/codes/llms/Qwen2.5-0.5B-Instruct/",  
  "architectures": [  
    "Qwen2ForCausalLM"  
  ],  
  "attention\_dropout": 0.0,  
  "bos\_token\_id": 151643,  
  "eos\_token\_id": 151645,  
  "hidden\_act": "silu",  
  "hidden\_size": 896,  
  "initializer\_range": 0.02,  
  "intermediate\_size": 4864,  
  "max\_position\_embeddings": 32768,  
  "max\_window\_layers": 21,  
  "model\_type": "qwen2",  
  "num\_attention\_heads": 14,  
  "num\_hidden\_layers": 24,  
  "num\_key\_value\_heads": 2,  
  "rms\_norm\_eps": 1e-06,  
  "rope\_scaling": null,  
  "rope\_theta": 1000000.0,  
  "sliding\_window": null,  
  "tie\_word\_embeddings": true,  
  "torch\_dtype": "bfloat16",  
  "transformers\_version": "4.45.2",  
  "use\_cache": false,  
  "use\_sliding\_window": false,  
  "vocab\_size": 151936  
}Qwen2Config {  
  "\_name\_or\_path": "/home/jovyan/codes/llms/Qwen2.5-0.5B-Instruct/",  
  "architectures": [  
    "Qwen2ForCausalLM"  
  ],  
  "attention\_dropout": 0.0,  
  "bos\_token\_id": 151643,  
  "eos\_token\_id": 151645,  
  "hidden\_act": "silu",  
  "hidden\_size": 896,  
  "initializer\_range": 0.02,  
  "intermediate\_size": 4864,  
  "max\_position\_embeddings": 32768,  
  "max\_window\_layers": 21,  
  "model\_type": "qwen2",  
  "num\_attention\_heads": 14,  
  "num\_hidden\_layers": 24,  
  "num\_key\_value\_heads": 2,  
  "rms\_norm\_eps": 1e-06,  
  "rope\_scaling": null,  
  "rope\_theta": 1000000.0,  
  "sliding\_window": null,  
  "tie\_word\_embeddings": true,  
  "torch\_dtype": "bfloat16",  
  "transformers\_version": "4.45.2",  
  "use\_cache": false,  
  "use\_sliding\_window": false,  
  "vocab\_size": 151936  
}  

          

加载模型权重


            
              
# Load model and tokenizer  
model = transformers.AutoModelForCausalLM.from_pretrained(  
    model_args.model_name_or_path,  
    config=config,  
    cache_dir=training_args.cache_dir,  
    device_map="cuda",  
    trust_remote_code=True,  
    quantization_config=GPTQConfig(  
        bits=4, disable_exllama=True  
    )  
    if training_args.use_lora and lora_args.q_lora else None,  
    # low\_cpu\_mem\_usage  
)  

          

模型结构如下:


            
              
Qwen2ForCausalLM(  
  (model): Qwen2Model(  
    (embed_tokens): Embedding(151936, 896)  
    (layers): ModuleList(  
      (0-23): 24 x Qwen2DecoderLayer(  
        (self_attn): Qwen2SdpaAttention(  
          (q_proj): Linear(in_features=896, out_features=896, bias=True)  
          (k_proj): Linear(in_features=896, out_features=128, bias=True)  
          (v_proj): Linear(in_features=896, out_features=128, bias=True)  
          (o_proj): Linear(in_features=896, out_features=896, bias=False)  
          (rotary_emb): Qwen2RotaryEmbedding()  
        )  
        (mlp): Qwen2MLP(  
          (gate_proj): Linear(in_features=896, out_features=4864, bias=False)  
          (up_proj): Linear(in_features=896, out_features=4864, bias=False)  
          (down_proj): Linear(in_features=4864, out_features=896, bias=False)  
          (act_fn): SiLU()  
        )  
        (input_layernorm): Qwen2RMSNorm((896,), eps=1e-06)  
        (post_attention_layernorm): Qwen2RMSNorm((896,), eps=1e-06)  
      )  
    )  
    (norm): Qwen2RMSNorm((896,), eps=1e-06)  
    (rotary_emb): Qwen2RotaryEmbedding()  
  )  
  (lm_head): Linear(in_features=896, out_features=151936, bias=False)  
)  

          

3. QLoRA 是什么

picture.image

QLoRA 是一种高效的模型微调方法,特别适合在资源有限的情况下对大型语言模型(LLM)进行微调。它的核心思想是通过量化(Quantization)和低秩适应(LoRA,Low-Rank Adaptation)来减少计算资源和内存的消耗,同时保持模型的性能。


QLoRA 的两个核心技术

  1. 量化(Quantization)
  • 量化是一种将模型参数从高精度(比如 32 位浮点数)压缩到低精度(比如 4 位或 8 位整数)的技术。
  • 通过量化,模型的内存占用和计算量会大幅减少,从而可以在更小的硬件(如 GPU)上运行更大的模型。
  • 例如,一个 13B 参数的模型,经过 4 位量化后,内存占用可以从几十 GB 降低到几 GB。
  1. 低秩适应(LoRA)
  • LoRA 是一种微调技术,它通过在模型的某些层中引入低秩矩阵来调整模型的行为。
  • 相比于全参数微调(fine-tuning),LoRA 只需要训练少量的额外参数(低秩矩阵),而不是整个模型的参数。
  • 这种方法不仅节省了内存和计算资源,还能显著加快训练速度。

lora参数设置如下:


            
              
lora_config = LoraConfig(  
    r=lora_args.lora_r,  
    lora_alpha=lora_args.lora_alpha,  
    target_modules=lora_args.lora_target_modules,  
    lora_dropout=lora_args.lora_dropout,  
    bias=lora_args.lora_bias,  
    task_type="CAUSAL\_LM",  
)  
lora_config  
```lora参数设置如下:  

          

lora_config = LoraConfig( r=lora_args.lora_r, lora_alpha=lora_args.lora_alpha, target_modules=lora_args.lora_target_modules, lora_dropout=lora_args.lora_dropout, bias=lora_args.lora_bias, task_type="CAUSAL_LM", ) lora_config


            
              
  
```python  
model = prepare_model_for_kbit_training(model)  # QLoRA 的第一步:准备模型  
model = get_peft_model(model, lora_config)      # QLoRA 的第二步:应用 LoRA  

          

这两行代码的作用如下:

  1. prepare_model_for_kbit_training(model)
  • 这个函数会对模型进行量化,将模型的参数从高精度转换为低精度(比如从 16 位或 32 位浮点数转换为 4 位或 8 位整数)。
  • 同时,它还会对模型进行一些必要的调整,以确保在低精度下训练的稳定性。
  1. get_peft_model(model, lora_config)
  • 这个函数会将 LoRA 技术应用到模型中。
  • lora_config是一个配置对象,定义了 LoRA 的具体参数,比如哪些层需要应用 LoRA、低秩矩阵的秩(rank)等。
  • 通过这一步,模型会引入一些额外的低秩矩阵,这些矩阵会在微调过程中被训练,而原始模型的参数保持不变。

QLoRA 的优势如下:

  1. 节省内存
  • 通过量化,模型的内存占用大幅减少,使得在资源有限的硬件上也能运行大型模型。
  • 例如,一个 13B 参数的模型,经过 4 位量化后,内存占用可以从几十 GB 降低到几 GB。
  1. 加速训练
  • 由于只需要训练少量的低秩矩阵,而不是整个模型的参数,训练速度会显著加快。
  1. 保持性能
  • QLoRA 通过量化和 LoRA 的结合,能够在节省资源的同时,保持模型的性能,甚至在一些任务上表现更好。

模型训练


            
              
tokenizer = transformers.AutoTokenizer.from_pretrained(  
        model_args.model_name_or_path,  
        cache_dir=training_args.cache_dir,  
        model_max_length=training_args.model_max_length,  
        padding_side="right",  
        use_fast=False,  
        trust_remote_code=True,  
    )  
tokenizer.pad_token_id  
# print\_trainable\_parameters 通常用于检查模型中有多少参数是可训练的(即在训练过程中会被更新的),以及有多少参数是不可训练的(即在训练过程中保持不变的)。  
model.print_trainable_parameters()  
data_module = make_supervised_data_module(  
    tokenizer=tokenizer, data_args=data_args, max_len=training_args.model_max_length  
)  
# Start trainner  
trainer = Trainer(  
    model=model,   
    tokenizer=tokenizer,   
    args=training_args,   
    **data_module  
)  
  
trainer.train()  

          

训练日志如下:picture.image

合并权重


            
              
from transformers import AutoModelForCausalLM  
from peft import PeftModel  
import torch  
# 加载基础权重  
model = AutoModelForCausalLM.from_pretrained(model_args.model_name_or_path, torch_dtype=torch.float16, device_map="auto", trust_remote_code=True)  
  
# 加载lora权重  
model = PeftModel.from_pretrained(model, "output/qwen2.5-0.5b-belle-qlora/checkpoint-134/")  
merged_model = model.merge_and_unload()  
merged_model.save_pretrained("output\_qwen\_merged", max_shard_size="2048MB", safe_serialization=True)  
  
# 保存分词器  
tokenizer.save_pretrained("output\_qwen\_merged")  

          

测试模型


            
              
from modelscope import AutoModelForCausalLM, AutoTokenizer  
  
model_name = "output\_qwen\_merged"  
  
model = AutoModelForCausalLM.from_pretrained(  
    model_name,  
    torch_dtype="auto",  
    device_map="auto"  
)  
tokenizer = AutoTokenizer.from_pretrained(model_name)  
  
prompt = "帮我重新润色下面这段文章:“中美贸易战主要影响因素分析与展望”,作者:高雪松,来源:《国际商务》2019年第6期。"  
messages = [  
    {"role": "system", "content": "You are a helpful assistant."},  
    {"role": "user", "content": prompt}  
]  
text = tokenizer.apply_chat_template(  
    messages,  
    tokenize=False,  
    add_generation_prompt=True  
)  
model_inputs = tokenizer([text], return_tensors="pt").to(model.device)  
  
generated_ids = model.generate(  
    **model_inputs,  
    max_new_tokens=512  
)  
generated_ids = [  
    output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)  
]  
  
response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]  
print(response)  

          

测试结果如下:


            
              
这篇文章的作者是高雪松,该文章从中美贸易问题的主要影响因素进行了详细的分析和展望。  

          

参考资料

  • QwenLM/Qwen
  • Qwen7b微调保姆级教程

相关文章

【LLM训练系列01】Qlora如何加载、训练、合并大模型

【LLM训练系列02】如何找到一个大模型Lora的target_modules

【LLM训练系列03】关于大模型训练常见概念讲解

0
0
0
0
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论