跳转至

27 模型工程(三):低成本领域模型方案,小团队怎么做大模型?

你好,我是 Tyler。

在前两节课中,我们学习了如何通过 self-instruct 的方法获取训练数据,以及如何以较低成本训练模型。你对这两个内容掌握得如何?

今天这节课,我们将继续深入探讨这些算法的具体实现。我们将学习数据增强、全量训练和 LoRA(低秩适应)的低成本领域模型训练。

为什么选 Alpaca 项目?

为了帮助你快速直观地建立感性认识,我在众多的学习对象中选择了Alpaca这个开源项目。目前,许多领域专属模型的开发方法几乎都源自Alpaca,而且Alpaca的开源实现与工业界的需求紧密契合,可以说达到了工业级的入门标准。而且,Alpaca的全量参数训练和LoRA加速训练方法都得到了出色的开源项目支持。

我们可以通过研究 Alpaca 项目的原始代码来了解大语言模型的训练方法。在你掌握并灵活使用 Alpaca 之后,就可以逐渐上手工业级复杂大语言模型的开发和微调。好,现在我们正式开始 Alpaca 开源项目的学习。

图片

先来看一下数据生成算法的实现,Alpaca 模型是通过对 7B 的 LLaMA 模型使用 self-instruct 论文中的技术生成的 5.2 万条指令遵循数据。self-instruct 论文提出了一种新的生成数据增强方法,可以有效地提高大语言模型的性能。

目前 Alpaca 模型仍在发展中,存在一些局限性,例如安全性问题。不过,安全性问题是开源大语言模型的通用问题,因此在使用 Alpaca 模型时,我们需要采取相应的安全措施,例如使用安全的输入数据和训练方法,以及在部署模型时应采取相应的风控和内容安全策略。

下面我们开始 Alpaca 的具体实现学习,大致分为数据生成、模型训练和训练提效这几个环节。

数据生成过程

第25节课我们学习了 Alpaca 的数据生成方法,Alpaca对self-instruct(自我教导)数据生成的方式进行了升级,提高了效率,降低了花费。这些升级包括用text-davinci-003(代替davinci)来生成指令数据,并且创建了一个新的提示词模版prompt.txt。

此外,他还采用了更大胆的批量解码方式,每一次会生成20条指令,大幅度降低了生成数据的费用。同时,数据生成流程也变得更简单了,取消了分类指令和非分类指令之间的差异,每个指令只生成一个实例,而不再生成2到3个实例。

这些优化成功生成了包含52000个实例的指令数据集,而成本只有不到500美元。初步研究还显示,与之前的self-instruct发布的数据相比,这样生成的这52000条数据更加多样化。

下一步,我们将带你生成这些数据,体会一下这种无中生有的快乐。

要运行这些代码,你需要使用你的OpenAI API密钥,设置环境变量OPENAI_API_KEY,然后用以下命令安装所需的依赖。

$ pip install -r requirements.txt

接下来,我们运行生成数据的指令。

$ python -m generate_instruction generate_instruction_following_data

这里generate_instruction_following_data 函数,是为了生成新的指令数据,确保新生成的指令文本不要和已有的指令文本过于相似。生成的数据可以用于微调预训练大语言模型。

下面我将为你解释一下这个函数的具体实现。步骤稍微有点多,需要你耐心一点,跟住我的节奏还是很容易理解的。

首先,它加载了种子任务数据,这是人工编写的指令数据,代码中将解析这些JSON格式的数据,提取指令、输入和输出信息。

seed_tasks = [json.loads(l) for l in open(seed_tasks_path, "r")]
seed_instruction_data = [
    {"instruction": t["instruction"], "input": t["instances"][0]["input"], "output": t["instances"][0]["output"]}
    for t in seed_tasks
]

接着,它创建了一个输出目录(如果不存在的话),用于存储生成的指令数据。

os.makedirs(output_dir, exist_ok=True)

之后我们需要初始化一些变量,其中包括生成请求的索引、存储机器生成指令的数据列表以及ROUGE评估器。

request_idx = 0
machine_instruction_data = []
scorer = rouge_scorer.RougeScorer(["rougeL"], use_stemmer=False)

这里还有一个进度条,用于可视化地追踪生成进度,如果已经有新的指令数据生成出来,就会更新进度条。

progress_bar = tqdm.tqdm(total=num_instructions_to_generate)
if machine_instruction_data:
    progress_bar.update(len(machine_instruction_data))

接下来,将所有种子任务指令和已生成的机器指令数据变成 token,以便后续做相似性比较。

all_instructions = [d["instruction"] for d in seed_instruction_data] + [
    d["instruction"] for d in machine_instruction_data
]
all_instruction_tokens = [scorer._tokenizer.tokenize(inst) for inst in all_instructions]

之后,算法通过循环生成新的指令数据,直到达到指定的指令数据生成数量。

while len(machine_instruction_data) < num_instructions_to_generate:
    request_idx += 1
    # ... (以下是生成新指令的主要部分)

接下来为每个生成请求创建一个输入文本批次,每个批次包含多个提示指令。

batch_inputs = []
for _ in range(request_batch_size):
    # only sampling from the seed tasks
    prompt_instructions = random.sample(seed_instruction_data, num_prompt_instructions)
    prompt = encode_prompt(prompt_instructions)
    batch_inputs.append(prompt)

再然后我们就可以使用OpenAI的API向模型提出请求,生成新的指令数据。

decoding_args = utils.OpenAIDecodingArguments(
    temperature=temperature,
    n=1,
    max_tokens=3072,
    top_p=top_p,
    stop=["\n20", "20.", "20."],
)
request_start = time.time()
results = utils.openai_completion(
    prompts=batch_inputs,
    model_name=model_name,
    batch_size=request_batch_size,
    decoding_args=decoding_args,
    logit_bias={"50256": -100},
)
request_duration = time.time() - request_start

再然后,处理生成的结果,提取新生成的指令数据。

process_start = time.time()
instruction_data = []
for result in results:
    new_instructions = post_process_gpt3_response(num_prompt_instructions, result)
    instruction_data += new_instructions

之后是比较关键的计算相似度环节。对每个新生成的指令数据,需要使用ROUGE评估方法,计算它与已有指令数据的相似性分数。如果相似度高于0.7,这个指令将不予保留。

total = len(instruction_data)
keep = 0
for instruction_data_entry in instruction_data:
    # ... (以下是计算相似性分数的主要部分)

最后,保存新生成的指令数据到文件,打印每个请求的持续时间、生成的指令数量和保留的指令数量。

process_duration = time.time() - process_start
print(f"Request {request_idx} took {request_duration:.2f}s, processing took {process_duration:.2f}s")
print(f"Generated {total} instructions, kept {keep} instructions")
utils.jdump(machine_instruction_data, os.path.join(output_dir, "regen.json"))

到这里,generate_instruction_following_data 函数就完成了它的使命,我们也拿到了生成的指令微调数据。

全量参数训练

接下来,我们用上一步中生成的数据,来训练 Llama 模型。最终生成的数据存储在 alpaca_data.json 文件中,其中的数据集包含了 52000 条指令,每个指令由以下字段组成。

  • instruction:一个字符串,描述模型应该执行的任务,这52000条指令都是独一无二的。
  • input:是一个可选的字段,用来表示指令的可选上下文或输入。大约40%的示例包含这个字段,例如,当指令是 “总结以下文章” 时,该字段的输入则是文章原文内容。
  • output:一个字符串,由 text-davinci-003 生成的、作为指令数据的输出。

你可以看一下文稿后面的例子,这样更好理解。

[
    {
        "instruction": "Give three tips for staying healthy.",
        "input": "",
        "output": "1.Eat a balanced diet and make sure to include plenty of fruits and vegetables. \n2. Exercise regularly to keep your body active and strong. \n3. Get enough sleep and maintain a consistent sleep schedule."
    },
    ...
]

如果你想要进行微调,首先需要使用以下指令安装必要的依赖。

$ pip install -r requirements.txt 

接下来,需要在拥有4个A100 80G GPU(FSDP full_shard 模式)的机器上,使用我们的数据集对LLaMA-7B进行微调。

我们使用后面的命令,就可以在 Python 3.10 上复现与论文中模型质量相似的结果。

首先修改以下配置内容。

  1. 第一要设置端口 <your_random_port>,这个端口用于多机多卡训练。
  2. 第二个参数是原始预训练 llama 模型所在的路径 <your_path_to_hf_converted_llama_ckpt_and_tokenizer>。
  3. 第三个参数是训练完成后模型保存的路径 <your_output_dir>。
torchrun --nproc_per_node=4 --master_port=<your_random_port> train.py \
    --model_name_or_path <your_path_to_hf_converted_llama_ckpt_and_tokenizer> \
    --data_path ./alpaca_data.json \
    --bf16 True \
    --output_dir <your_output_dir> \
    --num_train_epochs 3 \
    --per_device_train_batch_size 4 \
    --per_device_eval_batch_size 4 \
    --gradient_accumulation_steps 8 \
    --evaluation_strategy "no" \
    --save_strategy "steps" \
    --save_steps 2000 \
    --save_total_limit 1 \
    --learning_rate 2e-5 \
    --weight_decay 0. \
    --warmup_ratio 0.03 \
    --lr_scheduler_type "cosine" \
    --logging_steps 1 \
    --fsdp "full_shard auto_wrap" \
    --fsdp_transformer_layer_cls_to_wrap 'LlamaDecoderLayer' \
    --tf32 True

这个脚本也同样适用于OPT(Open Pre-trained Transformer Language Models)的微调。以下是微调OPT-6.7B的示例,你可以尝试一下 Meta 的另一款模型,看看和 Llama 有什么区别。

torchrun --nproc_per_node=4 --master_port=<your_random_port> train.py \
    --model_name_or_path "facebook/opt-6.7b" \
    --data_path ./alpaca_data.json \
    --bf16 True \
    --output_dir <your_output_dir> \
    --num_train_epochs 3 \
    --per_device_train_batch_size 4 \
    --per_device_eval_batch_size 4 \
    --gradient_accumulation_steps 8 \
    --evaluation_strategy "no" \
    --save_strategy "steps" \
    --save_steps 2000 \
    --save_total_limit 1 \
    --learning_rate 2e-5 \
    --weight_decay 0. \
    --warmup_ratio 0.03 \
    --lr_scheduler_type "cosine" \
    --logging_steps 1 \
    --fsdp "full_shard auto_wrap" \
    --fsdp_transformer_layer_cls_to_wrap 'OPTDecoderLayer' \
    --tf32 True

需要注意的是,这里提供的训练脚本是为了简化使用,让你可以轻松上手,所以没有经过特别的性能优化。如果要使用更多的GPU运行,你可以试着调整 gradient_accumulation_steps 的设置。

LoRA低成本训练

当然,不是所有的同学都有条件进行全量的训练实验,毕竟它需要8张A100的显卡,这时候我们可以运用上节课学到的LoRA(低秩适应)。

我们可以使用这种方法来复现斯坦福大学 Alpaca 项目的结果。在接下来要讲的这个项目中,也提供了一个 Instruct 模型,而且训练的质量与 text-davinci-003 类似,可以在树莓派上运行的(用于研究)、而且代码也可以轻松扩展到13b、30b 和 65b的模型。

除了训练代码,该项目还发布了已微调好的模型和 LoRA 权重以及推理的脚本。为了让微调的过程节省资源并且高效,该项目使用了 HuggingFace 的 PEFT 和 Tim Dettmers 的 bitsandbytes 来优化性能。

在默认的超参数设置下,LoRA 模型的输出与斯坦福 Alpaca 模型相当。不过进一步的调整可能会取得更好的性能。如果你对此感兴趣,不妨亲自尝试一下,并把你的结果在留言区里分享出来。

本地设置

我们首先在本地设置LoRA,首先安装必要的依赖。

$ pip install -r requirements.txt

如果 bitsandbytes 无法正常工作,可以考虑从源代码进行安装。

训练(finetune.py)

finetune.py 包含了基于 PEFT 微调 LLaMA 模型的示例,具体的用法是后面这样。

python finetune.py \
    --base_model 'decapoda-research/llama-7b-hf' \
    --data_path 'yahma/alpaca-cleaned' \
    --output_dir './lora-alpaca'

下面我带你梳理一下 train 函数的内容,这是模型训练代码的核心部分,这里我们挑主要的代码逻辑讲解。我们使用了 Hugging Face Transformers 库中的 LlamaForCausalLM 类来加载基础模型,这里通过 base_model 参数来设置。

    model = LlamaForCausalLM.from_pretrained(
        base_model,
        load_in_8bit=True,
        torch_dtype=torch.float16,
        device_map=device_map,
    )

接着,我们为 LoRA 方法创建配置,然后把它传到模型的设置中。

    config = LoraConfig(
        r=lora_r,
        lora_alpha=lora_alpha,
        target_modules=lora_target_modules,
        lora_dropout=lora_dropout,
        bias="none",
        task_type="CAUSAL_LM",
    )
    model = get_peft_model(model, config)

然后,这里的代码检查了是否有恢复训练的检查点。如果有,它会加载已有的模型权重。

    if resume_from_checkpoint:
        # Check the available weights and load them
        checkpoint_name = os.path.join(
            resume_from_checkpoint, "pytorch_model.bin"
        )  # Full checkpoint
        if not os.path.exists(checkpoint_name):
            checkpoint_name = os.path.join(
                resume_from_checkpoint, "adapter_model.bin"
            )  # only LoRA model - LoRA config above has to fit
            resume_from_checkpoint = (
                False  # So the trainer won't try loading its state
            )
        # The two files above have a different name depending on how they were saved, but are actually the same.
        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")

之后的这段代码用来检查是否在单机多卡(Multi-GPU)环境中运行,如果是,将模型配置为模型并行。

    if not ddp and torch.cuda.device_count() > 1:
        # keeps Trainer from trying its own DataParallelism when more than 1 gpu is available
        model.is_parallelizable = True
        model.model_parallel = True

然后,这里创建了一个 Hugging Face Transformers 的 Trainer 对象,用于管理和执行训练过程,其中设置了训练参数、数据集和数据收集器。

    trainer = transformers.Trainer(
        model=model,
        train_dataset=train_data,
        eval_dataset=val_data,
        args=transformers.TrainingArguments(
            per_device_train_batch_size=micro_batch_size,
            gradient_accumulation_steps=gradient_accumulation_steps,
            warmup_steps=100,
            num_train_epochs=num_epochs,
            learning_rate=learning_rate,
            fp16=True,
            logging_steps=10,
            optim="adamw_torch",
            evaluation_strategy="steps" if val_set_size > 0 else "no",
            save_strategy="steps",
            eval_steps=200 if val_set_size > 0 else None,
            save_steps=200,
            output_dir=output_dir,
            save_total_limit=3,
            load_best_model_at_end=True if val_set_size > 0 else False,
            ddp_find_unused_parameters=False if ddp else None,
            group_by_length=group_by_length,
            report_to="wandb" if use_wandb else None,
            run_name=wandb_run_name if use_wandb else None,
        ),
        data_collator=transformers.DataCollatorForSeq2Seq(
            tokenizer, pad_to_multiple_of=8, return_tensors="pt", padding=True
        ),
    )

最后,这里开始模型的训练,如果有提供的 checkpoint(检查点),则从该检查点恢复训练。

    trainer.train(resume_from_checkpoint=resume_from_checkpoint)

当然,你也可以基于后面的示例,根据具体的需要调整超参数。

python finetune.py \
    --base_model 'decapoda-research/llama-7b-hf' \
    --data_path 'yahma/alpaca-cleaned' \
    --output_dir './lora-alpaca' \
    --batch_size 128 \
    --micro_batch_size 4 \
    --num_epochs 3 \
    --learning_rate 1e-4 \
    --cutoff_len 512 \
    --val_set_size 2000 \
    --lora_r 8 \
    --lora_alpha 16 \
    --lora_dropout 0.05 \
    --lora_target_modules '[q_proj,v_proj]' \
    --train_on_inputs \
    --group_by_length

至此,你已经完成基于 LoRA 的模型训练了,你可以直接将这里的模型用于在线的模型推理。

推理(generate.py)

当然,如果你没有领域定制训练的需求,只是想看看 Alpaca 默认实现的模型效果是什么样的,可以直接使用 generate.py 从 Hugging Face 模型库读取基础模型和 LoRA 权重,然后运行一个 Gradio 接口,用于对指定输入进行推理。

你可以使用后面这段示例代码使用模型,具体的权重文件版本可以使用 lora_weights 参数进行指定。

python generate.py \
    --load_8bit \
    --base_model 'decapoda-research/llama-7b-hf' \
    --lora_weights 'tloen/alpaca-lora-7b'

是不是非常方便?你可以参考这节课学习的内容,课后亲手练习一下,尝试搭建起你自己的专属领域大模型,完成你在特定领域的具体任务。

总结

学到这里,我们做个总结吧。

在这节课中,我们学到了一系列方法和工具,它们可以帮助你以更低的成本进行模型训练、生成多样化的数据,我们还学习了如何通过 LoRA 来提高性能。如果你对这些技术和流程感兴趣,鼓励你深入学习和实践,以便能够训练出属于你自己的领域专属模型。

当然,目前的self-instruct和alpaca数据生成方法也存在一些局限性,正如我们在之前的课后习题中提到的,它们的数据质量受限于 GPT 知识的范围。因此,为了生成高质量的训练数据,超越 GPT 知识范围,我们可以考虑其他数据生成方法,比如结合低质量的大规模原始语料,利用大语言模型生成相对少量的高质量语料。

模型工程的任务主要包括两个方面:对大型语言模型的微调(从1-10)和构建全新的大型语言模型(从0-1)。至此,我们已经完成了模型训练的必修部分,也就是模型微调的内容。

思考题

根据前面学习的GPT系列原理的知识,想一想构建一个规模达到100B及以上的模型需要怎么做。

恭喜完成我们第 27 次的打卡学习,期待你在留言区和我交流互动。如果你觉得有收获,也欢迎你分享给你身边的朋友,邀 TA 一起讨论。

精选留言(5)
  • 顾琪瑶 👍(5) 💬(1)

    想问下老师, 在目前的行业内, 如果是偏向基于大模型的业务应用开发的话, 微调是必备技能吗, 还是可选的?

    2023-10-20

  • 周晓英 👍(3) 💬(2)

    老师您好,如果现有的Embedding模型无法完全满足需求,想训练自己领域的Ebedding模型,可以采用您文中的方法吗?

    2023-10-29

  • 陈东 👍(1) 💬(1)

    大模型在老师实践和工作中主要的作用?主要面对什么产业?产生什么价值?

    2023-10-20

  • Geek_24ebc1 👍(1) 💬(0)

    想问下老师,领域大模型,都需要提供哪些输入信息?有没有可参考的链接或文案。例如:想做一个油气测井大模型,或者医疗大模型。

    2024-05-15

  • R_R 👍(0) 💬(0)

    好文

    2024-03-07