中文模型的奋起直追:MOSS、baichuan-7B和ChatGLM2-6B的原理、部署与微调

第一部分 复旦MOSS

MOSS是复旦大学邱锡鹏团队推出的一个支持中英双语和多种插件的开源对话语言模型,moss-moon系列模型具有160亿参数,在FP16精度下可在单张A100/A800或两张3090显卡运行,在INT4/8精度下可在单张3090显卡运行

其基座语言模型在约七千亿中英文以及代码单词上预训练得到,后续经过对话指令微调、插件增强学习和人类偏好训练具备多轮对话能力及使用多种插件的能力

5.1 已开源的模型/数据

5.1.1 已开源的模型

  • moss-moon-003-base: MOSS-003基座模型,在高质量中英文语料上自监督预训练得到,预训练语料包含约700B单词,计算量约6.67x1022次浮点数运算。
  • moss-moon-003-sft: 基座模型在约110万多轮对话数据上微调得到,具有指令遵循能力、多轮对话能力、规避有害请求能力。
  • moss-moon-003-sft-plugin: 基座模型在约110万多轮对话数据和约30万插件增强的多轮对话数据上微调得到,在moss-moon-003-sft基础上还具备使用搜索引擎、文生图、计算器、解方程等四种插件的能力。
  • moss-moon-003-sft-int4: 4bit量化版本的moss-moon-003-sft模型,约占用12GB显存即可进行推理。
  • moss-moon-003-sft-int8: 8bit量化版本的moss-moon-003-sft模型,约占用24GB显存即可进行推理。
  • moss-moon-003-sft-plugin-int4: 4bit量化版本的moss-moon-003-sft-plugin模型,约占用12GB显存即可进行推理。
  • moss-moon-003-sft-plugin-int8: 8bit量化版本的moss-moon-003-sft-plugin模型,约占用24GB显存即可进行推理。
  • moss-moon-003-pm: 在基于moss-moon-003-sft收集到的偏好反馈数据上训练得到的偏好模型,将在近期开源。
  • moss-moon-003: 在moss-moon-003-sft基础上经过偏好模型moss-moon-003-pm训练得到的最终模型,具备更好的事实性和安全性以及更稳定的回复质量,将在近期开源。
  • moss-moon-003-plugin: 在moss-moon-003-sft-plugin基础上经过偏好模型moss-moon-003-pm训练得到的最终模型,具备更强的意图理解能力和插件使用能力,将在近期开源。

5.1.2 已开源的数据

  • moss-002-sft-data: MOSS-002所使用的多轮对话数据,覆盖有用性、忠实性、无害性三个层面,包含由text-davinci-003生成的约57万条英文对话和59万条中文对话
  • moss-003-sft-datamoss-moon-003-sft所使用的多轮对话数据,基于MOSS-002内测阶段采集的约10万用户输入数据和gpt-3.5-turbo构造而成,相比moss-002-sft-datamoss-003-sft-data更加符合真实用户意图分布,包含更细粒度的有用性类别标记、更广泛的无害性数据和更长对话轮数,约含110万条对话数据。目前仅开源少量示例数据,完整数据将在近期开源
  • moss-003-sft-plugin-datamoss-moon-003-sft-plugin所使用的插件增强的多轮对话数据,包含支持搜索引擎、文生图、计算器、解方程等四个插件在内的约30万条多轮对话数据。目前仅开源少量示例数据,完整数据将在近期开源
  • moss-003-pm-datamoss-moon-003-pm所使用的偏好数据,包含在约18万额外对话上下文数据及使用moss-moon-003-sft所产生的回复数据上构造得到的偏好对比数据,将在近期开源

5.2 MOSS模型量化版部署过程

我司七月杜助教写了一篇部署MOSS的教程,详情请点击:MOSS模型量化版部署过程


第二部分 baichuan-7B:与LLaMA的结构相同且表现优秀可商用

2.1 基于Transformer/RoPE/RMSNorm/SwiGLU + 1.2万亿训练数据/上下文窗口4096

baichuan-7B 是由百川智能(CEO为原搜狗创始人王小川)开发的一个开源可商用的大规模预训练语言模型

  • 基于 Transformer 结构,采用了和 LLaMA 一样的模型设计,比如
    位置编码:用的现阶段被大多模型采用的 rotary-embedding 方案,具有更好的外延效果
    激活层:SwiGLU, Feedforward 变化为 8/3 倍的隐含层大小,即 11,008
    Layer-Normalization: 基于 RMSNorm 的 Pre-Normalization

    关于LLaMA结构的解读,请参见:类ChatGPT模型LLaMA的解读与其微调:Alpaca-LoRA/Vicuna/BELLE
  • 在大约 1.2 万亿 tokens 上训练的 70 亿参数模型,支持中英双语
  • 上下文窗口长度为 4096
  • 在标准的中文和英文权威 benchmark(C-EVAL/MMLU)上均取得同尺寸最好的效果
    具体而言,C-Eval 数据集是一个全面的中文基础模型评测数据集,涵盖了 52 个学科和四个难度的级别
    我们使用该数据集的 dev 集作为 few-shot 的来源,在 test 集上进行了 5-shot 测试
    除了中文之外,作者团队也测试了模型在英文上的效果,MMLU 是包含 57 个多选任务的英文评测数据集,涵盖了初等数学、美国历史、计算机科学、法律等,难度覆盖高中水平到专家水平,是目前主流的LLM评测数据集


    一句话总结,即是在C-EVAL/MMLU等数据集上的表现好于ChatGLM-6B (当然,ChatGLM2-6B又变更强了)

2.2 baichuan-7B相比LLaMA-7B的优势

虽然baichuan-7B采用了和LLaMA一样的模型设计,但他们在原本的 LLaMA 框架上进行诸多修改

比如为提升模型的效果以及解码效率,做了

  • 分词改进
    词表大小为64K ,而LLaMA词表大小为32K

    具体而言,参考学术界方案使用 SentencePiece 中的 Byte-Pair Encoding (BPE) 作为分词算法,并且进行了以下的优化:
    目前大部分开源模型主要基于英文优化,因此对中文语料存在效率较低的问题,使用 2000 万条以中英为主的多语言语料训练分词模型,显著提升对于中文的压缩率

    对于数学领域,我们参考了 LLaMA 和 Galactica 中的方案,对数字的每一位单独分开,避免出现数字不一致的问题,对于提升数学能力有重要帮助
    对于罕见字词(如特殊符号等),支持 UTF-8 characters 的 byte 编码,因此做到未知字词的全覆盖
  • 数据集改进
    使用了大约 1.2T 中英 tokens 进行训练(基于开源的中英文数据和自行抓取的中文互联网数据以及部分高质量知识性数据进行的数据清洗),而 LLaMA 7B 使用 1T 英文 tokens 进行训练

比如为提升训练时的吞吐,做了以下优化

  • 算子优化技术:采用更高效算子,如 Flash-Attention,NVIDIA apex 的 RMSNorm 等。
  • 算子切分技术:将部分计算算子进行切分,减小内存峰值。
  • 混合精度技术:降低在不损失模型精度的情况下加速计算过程。
  • 训练容灾技术:训练平台和训练框架联合优化,IaaS + PaaS 实现分钟级的故障定位和任务恢复。
  • 通信优化技术,具体包括:
    采用拓扑感知的集合通信算法,避免网络拥塞问题,提高通信效率
    根据卡数自适应设置 bucket size,提高带宽利用率
    根据模型和集群环境,调优通信原语的触发时机,从而将计算和通信重叠

基于上述的几个优化技术,使得在千卡 A800 显卡上达到了 7B 模型 182 TFLOPS 的吞吐,GPU 峰值算力利用率高达 58.3%

2.3 baichuan-7B的微调

本次微调参考项目:https://github.com/wp931120/baichuan_sft_lora

由于baichuan没有 supervised finetune 这一步,没有和人类意图进行对齐,经常听不懂你下达的指令。该项目遂利用belle 0.5M 指令微调数据,采用qlora的量化微调的方式对百川大模型进行人类意图对齐训练

训练前置条件,先从huggingface 中将baichuan7b 大模型权重 ,然后,最后运行sft_lora.py 脚本
先将百川LLM 采用qlora的 nf4 和双重量化方式进行量化
在采用lora进行指令微调

本次微调baichuan-7B的步骤如下

  1. 微调之前的准备
    下载项目仓库
    git clone https://github.com/wp931120/baichuan_sft_lora.git
    cd baichuan_sft_lora
    配置环境
    conda create -n baichuan-7b python=3.9
    conda activate baichuan-7b
    pip install -r requirements.txt
    数据集下载
    sft 数据集采用的是belle 0.5M
    下载地址:https://huggingface.co/datasets/BelleGroup/train_0.5M_CN/tree/main
    将 belle 数据集 train_0.5M_CN 下载到本地放到项目目录下的dataset文件夹下
  2. 将百川LLM 采用qlora的 nf4 和双重量化方式进行量化
  3. 再采用lora进行指令微调
    wp931120x/baichuan_4bit_lora · Hugging Face
  4. 修改并运行sft_lora.py文件
    将sft_lora.py中的模型路径设置为自己的模型路径
    执行python sft_lora.py运行代码
    import os  # 导入os模块,这个模块提供了一种方便的使用操作系统依赖功能的方式
    os.environ['CUDA_VISIBLE_DEVICES'] = '0'  # 设置CUDA可见设备,'0'表示仅使用第一块GPU
    
    from datasets import load_dataset  # 导入load_dataset函数,用于加载数据集
    import transformers                # 导入transformers库,这是一个常用的NLP库
    
    # 导入Trainer和TrainingArguments,分别用于模型的训练和训练参数的设置
    from transformers import Trainer, TrainingArguments
    # 导入AutoTokenizer和AutoModelForCausalLM,分别用于自动化地从预训练模型中获取Tokenizer和模型
    from transformers import AutoTokenizer, AutoModelForCausalLM
    # 导入BitsAndBytesConfig,用于设置模型的量化配置  
    from transformers import BitsAndBytesConfig
      
    # 导入一些特定的函数和配置类
    from peft import (
        LoraConfig,
        get_peft_model,
        prepare_model_for_kbit_training,
        set_peft_model_state_dict,
    )
    import torch  # 导入PyTorch库,这是一个常用的深度学习库
    
    
    # 定义一些配置信息
    CUTOFF_LEN = 1024  
    VAL_SET_SIZE = 2000
    DATA_PATH = "./dataset/Belle_open_source_0.5M.json" 
    OUTPUT_DIR = "baichuansft"
    resume_from_checkpoint = "baichuansft"
    
    # 设置设备映射,""表示默认设备,0表示设备编号
    device_map = {"": 0}
    # 使用AutoTokenizer从预训练模型中获取Tokenizer
    tokenizer = AutoTokenizer.from_pretrained("./baichuan-7B",trust_remote_code=True)
    # 使用AutoModelForCausalLM从预训练模型中获取模型,并设置量化配置
    model = AutoModelForCausalLM.from_pretrained("./baichuan-7B",
                                                 trust_remote_code=True,
                                                 quantization_config=BitsAndBytesConfig(
                                                     load_in_4bit=True,
                                                     bnb_4bit_compute_dtype=torch.bfloat16,
                                                     bnb_4bit_use_double_quant=True,
                                                     bnb_4bit_quant_type='nf4'
                                                 ),
                                                 device_map=device_map)
    
    model = prepare_model_for_kbit_training(model)  # 准备模型进行kbit训练
    
    # 导入bitsandbytes模块
    import bitsandbytes as bnb
    
    # 定义一个函数,用于找到模型中所有的线性层的名称
    def find_all_linear_names(model):
        cls = bnb.nn.Linear4bit 
        lora_module_names = set()
        for name, module in model.named_modules():  # 遍历模型中的所有模块
            if isinstance(module, cls):  # 如果模块是线性层
                names = name.split('.')
                lora_module_names.add(names[0] if len(names) == 1 else names[-1])  # 添加到线性层名称集合中
    
        if 'lm_head' in lora_module_names:  # 如果'lm_head'在名称集合中,需要移除
            lora_module_names.remove('lm_head')
        return list(lora_module_names)  # 返回线性层名称列表
    
    # 获取所有的线性层的名称
    modules = find_all_linear_names(model)
    
    # 设置LoRA配置
    config = LoraConfig(
        r=8,
        lora_alpha=16,
        lora_dropout=0.05,
        bias="none",
        target_modules=modules,
        task_type="CAUSAL_LM",
    )
    
    # 获取用于训练的模型
    model = get_peft_model(model, config)
    tokenizer.pad_token_id = 0  # 设置tokenizer的pad_token_id为0
    
    # 如果有设置从检查点恢复
    if resume_from_checkpoint:
        # 检查可用的权重并加载
        checkpoint_name = os.path.join(
            resume_from_checkpoint, "pytorch_model.bin"
        )  # 完整的检查点
        # 如果完整的检查点不存在,则加载LoRA模型的检查点
        if not os.path.exists(checkpoint_name):
            checkpoint_name = os.path.join(
                resume_from_checkpoint, "adapter_model.bin"
            )  # 仅LoRA模型 - 上面的LoRA配置必须匹配
            resume_from_checkpoint = (
                False  # 所以训练器不会尝试加载状态
            )
        if os.path.exists(checkpoint_name):
            print(f"Restarting from {checkpoint_name}")
            adapters_weights = torch.load(checkpoint_name)
            set_peft_model_state_dict(model, adapters_weights)  # 设置模型的状态字典
        else:
            print(f"Checkpoint {checkpoint_name} not found")
    
    # 加载数据集
    data = load_dataset("json", data_files=DATA_PATH)
    
    # 定义tokenize函数,用于将输入进行tokenize
    def tokenize(prompt, add_eos_token=True):
        # 这里是tokenize的具体操作
        result = tokenizer(
            prompt,
            truncation=True,
            max_length=CUTOFF_LEN,
            padding=False,
            return_tensors=None,
        )
        # 添加EOS token
        if (
                result["input_ids"][-1] != tokenizer.eos_token_id
                and len(result["input_ids"]) < CUTOFF_LEN
                and add_eos_token
        ):
            result["input_ids"].append(tokenizer.eos_token_id)
            result["attention_mask"].append(1)
    
        if add_eos_token and len(result["input_ids"]) >= CUTOFF_LEN:
            result["input_ids"][CUTOFF_LEN - 1] = tokenizer.eos_token_id
            result["attention_mask"][CUTOFF_LEN - 1] = 1
    
        # 输入和标签都是input_ids
        result["labels"] = result["input_ids"].copy()
    
        return result
    
    # 定义generate_and_tokenize_prompt函数,用于生成并tokenize输入
    def generate_and_tokenize_prompt(data_point):
        instruction = data_point['instruction']
        input_text = data_point["input"]
        input_text = "Human: " + instruction + input_text + "\n\nAssistant: "
        input_text = tokenizer.bos_token + input_text if tokenizer.bos_token != None else input_text
        target_text = data_point["output"] + tokenizer.eos_token
        full_prompt = input_text + target_text
        tokenized_full_prompt = tokenize(full_prompt)
        return tokenized_full_prompt
    
    # 划分训练集和验证集,并进行shuffle和map操作
    if VAL_SET_SIZE > 0:
        train_val = data["train"].train_test_split(
            test_size=VAL_SET_SIZE, shuffle=True, seed=42
        )
        train_data = train_val["train"].shuffle().map(generate_and_tokenize_prompt)
        val_data = train_val["test"].shuffle().map(generate_and_tokenize_prompt)
    else:
        train_data = data['train'].shuffle().map(generate_and_tokenize_prompt)
        val_data = None
    
    # 创建Trainer对象,用于进行训练
    trainer = Trainer(
        model=model,
        train_dataset=train_data,
        eval_dataset=val_data,
        args=TrainingArguments(
            num_train_epochs=1,
            per_device_train_batch_size=1,
            per_device_eval_batch_size=1,
            learning_rate=3e-4,
            gradient_accumulation_steps=4,
            evaluation_strategy="steps" if VAL_SET_SIZE > 0 else "no",
            save_strategy="steps",
            eval_steps=2000 if VAL_SET_SIZE > 0 else None,
            save_steps=2000,
            output_dir=OUTPUT_DIR,
            report_to = "tensorboard",
            save_total_limit=3,
            load_best_model_at_end=True if VAL_SET_SIZE > 0 else False,
            optim="adamw_torch"
        ),
        data_collator=transformers.DataCollatorForSeq2Seq(tokenizer,
                                                          pad_to_multiple_of=8,
                                                          return_tensors="pt",
                                                          padding=True),
    )
    
    # 进行训练
    trainer.train(resume_from_checkpoint=False)
    # 保存预训练模型
    model.save_pretrained(OUTPUT_DIR)
    最终,显存占用为7G左右

 第三部分 ChatGLM2-6B的部署与微调

// 待更

猜你喜欢

转载自blog.csdn.net/v_JULY_v/article/details/131551173