4.8 数据预处理
在微调模型之前,我们不能直接使用原始数据集,需要将数据集中的提示转换成模型能够理解的格式。
为了使数据集适配微调流程,这里编写辅助函数来格式化输入数据集。具体来说,就是将对话摘要(即提示-响应对)转换成大型语言模型(LLM)能够识别的明确指令。
- def create_prompt_formats(sample):
- """
- 格式化样本的各种字段('instruction', 'output')
- 然后使用两个换行符将它们连接起来
- :param sample: 样本字典
- """
- INTRO_BLURB = "以下是描述任务的指令。写一个适当完成请求的响应。"
- INSTRUCTION_KEY = "### 指令:总结以下对话。"
- RESPONSE_KEY = "### 输出:"
- END_KEY = "### 结束"
-
- blurb = f"\n{INTRO_BLURB}"
- instruction = f"{INSTRUCTION_KEY}"
- input_context = f"{sample['dialogue']}" if sample["dialogue"] else None
- response = f"{RESPONSE_KEY}\n{sample['summary']}"
- end = f"{END_KEY}"
-
- parts = [part for part in [blurb, instruction, input_context, response, end] if part]
-
- formatted_prompt = "\n\n".join(parts)
- sample["text"] = formatted_prompt
-
- return sample
-
上述函数负责将输入数据转换成提示格式。
接下来,我们会用模型的分词器对这些提示进行处理,将其转换成标记化的形式。我们的目标是生成长度统一的输入序列,这样做有助于微调语言模型,因为它能提升处理效率并降低计算成本。同时,我们还得确保这些序列的长度不超过模型允许的最大标记数。
- from functools import partial
-
- # 来源 https://github.com/databrickslabs/dolly/blob/master/training/trainer.py
- def get_max_length(model):
- conf = model.config
- max_length = None
- for length_setting in ["n_positions", "max_position_embeddings", "seq_length"]:
- max_length = getattr(model.config, length_setting, None)
- if max_length:
- print(f"找到最大长度:{max_length}")
- break
- if not max_length:
- max_length = 1024
- print(f"使用默认最大长度:{max_length}")
- return max_length
-
-
- def preprocess_batch(batch, tokenizer, max_length):
- """
- 分批标记化
- """
- return tokenizer(
- batch["text"],
- max_length=max_length,
- truncation=True,
- )
-
- # 来源 https://github.com/databrickslabs/dolly/blob/master/training/trainer.py
- def preprocess_dataset(tokenizer: AutoTokenizer, max_length: int, seed, dataset):
- """格式化并标记化,使其准备好进行训练
- :param tokenizer (AutoTokenizer): 模型分词器
- :param max_length (int): 分词器发出的最大标记数
- """
-
- # 为每个样本添加提示
- print("预处理数据集...")
- dataset = dataset.map(create_prompt_formats)#, batched=True)
-
- # 对每个批次的数据集应用预处理并移除 'instruction', 'context', 'response', 'category' 字段
- _preprocessing_function = partial(preprocess_batch, max_length=max_length, tokenizer=tokenizer)
- dataset = dataset.map(
- _preprocessing_function,
- batched=True,
- remove_columns=['id', 'topic', 'dialogue', 'summary'],
- )
-
- # 过滤掉输入_ids超过最大长度的样本
- dataset = dataset.filter(lambda sample: len(sample["input_ids"]) < max_length)
-
- # 随机打乱数据集
- dataset = dataset.shuffle(seed=seed)
-
- return dataset
-
通过这些函数的处理,我们的数据集已经准备好进行微调了。
- ## 数据集预处理
- max_length = get_max_length(original_model)
- print(max_length)
-
- train_dataset = preprocess_dataset(tokenizer, max_length,seed, dataset['train'])
- eval_dataset = preprocess_dataset(tokenizer, max_length,seed, dataset['validation'])
-
使用PEFT库中的prepare_model_for_kbit_training方法来准备模型。
通过这个方法,我们对原始模型original_model进行初始化,设置好必要的配置,以便进行QLoRA训练。
接下来,我们配置LoRA参数,以便对基础模型进行微调。
- from peft import LoraConfig, get_peft_model
-
- config = LoraConfig(
- r=32, # 定义适配器的秩
- lora_alpha=32, # 学习权重的缩放因子
- target_modules=[
- 'q_proj', 'k_proj', 'v_proj', 'dense'
- ],
- bias="none",
- lora_dropout=0.05, # 防止过拟合
- task_type="CAUSAL_LM", # 任务类型
- )
-
- # 启用梯度检查点减少内存消耗
- original_model.gradient_checkpointing_enable()
-
- # 获取配置好的PEFT模型
- peft_model = get_peft_model(original_model, config)
-
这里的r(秩)参数控制适配器的复杂度,影响模型的表达能力和计算成本。lora_alpha参数用于调整学习权重,影响LoRA激活的强度。配置完成后,我们可以通过辅助函数查看模型的可训练参数数量。
- print(print_number_of_trainable_model_parameters(peft_model))
-
4.11 训练PEFT适配器
来设置训练参数,并初始化训练器。
- import transformers
- from transformers import TrainingArguments, Trainer, DataCollatorForLanguageModeling
-
- # 设置训练参数
- output_dir = f'./peft-dialogue-summary-training-{str(int(time.time()))}'
- peft_training_args = TrainingArguments(
- output_dir=output_dir,
- warmup_steps=1,
- per_device_train_batch_size=1,
- gradient_accumulation_steps=4,
- max_steps=1000,
- learning_rate=2e-4,
- optim="paged_adamw_8bit",
- logging_steps=25,
- logging_dir="./logs",
- save_strategy="steps",
- save_steps=25,
- evaluation_strategy="steps",
- eval_steps=25,
- do_eval=True,
- gradient_checkpointing=True,
- report_to="none",
- overwrite_output_dir=True,
- group_by_length=True,
- )
-
- # 禁用缓存,准备训练器
- peft_model.config.use_cache = False
- peft_trainer = Trainer(
- model=peft_model,
- args=peft_training_args,
- train_dataset=train_dataset,
- eval_dataset=eval_dataset,
- data_collator=DataCollatorForLanguageModeling(tokenizer, mlm=False),
- )
-
我们计划进行1000步训练,这个数字对于我们的特定数据集应该是合适的。不过,可能需要根据实际情况调整这个数值。现在,可以开始训练了,训练时间会根据超参数的不同而有所差异。
- peft_trainer.train()
-
训练完成后,准备一个用于推理的模型。给原始的Phi-2模型添加一个适配器,并设置为不可训练状态,因为我们只打算用它来做推理。
- import torch
- from transformers import AutoModelForCausalLM, AutoTokenizer
-
- base_model_id = "microsoft/phi-2"
- base_model = AutoModelForCausalLM.from_pretrained(base_model_id)
- eval_tokenizer = AutoTokenizer.from_pretrained(base_model_id, add_bos_token=True)
- eval_tokenizer.pad_token = eval_tokenizer.eos_token
-
- from peft import PeftModel
-
- ft_model = PeftModel.from_pretrained(
- base_model,
- "/kaggle/working/peft-dialogue-summary-training-1705417060/checkpoint-1000",
- torch_dtype=torch.float16,
- is_trainable=False
- )
-
微调是一个需要反复试验的过程。我们可能需要根据验证集和测试集的表现来调整模型结构、超参数或训练数据,以提升模型性能。
使用PEFT模型对同一个输入进行推理,以评估其性能。
- from transformers import set_seed
- set_seed(seed)
-
- # 选择测试集中的一个对话样本
- index = 5
- dialogue = dataset['test'][index]['dialogue']
- summary = dataset['test'][index]['summary']
-
- # 构建推理提示
- prompt = f"Instruct: Summarize the following conversation.\n{dialogue}\nOutput:\n"
-
- # 使用PEFT模型生成摘要
- peft_model_res = gen(ft_model, prompt, 100)
- peft_model_output = peft_model_res[0].split('Output:\n')[1]
- prefix, _, _ = peft_model_output.partition('###')
-
- # 打印结果
- dash_line = '-' * 100
- print(dash_line)
- print(f'输入提示:\n{prompt}')
- print(dash_line)
- print(f'人工摘要:\n{summary}\n')
- print(dash_line)
- print(f'PEFT模型输出:\n{prefix}')
-
-
这段代码将展示PEFT模型相对于人类基线摘要的表现。通过比较模型输出和人类生成的摘要,我们可以定性地评估PEFT模型的性能。
PEFT 模型输出
ROUGE(Recall-Oriented Understudy for Gisting Evaluation)是一种评估自动摘要和机器翻译软件的工具,它通过比较机器生成的摘要与人工生成的参考摘要来衡量效果。虽然不完美,但ROUGE指标能有效反映微调后摘要质量的整体提升。
我们通过ROUGE指标来定量评估模型生成的摘要。以下是评估过程:
- from transformers import AutoModelForCausalLM
- import pandas as pd
- import evaluate
- import numpy as np
-
- # 加载模型和数据
- original_model = AutoModelForCausalLM.from_pretrained(base_model_id, device_map='auto')
- dialogues = dataset['test'][0:10]['dialogue']
- human_baseline_summaries = dataset['test'][0:10]['summary']
-
- # 初始化摘要列表
- original_model_summaries = []
- peft_model_summaries = []
-
- # 生成摘要并评估
- for idx, dialogue in enumerate(dialogues):
- prompt = f"Instruct: Summarize the following conversation.\n{dialogue}\nOutput:\n"
- original_model_res = gen(original_model, prompt, 100)
- original_model_text_output = original_model_res[0].split('Output:\n')[1]
-
- peft_model_res = gen(ft_model, prompt, 100)
- peft_model_output = peft_model_res[0].split('Output:\n')[1]
- peft_model_text_output, _, _ = peft_model_output.partition('###')
-
- original_model_summaries.append(original_model_text_output)
- peft_model_summaries.append(peft_model_text_output)
-
- # 将结果存入DataFrame
- df = pd.DataFrame(list(zip(human_baseline_summaries, original_model_summaries, peft_model_summaries)),
- columns=['human_baseline_summaries', 'original_model_summaries', 'peft_model_summaries'])
-
- # 计算ROUGE指标
- rouge = evaluate.load('rouge')
- original_model_results = rouge.compute(predictions=original_model_summaries, references=human_baseline_summaries)
- peft_model_results = rouge.compute(predictions=peft_model_summaries, references=human_baseline_summaries)
-
- # 打印结果
- print('原始模型ROUGE指标:')
- print(original_model_results)
- print('PEFT模型ROUGE指标:')
- print(peft_model_results)
-
- # 计算提升百分比
- improvement = (np.array(list(peft_model_results.values())) - np.array(list(original_model_results.values())))
- for key, value in zip(peft_model_results.keys(), improvement):
- print(f'{key}提升: {value*100:.2f}%')
-
-
通过ROUGE指标,我们可以看到PEFT模型相较于原始模型在摘要质量上的显著提升。
微调大型语言模型(LLM)已成为寻求优化运营流程的企业必不可少的步骤。虽然LLM的初始训练赋予了广泛的语言理解能力,但微调过程将这些模型细化为能够处理特定主题并提供更准确结果的专用工具。为不同任务、行业或数据集定制LLM扩展了这些模型的能力,确保了它们在不断变化的数字环境中的相关性和价值。展望未来,LLM的持续探索和创新,加上精细的微调方法,有望推进更智能、更高效、更具情境意识的人工智能系统的发展。