0. 简介
随着chatgpt的爆火,最近也有很多大模型在不断地出现,比如说Bloom系列以及以LLAMA为基础的ziya和baichuan。这些模型相较于chatglm来说,更加具有发展前景,因为其是完全可商用,并可以不断迭代更新的。最近作者在跟着hiyouga大佬的LLaMA-Efficient-Tuning进行学习,相较于其他的项目来说,该项目是非常适合跟着学习并入门的。
1. 什么是SFT
SFT(Scalable Fine-Tuning)是一种用于自然语言处理的技术,它通过对预训练的语言模型进行微调,使其适应特定任务。在大模型SFT中,使用的是大型的预训练语言模型,例如LLAMA、GPT等,这些模型具有数十亿甚至数百亿个参数,可以处理大量的文本数据。
SFT的主要思想是在一个大型的预训练模型的基础上,针对特定的任务对模型进行微调。在微调过程中,模型会根据任务的特点调整模型的参数和结构,以提高模型在该任务上的表现。在微调过程中,可以使用不同的技术,例如数据增强、正则化、优化算法等。
SFT的优点是可以快速地针对不同的任务进行微调,而无需重新训练整个模型。此外,由于使用的是大型的预训练模型,可以利用海量的文本数据进行训练,从而获得更好的性能。不过,SFT也有一些缺点,例如需要大量的计算资源和时间进行微调,以及可能会出现过拟合等问题。
目前常用的SFT方法有P-Tuning v2、LORA、QLoRA、冻结(Freeze)、全参数(full-parameter)等方法。我们先来看一看在LLaMA-Efficient-Tuning中是如何写SFT的
2. 代码阅读–train_sft.py
下面是sft对应大模型的脚本,主要包括模型和数据的准备,数据集的划分,训练和评估等步骤。
首先,代码导入了一些必要的模块和函数。这包括一些用于数据处理、训练、加载预训练模型和绘制损失图的工具函数。(这部分和pt中一样)
# Prepare pretrained model and dataset
model_args, data_args, training_args, finetuning_args = prepare_args(stage="sft")# 用于准备各种参数,包括模型参数、数据参数、训练参数和微调参数。
dataset = prepare_data(model_args, data_args)# 用于准备数据集
model, tokenizer = load_pretrained(model_args, finetuning_args, training_args.do_train, stage="sft")# 用于加载sft微调的模型和分词器。
dataset = preprocess_data(dataset, tokenizer, data_args, training_args, stage="sft")# 用于预处理数据,例如将文本转换为模型可以理解的格式。
data_collator = DynamicDataCollatorWithPadding(tokenizer, data_args.ignore_pad_token_for_loss)# 动态地对数据进行填充,使得每个batch中的数据长度一致。
下面的代码是用于Seq2SeqTrainer的解码参数进行覆盖
# Override the decoding parameters of Seq2SeqTrainer
training_args.generation_max_length = training_args.generation_max_length if \
training_args.generation_max_length is not None else data_args.max_target_length# 设置训练参数(training_args)中的生成最大长度
training_args.generation_num_beams = data_args.eval_num_beams if \
data_args.eval_num_beams is not None else training_args.generation_num_beams # 设置训练参数中的生成束搜索数(generation_num_beams)
然后,根据是否进行训练,对数据集进行划分。如果进行训练,且开发集的比例大于0,那么数据集会被划分为训练集和开发集;否则,全部数据用于训练。如果不进行训练,那么全部数据用于评估或预测。
# Split the dataset
if training_args.do_train:
if data_args.dev_ratio > 1e-6:
dataset = dataset.train_test_split(test_size=data_args.dev_ratio)
trainer_kwargs = {"train_dataset": dataset["train"], "eval_dataset": dataset["test"]}
else:
trainer_kwargs = {"train_dataset": dataset}
else: # do_eval or do_predict
trainer_kwargs = {"eval_dataset": dataset}
接着,初始化Seq2SeqPeftTrainer对象,传入微调参数、模型、训练参数、分词器、数据处理器、回调函数和计算度量等参数(都是继承自Seq2SeqTrainer),以及前面划分的数据集。这个我们下一节将会仔细阅读里面的操作
# Initialize our Trainer
trainer = Seq2SeqPeftTrainer(
finetuning_args=finetuning_args,
model=model,
args=training_args,
tokenizer=tokenizer,
data_collator=data_collator,
callbacks=[LogCallback()],
compute_metrics=ComputeMetrics(tokenizer) if training_args.predict_with_generate else None,
**trainer_kwargs
)
在进行训练后,代码会记录训练的结果,并保存模型和训练结果。如果模型在所有进程中的进程号为0(主进程),并且设定了绘制损失图,那么会绘制训练损失和评估损失的图。
# Training
if training_args.do_train:
train_result = trainer.train()
trainer.log_metrics("train", train_result.metrics)
trainer.save_metrics("train", train_result.metrics)
trainer.save_state()
trainer.save_model()
if trainer.is_world_process_zero() and model_args.plot_loss:
plot_loss(training_args.output_dir, keys=["loss", "eval_loss"])
通过训练器对象(trainer)的evaluate方法,代码会在验证集上进行评估,并将结果保存在metrics变量中。
# Evaluation
if training_args.do_eval:# 如果训练参数(training_args)中有设置进行评估,则执行下列代码
metrics = trainer.evaluate(metric_key_prefix="eval", **gen_kwargs)
trainer.log_metrics("eval", metrics)
trainer.save_metrics("eval", metrics)#评估结果保存在训练器对象(trainer)的指定文件中,方便后续的分析和比较
如果训练参数(training_args)中有设置进行预测,则执行下列代码
# Predict
if training_args.do_predict:
predict_results = trainer.predict(dataset, metric_key_prefix="predict", **gen_kwargs)#过训练器对象(trainer)的predict方法,在测试集上进行预测,并将结果保存在predict_results变量中
trainer.log_metrics("predict", predict_results.metrics)
trainer.save_metrics("predict", predict_results.metrics)
trainer.save_predictions(predict_results, tokenizer)#将预测结果保存在训练器对象(trainer)的指定文件中
3. 代码阅读–seq2seq.py
下面的代码主要定义了一个名为ComputeMetrics的类和一个名为Seq2SeqPeftTrainer的子类,用于计算文本生成任务的评价指标。
3.1 ComputeMetrics函数
ComputeMetrics类接受一个PreTrainedTokenizer类型的参数tokenizer,用于计算给定的模型预测结果的指标。下面我们来仔细看一下这部分的代码
@dataclass
class ComputeMetrics:
r"""
Wraps the tokenizer into metric functions, used in Seq2SeqPeftTrainer.
Borrowed from: https://github.com/THUDM/ChatGLM-6B/blob/0c2806fea82683349194e21996dd6b3acc3c265b/ptuning/main.py#L307
"""
tokenizer: PreTrainedTokenizer# 定义了一个tokenizer变量,它的类型是PreTrainedTokenizer,这是从transformers.tokenization_utils中拿出的
def __call__(self, eval_preds: Sequence[Union[np.ndarray, Tuple[np.ndarray]]]) -> Dict[str, float]:
r"""
Uses the model predictions to compute metrics.
"""
preds, labels = eval_preds#获取输入的eval_preds序列中的两个数组,分别赋值给preds和labels变量
# 处理preds和labels数组中的IGNORE_INDEX值,将它们替换成tokenizer.pad_token_id
if isinstance(preds, tuple):
preds = preds[0]
# 因为如果ignore_pad_token_for_loss为True时,我们不能解码IGNORE_INDEX值。
preds = np.where(preds != IGNORE_INDEX, preds, self.tokenizer.pad_token_id)
labels = np.where(labels != IGNORE_INDEX, labels, self.tokenizer.pad_token_id)
# 定义了一个字典score_dict,它包含四个指标:rouge-1,rouge-2,rouge-l和bleu-4
score_dict = {"rouge-1": [], "rouge-2": [], "rouge-l": [], "bleu-4": []}
for pred, label in zip(preds, labels):# 遍历preds和labels中的每一个元素,分别赋值给pred和label变量
pred = pred[(pred == self.tokenizer.bos_token_id).nonzero()[0][0]:] # 从pred数组中删除第一个出现bos_token_id的位置及其之前的元素
hypothesis = list(jieba.cut(self.tokenizer.decode(pred, skip_special_tokens=True)))# pred和label解码成文本,然后用jieba库将它们分词
reference = list(jieba.cut(self.tokenizer.decode(label, skip_special_tokens=True)))
# 使用Rouge计算指标rouge-1,rouge-2和rouge-l的得分
if len(" ".join(hypothesis).split()) == 0:
result = {"rouge-1": {"f": 0.0}, "rouge-2": {"f": 0.0}, "rouge-l": {"f": 0.0}}
else:
rouge = Rouge()
scores = rouge.get_scores(" ".join(hypothesis), " ".join(reference))
result = scores[0]
# 将result中的每个指标的f值乘以100取整,然后将结果添加到score_dict字典中
for k, v in result.items():
score_dict[k].append(round(v["f"] * 100, 4))
bleu_score = sentence_bleu([list(label)], list(pred), smoothing_function=SmoothingFunction().method3)
score_dict["bleu-4"].append(round(bleu_score * 100, 4))# 使用NLTK库的sentence_bleu函数计算指标bleu-4的得分
return {k: float(np.mean(v)) for k, v in score_dict.items()}
3.2 Seq2SeqPeftTrainer函数
这段代码定义了一个名为Seq2SeqPeftTrainer的类,它继承自PeftTrainer类。详细的内容其实已经在PeftTrainer里面全部囊括了,它只是拓展了一个函数用于计算生成式指标,如BLEU和ROUGE。并让它在Predict处调用
class Seq2SeqPeftTrainer(PeftTrainer):
r"""
predict_results: PredictionOutput类型,表示模型的预测结果。
tokenizer: PreTrainedTokenizer类型,表示用于将预测结果转换为文本的分词器。
"""
def save_predictions(
self,
predict_results: PredictionOutput,
tokenizer: PreTrainedTokenizer
) -> None:
r"""
Saves model predictions to `output_dir`.
A custom behavior that not contained in Seq2SeqTrainer.
"""
if not self.is_world_process_zero():# 判断当前进程是否是主进程
return
preds = np.where(predict_results.predictions != IGNORE_INDEX, predict_results.predictions, self.tokenizer.pad_token_id)#预测结果中的IGNORE_INDEX值替换为分词器的pad_token_id值,得到预测结果preds和标签labels
labels = np.where(predict_results.label_ids != IGNORE_INDEX, predict_results.label_ids, self.tokenizer.pad_token_id)#每个预测结果pred,从第一个出现分词器的bos_token_id的位置开始截取,得到去除查询部分的预测结果
preds = [pred[(pred == self.tokenizer.bos_token_id).nonzero()[0][0]:] for pred in preds] # 预测结果和标签使用分词器的decode方法转换为文本形式,并去除特殊标记和空格
preds = [tokenizer.decode(pred, skip_special_tokens=True).strip() for pred in preds]
labels = [tokenizer.decode(label, skip_special_tokens=True).strip() for label in labels]
output_prediction_file = os.path.join(self.args.output_dir, "generated_predictions.jsonl")#预测结果和标签按照指定的格式组织成字典,并以json格式写入到输出文件generated_predictions.jsonl中
logger.info(f"Saving prediction results to {output_prediction_file}")
with open(output_prediction_file, "w", encoding="utf-8") as writer:
res: List[str] = []
for pred, label in zip(preds, labels):
res.append(json.dumps({"label": label, "predict": pred}, ensure_ascii=False))
writer.write("\n".join(res))
评论(0)
您还未登录,请登录后发表或查看评论