首发于 LLM微调记
Chinese-Vicuna中的对话数据处理方式

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关键词列表:[‘测试’,‘模型’]