version: 2559fc82 (2024-04-02 10:17:24)

LLM 简介

LLM,全称Large Language Model,即大语言模型。近些年,随着GPT3的诞生,人们发现给更大参数量的模型喂更多数据,可以让模型具备一定的智能,俗称力大砖飞。

如果你对LLM完全没有概念,你可以选择任意一个在线服务体验一下,感受LLM的魅力。下面是一些服务:

  1. ChatGPT 官方,需国外信用卡与手机号 / 外国镜像 / 国内镜像
  2. Claude 官方,需国外信用卡与手机号
  3. Google Gemini 官方,需要Google账号

P.S. 使用镜像时请注意数据安全

LLM 是怎么训练的

在开始介绍之前,我们假设LLM是一个黑盒,即 output_text = LLM(input_text)。 从你的使用体验来说,你可以认为是 answer = LLM(question)。

那LLM是怎么训练出来的呢,其实是一个多阶段的过程。 首先,我们有海量的文本,比如网页、书籍、小说、百科等等,我们也有一些问答数据集,但是数量相比前者少很多,那么怎么同时用上这两种数据呢?

第一阶段,我们使用那些海量文本来训练续写任务,即在有raw_text的情况下,有

input_text = raw_text.drop(index=raw_text.length - 1)
output_text = raw_text.drop(index=0)

然后基于这样的数据去训练。这个过程被称为Pretraining,这样训练得出的模型称为Base model,只有续写能力,无法进行对话。

第二阶段,使用那些问答数据集,他们天然的就是满足上面的数据格式的,因此可以做训练。这个过程被称为Finetuning,训练完成后就是Chat Model对话模型了。

一般来说,有这两个阶段就可以产生一个可以进行对话的模型了,当然那些对话模型为了更好的用户体验,和回答的安全性,还做了对齐训练,这个在这里就不多介绍了。

LLM 是怎么工作的

LLM是一个神经网络,现在主流的大模型一般都是 Decoder-only 的 Transformer。这里我们不具体介绍这两个概念,把Transformer当个黑盒,你可以理解成这样 next_word_probs = Transformer(input_text)。 即他会在给定输入的情况下,输出下一个词的概率,而通过一系列的采样策略,我们可以根据概率来选择合适的词,即 next_word = Sampler(next_word_probs)。

整个流程大概如下:


i = 1
loop
    next_word_probs = Transformer(input_text)
    next_word = Sampler(next_word_probs)
    input_text += next_word
until next_word == EOS
output_text = input_text

也就是我们不停的去预测下一个词,把他拼到输入上,直到遇到结束符EOS。

LLM 的数据是怎么表示的

上面为了简化,我们假设Transformer模型输入输出的基本单位是单词word,但是实际上我们并不是使用word,而是使用tokens。 什么是tokens,你可以理解成一个更加灵活的word,即他可以是一个word,也可以是多个word,也可以是word的碎片。

为什么不直接使用words呢,因为他有以下一些问题

  1. 特殊符号数量太多
  2. 词表外的新单词无法表示
  3. 有时压缩比太低

为了解决上述问题,我们会有一个分词器Tokenizer,有 tokens = Tokenizer.encode(text) 和 text = Tokenizer.decode(text)。

Tokenizer中会包含一些特殊符号,比如我们前面说的EOS,还有PAD,是为了把输入组成多条一样长度的数据时填充的内容,有些模型还会有BOS,就是输入开始时加入的固定token。

问答数据具体是怎么构造的

为了区别在训练Chat Model时区分原有的续写任务和新的对话任务,我们需要把问答数据以一种特殊的方式进行组装,组装后的文本被称为Prompt。而我们一般是基于模板Template来进行组装的。 比如对于下列对话 饭吃了么? 吃了。 那么我们可以把他拼成这样一个格式: 问题:饭吃了么 回答:吃了。 下面是一些主流模型的Prompt例子

  • <|im_start|>human\n{输入}<|im_end|><|im_start|>bot\n{输出}<|im_end|>
  • ### Human: {输入}\n### Assistant: {输出}<|im_end|>

Pretraining 和 Finetuning 有什么区别?

除了前面介绍了数据组装的不一样,还有一些地方有些许的不同:

  1. 对于过长的文本Pretraining会倾向于截断,而Finetuning则是更偏向于丢弃
  2. 对于Prompt的输入部分,Finetuning通常不计算loss

从用法上来说

  1. Pretraining需要海量数据,数据质量要求稍低,Finetuning要求较高质量的少量数据
  2. 对于大量新知识(如学习新语言、医学术语等等),一般Finetuning可能不够,需要Pretraining
  3. Pretraining的成本远高于Finetuning

如何定制自己的模型

这是一个比较大的问题,我们建议先试试ChatGPT/GPT-4之类的大模型能否大致在功能层面满足你的需求(不涉及风格、安全性和内部数据的情况下)。如果这类大模型都没法完成你的需求,那么用本地的开源模型应该也是做不了的。

OK, 我们假设他可以做,但是由于种种原因(价格贵、数据安全)我们不能使用这些闭源的大模型服务,那我们应该怎么办呢?

我们依次尝试以下的方法(代价从小到大)

  1. 如果我们有比较少的数据,而且这些数据有现有的接口可以提供,而且内容时常变动,那么我们可以使用RAG。

    什么是RAG? 即Retrieval-Augmented Generation,就是像搜索那样,先把用户的请求通过关键词搜索或者向量搜索在知识库中出关联的文档,然后我们将这些关联的文档作为输入和用户输入拼在一起喂给LLM,从而拿到输出。如果使用RAG,那么我们可以先不训练模型,可以先直接在训练好的Chat模型上进行尝试。如果效果不够好,那么也可以Finetuning一波。

  2. 如果我们有少量的数据,而且这些数据不会变动,那么我们可以做Finetuning。

    数据量可以不用很多,几千到几万条即可,就是质量和多样性要保证。

  3. 如果我们有中等数量(十万到千万条)的数据,那么可以直接从Base Model进行训练。

  4. 如果你有大量数据(亿条数据),那么可以做Continue Pretraining。

  5. 当然如果你有海量数据(千亿条),那么从零Pretraining吧。

ChatGPT等闭源模型和开源模型有什么区别

  1. ChatGPT/GPT4闭源,数据安全无法保证
  2. 闭源模型效果一般优于开源模型
  3. 较小的开源模型可以本地部署,自己训练

如何搞数据

首先当然是使用公开的数据集了,有各种各样的数据集,可以在Huggingface Datasets中搜索。 这里也推荐几个质量较高的Finetuning数据集

  1. RoleLLM 角色扮演数据集,中英文
  2. CharacterLLM 复旦的角色扮演数据集,英语

如果从Base模型开始训练可以使用下面的数据

  1. MOSS 复旦NLP搞的中文多轮对话数据集
  2. UltraChat 清华的多轮对话数据集,英语

当然只有公开数据可能不能满足定制化的需求的,所以你手头上需要有一批自己的数据。

  1. 如果是高质量的问答数据,而且数量够多,OK我们可以直接开始训练了
  2. 如果只有原始数据,可以用ChatGPT/GPT-4之类的大模型来构造问答对。
  3. 如果数据量不够,一方面可以用ChatGPT/GPT-4之类的大模型来扩充,或者就是各种方法来搜刮数据(爬虫、解包、ASR转译、人肉标注等等)

从Chat模型训练和Base模型训练有什么区别

  1. Chat模型做过对话训练,因此可以不需要公开的通用语料。只用特定任务的语料即可。
  2. Chat模型有特定的Prompt模板,需要用那个模板组装数据。而Base模型,你可以任意选择Prompt模板。
  3. Chat模型一般都做过对齐训练,所以如果需要训练一些(你懂得)的内容的时候,你需要从Base模型开始训练。
  4. 从Base模型训练需要添加通用数据,而且需要一定数据量的数据。一般我们会做两阶段的Finetuning,通用指令Finetuning(IFT)以及特定任务微调(SFT)。

我该选择什么开源模型

选择标准

  1. 首先需要看你的显卡是否是老黄(Nvidia)家的?有多少显存(可以通过nvidia-smi查看)?这决定了你可以使用什么参数量级的模型。一般而言,如果是3090/4090, 那么你有24GB的显存,你可以最多微调13-14B左右的模型。如果是3080, 那只有10GB显存,最多能微调6-7B左右的模型。另外不建议使用旧于10系的显卡。

    14B中的B是什么意思?其实是模型参数量,Billon,即十亿参数。一般大公司内部小团队会使用6-14B左右的模型,再小的模型基本上就无法很好的完成多轮对话了,只能做点较为简单的小任务。

  2. 决定了参数量范围后,我们可以开始找模型

    a. 现在最火的国外模型是Mistral-7B(Base模型),Zephyr-7B(Chat模型)以及Google最近发布的Gemma-7B。

    b. 如果需要中文模型,那么有Qwen、Baichuan、InternLM、ChatGLM等很多选择,从我们的使用体验来说,Qwen的体验是比较好的,而且他支持多语言词表,可以提供一定的多语种能力。

    c. 可以在Huggingface Models搜索公开的模型

总结

这一章节我们大致介绍了LLM的一些基本概念和玩法。我们可以先挑选模型和收集数据,准备好之后我们就可以正式开始炼丹了。

LLM 上手

下面开始!!!

环境配置

Windows

如果使用Windows操作系统,建议使用WSL(Windows Subsystem for Linux),具体可以参考官方文档进行安装。

MacOS

这篇文章使用Homebrew作为包管理器。 如果是M1+芯片的Mac,可以使用MLX来加速训练和推理。这里不建议使用非M1+芯片的Mac进行训练。

注:根据这个issue, MLX目前也不是在所有情况下都能加速,所以也可以用PyTorch的device="mps"来试试看。

通用

到这里,不管你是WSL还是Mac,假设我们都是POSIX的环境。基本的步骤都是通用的了。

  1. 安装Python开发环境

    建议使用conda来管理Python环境,从这里或者镜像下载安装包进行安装。安装后记得初始化conda环境。

    source <PATH_TO_CONDA>/bin/activate
    conda init
    
    # <PATH_TO_CONDA>是你指定的安装位置,一般为`~/anaconda3`
    
    # 然后你可以设置是否自动激活这个Python环境
    ## 如果自动激活
    conda config --set auto_activate_base True
    ## 如果不要自动激活,如果选择这个,每次需要`conda activate`来激活。
    conda config --set auto_activate_base False
    
    # (可选)配置环境源,加速环境配置,可以参考https://mirrors.tuna.tsinghua.edu.cn/help/anaconda/
    
    # MacOS用户需要检查是否安装的arm版本的python,请确保下面的命令输出arm而不是i386
    python3 -c "import platform; print(platform.processor())"
    
  2. 安装PyTorch

    PyTorch是热门的深度学习框架,我们需要先安装这个基础组件。可以参考这里或者下面的命令。

    # Linux / WSL
    conda install pytorch torchvision torchaudio pytorch-cuda=11.8 -c pytorch -c nvidia
    # MacOS
    conda install pytorch::pytorch torchvision torchaudio -c pytorch
    
  3. 安装其他依赖

    要本地使用LLM,我们还需要一些依赖,可以用下面的命令进行安装,

    pip install accelerate transformers -i https://pypi.tuna.tsinghua.edu.cn/simple
    

    对于MacOS上使用MLX的用户可以安装下面的依赖:

    MLX安装方式如下:

    pip install mlx-lm -i https://pypi.tuna.tsinghua.edu.cn/simple
    

    由于MLX仍处于快速开发阶段,为了快点享受到新功能/Bugfix,可以clone该repo并进行源码安装。

    git clone https://https://github.com/ml-explore/mlx-examples.git
    cd mlx-examples
    pip install -e ./llms -i https://pypi.tuna.tsinghua.edu.cn/simple
    
    # 如需更新,只需进入mlx-examples目录,执行`git pull`即可,当然最好也执行一下`pip install -U mlx -i https://pypi.tuna.tsinghua.edu.cn/simple`来升级mlx。
    

下载模型

一般基于Huggingface或者ModelScope(国内)来下载模型。

安装组件

# Ubuntu / Debian / WSL
sudo apt install git git-lfs
# MacOS
brew install git git-lfs

下载模型实例

以Qwen 1.5-1.8B-Chat为例,下面是下载的命令

# 选择一条执行即可
## 国外 Huggingface
git lfs clone https://huggingface.co/Qwen/Qwen1.5-1.8B-Chat
## 国内 Modelscope
git lfs clone https://www.modelscope.cn/Qwen/Qwen1.5-1.8B-Chat.git

本地使用

下面是一段简单的LLM聊天CLI的代码,可以存在test.py里面使用python test.py来运行。可以输入问题,回车后会给出回答,可以使用\quit来退出,\clear来开启新一轮对话。

import readline
import os
import platform

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
try:
    import mlx_lm
    is_mlx_available = True
except ImportError:
    is_mlx_available = False

use_mlx = True

if platform.system() == "Darwin":
    device = "mps"
    dtype = torch.float16
else:
    device = "cuda"
    dtype = "auto"

path = "<DOWNLOAD_PATH>/Qwen1.5-1.8B-Chat"

if is_mlx_available and use_mlx:
    model, tokenizer = mlx_lm.load(path)
else:
    model = AutoModelForCausalLM.from_pretrained(
        path,
        torch_dtype=dtype,
        device_map="auto"
    )
    tokenizer = AutoTokenizer.from_pretrained(path)

default_messages = [
    {"role": "system", "content": "You are a helpful assistant."},
]

messages = list(default_messages)
while True:
    prompt = input("Human:").strip("\n").strip()
    if prompt == "\\quit":
        break
    elif prompt == "\\new":
        os.system("clear")
        messages = list(default_messages)
        continue
    else:
        messages.append({"role": "user", "content": prompt})

    text = tokenizer.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=True
    )

    if is_mlx_available and use_mlx:
        # 之前MLX的推理不支持高级的采样策略,比如Top-P和Top-K,因此做了一些微小的工作
        # 见 https://github.com/ml-explore/mlx-examples/pull/486
        response = mlx_lm.generate(model, tokenizer, text, temp=0.6, top_p=0.8, max_tokens=512, repetition_penalty=1.1)
    else:
        model_inputs = tokenizer([text], return_tensors="pt").to(device)

        generated_ids = model.generate(
            model_inputs.input_ids,
            max_new_tokens=512,
            temperature=0.6,
            top_p=0.8,
            repetition_penalty=1.1,
        )
        generated_ids = [
            output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
        ]

        response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
    print(f"Assistant:{response}")
    messages.append({"role": "assistant", "content": response})

部分参数解释

下面为Huggingface的model.generate方法的参数介绍。

  • temperature: 温度,这个一般是用来指对于概率的一个线性缩放系数。这个值越大,生成的随机性越强,越小则越具有确定性。对于较小的模型,我们一般设置小于1的temperature,可以优先试下0.6-0.8。
  • do_sample: 是否开启采样。不开启采样,则采用贪婪解码(Greedy Decoding),即每次都选择概率最大的Token。默认开启。
  • max_new_tokens: 最长生成的token数。这个和max_length差不多,只不过扣除了prompt部分的token计数。max_length的值则受限于模型本身,之前一般都是2048,当然现在也有了32000或者更长的模型。
  • top_p: Top-P采样的概率值。一种去除小概率输出的采样策略,即对于每个token去按照概率从大到小排序,计算累计概率,将累积概率大于指定值的token的概率排除在选择之外。一般设置为0.8-1。
  • top_k: Top-K采样的个数。一种去除小概率输出的采样策略,即对于每个token去按照概率从大到小排序,选取前K个结果,其他则被排除在选择之外。现在一般用了top p就不使用top k了。
  • repetition_penalty: 重复惩罚因子。目前的LLM模型有一个特点,就是他有时会比较容易陷入解码循环,例如ABABAB或者ABCABC,这个一般我们也叫“复读”。为了避免这个问题,我们在采样时可以把已经出现的token的概率进行调整。这个参数可以看情况进行调整,一般设置为1.0-1.2。1.0表示不开启重复惩罚。

LLM 训练实例

那么,下面我们终于要开始训自己的模型了。这一章节我们来训一个自己的角色扮演模型。

环境配置

Windows

与上一章节的样例一样,需要WSL。

MacOS

特别注意:貌似只有M2芯片的Mac + MacOS Sonoma (14+)才支持Bfloat16,而即使是最新的PyTorch也不支持float16/bfloat16的AMP Autocast(见https://github.com/pytorch/pytorch/issues/104191),所以下面的流程对于Mac很可能无法使用。还好我们目前已经支持了MLX的训练。对了,如果你是直接阅读的这一章节,记得参照上一章的"安装其他依赖/MacOS"中的内容来升级mlx-lm和mlx。

目前训练框架对MLX功能支持如下:

  • 梯度累积 Gradient accumulation
  • 梯度暂存 Gradient checkpointing
  • 梯度裁剪 Gradient clipping
  • LoRA权重合并
  • 保存权重
  • 加载权重 & 恢复训练
  • 数据Packing
  • 全量训练
  • QLoRA

另外介绍一个工具asitop可以用来监测M芯片的GPU占用,可以用pip install asitop -i https://pypi.tuna.tsinghua.edu.cn/simple.

通用

到这里,不管你是WSL还是Mac,假设我们都是POSIX的环境。基本的步骤都是通用的了。

  1. 安装训练依赖

    pip install sentencepiece accelerate tokenizers typer jsonref pydantic -i https://pypi.tuna.tsinghua.edu.cn/simple
    

    [Linux/WSL 必选] 安装训练时所使用的依赖

    pip install peft bitsandbytes deepspeed -i https://pypi.tuna.tsinghua.edu.cn/simple
    

    [可选,仅Linux/WSL]Flash Attention是一个加速框架,用来加速Attention部分的计算。

    pip install https://github.com/Dao-AILab/flash-attention/releases/download/v2.5.5/flash_attn-2.5.5+cu118torch2.3cxx11abiFALSE-cp311-cp311-linux_x86_64.whl
    
  2. 下载训练框架

    git clone https://github.com/peterjc123/functionary
    

    我们把functionary目录的绝对路径记为FUNCTIONARY_HOME

    P.S. 如后续需要更新,只需进入functionary目录,然后git pull即可

下载数据

一般基于Huggingface或者Huggingface镜像(国内)来下载数据。

下载数据实例

对于角色扮演任务,我们使用RoleLLM所使用的Rolebench数据。

下面是下载的命令

# 选择一条执行即可
## 国外 Huggingface
git lfs clone https://huggingface.co/datasets/ZenMoore/RoleBench
## 国内 Huggingface镜像
GIT_LFS_SKIP_SMUDGE=1 git lfs clone https://hf-mirror.com/datasets/ZenMoore/RoleBench

数据预处理

简单的检视一下数据,可以看到数据在rolebench-zh下面,其中rolebench-zh/general/train.jsonl是角色对于通用问题的回答。而为了能扮演任意角色,我们也需要一个简单的人物介绍,这个可以在profiles-zh/desc.json里面找到。所以我们只需要把这些数据给拼起来就好了。

下面是处理数据的Python代码:

import os
import json

from transformers import AutoTokenizer

# RoleBench数据集的路径
home_dir = '<PATH_TO_ROLEBENCH>'
profile_path = os.path.join(home_dir, 'profiles-zh', 'desc.json')
general_data_path = os.path.join(home_dir, 'rolebench-zh', 'general', 'train.jsonl')
role_specific_data_path = os.path.join(home_dir, 'rolebench-zh', 'role_specific', 'train.jsonl')

# 模型的路径
model_dir = '<PATH_TO_QWEN_MODEL>'
tokenizer = AutoTokenizer.from_pretrained(model_dir)

# 输出路径
output_dir = '<PATH_TO_OUTPUT>'
output_path = os.path.join(output_dir, 'rolebench-train.jsonl')

with open(profile_path, 'r') as f:
    profile_dict = json.load(f)

def transform_message(role, profile, question, answer):
    messages = [
        {"role": "system", "content": f"你是{role},{profile}。请扮演{role}与我对话。"},
        {"role": "human", "content": question},
        {"role": "assistant", "content": answer},
    ]
    text = tokenizer.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=False
    ) + "<|im_end|>"
    return text

prompts = []
with open(general_data_path, 'r') as f:
    for l in f:
        obj = json.loads(l)
        role = obj['role']
        question = obj['question']
        answer = obj['generated'][0]
        profile = profile_dict[role].strip('。')
        prompt = transform_message(role, profile, question, answer)
        prompts.append(prompt)

os.makedirs(output_dir, exist_ok=True)
with open(output_path, 'w') as f:
    for prompt in prompts:
        f.write(json.dumps({"text": prompt}))
        f.write("\n")

P.S. 这里为了快速演示,只整理了中文的数据。为了更好的效果,可以把英语的数据也一起放进去。有时间可以自己研究一下。

开始训练

使用下面的命令即可开启LoRA模型训练。

export PYTHONPATH=${FUNCTIONARY_HOME}

if [[ "$OSTYPE" == "darwin"* ]]; then
    # 对比下面的命令,目前还不支持gradient_checkpointing,还去掉了几个不支持的flag比如tf32,bf16等等
    python3 ${FUNCTIONARY_HOME}/functionary/train/train_lora_mlx.py \
        --model_name_or_path ${MODEL_PATH}/Qwen1.5-1.8B-Chat \
        --train_data_path ${DATA_PATH}/rolebench-train.jsonl \
        --output_dir ${OUTPUT_PATH}/roleplay_model_lora \
        --num_train_epochs 1 \
        --per_device_train_batch_size 2 \
        --gradient_accumulation_steps 6 \
        --save_strategy "steps" \
        --save_steps 100 \
        --save_total_limit 3 \
        --logging_steps 10 \
        --learning_rate 2e-4 \
        --weight_decay 0. \
        --warmup_ratio 0.03 \
        --lr_scheduler_type "cosine" \
        --model_max_length 512 \
        --report_to none \
        --optim adamw_bnb_8bit \
        --do_train \
        --max_grad_norm 1.0 \
        --seed 42 \
        --lora_dropout 0.05 \
        --lora_r 64 \
        --lora_alpha 16 \
        --use_cpu True
else
    python3 ${FUNCTIONARY_HOME}/functionary/train/train_lora.py \
        --model_name_or_path ${MODEL_PATH}/Qwen1.5-1.8B-Chat \
        --train_data_path ${DATA_PATH}/rolebench-train.jsonl \
        --bf16 True \
        --output_dir ${OUTPUT_PATH}/roleplay_model_lora \
        --num_train_epochs 1 \
        --per_device_train_batch_size 2 \
        --gradient_accumulation_steps 6 \
        --save_strategy "steps" \
        --save_steps 100 \
        --save_total_limit 3 \
        --logging_steps 10 \
        --learning_rate 2e-4 \
        --weight_decay 0. \
        --warmup_ratio 0.03 \
        --lr_scheduler_type "cosine" \
        --tf32 True \
        --model_max_length 512 \
        --report_to none \
        --gradient_checkpointing True \
        --optim adamw_bnb_8bit \
        --do_train \
        --max_grad_norm 1.0 \
        --seed 42 \
        --lora_dropout 0.05 \
        --lora_r 64 \
        --lora_alpha 16
fi

训练的时候中我们正好来了解一些概念

  1. 什么是LoRA?

    虽然这些模型已经很小了,但是我们手头的显卡显存还是太少了,因此没法办法正常训练这些模型。好在有LoRA技术来帮我们,什么是LoRA呢?就是对于一个矩阵W,在做矩阵乘法的时候,我们加入两个小的矩阵AB,大小分别为[input_dim, lora_r][lora_r, output_dim],其中lora_r << dim,这样他们矩阵乘的结果是大小[input_dim, output_dim]的矩阵W',我们通过学习这两个小矩阵,而把原来的大矩阵给冻结住,矩阵乘法变为(W+W')(x)=Wx + lora_alpha * ABx,从而达到高效学习的效果。

  2. 这些参数是什么意思?

    通用参数

    • model_name_or_path 模型路径
    • train_data_path 数据路径
    • output_dir 输出模型路径

    训练参数

    • num_train_epochs 训练轮次
    • learning_rate 学习率
    • optim 优化器
    • weight_decay AdamW优化器的参数
    • lr_scheduler_type 学习的调节方式
    • warmup_ratio 学习率warmup的比率
    • max_grad_norm 梯度裁剪的最大值
    • seed 随机数种子
    • logging_steps 汇报频次
    • save_steps 模型保存频次
    • save_strategy 模型保存策略
    • save_total_limit 最大保存的模型数

    模型参数

    • model_max_length 模型最大输入长度,越长约占显存
    • bf16 是否开启Bfloat16支持,需要30x0及以上的显卡,可以加速训练
    • tf32 是否开启Tensor-float32支持,需要30x0及以上的显卡,可以加速训练
    • fp16 如果上面两个不支持,可以使用Float16来加速训练

    LoRA参数

    • lora_r LoRA矩阵的大小
    • lora_alpha 平衡LoRA和原模型权重的参数
    • lora_dropout LoRA层的dropout比例

合并模型

前面我们也知道了LoRA会训练两个矩阵AB,那么在推理时我们可以将他们和原矩阵W融合起来,下面的脚本可以完成这个工作。

export PYTHONPATH=${FUNCTIONARY_HOME}

if [[ "$OSTYPE" == "darwin"* ]]; then
    # 对比下面的命令,需要我们手工输入lora的参数
    lora_rank=64
    lora_alpha=16
    lora_dropout=0.05
    python3 ${FUNCTIONARY_HOME}/functionary/train/merge_lora_weight_mlx.py \
        ${OUTPUT_PATH}/roleplay_model \
        ${MODEL_PATH}/Qwen1.5-1.8B-Chat \
        ${OUTPUT_PATH}/roleplay_model_lora \
        ${lora_rank} \
        ${lora_alpha} \
        ${lora_dropout}
else
    python3 ${FUNCTIONARY_HOME}/functionary/train/merge_lora_weight.py \
        ${OUTPUT_PATH}/roleplay_model \
        ${MODEL_PATH}/Qwen1.5-1.8B-Chat \
        ${OUTPUT_PATH}/roleplay_model_lora
fi

使用训练好的模型

请参考上一节的使用方式,这里直接做了一些小修改。

import readline
import os
import platform

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
try:
    import mlx_lm
    is_mlx_available = True
except ImportError:
    is_mlx_available = False

use_mlx = True

if platform.system() == "Darwin":
    device = "mps"
    dtype = torch.float16
else:
    device = "cuda"
    dtype = "auto"

path = "<SAVE_PATH>/roleplay_model"

if is_mlx_available and use_mlx:
    model, tokenizer = mlx_lm.load(path)
else:
    model = AutoModelForCausalLM.from_pretrained(
        path,
        torch_dtype=dtype,
        device_map="auto"
    )
    tokenizer = AutoTokenizer.from_pretrained(path)

default_messages = [
    {"role": "system", "content": "你是李云龙,桀骜不驯,纪律性差,特立独行,脾气暴躁,是一个没文化又对政治缺乏兴趣的“大老粗”军人。请扮演李云龙和我对话。"},
]

messages = list(default_messages)
while True:
    prompt = input("Human:").strip("\n").strip()
    if prompt == "\\quit":
        break
    elif prompt == "\\new":
        os.system("clear")
        messages = list(default_messages)
    else:
        messages.append({"role": "user", "content": prompt})

    text = tokenizer.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=True
    )

    if is_mlx_available and use_mlx:
        # 之前MLX的推理不支持高级的采样策略,比如Top-P和Top-K,因此做了一些微小的工作
        # 见 https://github.com/ml-explore/mlx-examples/pull/486
        response = mlx_lm.generate(model, tokenizer, text, temp=0.6, top_p=0.8, max_tokens=512, repetition_penalty=1.1)
    else:
        model_inputs = tokenizer([text], return_tensors="pt").to(device)

        generated_ids = model.generate(
            model_inputs.input_ids,
            max_new_tokens=512,
            temperature=0.6,
            top_p=0.8,
            repetition_penalty=1.1,
        )
        generated_ids = [
            output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
        ]

        response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
    print(f"Assistant:{response}")
    messages.append({"role": "assistant", "content": response})

Training like a PRO

前面已经对简单的训练流程进行了介绍。通过前面的例子,相信大家已经对LLM的训练有了一些概念,下面会对已经入门的朋友们介绍一些高级的概念和用法,以及会遇到的一些问题。

数据篇

多轮数据

之前RoleBench中我们的组装方式都是按照单轮进行组装的(就是只有一问一答),而理想的情况下,我们希望模型可以应对用户在同一上下文中的多次问答,这就需要我们准备多轮对话数据。 (当然,因为我们之前是对Chat模型进行微调,因此他多少会有一些多轮能力,就是可能没那么好。) Anyway,还是来了解下多轮数据怎么处理。 假设有如下的一条多轮数据:

Human: 你吃了吗?
Assistant: 吃了
Human: 吃的啥
Assistant: 蛋炒饭
Human: 你猜我要说啥
Assistant: 蛋炒饭蛋炒饭,加点汤才好吃

那么最简单的方法是将最后一个回答的部分作为Response,而前面的部分作为Prompt。即给定

Human: 你吃了吗?
Assistant: 吃了
Human: 吃的啥
Assistant: 蛋炒饭
Human: 你猜我要说啥
Assistant: 

让模型学习去吐出

蛋炒饭蛋炒饭,加点汤才好吃

但是这样会产生一个弊端,即前面的几轮对话都没有参与学习(前面介绍过,Finetuning不计算Prompt部分的Loss),所以为了高效的训练,我们会变成下面这种状态。 给定

Human: 你吃了吗?
Assistant: 
Human: 吃的啥
Assistant: 
Human: 你猜我要说啥
Assistant: 

模型对每轮对话的输出同时进行学习

吃了
蛋炒饭
蛋炒饭蛋炒饭,加点汤才好吃

(从模型的角度,他并不知道这是一条多轮数据,对他来说这只是一个样本,我们事实上是通过Loss Mask来完成的上述的改动的)

从实现层面来说,由于前面我们使用的框架已经支持了多轮数据的训练,因此并不需要额外做什么操作,你只需要有这么一个多轮对话数据集就行了。有兴趣的话,可以在Minami-su/roleplay_multiturn_chat_1k_zh_v0.1这个多轮数据集上实验一下。

如何使用闭源大模型生成数据(Prompt工程)

不知道大家有没有玩过图像生成,那一段段魔咒的背后,其实就是怎么让模型生成我们想要的数据。下面给出一些Tips。

  1. 明确你的需求,并提炼出要点
  2. 使用陈述句,命令语气,不要使用谦词或者疑问句
  3. 可以给出一些例子(一般我们称为few shot)
  4. 使用代码来组织数据,比如JSON/Markdown
  5. 也可以试试使用英语Prompt,效果或许会好一些 也可以去看OpenAI自己的魔法书或者别人分享的资料。语言模型的Prompt还是相对比较简单的,不过具体任务还得自己摸索一下。

训练篇

显存不够/训练太慢怎么办?

前面一篇的训练命令中,我们一股脑的介绍了一大堆参数,这个相信大家一定是比较懵的,这些到底和显存/训练速度有什么关系。下面我们列一张表格,把那些和显存/训练速度有关的参数挑出来。

ArgumentsGPU UsageTraining SpeedDescription
gradient_checkpointing👇👇👇👇👇👇梯度保存,一种节省显存的方法
per_device_train_batch_size👆👆👆👆训练时每张显卡上的batch size,如果可以的话越大越快
model_max_length👆👇模型的最大序列长度
lora_rank👆👇LoRA矩阵的维度

有了这张表,相信大家就比较清晰了。由于下面两个参数影响不大,我们一般优先调上面两个参数。首先,我们把gradient_checkpointing关闭,per_device_train_batch_size设置为1,看是否可以正常训练。不行的话,你就只能打开gradient_checkpointing进行训练了。再尝试将per_device_train_batch_size调为更大的值。注意调节per_device_train_batch_size时,需要同步对gradient_accumulation_steps进行调整,否则实际的batch size就变了,可能会影响训练,需要保证effecitve_batch_size = gradient_accumulation_steps * per_device_train_batch_size不变。如果还是训不起来,而且你的模型输入都比较短的话,也可以尝试缩小model_max_length到256,但是这能节省的显存有限。

数据Packing

当我们数据很多,而且都比较短的时候(显著低于model_max_length时),我们可以使用数据Packing来加速训练。只需要在命令中加入--packing True --pack_length 512(这里的512需要换成你实际使用的model_max_length)。需要注意的是,这个或许导致更大的显存占用,可以参考上一小节对进行参数调节。

P.S. 在开启Flash Attention(Linux/WSL应该都开启了)的情况下,如果我们使用了数据Packing,那么更大的model_max_length比更大的per_device_train_batch_size要更节省显存一些,所以可以先放大model_max_length,再缩小per_device_train_batch_size,保持gradient_accumulation_steps * per_device_train_batch_size * model_max_length大致相同即可。

在MacOS上,由于目前MLX中的Flash Attention优化一般,因此不会有很大的速度提升,但是可以保持相对恒定的显卡使用率,因此还是能比不做data packing要快一些。

QLoRA

在训更大的模型的时候,你会发现就算调节上面的参数也没法正常完成训练,显存仍然不够。好在我们可以进行量化模型训练。什么是量化?即使用q = f / s这种方式来做线性缩放,其中常量s可以是对于f部分共享的,这样可以让一个浮点值可以用一个更小的整数值来表示。一个FP32浮点值占4个字节,一个BF16/FP16浮点值占2个字节,而量化后整数值可以只占4-8位,这样可以大幅的降低显存占用。 可以在命令中加入--q_lora True来启用QLoRA。

Loss曲线观测

作为Pro的LLM训练大师,怎么能不绘制Loss曲线呢?我们有两个后端可以对训练过程的变化进行记录

  1. Wandb (可以在外部访问)
  2. Tensorboard(局域网访问)

只需要将训练参数中的--report_to none中的none改为wandb或者tensorboard。对于wandb还需要申请账号以及在环境变量中添加Wandb的API key。具体参见https://wandb.ai/ 而tensorboard则需要先通过pip install tensorboard进行安装,然后用命令tensorboard --logdir=<output_dir>来启动网页后端。

可以发现LLM训练的Loss曲线一般在训练初期快速下降,到达一个点之后开始震荡,同时缓慢下降。

加入验证集

为了更好的对模型的训练效果进行观测,我们需要加入一个验证集,可以通过传入如下参数来开启模型验证。

    --eval_data_path <path to eval dataset> \
    --do_eval \
    --per_device_eval_batch_size 1

P.S. eval_data的数据格式和train_data是一样的。