从0开始微调LLama2系列 (3) - Lora微调
在上一期完成对模型简单体验之后,在这一期我们正式准备进行模型微调。模型微调的原理是利用一些特定领域的数据来提高模型在某些方面/领域的表现,增强模型的专业性、准确率等等。
本文首先介绍如何进行Lora微调,主要基于Chinese-LLaMA-Alpaca-2 项目。
注 :Chinese-LLaMA-Alpaca-2 项目 使用的是HuggingFace的Trainer接口,对多机多卡的支持并不友好
基本原理
对于LLM来说,模型的能力很大程度上来自于预训练过程,无论是RLHF还是fine-tuning,很大程度上是在做alignment [1]。
fine-tuning的效果和微调数据集、任务等因素相关。一般来说,高质量的数据会带来好的微调效果。基础模型比较擅长的任务,微调也能带来不错的提升。微调一般来说有两种方式:
- 全模型微调:不固定模型参数,基于SFT数据训练较少的epoch。全模型微调时,要特别注意可能出现灾难性遗忘(模型在多任务上效果下降)。训练数据的多样性(混合多个任务的SFT数据、包含原始预训练数据)非常重要。全模型微调本质上还是重复Pre-Train的过程,因此可以注入新的知识到模型中。
- 固定部分参数形式:增加一些旁路模块,微调时只更新这些旁路模块的参数,不更新整个模型,极大的减少所需要的显存。典型的adapter、Lora等微调方式,通过固定基础模型参数只训练额外参数等形式,有效避免了灾难性遗忘, 但效果可能不如全模型微调(未证实)。
数据集准备
我们所面对的场景主要是销售,基于销售和客户的对话构建出微调数据集。我们期望的是模型能够根据给出的对话内容,结构化的输出对对话内容的总结和提炼出来的标签,我们也期望模型能输出结构化的结果。
举个例子,假设对话内容是:
(客户):你好,我是玛丽,我听说你们这里有一家很好吃的餐厅,我想预订一张桌子。
(员工):你好,玛丽,欢迎来到我们餐厅!当然可以帮你预订桌子。请问您是几位?
(客户):我们是四个人,今晚7点半。
(员工):好的,四位玛丽,今晚7点半。请问您有任何特殊要求吗?比如靠窗户还是离吸烟区远一点?
(客户):靠窗户的位置最好,而且请给我们一个安静的角落,因为我们要庆祝一个特殊的场合。
(员工):明白了,靠窗户的安静角落,我已经记录下来了。请问您的联系电话是多少,以便我们确认预订?
(客户):我的电话号码是555-123-4567。
(员工):好的,谢谢您,玛丽。我们会提前为您准备好桌子,期待您的光临!
(客户):谢谢!期待今晚的晚餐。再见!
对对话内容的总结是,总结和内容标签可以完全利用GPT-4/3.5生成,避免人工手动标注数据:
- 总结: 玛丽预订了四人桌,要求靠窗户和安静角落,庆祝特殊场合,留下电话号码。
- 该内容的标签:餐厅预订,特殊场合,窗户位置,电话预订
微调时需要将这些数据处理成Stanford Alpaca格式,
[ {"instruction" : ..., "input" : ..., "output" : ...}, ...]
利用GPT-4写好转换脚本,最终微调的数据样例如下:
{
"instruction": "Based on the dialogue content, provide a summary and relevant tags. {\"dialogue\": \"(客户):你好,我是玛丽,我听说你们这里有一家很好吃的餐厅,我想预订一张桌子。(员工):你好,玛丽,欢迎来到我们餐厅!当然可以帮你预订桌子。请问您是几位?(客户):我们是四个人,今晚7点半。(员工):好的,四位玛丽,今晚7点半。请问您有任何特殊要求吗?比如靠窗户还是离吸烟区远一点?(客户):靠窗户的位置最好,而且请给我们一个安静的角落,因为我们要庆祝一个特殊的场合。(员工):明白了,靠窗户的安静角落,我已经记录下来了。请问您的联系电话是多少,以便我们确认预订?(客户):我的电话号码是555-123-4567。(员工):好的,谢谢您,玛丽。我们会提前为您准备好桌子,期待您的光临!(客户):谢谢!期待今晚的晚餐。再见!\"}",
"input": "",
"output": "{\"summary\": \"玛丽预订了四人桌,要求靠窗户和安静角落,庆祝特殊场合,留下电话号码。\", \"tags\": ["餐厅预订","特殊场合","窗户位置","电话预订"]}"
},
将数据集划分为train.json(训练数据)、valid.json(验证数据),保存在路径:/root/playground/finetuning-data/ih_test
Lora 微调
Chinese-LLaMA-Alpaca-2 提供了Lora微调的教程和脚本,在路径Chinese-LLaMA-Alpaca-2/scripts/training 下
核心脚本是run_clm_pt_with_perft.py(全模型微调)、run_clm_sft_with_peft.py(lora 微调)。对应的run_pt.sh, run_sft.sh给出了示例参数和如何运行。ds_zero2_no_offload.json是deepspeed的配 置。
deepspeed是微软开发的大模型训练优化框架,该框架应用了一系列技术。简单来说,由于LLM训练时需要大量显存,但这对大多数开发者并不友好,Deepspeed可以将显存占用load到内存中来减少显存占用,另外也支持模型并行、数据并行等技术。
打开run_sft脚本,我们能看到如下内容:
# 运行脚本前请仔细阅读wiki(https://github.com/ymcui/Chinese-LLaMA-Alpaca-2/wiki/sft_scripts_zh)
# Read the wiki(https://github.com/ymcui/Chinese-LLaMA-Alpaca-2/wiki/sft_scripts_zh) carefully before running the script
lr=1e-4
lora_rank=64
lora_alpha=128
lora_trainable="q_proj,v_proj,k_proj,o_proj,gate_proj,down_proj,up_proj"
modules_to_save="embed_tokens,lm_head"
lora_dropout=0.05
pretrained_model=path/to/hf/llama-2/or/chinese-llama-2/dir/or/model_id
chinese_tokenizer_path=path/to/chinese-llama-2/tokenizer/dir
dataset_dir=path/to/sft/data/dir
per_device_train_batch_size=1
per_device_eval_batch_size=1
gradient_accumulation_steps=8
max_seq_length=512
output_dir=output_dir
validation_file=validation_file_name
deepspeed_config_file=ds_zero2_no_offload.json
torchrun --nnodes 1 --nproc_per_node 1 run_clm_sft_with_peft.py \
--deepspeed ${deepspeed_config_file} \
--model_name_or_path ${pretrained_model} \
--tokenizer_name_or_path ${chinese_tokenizer_path} \
--dataset_dir ${dataset_dir} \
--per_device_train_batch_size ${per_device_train_batch_size} \
--per_device_eval_batch_size ${per_device_eval_batch_size} \
--do_train \
--do_eval \
--seed $RANDOM \
--fp16 \
--num_train_epochs 1 \
--lr_scheduler_type cosine \
--learning_rate ${lr} \
--warmup_ratio 0.03 \
--weight_decay 0 \
--logging_strategy steps \
--logging_steps 10 \
--save_strategy steps \
--save_total_limit 3 \
--evaluation_strategy steps \
--eval_steps 100 \
--save_steps 200 \
--gradient_accumulation_steps ${gradient_accumulation_steps} \
--preprocessing_num_workers 8 \
--max_seq_length ${max_seq_length} \
--output_dir ${output_dir} \
--overwrite_output_dir \
--ddp_timeout 30000 \
--logging_first_step True \
--lora_rank ${lora_rank} \
--lora_alpha ${lora_alpha} \
--trainable ${lora_trainable} \
--lora_dropout ${lora_dropout} \
--modules_to_save ${modules_to_save} \
--torch_dtype float16 \
--validation_file ${validation_file} \
--load_in_kbits 16 \
--gradient_checkpointing \
--ddp_find_unused_parameters False
一般来说我们需要关注的是如下参数:
# 预训练模型地址
pretrained_model=/root/playground/model/chinese-alpaca-2-13b
# 分词器地址 一般和模型地址相同
chinese_tokenizer_path=/root/playground/model/chinese-alpaca-2-13b
# 训练数据地址
dataset_dir=/root/playground/finetuning-data/ih_test
...
# 微调后模型输出地址
output_dir=/root/playground/model/finetuning-model
# 验证集地址
validation_file=/root/playground/finetuning-data/ih_test/valid.json
执行脚本后可以看到输出:
[WARNING|logging.py:305] 2023-10-01 14:48:37,523 >> `use_cache=True` is incompatible with gradient checkpointing. Setting `use_cache=False`...
{'loss': 1.5904, 'learning_rate': 5e-05, 'epoch': 0.02}
{'loss': 1.2601, 'learning_rate': 9.448285454973738e-05, 'epoch': 0.18}
{'loss': 1.0654, 'learning_rate': 7.413961013653726e-05, 'epoch': 0.36}
{'loss': 1.0089, 'learning_rate': 4.5560205233853266e-05, 'epoch': 0.54}
{'loss': 1.0417, 'learning_rate': 1.849560782091445e-05, 'epoch': 0.72}
{'loss': 1.0778, 'learning_rate': 2.179950786173879e-06, 'epoch': 0.9}
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 55/55 [03:39<00:00, 3.78s/it][INFO|trainer.py:1960] 2023-10-01 14:52:16,557 >>
Training completed. Do not forget to share your model on huggingface.co/models =)
{'train_runtime': 331.5023, 'train_samples_per_second': 1.345, 'train_steps_per_second': 0.166, 'train_loss': 1.0821823293512518, 'epoch': 0.99}
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 55/55 [03:39<00:00, 3.98s/it]
[INFO|tokenization_utils_base.py:2235] 2023-10-01 14:52:19,482 >> tokenizer config file saved in /root/playground/model/finetuning-model/sft_lora_model/tokenizer_config.json
[INFO|tokenization_utils_base.py:2242] 2023-10-01 14:52:19,486 >> Special tokens file saved in /root/playground/model/finetuning-model/sft_lora_model/special_tokens_map.json
***** train metrics *****
epoch = 0.99
train_loss = 1.0822
train_runtime = 0:05:31.50
train_samples = 446
train_samples_per_second = 1.345
train_steps_per_second = 0.166
10/01/2023 14:52:19 - INFO - __main__ - *** Evaluate ***
[INFO|trainer.py:3115] 2023-10-01 14:52:19,506 >> ***** Running Evaluation *****
[INFO|trainer.py:3117] 2023-10-01 14:52:19,507 >> Num examples = 49
[INFO|trainer.py:3120] 2023-10-01 14:52:19,507 >> Batch size = 1
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 49/49 [00:03<00:00, 14.47it/s]
***** eval metrics *****
epoch = 0.99
eval_loss = 0.7393
eval_runtime = 0:00:03.45
eval_samples = 49
eval_samples_per_second = 14.193
eval_steps_per_second = 14.193
perplexity = 2.0944
执行完成后载输出地址下可以看到如下内容:
(base) ➜ ~ ll /root/playground/model/finetuning-model
total 28K
-rw-r--r-- 1 root root 394 Oct 1 14:34 all_results.json
drwxr-xr-x 4 root root 4.0K Aug 28 18:02 checkpoint-200
drwxr-xr-x 4 root root 4.0K Aug 28 18:14 checkpoint-400
-rw-r--r-- 1 root root 223 Oct 1 14:34 eval_results.json
drwxr-xr-x 2 root root 4.0K Aug 19 11:37 sft_lora_model
-rw-r--r-- 1 root root 1.4K Oct 1 14:34 trainer_state.json
-rw-r--r-- 1 root root 192 Oct 1 14:34 train_results.json
在sft_lora_model下保存了我们的lora模型:
(base) ➜ ~ ll /root/playground/model/finetuning-model/sft_lora_model
total 1.6G
-rw-r--r-- 1 root root 522 Oct 1 14:34 adapter_config.json
-rw-r--r-- 1 root root 1.6G Oct 1 14:34 adapter_model.bin
-rw-r--r-- 1 root root 435 Oct 1 14:34 special_tokens_map.json
-rw-r--r-- 1 root root 844 Oct 1 14:34 tokenizer_config.json
-rw-r--r-- 1 root root 825K Oct 1 14:34 tokenizer.model
我们可以使用合并脚本来讲原始模型和lora模型进行合并,参考合并教程:
(base) ➜ Chinese-LLaMA-Alpaca-2 git:(master) python scripts/merge_llama2_with_chinese_lora_low_mem.py \
--base_model /mnt/bn/ih-llama2/root/playground/model/chinese-alpaca-2-13b \
--lora_model /root/playground/model/finetuning-model/sft_lora_model \
--output_type huggingface \
--output_dir /root/playground/model/finetuning-model/full_model
[2023-10-01 14:51:17,413] [INFO] [real_accelerator.py:158:get_accelerator] Setting ds_accelerator to cuda (auto detect)
================================================================================
Base model: /mnt/bn/ih-llama2/root/playground/model/chinese-alpaca-2-13b
LoRA model: /root/playground/model/finetuning-model/sft_lora_model
Loading /root/playground/model/finetuning-model/sft_lora_model
Loading ckpt pytorch_model-00001-of-00003.bin
Merging...
Saving ckpt pytorch_model-00001-of-00003.bin to /root/playground/model/finetuning-model/full_model in HF format...
Loading ckpt pytorch_model-00002-of-00003.bin
Merging...
Saving ckpt pytorch_model-00002-of-00003.bin to /root/playground/model/finetuning-model/full_model in HF format...
Loading ckpt pytorch_model-00003-of-00003.bin
Merging...
Saving ckpt pytorch_model-00003-of-00003.bin to /root/playground/model/finetuning-model/full_model in HF format...
Saving tokenizer
Saving config.json
Saving generation_config.json
Saving pytorch_model.bin.index.json
Done.
Check output dir: /root/playground/model/finetuning-model/full_model
最后展示一下显存占用大约为34GB
微调效果
先部署下模型:
(torch2) ➜ Chinese-LLaMA-Alpaca-2 git:(master) python scripts/openai_server_demo/openai_api_server.py --base_model /root/playground/model/finetuning-model/full_model
[2023-10-01 15:01:31,216] [INFO] [real_accelerator.py:133:get_accelerator] Setting ds_accelerator to cuda (auto detect)
USE_MEM_EFF_ATTENTION: True
STORE_KV_BEFORE_ROPE: False
Apply NTK scaling with ALPHA=1.0
Loading checkpoint shards: 100%|████████████████████████████████████████████████████████████████████████████████████████████████| 3/3 [00:18<00:00, 6.03s/it]
/root/miniconda3/envs/torch2/lib/python3.10/site-packages/transformers/generation/configuration_utils.py:362: UserWarning: `do_sample` is set to `False`. However, `temperature` is set to `0.9` -- this flag is only used in sample-based generation modes. You should set `do_sample=True` or unset `temperature`. This was detected when initializing the generation config instance, which means the corresponding file may hold incorrect parameterization and should be fixed.
warnings.warn(
/root/miniconda3/envs/torch2/lib/python3.10/site-packages/transformers/generation/configuration_utils.py:367: UserWarning: `do_sample` is set to `False`. However, `top_p` is set to `0.6` -- this flag is only used in sample-based generation modes. You should set `do_sample=True` or unset `top_p`. This was detected when initializing the generation config instance, which means the corresponding file may hold incorrect parameterization and should be fixed.
warnings.warn(
Vocab of the base model: 55296
Vocab of the tokenizer: 55296
2023-10-01 15:01:54,819 - INFO - Started server process [3360]
2023-10-01 15:01:54,820 - INFO - Waiting for application startup.
2023-10-01 15:01:54,820 - INFO - Application startup complete.
2023-10-01 15:01:54,820 - INFO - Uvicorn running on http://0.0.0.0:19327 (Press CTRL+C to quit)
测试一下:
(torch2) ➜ training git:(master) curl http://localhost:19327/v1/completions \
-H "Content-Type: application/json" \
-d '{
"prompt": "Based on the dialogue content, provide a summary and relevant tags. {\"dialogue\": \"(客户):你好,我是玛丽,我听说你们这里有一家很好吃的餐厅,我想预订一张桌子。(员工):你好,玛丽,欢迎来到我们餐厅!当然可以帮你预订桌子。请问您是几位?(客户):我们是四个人,今晚7点半。(员工):好的,四位玛丽,今晚7点半。请问您有任何特殊要求吗?比如靠窗户还是离吸烟区远一点?(客户):靠窗户的位置最好,而且请给我们一个安静的角落,因为我们要庆祝一个特殊的场合。(员工):明白了,靠窗户的安静角落,我已经记录下来了。请问您的联系电话是多少,以便我们确认预订?(客户):我的电话号码是555-123-4567。(员工):好的,谢谢您,玛丽。我们会提前为您准备好桌子,期待您的光临!(客户):谢谢!期待今晚的晚餐。再见!\"}"
}'
{"id":"cmpl-GtXwFJZdqfvWbtvGk7MzYx","object":"text_completion","created":1696143890,"model":"chinese-llama-alpaca-2","choices":[{"index":0,"text":"{\"summary\": \"客户询问关于座位、烟雾和电话联系等信息\", \"tags\": [\"座位安排\", \"烟雾问题\", \"电话联系\"]}"}]}#
看起来模型能够很好的输出结构化的数据。
下期预告
由于脚本使用的是Trainer接口,而该接口对模型并行&deepspeed-zero3支持并不是很好。因此如果想进行全模型微调/尝试更大的模型,在没有很好显卡的情况下显存经常吃紧,因此下一期带来如何利用原生Deepspeed进行微调LLM。