LLM 上手与微调

发布于 2023-08-14  446 次阅读


AI 摘要

用于微调的显存资源相对较少,因此在选择LLM模型的基座时需要考虑多个因素。首先,参数量是决定LLM模型性能上限的关键因素。一般来说,参数量越多,模型的理解和生成能力就越强。然而,参数量并不是唯一的衡量指标。有些模型在跑分能力上表现出色,但微调后在特定领域上的性能可能并不突出。 此外,基座模型还需要有下游生态的支持,即开发者和用户的广泛参与。随着新模型的不断出现,选择一个能够受到用户广泛关注和投入的模型对于长期发展至关重要。 还有一点需要考虑的是商业许可开放性。一个LLM模型是否能够得到商业化支持也会影响其持续发展的动力。 因此,在选择LLM模型基座时,可以参考性能榜单成绩,但不能只依赖于此,还需要实际动手进行实践。另外,还需要考虑计算资源和生成性能的容忍度,确保模型的大小不超过可用显存的大小。 为了减少显存的占用,可以考虑使用量化技术将参数的表示形式由16位降低为8位或4位。然而,量化可能会影响模型的性能和推理速度,因此需要在空间和性能之间找到平衡点。 最后,微调阶段需要更多的显存资源。因此,选择一个在微调阶段能够满足需求的基座模型也是很重要的。

前言

如今,Large Language Model(大语言模型,后文简称 LLM)的发展势头愈发强烈,除了财力雄厚或资源丰富的公司与研究机构专注于 LLM 的架构设计与预训练以外,越来越多的个人爱好者、小型研究团队(后文简称个人团队)也投入进 LLM 的改进之中。

相较于 LLM 模型设计与预训练对计算资源恐怖的开销,微调(Fine-Tuning)的计算开销则相对显得很少。因此,绝大部分个人团队开始研究如何在已经过预训练的 LLM 模型基座上进行微调,以达到将 LLM 迁移到某一个垂直领域应用的目的。

本文旨在分享作者在微调实践过程中的心得与踩坑经验,作者对于模型微调的经验并不算丰富,希望能够抛砖引玉,引发更多人的思考与讨论,让更多人投身进 LLM 生态的建设中。

基座选择

对 LLM 基座模型的选择是整个微调链路的第一个环节。好的 LLM 模型基座具备更强的综合能力,微调后展现出来的下游任务处理能力也会更强。

一般而言,决定 LLM 模型性能上限最关键的因素是 LLM 的参数量。参数量越多,模型推理时的理解能力和生成能力都会更强。当然,该说法并不绝对,是在训练方法相同的前提下才得以成立的。随着 LLM 架构和训练方法的发展,现在许多新模型能够在更小参数量的情况下获得比其他更大参数量模型更强的性能。下图展示了阿里近期发布的通义千问 7B 模型在 C-Eval 测试集上的跑分能力。其中,C-Eval 是一个针对中文大语言模型设计的评估基准。

图1 Qwen C-Eval 榜单

从榜单上不难看出,Qwen 虽然参数量只有 7B,但在 C-Eval 的多个领域上都取得了优秀表现,战胜了多个 13B 参数量的模型,甚至 chatGPT 都落于下风(不讲武德)。

但光看 benchmark 的数值就来选择基座模型实际上是片面的,原因有以下几点:

  1. 有的模型只是单纯跑分能力强,而微调后在垂直领域上并不能展现出同样优秀的性能表现
  2. 模型也需要有下游生态的支撑,简而言之,新模型不断涌现,又在不断刷榜,但最终这个模型的坑能够被填得多平,得看有多少用户投入该新模型的建设之中
  3. 模型涉及到的商业许可开放性不同,某一个 LLM 能否得到商业化支持也是其能否不断发展下去的动力源泉

因此,我们可以在一定程度上参考 LLM 发布的性能榜单成绩,但不能完全依赖,还需要亲自动手实践。

另一方面,我们还需要考虑手头的计算资源与生成性能容忍度。

显存资源方面,不考虑某些优化手段,一般在 LLM 的运行时需要将完整的 LLM 加载到显存之中,因此,我们至少需要让模型的大小小于显存大小。以 7B(70亿)参数量的模型为例,若每个参数以半精度浮点数(fp16)的形式进行加载,则每个参数需要消耗 2 字节的显存空间进行存储。这样算下来,7B 的模型加载到显存中至少需要消耗 14 GB 的空间。除了模型本身的占用外,还需要往显存中加载一些运行环境,这部分也需要占用掉一些显存资源。最后,在模型执行推理的过程中可能需要缓存一些中间结果,这些中间结果会使得显存占用再次加大。为了减少模型的显存占用,可以考虑使用量化(Quantization )技术来缩减每个参数的空间占用,例如,将每个需要占用 16 bit 空间的参数转换为 8bit 或者 4bit 的表示形式,以此来大幅缩减模型加载后的总体显存占用。但量化不可避免地会出现模型性能下降、推理速度变慢等问题,目前的研究主要集中在如何在减少空间与保留性能之间达成微妙的平衡。

其次,微调阶段需要占用的显存量会比推理阶段多非常多。举个例子,全量微调 ChatGLM 的 6B 模型需要占用 240GB 的显存,全量微调 LLaMA 的 65B 模型需要占用 780GB 显存。但好消息是目前的量化微调研究进行得十分顺利,在 QLoRA 的量化微调方法下,微调 ChatGLM-6B 只需要 10GB 的显存,LLaMA-65B 只需要 48GB 显存,且相比全量微调的性能保留较好。

生成性能方面,对于 LLM 而言,它在生成时的生成单位为 Token,参数量越大的模型在生成 Token 时所经过的计算步骤或计算开销会更多。一般而言,在处理同样输入长度与同样生成长度的情况下,参数量大的模型会比参数量小的模型耗时更多。这里的 Token 并不简单指一个中文字符或英文字符,在优秀的中文 embedding 算法下,一个 Token 可能最终会解码为多个中文字符,而在某些没针对中文优化的 embedding 算法下,可能需要三个 Token 才能解码出一个中文字符。且推理性能与显卡的算力有密切联系,显卡对应算力越强,推理时 Token 的生成速度也越快。因此,生成性能这方面的考量应当结合自身计算条件来选择。

最后,在微调时应当尽量选用 LLM 的基座模型。以 Qwen 为例,阿里同时开源了 Qwen-7B 与 Qwen-7B-Chat 两个模型,其中 Qwen-7B-Chat 是在 Qwen-7B 的基础上由阿里经过【对齐】后的支持多轮对话的聊天模型。在这种情况下,应当优先选择 Qwen-7B 这个模型进行微调。因为对齐的操作与微调数据集会改变模型本身对输入内容(prompt)的理解方法,在官方对齐后的模型上再次微调可能并不能取得期望的效果。

在后文的例子中,我将会选用 Baichuan-13B 这个模型系列作为具体的模型来讲述。

数据集准备

我在本章中准备只拿 SFT 来举例(Supervised fine-tuning,自监督微调)。所谓的 SFT,就是利用我们自己构造的类似 QA 问答对形式的数据集喂给模型进行微调,这其实是目前微调环节中的一部分,在 SFT 之前可能会经过二次预训练(主要目的是灌输垂直领域知识),在 SFT 之后可能还有 RW(Reward Model,奖励模型) 和 PPO(Proximal Policy Optimization,近端策略优化)微调的环节,但 SFT 在整个微调链路中扮演着最重要的角色。

这里顺带提一嘴 RHLF(Reinforcement Learning from Human Feedback,基于人类反馈的强化学习),虽然在定义里 RHLF 涵盖了微调的三阶段(SFT、RW、PPO),但实际上 RHLF 更集中在 RW 和 PPO 阶段的任务。它更倾向于改善并控制模型的输出内容,例如不能让模型输出一些不符合伦理道德的言论,不能让模型生成中伤提问者的内容等等。从这个角度看来,RHLF 其实并没有太针对于领域知识和内容,而是让模型输出变得更加“纯良”。当然,提出 RHLF 的 OpenAI 都没能保证 chatGPT 百分之一百的输出友好度,当用户输入一些 prompt 时,模型就会解除束缚,为所欲为地发言。

SFT 的过程能够教会模型需要应对怎样的输入格式,以及规范出怎样的输出风格。为了保证 SFT 后模型的效果,需要格外注意数据集的内容。例如,如果目标是把 LLM 基座模型训练为一个服装选购推荐AI,则需要构造一个高质量的专注于服装推荐场景的 QA 问答对数据集,且数据集中用户询问方式应尽量多元化,回答风格应尽量统一。对于垂直领域的微调,数据集一般都是私人收集和建立的,大模型时代里,数据集是除了算力资源以外的另一大护城河。对于某一互动场景的微调,则可以使用现有的各种开源数据集,例如,假如我们希望模型需要具有多轮对话的能力,就可以使用开源多轮对话的数据集先对 LLM 基座进行微调,使 LLM 具备多轮对话的能力后再使用领域知识进行微调;而假如我们希望未来让模型完成语义判断(例如判断输入是积极内容或是消极内容),则使用开源的语义判断数据集先对 LLM 基座进行微调,后续同理。

那假如要做多轮对话的下游应用,为什么不直接选用官方发布的对齐版本(例如 Baichuan-13B-Chat)来做呢?首先,我们需要保证微调过程与推理过程使用的 prompt 保持不变。官方的对齐版本可能微调时使用的 prompt 、语义、场景、风格等要素与我们后续使用的有所不同,因此很可能会出现性能损失。其次,官方的对齐操作可能已经涵盖了整个 RLHF 过程,当完成了 RLHF 后再次做 SFT 操作,可能会对整体性能产生影响。

说个题外话,Baichuan-13B-Chat 其实可以仅通过用户手动构造 prompt 来实现多轮对话,但官方发布的时候并未提及它的多轮对话能力,只保留了单轮对话的接口与示范。可能是官方自己觉得模型对齐后的多轮对话能力不强,所以未选择直接公布。有这种例子作说明,我想大家也不难理解为什么要自己用基座模型来开始微调过程了。

在这里我并不会推荐某个具体的数据集,但我可以推荐几个优质的中文 LLM 开源项目供大家参考:

  1. Firefly(流萤): 中文对话式大语言模型微调 (github.com)
  2. Easy-to-use fine-tuning framework (github.com)
  3. api-for-open-llm: 开源大模型的统一后端接口 (github.com)

其中,1 和 2 是开源的 LLM 项目,他们提供了训练/微调框架以及经过整理的数据集,通过这两个项目,你可以很简单地动手微调自己的模型。但有个小前提,那就是你的目标模型需要是这些训练框架已经适配过的,如果你的目标模型不在适配列表里,可能你需要再去找找其他的工具。3 则是一个可以快速部署模型并以 OpenAI 格式的 HTTP 接口向外提供服务的项目,你可以使用这个项目让自身其他依赖 OpenAI 接口的服务无缝迁移到自己的本地模型上。

其次,项目 1 和 2 都开源了众多自己微调过的模型,例如,Firefly 项目开源了 firefly-baichuan-13b ,该模型是在 Baichuan-13B 基座模型的基础上,使用 Firefly 配套的训练框架和开源数据集,经过 SFT 阶段后的多轮对话模型,项目作者也发布了相应的训练文章,见此处:微调百川Baichuan-13B保姆式教程,手把手教你训练百亿大模型 (qq.com)。该模型的多轮对话能力要比官方对齐版本有着明显进步。如果你没有过多精力或资源完成这一步初步微调,大可考虑使用这些项目开源的微调版本,每个人的时间和精力都是有限的,拥抱大佬和社区,减少单调的重复工作才是最正确的选择。

后续的微调章节会以 Firefly 项目提供的微调框架进行展开。

运行模型

在微调工作进行前,请先务必保证自己具备一个完善的环境能支撑模型的运行。

LLM 的运行环境配置相对来说是比较繁琐的,Python 各种库的依赖版本管理就是一坨答辩。由于太答辩,这里写满了恐怕都写不下“常见”的解决方法,因此我建议在环境问题上卡关的各位朋友先把机子上的环境给清理一遍再一步一步仔细检查和重试。

当你准备好了 Python、Conda、CUDA、依赖库环境后,就可以尝试下载模型并启动模型了。

以下是 Baichuan-13B-Chat 在 Github 上的运行示例代码:

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.generation.utils import GenerationConfig
tokenizer = AutoTokenizer.from_pretrained("baichuan-inc/Baichuan-13B-Chat", use_fast=False, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained("baichuan-inc/Baichuan-13B-Chat", device_map="auto", torch_dtype=torch.float16, trust_remote_code=True)
model.generation_config = GenerationConfig.from_pretrained("baichuan-inc/Baichuan-13B-Chat")
messages = []
messages.append({"role": "user", "content": "世界上第二高的山峰是哪座"})
response = model.chat(tokenizer, messages)
print(response)
#乔戈里峰。世界第二高峰———乔戈里峰西方登山者称其为k2峰,海拔高度是8611米,位于喀喇昆仑山脉的中巴边境上

其中有几个值得关注的地方。首先,代码中的 AutoTokenizer.from_pretrainedAutoModelForCausalLM.from_pretrained 函数指定的加载路径是 "baichuan-inc/Baichuan-13B-Chat",这个路径实际上指向了 hugging face 的模型仓库,如果本地的 hugging face 缓存目录中不存在对应模型的缓存,则会先在线从 hugging face 下载,然后再从本地缓存中加载。由于网络问题,可能以这种方式在线下载速度会很慢,甚至下载失败的情况,因此,我们可以选择先去 hugging face 或国内镜像站中提前把模型完整文件下载到本地,然后从本地加载模型权重文件。

但单纯把 "baichuan-inc/Baichuan-13B-Chat" 这个路径改成本地模型所在目录路径并不能完全避免从 hugging face 线上加载内容,在本地未建立相关 cache 的情况下,把 from_pretrained 函数中的 trust_remote_code=True 设置为 False,此时就会遇到模型加载报错的情况。以下给出两种可以彻底离线的解决方法。

一种解决方法是把 AutoTokenizer 和 AutoModelForCausalLM 手动更改成模型本身对应的实现类,在 hugging face 的标准中,具体实现类登记在模型根目录的 config.json 和 tokenizer.json 文件中。json 文件中的 auto_map 字段指明了模型应当选择代码中的哪个类进行加载。我们只需要将 AutoTokenizer 和 AutoModel 改成实现类名即可完成彻底的离线模型加载(有的模型可能需要手动再指明 AutoConfiguration 的类)。

上述解决方法需要更改原始代码,不太优雅。另一种方法是直接修改 config.json 与 tokenizer.json 文件的映射路径。以 baichuan-13B-chat 为例,config.json 的部分原始内容如下:

"auto_map": {
    "AutoConfig": "configuration_baichuan.BaichuanConfig",
    "AutoModelForCausalLM": "modeling_baichuan.BaichuanForCausalLM"
  },

我们只需要将其修改为本地路径的映射关系即可:

"auto_map": {
    "AutoConfig": "/root/data/baichuan--configuration_baichuan.BaichuanConfig",
    "AutoModelForCausalLM": "/root/data/baichuan--modeling_baichuan.BaichuanForCausalLM"
  },

其他需要使用 AutoMap 的地方用相同格式重写即可。

当模型成功加载并启动后,你就可以开始在命令行自由提问和探索了,不再赘述。

微调模型

大部分内容可直接通过阅读 Firefly 的文档来获取,不再重复赘述,以后发现其他问题再进行补充。

部署模型

大部分内容可直接通过阅读 api-for-open-llm 的文档来获取,不再重复赘述,以后发现其他问题再进行补充。

补充

RM 和 PPO 的微调方法在项目 LLaMA-Efficient-Tuning 中实现了,如果有需要可以参考项目说明。

除了文中列举的部分开源项目,国内外还有众多开发者在共同构建 LLM 的开源生态,大家可以自行搜索并鼓励各位进行比较,寻找最适合自己的轮子,探索最适合自己的路子,为 LLM 生态注入自己的努力。

届ける言葉を今は育ててる
最后更新于 2023-09-20