逐行代码学习ChatGLM2-6B大模型SFT微调(通过prompt ptune实现),项目中的ptune/main.py文件
  FmrvKioKNQio 2023年11月02日 90 0


项目地址

#!/usr/bin/env python
# coding=utf-8
"""
Fine-tuning the library models for sequence to sequence.
下面给出了一个命令行运行微调main.py的例子

#1.激活环境

#2.执行torchrun
#--standalone 这个标志指定使用“standalone”模式运行分布式训练。这意味着它不依赖于任何特定的集群管理器(如Slurm、Kubernetes等)。这通常是本地运行或简单的分布式设置的选择。
#--nnodes=1 这指定了参与分布式训练的节点(或机器)的数量。在这个例子中,它被设置为1,意味着只有一个节点(很可能是本地机器)参与训练
#--nproc-per-node=1:这指定了每个节点上要运行的进程数量。这通常与你想要使用的GPU数量相关。在这个例子中,它被设置为1,这意味着在指定的节点上只有一个进程运行。
#如果你在一台有4个GPU的机器上设置--nproc-per-node=4,那么会在该机器上启动4个进程,每个进程使用一个GPU

具体命令如下
source /public1/home/stu52275901023/anaconda3/bin/activate ptune
torchrun --standalone --nnodes=1 --nproc-per-node=1 main.py \
    --do_train \
    --train_file ../brain_data/brain_data_tra.json \
    --validation_file ../brain_data/brain_data_val.json \
    --preprocessing_num_workers 10 \
    --prompt_column prompt \
    --response_column response \
    --history_column history \
    --overwrite_cache \
    --model_name_or_path ../model/chatglm2-6b \
    --output_dir ../ptune_point_V1/ \
    --overwrite_output_dir \
    --max_source_length 512 \
    --max_target_length 512 \
    --per_device_train_batch_size 6 \
    --per_device_eval_batch_size 1 \
    --gradient_accumulation_steps 16 \
    --predict_with_generate \
    --max_steps 500 \
    --logging_steps 10 \
    --save_steps 100 \
    --learning_rate 1e-2 \
    --pre_seq_len 512 \

"""
# You can also adapt this script on your own sequence to sequence task. Pointers for this are left as comments.
#基础库
import logging
import os
import sys
import json

import numpy as np
from datasets import load_dataset
import jieba 
from rouge_chinese import Rouge
from nltk.translate.bleu_score import sentence_bleu, SmoothingFunction
import torch

#transformer库
import transformers
from transformers import (
    AutoConfig,
    AutoModel,
    AutoTokenizer,
    DataCollatorForSeq2Seq,
    HfArgumentParser,
    Seq2SeqTrainingArguments,
    set_seed,
)
from trainer_seq2seq import Seq2SeqTrainer

#注意,注意,从当前目录下的arguments.py导入ModelArguments, DataTrainingArguments数据参数类
from arguments import ModelArguments, DataTrainingArguments

logger = logging.getLogger(__name__)


def main():
    #导入相关参数,√ 表示刚才命令行例子中用到了
    """
        ModelArguments(自己定义的):
            这个类可能包含与模型配置和加载有关的参数。

          √ model_name_or_path: 预训练模型的名称(如"bert-base-uncased")或模型文件的路径。
            config_name: 可选的配置名称或路径,如果没有设置,则默认使用model_name_or_path。
            tokenizer_name: 可选的分词器名称或路径。
          √ pre_seq_len:这个参数很重要,表示前缀编码器prefixencode层的第一层参数数量,同时设定了这个参数表示微调时只会修改prefixencode层参数
            ptuning_checkpoint:可以设定这个值,该值是一个路径,如果有则会接着检查点继续训练


        DataTrainingArguments(自己定义的):
            这个类通常定义了与训练数据相关的参数。

          √ train_file: 训练数据的路径。
          √ validation_file: 验证数据的路径。
          √ max_source_length: 输入序列的最大长度,需要注意的是多轮对话下,这个长度应该大于当前轮的qurey+history。
          √ max_target_length:目标序列的最大长度,这个长度应该大于任一一轮的response。
          √ overwrite_cache: 如果设置为True,则在处理数据时覆盖现有的缓存。
          √ preprocessing_num_workers :用于预处理的进程数
          √ prompt_column:表示训练的json文件中,prompt的关键字
          √ response_column: 表示训练的json文件中,response的关键字
          √ history_column: 表示训练的json文件中,history的关键字
        
        Seq2SeqTrainingArguments(API在transformer下的trainer里:https://huggingface.co/docs/transformers/main_classes/trainer#transformers.TrainingArguments.gradient_accumulation_steps):
            这个类可能专为序列到序列模型定义了参数,如T5、Bart等。
            
          √ output_dir:模型输出路径
          √ overwrite_output_dir :是否重写模型输出路径内容
          √ learning_rate: 学习率。
            num_train_epochs: 训练时代数。
          √ max_steps : 最大训练步数,会覆盖num_train_epochs
          √ predict_with_generate:是否使用generate计算指标
          √ per_device_train_batch_size: 每个设备上的训练时批次大小。
          √ per_device_eval_batch_size: 每个设备上的评估时批次大小。
          √ gradient_accumulation_steps (int, optional, defaults to 1):更新梯度时累计的梯度次数
            weight_decay: 权重衰减。
          √ logging_steps: 多少步进行一次日志记录。
          √ save_steps: 多少步保存一次模型。
          √ do_train :bool 是否训练
          √ do_eval  :bool 是否评估
            do_predict :bool 是否预测
    """
    #Hf是Huggingface的缩写
    parser = HfArgumentParser((ModelArguments, DataTrainingArguments, Seq2SeqTrainingArguments))

    #如果是json参数文件,则有两个参数,第二个参数是json的路径
    if len(sys.argv) == 2 and sys.argv[1].endswith(".json"):
        # If we pass only one argument to the script and it's the path to a json file,
        # let's parse it to get our arguments.
        model_args, data_args, training_args = parser.parse_json_file(json_file=os.path.abspath(sys.argv[1]))
    else:
        #否则直接命令行读取参数
        model_args, data_args, training_args = parser.parse_args_into_dataclasses()

    # Setup logging
    logging.basicConfig(
        format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
        datefmt="%m/%d/%Y %H:%M:%S",
        handlers=[logging.StreamHandler(sys.stdout)],
    )
    #看下训练参数里是否说要输出log
    if training_args.should_log:
        # The default of training_args.log_level is passive, so we set log level at info here to have that default.
        transformers.utils.logging.set_verbosity_info()

    log_level = training_args.get_process_log_level()
    logger.setLevel(log_level)
    # datasets.utils.logging.set_verbosity(log_level)
    transformers.utils.logging.set_verbosity(log_level)
    transformers.utils.logging.enable_default_handler()
    transformers.utils.logging.enable_explicit_format()

    # Log on each process the small summary:
    logger.warning(
        f"Process rank: {training_args.local_rank}, device: {training_args.device}, n_gpu: {training_args.n_gpu}"
        + f"distributed training: {bool(training_args.local_rank != -1)}, 16-bits training: {training_args.fp16}"
    )
    logger.info(f"Training/evaluation parameters {training_args}")

    # Set seed before initializing model.
    set_seed(training_args.seed)

    # Load dataset
    data_files = {}
    if data_args.train_file is not None:
        data_files["train"] = data_args.train_file
        extension = data_args.train_file.split(".")[-1]
    if data_args.validation_file is not None:
        data_files["validation"] = data_args.validation_file
        extension = data_args.validation_file.split(".")[-1]
    if data_args.test_file is not None:
        data_files["test"] = data_args.test_file
        extension = data_args.test_file.split(".")[-1]

    #做成原始数据
    raw_datasets = load_dataset(
        extension,
        data_files=data_files,
        cache_dir=model_args.cache_dir,
        use_auth_token=True if model_args.use_auth_token else None,
    )

    # Load pretrained model and tokenizer
    config = AutoConfig.from_pretrained(model_args.model_name_or_path, trust_remote_code=True)
    #重新设定pre_seq_len的值
    config.pre_seq_len = model_args.pre_seq_len
    #重新设定prefix_projection的值,这个是个bool类型,true表示有前缀
    config.prefix_projection = model_args.prefix_projection

    tokenizer = AutoTokenizer.from_pretrained(model_args.model_name_or_path, trust_remote_code=True)

    #是否有额外前缀编码层状态
    if model_args.ptuning_checkpoint is not None:
        # Evaluation
        # Loading extra state dict of prefix encoder
        #在模型文件目录下有个config.json,里面完成了"AutoModel"到"modeling_chatglm.ChatGLMForConditionalGeneration"的映射
        #所以下面的代码会导入modeling_chatglm.py文件里的ChatGLMForConditionalGeneration
        model = AutoModel.from_pretrained(model_args.model_name_or_path, config=config, trust_remote_code=True)
        prefix_state_dict = torch.load(os.path.join(model_args.ptuning_checkpoint, "pytorch_model.bin"))
        new_prefix_state_dict = {}
        for k, v in prefix_state_dict.items():
            if k.startswith("transformer.prefix_encoder."):
                new_prefix_state_dict[k[len("transformer.prefix_encoder."):]] = v
        model.transformer.prefix_encoder.load_state_dict(new_prefix_state_dict)
    else:
        model = AutoModel.from_pretrained(model_args.model_name_or_path, config=config, trust_remote_code=True)
    
    #是否量化
    if model_args.quantization_bit is not None:
        print(f"Quantized to {model_args.quantization_bit} bit")
        model = model.quantize(model_args.quantization_bit)
    if model_args.pre_seq_len is not None:
        # P-tuning v2
        model = model.half()
        model.transformer.prefix_encoder.float()
    else:
        # Finetune
        model = model.float()
    
    #有前缀则取出前缀
    prefix = data_args.source_prefix if data_args.source_prefix is not None else ""

    # Preprocessing the datasets.
    # We need to tokenize inputs and targets.
    if training_args.do_train:
        column_names = raw_datasets["train"].column_names
    elif training_args.do_eval:
        column_names = raw_datasets["validation"].column_names
    elif training_args.do_predict:
        column_names = raw_datasets["test"].column_names
    else:
        logger.info("There is nothing to do. Please pass `do_train`, `do_eval` and/or `do_predict`.")
        return

    # Get the column names for input/target.
    prompt_column = data_args.prompt_column
    response_column = data_args.response_column
    history_column = data_args.history_column
    
    # Temporarily set max_target_length for training.
    max_target_length = data_args.max_target_length

    def preprocess_function_eval(examples):
        inputs, targets = [], []
        for i in range(len(examples[prompt_column])):
            if examples[prompt_column][i] and examples[response_column][i]:
                query = examples[prompt_column][i]
                history = examples[history_column][i] if history_column is not None else None
                prompt = tokenizer.build_prompt(query, history)
                inputs.append(prompt)
                targets.append(examples[response_column][i])

        inputs = [prefix + inp for inp in inputs]
        model_inputs = tokenizer(inputs, max_length=data_args.max_source_length, truncation=True, padding=True)
        labels = tokenizer(text_target=targets, max_length=max_target_length, truncation=True)

        if data_args.ignore_pad_token_for_loss:
            labels["input_ids"] = [
                [(l if l != tokenizer.pad_token_id else -100) for l in label] for label in labels["input_ids"]
            ]
        model_inputs["labels"] = labels["input_ids"]

        return model_inputs

    def preprocess_function_train(examples):
        #最大训练长度
        max_seq_length = data_args.max_source_length + data_args.max_target_length + 1

        #初始化训练时模型输入
        model_inputs = {
            "input_ids": [],#history+prompt
            "labels": [],#response
        }
        
        '''
            #遍历每一条数据,数据示例如下
            {
            "prompt": "翻译论语吾日三省吾身",
            "response": "好的。\n 【译文】曾子说:\"我每天多次地反省自己:替别人办事是不是尽心竭力呢?跟朋友交往是不是诚实呢?老师传授的知识是不是复习过了呢? \"",
            "history": []
            },
            {
                "prompt": "我想学习欧阳修的醉翁亭记",
                "response": "好的。【原文】醉翁亭记  宋 欧阳修环滁皆山也。其西南诸峰,林壑(hè)尤美。望之蔚然而深秀者,琅琊也。山行六七里,渐 闻水声潺潺而泻出于两峰之间者,酿泉也。峰回路转,有亭翼然临于泉上者,醉翁亭也。作亭 者谁?山之僧智仙也。名之者谁?太守自谓也。太守与客来饮于此,饮少辄(zhé)醉,而年又 最高,故自号曰醉翁也。醉翁之意不在酒,在乎山水之间也。山水之乐,得之心而寓之酒也。若夫日出而林霏开,云归而岩穴暝,晦明变化者,山间之朝暮也。野芳发而幽香,佳木秀 而繁阴,风霜高洁,水落而石出者,山间之四时也。朝而往,暮而归,四时之景不同,而乐亦 无穷也。至于负者歌于途,行者休于树,前者呼,后者应,伛(yǔ)偻(lǚ)提携,往来而不绝者, 滁人游也。临溪而渔,溪深而鱼肥。酿泉为酒,泉香而酒洌;山肴野蔌(sù),杂然而前陈者,太守宴也。宴酣之乐,非丝非竹,射者中,弈者胜,觥(gōng)筹交错,起坐而喧哗者,众宾欢也。苍颜白发,颓然乎其间者,太守醉也。已而夕阳在山,人影散乱,太守归而宾客从也。树林阴翳(yì),鸣声上下,游人去而禽鸟乐也。然而禽鸟知山林之乐,而不知人之乐;人知从太守游而乐,而不知太守之乐其乐也。醉能同其乐,醒能述以文者,太守也。太守谓谁?庐陵欧阳修也。【译文】环绕着滁州城的都是山。它西南面的各个山峰,树林、山谷尤其优美,远远望去那树木茂盛,又幽深又秀丽的,是琅琊山。沿着山路走六七里,就渐渐听到潺潺的水声,从两座山峰中间飞泻出来的,是酿泉。山势回环,路也跟着拐弯,有亭子四角翘起,像鸟张开翅膀一样,坐落在泉水边上的,是醉翁亭。修建亭子的人是谁?是山中的和尚智仙。给它命名的人是谁?是太守用自己的号(醉翁)来命名的。太守和客人到这里来喝酒,喝一点就醉了,而且年龄又最大,所以给自己取个别号叫醉翁。醉翁的情趣不在于喝酒,而在于秀丽的山水之间。欣赏山水的乐趣,领会在心里,寄托在喝酒上。像那太阳出来,树林中的雾气消散,暮云回聚拢来,山里就昏暗了,或明或暗,变化不一,这是山间的早晨和傍晚。野花开放,散发清幽的香气,好的树木枝叶繁茂,形成浓郁的绿阴,秋高气爽,霜色洁白,水位低落,石头显露,这是山里的四季的景色。早晨上山,傍晚返回,四季的景色不同,因而乐趣也没有穷尽。至于背着东西的人路上唱歌,走路的人在树下休息,前面的人呼唤,后面的人答应,老人小孩络绎不绝,这是滁州人出游。到溪水捕鱼,溪水深,鱼儿肥,用酿泉的水酿酒,泉水香而酒色清澈,山中野味,野菜,交错地在前面摆着,这是太守举行的酒宴。酒宴上饮酒的乐趣,不在于音乐。投壶的人中了目标,下棋的人得胜了,酒杯和酒筹交互错杂,人们有时站立,有时坐着,大声喧哗,宾客们(尽情)欢乐。脸色苍老,头发花白,醉醺醺地在宾客们中间的是喝醉了的太守。不久夕阳落山,人影纵横散乱,太守回归而宾客跟从。这时树林里浓荫遮蔽,鸟儿到处鸣叫,游人离开后禽鸟就快乐了。然而禽鸟只知道山林的乐趣,却不知道人的乐趣,人们只知道跟随太守游玩的乐趣,却不知道太守因为他们的快乐而快乐。醉了能够和大家一起享受快乐,醒来能够用文章记述这乐事的人,是太守。太守是谁?是庐陵的欧阳修。",
                "history": [
                    [
                        "我想学习一下杜牧的泊秦淮",
                        "好的。【原文】泊秦淮 唐 杜牧烟笼寒水月笼沙,夜泊秦淮近酒家。商女不知亡国恨,隔江犹唱后庭花。【译文】迷离月色和轻烟笼罩着寒水白沙,夜晚船只停泊在秦淮河边靠近岸上的酒家。卖唱的歌女不知道什么是亡国之恨,隔着江水仍在高唱着《玉树后庭花》。【注释】选自冯集梧《樊川文集》卷四。(上海古籍出版社1978年版)。秦淮,即秦淮河,发源于江苏句容大茅山与溧(lì)水东庐山两山间,经南京流入长江。相传为秦始皇南巡会稽时开凿的,用来疏通淮水,故称秦淮河。泊:停泊。商女:以卖唱为生的歌女。犹:副词。还,仍然。后庭花:歌曲《玉树后庭花》的简称。南朝陈皇帝陈叔宝(即陈后主)溺于声色,作此曲与后宫美女寻欢作乐,终致亡国,所以后世称此曲为“亡国之音”。"
                    ],
                    [
                        "翻译一下诸葛亮的出师表",
                        "好的。【译文】臣诸葛亮上表进言:先帝开创大业未完成一半,竟中途去世。如今天下分成三国,我益州地区人力疲惫、 民生凋敝,这真是处在万分危急、存亡难料的时刻。但是,宫廷里侍奉守卫的臣子,不敢稍有懈怠;疆场上 忠诚有志的将士,舍身忘死的作战,这都是追念先帝的特殊恩遇,想报答给陛下的缘故。陛下确实应该广开 言路听取群臣意见,发扬光大先帝遗留下来的美德,振奋鼓舞志士们的勇气,绝不应随便看轻自己,说出无 道理的话,从而堵塞了忠诚进谏的道路。宫里身边的近臣和丞相府统领的官吏,本都是一个整体,升赏惩罚, 扬善除恶,不应标准不同。如有作坏事违犯法纪的,或尽忠心做善事的,应该一律交给主管部门加以惩办或 奖赏,以显示陛下在治理方面公允明察,切不应私心偏袒,使宫廷内外施法不同。 侍中、侍郎郭攸之、费、董允等,这都是些品德良善诚实、情志意念忠贞纯正的人,因而先帝才选留下 来辅佐陛下。我认为宫内的事情,事无论大小,都要征询他们的意见,然后再去施行。这样一定能够补正疏 失,增益实效。将军向宠,性情德行平和公正,了解通晓军事,当年试用,先帝曾加以称赞,说他能干,因 而经众人评议荐举任命为中部督。我认为军营里的事情,事情无论大小,都要征询他的意见,就一定能够使 军伍团结和睦,德才高低的人各有合适的安排。亲近贤臣,远避小人,这是汉朝前期所以能够兴盛的原因; 亲近小人,远避贤臣,这是汉朝后期所以衰败的原因。先帝在世的时候,每次跟我评论起这些事,对于桓帝、灵帝时代,没有不哀叹和憾恨的。侍中郭攸之、费,尚书陈震,长史张裔,参军蒋琬,这些都是忠贞、坦直,能以死报国的节义臣子,诚愿陛下亲近他们,信任他们,则汉王室的兴盛,就时间不远了。我本是个平民,在南阳郡务农亲耕,在乱世间只求保全性命,不希求诸侯知道我而获得显贵。先帝不介意我的卑贱,委屈地自我降低身份,接连三次到草庐来访看我,征询我对时局大事的意见,因此我深为感激,从而答应为先帝驱遣效力。后来正遇危亡关头,在战事失败的时候我接受了任命,在危机患难期间我受到委任,至今已有二十一年了。先帝深知我做事谨慎,所以临去世时把国家大事嘱托给我了。接受遗命以来,日夜担忧兴叹,只恐怕托付给我的大任不能完成,从而损害先帝的英明。所以我五月率兵南渡泸水,深入荒芜之境。如今南方已经平定,武库兵器充足,应当鼓励和统率全军,北伐平定中原地区,我希望竭尽自己低下的才能,消灭奸邪势力,复兴汉朝王室,迁归旧日国都。这是我用来报答先帝,并尽忠心于陛下的职责本分。至于掂量利弊得失,毫无保留地进献忠言,那就是郭攸之、费、董允的责任了。希望陛下责成我去讨伐奸贼并取得成效,如果不取得成效,那就惩治我失职的罪过,用来上告先帝的神灵。如果没有发扬圣德的言论,那就责备郭攸之、费、董允等人的怠慢,公布他们的罪责。陛下也应该自己思虑谋划,征询从善的道理,明察和接受正直的进言,远念先帝遗诏中的旨意,我就受恩、感激不尽了。如今正当离朝远征,流着泪写了这篇表文,激动得不知该说些什么话。"
                    ],
                    [
                        "介绍一下王维的使至塞上",
                        "好的。【原文】使至塞上 唐 王维单车欲问边,属国过居延。 征蓬出汉塞,归雁入胡天。 大漠孤烟直,长河落日圆。 萧关逢候骑,都护在燕然。【译文】轻车简从将要去慰问边关,我要到远在西北边塞的居延。像随风而去的蓬草一样出临边塞,北归大雁正翱翔云天。浩瀚沙漠中孤烟直上云霄,黄河边上落日浑圆。到萧关时遇到侦察骑兵,得知主帅尚在前线未归。【注释】使至塞上:奉命出使边塞。使:出使。单车:一辆车,车辆少,这里形容轻车简从。问边:到边塞去察看,指慰问守卫边疆的官兵。属国:有几种解释:一指少数民族附属于汉族朝廷而存其国号者。汉、唐两朝均有一些属国。二指官名,秦汉时有一种官职名为典属国,苏武归汉后即授典属国官职。唐人有时以“属国”代称出使边陲的使臣。居延:地名,汉代称居延泽,唐代称居延海,在今内蒙古额济纳旗北境。又西汉张掖郡有居延县(参《汉书·地理志》),故城分布在今内蒙古自治区额济纳旗和甘肃省金塔县的境内。又东汉凉州刺史部有张掖居延属国,辖境在居延泽一带。此句一般注本均言王维路过居延。然而王维此次出使,实际上无需经过居延。因而林庚、冯沅君主编的《中国历代诗歌选》认为此句是写唐王朝“边塞的辽阔,附属国直到居延以外”。征蓬:随风飘飞的蓬草,此处为诗人自喻。归雁:雁是候鸟,春天北飞,秋天南行,这里是指大雁北飞。胡天:胡人的领空。这里是指唐军占领的北方地方。大漠:大沙漠,此处大约是指凉州之北的沙漠。孤烟:赵殿成注有二解:一云古代边防报警时燃狼粪,“其烟直而聚,虽风吹之不散”。二云塞外多旋风,“袅烟沙而直上”。据后人有到甘肃、新疆实地考察者证实,确有旋风如“孤烟直上”。又:孤烟也可能是唐代边防使用的平安火。长河:黄河。萧关:古关名,又名陇山关,故址在今宁夏固原东南。候骑:负责侦察、通讯的骑兵。王维出使河西并不经过萧关,此处大概是用何逊诗“候骑出萧关,追兵赴马邑”之意,非实写。都护:唐朝在西北边疆置安西、安北等六大都护府,其长官称都护,每府派大都护一人,副都护二人,负责辖区一切事务。这里指前线统帅。燕然:燕然山,即今蒙古国杭爱山。东汉窦宪北破匈奴,曾于此刻石记功。这里代指前线。“单车”两句:一作“衔命辞天阙,单车欲问边”。候骑:骑马的侦察兵。"
                    ]
                ]
            }
        '''
        for i in range(len(examples[prompt_column])):
            #如果有prompt和response
            if examples[prompt_column][i] and examples[response_column][i]:
                query, answer = examples[prompt_column][i], examples[response_column][i]
                #如果有history
                history = examples[history_column][i] if history_column is not None else None
                '''
                    将当前对话prompt/query和历史对话history制作成新的prompt,源码如下
                    prompt = ""
                    for i, (old_query, response) in enumerate(history):
                        prompt += "[Round {}]\n\n问:{}\n\n答:{}\n\n".format(i + 1, old_query, response)
                    prompt += "[Round {}]\n\n问:{}\n\n答:".format(len(history) + 1, query)
                '''
                prompt = tokenizer.build_prompt(query, history)
                
                #有前缀则加在前面
                prompt = prefix + prompt
                #将prompt变成id列表
                a_ids = tokenizer.encode(text=prompt, add_special_tokens=True, truncation=True,
                                         max_length=data_args.max_source_length)
                #将response变成id列表
                b_ids = tokenizer.encode(text=answer, add_special_tokens=False, truncation=True,
                                         max_length=data_args.max_target_length)
                #记录prompt长度
                context_length = len(a_ids)
                #prompt和response的ids组成新ids,最后加上结束符用以训练。
                input_ids = a_ids + b_ids + [tokenizer.eos_token_id]
                #tokenizer.pad_token_id 是Hugging Face transformers库中tokenizer的一个属性,它返回用于padding的token的ID。
                #实际训练时这个值为100。
                labels = [tokenizer.pad_token_id] * context_length + b_ids + [tokenizer.eos_token_id]
                
                #input_ids和labels扩充到最大训练长度
                pad_len = max_seq_length - len(input_ids)
                input_ids = input_ids + [tokenizer.pad_token_id] * pad_len
                labels = labels + [tokenizer.pad_token_id] * pad_len

                if data_args.ignore_pad_token_for_loss:
                    labels = [(l if l != tokenizer.pad_token_id else -100) for l in labels]

                model_inputs["input_ids"].append(input_ids)
                model_inputs["labels"].append(labels)

        return model_inputs
    
    def print_dataset_example(example):
        print("input_ids", example["input_ids"])
        print("inputs", tokenizer.decode(example["input_ids"]))
        print("label_ids", example["labels"])
        print("labels", tokenizer.decode(example["labels"]))

    #如果训练
    if training_args.do_train:
        if "train" not in raw_datasets:
            raise ValueError("--do_train requires a train dataset")
        train_dataset = raw_datasets["train"]
        
        #是否设定了训练数据条数,如果有的话就选随机选择max_train_samples条数据
        if data_args.max_train_samples is not None:
            max_train_samples = min(len(train_dataset), data_args.max_train_samples)
            train_dataset = train_dataset.select(range(max_train_samples))
        #开会话调用刚才preprocess_function_train映射成数据集
        with training_args.main_process_first(desc="train dataset map pre-processing"):
            train_dataset = train_dataset.map(
                preprocess_function_train,
                batched=True,
                num_proc=data_args.preprocessing_num_workers,
                remove_columns=column_names,
                load_from_cache_file=not data_args.overwrite_cache,
                desc="Running tokenizer on train dataset",
            )
        #输出第一个训练实例
        print_dataset_example(train_dataset[0])

    if training_args.do_eval:
        max_target_length = data_args.val_max_target_length
        if "validation" not in raw_datasets:
            raise ValueError("--do_eval requires a validation dataset")
        eval_dataset = raw_datasets["validation"]
        if data_args.max_eval_samples is not None:
            max_eval_samples = min(len(eval_dataset), data_args.max_eval_samples)
            eval_dataset = eval_dataset.select(range(max_eval_samples))
        with training_args.main_process_first(desc="validation dataset map pre-processing"):
            eval_dataset = eval_dataset.map(
                preprocess_function_eval,
                batched=True,
                num_proc=data_args.preprocessing_num_workers,
                remove_columns=column_names,
                load_from_cache_file=not data_args.overwrite_cache,
                desc="Running tokenizer on validation dataset",
            )
        print_dataset_example(eval_dataset[0])

    if training_args.do_predict:
        max_target_length = data_args.val_max_target_length
        if "test" not in raw_datasets:
            raise ValueError("--do_predict requires a test dataset")
        predict_dataset = raw_datasets["test"]
        if data_args.max_predict_samples is not None:
            max_predict_samples = min(len(predict_dataset), data_args.max_predict_samples)
            predict_dataset = predict_dataset.select(range(max_predict_samples))
        with training_args.main_process_first(desc="prediction dataset map pre-processing"):
            predict_dataset = predict_dataset.map(
                preprocess_function_eval,
                batched=True,
                num_proc=data_args.preprocessing_num_workers,
                remove_columns=column_names,
                load_from_cache_file=not data_args.overwrite_cache,
                desc="Running tokenizer on prediction dataset",
            )
        print_dataset_example(predict_dataset[0])

    # Data collator
    label_pad_token_id = -100 if data_args.ignore_pad_token_for_loss else tokenizer.pad_token_id
    #DataCollatorForSeq2Seq对象是用于处理和准备序列到序列任务的数据的数据整理器
    data_collator = DataCollatorForSeq2Seq(
        tokenizer,
        model=model,
        label_pad_token_id=label_pad_token_id,
        pad_to_multiple_of=None,
        padding=False
    )

    # Metric
    def compute_metrics(eval_preds):
        preds, labels = eval_preds
        if isinstance(preds, tuple):
            preds = preds[0]
        decoded_preds = tokenizer.batch_decode(preds, skip_special_tokens=True)
        if data_args.ignore_pad_token_for_loss:
            # Replace -100 in the labels as we can't decode them.
            labels = np.where(labels != -100, labels, tokenizer.pad_token_id)
        decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)

        score_dict = {
            "rouge-1": [],
            "rouge-2": [],
            "rouge-l": [],
            "bleu-4": []
        }
        for pred, label in zip(decoded_preds, decoded_labels):
            hypothesis = list(jieba.cut(pred))
            reference = list(jieba.cut(label))
            rouge = Rouge()
            scores = rouge.get_scores(' '.join(hypothesis) , ' '.join(reference))
            result = scores[0]
            
            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))

        for k, v in score_dict.items():
            score_dict[k] = float(np.mean(v))
        return score_dict

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

    #设置beam search数量
    training_args.generation_num_beams = (
        data_args.num_beams if data_args.num_beams is not None else training_args.generation_num_beams
    )
    # Initialize our Trainer
    trainer = Seq2SeqTrainer(
        model=model,
        args=training_args,
        train_dataset=train_dataset if training_args.do_train else None,
        eval_dataset=eval_dataset if training_args.do_eval else None,
        tokenizer=tokenizer,
        data_collator=data_collator,
        compute_metrics=compute_metrics if training_args.predict_with_generate else None,
        save_changed=model_args.pre_seq_len is not None
    )

    # Training
    if training_args.do_train:
        checkpoint = None
        #是否从历史检查点恢复
        if training_args.resume_from_checkpoint is not None:
            checkpoint = training_args.resume_from_checkpoint
        # elif last_checkpoint is not None:
        #     checkpoint = last_checkpoint
        #Gradient Checkpointing 是一种内存优化技巧,允许你用更少的GPU内存进行训练,尤其是对于大型模型和长序列
        #它的原理是不保存每一层的所有中间激活,而是只保存一部分,并在反向传播时重新计算其他激活。这样可以节省内存,但可能会增加计算时间。
        model.gradient_checkpointing_enable()
        #tensor有一个属性叫requires_grad,它决定是否在反向传播时计算这个tensor的梯度。通常,模型的权重会设置requires_grad=True,因为我们需要它们的梯度来更新权重
        model.enable_input_require_grads()
        #训练
        train_result = trainer.train(resume_from_checkpoint=checkpoint)
        # trainer.save_model()  # Saves the tokenizer too for easy upload

        metrics = train_result.metrics
        max_train_samples = (
            data_args.max_train_samples if data_args.max_train_samples is not None else len(train_dataset)
        )
        metrics["train_samples"] = min(max_train_samples, len(train_dataset))

        trainer.log_metrics("train", metrics)
        trainer.save_metrics("train", metrics)
        trainer.save_state()

    # Evaluation
    results = {}
    max_seq_length = data_args.max_source_length + data_args.max_target_length + 1
    if training_args.do_eval:
        logger.info("*** Evaluate ***")
        metrics = trainer.evaluate(metric_key_prefix="eval", do_sample=True, top_p=0.7, max_length=max_seq_length, temperature=0.95)
        max_eval_samples = data_args.max_eval_samples if data_args.max_eval_samples is not None else len(eval_dataset)
        metrics["eval_samples"] = min(max_eval_samples, len(eval_dataset))

        trainer.log_metrics("eval", metrics)
        trainer.save_metrics("eval", metrics)

    if training_args.do_predict:
        logger.info("*** Predict ***")
        predict_results = trainer.predict(predict_dataset, metric_key_prefix="predict", max_length=max_seq_length, do_sample=True, top_p=0.7, temperature=0.95)
        metrics = predict_results.metrics
        max_predict_samples = (
            data_args.max_predict_samples if data_args.max_predict_samples is not None else len(predict_dataset)
        )
        metrics["predict_samples"] = min(max_predict_samples, len(predict_dataset))

        trainer.log_metrics("predict", metrics)
        trainer.save_metrics("predict", metrics)

        if trainer.is_world_process_zero():
            if training_args.predict_with_generate:
                predictions = tokenizer.batch_decode(
                    predict_results.predictions, skip_special_tokens=True, clean_up_tokenization_spaces=True
                )
                predictions = [pred.strip() for pred in predictions]
                labels = tokenizer.batch_decode(
                    predict_results.label_ids, skip_special_tokens=True, clean_up_tokenization_spaces=True
                )
                labels = [label.strip() for label in labels]
                output_prediction_file = os.path.join(training_args.output_dir, "generated_predictions.txt")
                with open(output_prediction_file, "w", encoding="utf-8") as writer:
                    for p, l in zip(predictions, labels):
                        res = json.dumps({"labels": l, "predict": p}, ensure_ascii=False)
                        writer.write(f"{res}\n")
    return results


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


if __name__ == "__main__":
    main()


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读