llama-factory 系列教程 (六),linux shell 脚本自动实现批量大模型的训练、部署与评估

背景

最近在做大模型微调训练的评估,每次都要手动训练大模型,手动评估。
发现这样太浪费时间了,于是就尝试着使用linux shell 脚本,利用 for 循环自动实现大模型的训练、部署与评估。

实验:在不同的文本分类数据集尺寸上微调大模型

在这次实验中,我们分别使用了100、500、1000和2000条数据对大模型进行了微调。我们的目标是评估不同大小的数据集对大模型表现的影响。

项目开源地址:https://github.com/JieShenAI/csdn/blob/main/24/07/few_shot_sft/readme.md

实验方法

为了高效地完成微调任务,我们使用了Linux shell脚本 自动化运行。具体步骤如下:

  1. 数据准备:将不同大小的数据集准备好。
  2. 批量微调:利用Linux shell脚本批量化地微调大模型,自动保存微调后的模型权重。
  3. 自动评估:微调完成后,脚本会自动调用评估程序,对模型在测试集上的表现进行评估。

这种方法极大地提高了工作效率。若不使用自动化脚本,我们需要手动逐个训练模型,然后手动运行评估程序,这不仅耗时,而且容易出错。

优势

  • 时间节省:利用自动化脚本,我们可以在夜间让计算机自行完成微调和评估工作,第二天早上起床后即可查看结果。
  • 减少人工干预:整个过程无需过多人工干预,减少了人工的时间与精力。

通过这种方式,我们能够得出不同大小数据集对大模型表现的影响,为进一步的研究提供了宝贵的数据支持。

项目文件介绍

  • build_llm_data.ipynb
    从训练集中随机筛选并转换为Alpaca样式的数据集格式
    在大模型的微调过程中,从训练集中随机抽取不同规模的数据样本,以便进行模型的测试和优化。本文从训练集中随机筛选100、500、1000和2000条数据,并将这些数据转换为Alpaca样式的微调数据集格式,最后将筛选后的数据保存在data文件夹下。
    本文在文本分类数据集上进行模型训练。
    下述是转化为大模型微调的数据集样例:
    [
      {
        "instruction": "You are a document classifier. When given the text, you classify the text into one of the following categories:\n\n\"Human Necessities\"\n\"Performing Operations; Transporting\"\n\"Chemistry; Metallurgy\"\n\"Textiles; Paper\"\n\"Fixed Constructions\"\n\"Mechanical Engineering; Lightning; Heating; Weapons; Blasting\"\n\"Physics\"\n\"Electricity\"\n\"General tagging of new or cross-sectional technology\"\n\"Unknown\"\n\nYour output should only contain one of the categories and no explanation or any other text.",
        "input": "Classify the document:\nan image sensor device may include a dual - gated charge storage region within a substrate . the dual - gated charge storage region includes first and second diodes within a common charge generating region . this charge generating region is configured to receive light incident on a surface of the image sensor device . the first and second diodes include respective first conductivity type regions responsive to first and second gate signals , respectively . these first and second gate signals are active during non - overlapping time intervals .",
        "output": "Electricity"
      },
      ...
    ]
    
  • train.sh
    在开始训练之前,需要在 LLaMA-Factory/data/dataset_info.json 文件中注册 data 目录下的数据集。接下来,从 LLaMA-Factory 的可视化界面获取 LoRA 微调的命令行。train.sh 脚本实现了批量化训练,并在训练完成后保存 LoRA 的权重。
    # 对所有切分后的数据集进行训练
    cd LLaMA-Factory
    data_files=(llm_train_100 llm_train_500 llm_train_1000 llm_train_2000)
    echo ${data_files[@]}
    
    for data_file in ${data_files[@]}; do
        echo ${data_file}
        llamafactory-cli train \
            --stage sft \
            --do_train True \
            --model_name_or_path ZhipuAI/glm-4-9b-chat \
            --preprocessing_num_workers 16 \
            --finetuning_type lora \
            --template glm4 \
            --flash_attn auto \
            --dataset_dir data \
            --dataset ${data_file} \
            --cutoff_len 1024 \
            --learning_rate 5e-05 \
            --num_train_epochs 3.0 \
            --max_samples 100000 \
            --per_device_train_batch_size 2 \
            --gradient_accumulation_steps 4 \
            --lr_scheduler_type cosine \
            --max_grad_norm 1.0 \
            --logging_steps 5 \
            --save_steps 100 \
            --warmup_steps 0 \
            --optim adamw_torch \
            --packing False \
            --report_to none \
            --output_dir saves/GLM-4-9B-Chat/lora/240731-${data_file} \
            --fp16 True \
            --plot_loss True \
            --ddp_timeout 180000000 \
            --include_num_input_tokens_seen True \
            --lora_rank 8 \
            --lora_alpha 16 \
            --lora_dropout 0 \
            --lora_target all
    done
    
    # nohup bash train.sh > train.log 2>&1 &
    
  • eval.sh
    在训练完成后,使用 VLLM 部署训练完成的 LoRA 模型,并将其部署成 API 接口,便于通过 infer_eval.py 进行评估。eval.sh 脚本实现了对训练模型的批量部署与评估,自动化地逐个部署和推理。在评估完成一个大模型后,脚本会杀死正在部署的进程,开始部署下一个大模型,并进行新的评估。
    # conda activate llm
    cd LLaMA-Factory
    
    # kw_arr=(llm_train_100 llm_train_500 llm_train_1000 llm_train_2000)
    kw_arr=(llm_train_100 llm_train_500 llm_train_1000)
    
    
    for kw in "${kw_arr[@]}"; do
        echo $kw
        CUDA_VISIBLE_DEVICES=0 API_PORT=8000 llamafactory-cli api \
            --model_name_or_path /home/jie/.cache/modelscope/hub/ZhipuAI/glm-4-9b-chat \
            --adapter_name_or_path ./saves/GLM-4-9B-Chat/lora/240731-${kw} \
            --template glm4 \
            --finetuning_type lora \
            --infer_backend vllm \
            --vllm_enforce_eager &
            
        # 模型预测推理脚本,便于后续评估
        python ../infer_eval.py ${kw} > ../logs/${kw}.log 2>&1
        # 杀掉服务进程
        pkill -f llamafactory
        echo "Stopped llamafactory"
    done
    
    # nohup bash eval.sh > eval.log 2>&1 &
    
  • infer_eval.py
    利用在线部署的大模型,结合 LangChain 工具,在测试集上逐个进行评估。
    import os
    import json
    import random
    import logging
    import argparse
    import pickle
    import evaluate
    from tqdm import tqdm
    from datasets import load_dataset
    from dataclasses import dataclass, field
    from langchain_openai import ChatOpenAI
    from langchain_core.messages import HumanMessage, SystemMessage
    from langchain_core.output_parsers import StrOutputParser
    
    
    os.environ['HTTP_PROXY'] = 'http://127.0.0.1:7890'
    os.environ['HTTPS_PROXY'] = 'http://127.0.0.1:7890'
    
    
    logging.basicConfig(
        format="%(asctime)s - %(levelname)s - %(name)s -   %(message)s",
        datefmt="%m/%d/%Y %H:%M:%S",
        handlers=[logging.FileHandler('../eval.log')],
        level=logging.INFO
    )
    
    
    @dataclass
    class EvalData:
        name : str
        in_cnt : int = 0
        not_in_cnt : int = 0
        preds : list = field(default_factory=list)
        labels : list = field(default_factory=list)
        not_in_texts : list = field(default_factory=list)
        eval : dict = field(default_factory=dict)
    
    def save_obj(obj, name):  
        """  
        将对象保存到文件  
        :param obj: 要保存的对象  
        :param name: 文件的名称(包括路径)  
        """  
        with open(name, 'wb') as f:  
            pickle.dump(obj, f, pickle.HIGHEST_PROTOCOL)
    
    
    def load_obj(name):  
        """  
        从文件加载对象  
        :param name: 文件的名称(包括路径)  
        :return: 反序列化后的对象  
        """  
        with open(name, 'rb') as f:  
            return pickle.load(f)
    
    
    LABELS_DICT = {
        0: "Human Necessities",
        1: "Performing Operations; Transporting",
        2: "Chemistry; Metallurgy",
        3: "Textiles; Paper",
        4: "Fixed Constructions",
        5: "Mechanical Engineering; Lightning; Heating; Weapons; Blasting",
        6: "Physics",
        7: "Electricity",
        8: "General tagging of new or cross-sectional technology",
    }
    
    
    LABELS_NAME = [
        LABELS_DICT[i]
        for i in range(9)
    ]
    
    LABELS_2_IDS = {
        v : k
        for k, v in LABELS_DICT.items()
    }
    
    
    def compute_metrics(pred, label):
        res = {}
        accuracy = evaluate.load("accuracy")
        res.update(accuracy.compute(
                predictions=pred, 
                references=label
            ))
    
        precision = evaluate.load("precision")
        res.update(precision.compute(
                predictions=pred, 
                references=label,
                average="macro"
            ))
    
        recall = evaluate.load("recall")
        res.update(recall.compute(
                predictions=pred, 
                references=label,
                average="macro"
            ))
    
        f1 = evaluate.load("f1")
        res.update(f1.compute(
                predictions=pred, 
                references=label,
                average="macro"
            ))
        return res
    
    
    def eval(kw):
        eval_data = EvalData(name=kw)
        model = ChatOpenAI(
            api_key="0",
            base_url="http://localhost:8000/v1",
            temperature=0
        )
    
        valid_dataset = load_dataset(
            "json",
            data_files="../data/llm_valid.json"
        )["train"]
        # labels = valid_dataset["output"][:50]
        labels = valid_dataset["output"]
        
        eval_data.labels = labels
        
        parser = StrOutputParser()
        preds = []
        cnt = 0
        for item in tqdm(valid_dataset):
            cnt += 1
            messages = [
                SystemMessage(content=item['instruction']),
                HumanMessage(content=item['input']),
            ]
            chain = model | parser
            pred = chain.invoke(messages).strip()
            preds.append(pred)
            # if cnt == 50:
            #     break
        
        eval_data.preds = preds
    
        not_in_texts = []
        in_cnt = 0
        not_in_cnt = 0
    
        for pred in preds:
            if pred in LABELS_NAME:
                in_cnt += 1
            else:
                not_in_cnt += 1
                not_in_texts.append(pred)
        
        eval_data.in_cnt = in_cnt
        eval_data.not_in_cnt = not_in_cnt
        eval_data.not_in_texts = not_in_texts
        
        pred_num = [
            LABELS_2_IDS[pred] if pred in LABELS_NAME else random.choice(range(9))
            for pred in preds
        ]
        label_num = [
            LABELS_2_IDS[label]
            for label in labels
        ]
        
        eval_data.eval = compute_metrics(pred=pred_num, label=label_num)
        
        logging.info(f"in_cnt: {in_cnt}, not_in_cnt: {not_in_cnt}")
        logging.info(f"eval: {eval_data.eval}")
        
        # 推理结果保存
        save_obj(
                eval_data,
                f"../objs/{kw}.pkl"
            )
    
    
    if __name__ == "__main__":
        parser = argparse.ArgumentParser(description="输入大模型名,开始推理")
        parser.add_argument("kw", help="目前部署的大模型名字")
        args = parser.parse_args()
        logging.info(args.kw)
        eval(args.kw)
    
  • see_result.ipynb
    导入保存到objs文件夹中的预测结果,并进行结果的渲染
    最后结果如下图所示,数据集量越大效果越好:

result.png

各位读者在看完,训练脚本 train.sh, 部署和推理脚本 eval.sh,应该已经明白本项目大致流程。

一言以蔽之,就是在shell脚本中,使用 for 循环实现训练、部署、评估流程。

若大家想复现本文实验,本项目已经在Github开源,项目开源地址:https://github.com/JieShenAI/csdn/blob/main/24/07/few_shot_sft/readme.md

本文主要是为大家展示,使用linux shell 脚本,自动化处理的流程,故在项目的具体细节没有过多的解释。

应该与Bert文本分类进行对比,就可以明显看出大模型的few-shot能力,有读者感兴趣可以实现一下。

  • 30
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jieshenai

为了遇见更好的文章

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值