从0开始微调LLama2系列 (3) - Lora微调

2,821 阅读11分钟

从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

f1.png 核心脚本是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

f2.png

微调效果

先部署下模型:

(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。