Chinese-Vicuna中的对话数据处理方式
最近尝试着使用 alpaca-lora 微调出来一个能唠嗑的模型;这里面做一些记录。
这篇关于:如何处理对话数据用于训练一个对话模型。
Chinese-Vicuna中的对话数据处理方式
我们看到的微调,比如说 standard_alpaca ,或者 alpaca-lora ,这些都是单轮的对话,也就是说,我问一个问题,微调后的模型回答一个问题。就完事了。
这与唠嗑相差甚大,唠嗑是有上下文的关系的,如果模型在训练的时候只是给单轮对话的信息的话,就不是很适合唠嗑的场景。那么对于多轮对话的模型,我们该怎么去组织数据呢?
本文记录的是基于 Chinese-Vicuna 来看如何来组织数据训练一个可以多轮唠嗑的模型。
原始数据
本文以 BelleGroup/train_3.5M_CN 中的数据来举例子,这个数据量级很大,也包含多轮对话的数据。
首先我们加载数据:
from datasets import load_dataset, Dataset
data_belle = load_dataset("BelleGroup/train_3.5M_CN")
print(data_belle)
DatasetDict({
train: Dataset({
features: ['conversations', 'id'],
num_rows: 3606402
})
这个数据集一共有3606402条数据,举个例子看一条(选这一条是因为刚好这个数据集的第一条,不是多轮对话的方式……所以就顺位选了第二条),这一条也将作为我们的demo数据,来演示多轮对话的时候,输入到模型里面长什么样子。这个数据一共有两轮对话,而且两轮对话之间关系十分密切。
import json
demo_data = [data_belle['train'][1]]
data = Dataset.from_list(demo_data)
print(json.dumps(data[0], indent=2, ensure_ascii=False))
{
"conversations": [
"from": "human",
"value": "给定一段文本和关键词列表,删除文本中包含所有给定关键词的子字符串。\n文本:\"这是一个测试句子,目的是看看模型是否可以正确地从这个句子中删除关键词。\"\\n关键词列表:[‘测试’,‘模型’]"
"from": "assistant",
"value": "删除包含所有给定关键词的子字符串后,文本变为:\"这是一个句子,目的是看看是否可以正确地从这个句子中删除关键词。\""
"from": "human",
"value": "好的。现在请你将这个文本中的所有的逗号都替换成空格。"
"from": "assistant",
"value": "好的,请稍等一下,现在我会将文本中的所有逗号替换为空格。处理后文本为:\"这是一个句子 目的是看看是否可以正确地从这个句子中删除关键词。\"。处理结果如何?"
"id": "16012449"
}
Chinese-Vicuna的处理方式
我们分别来看下 Chinese-Vicuna 是如何处理这一条数据的。
本文主要关注于如何处理原始数据,生成可以用来训练多轮对话模型的模型输入,再加上不同的仓库的输入数据格式也不大一样,和我们举的例子也不大一样,所以和源码相比,可能会有一些变化。
微调训练的代码是:
finetune_chat.py
生成
prompt
的代码是
prompt.py
中的
chat_prompt
。
在使用指令数据进行微调的时候,一般都会有一个模板,将对话的信息镶嵌到模板中去,之后将镶嵌后的整体信息送入模型进行训练。
先来看下 Chinese-Vicuna 的对话模板:
prompt_pre = (
"The following is a conversation between an AI assistant called Assistant and a human user called User. "
"The assistant is intelligent, knowledgeable and polite to answer questions of user.\n\n"
prompt_history = "User:{input}\n\nAssistant:{output}\n\n"
prompt_post = "User:{input}\n\nAssistant:"
这里面有三个变量,第一个就是个固定模板,跟开场白一样的东西,第二个是用来表征历史对话信息的,第三个就是最后一轮对话的输入的问题的一个模板,因为涉及到训练和推理这两个阶段,训练的时候,会把最后一轮对话的回答补上去,但是推理的时候就不会。
接下来就是具体的操作了,let's 开干。
def generate_prompt(data_point, stage='train'):
user_prompt = prompt_pre # 固定开场白
# 这里面的字段是conversions,而不是input,因为上面的例子的字段是conversations
conversations = data_point['conversations']
# 获取多轮对话的轮数
assert len(conversations) % 2 == 0, f"{data_point} not compeleted finised the conversation"
num_turns = len(conversations) // 2
for i in range(num_turns - 1): # 最后一轮对话单独处理,此处不处理
assert conversations[2*i]['from'] == "human"
assert conversations[2*i+1]['from'] == "assistant"
human = conversations[2*i]['value']
assistant = conversations[2*i+1]['value']
user_prompt += prompt_history.format_map({'input': human, 'output': assistant})
# 添加最后一轮对话的输入部分
user_prompt += prompt_post.format_map({'input': conversations[2*num_turns-2]['value']})
# 根据是训练还是推理,用不同的方式来处理最后一轮对话的回答部分
if stage == 'train':
user_prompt += conversations[2*num_turns-1]['value']
return {"prompt": user_prompt}
训练的时候镶嵌到模板中的输入文本是这样的:
fn_kwargs = {"stage": 'train'}
data_train = data.map(generate_prompt, fn_kwargs=fn_kwargs)
print(data_train[0]['prompt'])
The following is a conversation between an AI assistant called Assistant and a human user called User. The assistant is intelligent, knowledgeable and polite to answer questions of user.
User:给定一段文本和关键词列表,删除文本中包含所有给定关键词的子字符串。
文本:"这是一个测试句子,目的是看看模型是否可以正确地从这个句子中删除关键词。"\n关键词列表:[‘测试’,‘模型’]
Assistant:删除包含所有给定关键词的子字符串后,文本变为:"这是一个句子,目的是看看是否可以正确地从这个句子中删除关键词。"
User:好的。现在请你将这个文本中的所有的逗号都替换成空格。
Assistant:好的,请稍等一下,现在我会将文本中的所有逗号替换为空格。处理后文本为:"这是一个句子 目的是看看是否可以正确地从这个句子中删除关键词。"。处理结果如何?
验证或者测试的时候,输入文本是这样的:
fn_kwargs = {"stage": 'val'}
data_val = data.map(generate_prompt, fn_kwargs=fn_kwargs)
print(data_val[0]['prompt'])
The following is a conversation between an AI assistant called Assistant and a human user called User. The assistant is intelligent, knowledgeable and polite to answer questions of user.
User:给定一段文本和关键词列表,删除文本中包含所有给定关键词的子字符串。
文本:"这是一个测试句子,目的是看看模型是否可以正确地从这个句子中删除关键词。"\n关键词列表:[‘测试’,‘模型’]
Assistant:删除包含所有给定关键词的子字符串后,文本变为:"这是一个句子,目的是看看是否可以正确地从这个句子中删除关键词。"
User:好的。现在请你将这个文本中的所有的逗号都替换成空格。
Assistant:
所以我们看到,区别就在于最后一轮的Assistant的输出有或者没有,也就是说在模型验证/测试的时候,需要模型预测的就是最后一轮对话的回答。之前的历史对话,都将作为上下文信息,送入到模型中去,来更好的输出当前问题的回复。
推理的时候,怎么维护历史信息:
那么在推理的时候,历史信息是如何处理的呢?这部分的代码位于
chat.py
,模型训练好了之后,用来进行对话推理的时候,历史信息的处理和训练的时候是类似的,维护一个列表叫做
history
。操作起来的时候,就是将历史信息分别按照
User
和
Assistant
的角色拼起来,再镶嵌到模板里面,第一轮对话的时候,这个
history
是个空列表。还是用我们的样例数据来演示一下,那第一轮对话的时候,效果可能就是下面这个样子的:
User: 给定一段文本和关键词列表,删除文本中包含所有给定关键词的子字符串。
文本:"这是一个测试句子,目的是看看模型是否可以正确地从这个句子中删除关键词。"\n关键词列表:[‘测试’,‘模型’]