背景信息
在过去的五周里,我花了很多时间参加了名为《掌握LLMs:开发人员和数据科学家大会》(Mastering LLMs: A Conference For Developers & Data Scientists)的在线课程,该课程由丹·贝克尔(丹·贝克尔)和哈梅尔·侯赛因(哈梅尔·侯赛因)出色举办。许多专家嘉宾分享了他们在实际工作中使用LLM应用程序的经验。课程特别关注了LLM的微调,它在某些情况下可以用来克服提示工程和检索增强生成(RAG)的局限性。讲师们指出,由于微调需要耗费大量的资源和标注,因此应谨慎使用,以下是一些需要进行微调的原因:
- 数据是私密的
- 问题非常局限
- 提示工程难以实现
完成课程后,我特别想尝试微调自己的大语言模型,但没有具体的应用场景。这时,我想到一个主意:“为什么不微调一个大语言模型,让它说话像我一样,然后用它来做一个聊天机器人呢?”我可以让它和我的妻子、孩子们以及朋友们聊天,这样一来,我就能空出更多时间了!
幸运的是,这个用例满足了三个条件。
- 数据是私密的 => 是的,我的个人聊天数据是私密的
- 问题很具体 => 是的,我不认为还有其他人想要微调一个大型语言模型来让模型模仿我的说话风格
- 提示工程不可行 => 是的,提示工程每次对话都需要插入关于我自己和场景的内容,这会变得很麻烦,
就这样,项目就开始了。
数据为王;可以说它就是一切(差不多)在任何数据科学或机器学习项目中,最重要的是数据。没有好的数据,就没有好的模型。我的LLM该如何学习像我一样说话呢?因为我的妻子将是这个聊天机器人最主要的使用者,所以我的数据源必须包含我和我妻子之间的大量对话,以便LLM能够更好地学习。WhatsApp是我主要的通信方式,我和我妻子的大部分对话都发生在那里。因此,我将我们WhatsApp的聊天记录导出,用作训练数据。我还导出了与另外九位朋友的对话,以便机器人不只是知道如何与我妻子对话。
预处理
从WhatsApp导出的数据不能直接使用。需要先将它们预处理成适合大型语言模型(LLM)学习的格式。整个过程可以通过三个步骤来完成:
- 将同一用户的连续消息合并成一条消息
- 将整个聊天记录按对话分组
- 拼接每个对话中的消息,并使用提示模板格式化
对于第一步,我将同一发送者五分钟内发送的连续消息合并为一条。对于第二步,我参照了Daniel Pleus的博客文章和笔记本,采用了他的方法。我将消息按照至少一小时的间隔分组成对话块。如果某个对话块超过3000个token,我会将其拆分成几个更小的块。下图展示了示例。
将WhatsApp聊天历史整理成对话片段
最后,在第3步中,我使用[Llama3的提示模板],将对话块中的所有消息合并成一行,如下所示:
<|start_header_id|>系统<|end_header_id|>
信息1<|eot_id|><|start_header_id|>用户<|end_header_id|>
她的信息1<|eot_id|><|start_header_id|>系统<|end_header_id|>
信息2<|eot_id|><|start_header_id|>用户<|end_header_id|>
她的信息2<|eot_id|><|start_header_id|>系统<|end_header_id|>
信息3<|eot_id|><|start_header_id|>用户<|end_header_id|>
她的信息3<|eot_id|>
例如,前面的图表中的对话块3将会变成。
<|start_header_id|>系统<|end_header_id|>
电影票价5.40/n成人票价每人 15.50 天哪/n星期一就11.50啦 哈哈<|eot_id|><|start_header_id|>用户<|end_header_id|>
哇噢 好吧<|eot_id|>
每一行随后被进一步格式化为包含键“input”的JSON字符串,并与其他对话块的处理结果连接起来,从而生成如下所示的JSONL文件。
{"input": <妻子对话块_1>}
{"input": <妻子对话块_2>}
{"input": <妻子对话块_3>}
...
{"input": <妻子对话块_1000>}
{"input": <朋友一号对话块一>}
{"input": <朋友一号对话块二>}
{"input": <朋友一号对话块三>}
...
{"input": <朋友一号对话块一百>}
...
{"input": <朋友二对话块一>}
{"input": <朋友二对话块二>}
{"input": <朋友二对话块三>}
...
{"input": <朋友九对话块一>}
...
{"input": <朋友九对话块五十>}
删除了少于100个标记的对话块后,我总共有4845行对话,可用于训练。具体如下:
我跟某人聊过头了
此段的代码可以在预处理文件夹里找到,位于该项目GitHub仓库中的相应文件夹里_。
更聪明地训练,而不是更卖力地训练微信对话格式整理好后,我准备开始微调了!但在那之前,我得先做两个决定:
- 要微调哪个模型呢?
- 要用哪种微调方法呢?
基本型号
我选择的基础模型是Mistral-7B-v0.2并对其进行微调。你可能在想:“等会儿,你不是选择了Llama3的提示模板吗?你为什么要对Mistral模型进行微调呢?”
这其实无关紧要,因为我是使用未经指令调优的Mistral-7B-v0.2 原始模型进行微调,所以我可以使用任何我喜欢的提示模板,只要保持一致性即可。这是根据Hamel Husain的建议,他在《精通LLMs课程》里提到,他通常会选择原始模型而非指令调优后的模型进行微调,以避免因不同的提示模板导致微调效果混乱。
微调技巧
有多种调整技术,目前最常见的三种方法包括如下:
- 全量精调
- 低秩适应(LoRa),
- 量化低秩适应技术(QLoRa)
关于这三种不同技术之间差异的详细解释,我建议你参考这篇博文博客文章,作者是Benjamin Marie(点击这里查看原文)。
插图作者:Benjamin Marie
我下面以表格的形式做了一个 TL;DR 版本,并参考了上述内容和 Google Cloud 的比较。QLoRA 大约比 LoRA 小 75%,在内存使用上比 LoRA 少 75%。LoRA 的调优速度比 QLoRA 快约 66%。尽管这两种方法都相对简单,但 LoRA 的成本比 QLoRA 便宜大约 40%。更高的最大序列长度会增加 GPU 内存的使用量。
全量微调,(低秩适应)LoRA,(全量低秩适应)QLoRA
在我的情况下,GPU 内存使用是一个关键因素,因为高内存的 GPU 实例价格昂贵。由于我预处理的对话中,上下文长度长达 3000 个 token(标记),这使得情况更加复杂。因此,我选择了使用 QLoRa 并采用了以下训练参数:
max_length=3000
lora_r=32
lora_alpha=64
lora_dropout=0.05
epochs=5
batch_size=1
gradient_accumulation_steps=8 #(以达到相当于批处理大小为8的效果)
我使用了Hugging Face Spaces的JupyterLab的Docker容器,该容器配备了一个A10G-Small GPU,具有24GB的VRAM,来进行微调。每小时成本是1.00美元。微调大约用了11GB的GPU内存,在大约60小时后,模型训练完成。这大约花费了60美元,总共。由于我报名课程时获得了Hugging Face提供的500美元免费信用额度,因此我没有自掏腰包支付任何费用。
在A10 GPU上进行了微调
本段代码改编自 Brev.dev 的 Mistral 微调笔记本文档,可以在我的 GitHub 代码库 的 finetuning 文件夹中找到。
猜猜看我们准备好测试新模型了!
我在配备了T4 GPU和16GB GPU内存的实例上部署了模型。每小时只要0.40美元,这比A10G-Small实例便宜多达60%。较低的GPU内存并未影响使用,因为微调后的模型只用了约6GB的GPU内存。我通过transfomers和bitsandbytes加载了模型和适配器,然后用它搭建了一个FastAPI服务,创建了推理端点。
运行在配备T4 GPU的实例上,用于推断
这个端点可以通过FastAPI自带的swagger页面访问:
让我们看看对于“你好吗?”这个提示,我们得到了什么样的回复。由于推断代码没有做任何格式化,我需要用与训练时相同的模板来格式化我的提示。
<|start_header_id|>用户<|end_header_id|>最近怎么样?<|start_header_id|>助手<|end_header_id|>
在最后的消息后面添加了后缀_\< |start_header_id|>system\<|end_headerid|>,告诉模型要在每次生成时都以系统身份发言(比如像我这样)。模型会最多生成100个令牌,或者直到遇到第一个结束令牌_\< |eot_id|>_为止。这些参数是通过_generate_方法指定的,具体设置如下:
{
"prompt": "<|start_header_id|>user<|end_header_id|>最近怎么样?<|eot_id|><|start_header_id|>system<|end_header_id|>",
"temperature": 0.5,
"max_new_tokens": "最大新生成的token数",
"repetition_penalty": "重复惩罚因子",
"custom_stop_tokens": "自定义停止token"
}
模型这样回复我:
{
"generated_text": "<|start_header_id|>user<|end_header_id|>你好吗?<|eot_id|><|start_header_id|>system<|end_header_id|>我挺好的,你呢?\n明天一起吃午饭怎么样?<|eot_id|><"
}
去掉格式后的提示内容是:
我挺好的,你咋样?
明天下午想一起吃个饭吗?
还可以。挺友善的,就像我。
该段代码可以在我的 GitHub仓库页面 的_ inference文件夹 中找到。_
是机器人,还是不是?最后一步是设置一个Telegram机器人与推理端点对话。该机器人是使用python-telegram-bot库构建的。首先,你需要向@BotFather获取一个Telegram Bot令牌,才能部署机器人。
尽管我可以轻松地使用该库来搭建机器人,但我仍然需要弄清楚如何让机器人接收用户消息、与大型语言模型对话并获取回复。
这些是操作步骤。
- 将用户消息格式化后添加到对话列表中
- 不断将用户与机器人之间的历史消息(从最近的消息开始)格式化并添加到列表中,直到对话历史的长度达到最大字数限制(3000)
- 用 _ <|eotid|> 作为分隔符连接列表中的消息,并在提示的末尾加上 _ <|start_header_id|>system<|end_headerid|> ,让LLM以系统身份回应。
完成之后,我通过运行python脚本部署了机器人,我的Telegram聊天机器人就启动了。
在我的GitHub仓库中的telegram_bot文件夹,查看如何构建和部署机器人的代码。
整体设置结合以上所有信息,我们得到如下信息:
用户试用当然,我首先请来测试的是我的妻子。这是我和我妻子的对话内容,附有我对机器人回答的注释。
我和老婆的聊天
看起来不错。它用我平时和妻子说话的方式和她交谈,并且也有和我一样的担忧。
然后,我让一个朋友和那个聊了几句……
和朋友的聊天
这还算过得去。机器人知道我在哪里工作以及我在工作中做什么,但提到这些董事的评论好像有点过分了。
另一个朋友一跟它说话,事情就开始乱套了。
喂,你说什么?
我确实算数更厉害。也不到处给人看我的乳头。那么LLM是从哪学的这些呢?🤔 绝对不是我教的。
改进的想法和建议和不同的人聊天
玩了一阵子后,我觉得它说话的方式就像我平时跟老婆聊天一样。这并不出乎意料,因为它训练的数据中有超过75%是我们俩的对话,因为大多数对话都是我与她的内容。要让它变得能跟任何人聊得更顺畅一些,我有两招。
- 减少我和我妻子之间的训练样本数量
- 为每个人设置单一的_用户_角色,改为创建例如妻子、_朋友_和_老板_这样的独立角色,在提示模板里采用不同的角色设定
对未知的幻觉
当遇到不熟悉的问题或情况时,机器人往往会编造信息,尤其是当这些信息没有在我的训练数据中提及的时候。我可以在进行聊天微调之前,明确添加一些关于我的背景资料,这样它就能学习到这些信息。
更好的服务体验
使用FastAPI来提供模型服务是可行的,但这不是最好的方法。将QLoRa适配器合并到基础模型里,然后使用像Text-Generation-Inference和vLLM这样的模型服务容器来提供服务,可能会有更好的吞吐量。
当个主机很烧钱
即使 QLoRa 模型可以在相对便宜的 T4 GPU 实例上运行,每小时只需 0.40 美元,但长期下来,每天会花费 9.60 美元,一个月就是 288 美元。我可不想为这么一个小项目花这么多钱。我也可以试试用 llama.cpp 进行 CPU 推理,或者只在特定时间段启动实例。
使用机器人程序遗憾的是,我不能公开分享机器人的网址,因为它是在处理私人和敏感信息的情况下训练的,可能泄露这些信息。不过,如果你认识我的话,可以给我发个消息试试,就是玩玩!
如果你想自定义调整你自己的聊天机器人,可以参考我在GitHub上的代码仓库:https://github.com/watsonchua/finetune-your-clone!
共同学习,写下你的评论
评论加载中...
作者其他优质文章