基础知识
Tutorial/docs/L1/XTuner/xtuner_finetune_basic.md at camp3 · InternLM/Tutorial (github.com)
- 预训练的 backbone 在 llm 也被叫做基座模型
- 优势:加快模型的收敛速度,降低模型过拟合的风险,并在不消耗过多计算资源的情况下获取较好的模型性能。
Finetune 的两种范式
在大模型的下游应用中,经常会用到两种微调模式:增量预训练 和 指令跟随 。(优化模型质量,制作 agent)
增量预训练
增量预训练是一种在已有预训练模型(比如:InternLM基座模型)的基础上,利用特定领域的数据进行进一步训练的方法。它的目的是在保持模型原有能力的同时,注入新的领域知识,进一步优化现有的预训练模型,从而提升模型在特定领域任务中的表现(比如:InternLM垂类基座模型)。
增量预训练模型能够接受少量的新数据进行更新并适应新的任务,而不需要重新训练整个模型,这种方式可以很好地利用现有的预训练模型的知识,并在新数据上获得更好的性能。
指令跟随
指令跟随是指让模型根据用户输入的指令来执行相应的操作。模型通过对大量自然语言指令和相应操作的数据进行训练,学习如何将指令分解为具体的子任务,并选择合适的模块来执行这些任务(比如:InternLM垂类对话模型)。
热门微调技术
大多数大型语言模型(LLM)的参数规模巨大,导致模型的训练和微调成本高昂。近年来,如何高效地对大模型进行微调成为了研究热点,LoRA 和 QLoRA 两种微调技术因其高效性和实用性受到了广泛关注。本次实验用 QLoRA 方法,会训练产生一个 Adapter,最后再把 Adapter 和基座模型合并到一起。
LoRA 简介
LoRA(Low-Rank Adaptation)是一种使用低精度权重对大型预训练语言模型进行微调的技术,它的核心思想是在不改变原有模型权重的情况下,通过添加少量新参数来进行微调。这种方法降低了模型的存储需求,也降低了计算成本,实现了对大模型的快速适应,同时保持了模型性能。
然而,由于使用了低精度权重,LoRA 的一个潜在的缺点是在微调过程中可能会丢失一些原始模型的高阶特征信息,因此可能会降低模型的准确性。
GPT 的 LoRA 教学
LoRA(Low-Rank Adaptation)的核心思想是通过低秩矩阵来有效地调整大型预训练模型的权重,以适应特定任务。
假设模型的某一层的权重矩阵为 \( W \),LoRA 通过增加一个低秩矩阵来进行调整:
\[
W' = W + \Delta W
\]
其中,\(\Delta W\) 是 LoRA 引入的调整量,表示为:
\[
\Delta W = A \cdot B
\]
- **\( A \)**:一个低秩矩阵,维度为 \( d \times r \)(\( d \) 是原始权重的维度,\( r \) 是低秩的秩)。
- **\( B \)**:另一个低秩矩阵,维度为 \( r \times d \)。
1. **低秩表示**:通过将参数调整表示为两个低秩矩阵的乘积,LoRA 将高维的参数调整表示为低维的表示,从而减少参数的数量和计算复杂度。
2. **缩放因子**:在实际应用中,调整量 \(\Delta W\) 还会乘以一个缩放因子 \( \alpha \),以控制其影响力:
\[
\Delta W = \alpha \cdot (A \cdot B)
\]
QLoRA 简介
QLoRA(Quantized LoRA)微调技术是对 LoRA 的一种改进,它通过引入高精度权重和可学习的低秩适配器来提高模型的准确性。并且在 LoRA 的基础上,引入了量化技术。
通过将预训练模型量化为 int4 格式,可以进一步减少微调过程中的计算量,同时也可以减少模型的存储空间,这对于在资源有限的设备上运行模型非常有用。最终,可以使我们在消费级的显卡上进行模型的微调训练。
XTuner 简介
XTuner 一个大语言模型 & 多模态模型微调工具箱。由 MMRazor 和 MMDeploy 联合开发。
- 傻瓜化: 以 配置文件 的形式封装了大部分微调场景,0基础的非专业人员也能一键开始微调。
- 轻量级: 对于 7B 参数量的LLM,微调所需的最小显存仅为 8GB 。支持 colab
功能亮点
- 适配多种生态
- 支持多种微调算法
- 适配多种开源生态(HuggingFace、ModelScope 等)
- 自动优化加速器
- 适配多种硬件
常用命令
列出包含指定名称的预定义配置文件
xtuner list-cfg -p $NAME
复制配置文件
xtuner copy-cfg $CONFIG $SAVE_PATH
执行微调训练
xtuner train $CONFIG
将 pth 格式的模型文件转换成 HuggingFace 格式的模型
xtuner convert pth_to_hf $CONFIG $PATH_TO_PTH_MODEL $SAVE_PATH_TO_HF_MODEL
CONFIG
表示微调的配置文件, PATH_TO_PTH_MODEL
表示微调的模型权重文件路径,即要转换的模型权重, SAVE_PATH_TO_HF_MODEL
表示转换后的 HuggingFace 格式文件的保存路径。
除此之外,我们其实还可以在转换的命令中添加几个额外的参数,包括:
参数名 | 解释 |
---|---|
--fp32 | 代表以fp32的精度开启,假如不输入则默认为fp16 |
--max-shard-size {GB} | 代表每个权重文件最大的大小(默认为2GB) |
将原始模型与微调结果进行合并
xtuner convert merge $LLM $ADAPTER $SAVE_PATH
该命令需要三个参数:LLM
表示原模型路径,ADAPTER
表示 Adapter 层的路径, SAVE_PATH
表示合并后的模型最终的保存路径。
在模型合并这一步还有其他很多的可选参数,包括:
参数名 | 解释 |
---|---|
--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 通过在大规模数据集上学习图像和对应文本之间的对应关系,从而实现了对图像内容的理解和分类,甚至能够根据文本提示生成图像。
实验部分
主要准备数据集 (data) 和训练配置文件 (logic)
微调数据集
微调数据集(不是全部数据集)文件是 datas/assistant.json
,在其中加入大量微调的相关内容即可。样例:
配置文件
xtuner list-cfg -p internlm2
配置文件名的解释
以 internlm2_1_8b_full_custom_pretrain_e1 和 internlm2_chat_1_8b_qlora_alpaca_e3 举例:
配置文件 internlm2_1_8b_full_custom_pretrain_e1 | 配置文件 internlm2_chat_1_8b_qlora_alpaca_e3 | 说明 |
---|---|---|
internlm2_1_8b | internlm2_chat_1_8b | 模型名称 |
full | qlora | 使用的算法 |
custom_pretrain | alpaca | 数据集名称 |
e1 | e3 | 把数据集跑几次 |
配置文件介绍
整体的配置文件分为五部分:
PART 1 Settings:涵盖了模型基本设置,如预训练模型的选择、数据集信息和训练过程中的一些基本参数(如批大小、学习率等)。
PART 2 Model & Tokenizer:指定了用于训练的模型和分词器的具体类型及其配置,包括预训练模型的路径和是否启用特定功能(如可变长度注意力),这是模型训练的核心组成部分。
PART 3 Dataset & Dataloader:描述了数据处理的细节,包括如何加载数据集、预处理步骤、批处理大小等,确保了模型能够接收到正确格式和质量的数据。
PART 4 Scheduler & Optimizer:配置了优化过程中的关键参数,如学习率调度策略和优化器的选择,这些是影响模型训练效果和速度的重要因素。
PART 5 Runtime:定义了训练过程中的额外设置,如日志记录、模型保存策略和自定义钩子等,以支持训练流程的监控、调试和结果的保存。
一般来说我们需要更改的部分其实只包括前三部分,而且修改的主要原因是我们修改了配置文件中规定的模型、数据集。后两部分都是 XTuner 官方帮我们优化好的东西,一般而言只有在魔改的情况下才需要进行修改。
配置文件的每一项配置就是一个 dict 对象。通过 dict['type'] 注册一个函数 (构造函数) 回调,然后其他的 attr 配置了 args。type 包括上面提到的分词器 (AutoTokenizer.from_pretrained), 模型,Optimizer,scheduler,等等。
本次实验案例
在 PART 1 的部分,由于我们不再需要在 HuggingFace 上自动下载模型,因此我们先要更换模型的路径以及数据集的路径为我们本地的路径。
为了训练过程中能够实时观察到模型的变化情况,XTuner 贴心的推出了一个 evaluation_inputs
的参数来让我们能够设置多个问题来确保模型在训练过程中的变化是朝着我们想要的方向前进的。我们可以添加自己的输入。
在 PART 3 的部分,由于我们准备的数据集是 JSON 格式的数据,并且对话内容已经是 input
和 output
的数据对,所以不需要进行格式转换。
# I delete the import section for 优快云
#######################################################################
# 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)
启动微调
当我们准备好了所有内容,我们只需要将使用 xtuner train $config
命令令即可开始训练。
xtuner train
命令用于启动模型微调进程。训练过程中产生的所有文件,包括日志、配置文件、检查点文件、微调后的模型等,默认保存在当前目录下的work_dirs
,可以使用--work-dir
参数覆盖。
在训练完后,我们的目录结构应该是这样子的。
├── work_dirs
│ └── internlm2_chat_1_8b_qlora_alpaca_e3_copy
│ ├── 20240626_222727
│ │ ├── 20240626_222727.log
│ │ └── vis_data
│ │ ├── 20240626_222727.json
│ │ ├── config.py
│ │ ├── eval_outputs_iter_95.txt
│ │ └── scalars.json
│ ├── internlm2_chat_1_8b_qlora_alpaca_e3_copy.py
│ ├── iter_96.pth
│ └── last_checkpoint
模型格式转换
模型转换的本质其实就是将原本使用 Pytorch 训练出来的模型权重文件转换为目前通用的 HuggingFace 格式文件。我们可以使用 xtuner convert pth_to_hf
命令来进行模型格式转换。
除此之外,我们其实还可以在转换的命令中添加几个额外的参数,包括:
参数名 | 解释 |
---|---|
--fp32 | 代表以fp32的精度开启,假如不输入则默认为fp16 |
--max-shard-size {GB} | 代表每个权重文件最大的大小(默认为2GB) |
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 格式文件。
├── hf
│ ├── README.md
│ ├── adapter_config.json
│ ├── adapter_model.bin
│ └── xtuner_config.py
此时,hf 文件夹即为我们平时所理解的所谓 “LoRA 模型文件”
可以简单理解:LoRA 模型文件 = Adapter
模型合并
对于 LoRA 或者 QLoRA 微调出来的模型其实并不是一个完整的模型,而是一个额外的层(Adapter),训练完的这个层最终还是要与原模型进行合并才能被正常的使用。
对于全量微调的模型(full)其实是不需要进行整合这一步的,因为全量微调修改的是原模型的权重而非微调一个新的 Adapter ,因此是不需要进行模型整合的。
在 XTuner 中提供了一键合并的命令 xtuner convert merge
,在使用前我们需要准备好三个路径,包括原模型的路径、训练好的 Adapter 层的(模型格式转换后的)路径以及最终保存的路径。
在模型合并这一步还有其他很多的可选参数,包括:
参数名 | 解释 |
---|---|
--max-shard-size {GB} | 代表每个权重文件最大的大小(默认为2GB) |
--device {device_name} | 这里指的就是device的名称,可选择的有cuda、cpu和auto,默认为cuda即使用gpu进行运算 |
--is-clip | 这个参数主要用于确定模型是不是CLIP模型,假如是的话就要加上,不是就不需要添加 |
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
微调后的模型对话
微调完成后,我们可以再次运行xtuner_streamlit_demo.py
脚本来观察微调后的对话效果,不过在运行之前,我们需要将脚本中的模型路径修改为微调后的模型的路径。
- model_name_or_path = "/root/InternLM/XTuner/Shanghai_AI_Laboratory/internlm2-chat-1_8b" + model_name_or_path = "/root/InternLM/XTuner/merged"
然后,我们可以直接启动应用。
streamlit run /root/InternLM/Tutorial/tools/xtuner_streamlit_demo.py
运行后,确保端口映射正常。
实验结果
显然过拟合了,xs