0%

大模型应用系列(十三) 大模型微调项目实战:情绪对话模型

微调项目实战:微调一个情绪对话模型,分为数据集构建,模型选型,微调,部署, 开发五个阶段。

一. 设计

1.1 微调可以干什么?

微调的目标:基于现有的私有数据,让模型具备处理改数据的功能
注意:目前基于大模型的专业问答系统,核心技术并不是微调实现。

专业问答系统的应用落地核心是基于RAG来实现(微调+RAG)

1.2 为何不选择直接用微调来实现问答系统?

1.. 大模型存在缺陷——幻觉问题(一本正经地胡说八道),对于专业问答系统而言,幻觉的存在是不可容忍的,而模型微调是无法杜绝幻觉问题的。如果问题不在训练集中,就可能出现幻觉现象。
2.. 微调受到训练数据约束的,无法动态适应由于业务场景改变而带来的变化。

1.3. 微调目前如何落地?

如果当前的业务场景涉及到模型本身的变化: a. 自我认知变化(例如, 名称,功能介绍等);b. 模型的对话风格;c. 针对专业问答系统的问题理解不到位时,会使用微调技术帮助更好的地理解用户的问题。

二.项目实施流程

2.1 数据集构建
2.1.1 数据来源: (业务场景: 日常对话)

a.甲方提供;
b.自己搜集(成本较高,难度较大)。
制定数据标准(需要沟通)
数据获取方式:手动采集,爬虫,数据接口,AI生成。
c. 数据清洗,标注。
标注(1. 自动化标注;2. 人工标注)
d. 指定数据集格式。

本项目数据来源:
1.人工指定。
2.基于现有开源数据,让AI实现情绪数据制作。
注意:如果使用AI帮助处理数据集,尽可能选择效果好的API接口,不要使用本地的大模型来处理。

1.准备环境: 大模型, https://bigmodel.cn/ 参考开发文档。智朴清言
2.准备部分现有对话数据: [问题1,问题2,问题3]
指定模板,让AI根据模板的风格生成回答。

  1. 确定AI数据标注脚本的可行性后,得确定原始数据:input_data
    一般来说input_data时现成的,本项目用开源数据集。
    推荐两个: 微博情绪对话:CDial-GPT, LCCC

文本去重实现流程:

  1. 先对文本模型进行编码(Embedding模型实现: 将文本转为词向量)
  2. 使用数学算法比较相似度(余弦相似度,欧氏距离)
  3. 设定阈值,距离小于阈值则判定为重复。
    本项目用的Embedding模型: text2vec-base-chinese-sentence
2.1.2 具体代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import json
import time
import random
from zhipuai import ZhipuAI
from sentence_transformers import SentenceTransformer
import numpy as np
style_config = {
"温柔":{
"system_prompt":"你是一个温柔体贴的聊天助手,说话时总是充满关怀,使用以下特征:\n1.包含'呢、呀、啦'等语气词\n2. 使用🌸💖😊等温暖表情\n3. 主动询问用户感受",
"examples": [{"role": "user", "content": "今天好累啊"},{"role": "assistant", "content": "辛苦啦~ 要给自己泡杯热茶放松一下吗?🌸"},{"role": "user", "content": "考试没考好..."},{"role": "assistant", "content": "没关系的呀~ 下次一定会更好!需要我陪你聊聊吗?😊"}],
"temperature": 0.3
},
"毒舌":{
"system_prompt":"你是一个喜欢用犀利吐槽表达关心的朋友,需满足:\n1. 使用网络流行语(如'栓Q''退退退')\n2. 包含夸张比喻('你这速度堪比树懒')\n3. 结尾隐藏关心",
"examples": [{"role": "user", "content": "又胖了5斤!"},{"role": "assistant", "content": "好家伙!你这是要把体重秤压成分子料理?🏋"},{"role": "user", "content": "游戏又输了"},{"role": "assistant", "content": "菜就多练练!需要给你推荐《从零开始的电竞之路》吗?🎮"}],
"temperature": 0.5
},
}
#初始化模型
client = ZhipuAI(api_key="{改为你自己的API-KEY}") # 替换为你的API Key
#加载Embeddingmodel
style_model = SentenceTransformer(r"./text2vec-base-chinese")

# 生成函数
def generate_style_data(style_name, num_samples=50):
config = style_config[style_name]
data = []

# 构建消息上下文(包含系统提示和示例对话)
messages = [
{"role": "system", "content": config["system_prompt"]},
*config["examples"] # 直接展开示例对话
]

# 用户输入库(可自定义扩展)
# user_inputs = json.load()
with open(f"user_input.json", "r", encoding="utf-8") as f:
user_inputs = json.load(f)

for _ in range(num_samples):
try:
# 随机选择用户输入
user_msg = random.choice(user_inputs)

# 添加当前用户消息
current_messages = messages + [
{"role": "user", "content": user_msg}
]

# 调用API(修正模型名称)
response = client.chat.completions.create(
model="glm-3-turbo",
messages=current_messages,
temperature=config["temperature"],
max_tokens=100
)

# 获取回复内容(修正访问路径)
reply = response.choices[0].message.content
print(reply)
# 质量过滤(数据审核)
if is_valid_reply(style_name, user_msg, reply):
data.append({
"user": user_msg,
"assistant": reply,
"style": style_name
})

time.sleep(1.5) # 频率限制保护

except Exception as e:
print(f"生成失败:{str(e)}")
if len(data) % 500 == 0:
with open(f"{style_name}_data.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"数据已保存,有效样本数:{len(data)}")


def is_valid_reply(style, user_msg, reply):
"""质量过滤规则(添加空值检查)"""
# 基础检查
if not reply or len(reply.strip()) == 0:
return False

# 规则1:回复长度检查
if len(reply) < 5 or len(reply) > 150:
return False

# 规则2:风格关键词检查
style_keywords = {
"温柔": ["呢", "呀", "😊", "🌸"],
"毒舌": ["好家伙", "栓Q", "!", "🏋️"]
}
if not any(kw in reply for kw in style_keywords.get(style, [])):
return False

# 规则3:语义相似度检查
try:
ref_text = next(msg["content"] for msg in style_config[style]["examples"]
if msg["role"] == "assistant")
ref_vec = style_model.encode(ref_text)
reply_vec = style_model.encode(reply)
similarity = np.dot(ref_vec, reply_vec)
return similarity > 0.65
except:
return False

#=============================
#3.执行生成(添加容错)
#============================
if __name__ == '__main__':

print("开始生成温柔风格数据...")
style_name = "温柔"
generate_style_data(style_name, 12000)

print("开始生成毒舌风格数据...")
style_name = "毒舌"
generate_style_data(style_name, 12000)

2.2 模型选型

模型选型可以从以下几个方面进行:

  1. 模型系列: Qwen, ChatGPT, ChatGLM, llama等
  2. 模型类别: base, Instruct, chat等
  3. 模型大小: 参数量,根据任务复杂程度和资源规格选择。
  4. 模型能力: 通过评测区别

1. 模型系列。 本任务是中文对话生成,所以选择预训练数据中文多的模型,这里用Qwen2.5

2. 模型类别。 本任务是对话任务,所以选择Chat类。

3. 模型大小。 本任务比较简单,如果是coding或着math任务,则对模型能力要求比较高,在这个任务中1.5B可能就不错了,为了效果明显,这里选择7B和4B作为候选模型。

4. 模型能力。本任务主要考察模型的语义理解能力,根据需要的模型能力,选择对应的评测数据集进行评测。大模型应用系列(十二) 大模型评估,openCompass的安装和使用 | 乌漆嘛黑

通过以上1-3分析,选择Qwen1.5-0.5B-chat 和 Qwen1.5-1.8B-Chat 作为评测(其实实际上可能选择不同系列的模型,这里为了化简直接选择两个Qwen),具体评测方法参考, 当前任务大多是短语对话,这里选择CLUE中的FewCLUE_bustm_gen (短文本分类) 和 FewCLUE_ocnli_fc_gen(自然语言推理)对候选模型进行评估。 模型下载可以参考: 大模型应用系列(二) Huggingface的安装和使用 | 乌漆嘛黑, 评估结果如下,可以看出,1.8B模型性能更好。

image-20250607173916046

2.3 微调模型

参照前面的教程,微调得到情绪对话风格模型,这里我按照 大模型应用系列(十) 分布式训练与微调 | 乌漆嘛黑 使用Xtuner微调框架,因为这是对话模型,微调过程中适合用主观结果作为评测,llamaFactory的中间结果显示的是loss下降趋势,Xtuner中间结果是显示主观评测结果,比较适合我们。

2.4 部署模型

这里使用LmDeploy框架进行部署,因为它的性能比llvm高,可参考 大模型应用系列(六) ollama,vllm,LMDeploy 部署大模型 | 乌漆嘛黑 要注意的点是Xtuner微调过程中用的是自定义对话模板,所以要对齐对话模板,可以参考 大模型应用系列(九) 对话模板对齐 | 乌漆嘛黑 进行对话模板对齐。完成对话模板对齐以及LMDeploy部署后,输出如下:

image-20250607164222732

符合我们的对话风格。

2.5 软件开发

一般来说,大模型开发到部署成功就完成了,软件开发是交给其他开发人员做的,大模型开发人员只需要提供API给软件开发人员就行了,为了项目完整,这里使用python开发一个简单的界面,代码如下: 注意要改好api端口和模型地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import streamlit as st
from openai import OpenAI

# 初始化客户端
client = OpenAI(base_url="http://localhost:8000/v1/", api_key="suibianxie")

# 设置页面标题
st.title("项目一效果演示")

# 初始化session状态(仅用于显示历史)
if "messages" not in st.session_state:
st.session_state.messages = []

# 显示历史消息
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])

# 获取用户输入
if prompt := st.chat_input("请输入您的问题,或输入exit退出"):
# 处理退出命令
if prompt.lower() == "exit":
st.info("退出对话。")
st.stop()

# 添加用户消息到显示历史
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)

try:
# 发起API请求(每次只发送当前消息)
response = client.chat.completions.create(
messages=[{"role": "user", "content": prompt}], # 每次只发送当前问题
model="/root/autodl-tmp/Qwen1.5-0.5B-boot"
)

# 获取模型回复
model_response = response.choices[0].message.content

# 添加AI回复到显示历史
st.session_state.messages.append({"role": "assistant", "content": model_response})
with st.chat_message("assistant"):
st.markdown(model_response)

except Exception as e:
st.error(f"发生错误:{e}")

运行前先安装环境:

1
pip install streamlit

运行app

1
streamlit run app.py

运行后效果如下:

image-20250607234150289

三. 问题补充

3.1 Embedding模型
3.1.1 类别

现在的词嵌入模型分两种,huggingface model 和sentence model ,要用对应的方法加载,本文使用的是sentence model, (text2vec-base-chinese-sentence) 如果模型文件夹中没有Pooling文件夹,则需要下载有Pooling文件夹的版本

image-20250604220644677

3.1.2 文本相似度的计算

在计算文本相似度时,规范的方法是计算两个文本的词向量,归一化后再计算相似度。一般的嵌入模型最后都是有归一化的,但有些不规范可能没有,比如我用的 text2vec-base-chinese-sentence,最后的词向量没有进行归一化,这样会导致相似度的计算结果很大。

可以通过检查模型的model.json文件来判断是否有归一化层,如果只有两层,说明模型没有归一化层。

归一化分为数据归一化和模型归一化,对结果进行归一化的是数据归一化,对模型增加归一化层的是特征归一化。数据归一化是有偏差的。所以要进行模型归一化,对模型添加归一化层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import numpy as np
from sentence_transformers import SentenceTransformer,models

model_path = r"D:\PycharmProjects\demo_15\embedding_model\sungw111\text2vec-base-chinese-sentence"
bert = models.Transformer(model_path)
pooling = models.Pooling(bert.get_word_embedding_dimension(),
pooling_mode='mean')

# 添加缺失的归一化层
normalize = models.Normalize()

# 组合完整模型
full_model = SentenceTransformer(modules=[bert, pooling, normalize])
print(full_model)

save_path=r"D:\PycharmProjects\demo_15\embedding_model\zy\text2vec-base-chinese-sentence"
full_model.save(save_path)

# 加载修复后的模型
model = SentenceTransformer(r"D:\PycharmProjects\demo_15\embedding_model\zy\text2vec-base-chinese-sentence")

# 验证向量归一化
text = "测试文本"
vec = model.encode(text)
print("修正后模长:", np.linalg.norm(vec)) # 应输出≈1.0

转化之后模型文件夹下的model.json中会有normalize层

image-20250604221305857

3.2 经验
3.2.1 微调框架的选择

一般在大模型微调中,如果用Xtuner一般看主观评测,如果用LlamaFactory, 一般看loss下载趋势图,微调中loss一般可以降到0.05左右,大模型很难过拟合,所以epoch可以设置很大,然后根据评测结果或者loss趋势终止。

3.2.2 微调一般要多长时间

大模型微调一般很难过拟合,建议一开始采用比较大的epoch,在微调过程中根据测试结果终止。一般最后loss可以到0.05左右。

3.2.3 微调:有了系统提示词做角色定位,还需要微调做自我认知吗?

需要,根据系统提示词有时可能失效,因为模型本质的自我认知还是不变(比如它本质还是认为他是千问),一般的自我认知数据集枚举了可能的各种提问方式,可以本质上改变模型的自我认知(比如llamaFactory自带的自我认知数据集)

如果您读文章后有收获,可以打赏我喝咖啡哦~