【Parallel-R1代码实现】sftv2

数据构建方案:
code数据改为并行。仿照multiunvierse。

第一步:找到可能的困难点
第二步:进行并行生成
第三步:仿照ReTool,对数据进行mock
第四部:生成一条数据

方案备选:

  1. 从零开始构建数据,加载大的LLM做状态机转换,进行推理,构造sft的数据回答(没有办法加载闭源模型,数据效果有影响)
  2. 从零开始构建数据,使用生成推理链,然后做并行改写+code改写的工作(工程量过大)
  3. 使用zero-shot的prompt,直接用LLM进行生成(这一版方案似乎效果不好)(方案太简单,数据质量不高)
  4. 使用开源的并行数据集,从他们做自然语言改为code的操作(自然语言改code本身是一件很难的事情,又一个专门的领域,因此需要后期大量的后处理工作)
  5. 使用开源的code数据集,对他们做提取并行思维的操作(做code的改写,似乎比做code的生成更加容易)

prompt模版:

数据质量筛选:

在采用 “从 Code 数据生成 Parallel”(即方案2)的策略后,数据筛选是决定 SFT 最终效果的生死线。因为是 LLM 生成的数据,必须假设其中包含幻觉、错误和低质量内容。

鉴于你的数据包含 结构化标签(XML)可执行代码(Python),你可以构建一个非常强大且自动化的 “漏斗式筛选管线”

我将筛选策略分为三个层级:硬性过滤(Hard Filters)逻辑过滤(Execution Filters)语义过滤(Semantic Filters)


🟢 第一层:硬性过滤(Format & Syntax)

目标:快速剔除格式崩坏、无法解析的垃圾数据。成本极低。

  1. 标签完整性检查(XML Parsing)

    • 检查是否包含完整的 <Parallel>, <Path>, <code>, <Summary> 等标签。
    • 检查标签嵌套是否正确(例如 <code> 是否在 <Path> 内部)。
    • 操作:使用正则表达式或简单的 XML/HTML Parser。如果 Parse 失败,直接丢弃。
  2. 代码语法检查(AST Parsing)

    • 不要等到运行时才发现语法错误。
    • 操作:使用 Python 内置的 ast.parse() 尝试解析提取出的代码块。
    • 如果抛出 SyntaxError,说明生成的代码连 Python 语法都不对,直接丢弃。
  3. 空内容/长度过滤

    • 检查 <Path> 内是否为空,或者只有寥寥几个字符。
    • 操作:设定 Token 数量阈值(例如每个 Path 至少 20 tokens),防止模型“偷懒”。

🟡 第二层:逻辑与执行过滤(Execution & Consistency)

目标:利用代码的可执行性,验证逻辑正确性。这是 Code-SFT 最大的优势。

这是最关键的一步。你需要构建一个简单的 Python Sandbox(沙箱) 来批量运行代码。

  1. 可执行性验证(Runtime Check)

    • 操作:在沙箱中运行提取出的代码。设置超时时间(例如 5秒),防止死循环。
    • 筛选标准:任何抛出 RuntimeError, NameError, Timeout 的样本,丢弃。
  2. 结果一致性验证(Self-Consistency)

    • 这是 Parallel Thinking 的核心验证逻辑。
    • 逻辑
      • 运行 Path 1 Code -> 得到 Result A
      • 运行 Path 2 Code -> 得到 Result B
      • 提取 <Summary> 中的文本答案 -> 得到 Result C
      • 获取原始数据的 Ground Truth -> Result GT
    • 筛选标准:必须满足 Result A ≈ Result B ≈ Result GT
    • 注意:对于浮点数,使用 math.isclose 进行容差比较;对于字符串/分数,进行标准化处理。
  3. 反“硬编码”检查(Anti-Hardcoding)

    • 有些模型会“作弊”,直接 print(42) 而不进行计算。
    • 操作:检查代码中是否存在计算逻辑。
    • 简单版:检查代码行数 > 3。
    • 进阶版:检查 AST 中是否有运算节点(ast.Add, ast.Mult 等),而不仅仅是 ast.Constantprint

🔴 第三层:语义与多样性过滤(Diversity & Quality)

目标:确保 Parallel 是真的“不同思路”,而不是简单的换变量名。

如果 Path 2 只是把 Path 1 的变量 x 改成了 y,或者加了几个注释,这种数据对训练“多角度思考”没有意义,甚至有害(会导致模型坍缩)。

  1. 代码结构差异性(Code Structure Similarity)

    • 问题:如何判断两段代码是不同的算法?
    • 方法 A(简单):计算两段代码文本的 Jaccard 相似度Levenshtein 编辑距离。如果相似度 > 0.8,说明改动太小,丢弃。
    • 方法 B(推荐)MinHash + AST 特征
      • 对两段代码进行 AST 解析,提取节点类型序列(忽略变量名)。
      • 如果节点序列完全一致,说明逻辑结构没变,丢弃。
  2. 解法多样性分类(Method Classification - Optional)

    • 如果你追求极致质量,可以使用一个小模型(如 Qwen-7B)作为 Reward Model
    • Prompt:"这两段代码是否使用了不同的数学方法?(例如代数法 vs 暴力枚举,sympy vs numpy)。回答 Yes/No。"
    • 保留回答 "Yes" 的样本。
  3. 思维链质量(CoT Quality)

    • 检查代码周围的自然语言注释/解释是否通顺。
    • 启发式规则:如果 <Path> 中全是代码没有自然语言,或者全是自然语言没有代码,可视情况降权。

⚙️ 工程实现建议

你可以写一个 Python 脚本 filter_pipeline.py,流程如下:

import ast
import multiprocessing
from concurrent.futures import ProcessPoolExecutor

def check_syntax(code_str):
    try:
        ast.parse(code_str)
        return True
    except SyntaxError:
        return False

def execute_and_get_result(code_str):
    # ⚠️ 实际工程中请使用 docker 或 python box 等安全沙箱
    # 这里仅做示意
    try:
        # 重定向 stdout 捕获 print 的结果
        # exec(code_str) ...
        return result
    except Exception:
        return None

def calculate_diversity(code1, code2):
    # 简单的文本相似度过滤
    # 实际可用 AST 比较
    set1 = set(code1.split())
    set2 = set(code2.split())
    intersection = len(set1 & set2)
    union = len(set1 | set2)
    jaccard = intersection / union
    return jaccard # 越小越好

def process_sample(sample):
    # 1. Format Check
    if not has_tags(sample): return None
    
    code1 = extract_code(sample, path=1)
    code2 = extract_code(sample, path=2)
    
    # 2. Syntax Check
    if not (check_syntax(code1) and check_syntax(code2)):
        return None
        
    # 3. Execution Check
    res1 = execute_and_get_result(code1)
    res2 = execute_and_get_result(code2)
    gt = sample['ground_truth']
    
    if res1 != gt or res2 != gt: # 允许一定容差
        return None
        
    # 4. Diversity Check
    similarity = calculate_diversity(code1, code2)
    if similarity > 0.7: # 如果两段代码 70% 相似,视为重复
        return None
        
    return sample # 保留样本

# 并行处理清洗
# dataset = ...
# with ProcessPoolExecutor() as executor:
#     clean_data = list(executor.map(process_sample, dataset))

📊 预期通过率(Funnel Metrics)

根据经验,使用 GPT-4o 或 Claude-3.5 生成并经过上述筛选,预期的留存率如下:

  • 初始生成:100%
  • 格式/语法过滤后:~95% (LLM 写代码语法通常没问题)
  • 执行与答案正确性过滤后:~60% - 70% (Path 2 容易写错或算错)
  • 多样性过滤后:~40% - 50% (剔除微小改动)

结论:如果你需要 5k 条高质量数据,建议生成 10k - 12k 条原始数据。这个损耗是完全值得的,因为留下的全是逻辑自洽、代码可运行、且具备多样性思维的黄金数据。

posted @ 2025-12-25 15:38  Brain404  阅读(1)  评论(0)    收藏  举报