一 背景
模型微调在大模型的使用场景中比较常见,其中LLaMA Factory是一款开源低代码大模型微调框架,集成了业界最广泛使用的微调技术,支持通过Web UI界面零代码微调大模型,目前已经成为开源社区内最受欢迎的微调框架之一,本文主要通过使用开源的LLaMA Factory进行模型微调的相关技术学习。
二 环境
本文主要使用了阿里云PAI提供的DSW作为测试环境,相关环境准备如下
1. 环境初始化
登录PAI的GPU环境,执行以下命令,安装LLaMA-Factory相关依赖
git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git pip uninstall -y vllm pip install llamafactory[metrics]==0.7.1
安装后可以通过以下命令判断是否安装成功:
llamafactory-cli version
成功截图如下:
2.数据准备
cd LLaMA-Factory wget https://atp-modelzoo-sh.oss-cn-shanghai.aliyuncs.com/release/tutorials/llama_factory/data.zip mv data rawdata unzip data.zip -d data
测试数据包容以下内容
其中train.json用作微调数据,eval用做评测数据。其中在数据内添加了很多按照诸葛亮的身份相关的对话。
其中数据格式主要采用了sharegpt 格式,数据格式如下图所示。sharegpt 格式支持更多的角色种类,例如 human、gpt、observation、function 等等。它们构成一个对象列表呈现在 conversations 列中。
3.模型选择
由于大模型对资源要求比较高,如果选择大参数的模型,在微调过程中经常会出现oom的问题,所以本次选取参数量较小的Qwen1.5-0.5B模型进行验证测试。
三 微调
通过以下命令启动微调页面
export USE_MODELSCOPE_HUB=1 && \ llamafactory-cli webui
其中相关参数说明如下:
参数的介绍如下:
- 截断长度:一条数据分词后会成为一个 token 序列,当 token 序列的长度超过截断长度时会被分割成若干段输入进模型,这里保持1024不变;
- 学习率:设置为 1e-4;
- 训练轮数设置为 3.0,最大样本数为 100000;
- 计算类型使用 fp16(V100 并不支持 bf16);
- 批处理大小(Batch Size)设为 2(单卡3090);
- 梯度累计(Gradient Accumulation):增大该参数可以减少显存的占用,本次实验设置为 2;
- 学习率调节器:使用默认的 cosine
- 最大梯度范数:用于梯度裁剪的范数,默认为 1.0
- 验证集比例使用:0
相关选项如上图,最后生产命令如下
CUDA_VISIBLE_DEVICES=0 llamafactory-cli train \ --stage sft \ --do_train True \ --model_name_or_path qwen/Qwen1.5-0.5B \ --preprocessing_num_workers 16 \ --finetuning_type lora \ --template default \ --flash_attn auto \ --dataset_dir data \ --dataset train \ --cutoff_len 1024 \ --learning_rate 0.0001 \ --num_train_epochs 3.0 \ --max_samples 100000 \ --per_device_train_batch_size 2 \ --gradient_accumulation_steps 2 \ --lr_scheduler_type cosine \ --max_grad_norm 1.0 \ --logging_steps 5 \ --save_steps 100 \ --warmup_steps 0 \ --optim adamw_torch \ --packing False \ --report_to none \ --output_dir saves/Qwen1.5-0.5B/lora/train_2024-06-01-20-46-31 \ --fp16 True \ --plot_loss True \ --lora_rank 8 \ --lora_alpha 16 \ --lora_dropout 0 \ --loraplus_lr_ratio 16 \ --lora_target all
点击微调,微调截图如下
微调结束后页面显示训练完毕。
四 评估
切换到评估页签,选择上一步生成的适配器路径,开始对模型进行评估:
对应命令如下:
CUDA_VISIBLE_DEVICES=0 llamafactory-cli train \ --stage sft \ --model_name_or_path qwen/Qwen1.5-0.5B \ --adapter_name_or_path saves/Qwen1.5-0.5B/lora/train_2024-06-01-20-46-31 \ --preprocessing_num_workers 16 \ --finetuning_type lora \ --template default \ --flash_attn auto \ --dataset_dir data \ --dataset eval \ --cutoff_len 1024 \ --max_samples 100000 \ --per_device_eval_batch_size 2 \ --predict_with_generate True \ --max_new_tokens 512 \ --top_p 0.7 \ --temperature 0.95 \ --output_dir saves/Qwen1.5-0.5B/lora/eval_2024-06-01-20-45-31 \ --do_predict True
评估后截图如下
其中 ROUGE 分数衡量了模型输出答案(predict)和验证集中标准答案(label)的 相似度,ROUGE 分数越高代表模型学习得更好。
各个字段详细含义如下:
predict_bleu-4: 4-gram BLEU 分数,表示生成的文本与参考答案之间的相似度。BLEU 分数越高,表示生成的文本与参考答案越相似。
predict_rouge-1: ROUGE-1 分数,用于衡量生成的文本与参考答案之间的重叠程度,其中包含一个词的片段。ROUGE-1 分数越高,表示生成的文本包含了更多参考答案中的短语或词组。
predict_rouge-2: ROUGE-2 分数,与 ROUGE-1 类似,但考虑了两个连续词的片段。ROUGE-2 分数越高,表示生成的文本与参考答案之间的重叠程度更高。
predict_rouge-l: ROUGE-L 分数,使用最长公共子序列(Longest Common Subsequence,LCS)作为匹配标准,考虑了生成的文本和参考答案之间的最长匹配子序列。ROUGE-L 分数越高,表示生成的文本与参考答案之间的重叠程度更高。
predict_runtime: 生成文本的运行时间,表示生成模型完成预测所花费的时间。
predict_samples_per_second: 每秒生成的样本数,表示生成模型在单位时间内处理的样本数量。
predict_steps_per_second: 每秒生成的步数,表示生成模型在单位时间内完成的步数。步数可以是模型训练的迭代步骤或推理的步骤,具体取决于任务和模型。
我们可以选择去掉适配器,用原始的模型进行评估,评估效果如下,可以看到对比上面使用微调模型的,数据还是低一些:
五 chat验证
1.微调后的模型
使用微调后的适配器进行加载模型
从回答中可以发现模型学习到了数据集中的内容,能够恰当地模仿诸葛亮的语气对话。
2.老模式
选择卸载模型,然后清空适配器,在选择加载模型,则修改成原始模型,
重新向模型发送相同的内容,发现原始模型无法模仿诸葛亮的语气生成中文回答。
六 导出
1.微调模型导出
导出成功后可以在目标目录生成的模型文件如下图所示:
2.测试代码
为了对比方便,我们先用正常没有经过微调的模型测试,测试代码如下:
from threading import Thread from modelscope import (AutoModelForCausalLM, AutoTokenizer) from transformers import TextIteratorStreamer device = "cuda" # 将模型加载到哪个硬件,此处为GPU from modelscope import snapshot_download model_dir = snapshot_download('qwen/Qwen1.5-0.5B-Chat') # model_dir="Qwen1.5-0.5B/lora/0603" print(f"模型地址model_dir:{str(model_dir)}") model = AutoModelForCausalLM.from_pretrained( model_dir, # 模型文件夹路径 device_map="auto" ) tokenizer = AutoTokenizer.from_pretrained(model_dir) while True: user_input = input("请输入问题(q退出):") if user_input.lower() == "q": print("exit") break try: messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": user_input} ] text = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=False ) inputs = tokenizer([text], return_tensors="pt").to(device) streamer = TextIteratorStreamer(tokenizer) generation_kwargs = dict(inputs, streamer=streamer, max_new_tokens=512) thread = Thread(target=model.generate, kwargs=generation_kwargs) thread.start() generated_text = "" count = 0 for new_text in streamer: generated_text += new_text print(new_text, end="", flush=True) print() except Exception as e: print(f"出错了:{str(e)}")
3.正常模型测试效果
4.微调后模型测试
然后我们将代码中的模型地址换成我们上一步导出的微调模型地址
效果如下
通过上面的测试代码可以看到微调后的模型已经可以正常工作了,满足预期。