SFT洗数据,有多少细节?

 作者:ybq

链接:https://zhuanlan.zhihu.com/p/6497090767

最近在清洗 sft 的数据,不得不说这工作是真磨人啊,细节多到让人抓狂。可能,这就是为什么从业者们都懂得 llm 的方法论,却依然没几个团队能造出好数据训出好模型吧。

借此机会,举个例子给大家聊聊 sft 数据能有多少繁琐的细节?也算是吐槽和分享自己的日常了。


先说一下为什么都 2024 年底了,还需要清洗 sft 数据,这不应该是去年就已经完成的工作吗?因为数据会过时,去年的高质量数据不代表今年还是高质量数据。

例如,user:你会选择猫作为宠物还是狗呢?

  • 去年的 gpt4:作为大语言模型,我无法养宠物,吧啦吧啦。

  • 今年的 gpt4:猫吧啦吧啦,狗吧啦吧啦,虽然我没有实体,但我推荐你吧啦吧啦。

显然,在去年,过度的安全是在大家的认可和接受范围之内的,但今年只会让用户觉着无趣,因此这条数据需要清洗,需要拿 gpt4 重新标注再加人工重新 review —— 总之,只要 OpenAI 还在更新模型,还在努力的为我们提供优质的数据生产器,我们就没理由不清洗数据。

题外话说完了,下面我以一个巨简单的 case 作为切入点,来谈谈 sft 的数据细节有多繁琐。这个 case 就是"以 json 格式输出"。

json 大家都很熟悉吧,我就不解释了。在我的认知里,json 就是能被 json.loads() 调用成功且不报错的字符串,想让模型做到以 json 格式输出也很简单,让 answer 过两行代码就搞定了。

data = json.loads(line)
json_str = json.dumps(data, indent=2, ensure_ascii=False)

大家肯定不觉着 json 工作有啥好展开说的,但有没有一种可能,json 格式还有变种:

  • makrdown 格式,需要在字符串前后加 ``json 和 ``` ,一般默认都要带上;

  • intent = 4 呢 还是 intent = 2 呢还是 intent = 0 呢,prompt 说了还好办,prompt 要是没说呢,难道模型随机选一个数字来输出吗?我们需要统一 answer 中所有的 json 默认 intent;

  • 有一种格式叫 jsonl,或者说是把 json 结果输出到一行中;

  • 有一种需求叫不准进行 markdown 渲染;

  • 有两种 prompt 分别叫:“先分析,再输出 json” 和 “除了 json,其他内容一律不准输出”;

  • ……

emm,目前为止,大家可能还是不觉着 json 格式有多烦人,无非就是两个工作嘛:

  1. 选一个默认的 json 格式:带不带 markdown,intent 设置成几,是否输出在一行。然后把 sft 中所有涉及到的 json 数据全部清洗成这种格式;

  2. 补充那些对 json 格式有明确要求的 prompt,让模型不丢失灵活切换 json 格式的能力。

那我就继续提问:如果 prompt 里有 few_shot,但这个 few_shot 它是一个错误的 json,这时候是让模型模仿这个错误的 json 还是让模型严格按照 json 格式输出呢?

我随手测了下市面上体量最大的五家公司,好家伙给我五个答案 (do_sample = True):

  1. 4o: markdown + 标准 json + intent 2

  2. 豆包:标准 json + intent 0

  3. kimi:markdown + few_shot_json + jsonl

  4. qwen:few_shot_json + jsonl

  5. 文心:markdown + 标准 json + intent 4

你问我谁对,我只能说不知道。都是在尝试去 follow 格式,只不过有的模型是在 follow few_shot 的格式,有的模型是在 follow json 的格式。大家都对,全赢!我只是想用这个 case 强调,在训模型的时候,我们可以选用任何一种 json 风格,但是一定要统一。不要让模型随机出 json 风格,那不仅不利于模型训练,也会给用户的批量请求带来困扰。至于用哪种风格,让 PM 去调研一下客户需求即可。

大家也不要吐槽我的这个 prompt,觉着我是在没事儿找事儿,从业者应该都知道,这就是用户们最喜欢用的 prompt,你难道指望用户每次请求的时候,先去检验一下自己的示例是不是一个标准化 json 吗?单引号,中文引号,中文逗号,括号不匹配,……,各种错误比比皆是。

图片

GPT4o

图片

kimi

图片

qwen 2.5

图片

文心 3.5

图片

豆包

到了这一步,你是不是有点认同我“以 json 格式输出”不是一个简单的工作了。但别着急,我还有一个问题要问:在数值任务上,针对数字,json 格式是使用 float / int 类型呢,还是 str 类型呢?

大家肯定都想选前者,我认同,但我要提醒一下,前者会影响模型的准确率,因为数值任务有个要素叫“单位”。

图片

GPT4o

这里必须质问 ChatGPT 老师,我的单位“百万$” 去哪里了?虽然没有在 prompt 中明确指出要带单位,但这不应该是默认的常识吗?我们不能指望用户写出高质量的 prompt 呀。

当然,我并不是要为了拷打 ChatGPT 老师,我是想表达,在 next_token_prediction 的过程中,当模型的 “net_profit”: 的下一个 token 生成了数字 5,而不是双引号 " 的时候,神仙也救不回来这个 response 了。因为一旦没有输出引号,就代表模型选择了输出数值类型,那就必然没办法带单位了。这也就是我说的让模型习惯用 float / int 类型输出数值,可能会影响效果的原因。

不过也有补救办法,构造 sft 数据提醒模型每次额外输出一个 unit 字段。

{
  "2018": {
    "net_profit": 5678,
    "unit": "百万$"
  }
}

至此,“以 json 格式输出”的大多数细节,我已经强调完毕了。所有的细节总结下来,还是那句话:用什么格式不重要,只用一种格式很重要!

无独有偶,当我被 json 格式输出搞得焦头烂额的时候, https://www.zhihu.com/people/bf1764dccc55b8f831b89c9103f41564 同学也没好到哪里去。

最近深入研究数学的他,cot 数据不好造他不找我讨论,数据集翻译成中文后专有名词失真他不找我讨论,o1 造数据太贵他也不吐槽,但是他却因为一个问题来找我讨论:“你觉着模型输出的美元符号该不该带转义符号呢?”

  • 数学数据有大量的 latex 表达式,都需要用 进行包裹起来,也就是:公式。那问题来了,美元 作为另外的含义,是不是得额外加个反斜杠呢?

  • 当 表示美元的时候,它作为单位符号是不是不应该被公式$ 包裹起来(很多数据集包起来了)?

  • 数值 1234 是不是应该全部写成 1,234?

  • 如果全都要用 1,234 的表达方式,怎么实现批量替换呢?毕竟 公式 中的数字可不能给它加上这个逗号

  • ……

这真是:你以为 llm 算法工程师在各种算法中挥斥方裘,实际上在和标点符号斗智斗勇。


最后强调一句,这仅仅是一个以 json 格式输出啊,sft 最基本的指令任务,至于那些复杂指令的数据如何构造、有多繁琐,只能靠大家自己去意会了。很多 llm 的工作不是想象中那么简单的,懂得怎么做仅仅是刚入门,只有实践了才知道有多少细节需要兼顾,有多少复杂的场景需要处理。我举的是洗符号、洗格式、洗 prompt 的例子,但还有更多的算法实践是举例举不出来的。

<think>我们正在讨论SFT(监督微调)的实现方法。根据用户要求,我们需要提供SFT微调的伪代码或实现步骤。结合之前的引用,虽然引用中提到了PEFT(参数高效微调)和TTRL(测试时强化学习),但用户明确要求了解SFT微调。SFT(SupervisedFine-Tuning)是微调预训练模型的一种基本方法,使用有标签的数据以监督学习的方式调整模型参数。下面将分步骤描述SFT微调的流程,并给出伪代码。###SFT微调步骤1.**准备数据集**:需要准备一个监督数据集,包含输入和对应的期望输出(标签)。例如,对于文本生成任务,输入是一段文本,输出是目标文本。2.**加载预训练模型**:从预训练模型(如BERT、GPT等)初始化模型权重。3.**定义微调任务**:根据任务类型(如分类、生成等)在预训练模型基础上添加任务特定的层(如分类头),或者直接使用预训练模型的结构。4.**设置训练参数**:选择优化器、学习率、批大小等超参数。5.**训练循环**:在训练数据上迭代,计算损失并更新模型参数。6.**评估与保存**:在验证集上评估模型性能,保存最佳模型。###SFT微调伪代码以下是一个通用的SFT微调伪代码示例,以文本分类任务为例:```plaintext//步骤1:准备数据集LOADdatasetFROM"data_path"SPLITdatasetINTOtrain_set,val_set//步骤2:加载预训练模型model←LOAD_PRETRAINED_MODEL("bert-base-uncased")ADDclassification_headONmodel//添加分类层//步骤3:设置训练参数optimizer←AdamW(model.parameters(),lr=5e-5)loss_fn←CrossEntropyLoss()epochs←3batch_size←32//步骤4:训练循环FORepochIN1..epochsDOFOREACHbatchINtrain_set(WITHbatch_size)DO//前向传播inputs,labels←batchoutputs←model(inputs)//[batch_size,num_classes]//计算损失loss←loss_fn(outputs,labels)//反向传播optimizer.zero_grad()loss.backward()optimizer.step()ENDFOR//验证集评估val_loss,val_acc←EVALUATE(model,val_set,loss_fn)PRINT"Epoch",epoch,"ValLoss:",val_loss,"ValAcc:",val_accENDFOR//步骤5:保存模型SAVEmodelTO"sft_model.bin"```###关键点说明-**数据集格式**:对于不同任务,输入和标签的形式不同。在文本分类中,输入是文本序列,标签是类别索引;在生成任务中,输入是源文本,标签是目标文本。-**模型调整**:对于分类任务,通常在预训练模型顶部添加一个线性层作为分类头。对于生成任务(如使用GPT),则直接使用预训练的语言模型结构,在微调时调整整个模型的参数。-**损失函数**:分类任务常用交叉熵损失;生成任务常用交叉熵损失(对每个token计算)[^2]。-**学习率**:通常使用较小的学习率(如5e-5)以避免破坏预训练模型学到的知识。###SFT与提示微调(Prompt-Tuning)的区别用户引用[1]提到Prompt-Tuning使用softprompt,而SFT微调会更新整个模型或大部分参数。两者的主要区别在于:-**SFT**:更新模型的所有参数(或添加任务特定层并更新这些层)。-**Prompt-Tuning**:固定预训练模型参数,只优化少量可学习的提示向量(softprompt)[^1]。###数学表示在微调过程中,模型参数更新公式如下:$$\theta_{t}=\theta_{t-1}-\eta\nabla_{\theta}\mathcal{L}(f_{\theta}(x),y)$$其中:-$\theta$是模型参数-$\eta$是学习率-$\mathcal{L}$是损失函数-$(x,y)$是训练样本###相关问题§§相关问题§§1.SFT微调需要多少数据才能达到较好效果?2.如何避免SFT微调中的过拟合问题?3.SFT与参数高效微调(PEFT)方法(如LoRA)有何区别?4.在生成任务(如对话系统)中如何实现SFT微调?以上内容提供了SFT微调的基本步骤和伪代码,并强调了其与提示微调的区别。用户可以根据具体任务调整实现细节。</think>### SFT(监督微调)伪代码与实现步骤 #### 一、SFT核心流程 ```plaintext FUNCTION supervised_fine_tuning(pretrained_model, dataset): // 步骤1:准备监督数据集[^3] sft_dataset ← LOAD_DATASET(dataset) // 格式: [input, target_output] // 步骤2:初始化模型 model ← LOAD_PRETRAINED(pretrained_model) // 如LLaMA, GPT tokenizer ← GET_TOKENIZER(pretrained_model) // 步骤3:配置训练参数 optimizer ← AdamW(lr=2e-5, weight_decay=0.01) loss_fn ← CrossEntropyLoss() // 步骤4:微调循环 FOR epoch IN 1..num_epochs: FOR batch IN sft_dataset: // 编码输入输出 inputs ← tokenizer(batch.input) labels ← tokenizer(batch.target_output) // 前向传播 outputs ← model(**inputs) loss ← loss_fn(outputs.logits, labels) // 反向传播 loss.backward() optimizer.step() optimizer.zero_grad() // 验证集评估 eval_loss ← EVALUATE(model, validation_set) SAVE_CHECKPOINT(model, f"epoch_{epoch}") // 步骤5:保存最终模型 RETURN model.save("sft_final_model") END FUNCTION ``` #### 二、关键实现细节 1. **数据格式要求** ```python # 示例数据样本 { "input": "解释牛顿第一定律", "target_output": "牛顿第一定律指出:物体在不受外力作用时..." } ``` 2. **损失函数设计** $$ \mathcal{L}_{SFT} = -\sum_{t=1}^{T} \log P(y_t | y_{<t}, x; \theta) $$ 仅计算**目标序列**的交叉熵损失[^2] 3. **梯度更新策略** ```plaintext // 只微调特定层(高效训练) FOR param IN model.parameters(): param.requires_grad ← False // 冻结所有层 UNFREEZE(model.decoder.layers[-4:]) // 仅解冻最后4层 ``` #### 三、数学原理 监督微调通过最大化目标序列的似然概率调整参数: $$ \theta^* = \arg\max_{\theta} \sum_{(x,y) \in \mathcal{D}} \log P(y|x; \theta) $$ 其中: - $x$ 是输入序列 - $y$ 是目标序列 - $\mathcal{D}$ 是监督数据集 #### 四、完整PyTorch实现示例 ```python from transformers import AutoModelForCausalLM, Trainer, TrainingArguments # 加载预训练模型 model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b") # 配置训练参数 training_args = TrainingArguments( output_dir="./results", per_device_train_batch_size=4, num_train_epochs=3, learning_rate=2e-5, fp16=True # 混合精度训练 ) # 实现数据整理器 def collate_fn(batch): return { "input_ids": pad_sequence([item["input_ids"] for item in batch]), "labels": pad_sequence([item["labels"] for item in batch]) } # 启动训练 trainer = Trainer( model=model, args=training_args, train_dataset=dataset, data_collator=collate_fn ) trainer.train() ``` #### 五、最佳实践 1. **数据质量优化** ```plaintext // 清数据 REMOVE_LOW_QUALITY_SAMPLES(dataset) // 过滤错误标注 BALANCE_DOMAIN_DISTRIBUTION(dataset) // 平衡领域分布 ``` 2. **渐进式解冻** ```plaintext // 分阶段解冻层 FOR epoch IN 1..3: UNFREEZE(model.layers[-epoch]) ``` 3. **早停策略** ```plaintext IF eval_loss NOT DECREASE FOR 3 epochs: STOP_TRAINING() ``` > SFT微调使预训练模型**适应特定任务分布**,相比Prompt-Tuning[^1]能更充分挖掘模型潜力,尤其适合需要精确控制的专业领域任务[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值