福田附近网站开发公司,建设银行天津分行门户网站,怎么建一个购物网站,四川九江龙钢结构网架公司这里写目录标题 0 前言1 模型微调1.1 找到配置文件1.2 修改配置文件PART 1PART 2PART 3使用Tensorboard实现训练可视化 1.3 微调训练1.4 训练的停止条件1.5 模型转化、测试与合并模型的转化模型的测试模型合并 2 推理部署2.1 对话模板对齐#xff08;本节有bug#xff0c;具体… 这里写目录标题 0 前言1 模型微调1.1 找到配置文件1.2 修改配置文件PART 1PART 2PART 3使用Tensorboard实现训练可视化 1.3 微调训练1.4 训练的停止条件1.5 模型转化、测试与合并模型的转化模型的测试模型合并 2 推理部署2.1 对话模板对齐本节有bug具体看第2.4节2.2 启动 LMDeploy 推理服务2.3 使用Streamlit演示2.4 修复 LMDeploy 中的对话模板Bug 0 前言
上篇文章我们完成了数据集的制作得到了一个拥有近两万条样本的数据集随后进行了模型选型筛选出了 Qwen2.5-1.5B-Instruct 作为我们的基座模型这篇文章我们来完成剩下的工作包括模型的微调与部署。
1 模型微调
1.1 找到配置文件
本项目使用的微调框架为 Xtuner目前Xtuner只能支持Qwen1.5但是没关系我们可以把Qwen2.5的模型路径给配上。
先找到 qwen1_5_1_8b_chat 的配置文件本项目使用 QLoRA 微调所以我们这里用 qwen1_5_1_8b_chat_qlora_alpaca_e3.py位置如下图所示
注意这里必须是 Chat 模型的配置文件我一次找的是 qwen1_5_1_8b 不带 chat 的结果因为对话模板问题导致评估的时候生成的东西怪怪的。
先把配置文件复制一份到 /data/coding/utils/xtuner 目录下然后改名为 qwen2_5_chat_dialog_style.py接下来我们修改这个配置文件。
1.2 修改配置文件
下面的参数我是根据每张卡 12G 显存的GPU来配置的如果显存不一样只需要调整 batch_size 和 max_length 即可。
PART 1
这一部分要改的东西是最多的包括模型路径pretrained_model_name_or_path、数据集路径data_files、输入样本最大长度max_length、批次大小batch_size、训练的最大伦次max_epochs、保存的检查点数量save_total_limit还有用于主观评估的问题evaluation_inputs要修改的地方我已经在下面的程序片段中注释出来了evaluation_inputs没有注释出来。
#######################################################################
# PART 1 Settings #
#######################################################################
# Model
pretrained_model_name_or_path /data/coding/model_weights/Qwen/Qwen2.5-1.5B-Instruct # 修改
use_varlen_attn False# Data
# alpaca_en_path tatsu-lab/alpaca # 修改注释掉
data_files /data/coding/EmotionalDialogue/convert_data.json # 修改
prompt_template PROMPT_TEMPLATE.qwen_chat
max_length 256 # 修改
pack_to_max_length True# parallel
sequence_parallel_size 1# Scheduler Optimizer
batch_size 8 # per_device # 修改
accumulative_counts 16
accumulative_counts * sequence_parallel_size
dataloader_num_workers 0
max_epochs 1000 # 修改
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 5 # Maximum checkpoints to keep (-1 means unlimited) # 修改# Evaluate the generation performance during the training
evaluation_freq 500
SYSTEM SYSTEM_TEMPLATE.alpaca
evaluation_inputs [闺蜜把我秘密当谈资该不该撕破脸, 老妈非让我嫁给她同事儿子怎么逃啊,男朋友给女主播刷火箭算精神出轨吗, 室友半夜和对象视频娇喘怎么提醒, 亲戚说我不生孩子就是自私好想掀桌, 领导周末发60秒语音矩阵装没看见行吗,被同事追问有没有整容怎么优雅翻白眼, 相亲对象第一次见面就想搂肩油腻,暗恋的人突然问我喜欢什么类型, 针灸减肥被扎成仙人掌一斤没掉]
这里用于主观评估的问题至少要有5个以上我们本次微调用的数据达到了万这个级别了所以我这里选了10个挑的方式很随机。如果是其他项目的话用于主观评估的问题需要有代表性必须覆盖所有的目标场景。
PART 2
这部分只有一个 LoRA 的缩放系数改一下一般情况下lora_alpha 是秩的两倍这是前人总结出来的经验其他参数用默认。
#######################################################################
# PART 2 Model Tokenizer #
#######################################################################
tokenizer dict(typeAutoTokenizer.from_pretrained,pretrained_model_name_or_pathpretrained_model_name_or_path,trust_remote_codeTrue,padding_sideright,
)model dict(typeSupervisedFinetune,use_varlen_attnuse_varlen_attn,llmdict(typeAutoModelForCausalLM.from_pretrained,pretrained_model_name_or_pathpretrained_model_name_or_path,trust_remote_codeTrue,torch_dtypetorch.float16,quantization_configdict(typeBitsAndBytesConfig,load_in_4bitTrue,load_in_8bitFalse,llm_int8_threshold6.0,llm_int8_has_fp16_weightFalse,bnb_4bit_compute_dtypetorch.float16,bnb_4bit_use_double_quantTrue,bnb_4bit_quant_typenf4,),),loradict(typeLoraConfig,r64,lora_alpha128, # 修改lora_dropout0.1,biasnone,task_typeCAUSAL_LM,),
)PART 3
这部分主要改一下数据集相关的配置就两个地方dataset 和 dataset_map_fn其他用默认
#######################################################################
# PART 3 Dataset Dataloader #
#######################################################################
alpaca_en dict(typeprocess_hf_dataset,# datasetdict(typeload_dataset, pathalpaca_en_path), # 修改datasetdict(typeload_dataset, pathjson,data_filesdata_files), # 修改tokenizertokenizer,max_lengthmax_length,# dataset_map_fnalpaca_map_fn, # 修改dataset_map_fnNone, # 修改template_map_fndict(typetemplate_map_fn_factory, templateprompt_template),remove_unused_columnsTrue,shuffle_before_packTrue,pack_to_max_lengthpack_to_max_length,use_varlen_attnuse_varlen_attn,
)sampler SequenceParallelSampler if sequence_parallel_size 1 else DefaultSamplertrain_dataloader dict(batch_sizebatch_size,num_workersdataloader_num_workers,datasetalpaca_en,samplerdict(typesampler, shuffleTrue),collate_fndict(typedefault_collate_fn, use_varlen_attnuse_varlen_attn),
)使用Tensorboard实现训练可视化
在配置文件中搜索 set visualizer 然后按照如下方式修改
# set visualizer
from mmengine.visualization import Visualizer, TensorboardVisBackend
visualizer dict(typeVisualizer, vis_backends[dict(typeTensorboardVisBackend)])至此配置文件修改完毕。
1.3 微调训练
如果选择单卡微调那么命令为
xtuner train qwen2_5_chat_dialog_style.py显存占用情况如下
如果选择两张卡分布式微调那么命令为
NPROC_PER_NODE2 xtuner train qwen2_5_chat_dialog_style.py --deepspeed deepspeed_zero2因为我没跑所以我也不知道显存占用情况怎么样如果出现显存不足那就把 --deepspeed 改成 deepspeed_zero2_offload 或deepspeed_zero3也可以调小 batch_size 和 max_length。
损失函数下降情况可以利用 tensorboard 查看新开一个终端输入以下命令
tensorboard --logdir /data/coding/utils/xtuner/work_dirs/qwen2_5_chat_dialog_style/20250614_193605/vis_data然后在指定窗口http://localhost:6006/远程服务器需要SSH连接或者端口转发查看。 1.4 训练的停止条件
模型每训练完 500 个step就会评估一次下面截图是训练了3500个step后的评估结果 关于训练什么时候停止关键需要看这十个主观评估的问题是否都达到了预期的效果一个都不能少。每达到预期效果则说明没收敛当然即便这十个问题都达到了预期效果也不能说模型收敛了因为主观评估的问题太少有一定的偶然性。不过大模型训练到收敛是有难度的而且GPU算力资源有限因此没必要训练到收敛只要主观评估的结果连续若干次都能达到预期训练就可以停止了。
查看主观评估的结果不需要去翻控制台的打印信息也不需要去看日志在工作目录下的 vis_data 目录中
我训练了18000个step大概相当于31个epoch花了超过12小时发现十个主观评估问题都达到要求后就停止训练了。下面是损失函数下降情况
停止的时候损失降到了0.2以下。
1.5 模型转化、测试与合并
模型的转化
我们使用模型最后一个检查点因为Xtuner保存的检查点是低秩适配器而且是以 pth 的文件保存现在要将其转成 Hugging Face 模型使用以下命令
xtuner convert pth_to_hf qwen2_5_chat_dialog_style.py /data/coding/utils/xtuner/work_dirs/qwen2_5_chat_dialog_style/iter_18000.pth /data/coding/utils/xtuner/adapter_save_dir/qwen2_5/转化成功后控制台会显示 All done同时在我们的指定路径下有会保存的 hugging face 模型。
模型的测试
我们先来和模型对话看看效果怎么样在终端输入
xtuner chat /data/coding/model_weights/Qwen/Qwen2.5-1.5B-Instruct --adapter /data/coding/utils/xtuner/adapter_save_dir/qwen2_5 --prompt-template qwen_chat --system-template alpaca下面是测试情况 从上面的回复可以看到模型的回答风格达到了我们的预期说明模型达到想要的效果。我们还可以把十个主观评估的问题依次输入或者输入其他可用于主观评估的问题。
刚刚输入到终端的命令有两个模板 --prompt-template 是对话模板--system-template 是系统提示词模板。因为我们在配置文件中有 prompt_template PROMPT_TEMPLATE.qwen_chat 和 SYSTEM SYSTEM_TEMPLATE.alpaca 这两句话所以指定对话模板为 qwen_chat系统提示词模板为 alpaca。如果想查看有哪些模板可以在终端输入 xtuner chat -h 模型合并
终端输入以下命令
xtuner convert merge /data/coding/model_weights/Qwen/Qwen2.5-1.5B-Instruct /data/coding/utils/xtuner/adapter_save_dir/qwen2_5/ /data/coding/EmotionalDialogue/model_weights/Qwen2.5-1.5B-Dialog-Style/如果看到控制台输出 All done说明合并成功。
2 推理部署
2.1 对话模板对齐本节有bug具体看第2.4节
我们在训练的时候模板用的是 Xtuner 定义的 qwen_chat 和 alpaca我们可以在 xtuner/xtuner/utils/templates.py 中找到它们的详细定义
实际上Qwen1、Qwen1.5、Qwen2.5 自带的对话模板是不一样的但是训练框架不会管这么多它训练的时候用的都是 qwen_chat 和 alpaca我们在推理部署的时候需要使用训练时的对话模板。
常见的推理框架如 vLLM 和 LMDeploy 都是支持自己指定对话模板的vLLM 需要把对话模板转化为 jinjia2 格式LMDeploy 则需要转为 json 格式。本项目用的推理框架是 LMDeploy在 LMDeploy 的官方文档中指明了对话模板的组织样式
{model_name: your awesome chat template name, # 模型名称可以任意但不要和内置对话模板名相同否则会覆盖system: |im_start|system\n, # 系统提示词前缀meta_instruction: You are a robot developed by LMDeploy., # 系统提示词eosys: |im_end|\n, # 系统提示词后缀user: |im_start|user\n, # 用户提示词前缀eoh: |im_end|\n, # 用户提示词后缀assistant: |im_start|assistant\n, # 模型回复前缀eoa: |im_end|, # 模型回复后缀separator: \n, # 分隔符capability: chat, # 能力一般都是固定值 chatstop_words: [|im_end|] # 停止符
}输入到模型中的信息会被拼接成如下格式
# user_content 是用户输入的内容assistant_content 是模型答复
{system}{meta_instruction}{eosys}{user}{user_content}{eoh}{assistant}{assistant_content}{eoa}{separator}{user}...下面是我根据 Xtuner 中的 qwen_chat 和 alpaca 构建的对话模板
{model_name: Qwen2_5 Dialog Style,system: |im_start|system\n,meta_instruction: Below is an instruction that describes a task. Write a response that appropriately completes the request.\n,eosys: |im_end|\n,user: |im_start|user\n,eoh: |im_end|\n,assistant: |im_start|assistant\n,eoa: |im_end|,separator: \n,capability: chat,stop_words: [|im_end|, |endoftext|]
}将上述内容保存为 dialog_style_chat_template.json。这里separator 和 stop_words 在 qwen_chat 中有capability 固定为 chateoa 可以从之前微调的时候的主观评估输出中找它其实也是 qwen_chat 中的 SUFFIX 我们测试一下自定义的对话模板
lmdeploy chat /data/coding/EmotionalDialogue/model_weights/Qwen2.5-1.5B-Dialog-Style --chat-template /data/coding/utils/xtuner/dialog_style_chat_template.json结果如下 可以看到输出的信息和我们设定的模板一致第二轮因为涉及多轮对话所以第二轮没有系统提示词模型回复的后缀|im_end|会被后台程序自动去掉这个不重要。
我们可以对比一下原始模型在控制台输入
lmdeploy chat /data/coding/model_weights/Qwen/Qwen2.5-1.5B-Instruct使用原始模型时不需要指定对话模板lmdeploy 会去模型的权重文件目录中找自带的对话模板即 Qwen2.5 官方推出的对话模板。下面是原始模型的问答记录 可以看到微调后的模型与微调前答复的风格有了明显不同。
2.2 启动 LMDeploy 推理服务
启动推理比较简单只需要一行命令启动服务
lmdeploy serve api_server /data/coding/EmotionalDialogue/model_weights/Qwen2.5-1.5B-Dialog-Style --chat-template /data/coding/utils/xtuner/dialog_style_chat_template.json --model-name dialog_style如果控制台出现下面的信息说明推理的服务已经启动
如果想使用 KV Cache 量化可以指定 --quant-policy即
lmdeploy serve api_server /data/coding/EmotionalDialogue/model_weights/Qwen2.5-1.5B-Dialog-Style --chat-template /data/coding/utils/xtuner/dialog_style_chat_template.json --model-name dialog_style --quant-policy 82.3 使用Streamlit演示
新建一个名为 demo_lmdeploy_streamlit.py 的文件内容如下
import streamlit as st
from openai import OpenAI# 合并对话历史
def build_messages(prompt, history):if history:messages history.copy()else:# 若 history 为 None则说明是单轮对话messages []user_message {role: user, content: prompt}messages.append(user_message)return messages# 初始化客户端
st.cache_resource
def get_client():# 如果没有 st.cache_resource那么每次在前端界面输入信息时程序就会再次执行导致模型重复导入client OpenAI(base_urlhttp://0.0.0.0:23333/v1/,api_keysuibianxie)return client# 创建一个标题和一个副标题
st.title( Style Chatbot)
st.caption( A streamlit chatbot powered by Self-LLM)# 如果session_state中没有messages则创建一个包含默认消息的列表
if messages not in st.session_state:st.session_state[messages] []# 遍历session_state中的所有消息并显示在聊天界面上
for msg in st.session_state.messages:st.chat_message(msg[role]).write(msg[content])# 初始化客户端
client get_client()# 如果用户在聊天输入框中输入了内容则执行以下操作
if prompt : st.chat_input():# 在聊天界面上显示用户的输入st.chat_message(user).write(prompt)# 将当前提示词添加到消息列表由于只考虑单轮因此 history 置为 Nonemessages build_messages(promptprompt, historyNone)#调用模型chat_complition client.chat.completions.create(messagesmessages,modeldialog_style)#获取回答model_response chat_complition.choices[0]response_text model_response.message.content# 将用户问题和模型的输出添加到session_state中的messages列表中st.session_state.messages.append({role: user, content: prompt})st.session_state.messages.append({role: assistant, content: response_text})# 在聊天界面上显示模型的输出st.chat_message(assistant).write(response_text)
在终端中运行以下命令启动streamlit服务并将端口映射到本地然后在浏览器中打开链接 http://localhost:6006/ 即可看到聊天界面。
streamlit run demo_lmdeploy_streamlit.py --server.address 127.0.0.1 --server.port 6006界面如下 可以用相同的输入多试几次如果每次生成的答案都一样则说明过拟合。因为大模型在生成答案的时候是通过softmax对词表中各个单词计算概率如果多次输入同一个问题而每次答案都一样假设生成的序列为“你好幸苦呀”说明在生成第一个 token 的时候“你” 这个字的概率远远高于其他字在生成第二个 token 的时候“好”这个字的概率远远高于其他字后面的序列都是如此最后导致生成的答案几乎没有了随机性。
我们训练用的数据接近两万条但只有一千个问题也就是说每个问题对应20种回答所以本项目不太可能出现过拟合因为在生成每个 token 的时候在多个方向都有一定的概率使得模型的回复有一定的随机性。
尝试如下 2.4 修复 LMDeploy 中的对话模板Bug
经过反复盘查发现是对话模板出了问题LMDeploy 的官方文档给的对话模板格式有问题可能是版本比较老的官方文档。下面是我最终的对话模板
{meta_instruction: You are a helpful assistant.,capability: chat,eosys: |im_end|\n,eoh: |im_end|\n,system: |im_start|system\n{{ system }}|im_end|\n,user: |im_start|user\n{{ input }}|im_end|,assistant: |im_start|assistant\n,eoa: |im_end|,separator: \n,stop_words: [|im_end|,|endoftext|]
}上面的对话模板除了meta_instruction之外其他一个字符都不能改。因为我尝试了很多该法比如增加 model_name 字段、去掉 eosys 和 eoh 字段user 字段的换行符去掉等每修改一次进行300次单轮对话实验即重启 LMDeploy 推理服务然后运行下面的统计代码结果都会出现问题包括但不限于生成内容不带风格名称、生成内容全为温柔风格等。唯一改了基本没影响的只有把 meta_instruction 字段改成Below is an instruction that describes a task. Write a response that appropriately completes the request.\n。
下面是统计的代码
from openai import OpenAI
from tqdm import tqdm# 初始化客户端
client OpenAI(base_urlhttp://0.0.0.0:23333/v1/,api_keysuibianxie)# 提示词
prompt 相亲对象第一次见面就想搂肩油腻
messages[{role:user,content: prompt}]# 统计信息初始化
style_counts {温柔:0, 毒舌:0, 温柔开头的其他回答:0, 毒舌开头的其他回答:0, 不带风格名称:0}# 获取异常回复
abnormal_answer []for i in tqdm(range(300)):# 调用模型try:chat_completion client.chat.completions.create(messagesmessages,model/data/coding/EmotionalDialogue/model_weights/Qwen2.5-1.5B-Dialog-Style)except:chat_completion client.chat.completions.create(messagesmessages,modeldialog_style)# 获取输出内容model_reply chat_completion.choices[0].message.content# 统计输出结果key model_reply.split(\n)[0]if key 温柔 or key 毒舌:style_counts[key] 1else:if key[:2] 温柔:style_counts[温柔开头的其他回答] 1elif key[:2] 毒舌:style_counts[毒舌开头的其他回答] 1else:style_counts[不带风格名称] 1line str(i) keyabnormal_answer.append(line)# 异常信息写到 txt 文件中
with open(abnormal_answer.txt, w, encodingutf-8) as f:for item in abnormal_answer:f.write(item \n) # 每个字符串单独一行print(style_counts)
输出
{温柔: 206, 毒舌: 94, 温柔开头的其他回答: 0, 毒舌开头的其他回答: 0, 不带风格名称: 0}假如我不另外指定对话模板此时 LMDeploy 会去模型的权重目录中找自带的对话模板我再统计一次结果为
{温柔: 26, 毒舌: 274, 温柔开头的其他回答: 0, 毒舌开头的其他回答: 0, 不带风格名称: 0}我们训练用的数据集两种风格的比例是1:1上面的两次统计结果均严重偏离这个比例原因我也不知道。总之LMDeploy 的对话模板坑比较多而且特别玄学。
Qwen 模型的对话模板现在是解决了那以后要是微调其他模型推理的时候怎么改对话模板我找到了一份转换代码下面介绍如何使用。
假设我们现在要微调 Chat-GLM3先找到 Xtuner 中相关的对话模板 我们把 Xtuner 中的对话模板复制然后粘贴到下面 train_chat 后面
import re
import json
from typing import Dict, Anydef universal_converter(original_template: Dict[str, Any]) - Dict[str, Any]:将多种风格的原始模板转换为lmdeploy官方格式# 字段映射关系核心逻辑field_mapping {# 基础字段映射SYSTEM: system,INSTRUCTION: (user, assistant), # 需要拆分处理SUFFIX: eoa,SEP: separator,STOP_WORDS: stop_words,# 特殊处理字段SUFFIX_AS_EOS: None, # 该字段在官方模板中不需要}# 初始化目标模板包含必填字段默认值converted {meta_instruction: You are a helpful assistant., # 必填项capability: chat, # 必填项eosys: |im_end|\n, # 通常固定格式eoh: |im_end|\n, # 通常固定格式}# 自动处理字段映射for src_key, dest_key in field_mapping.items():if src_key in original_template:value original_template[src_key]# 处理需要拆分的字段如INSTRUCTIONif isinstance(dest_key, tuple) and src_key INSTRUCTION:# 使用正则拆分user和assistant部分parts re.split(r(\|im_start\|assistant\n?), value)converted[user] parts[0].strip()if len(parts) 1:converted[assistant] parts[1] parts[2] if len(parts) 2 else parts[1]# 处理直接映射字段elif dest_key and not isinstance(dest_key, tuple):converted[dest_key] value# 特殊处理system字段的占位符if system in converted:converted[system] converted[system].replace({system}, {{ system }})# 处理用户输入占位符if user in converted:converted[user] converted[user].replace({input}, {{ input }})# 自动处理停止词兼容列表和字符串if stop_words in converted and isinstance(converted[stop_words], str):converted[stop_words] [converted[stop_words]]# 保留原始模板中的额外字段带警告for key in original_template:if key not in field_mapping:print(fWarning: 发现未映射字段 [{key}]已保留原样)converted[key] original_template[key]return converted# 示例用法## qwen_chat
# train_chat dict(
# SYSTEM(|im_start|system\n{system}|im_end|\n),
# INSTRUCTION(|im_start|user\n{input}|im_end|\n |im_start|assistant\n),
# SUFFIX|im_end|,
# SUFFIX_AS_EOSTrue,
# SEP\n,
# STOP_WORDS[|im_end|, |endoftext|]
# )## chat
train_chat dict(SYSTEM|system|\n{system},INSTRUCTION|user|\n{input}|assistant|\n,SEP\n,)# 执行转换
converted_template universal_converter(train_chat)# 生成JSON文件
with open(chat_template.json, w) as f:json.dump(converted_template, f,indent2,ensure_asciiFalse,separators(,, : ))运行成功后用于 LMDeploy 的对话模板将保存在 chat_template.json 中。
如果这种方式不管用那就不指定对话模板了让 LMDeploy 去模型的权重目录中找自带的对话模板。