weizhang2024

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

第四阶段 审查模块开发 微调-SFT部分

SFT部分

监督微调(Supervised Fine-Tuning)是指在标注数据集上对预训练模型进行微调,使模型能够适应特定的任务需求。

功能

  • 特定任务优化:通过在标注数据集上进行微调,模型能够调整其参数以更好地适应特定任务,如分类、回归、序列标注等。
  • 提高性能:微调阶段通过优化特定任务的损失函数,提高模型在该任务上的性能。
# coding=utf-8
# Implements several parameter-efficient supervised fine-tuning method.
# This code is inspired by
# https://github.com/huggingface/transformers/blob/v4.29.2/examples/pytorch/summarization/run_summarization.py
# import torch
# torch.distributed.init_process_group(backend="gloo")
from utils import (
    DynamicDataCollatorWithPadding,
    Seq2SeqPeftTrainer,
    ComputeMetrics,
    LogCallback,
    load_pretrained,
    prepare_args,
    prepare_data,
    preprocess_data,
    get_logits_processor,
    plot_loss
)


def main():

    # 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")
    dataset = preprocess_data(dataset, tokenizer, data_args, training_args, stage="sft")
    data_collator = DynamicDataCollatorWithPadding(
        tokenizer=tokenizer,
        ignore_pad_token_for_loss=(data_args.ignore_pad_token_for_loss and not training_args.predict_with_generate)
    )

    # 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.generation_num_beams = data_args.eval_num_beams if \
                data_args.eval_num_beams is not None else training_args.generation_num_beams

    # 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}

    # 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
    )

    # Keyword arguments for `model.generate`
    gen_kwargs = {
        "do_sample": True,
        "top_p": 0.7,
        "max_new_tokens": data_args.max_target_length + 1,
        "temperature": 0.95,
        "logits_processor": get_logits_processor()
    }

    # 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"])

    # Evaluation
    if training_args.do_eval:
        metrics = trainer.evaluate(metric_key_prefix="eval", **gen_kwargs)
        if training_args.predict_with_generate: # eval_loss will be wrong if predict_with_generate is enabled
            metrics.pop("eval_loss", None)
        trainer.log_metrics("eval", metrics)
        trainer.save_metrics("eval", metrics)

    # Predict
    if training_args.do_predict:
        predict_results = trainer.predict(dataset, metric_key_prefix="predict", **gen_kwargs)
        if training_args.predict_with_generate: # predict_loss will be wrong if predict_with_generate is enabled
            predict_results.metrics.pop("predict_loss", None)
        trainer.log_metrics("predict", predict_results.metrics)
        trainer.save_metrics("predict", predict_results.metrics)
        trainer.save_predictions(predict_results)


def _mp_fn(index):
    # For xla_spawn (TPUs)
    main()


if __name__ == "__main__":
    main()

整个前置过程中实现与上一篇博客中的pt中的实现基本一致,但是不同的是在 DynamicDataCollatorWithPadding 引入了 ignore_pad_token_for_loss=(data_args.ignore_pad_token_for_loss and not training_args.predict_with_generate)目的是设置是否在计算损失时忽略填充标记。在自然语言处理(NLP)任务中,输入和输出序列通常需要进行填充(padding)以适应批处理操作。填充值(pad token)用于填充较短的序列,使它们具有相同的长度。在计算损失时,这些填充值不应该对损失的计算产生影响,因为它们不是真实的训练数据。在训练阶段,通常希望忽略填充值的损失计算,因为填充值并不代表有效的训练数据。这可以防止填充值对模型参数的更新产生不必要的影响,从而提高模型的训练效果。在生成文本的任务中(如预测或评估阶段),有时需要对整个序列进行处理,包括填充值。这是因为在生成过程中,模型可能需要考虑整个输入序列的结构,包括填充值的影响。这有助于在训练过程中提高模型性能,同时在生成文本时保留必要的序列信息。

training_args.generation_max_length 是用于控制生成的最大文本长度,而 data_args.max_target_length 则是从数据参数中获取的默认目标长度。如果没有指定生成的最大长度,则使用默认的目标长度。

training_args.generation_num_beams 控制生成时的 Beam 数量,而 data_args.eval_num_beams 则是评估时使用的 Beam 数量。如果评估参数中指定了 Beam 数量,则优先使用它;否则,使用训练参数中的值。

而在模型建立上采用了与继承自之前的PeftTrainerSeq2SeqPeftTrainer类,主要用于计算生成任务的评估指标,如 BLEU 和 ROUGE。它通过重写 prediction_step 和添加 save_predictions 方法,实现了自定义的预测步骤和保存预测结果的功能。

class Seq2SeqPeftTrainer(PeftTrainer):
    r"""
    Inherits PeftTrainer to compute generative metrics such as BLEU and ROUGE.
    用于计算生成任务的评估指标
    """

    def prediction_step(
        self,
        model: nn.Module,
        inputs: Dict[str, Union[torch.Tensor, Any]],
        prediction_loss_only: bool,
        ignore_keys: Optional[List[str]] = None,
    ) -> Tuple[Optional[float], Optional[torch.Tensor], Optional[torch.Tensor]]:
        r"""
        Removes the prompt part in the generated tokens.

        Subclass and override to inject custom behavior.
        """
        input_ids = inputs["input_ids"]
        loss, generated_tokens, labels = super().prediction_step(
            model, inputs, prediction_loss_only=prediction_loss_only, ignore_keys=ignore_keys
        )
        generated_tokens = generated_tokens[:, input_ids.size(-1):] if generated_tokens is not None else None
        return (loss, generated_tokens, labels)

    def save_predictions(
            self,
            predict_results: PredictionOutput
    ) -> None:
        r"""
        Saves model predictions to `output_dir`.

        A custom behavior that not contained in Seq2SeqTrainer.
        """
        if not self.is_world_process_zero():
            return

        output_prediction_file = os.path.join(self.args.output_dir, "generated_predictions.jsonl")
        logger.info(f"Saving prediction results to {output_prediction_file}")

        preds = np.where(predict_results.predictions != IGNORE_INDEX, predict_results.predictions, self.tokenizer.pad_token_id)
        labels = np.where(predict_results.label_ids != IGNORE_INDEX, predict_results.label_ids, self.tokenizer.pad_token_id)

        decoded_preds = self.tokenizer.batch_decode(preds, skip_special_tokens=True, clean_up_tokenization_spaces=True)
        decoded_labels = self.tokenizer.batch_decode(labels, skip_special_tokens=True, clean_up_tokenization_spaces=True)

        with open(output_prediction_file, "w", encoding="utf-8") as writer:
            res: List[str] = []
            for pred, label in zip(decoded_preds, decoded_labels):
                res.append(json.dumps({"label": label, "predict": pred}, ensure_ascii=False))
            writer.write("\n".join(res))

prediction_step这个方法用于在生成的预测结果中移除提示部分的令牌。调用父类 PeftTrainerprediction_step 方法获取损失、生成的令牌和标签。移除生成的令牌中与输入长度相同的部分,保留生成的文本。返回损失、处理后的生成令牌和标签。

save_predictions这个方法用于将模型的预测结果保存到指定的输出目录中。检查当前进程是否为主进程,如果不是,则直接返回。构建输出文件路径,并记录保存预测结果的日志信息。将预测结果中的 IGNORE_INDEX 替换为填充值(pad token)。解码预测结果和标签,将它们转换为可读的文本。将解码后的预测结果和标签保存到 JSON Lines 格式的文件中。

设置了用于调用 model.generate 方法的关键字参数 gen_kwargs,这些参数对生成文本的行为和质量有直接影响。设置这些参数的意义如下:

  • 多样性:通过启用采样(do_sample=True)和设置核采样(top_p=0.7),可以确保生成的文本具有较高的多样性,而不是每次生成相同的文本。这对于对话生成、故事生成等任务非常重要。
  • 长度控制:通过设置 max_new_tokens,可以控制生成的文本长度,确保生成的文本不超过预期长度,避免生成过长的文本带来的问题。
  • 随机性控制:通过设置温度参数(temperature=0.95),可以调节生成过程的随机性,平衡生成文本的质量和多样性。
  • 自定义处理:通过 logits_processor,可以对生成过程中间的 logits 进行自定义处理,进一步控制生成文本的质量和内容。
posted on 2024-05-31 12:43  weiZhang2024  阅读(42)  评论(0)    收藏  举报