【InternLM 实战营第二期笔记+作业04】XTuner 微调 LLM:1.8B、多模态、Agent

第四次课程官方操作文档:Tutorial/xtuner at camp2 · InternLM/Tutorial · GitHub

实操教学文档:https://github.com/InternLM/Tutorial/blob/camp2/xtuner/personal_assistant_document.md

第四次课程录播链接:XTuner 微调 LLM:1.8B、多模态、Agent_哔哩哔哩_bilibili

一、Finetune(微调)简介

1.增量预训练和指令跟随

LLM的下游应用中,增量预训练和指令跟随主要的两种微调模式。

  • 增量预训练:再输入书籍、文字补充数据库
  • 指令跟随:让模型学会对话模版,学习数据是高质量的对话数据,需要高质量的标注

举例:指令微调让模型不再简单地拟合问题,可以根据范式输出回答

2.一条数据的一生

system:前置条件、user、assistant

xtuner里以json格式构建

对话模版 不同厂家有不同的对话模版

LoRA在原本的Linear旁,新增一个支路,包含两个连续的小linear,新增的支路通常叫Adapter,adapter层参数量远小于原本的linear,减小显存开销。lora相当于对模型套一个壳,对部分重要的参数做封装,节省资源。

LoRA 的常见场景是 Stable Diffusion , 在 Stable Diffusion 中为了画出心仪的角色或风格,往往会在一个底模上面套一个 LoRA 模型。换一个 LoRA 模型就意味着换一个角色或者换一个出图风格,底模保持不变。

full finetuning:

  • Base Model 参与训练并更新参数
  • 需要保存 Base Model 中参数的优化器状态

lora:

  • Base Model 只参与 Forward
  • 只有 Adapter 部分 Backward 更新参数
  • 只需保存 Adapter 中参数的优化器状态

QLoRA:

  • Base Model 量化为 4-bit(4bit加载是一种不精确的加载,belike四舍五入,节省显存开销)
  • 优化器状态在 CPU 与 GPU 间 Offload

Offload 指的是:优化器(如Adam、SGD等)的状态(如梯度、动量等)在CPU和GPU之间进行了转移。

  • Base Model 只参与 Forward,只有 Adapter 部分 Backward更新参数
  • 只需保存 Adapter 中参数的优化器状态

二、XTuner(书生浦语开发的微调框架)

  • 傻瓜化:以配置文件的形式封装了大部分微调场景,0基础的非专业人员也能一键开始微调。
  • 轻量级:对于 7B参数量的LLM,微调所需的最小显存仅为8GB:消费级显卡,colab

1.xtuner简介

与llama-factory微调框架作对比

2.xtuner快速上手

 安装

pip install xtuner

挑选配置模版

xtuner list-cfg -p internlm_20b

一键训练

xtuner train internlm_20b_qlora_oasst1_512_e3

命名规则

  • 模型名:internlm_20b(无chat是基座模型,有chat是指令微调后的模型)
  • 使用算法:qlora
  • 数据集:oasst1
  • 数据长度:512
  • epoch(训练周期):e3(整个数据集跑3次)

拷贝配置模版

xtuner copy-cfg internlm_20b_qlora_oasst1_512_e3 ./

修改配置模版 (vi是Linux的文本编辑器,命令是启动文本编辑器)

vi internlm_20b_qlora_oasst1_512_e1_copy.py

启动训练

xtuner train internlm_20b_qlora_oasst1_512_e1_copy.py

常用超参

  • data_path:数据路径或huggingface仓库名
  • max_length:单条数据量最大token数,超过则截断
  • pack_to_max_length:是否将多条短数据拼接到max_length,提高gpu利用率
  • accumulative_counts:梯度累计,每多少次backward更新一次参数
  • evaluation_inputs:输入问题,会根据给定的问题进行推理,用于观测训练状态(有没有过拟合)
  • evaluation_freq:evaluation的评测间隔iter数

 float16模型对话

xtuner chat internlm/internlm-chat-20b

4bit模型对话

xtuner chat internlm/internlm-chat-20b --bits 4

 加载adapter模型对话(从指定的目录中加载一个适配器和环境变量)

xtuner chat internlm/internlm-chat-20b --adapter $ADAPTER_DIR

3.xtuner数据引擎

数据处理流程

原始问答对示例

###System:你是一名友好的AI助手
###User:你好!
###Assistant:您好,我是一名AI助手,请问有什么可以帮到您?
###User:世界最高峰是什么峰?
###Assistant:世界最高峰是珠穆朗玛峰

 格式化问答对示例

[
    {
        "conversation":[
            {
                "system":"你是一名友好的小助手",
                "input":"你好!",
                "output":"您好,我是一名AI助手,请问有什么可以帮助您?"
            },
            {
                "input":"世界最高峰是什么峰?",
                "output":"世界最高峰是珠穆朗玛峰"
            }
        ]
    }
]

可训练语料示例

<|System|>:你是一名友好的AI助手。

<|User|>:你好!

<|Bot|>:您好,我是一名AI助手,请问有什么可以帮助您?

<|User|>:世界最高峰是什么峰?

<|Bot|>:世界最高峰是珠穆朗玛峰。

xtuner内置了多种数据集的映射函数和多种对话模版映射函数

拷贝配置模版、修改配置模版和启动训练的命令

当我们拷贝一个配置模版后,通常需要改的参数是(如上右图)

  • data_path(数据路径)
  • dataset(数据集格式)
  • dataset_map_fn(数据集映射函数)

三、8GB显存玩转大语言模型

Flash Attention和DeepSpeed Zero是xtuner是最重要的两个优化技巧,但是配置很复杂,但是xtuner已经解决了这个难题:xtuner会自动dispatch flash attention,一键启动deepspeed zero,命令是

xtuner train internlm_20b_qlora_oasst1_512_e3 \
 --deepspeed deepspeed_zero3

InternLM2-Chat-1.8B:通过在线 RLHF 在 InternLM2-Chat-1.8B-SFT 之上进一步对齐。

InternLM2-Chat-1.8B 表现出更好的指令跟随、聊天体验和函数调用推荐下游应用程序使用。(模型大小仅为3.78GB)

在 FP16 精度模式下,InternLM2-1.88 仅需 4GB 显存的笔记本显卡即可顺畅运行。拥有 8GB 显存的消费级显卡,即可轻松进行 1.8B 模型的微调工作。

四、多模态LLM

1.给LLM装上电子眼:多模态LLM原理简介

多模态数据的处理

文本 embedding模型 转文本向量 转化为输出文本

图像 image projector 转图像向量,一并输出

2.什么型号的电子眼:LLaVA方案简介

  • Haotian Liu使用 GPT-4V 对图像数据生成描述,构建出大量<question text><image>--<answer text>的数据对。
  • 利用这些数据对,配合文本单模态 LLM,训练出一个 Image Projector。
  • 所使用的文本单模型 LLM 和训练出来的 image Projector,统称为 LLaVA 模型

Image Projector的训练和测试,有点类似之前我们讲过的LLoRA微调方案。二者都是在已有LLM的基础上,用新的数据训练一个新的小文件。

五、快速上手

实战目标:

  • 我们将构造<questiontext><image>--<answer text>数据对,基于InternLM2_Chat _1.8B这个文本单模态模型,使用LLaVA方案,训练一个给InternLM2_Chat_1.8B使用的lmage Projector文件。
  • LLaVA方案中,给LLM增加视觉能力的过程,即是训练lmage Projector文件的过程。

1.pretrain阶段和finetune阶段

实战过程分为2个阶段:Pretrain(大量的图像描述、标签训练,此阶段质量不保证)和Finetune(数据少,但是高质量高复杂度的对话数据、图像,belike百度弱智吧)

pretrain阶段:使用大量的图片+简单的文本(caption,即图片标题)数据对,使LLM理解图像中的普遍特征,即对大量的图片进行粗看,此阶段无论用户问它什么,它都只会回答输入图片的标题。

finetune阶段:使用图片+复杂文本数据对,来对pretrain得到的Image Projector进行进一步的训练。

2.动手实战

https://github.com/InternLM/xtuner

此部分视频的官方教程文档

Tutorial/xtuner/personal_assistant_document.md at camp2 · InternLM/Tutorial · GitHub

2.1xtuner安装

# 如果你是在 InternStudio 平台,则从本地 clone 一个已有 pytorch 的环境:
# pytorch    2.0.1   py3.10_cuda11.7_cudnn8.5.0_0

cd ~ && studio-conda xtuner0.1.17

(等了肥肠久)

conda env list

查看所有环境,发现xtuner已经安装好了,激活

# 激活环境
conda activate xtuner0.1.17
# 进入家目录 (~的意思是 “当前用户的home路径”)
cd ~
# 创建版本文件夹并进入,以跟随本教程
mkdir -p /root/xtuner0117 && cd /root/xtuner0117

# 拉取 0.1.17 的版本源码
git clone -b v0.1.17  https://github.com/InternLM/xtuner
# 无法访问github的用户请从 gitee 拉取:
# git clone -b v0.1.15 https://gitee.com/Internlm/xtuner

# 进入源码目录
cd /root/xtuner0117/xtuner

最后源码安装,我用了镜像源,源码太慢了(镜像源也等了很久)

pip install -e '.[all]' -i https://mirrors.aliyun.com/pypi/simple/

等successfully和warning出完环境就配好了

2.2前期准备

2.2.1数据集准备

为了让模型知道在询问自己是谁的时候回复成我们想要的样子,我们就需要通过在微调数据集中大量掺杂这部分的数据。

先创建一个文件夹来存放我们这次训练所需要的所有文件。

# 前半部分是创建一个文件夹,后半部分是进入该文件夹。
mkdir -p /root/ft && cd /root/ft

# 在ft这个文件夹里再创建一个存放数据的data文件夹
mkdir -p /root/ft/data && cd /root/ft/data

data 目录下新建一个 generate_data.py 文件

# 创建 `generate_data.py` 文件
touch /root/ft/data/generate_data.py

打开这个文件,复制下列内容,ctrls(这的对话结构是前面提到的格式化问答对示例)

import json

# 设置用户的名字
name = 'hoho'
# 设置需要重复添加的数据次数
n =  10000

# 初始化OpenAI格式的数据结构
data = [
    {
        "messages": [
            {
                "role": "user",
                "content": "请做一下自我介绍"
            },
            {
                "role": "assistant",
                "content": "我是{}的小助手,内在是上海AI实验室书生·浦语的1.8B大模型哦".format(name)
            }
        ]
    }
]

# 通过循环,将初始化的对话数据重复添加到data列表中
for i in range(n):
    data.append(data[0])

# 将data列表中的数据写入到一个名为'personal_assistant.json'的文件中
with open('personal_assistant.json', 'w', encoding='utf-8') as f:
    # 使用json.dump方法将数据以JSON格式写入文件
    # ensure_ascii=False 确保中文字符正常显示
    # indent=4 使得文件内容格式化,便于阅读
    json.dump(data, f, ensure_ascii=False, indent=4)

改自己的名

# 将对应的name进行修改(在第4行的位置)
- name = '不要姜葱蒜大佬'
+ name = "hoho"

修改完运行该文件

# 确保先进入该文件夹
cd /root/ft/data

# 运行代码
python /root/ft/data/generate_data.py

|-- data/
    |-- personal_assistant.json
    |-- generate_data.py
做完这步操作后,data路径下应该这样,如果没有json,可以关闭开发机再重启运行一次

2.2.2模型准备

在准备好了数据集后,接下来我们就需要准备好我们的要用于微调的模型。由于本次课程显存方面的限制,这里使用 InternLM 最新推出的小模型 InterLM2-Chat-1.8B 来完成此次的微调。

方式1.复制模型

在 InternStudio 上运行,可以不用通过 OpenXLab 或者 Modelscope 进行模型的下载。直接通过以下代码一键创建文件夹并将所有文件复制进去。(我第一遍是这么干的,后来文件夹爆了,还是推荐大家用第二种软连接方式节省空间)

# 创建目标文件夹,确保它存在。
# -p选项意味着如果上级目录不存在也会一并创建,且如果目标文件夹已存在则不会报错。
mkdir -p /root/ft/model

# 复制内容到目标文件夹。-r选项表示递归复制整个文件夹。
cp -r /root/share/new_models/Shanghai_AI_Laboratory/internlm2-chat-1_8b/* /root/ft/model/

方式2.软连接访问

# 删除/root/ft/model目录
rm -rf /root/ft/model

# 创建符号链接
ln -s /root/share/new_models/Shanghai_AI_Laboratory/internlm2-chat-1_8b /root/ft/model
  • ln 是 "link" 的缩写,用于创建链接。
  • -s 选项指定要创建的是一个软链接,硬链接和符号链接的区别在于,硬链接指向文件系统中的实际数据块,而符号链接只是一个指向另一个文件或目录的路径。
  • 前者是链接到的原始文件或目录,后者是新创建的软链接的路径
  • 即访问后者时,实际上是访问前者

文件结构如下:

|-- model/
    |-- tokenizer.model
    |-- config.json
    |-- tokenization_internlm2.py
    |-- model-00002-of-00002.safetensors
    |-- tokenizer_config.json
    |-- model-00001-of-00002.safetensors
    |-- model.safetensors.index.json
    |-- configuration.json
    |-- special_tokens_map.json
    |-- modeling_internlm2.py
    |-- README.md
    |-- configuration_internlm2.py
    |-- generation_config.json
    |-- tokenization_internlm2_fast.py

在该情况下的文件结构如下所示,可以看到和上面的区别在于多了一些软链接相关的文件。

|-- model/
    |-- tokenizer.model
    |-- config.json
    |-- .mdl
    |-- tokenization_internlm2.py
    |-- model-00002-of-00002.safetensors
    |-- tokenizer_config.json
    |-- model-00001-of-00002.safetensors
    |-- model.safetensors.index.json
    |-- configuration.json
    |-- .msc
    |-- special_tokens_map.json
    |-- .mv
    |-- modeling_internlm2.py
    |-- README.md
    |-- configuration_internlm2.py
    |-- generation_config.json
    |-- tokenization_internlm2_fast.py
2.2.3配置文件config选择

在准备好了模型和数据集后,我们就要根据我们选择的微调方法方法结合前面的信息来找到与我们最匹配的配置文件了,从而减少我们对配置文件的修改量。

所谓配置文件(config),是一种用于定义和控制模型训练和测试过程中各个方面的参数和设置的工具。准备好的配置文件只要运行起来就代表着模型就开始训练或者微调了。

XTuner 提供多个开箱即用的配置文件,用户可以通过下列命令查看:

开箱即用意味着假如能够连接上 Huggingface 以及有足够的显存,其实就可以直接运行这些配置文件,XTuner就能够直接下载好这些模型和数据集然后开始进行微调

# 列出所有内置配置文件
# xtuner list-cfg

# 假如我们想找到 internlm2-1.8b 模型里支持的配置文件
xtuner list-cfg -p internlm2_1_8b
  • 这里用到了第一个 XTuner 的工具 list-cfg ,对于这个工具而言,可以不添加额外的参数,就像上面的一样,这样就会将所有的配置文件都打印出来。
  • 同时也可以加上一个参数 -p--pattern ,后面输入的内容将会在所有的 config 文件里进行模糊匹配搜索,然后返回最有可能得内容。我们可以用来搜索特定模型的配置文件,如例子中的 internlm2_1_8b ,也可以用来搜索像是微调方法 qlora 。 根据上面的定向搜索指令可以看到目前只有两个支持 internlm2-1.8B 的模型配置文件。

显示有1.8b全量的alpaca数据集和1.8b微调的qlora数据集;e3意思是epoch训练周期3

配置文件名的解释

虽然我们用的数据集并不是 alpaca 而是我们自己通过脚本制作的小助手数据集 ,但是由于我们是通过 QLoRA 的方式对 internlm2-chat-1.8b 进行微调。而最相近的配置文件应该就是 internlm2_1_8b_qlora_alpaca_e3 ,因此我们可以选择拷贝这个配置文件到当前目录:

# 创建一个存放 config 文件的文件夹
mkdir -p /root/ft/config

# 使用 XTuner 中的 copy-cfg 功能将 config 文件复制到指定的位置
xtuner copy-cfg internlm2_1_8b_qlora_alpaca_e3 /root/ft/config
  • 这里我们就用到了 XTuner 工具箱中的第二个工具 copy-cfg
  • 该工具有两个必须要填写的参数 {CONFIG_NAME}{SAVE_PATH} ,在我们的输入的这个指令中,我们的 {CONFIG_NAME} 对应的是上面搜索到的 internlm2_1_8b_qlora_alpaca_e3 ,而 {SAVE_PATH} 则对应的是刚建的 /root/ft/config。假如需要复制其他的配置文件只需要修改这两个参数即可实现。 输入后我们就能够看到在我们的 /root/ft/config 文件夹下有一个名为 internlm2_1_8b_qlora_alpaca_e3_copy.py 的文件了。
2.2.4总结
  • 首先是在 GitHub 上克隆了 XTuner 的源码,并把相关的配套库也通过 pip 的方式进行了安装。
  • 利用脚本准备好了一份关于调教模型认识自己身份的数据集。
  • 再然后我们根据自己的显存及任务情况确定了使用 InternLM2-chat-1.8B 这个模型,并且将其复制到我们的文件夹里。
  • 最后我们在 XTuner 已有的配置文件中,根据微调方法、数据集和模型挑选出最合适的配置文件并复制到我们新建的文件夹中

文档里这段很有意思:

是不是感觉其实微调也不过如此!事实上确实是这样的!其实在微调的时候最重要的还是要自己准备一份高质量的数据集,这个才是你能否真微调出效果最核心的利器

微调也经常被戏称为是炼丹,就是说你炼丹的时候你得思考好用什么样的材料、用多大的火候、烤多久的时间以及用什么丹炉去烧。这里的丹炉其实我们可以想象为 XTuner ,只要丹炉的质量过得去,炼丹的时候不会炸,一般都是没问题的。但是假如炼丹的材料(就是数据集)本来就是垃圾,那无论怎么炼(微调参数的调整),炼多久(训练的轮数),炼出来的东西还只能且只会是垃圾。只有说用了比较好的材料,那么我们就可以考虑说要炼多久以及用什么办法去炼的问题。

2.3配置文件修改

在选择了一个最匹配的配置文件并准备好其他内容后,下面我们要做的事情就是根据我们自己的内容对该配置文件进行调整,使其能够满足我们实际训练的要求。

将教学文档中的代码复制到 /root/ft/config/internlm2_1_8b_qlora_alpaca_e3_copy.py 文件中。

整体的配置文件分为五部分:

  1. PART 1 Settings:涵盖了模型基本设置,如预训练模型的选择、数据集信息和训练过程中的一些基本参数(如批大小、学习率等)。

  2. PART 2 Model & Tokenizer:指定了用于训练的模型和分词器的具体类型及其配置,包括预训练模型的路径和是否启用特定功能(如可变长度注意力),这是模型训练的核心组成部分。

  3. PART 3 Dataset & Dataloader:描述了数据处理的细节,包括如何加载数据集、预处理步骤、批处理大小等,确保了模型能够接收到正确格式和质量的数据。

  4. PART 4 Scheduler & Optimizer:配置了优化过程中的关键参数,如学习率调度策略和优化器的选择,这些是影响模型训练效果和速度的重要因素。

  5. PART 5 Runtime:定义了训练过程中的额外设置,如日志记录、模型保存策略和自定义钩子等,以支持训练流程的监控、调试和结果的保存。

一般来说我们需要更改的部分其实只包括前三部分,而且修改的主要原因是我们修改了配置文件中规定的模型、数据集。后两部分都是 XTuner 官方帮我们优化好的东西,一般而言只有在魔改的情况下才需要进行修改。下面我们将根据项目的要求一步步的进行修改和调整吧!

2.4模型训练

2.4.1常规训练

完成配置文件后,使用xtuner train指令即可开始训练

# 指定保存路径
xtuner train /root/ft/config/internlm2_1_8b_qlora_alpaca_e3_copy.py --work-dir /root/ft/train
  • xtuner train: 调用 xtuner 工具的 train 命令来训练模型。
  • /root/ft/config/internlm2_1_8b_qlora_alpaca_e3_copy.py: 这是配置文件的路径,它包含了训练模型所需的所有设置和参数。
  • --work-dir /root/ft/train: 这是一个命令行选项,用于指定训练过程中保存输出(如模型检查点、日志等)的工作目录。
  • 如果不添加 --work-dir,文件将默认保存在 ./work_dirs/internlm2_1_8b_qlora_alpaca_e3_copy 的位置。这是基于当前的工作目录(即执行命令的目录)来推断的。

deepspeed加速方法

我们也可以结合 XTuner 内置的 deepspeed 来加速整体的训练过程,共有三种不同的 deepspeed 类型可进行选择,分别是 deepspeed_zero1, deepspeed_zero2deepspeed_zero3

选择哪种deepspeed类型主要取决于你的具体需求,包括模型的大小、可用的硬件资源(特别是GPU内存)以及训练的效率需求。一般来说:

  • 如果你的模型较小,或者内存资源充足,可能不需要使用最高级别的优化。
  • 如果你正在尝试训练非常大的模型,或者你的硬件资源有限,使用deepspeed_zero2或deepspeed_zero3可能更合适,因为它们可以显著降低内存占用,允许更大模型的训练。
  • 选择时也要考虑到实现的复杂性和运行时的开销,更高级的优化可能需要更复杂的设置,并可能增加一些计算开销。
# 使用 deepspeed 来加速训练
xtuner train /root/ft/config/internlm2_1_8b_qlora_alpaca_e3_copy.py --work-dir /root/ft/train_deepspeed --deepspeed deepspeed_zero2
2.4.2大模型幻觉问题

打开ft/train下的log,可以看到

在300轮的时候模型已经学会了在我问 “你是谁” 或者说 “请你介绍一下我自己” 的时候回答 “我是hoho的小助手,内在是上海AI实验室书生·浦语的1.8B大模型哦”。

但是在600轮的时候回答的还是 “我是hoho的小助手,内在是上海AI实验室书生·浦语的1.8B大模型哦” 这一段话。这表明模型在第一批次第600轮的时候已经出现严重的过拟合(即模型丢失了基础的能力,只会成为某一句话的复读机)现象了,到后面的话无论我们再问什么,得到的结果也就只能是回答这一句话了,模型已经不会再说别的话了。

因此假如以通用能力的角度选择最合适的权重文件的话我们可能会选择前面的权重文件进行后续的模型转化及整合工作。

假如我们想要解决这个问题,其实可以通过以下两个方式解决:

  1. 减少保存权重文件的间隔并增加权重文件保存的上限:这个方法实际上就是通过降低间隔结合评估问题的结果,从而找到最优的权重文。我们可以每隔100个批次来看什么时候模型已经学到了这部分知识但是还保留着基本的常识,什么时候已经过拟合严重只会说一句话了。但是由于再配置文件有设置权重文件保存数量的上限,因此同时将这个上限加大也是非常必要的。
  2. 增加常规的对话数据集从而稀释原本数据的占比:这个方法其实就是希望我们正常用对话数据集做指令微调的同时还加上一部分的数据集来让模型既能够学到正常对话,但是在遇到特定问题时进行特殊化处理。比如说我在一万条正常的对话数据里混入两千条和小助手相关的数据集,这样模型同样可以在不丢失对话能力的前提下学到剑锋大佬的小助手这句话。这种其实是比较常见的处理方式,大家可以自己动手尝试实践一下。
模型续训指南

假如我们的模型训练过程中突然被中断了,我们也可以通过在原有指令的基础上加上 --resume {checkpoint_path} 来实现模型的继续训练。需要注意的是,这个继续训练得到的权重文件和中断前的完全一致,并不会有任何区别。下面我将用训练了500轮的例子来进行演示。

# 模型续训
xtuner train /root/ft/config/internlm2_1_8b_qlora_alpaca_e3_copy.py --work-dir /root/ft/train --resume /root/ft/train/iter_600.pth

2.5模型转换、整合、测试及部署

2.5.1模型转换

模型转换的本质其实就是将原本使用 Pytorch 训练出来的模型权重文件转换为目前通用的 Huggingface 格式文件

# 创建一个保存转换后 Huggingface 格式的文件夹
mkdir -p /root/ft/huggingface

# 模型转换
# xtuner convert pth_to_hf ${配置文件地址} ${权重文件地址} ${转换后模型保存地址}
xtuner convert pth_to_hf /root/ft/train/internlm2_1_8b_qlora_alpaca_e3_copy.py /root/ft/train/iter_768.pth /root/ft/huggingface

转换完成后,模型被转换为hugging face的bin文件

此时,huggingface 文件夹即为我们平时所理解的所谓 “LoRA 模型文件”

可以简单理解:LoRA 模型文件 = Adapter

还可以在转换的指令中添加几个额外的参数,包括以下两个:

参数名解释
--fp32代表以fp32的精度开启,默认fp16
--max-shard-size{GB}代表每个权重文件最大的大小,默认2GB

由于本次测试的模型文件较小,并且已经验证过拟合,故没有添加。假如加上的话应该是这样的:

xtuner convert pth_to_hf /root/ft/train/internlm2_1_8b_qlora_alpaca_e3_copy.py /root/ft/train/iter_768.pth /root/ft/huggingface --fp32 --max-shard-size 2GB
2.5.2模型整合

对于 LoRA 或者 QLoRA 微调出来的模型其实并不是一个完整的模型,而是一个额外的层(adapter)。那么训练完的这个层最终还是要与原模型进行组合才能被正常的使用。

而对于全量微调的模型(full)是不需要进行整合这一步的,因为全量微调修改的是原模型的权重而非微调一个新的 adapter ,是不需要进行模型整合的。

在 XTuner 中也是提供了一键整合的指令,但是在使用前我们需要准备好三个地址,包括原模型的地址、训练好的 adapter 层的地址(转为 Huggingface 格式后保存的部分)以及最终保存的地址。

# 创建一个名为 final_model 的文件夹存储整合后的模型文件
mkdir -p /root/ft/final_model

# 解决一下线程冲突的 Bug 
export MKL_SERVICE_FORCE_INTEL=1

# 进行模型整合
# xtuner convert merge  ${NAME_OR_PATH_TO_LLM} ${NAME_OR_PATH_TO_ADAPTER} ${SAVE_PATH} 
xtuner convert merge /root/ft/model /root/ft/huggingface /root/ft/final_model

除了以上的三个基本参数以外,其实在模型整合这一步还是其他很多的可选参数,包括:

参数名解释
--max-shard-size {GB}代表每个权重文件最大的大小(默认为2GB)
--device {device_name}这里指的就是device的名称,可选择的有cuda、cpu和auto,默认为cuda即使用gpu进行运算
--is-clip这个参数主要用于确定模型是不是CLIP模型,假如是的话就要加上,不是就不需要添加

CLIP(Contrastive Language–Image Pre-training)模型是 OpenAI 开发的一种预训练模型,它能够理解图像和描述它们的文本之间的关系。CLIP 通过在大规模数据集上学习图像和对应文本之间的对应关系,从而实现了对图像内容的理解和分类,甚至能够根据文本提示生成图像。 在模型整合完成后,我们就可以看到 final_model 文件夹里生成了和原模型文件夹非常近似的内容,包括了分词器、权重文件、配置信息等等。当我们整合完成后,我们就能够正常的调用这个模型进行对话测试了。

2.5.3对话测试(基础作业)

在 XTuner 中提供了一套基于 transformers 的对话代码,让我们可以直接在终端与 Huggingface 格式的模型进行对话操作。我们只需要准备我们刚转换好的模型路径并选择对应的提示词模版(prompt-template)即可进行对话。

输入指令查看帮助

xtuner chat --help

 调用chat脚本,选用的模型是final_model,调用的模版是--prompt-template internlm2_chat

# 与模型进行对话
xtuner chat /root/ft/final_model --prompt-template internlm2_chat


出现这个就是可以了

输入问题后要敲两下回车,清除历史记录需要输入 RESET,假如我们想要退出则需要输入 EXIT(必须是大写)敲两下回车。

过拟合了,再调出原来的model模型看下

# 同样的我们也可以和原模型进行对话进行对比
xtuner chat /root/ft/model --prompt-template internlm2_chat

冷漠,但不傻

xtuner chat这个指令而言,有很多其他的参数可以设置,包括:

启动参数解释
--system指定SYSTEM文本,用于在对话中插入特定的系统级信息
--system-template指定SYSTEM模板,用于自定义系统信息的模板
--bits指定LLM运行时使用的位数,决定了处理数据时的精度
--bot-name设置bot的名称,用于在对话或其他交互中识别bot
--with-plugins指定在运行时要使用的插件列表,用于扩展或增强功能
--no-streamer关闭流式传输模式,对于需要一次性处理全部数据的场景
--lagent启用lagent,用于特定的运行时环境或优化
--command-stop-word设置命令的停止词,当遇到这些词时停止解析命令
--answer-stop-word设置回答的停止词,当生成回答时遇到这些词则停止
--offload-folder指定存放模型权重的文件夹,用于加载或卸载模型权重
--max-new-tokens设置生成文本时允许的最大token数量,控制输出长度
--temperature设置生成文本的温度值,较高的值会使生成的文本更多样,较低的值会使文本更确定
--top-k设置保留用于顶k筛选的最高概率词汇标记数,影响生成文本的多样性
--top-p设置累计概率阈值,仅保留概率累加高于top-p的最小标记集,影响生成文本的连贯性
--seed设置随机种子,用于生成可重现的文本内容

还有一个非常重要的参数 --adapter

# 使用 --adapter 参数与完整的模型进行对话
xtuner chat /root/ft/model --adapter /root/ft/huggingface --prompt-template internlm2_chat
  • xtuner chat /root/ft/model:与位于/root/ft/model路径下的模型进行对话。 
  • --adapter /root/ft/huggingface:指定要使用的适配器的位置。在这里,adapters适配器位于/root/ft/huggingface路径下。测试不同的权重文件生成的 adapter 来找到最优的 adapter 进行最终的模型整合工作。
  • --prompt-template internlm2_chat:指定对话时使用的提示模板。
2.5.4web demo部署(基础作业)

下载web demo所需依赖

pip install streamlit==1.24.0

下载InterLM项目代码

# 创建存放 InternLM 文件的代码
mkdir -p /root/ft/web_demo && cd /root/ft/web_demo

# 拉取 InternLM 源文件
git clone https://github.com/InternLM/InternLM.git

# 进入该库中
cd /root/ft/web_demo/InternLM

/root/ft/web_demo/InternLM/chat/web_demo.py 中的内容替换为以下的代码(与源代码相比,此处修改了模型路径和分词器路径,并且也删除了 avatar 及 system_prompt 部分的内容,同时与 cli 中的超参数进行了对齐)。

"""This script refers to the dialogue example of streamlit, the interactive
generation code of chatglm2 and transformers.

We mainly modified part of the code logic to adapt to the
generation of our model.
Please refer to these links below for more information:
    1. streamlit chat example:
        https://docs.streamlit.io/knowledge-base/tutorials/build-conversational-apps
    2. chatglm2:
        https://github.com/THUDM/ChatGLM2-6B
    3. transformers:
        https://github.com/huggingface/transformers
Please run with the command `streamlit run path/to/web_demo.py
    --server.address=0.0.0.0 --server.port 7860`.
Using `python path/to/web_demo.py` may cause unknown problems.
"""
# isort: skip_file
import copy
import warnings
from dataclasses import asdict, dataclass
from typing import Callable, List, Optional

import streamlit as st
import torch
from torch import nn
from transformers.generation.utils import (LogitsProcessorList,
                                           StoppingCriteriaList)
from transformers.utils import logging

from transformers import AutoTokenizer, AutoModelForCausalLM  # isort: skip

logger = logging.get_logger(__name__)


@dataclass
class GenerationConfig:
    # this config is used for chat to provide more diversity
    max_length: int = 2048
    top_p: float = 0.75
    temperature: float = 0.1
    do_sample: bool = True
    repetition_penalty: float = 1.000


@torch.inference_mode()
def generate_interactive(
    model,
    tokenizer,
    prompt,
    generation_config: Optional[GenerationConfig] = None,
    logits_processor: Optional[LogitsProcessorList] = None,
    stopping_criteria: Optional[StoppingCriteriaList] = None,
    prefix_allowed_tokens_fn: Optional[Callable[[int, torch.Tensor],
                                                List[int]]] = None,
    additional_eos_token_id: Optional[int] = None,
    **kwargs,
):
    inputs = tokenizer([prompt], padding=True, return_tensors='pt')
    input_length = len(inputs['input_ids'][0])
    for k, v in inputs.items():
        inputs[k] = v.cuda()
    input_ids = inputs['input_ids']
    _, input_ids_seq_length = input_ids.shape[0], input_ids.shape[-1]
    if generation_config is None:
        generation_config = model.generation_config
    generation_config = copy.deepcopy(generation_config)
    model_kwargs = generation_config.update(**kwargs)
    bos_token_id, eos_token_id = (  # noqa: F841  # pylint: disable=W0612
        generation_config.bos_token_id,
        generation_config.eos_token_id,
    )
    if isinstance(eos_token_id, int):
        eos_token_id = [eos_token_id]
    if additional_eos_token_id is not None:
        eos_token_id.append(additional_eos_token_id)
    has_default_max_length = kwargs.get(
        'max_length') is None and generation_config.max_length is not None
    if has_default_max_length and generation_config.max_new_tokens is None:
        warnings.warn(
            f"Using 'max_length''s default ({repr(generation_config.max_length)}) \
                to control the generation length. "
            'This behaviour is deprecated and will be removed from the \
                config in v5 of Transformers -- we'
            ' recommend using `max_new_tokens` to control the maximum \
                length of the generation.',
            UserWarning,
        )
    elif generation_config.max_new_tokens is not None:
        generation_config.max_length = generation_config.max_new_tokens + \
            input_ids_seq_length
        if not has_default_max_length:
            logger.warn(  # pylint: disable=W4902
                f"Both 'max_new_tokens' (={generation_config.max_new_tokens}) "
                f"and 'max_length'(={generation_config.max_length}) seem to "
                "have been set. 'max_new_tokens' will take precedence. "
                'Please refer to the documentation for more information. '
                '(https://huggingface.co/docs/transformers/main/'
                'en/main_classes/text_generation)',
                UserWarning,
            )

    if input_ids_seq_length >= generation_config.max_length:
        input_ids_string = 'input_ids'
        logger.warning(
            f"Input length of {input_ids_string} is {input_ids_seq_length}, "
            f"but 'max_length' is set to {generation_config.max_length}. "
            'This can lead to unexpected behavior. You should consider'
            " increasing 'max_new_tokens'.")

    # 2. Set generation parameters if not already defined
    logits_processor = logits_processor if logits_processor is not None \
        else LogitsProcessorList()
    stopping_criteria = stopping_criteria if stopping_criteria is not None \
        else StoppingCriteriaList()

    logits_processor = model._get_logits_processor(
        generation_config=generation_config,
        input_ids_seq_length=input_ids_seq_length,
        encoder_input_ids=input_ids,
        prefix_allowed_tokens_fn=prefix_allowed_tokens_fn,
        logits_processor=logits_processor,
    )

    stopping_criteria = model._get_stopping_criteria(
        generation_config=generation_config,
        stopping_criteria=stopping_criteria)
    logits_warper = model._get_logits_warper(generation_config)

    unfinished_sequences = input_ids.new(input_ids.shape[0]).fill_(1)
    scores = None
    while True:
        model_inputs = model.prepare_inputs_for_generation(
            input_ids, **model_kwargs)
        # forward pass to get next token
        outputs = model(
            **model_inputs,
            return_dict=True,
            output_attentions=False,
            output_hidden_states=False,
        )

        next_token_logits = outputs.logits[:, -1, :]

        # pre-process distribution
        next_token_scores = logits_processor(input_ids, next_token_logits)
        next_token_scores = logits_warper(input_ids, next_token_scores)

        # sample
        probs = nn.functional.softmax(next_token_scores, dim=-1)
        if generation_config.do_sample:
            next_tokens = torch.multinomial(probs, num_samples=1).squeeze(1)
        else:
            next_tokens = torch.argmax(probs, dim=-1)

        # update generated ids, model inputs, and length for next step
        input_ids = torch.cat([input_ids, next_tokens[:, None]], dim=-1)
        model_kwargs = model._update_model_kwargs_for_generation(
            outputs, model_kwargs, is_encoder_decoder=False)
        unfinished_sequences = unfinished_sequences.mul(
            (min(next_tokens != i for i in eos_token_id)).long())

        output_token_ids = input_ids[0].cpu().tolist()
        output_token_ids = output_token_ids[input_length:]
        for each_eos_token_id in eos_token_id:
            if output_token_ids[-1] == each_eos_token_id:
                output_token_ids = output_token_ids[:-1]
        response = tokenizer.decode(output_token_ids)

        yield response
        # stop when each sentence is finished
        # or if we exceed the maximum length
        if unfinished_sequences.max() == 0 or stopping_criteria(
                input_ids, scores):
            break


def on_btn_click():
    del st.session_state.messages


@st.cache_resource
def load_model():
    model = (AutoModelForCausalLM.from_pretrained('/root/ft/final_model',
                                                  trust_remote_code=True).to(
                                                      torch.bfloat16).cuda())
    tokenizer = AutoTokenizer.from_pretrained('/root/ft/final_model',
                                              trust_remote_code=True)
    return model, tokenizer


def prepare_generation_config():
    with st.sidebar:
        max_length = st.slider('Max Length',
                               min_value=8,
                               max_value=32768,
                               value=2048)
        top_p = st.slider('Top P', 0.0, 1.0, 0.75, step=0.01)
        temperature = st.slider('Temperature', 0.0, 1.0, 0.1, step=0.01)
        st.button('Clear Chat History', on_click=on_btn_click)

    generation_config = GenerationConfig(max_length=max_length,
                                         top_p=top_p,
                                         temperature=temperature)

    return generation_config


user_prompt = '<|im_start|>user\n{user}<|im_end|>\n'
robot_prompt = '<|im_start|>assistant\n{robot}<|im_end|>\n'
cur_query_prompt = '<|im_start|>user\n{user}<|im_end|>\n\
    <|im_start|>assistant\n'


def combine_history(prompt):
    messages = st.session_state.messages
    meta_instruction = ('')
    total_prompt = f"<s><|im_start|>system\n{meta_instruction}<|im_end|>\n"
    for message in messages:
        cur_content = message['content']
        if message['role'] == 'user':
            cur_prompt = user_prompt.format(user=cur_content)
        elif message['role'] == 'robot':
            cur_prompt = robot_prompt.format(robot=cur_content)
        else:
            raise RuntimeError
        total_prompt += cur_prompt
    total_prompt = total_prompt + cur_query_prompt.format(user=prompt)
    return total_prompt


def main():
    # torch.cuda.empty_cache()
    print('load model begin.')
    model, tokenizer = load_model()
    print('load model end.')


    st.title('InternLM2-Chat-1.8B')

    generation_config = prepare_generation_config()

    # Initialize chat history
    if 'messages' not in st.session_state:
        st.session_state.messages = []

    # Display chat messages from history on app rerun
    for message in st.session_state.messages:
        with st.chat_message(message['role'], avatar=message.get('avatar')):
            st.markdown(message['content'])

    # Accept user input
    if prompt := st.chat_input('What is up?'):
        # Display user message in chat message container
        with st.chat_message('user'):
            st.markdown(prompt)
        real_prompt = combine_history(prompt)
        # Add user message to chat history
        st.session_state.messages.append({
            'role': 'user',
            'content': prompt,
        })

        with st.chat_message('robot'):
            message_placeholder = st.empty()
            for cur_response in generate_interactive(
                    model=model,
                    tokenizer=tokenizer,
                    prompt=real_prompt,
                    additional_eos_token_id=92542,
                    **asdict(generation_config),
            ):
                # Display robot response in chat message container
                message_placeholder.markdown(cur_response + '▌')
            message_placeholder.markdown(cur_response)
        # Add robot response to chat history
        st.session_state.messages.append({
            'role': 'robot',
            'content': cur_response,  # pylint: disable=undefined-loop-variable
        })
        torch.cuda.empty_cache()


if __name__ == '__main__':
    main()

将端口映射到本地,Windows + R(Windows 即开始菜单键)打开powershell

# 从本地使用 ssh 连接 studio 端口
# 将下方端口号 38374 替换成自己的端口号
ssh -CNg -L 6006:127.0.0.1:6006 root@ssh.intern-ai.org.cn -p 38374

注意这粘密码 ctrlc之后 到powershell里 点一下鼠标右键再回车 不要有其他动作 别ctrlv

回到intern平台ctrlc退出对话

再调用一下原来的模型,把刚才上面一长串python文件修改183行和186行的文件地址即可(可以用本地的编辑器复制代码打开,看在第几行)

# 修改模型地址(第183行)
- model = (AutoModelForCausalLM.from_pretrained('/root/ft/final_model',
+ model = (AutoModelForCausalLM.from_pretrained('/root/ft/model',

# 修改分词器地址(第186行)
- tokenizer = AutoTokenizer.from_pretrained('/root/ft/final_model',
+ tokenizer = AutoTokenizer.from_pretrained('/root/ft/model',

基础作业结束

六、XTuner微调LLM(进阶作业)

!!!第一遍我先做的微调,后做openxlab部署,但是在openxlab部署时发现final_model都丢失了!!!非常痛苦地返回头整了很久,可能跟微调这重装xtuner有关系,我看做出来openxlab的大佬都是后做微调的,建议大家先去做openxlab部署

视频这接着开始使用XTuner微调LLM,教程又开始安装xtuner,之前已经安装过了,完全可以跳过这块的(啊我的算力)

1.概述

我们将自己构造<question text><image>--<answer text>数据对,基于InternLM2_Chat_1.8B这个文本单模态模型,使用LLaVA方案,训练一个给InternLM2_Chat_1.8B使用的Image Projector文件。

LLaVA方案中,给LLM增加视觉能力的过程,即是训练Image Projector文件的过程,该过程分为两个阶段:Pretrain和Finetune

2pretrain阶段

在Pretrain阶段,我们会使用大量的图片+简单文本(caption, 即图片标题)数据对,使LLM理解图像中的普遍特征。即,对大量的图片进行粗看

Pretrain阶段训练完成后,此时的模型已经有视觉能力了!但是由于训练数据中都是图片+图片标题,所以此时的模型虽然有视觉能力,但无论用户问它什么,它都只会回答输入图片的标题。即,此时的模型只会给输入图像“写标题”

Pretrain阶段相当于是开发LLM时预训练工作,对硬件要求非常高,有8卡的学有余力同学可以自行尝试。详见xtuner/docs/zh_cn/user_guides/dataset_prepare.md at main · InternLM/xtuner · GitHub

LLaVA

在本次实战营中,我们已经为大家提供了Pretrain阶段的产物——iter_2181.pth文件。它就是幼稚园阶段的Image Projector!大家带着iter_2181.pth文件继续进入下一阶段进行Finetune即可。

3finetune阶段

在Finetune阶段,我们会使用图片+复杂文本数据对,来对Pretrain得到的Image Projector即iter_2181.pth进行进一步的训练。

(开始finetune要等肥肠久 大概半个小时)

finetune前

finetune后

结束!

七、OpenXLab部署(进阶作业)

这部分的官方教程文档

https://github.com/InternLM/Tutorial/tree/camp2/tools/openxlab-deployhttps://github.com/InternLM/Tutorial/tree/camp2/tools/openxlab-deploy

1.OpenXlab简介

AI领域的一站式服务平台,包括数据集中心、模型中心和应用中心

2.部署demo

如果用Internstudio平台配置可以跟往期大佬的文章,非常详细,非常保姆

《书生·浦语大模型实战营》第四节课《XTuner 微调 LLM:1.8B、多模态、Agent》实践笔记-CSDN博客

!!!以下操作请先激活xtuner0.1.17环境

2.1配置git

安装git和git lfs
apt-get update
apt-get install git
apt-get install git-lfs
git lfs install


 OpenXLab-登录

设置您的 Git 用户名,OpenXLab 使用你在平台的用户名作为 Git的用户名,具体获取路径,可登录 OpenXLab 后,点击个人头像下的 【账号与安全】查看个人的用户名

配置git username和git email(换成自己的)
git config --global user.name "Username"
git config --global user.email "email@email.com"
创建空的模型仓库

openxlab平台右上角创建模型

点进去,点模型文件,找到该仓库的git地址,复制,拉取空仓库至本地

git clone https://code.openxlab.org.cn/hoo01/hoo01_robot2.git
获得git acess token

openxlab右上角点头像密钥管理,添加完令牌后,记得复制生成的 Access Token,如下图所示,在后续上传模型文件,执行git push 命令时会需要填入 Username 和 Access Token 信息

其他验证方式参考:

平台也提供SSH密钥方式身份验证,如想了解 SSH 密钥身份验证可参考:上传模型文件 | OpenXLab浦源 - 文档中心

上传模型文件

测试git访问令牌

创建readme.md文件,提交

cd hoo01_robot2/
touch README.md
git add README.md
git commit -m "README.md"
git push

移动模型文件到仓库中
ls -al /root/ft/final_model/
mv /root/ft/final_model/* .
ls -al

LFS管理大文件

使用 git lfs track 命令来标记你希望通过 Git LFS 管理的大文件。例如,您想要通过LFS管理所有的 .bin .model的模型文件,可以使用以下命令:

git lfs track "*.bin"
git lfs track "*.model"

   标记LFS管理的文件后,提交更新的信息,执行 git push 上传模型,命令如下所示:

cd hoo01_robot2
git add -A
git commit -m "upload model"
git push

命令行解释

  1. git add -A:添加所有新文件、修改过的文件和删除的文件到暂存区。
  2. git commit -m "upload model":创建一个新的提交,附带提交信息"upload model"。
  3. git push:将本地的提交推送到远程仓库。

 

建github仓库

打开主页,右上角+ new repository

create new file

建requirements.txt

gradio==4.10.0
transformers
sentencepiece
einops
accelerate
tiktoken

再建packages.txt

git
git-lfs

再创建app.py

import gradio as gr
import os
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, AutoModel
from openxlab.model import download

base_path = './hoo01_robot2/'
os.system(f'git clone https://code.openxlab.org.cn/hoo01/hoo01_robot2.git {base_path}')
os.system(f'cd {base_path} && git lfs pull')

tokenizer = AutoTokenizer.from_pretrained(base_path,trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(base_path,trust_remote_code=True,torch_dtype=torch.float16)

def chat(message,history):
    for response,history in model.stream_chat(tokenizer,message,history,max_length=2048,top_p=0.7,temperature=1):
        yield response

gr.ChatInterface(chat,
                 title="hoo01_robot2",
                description="""
hoo01_robot2 is talking to you.  
                 """,
                 ).queue(1).launch()

回到openxlab创建应用

右上角创建应用,选gradio

(最后我的应用点开是回复不了的,随机点了个别人的应用也回复不了,目前等助教解决,就先这样)

五、总结

1.本节课理论部分可搭配附加课弱智吧数据集训练课食用。

2.微调步骤:

  • 选择数据集(1.数据集格式调整看上去很简单,其实很容易出错。新手如果想照搬教程的config,数据集清洗后的格式最好和教程一模一样,不要自己创造新的字段。2.如果想训练角色,可以先准备一段prompt给大模型,使用API生成大批量的对话,再调用API生成回答(见上一期的优秀项目chat西游))
  • 划分训练集和验证集(两节课好像都没用验证集,期待浦语出验证集教学)
  • 自定义微调(修改配置文件,关键是修改模型和数据的路径,基础模型也可以选之前微调过的模型)
  • 启动训练,将得到的PTH模型转化为HF模型(大家都在用的deepseed加速方法)
  • 合并到大语言模型
  • 部署前端 open xlab方式
  • 13
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值