Datawhale AI夏令营 科大讯飞AI大赛(大模型技术) Task2 心得

135 阅读11分钟

本次任务重点关注构建微调数据集(QA对)。

image.png

就是图中的阶段一。

首先,为什么任务主要关注如何构建微调数据集(QA对)呢?

因为平台限制:  本次赛事规定选手须在讯飞星辰MaaS平台进行任务开发。需要注意的是,该平台目前主要支持模型的微调、训练和部署,不直接提供应用开发或Agent功能嵌入。 所以我们能够提升模型性能的手段很有限,最有效的手段就是构建高质量的微调数据集(QA对)。

补充介绍:
本次构建的数据集属于Supervised Tine-Tuning(SFT,监督微调;是目前最主流的微调方法)数据集。该数据集的格式主要有:Alpaca、Sharegpt。本次用的是Alpaca格式。
Alpaca字段包含“instruction”和“output”、“input”。其中前面两项必须包含;Alpaca的格式:
{"instruction": "Z152次列车的终到站是哪里?", "output": "Z152次列车的终到站是北京西。"}
此外,如果SFT数据集是QA对,那么对应关系是:
{'instruction': 'Question内容', 'output': 'Answer内容'}。

其次,如何用给定的表格类型(.csv)的文件构建出Alpaca格式的SFT数据集的思路是怎么样的?

1、错误思路:直接利用大模型来生成问题和答案

例如,在Prompt里给定一行表格数据而不提供问题列表,让大模型直接基于我们给定的输出模板给出 “问题-答案”的json信息。

这种方法看似高效,但经过实践和深入讨论,我们发现这种  “让模型既生成问题又生成答案”  的方式存在一个核心问题:无法保证生成的问题和答案的准确性。

如果模型生成的问题本身就是错误的,或者答案与表格数据不符,那么用这样的数据去微调我们的目标模型(学生模型),会导致学生模型学到错误的知识,最终影响其在真实测试集上的表现。

2、正确思路:采用模型蒸馏(Model Distillation)的思想。

  1. 编程生成问题(确保问题正确性) :我们不再让大模型凭空生成问题,而是通过编程(例如使用Pandas结合模板)来构造基于表格数据的确定性问题。例如,针对每一行数据,我们可以生成“X车次的终到站是哪里?”、“Y车次应该从哪个检票口检票?”等单字段查询问题。对于更复杂的问题类型,也可以设计相应的编程逻辑来生成。这样,我们能确保问题的正确性和相关性。
  2. 使用更强大的大模型(教师模型)生成答案(确保答案质量) :对于这些编程生成的问题,我们再将其输入给一个能力更强、更稳定的教师模型(例如一个大型的通用LLM,如Qwen3-8B),让教师模型根据给定的表格数据和问题来生成答案。由于教师模型能力更强,它生成答案的准确性会更高。
  3. 构建高质量SFT数据集:将编程生成的“正确问题”和教师模型生成的“高质量答案”配对,形成{"instruction": "问题", "output": "答案"}格式的SFT(Supervised Fine-Tuning)数据集。
  4. 微调目标模型(学生模型) :最后,使用这个高质量的SFT数据集来微调我们的目标模型(学生模型)。通过这种方式,我们让学生模型学习如何从结构化数据中提取信息并生成准确的答案,从而在讯飞星辰MaaS平台上达到更好的性能。

3、更好的方法。

但如果仅限于此,分数会有上限和瓶颈,且不适合扩展。

因此更好的方法可能是构造一个表格问答Agent,这个Agent拥有以下7个工具(tool),可用于构造正确的回答。

图片

然后,本次比赛我们方案是什么?要掌握哪些知识?有了思路之后如何实操?

类别内容
baseline 方案概述1. 读取样例赛题样例数据
2. 基于样例数据,人工构建问题清单
3. 给定样例数据和问题清单,调用大模型生成答案
4. 整理成用于微调大模型的指令数据集
5. 使用讯飞星辰平台微调模型,基座模型选择 qwen3-8B 等模型(可自行选择,小模型可能更快)
6. 部署模型,并提交 resourceID到比赛平台,等待评分结果。
baseline 必备知识点清单1. 大模型 prompt 提示词
2. 大模型的 http 调用
3. pandas 遍历整理数据
4. 讯飞星辰微调模型。
实操本 Baseline 方案的核心思路是通过模型蒸馏的方法,将一个强大的教师模型(如 Qwen3-8B)在特定任务上的知识,迁移到我们最终需要微调的学生模型上
具体步骤如下:
表格数据文本化:将结构化的列车时刻表数据(每一行代表一趟列车的信息)转换为易于大模型理解的文本格式。
编程生成问题:针对每一行列车数据,我们手动设计问题模板,并通过编程方式(例如 Python 脚本)批量生成问题
教师模型生成答案:将文本化的列车信息和编程生成的问题作为 Prompt 输入给一个能力更强的教师模型。教师模型根据其强大的理解和推理能力,为每个问题生成对应的答案.
构建 SFT 数据集:将编程生成的问题和教师模型生成的答案配对形成{"instruction": "问题", "output": "答案"}的 JSON 格式数据集。这个数据集就是用于微调学生模型的 SFT 数据。
学生模型 LoRA 微调:将构建好的 SFT 数据集上传到讯飞星辰 MaaS 平台,并使用 LoRA 技术对选定的基础模型(学生模型)进行微调。微调后的模型将能够根据用户提出的问题,从内部学习到的表格知识中给出准确的回答。

image.png

baseline核心逻辑

最后,对datawhale官方提供的代码进行解析:

第一段:

import pandas as pd
import requests
import re
import json
from tqdm import tqdm
# 读取数据
data = pd.read_excel('data/info_table.xlsx')
data = data.fillna('无数据')
data
代码解析:

先导入了所需的库;然后从举办方提供的原始表格中提取数据存放到DataFrame格式的变量data中(此时缺失值以 NaN 或 None 形式存在),再通过fillna()函数,将data中所有缺失的项(NaNNone)替换成指定的值(可以自定义,这里是字符串‘无数据’),最后在交互环境中显示处理后的 data,检查是否有与预期结果不同的地方。

第二段:

由于第二段代码较长较多,这里需要分成几个模块来解析。

1、API调用模块(用于生成数据集中的instruction,output)
def call_llm(content: str):
    """
    调用大模型
    
    Args:
        content: 模型对话文本
    
    Returns:
        list: 问答对列表
    """
    # 调用大模型(硅基流动免费模型,推荐学习者自己申请)
    url = "https://api.siliconflow.cn/v1/chat/completions"
    payload = {
        "model": "Qwen/Qwen3-8B",
        "messages": [
            {
                "role": "user",
                "content": content  # 最终提示词,"/no_think"是关闭了qwen3的思考
            }
        ]
    }
    headers = {
        "Authorization": "Bearer 填入token",
        "Content-Type": "application/json"
    }
    resp = requests.request("POST", url, json=payload, headers=headers).json()
    # 使用正则提取大模型返回的json
    content = resp['choices'][0]['message']['content'].split('</think>')[-1]
    pattern = re.compile(r'^```json\s*([\s\S]*?)```$', re.IGNORECASE)  # 匹配 ```json 开头和 ``` 结尾之间的内容(忽略大小写)
    match = pattern.match(content.strip())  # 去除首尾空白后匹配
    if match:
        json_str = match.group(1).strip()  # 提取JSON字符串并去除首尾空白
        data = json.loads(json_str)
        return data
    else:
        return content
代码解析:

这一段代码主要实现的功能是是将prompt输入进大模型,得到输出并且对大模型的输出进行处理。
拆开来看:

首先下面的这一段代码:
    # 调用大模型(硅基流动免费模型,推荐学习者自己申请)
    url = "https://api.siliconflow.cn/v1/chat/completions"
    payload = {
        "model": "Qwen/Qwen3-8B",
        "messages": [
            {
                "role": "user",
                "content": content  # 最终提示词,"/no_think"是关闭了qwen3的思考
            }
        ]
    }
    headers = {
        "Authorization": "Bearer 填入token",
        "Content-Type": "application/json"
    }

实现的是从硅基流动平台用API调用指定的模型得到输出。
其中:message部分的content是从外界传入的字符串格式的数据,作为向大模型输入的文本;content-Type向大模型说明了输入的文本是json格式,避免模型识别出错。

而且,硅基流动官方提供的API文档包含调用模型的代码示例,可以在此用作参考,如下:

import requests

url = "https://api.siliconflow.cn/v1/chat/completions"

payload = {
    "model": "Qwen/QwQ-32B",
    "messages": [
        {
            "role": "user",
            "content": "What opportunities and challenges will the Chinese large model industry face in 2025?"
        }
    ]
}
headers = {
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())

可以看出,这里使用的模型调用代码是在硅基流动提供的代码的基础上稍作修改得到的。

然后看对模型返回的输出进行处理的代码:
    resp = requests.request("POST", url, json=payload, headers=headers).json()
    # 使用正则提取大模型返回的json
    content = resp['choices'][0]['message']['content'].split('</think>')[-1]
    pattern = re.compile(r'^```json\s*([\s\S]*?)```$', re.IGNORECASE)  # 匹配 ```json 开头和 ``` 结尾之间的内容(忽略大小写)
    match = pattern.match(content.strip())  # 去除首尾空白后匹配
    if match:
        json_str = match.group(1).strip()  # 提取JSON字符串并去除首尾空白
        data = json.loads(json_str)
        return data
    else:
        return content

这一段代码的作用是:尝试从大模型返回的文本中提取 JSON 格式的数据,如果成功则解析为结构化数据,否则返回原始文本

最后,这一整段代码的输入是文本化的列车信息和编程生成的问题合成的promptcontent,经过处理后的输出是问题以及每个问题生成对应的答案data

2、问题列表生成模块(用于生成数据集中的instruction)
def create_question_list(row: dict):
    """
    根据一行数创建问题列表
    
    Args:
        row: 一行数据的字典形式
    
    Returns:
        list: 问题列表
    """
    question_list = []
    # ----------- 添加问题列表数据 begin ----------- #
    # 检票口
    question_list.append(f'{row["车次"]}号车次应该从哪个检票口检票?')
    # 站台
    question_list.append(f'{row["车次"]}号车次应该从哪个站台上车?')
    # 目的地
    question_list.append(f'{row["车次"]}次列车的终到站是哪里?')
    
    # ----------- 添加问题列表数据 end ----------- #
    return question_list
代码解析:

本段代码接受的是某一行的文本信息row,输出的是对应的问题列表question_list

3、生成输出模块(输出包括instruction,output):
# 简单问题的prompt
prompt = '''你是列车的乘务员,请你基于给定的列车班次信息回答用户的问题。
# 列车班次信息
{}

# 用户问题列表
{}

'''
output_format = '''# 输出格式
按json格式输出,且只需要输出一个json即可
```json
[{
    "q": "用户问题",
    "a": "问题答案"
},
...
]
'''

train_data_list = []
error_data_list = []
# 提取列
cols = data.columns
# 遍历数据(baseline先10条数据)
i = 1
for idx, row in tqdm(data.iterrows(), desc='遍历生成答案', total=len(data)):
    try:
        # 组装数据
        row = dict(row)
        row['到点'] = str(row['到点'])
        row['开点'] = str(row['开点'])
        # 创建问题对
        question_list = create_question_list(row)
        # 大模型生成答案
        llm_result = call_llm(prompt.format(row, question_list) + output_format)
        # 总结结果
        train_data_list += llm_result
    except:
        error_data_list.append(row)
        continue
代码解析:
首先,这一部分给出了prompt跟大模型的output的格式:
# 简单问题的prompt
prompt = '''你是列车的乘务员,请你基于给定的列车班次信息回答用户的问题。
# 列车班次信息
{}

# 用户问题列表
{}

'''
output_format = '''# 输出格式
按json格式输出,且只需要输出一个json即可
```json
[{
    "q": "用户问题",
    "a": "问题答案"
},
...
]
'''
然后,这一部分的代码完成了生成数据集前的准备工作:
train_data_list = []
error_data_list = []
# 提取列
cols = data.columns
# 遍历数据(baseline先10条数据)

这里生成了两个分别用于存放成功、失败数据的列表,并且得到了处理后的表格数据data的所有列名,存放在cols中。

最后,这一部分开始正式生成数据集:
for idx, row in tqdm(data.iterrows(), desc='遍历生成答案', total=len(data)):
    try:
        # 组装数据
        row = dict(row)
        row['到点'] = str(row['到点'])
        row['开点'] = str(row['开点'])
        # 创建问题对
        question_list = create_question_list(row)
        # 大模型生成答案
        llm_result = call_llm(prompt.format(row, question_list) + output_format)
        # 总结结果
        train_data_list += llm_result
    except:
        error_data_list.append(row)
        continue

这里用tqdm函数生成了一个进度条,其中监视对象是当前执行的行,描述文字是'遍历生成答案',总长度是data的行数。
然后,对于data中的每一行(row),先将row转化为字典形式并且将时间字段转化为字符串形式避免出错;然后调用先前的问题列表生成模块,将row输入进去,得到question_list
再按照之前规定的prompt的格式,将row,question_list输入进prompt.format()生成prompt。

之前规定的prompt格式:

prompt = '''你是列车的乘务员,请你基于给定的列车班次信息回答用户的问题。
# 列车班次信息
{}
# 用户问题列表
{}
'''

在此处,是将row填进前面的列表,将question_list填进后面的列表。

最后,将生成的prompt当作content输入进API调用模块,并且规定模块输出的格式为之前规定的output的格式,得到了输出llm_result,将llm_result加入train_data_list。 在这一段代码中采用了try/except的格式,如果出了错,就停止操作,并且将出了错的row加入error_data_list,然后执行下一个row

第三段:

# 转换训练集
data_list = []
for data in tqdm(train_data_list, total=len(train_data_list)):
    if isinstance(data, str):
        continue
    data_list.append({'instruction': data['q'], 'output': data['a']})

json.dump(data_list, open('single_row.json', 'w', encoding='utf-8'), ensure_ascii=False)
data_list[:2]
代码解析:

这一段代码实现的是将train_data_list转化为json格式的data_list。具体而言:
先建立一个空的列表data_list,然后遍历train_data_list的每一个元素(迭代变量(iteration variable)命名为data)并且用tqdm函数画出进度条。如果data是字符串格式,则跳过;否则,先创建一个新的字典,key分别是instruction,output;然后提取出data中的q,a,分别放入这个字典的instruction,output的value中,然后将该字典加入到data_list中。

之前规定的大模型的output格式:

output_format = '''# 输出格式
按json格式输出,且只需要输出一个json即可
```json
[{
    "q": "用户问题",
    "a": "问题答案"
},
...
]

这里将"q": "用户问题"放入instruction,"a": "问题答案"放入output中。

最后将data_list写入到single_row.json文件中,并且展示索引为0,1(不包括2)的元素。