背景信息
在过去的五周里,我花了很多时间参加了名为《掌握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课程》里提到,他通常会选择原始模型而非指令调优后的模型进行微调,以避免因不同的提示模板导致微调效果混乱。
微调技巧
有多种调整技术,目前最常见的三种方法包括如下:
关于这三种不同技术之间差异的详细解释,我建议你参考这篇博文博客文章,作者是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令牌,才能部署机器人。
尽管我可以轻松地使用该库来搭建机器人,但我仍然需要弄清楚如何让机器人接收用户消息、与大型语言模型对话并获取回复。
这些是操作步骤。
完成之后,我通过运行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!