第五关
使用 XTuner 微调 InternLM2-Chat-1.8B 实现自己的小助手认知,如下图所示(图中的伍鲜同志需替换成自己的昵称),记录复现过程并截图。
一.准备工作
环境安装:我们想要用简单易上手的微调工具包 XTuner 来对模型进行微调的话,第一步是安装 XTuner !安装基础的工具是一切的前提,只有安装了 XTuner 我们才能够去执行后续的操作。
前期准备:在完成 XTuner 的安装后,我们下一步就需要去明确我们自己的微调目标了。我们想要利用微调做一些什么事情呢,然后为了实现这个目标,我们需要准备相关的硬件资源和数据。
启动微调:在确定了自己的微调目标后,我们就可以在 XTuner 的配置库中找到合适的配置文件并进行对应的修改。修改完成后即可一键启动训练!训练好的模型也可以仅仅通过在终端输入一行命令来完成转换和部署工作!
1.1创建开发机
准备好开发机之后,就可以进行下一步的微调任务了。
进入开发机之后,确保自己已经克隆了Tutorial仓库的资料到本地,可以运行一下命令。
mkdir -p /root/InternLM/Tutorial
git clone -b camp3 https://github.com/InternLM/Tutorial /root/InternLM/Tutorial
1.2创建虚拟环境
在安装 XTuner 之前,我们需要先创建一个虚拟环境。使用 Anaconda 创建一个名为 xtuner0121 的虚拟环境,可以直接执行命令。
# 创建虚拟环境
conda create -n xtuner0121 python=3.10 -y
# 激活虚拟环境(注意:后续的所有操作都需要在这个虚拟环境中进行)
conda activate xtuner0121
# 安装一些必要的库
conda install pytorch==2.1.2 torchvision==0.16.2 torchaudio==2.1.2 pytorch-cuda=12.1 -c pytorch -c nvidia -y
# 安装其他依赖
pip install transformers==4.39.3
pip install streamlit==1.36.0
1.3安装 XTuner
虚拟环境创建完成后,就可以安装 XTuner 了。首先,从 Github 上下载源码。
# 创建一个目录,用来存放源代码
mkdir -p /root/InternLM/code
cd /root/InternLM/code
git clone -b v0.1.21 https://github.com/InternLM/XTuner /root/InternLM/code/XTuner
其次,进入源码目录,执行安装。
# 进入到源码目录
cd /root/InternLM/code/XTuner
conda activate xtuner0121
# 执行安装
pip install -e '.[deepspeed]'
最后,我们可以验证一下安装结果。
xtuner version
1.4 模型准备
软件安装好后,我们就可以准备要微调的模型了。
对于学习而言,我们可以使用 InternLM 推出的1.8B的小模型来完成此次微调演示。
对于在 InternStudio 上运行的小伙伴们,可以不用通过 HuggingFace、OpenXLab 或者 Modelscope 进行模型的下载,在开发机中已经为我们提供了模型的本地文件,直接使用就可以了。
我们可以通过以下代码一键通过符号链接的方式链接到模型文件,这样既节省了空间,也便于管理。
# 创建一个目录,用来存放微调的所有资料,后续的所有操作都在该路径中进行
mkdir -p /root/InternLM/XTuner
cd /root/InternLM/XTuner
mkdir -p Shanghai_AI_Laboratory
ln -s /root/share/new_models/Shanghai_AI_Laboratory/internlm2-chat-1_8b Shanghai_AI_Laboratory/internlm2-chat-1_8b
执行上述操作后,Shanghai_AI_Laboratory/internlm2-chat-1_8b 将直接成为一个符号链接,这个链接指向 /root/share/new_models/Shanghai_AI_Laboratory/internlm2-chat-1_8b 的位置。
这意味着,当我们访问 Shanghai_AI_Laboratory/internlm2-chat-1_8b 时,实际上就是在访问 /root/share/new_models/Shanghai_AI_Laboratory/internlm2-chat-1_8b 目录下的内容。通过这种方式,我们无需复制任何数据,就可以直接利用现有的模型文件进行后续的微调操作,从而节省存储空间并简化文件管理。
模型文件准备好后,我们可以使用tree命令来观察目录结构。
apt-get install -y tree
tree -l
目录结构是这样的
├── Shanghai_AI_Laboratory
│ └── internlm2-chat-1_8b -> /root/share/new_models/Shanghai_AI_Laboratory/internlm2-chat-1_8b
│ ├── README.md
│ ├── config.json
│ ├── configuration.json
│ ├── configuration_internlm2.py
│ ├── generation_config.json
│ ├── model-00001-of-00002.safetensors
│ ├── model-00002-of-00002.safetensors
│ ├── model.safetensors.index.json
│ ├── modeling_internlm2.py
│ ├── special_tokens_map.json
│ ├── tokenization_internlm2.py
│ ├── tokenization_internlm2_fast.py
│ ├── tokenizer.model
│ └── tokenizer_config.json
二.快速开始
这里我们用 internlm2-chat-1_8b 模型,通过 QLoRA 的方式来微调一个自己的小助手认知作为案例来进行演示。
2.1 微调前的模型对话
我们可以通过网页端的 Demo 来看看微调前 internlm2-chat-1_8b 的对话效果。
首先,我们需要准备一个Streamlit程序的脚本。
Streamlit程序的完整代码是:tools/xtuner_streamlit_demo.py。
然后,我们可以直接启动应用。
conda activate xtuner0121
streamlit run /root/InternLM/Tutorial/tools/xtuner_streamlit_demo.py
2.2 指令跟随微调
下面我们对模型进行微调,让模型认识到自己的弟位,了解它自己是你的一个助手。
2.2.1 准数据文件
为了让模型能够认清自己的身份弟位,在询问自己是谁的时候按照我们预期的结果进行回复,我们就需要通过在微调数据集中大量加入这样的数据。我们准备一个数据集文件datas/assistant.json,文件内容为对话数据。
cd /root/InternLM/XTuner
mkdir -p datas
touch datas/assistant.json
为了简化数据文件准备,我们也可以通过脚本生成的方式来准备数据。创建一个脚本文件 xtuner_generate_assistant.py :
cd /root/InternLM/XTuner
touch xtuner_generate_assistant.py
为了训练出自己的小助手,需要将脚本中name后面的内容修改为你自己的名称。
# 将对应的name进行修改(在第4行的位置)
- name = '伍鲜同志'
+ name = "你自己的名称"
然后执行该脚本来生成数据文件。
cd /root/InternLM/XTuner
conda activate xtuner0121
python xtuner_generate_assistant.py
准备好数据文件后,我们的目录结构应该是这样子的。
├── Shanghai_AI_Laboratory
│ └── internlm2-chat-1_8b -> /root/share/new_models/Shanghai_AI_Laboratory/internlm2-chat-1_8b
│ ├── README.md
│ ├── config.json
│ ├── configuration.json
│ ├── configuration_internlm2.py
│ ├── generation_config.json
│ ├── model-00001-of-00002.safetensors
│ ├── model-00002-of-00002.safetensors
│ ├── model.safetensors.index.json
│ ├── modeling_internlm2.py
│ ├── special_tokens_map.json
│ ├── tokenization_internlm2.py
│ ├── tokenization_internlm2_fast.py
│ ├── tokenizer.model
│ └── tokenizer_config.json
├── datas
│ └── assistant.json
├── xtuner_generate_assistant.py
2.2.2 准备配置文件
在准备好了模型和数据集后,我们就要根据我们选择的微调方法结合微调方案来找到与我们最匹配的配置文件了,从而减少我们对配置文件的修改量。
下面我们将根据项目的需求一步步的进行修改和调整吧!
在 PART 1 的部分,由于我们不再需要在 HuggingFace 上自动下载模型,因此我们先要更换模型的路径以及数据集的路径为我们本地的路径。
为了训练过程中能够实时观察到模型的变化情况,XTuner 贴心的推出了一个 evaluation_inputs 的参数来让我们能够设置多个问题来确保模型在训练过程中的变化是朝着我们想要的方向前进的。我们可以添加自己的输入。
在 PART 3 的部分,由于我们准备的数据集是 JSON 格式的数据,并且对话内容已经是 input 和 output 的数据对,所以不需要进行格式转换。
internlm2_chat_1_8b_qlora_alpaca_e3_copy.py
# 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
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/InternLM/XTuner/Shanghai_AI_Laboratory/internlm2-chat-1_8b'
use_varlen_attn = False
# Data
alpaca_en_path = 'datas/assistant.json'
prompt_template = PROMPT_TEMPLATE.internlm2_chat
max_length = 2048
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 = 3
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 = SYSTEM_TEMPLATE.alpaca
evaluation_inputs = [
'请介绍一下你自己', 'Please introduce yourself'
]
#######################################################################
# 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 #
#######################################################################
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=None,
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)
2.2.3 启动微调
完成了所有的准备工作后,我们就可以正式的开始我们下一阶段的旅程:XTuner 启动~!当我们准备好了所有内容,我们只需要将使用 xtuner train 命令令即可开始训练。
cd /root/InternLM/XTuner
conda activate xtuner0121
xtuner train ./internlm2_chat_1_8b_qlora_alpaca_e3_copy.py
训练没10次迭代大概3分钟,由于开发机要到时间了只训练540就手动停止了,在训练完后,我们的目录结构应该是这样子的。
2.2.4 模型格式转换
模型转换的本质其实就是将原本使用 Pytorch 训练出来的模型权重文件转换为目前通用的 HuggingFace 格式文件,那么我们可以通过以下命令来实现一键转换。
我们可以使用 xtuner convert pth_to_hf 命令来进行模型格式转换。
cd /root/InternLM/XTuner
conda activate xtuner0121
# 先获取最后保存的一个pth文件
pth_file=`ls -t ./work_dirs/internlm2_chat_1_8b_qlora_alpaca_e3_copy/*.pth | head -n 1`
export MKL_SERVICE_FORCE_INTEL=1
export MKL_THREADING_LAYER=GNU
xtuner convert pth_to_hf ./internlm2_chat_1_8b_qlora_alpaca_e3_copy.py ${pth_file} ./hf
模型格式转换完成后,我们的目录结构应该是这样子的。
转换完成后,可以看到模型被转换为 HuggingFace 中常用的 .bin 格式文件,这就代表着文件成功被转化为 HuggingFace 格式了。
此时,hf 文件夹即为我们平时所理解的所谓 “LoRA 模型文件”
2.2.5 模型合并
对于 LoRA 或者 QLoRA 微调出来的模型其实并不是一个完整的模型,而是一个额外的层(Adapter),训练完的这个层最终还是要与原模型进行合并才能被正常的使用。
在 XTuner 中提供了一键合并的命令 xtuner convert merge,在使用前我们需要准备好三个路径,包括原模型的路径、训练好的 Adapter 层的(模型格式转换后的)路径以及最终保存的路径。
cd /root/InternLM/XTuner
conda activate xtuner0121
export MKL_SERVICE_FORCE_INTEL=1
export MKL_THREADING_LAYER=GNU
xtuner convert merge /root/InternLM/XTuner/Shanghai_AI_Laboratory/internlm2-chat-1_8b ./hf ./merged --max-shard-size 2GB
模型合并完成后,我们的目录结构应该是这样子的。
在模型合并完成后,我们就可以看到最终的模型和原模型文件夹非常相似,包括了分词器、权重文件、配置信息等等。
2.3 微调后的模型对话
微调完成后,我们可以再次运行xtuner_streamlit_demo.py脚本来观察微调后的对话效果,不过在运行之前,我们需要将脚本中的模型路径修改为微调后的模型的路径
# 直接修改脚本文件第18行
- model_name_or_path = "/root/InternLM/XTuner/Shanghai_AI_Laboratory/internlm2-chat-1_8b"
+ model_name_or_path = "/root/InternLM/XTuner/merged"
然后,我们可以直接启动应用。
conda activate xtuner0121
streamlit run /root/InternLM/Tutorial/tools/xtuner_streamlit_demo.py
完结