目录
一.引言
Hupu 上特别有意思的就是它的评分频道,如下图所示,给定一个领域,其中包含对应领域下的多个分领域人物或事件,网友会基于角色或事件进行打分,最后得到一个平均评分与高赞评论。基于下述数据,我们可以构建一个神评模型,也可以基于评分构建一个打分模型,下面我们使用 LLM 尝试构建一个评分模型。
二. Data Process
1.原始数据
原始数据包含多列:
Head | 标题 |
Ids | 投票 ID |
Introduce | 简介 |
Title | 分类 |
TitleHot | 分类热度 |
TitleScore | 分类评分 |
Comments | 神评 |
Starts | 神评点赞数 |
以第一条数据为例,Introduce 简介大类为游戏厂商,Title 对应小类为 R星,其热度为 4341,网友平均得分为 9.7,后面是 "|" 分割的神评与点赞数,这里我们不做处理。
2.数据处理
由于我们主要关注评分,所以这里一些神评会忽略,可以用作另一个神评模型使用。使用的字段为 Introduce 简介,Title 分类以及 Title Score 分类对应评分。
score_data = []
df = pd.read_csv(file_path, header=0)
for index, row in df.iterrows():
introduce = row["Introduce"]
title = row["Title"]
titleScore = row["TitleScore"]
if titleScore != "-":
print(f"Intro: {introduce} Title: {title} Score: {titleScore}")
instruction = f"{introduce}中的{title}你能打几颗星:"
sample = {"instruction": instruction, "input": "", "output": f"{titleScore}"}
score_data.append(sample)
output_path = "../data/hupu.json"
with open(output_path, "w", encoding='utf8') as f:
json.dump(score_data, f, ensure_ascii=False, indent=4)
3.SFT 数据
经过上面的处理我们得到了训练所需的 Json 数据,其中 Query 都是结构化的:
instruction = f"{introduce}中的{title}你能打几颗星:"
sample = {"instruction": instruction, "input": "", "output": f"{titleScore}"}
保存后样式如下:
这里评分数据我们一共 69903,按照 0.005 的比例设置验证集,每个训练 epoch 验证一次。
三.Training LoRA
1.训练参数
--lora_rank 8 \
--learning_rate 1e-4 \
--num_train_epochs 5.0 \
训练中一些主要训练参数如上,采用 LoRA 方式训练,学习率为 1e-4,训练 epoch 5。不过按以往的经验,训练 3 个 epoch 就差不多,这里底座模型采用 Baichaun2-13B-Chat。
2.训练指标
***** train metrics *****
epoch = 5.0
train_loss = 0.6223
train_runtime = 1:33:46.67
train_samples_per_second = 50.013
train_steps_per_second = 1.563
由于 Output 比较简单,模型整体收敛的也比较快:
把前面快速下降的部分去掉再看下趋势变化,后面随着 epoch 的增加还是有轻微的阶跃:
3.验证指标
***** eval metrics *****
epoch = 5.0
eval_loss = 0.6729
eval_runtime = 0:00:01.67
eval_samples_per_second = 168.973
eval_steps_per_second = 21.495
训练与验证的 Loss 整体保持一致,但也和生成的内容比较简单可能也有关系,因为只有分数。
四.Test Output
1.LoRA 模型输出
询问的 Query 保持与原始数据相同的格式,即 {introduce} 中的 {title} 你能打几颗星,ori 是原始回复,clean 是文本清理的逻辑,这里由于输出比较简单,所以 ori 和 clean 是一样的,这里 clean 的一般逻辑是去除一些无用的符号等。
2.指标评估
将上面的数据稍加清洗,再结合 LoRA 的输出结果,我们可以得到一批数据的原始评分和预测评分,随机抽了一部分看着偏差不是太大,很多都能完全命中。
def calculate_mae(real_values, predicted_values):
"""计算 MAE"""
return sum(abs(r - p) for r, p in zip(real_values, predicted_values)) / len(real_values)
def calculate_mse(real_values, predicted_values):
"""计算 MSE"""
return sum((r - p) ** 2 for r, p in zip(real_values, predicted_values)) / len(real_values)
def calculate_rmse(real_values, predicted_values):
"""计算 RMSE"""
mse = calculate_mse(real_values, predicted_values)
return mse ** 0.5
基于以上数据浅算以下 MAE、MSE 和 RMSE 的指标:
MAE: 1.1124102280798658
MSE: 3.629732065346047
RMSE: 1.9051855724170408
接下来我们用 _pre - _real 作差看一下柱状图的分布,可以看到有少部分离群点预测在 5 分的 gap 上,大部分在 ±2.5 分左右,大部分还是可以作为参考,不过和传统的机器学习回归模型预估相比,这个 MES 相对来说还是比较大的:
3.原始模型输出
做完上面的实验,博主想了下,因为有一些简历里的角色其实 Baichuan2-13B-Chat 模型应该是学习过的,会不会原生模型就具备评分的能力,所以我们撤掉了 Lora 的 adapter,用同样的 Prompt 看看原生模型是不是就可以直接打分了:
- 未知角色 => 忽略
LoRA 预测: 蜜蜂dongdong 6.0 6.0
- 已知角色 => 预测
LoRA 预测: 无殇 5.2 7.5,这里 BC 基于其在原作的表现中给无殇打至少 4 颗星,和我们模型整体打分趋势是一致的。
- 已知事物 => 建议
LoRA 预测: 乱斗丛林 5.5 5.5
五.总结
关于评分数据 SFT 测评大概的分享就这么多,一方面我们可以看到原始模型在对应评分数据训练后可以达到评分的作用,不过整体还存在一定误差;另一方面也可以看到原生模型在数据安全以及 RLHF 上做的努力,对于一些未知领域或者有主观意愿评价等可能会引战的 Prompt 输入,模型的输出整体而言不会有太大问题,不会出现胡乱评分的情况,这应该与人类意图对齐的安全数据有很大关系,通过 PPO、DPO 可以大大增加模型的安全性,在一定程度上保持其 Helpful & Harmless。
文末感谢提供数据的兄嘚: Taoooo!