【Datawhale AI夏令营】TASK-4-2024/8/20

TASK4 微调原理与实践直播

1 引言

1.1 大模型微调技术简介

模型微调也被称为指令微调(Instruction Tuning)或者有监督微调(Supervised Fine-tuning, SFT),该方法利用成对的任务输入与预期输出数据,训练模型学会以问答的形式解答问题,从而解锁其任务解决潜能。经过指令微调后,大语言模型能够展现出较强的指令遵循能力,可以通过零样本学习的方式解决多种下游任务。

然而,值得注意的是,指令微调并非无中生有地传授新知,而是更多地扮演着催化剂的角色,激活模型内在的潜在能力,而非单纯地灌输信息。

相较于预训练所需的海量数据,指令微调所需数据量显著减少,从几十万到上百万条不等的数据,均可有效激发模型的通用任务解决能力,甚至有研究表明,少量高质量的指令数据(数千至数万条)亦能实现令人满意的微调效果。这不仅降低了对计算资源的依赖,也提升了微调的灵活性与效率。

1.2 轻量化微调技术简介

然而,由于大模型的参数量巨大, 进行全量参数微调需要消耗非常多的算力。为了解决这一问题,研究者提出了参数高效微调(Parameter-efficient Fine-tuning),也称为轻量化微调 (Lightweight Fine-tuning),这些方法通过训练极少的模型参数,同时保证微调后的模型表现可以与全量微调相媲美。

常用的轻量化微调技术有LoRA、Adapter 和 Prompt Tuning。

1.3 LoRA技术简介

LoRA 是通过低秩矩阵分解,在原始矩阵的基础上增加一个旁路矩阵,然后只更新旁路矩阵的参数。

2 实战

2.2 模型下载

from modelscope import snapshot_download
model_dir = snapshot_download('IEITYuan/Yuan2-2B-Mars-hf', cache_dir='.')

2.3 数据处理

# 导入环境
import torch
import pandas as pd
from datasets import Dataset
from transformers import AutoTokenizer, AutoModelForCausalLM, DataCollatorForSeq2Seq, TrainingArguments, Trainer

运行 2.3 数据处理 下面的单元格。

我们使用 pandas 进行数据读取,然后转成 Dataset 格式:

# 读取数据
df = pd.read_json('./data.json')
ds = Dataset.from_pandas(df)

# 查看数据
len(ds)
ds[:1]
{'input': ['# 任务描述\n假设你是一个AI简历助手,能从简历中识别出所有的命名实体,并以json格式返回结果。\n\n# 任务要求\n实体的类别包括:姓名、国籍、种族、职位、教育背景、专业、组织名、地名。\n返回的json格式是一个字典,其中每个键是实体的类别,值是一个列表,包含实体的文本。\n\n# 样例\n输入:\n张三,男,中国籍,工程师\n输出:\n{"姓名": ["张三"], "国籍": ["中国"], "职位": ["工程师"]}\n\n# 当前简历\n高勇:男,中国国籍,无境外居留权,\n\n# 任务重述\n请参考样例,按照任务要求,识别出当前简历中所有的命名实体,并以json格式返回结果。'],
 'output': ['{"姓名": ["高勇"], "国籍": ["中国国籍"]}']}

然后我们需要加载 tokenizer:

# 加载 tokenizer
path = './IEITYuan/Yuan2-2B-Mars-hf'

tokenizer = AutoTokenizer.from_pretrained(path, add_eos_token=False, add_bos_token=False, eos_token='<eod>')
tokenizer.add_tokens(['<sep>', '<pad>', '<mask>', '<predict>', '<FIM_SUFFIX>', '<FIM_PREFIX>', '<FIM_MIDDLE>','<commit_before>','<commit_msg>','<commit_after>','<jupyter_start>','<jupyter_text>','<jupyter_code>','<jupyter_output>','<empty_output>'], special_tokens=True)
tokenizer.pad_token = tokenizer.eos_token

为了完成模型训练,需要完成数据处理,这里我们定义了一个数据处理函数 process_func

# 定义数据处理函数
def process_func(example):
    MAX_LENGTH = 384    # Llama分词器会将一个中文字切分为多个token,因此需要放开一些最大长度,保证数据的完整性

    instruction = tokenizer(f"{example['input']}<sep>")
    response = tokenizer(f"{example['output']}<eod>")
    input_ids = instruction["input_ids"] + response["input_ids"]
    attention_mask = [1] * len(input_ids) 
    labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] # instruction 不计算loss

    if len(input_ids) > MAX_LENGTH:  # 做一个截断
        input_ids = input_ids[:MAX_LENGTH]
        attention_mask = attention_mask[:MAX_LENGTH]
        labels = labels[:MAX_LENGTH]

    return {
        "input_ids": input_ids,
        "attention_mask": attention_mask,
        "labels": labels
    }

具体来说,需要使用tokenizer将文本转成id,同时将 inputoutput 拼接,组成 input_idsattention_mask

这里我们可以看到,源大模型需要在 input 后面添加一个特殊的token <sep>, 在 output 后面添加一个特殊的token <eod>

同时,为了防止数据超长,还有做一个截断处理。

然后使用 map 函数对整个数据集进行预处理:

# 处理数据集
tokenized_id = ds.map(process_func, remove_columns=ds.column_names)
tokenized_id
Dataset({
    features: ['input_ids', 'attention_mask', 'labels'],
    num_rows: 200
})

处理完成后,我们使用tokenizer的decode函数,将id转回文本,进行最后的检查:

# 数据检查
tokenizer.decode(tokenized_id[0]['input_ids'])
'# 任务描述\n假设你是一个AI简历助手,能从简历中识别出所有的命名实体,并以json格式返回结果。\n\n# 任务要求\n实体的类别包括:姓名、国籍、种族、职位、教育背景、专业、组织名、地名。\n返回的json格式是一个字典,其中每个键是实体的类别,值是一个列表,包含实体的文本。\n\n# 样例\n输入:\n张三,男,中国籍,工程师\n输出:\n{"姓名": ["张三"], "国籍": ["中国"], "职位": ["工程师"]}\n\n# 当前简历\n高勇:男,中国国籍,无境外居留权,\n\n# 任务重述\n请参考样例,按照任务要求,识别出当前简历中所有的命名实体,并以json格式返回结果。<sep> {"姓名": ["高勇"], "国籍": ["中国国籍"]}<eod>'
tokenizer.decode(list(filter(lambda x: x != -100, tokenized_id[0]["labels"])))
'{"姓名": ["高勇"], "国籍": ["中国国籍"]}<eod>'

2.4 训练模型

首先我们需要加载源大模型参数,然后打印模型结构:

# 模型加载
model = AutoModelForCausalLM.from_pretrained(path, device_map="auto", torch_dtype=torch.bfloat16, trust_remote_code=True)
model
YuanForCausalLM(
  (model): YuanModel(
    (embed_tokens): Embedding(135040, 2048, padding_idx=77185)
    (layers): ModuleList(
      (0-23): 24 x YuanDecoderLayer(
        (self_attn): YuanAttention(
          (v_proj): Linear(in_features=2048, out_features=2048, bias=False)
          (o_proj): Linear(in_features=2048, out_features=2048, bias=False)
          (rotary_emb): YuanRotaryEmbedding()
          (lf_gate): LocalizedFiltering(
            (conv1): Conv2d(2048, 1024, kernel_size=(2, 1), stride=(1, 1), padding=(1, 0))
            (conv2): Conv2d(1024, 2048, kernel_size=(2, 1), stride=(1, 1), padding=(1, 0))
            (output_layernorm): YuanRMSNorm()
          )
          (q_proj): Linear(in_features=2048, out_features=2048, bias=False)
          (k_proj): Linear(in_features=2048, out_features=2048, bias=False)
        )
        (mlp): YuanMLP(
          (up_proj): Linear(in_features=2048, out_features=8192, bias=False)
          (gate_proj): Linear(in_features=2048, out_features=8192, bias=False)
          (down_proj): Linear(in_features=8192, out_features=2048, bias=False)
          (act_fn): SiLU()
        )
        (input_layernorm): YuanRMSNorm()
        (post_attention_layernorm): YuanRMSNorm()
      )
    )
    (norm): YuanRMSNorm()
  )
  (lm_head): Linear(in_features=2048, out_features=135040, bias=False)
)

可以看到,源大模型中包含24层 YuanDecoderLayer,每层中包含 self_attnmlplayernorm

另外为了进行模型使用训练,需要先执行 model.enable_input_require_grads()

model.enable_input_require_grads() # 开启gradient_checkpointing时,要执行该方法

最后,我们打印下模型的数据类型,可以看到是 torch.bfloat16

# 查看模型数据类型
model.dtype

在本节中,我们使用Lora进行轻量化微调,首先需要配置 LoraConfig

# 配置Lora
from peft import LoraConfig, TaskType, get_peft_model

config = LoraConfig(
    task_type=TaskType.CAUSAL_LM, 
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
    inference_mode=False, # 训练模式
    r=8, # Lora 秩
    lora_alpha=32, # Lora alaph,具体作用参见 Lora 原理
    lora_dropout=0.1# Dropout 比例
)
config
LoraConfig(peft_type=<PeftType.LORA: 'LORA'>, auto_mapping=None, base_model_name_or_path=None, revision=None, task_type=<TaskType.CAUSAL_LM: 'CAUSAL_LM'>, inference_mode=False, r=8, target_modules={'q_proj', 'down_proj', 'o_proj', 'up_proj', 'v_proj', 'gate_proj', 'k_proj'}, lora_alpha=32, lora_dropout=0.1, fan_in_fan_out=False, bias='none', use_rslora=False, modules_to_save=None, init_lora_weights=True, layers_to_transform=None, layers_pattern=None, rank_pattern={}, alpha_pattern={}, megatron_config=None, megatron_core='megatron.core', loftq_config={}, use_dora=False, layer_replication=None)

然后构建一个 PeftModel:

# 构建PeftModel
model = get_peft_model(model, config)
model
PeftModelForCausalLM(
  (base_model): LoraModel(
    (model): YuanForCausalLM(
      (model): YuanModel(
        (embed_tokens): Embedding(135040, 2048, padding_idx=77185)
        (layers): ModuleList(
          (0-23): 24 x YuanDecoderLayer(
            (self_attn): YuanAttention(
              (v_proj): lora.Linear(
                (base_layer): Linear(in_features=2048, out_features=2048, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Dropout(p=0.1, inplace=False)
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=2048, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=2048, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
              )
              (o_proj): lora.Linear(
                (base_layer): Linear(in_features=2048, out_features=2048, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Dropout(p=0.1, inplace=False)
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=2048, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=2048, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
              )
              (rotary_emb): YuanRotaryEmbedding()
              (lf_gate): LocalizedFiltering(
                (conv1): Conv2d(2048, 1024, kernel_size=(2, 1), stride=(1, 1), padding=(1, 0))
                (conv2): Conv2d(1024, 2048, kernel_size=(2, 1), stride=(1, 1), padding=(1, 0))
                (output_layernorm): YuanRMSNorm()
              )
              (q_proj): lora.Linear(
                (base_layer): Linear(in_features=2048, out_features=2048, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Dropout(p=0.1, inplace=False)
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=2048, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=2048, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
              )
              (k_proj): lora.Linear(
                (base_layer): Linear(in_features=2048, out_features=2048, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Dropout(p=0.1, inplace=False)
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=2048, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=2048, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
              )
            )
            (mlp): YuanMLP(
              (up_proj): lora.Linear(
                (base_layer): Linear(in_features=2048, out_features=8192, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Dropout(p=0.1, inplace=False)
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=2048, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=8192, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
              )
              (gate_proj): lora.Linear(
                (base_layer): Linear(in_features=2048, out_features=8192, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Dropout(p=0.1, inplace=False)
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=2048, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=8192, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
              )
              (down_proj): lora.Linear(
                (base_layer): Linear(in_features=8192, out_features=2048, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Dropout(p=0.1, inplace=False)
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=8192, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=2048, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
              )
              (act_fn): SiLU()
            )
            (input_layernorm): YuanRMSNorm()
            (post_attention_layernorm): YuanRMSNorm()
          )
        )
        (norm): YuanRMSNorm()
      )
      (lm_head): Linear(in_features=2048, out_features=135040, bias=False)
    )
  )
)

通过 model.print_trainable_parameters(),可以看到需要训练的参数在所有参数中的占比:

# 打印需要训练的参数
model.print_trainable_parameters()
trainable params: 9,043,968 || all params: 2,097,768,448 || trainable%: 0.4311

然后,我们设置训练参数 TrainingArguments:

# 设置训练参数
args = TrainingArguments(
    output_dir="./output/Yuan2.0-2B_lora_bf16",
    per_device_train_batch_size=12,
    gradient_accumulation_steps=1,
    logging_steps=1,
    save_strategy="epoch",
    num_train_epochs=3,
    learning_rate=5e-5,
    save_on_each_node=True,
    gradient_checkpointing=True,
    bf16=True
)
  • output_dir:
    • “./output/Yuan2.0-2B_lora_bf16” 指定了训练的输出目录,所有训练日志、模型检查点和其他输出文件都将保存在此目录下。
  • per_device_train_batch_size:
    • 12 表示每个训练设备(GPU/TPU)在每个训练步骤中处理的样本数量。较小的批次大小可以帮助减少内存消耗,而较大的批次大小则可以提高训练效率。
  • gradient_accumulation_steps:
    • 1 表示每个训练步骤后立即更新模型的权重。如果设置为大于1的值,模型将在执行权重更新之前累积多个步骤的梯度,这相当于增大了有效批次大小,而不增加内存消耗。
  • logging_steps:
    • 1 表示每执行一个训练步骤后都会记录日志。这包括损失值、学习率等信息。如果设置为更大的数,日志记录将不那么频繁。
  • save_strategy:
    • “epoch” 表示模型将在每个训练轮数(epoch)结束时保存。其他选项包括 “steps”,表示根据指定的步骤间隔保存。
  • num_train_epochs:
    • 3 表示模型将在训练数据上完整地迭代3次。增加训练轮数可以提高模型的性能,但也可能增加过拟合的风险。
  • learning_rate:
    • 5e-5 表示初始的学习率,用于调整模型权重。学习率是深度学习中最重要的超参数之一,它决定了权重更新的幅度。
  • save_on_each_node:
    • True 表示在分布式训练中,每个节点都将保存模型的检查点。这对于故障恢复和多节点训练很有用。
  • gradient_checkpointing:
    • True 表示启用梯度检查点(gradient checkpointing),这是一种内存优化技术,可以减少训练过程中的内存消耗,但可能会增加额外的计算成本。
  • bf16:
    • True 表示使用 Brain Floating Point (BF16) 精度进行训练,这是16位浮点格式的一种,比标准的FP16具有更好的数值稳定性,同时仍然可以减少内存使用和提高训练速度。

同时初始化一个 Trainer:

# 初始化Trainer
trainer = Trainer(
    model=model,
    args=args,
    train_dataset=tokenized_id,
    data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
)

最后运行 trainer.train() 执行模型训练。

# 模型训练
trainer.train()

在训练过程中,会打印模型训练的loss,我们可以通过loss的降低情况,检查模型是否收敛:

Step	Training Loss
1	1.290000
2	1.350000
3	0.932900
4	0.699000
5	0.582200
6	0.609600
7	0.450300
……
49	0.233200
50	0.166800
51	0.108500

模型训练完成后,会打印训练相关的信息:

TrainOutput(global_step=51, training_loss=0.3522653572407423, metrics={'train_runtime': 86.8022, 'train_samples_per_second': 6.912, 'train_steps_per_second': 0.588, 'total_flos': 1792416895205376.0, 'train_loss': 0.3522653572407423, 'epoch': 3.0})

同时,我们会看到左侧 output 文件夹下出现了3个文件夹,每个文件夹下保存着每个epoch训完的模型。这里,以epoch3为例,可以看到其中保存了训练的config、state、ckpt等信息。

2.5 效果验证

完成模型训练后,我们通过定义一个生成函数 generate(),来进行效果验证:

# 定义生成函数
def generate(prompt):
    prompt = prompt + "<sep>"
    inputs = tokenizer(prompt, return_tensors="pt")["input_ids"].cuda()
    outputs = model.generate(inputs, do_sample=False, max_length=256)
    output = tokenizer.decode(outputs[0])
    print(output.split("<sep>")[-1])

同时定义好输入的prompt template,这个要和训练保持一致。

# 输入prompt template
template = '''
# 任务描述
假设你是一个AI简历助手,能从简历中识别出所有的命名实体,并以json格式返回结果。

# 任务要求
实体的类别包括:姓名、国籍、种族、职位、教育背景、专业、组织名、地名。
返回的json格式是一个字典,其中每个键是实体的类别,值是一个列表,包含实体的文本。

# 样例
输入:
张三,男,中国籍,工程师
输出:
{"姓名": ["张三"], "国籍": ["中国"], "职位": ["工程师"]}

# 当前简历
input_str

# 任务重述
请参考样例,按照任务要求,识别出当前简历中所有的命名实体,并以json格式返回结果。
'''

最后,我们输入样例进行测试:

input_str = '张三,汉族,金融学硕士。'
prompt = template.replace('input_str', input_str).strip()
generate(prompt)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值