[InternLM训练营第二期笔记]4. XTuner 微调 LLM:1.8B、多模态、Agent

发布于:2024-04-24 ⋅ 阅读:(25) ⋅ 点赞:(0)

该系列是上海AI Lab举行的书生 浦语大模型训练营的相关笔记部分。
该笔记是第四节课,学习大语言模型微调的基本概念,以及利用XTuner工具进行微调。


0. 什么是LLM中的微调

0.1 两种Finetune范式

微调的目的是为了让foundation模型在特定的领域更好地发挥作用。目前Finetune的范式主要有两种:增量预训练指令跟随

其中,增量预训练的意思是继续训练,加入领域内的一些特定的知识。这个过程是无监督的,不需要人为制造高质量对话。

而指令跟随微调,是让模型学会对话模板,因此是有监督的,数据是高质量的问答数据。

在这里插入图片描述

0.2 一条数据的一生

那么数据从开始到结束,是怎么运作的呢?

首先,获得原始数据之后,需要将其转换成标准格式数据,这个标准格式数据是训练框架能够识别的,例如下面:

在这里插入图片描述
其中,system字段相当于一个前置条件,后面的User和Assistant也规定了问答的角色。在InternLM的用户定义中,是如下的json文件:

在这里插入图片描述

第三步,是需要添加对话模板,也就是让LLM区分出System,User和Assistant:

在这里插入图片描述
以上的代码模板是来自于用户定义的json文件的,当然不同的LLM有不同的格式。

此外,为了让LLM知道什么时候开始一段话,以及结束一段话,一般还有起始符和结束符<s>和</s>.

当然在计算loss的时候,只对模型的output计算。例如,InternLM中一条数据的input和output就是这样的:

在这里插入图片描述

0.3 微调的具体方案:LoRA与QLoRA

如果我们重新训练LLM是非常昂贵的。LoRA的核心思想就是,每一个线性层可以通过低秩分解的方式来提取最主要的信息,从而大大减小参数量,也就是 M ∈ R M × N = M 1 M 2 , M 1 = R M × k , M 2 ∈ R k × N , k < < M , N M\in \mathbb{R}^{M \times N} = M_1M_2, M_1=\mathbb{R}^{M\times k}, M_2 \in \mathbb{R}^{k \times N}, k << M, N MRM×N=M1M2,M1=RM×k,M2Rk×N,k<<M,N

在这里插入图片描述
下图是LoRA和QLoRA(量化版)的具体区别:

在这里插入图片描述

1. 实战环节:XTuner微调个人小助手

这个环节的目的是体验从用户自定义数据到微调模型的过程(微调方式是前面讲到的指令微调),然后看看模型是否能给出我们想要的答案。

1.1 准备工作

在InternLM中创建开发机,选择10% * A100即可,cuda版本选择11.7.

首先直接运行

studio-conda xtuner0.1.17
conda activate xtuner0.1.17

安装环境. 随后创建文件夹以及拉取XTuner代码:

cd ~
mkdir -p /root/xtuner0117 && cd /root/xtuner0117
git clone -b v0.1.17  https://github.com/InternLM/xtuner
cd /root/xtuner0117/xtuner
pip install -e '.[all]'

随后,我们需要构造并转换数据集格式:

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

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

然后,复制以下生成数据集的代码:

vim /root/ft/data/generate_data.py
import json

# 设置用户的名字
name = 'wjp'
# 设置需要重复添加的数据次数
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)

随后运行:

cd /root/ft/data && python /root/ft/data/generate_data.py

打开生成的json文件,可以发现成功生成了n组我们想要的对话~

在这里插入图片描述
然后,从share文件夹中搞出来1.8B的模型,在此为了省空间,就直接创建软链接:

ln -s /root/share/new_models/Shanghai_AI_Laboratory/internlm2-chat-1_8b /root/ft/model

然后,我们在所有的配置文件中,查找跟internlm2 1.8b有关的,运行以下命令,其中-p表示pattern,名称匹配模式的意思

xtuner list-cfg -p internlm2_1_8b

在这里插入图片描述

所以我们找到了两个相关的配置文件,从文件名称可以看出,第一个是全参数微调的,第二个是用QLoRA微调的,数据集名称是alpaca, epoch为3. 由于我们显存有限,当然选择QLoRA微调,接下来复制config文件:

mkdir -p /root/ft/config
xtuner copy-cfg internlm2_1_8b_qlora_alpaca_e3 /root/ft/config

1.2 配置文件修改

我们需要对权重路径、训练轮数等参数进行修改,修改后的config如下:

# Copyright (c) OpenMMLab. All rights reserved.
import torch
from datasets import load_dataset
from mmengine.dataset import DefaultSampler
from mmengine.hooks import (CheckpointHook, DistSamplerSeedHook, IterTimerHook,
                            LoggerHook, ParamSchedulerHook)
from mmengine.optim import AmpOptimWrapper, CosineAnnealingLR, LinearLR
from peft import LoraConfig
from torch.optim import AdamW
from transformers import (AutoModelForCausalLM, AutoTokenizer,
                          BitsAndBytesConfig)

from xtuner.dataset import process_hf_dataset
from xtuner.dataset.collate_fns import default_collate_fn
# 由于我们的数据集不再是原本的 aplaca 数据集,因此我们也要进入 PART 3 的部分对相关的内容进行修改。包括说我们数据集输入的不是一个文件夹而是一个单纯的 json 文件以及我们的数据集格式要求改为我们最通用的 OpenAI 数据集格式。

from xtuner.dataset.map_fns import openai_map_fn, template_map_fn_factory
# from xtuner.dataset.map_fns import alpaca_map_fn, template_map_fn_factory
from xtuner.engine.hooks import (DatasetInfoHook, EvaluateChatHook,
                                 VarlenAttnArgsToMessageHubHook)
from xtuner.engine.runner import TrainLoop
from xtuner.model import SupervisedFinetune
from xtuner.parallel.sequence import SequenceParallelSampler
from xtuner.utils import PROMPT_TEMPLATE, SYSTEM_TEMPLATE

#######################################################################
#                          PART 1  Settings                           #
#######################################################################
# Model
pretrained_model_name_or_path = '/root/ft/model'  # 模型地址修改
use_varlen_attn = False

# Data
alpaca_en_path = '/root/ft/data/personal_assistant.json'  # 数据集地址修改
prompt_template = PROMPT_TEMPLATE.default
max_length = 1024  # 单条数据的token数 其实绰绰有余
pack_to_max_length = True

# parallel
sequence_parallel_size = 1

# Scheduler & Optimizer
batch_size = 1  # per_device
accumulative_counts = 16
accumulative_counts *= sequence_parallel_size
dataloader_num_workers = 0
max_epochs = 2 # 降低一些轮数
optim_type = AdamW
lr = 2e-4
betas = (0.9, 0.999)
weight_decay = 0
max_norm = 1  # grad clip
warmup_ratio = 0.03

# Save
save_steps = 500
save_total_limit = 2  # Maximum checkpoints to keep (-1 means unlimited)

# Evaluate the generation performance during the training
evaluation_freq = 100  # 修改评估频率
SYSTEM = SYSTEM_TEMPLATE.alpaca
evaluation_inputs = ['请你介绍一下你自己', '你是谁', '你是我的小助手吗']  # 对我们关心的问题进行评估

#######################################################################
#                      PART 2  Model & Tokenizer                      #
#######################################################################
tokenizer = dict(
    type=AutoTokenizer.from_pretrained,
    pretrained_model_name_or_path=pretrained_model_name_or_path,
    trust_remote_code=True,
    padding_side='right')

model = dict(
    type=SupervisedFinetune,
    use_varlen_attn=use_varlen_attn,
    llm=dict(
        type=AutoModelForCausalLM.from_pretrained,
        pretrained_model_name_or_path=pretrained_model_name_or_path,
        trust_remote_code=True,
        torch_dtype=torch.float16,
        quantization_config=dict(
            type=BitsAndBytesConfig,
            load_in_4bit=True,
            load_in_8bit=False,
            llm_int8_threshold=6.0,
            llm_int8_has_fp16_weight=False,
            bnb_4bit_compute_dtype=torch.float16,
            bnb_4bit_use_double_quant=True,
            bnb_4bit_quant_type='nf4')),
    lora=dict(
        type=LoraConfig,
        r=64,
        lora_alpha=16,
        lora_dropout=0.1,
        bias='none',
        task_type='CAUSAL_LM'))

#######################################################################
#                      PART 3  Dataset & Dataloader                   #
#######################################################################
# 这部分也需要修改 改成openai格式
alpaca_en = dict(
    type=process_hf_dataset,
    dataset=dict(type=load_dataset, path='json', data_files=dict(train=alpaca_en_path)),
    tokenizer=tokenizer,
    max_length=max_length,
    dataset_map_fn=openai_map_fn,
    template_map_fn=dict(
        type=template_map_fn_factory, template=prompt_template),
    remove_unused_columns=True,
    shuffle_before_pack=True,
    pack_to_max_length=pack_to_max_length,
    use_varlen_attn=use_varlen_attn)

sampler = SequenceParallelSampler \
    if sequence_parallel_size > 1 else DefaultSampler
train_dataloader = dict(
    batch_size=batch_size,
    num_workers=dataloader_num_workers,
    dataset=alpaca_en,
    sampler=dict(type=sampler, shuffle=True),
    collate_fn=dict(type=default_collate_fn, use_varlen_attn=use_varlen_attn))

#######################################################################
#                    PART 4  Scheduler & Optimizer                    #
#######################################################################
# optimizer
optim_wrapper = dict(
    type=AmpOptimWrapper,
    optimizer=dict(
        type=optim_type, lr=lr, betas=betas, weight_decay=weight_decay),
    clip_grad=dict(max_norm=max_norm, error_if_nonfinite=False),
    accumulative_counts=accumulative_counts,
    loss_scale='dynamic',
    dtype='float16')

# learning policy
# More information: https://github.com/open-mmlab/mmengine/blob/main/docs/en/tutorials/param_scheduler.md  # noqa: E501
param_scheduler = [
    dict(
        type=LinearLR,
        start_factor=1e-5,
        by_epoch=True,
        begin=0,
        end=warmup_ratio * max_epochs,
        convert_to_iter_based=True),
    dict(
        type=CosineAnnealingLR,
        eta_min=0.0,
        by_epoch=True,
        begin=warmup_ratio * max_epochs,
        end=max_epochs,
        convert_to_iter_based=True)
]

# train, val, test setting
train_cfg = dict(type=TrainLoop, max_epochs=max_epochs)

#######################################################################
#                           PART 5  Runtime                           #
#######################################################################
# Log the dialogue periodically during the training process, optional
custom_hooks = [
    dict(type=DatasetInfoHook, tokenizer=tokenizer),
    dict(
        type=EvaluateChatHook,
        tokenizer=tokenizer,
        every_n_iters=evaluation_freq,
        evaluation_inputs=evaluation_inputs,
        system=SYSTEM,
        prompt_template=prompt_template)
]

if use_varlen_attn:
    custom_hooks += [dict(type=VarlenAttnArgsToMessageHubHook)]

# configure default hooks
default_hooks = dict(
    # record the time of every iteration.
    timer=dict(type=IterTimerHook),
    # print log every 10 iterations.
    logger=dict(type=LoggerHook, log_metric_by_epoch=False, interval=10),
    # enable the parameter scheduler.
    param_scheduler=dict(type=ParamSchedulerHook),
    # save checkpoint per `save_steps`.
    checkpoint=dict(
        type=CheckpointHook,
        by_epoch=False,
        interval=save_steps,
        max_keep_ckpts=save_total_limit),
    # set sampler seed in distributed evrionment.
    sampler_seed=dict(type=DistSamplerSeedHook),
)

# configure environment
env_cfg = dict(
    # whether to enable cudnn benchmark
    cudnn_benchmark=False,
    # set multi process parameters
    mp_cfg=dict(mp_start_method='fork', opencv_num_threads=0),
    # set distributed parameters
    dist_cfg=dict(backend='nccl'),
)

# set visualizer
visualizer = None

# set log level
log_level = 'INFO'

# load from which checkpoint
load_from = None

# whether to resume training from the loaded checkpoint
resume = False

# Defaults to use random seed and disable `deterministic`
randomness = dict(seed=None, deterministic=False)

# set log processor
log_processor = dict(by_epoch=False)

1.3 训练

直接运行

# 使用 deepspeed 来加速训练
xtuner train /root/ft/config/internlm2_1_8b_qlora_alpaca_e3_copy.py --work-dir /root/ft/train_deepspeed --deepspeed deepspeed_zero2

命令中的deepspeed_zero2是加速训练的工具。

训练过程中,刚开始效果一般

在这里插入图片描述

在第300次迭代以后,效果就不错了
在这里插入图片描述
第600次以后,就有点过拟合了
在这里插入图片描述

1.4 模型转换与模型整合

模型转换的目的就是将pth文件转换成通用的huggingface文件:

mkdir -p /root/ft/huggingface

xtuner convert pth_to_hf /root/ft/config/internlm2_1_8b_qlora_alpaca_e3_copy.py /root/ft/train_deepspeed/iter_600.pth /root/ft/huggingface

这就是转换后的huggingface格式的模型:

在这里插入图片描述

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

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

直接运行以下命令:

mkdir -p /root/ft/final_model

export MKL_SERVICE_FORCE_INTEL=1
xtuner convert merge /root/ft/model /root/ft/huggingface /root/ft/final_model

最后,我们进行合并后模型的对话测试:

在这里插入图片描述

2. 进阶作业1——将模型上传到OpenXLab

首先在OpenXLab网站中更改自己的用户名,比如我改成jackwoo

然后运行

apt-get install git-lfs
git lfs install

git config --global user.name "jackwoo"
git config --global user.email "XXXX@163.com"

在OpenXLav中点击创建——创建模型

然后将仓库拉取到本地:

git clone https://code.openxlab.org.cn/jackwoo/XTunerTest.git

然后将前面训练出来的模型复制到仓库里:

cp -r ~/ft/final_model ~/XTunerTest/

因为这里面有大文件,因此我们要用lfs标记他们:

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

这里科普一下git lfs的作用

在这里插入图片描述

在上传更改之前,与GitHub一样,需要获取令牌:

点击用户头像——密钥管理——令牌 选择可写权限

在这里插入图片描述

然后上传更改

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

然而事实证明,我在开发机中push之后提示输入密码而不是token,密码输入为token后也失败。因此直接

git push https://$TOKEN的值@code.openxlab.org.cn/jackwoo/XTunerTest.git

这次输入密码,直接输入token就对了。

在这里插入图片描述

3. 进阶作业2——微调多模态大模型LLaVA

多模态大模型微调的原理基本是,在已有LLM的基础上,训练一个image encoder,每次将文本和image encoder编码出的图像特征向量一同输入大模型,就可以协同理解的。

下面直接上实战步骤。

本实验需要30% * A100. 官方文档见https://github.com/InternLM/Tutorial/blob/camp2/xtuner/llava/xtuner_llava.md.

为了方便,官方已经提供预训练好的image encoder(用的ViT),这样我们直接把官方的仓库git clone下来,就不用自己手动搞了。

cd ~

git clone https://github.com/InternLM/tutorial -b camp2

我们看到官方对图片已经做了问答的标注:

在这里插入图片描述
现在我们把这个标注重复若干次,变成多组标注:

cd tutorial/

python /root/tutorial/xtuner/llava/llava_data/repeat.py \
  -i /root/tutorial/xtuner/llava/llava_data/unique_data.json \
  -o /root/tutorial/xtuner/llava/llava_data/repeated_data.json \  # 输出文件
  -n 200  # 重复200次

我们使用xtuner命令查一下对于llava的相关config:

xtuner list-cfg -p llava_internlm2_chat_1_8b

在这里插入图片描述

哦我们选择第二个:

xtuner copy-cfg \
  llava_internlm2_chat_1_8b_qlora_clip_vit_large_p14_336_lora_e1_gpu8_finetune \
  /root/tutorial/xtuner/llava

跟上面一样,我们需要修改一些路径信息:

# Model
llm_name_or_path = '/root/share/new_models/Shanghai_AI_Laboratory/internlm2-chat-1_8b'
visual_encoder_name_or_path = '/root/share/new_models/openai/clip-vit-large-patch14-336'
# Specify the pretrained pth
pretrained_pth = '/root/tutorial/xtuner/llava/iter_2181.pth'  # noqa: E501

# Data
data_root = '/root/tutorial/xtuner/llava/llava_data/'
data_path = data_root + 'repeated_data.json'
image_folder = data_root
prompt_template = PROMPT_TEMPLATE.internlm2_chat
max_length = int(2048 - (336 / 14)**2)

# Scheduler & Optimizer
batch_size = 1  # per_device
accumulative_counts = 1
dataloader_num_workers = 0
max_epochs = 1
optim_type = AdamW
lr = 2e-4
betas = (0.9, 0.999)
weight_decay = 0
max_norm = 1  # grad clip
warmup_ratio = 0.03

# Save
save_steps = 500
save_total_limit = 2  # Maximum checkpoints to keep (-1 means unlimited)

# Evaluate the generation performance during the training
evaluation_freq = 500
SYSTEM = ''
evaluation_images = 'https://llava-vl.github.io/static/images/view.jpg'
evaluation_inputs = ['Please describe this picture','What is the equipment in the image?']

然后开始训练:

cd /root/tutorial/xtuner/llava/

xtuner train /root/tutorial/xtuner/llava/llava_internlm2_chat_1_8b_qlora_clip_vit_large_p14_336_lora_e1_gpu8_finetune_copy.py --deepspeed deepspeed_zero2

训练完毕后,进行性能对比:

训练前的:

# 解决小bug
export MKL_SERVICE_FORCE_INTEL=1
export MKL_THREADING_LAYER=GNU

# pth转huggingface
xtuner convert pth_to_hf \
  llava_internlm2_chat_1_8b_clip_vit_large_p14_336_e1_gpu8_pretrain \
  /root/tutorial/xtuner/llava/iter_2181.pth \
  /root/tutorial/xtuner/llava/llava_data/iter_2181_hf

# 启动!
xtuner chat /root/share/new_models/Shanghai_AI_Laboratory/internlm2-chat-1_8b \
  --visual-encoder /root/share/new_models/openai/clip-vit-large-patch14-336 \
  --llava /root/tutorial/xtuner/llava/llava_data/iter_2181_hf \
  --prompt-template internlm2_chat \
  --image /root/tutorial/xtuner/llava/llava_data/test_img/oph.jpg

在这里插入图片描述
可以看到 效果不太像,只能复述图片标题

训练后的:

# 解决小bug
export MKL_SERVICE_FORCE_INTEL=1
export MKL_THREADING_LAYER=GNU

# pth转huggingface
xtuner convert pth_to_hf \
  /root/tutorial/xtuner/llava/llava_internlm2_chat_1_8b_qlora_clip_vit_large_p14_336_lora_e1_gpu8_finetune_copy.py \
  /root/tutorial/xtuner/llava/work_dirs/llava_internlm2_chat_1_8b_qlora_clip_vit_large_p14_336_lora_e1_gpu8_finetune_copy/iter_1200.pth \
  /root/tutorial/xtuner/llava/llava_data/iter_1200_hf

# 启动!
xtuner chat /root/share/new_models/Shanghai_AI_Laboratory/internlm2-chat-1_8b \
  --visual-encoder /root/share/new_models/openai/clip-vit-large-patch14-336 \
  --llava /root/tutorial/xtuner/llava/llava_data/iter_1200_hf \
  --prompt-template internlm2_chat \
  --image /root/tutorial/xtuner/llava/llava_data/test_img/oph.jpg

在这里插入图片描述
可以看到 微调后模型可以更细致地描述图片的内容。