☰
小模型LoRA微调前:用64条样本测SST-2情感分类基线
2026/10/6 15:03:17 网站建设 项目流程

我准备对一个 1.5B 级别的小模型做 SST-2 情感分类微调,动手之前先按流程测了个基线。第一次做的时候没有这一步,LoRA 跑完准确率从 52% 升到 88%,当时觉得是调参调对了。后来出于好奇把同一批开发样本喂给未微调的基座,发现零样本就已经能到 85%。也就是说那次微调的净收益只有 3 个百分点,不是 36 个百分点。这就是基线缺失造成的最典型误判。

这篇想分享的是一套很轻量的做法:用 SST-2 开发集中挑出的 64 条样本,在微调前给小模型建立两条基线——能力基线(它到底会不会做这个任务)和格式基线(它能不能按要求稳定输出)。适合谁?手里只有 1B~3B 小模型、想在有限算力上做 LoRA 微调的人,以及被“微调完效果提升多少”这种问题困扰过的朋友。文章最后会给一张决策表,告诉你什么样的情况值得微调,什么样的情况换提示词或换基座更划算。

1. 先搞清楚基座“本来就会”什么,再谈微调

很多人在微调前没有任何评估,直接拿训练集开跑,这种行为我见得太多。开源小模型在海量语料上预训练过,SST-2 这种经典二分类任务太常见了。一个 1.5B 模型很可能已经见过大量“电影评论 + 情感标签”的语料,所以它不一定需要你教,你只需要把已有的能力用提示词激发出来。没搞清楚这一点就微调,最后得到的“提升”可能只是幻觉。

1.1 你无法从名称判断一个开源模型会不会做 SST-2

只看模型名字,你根本猜不出它在情感分类上的表现。同系列不同尺寸的模型差距非常大:有的 7B 模型在简单情感句上反而输给某些经过对齐的 1.5B 模型;有的 3B 模型对“positive/negative”这两个词特别敏感,换一个等价说法就崩掉。更麻烦的是,预训练语料不一定包含 SST-2 本身,但一定包含大量的影评和评价文本,模型对“正负面情感”这个概念的把握程度,只能通过一次受控测试来量化。

这里说的“受控测试”,不是拿几条样本跑一下看个热闹。你要知道的是:如果任务本身模型已经会了,微调的数据组织方式就得完全不同;如果模型只是勉强会用,LoRA 的作用就变成了稳定输出;如果模型完全不会,那要担心的就不是 LoRA 参数,而是基座选错了。

1.2 微调基线解决的不只是准确率问题

我习惯把基线拆成两条,分开记录:

  • 能力基线:在给定提示下,模型能不能正确区分 positive 和 negative。
  • 格式基线:在给定输出约束下,模型能不能一直按你预期的格式返回结果。

这两条经常被混在一起。你可能会看到某个模型准确率还行,但输出永远是 “Positive.”、“positive sentiment”、“pos” 这类变体,解析器没法统一处理。如果没有格式基线,你设计微调数据时就会很盲目:到底要不要加“只输出标签”的强调?要不要用 JSON?要不要在系统提示里写格式示例?这些决策完全取决于基座在零样本下的格式稳定性。

1.3 为什么偏偏是 64 条

64 不是个数量的执念,而是工程上的权衡。对二分类任务来说,正负各 32 已经能覆盖不少情况;包含否定句、转折句、长句、口语化表达之后,你至少能发现“模型是否存在结构性失效”。而且评估脚本要反复调模板、调温度、调种子,用完整 dev 集 872 条来跑,每次迭代要多花好几倍时间。在小模型 CPU 推理的环境里,这个差距会被放大到让人失去耐心。

64 条样本是为了筛错,不是为了发论文。它的定位是快速回答三个问题:这个基座能不能做这个任务?输出格式稳不稳?值不值得进入 LoRA 微调流程?后面如果要出正式对比报告,再扩到完整 dev 集不迟。

2. 64 条开发样本的构造:抽样逻辑决定基线可信度

从 SST-2 dev 集里抽 64 条,听起来很简单,但抽样方式直接决定基线结论靠不靠谱。你绝不能从原始 dev 文件里直接取前 64 条,因为 GLUE 的 dev 文件不是按难度均匀排列的,前几十条往往存在主题或句式上的偏向。取偏了,基线的“高”或“低”都没有参考价值。

2.1 直接取前 64 条会得到什么

我试过偷懒直接切前 64 条,结果 64 条里有 21 条都带 “great”、“amazing” 这种高频正面词。零样本测试准确率高达 92%,看起来很强,换一批样本测立刻跌到 78%。这不是模型不稳定,是样本集没代表性。反过来,如果前几条恰好都是否定句式、转折句,你又会得出“模型不会做情感分类”的错误结论。

SST-2 的样本分布大概有这样一个特点:短句常常是直白的褒贬,长句才有转折和反讽。忽略句子长度分布,基线就会偏向某一种难度。

2.2 分层抽样:先按标签分,再按长度分

我的做法是三步:

  1. 按标签把 dev 分成 positive 和 negative 两组。
  2. 每一组内部按 token 长度分成三桶:短(<10)、中(10~20)、长(>20)。
  3. 设定随机种子,从每个桶里按比例抽取,最后合并成正负各 32 的样本集。

参考代码如下:

import pandas as pd import random df = pd.read_csv("dev.tsv", sep="\t", header=None, names=["label", "text"]) # GLUE 版 dev.tsv 中 label 是 0/1,先映射成 sentiment label_map = {0: "negative", 1: "positive"} df["sentiment"] = df["label"].map(label_map) df["token_len"] = df["text"].str.split().str.len() def bucket(x): if x < 10: return "short" elif x < 20: return "mid" return "long" df["bucket"] = df["token_len"].map(bucket) selected = [] for senti in ["negative", "positive"]: sub = df[df["sentiment"] == senti] for b in ["short", "mid", "long"]: bucket_df = sub[sub["bucket"] == b] n = round(32 * len(bucket_df) / len(sub)) selected.append( bucket_df.sample(n=n, random_state=42) ) sub_dev = pd.concat(selected).sample(frac=1, random_state=42) print(sub_dev["sentiment"].value_counts()) print(sub_dev["bucket"].value_counts())

然后我会把抽出来的样本手工过一遍。重点看有没有出现连续十几条同样句式的情况,以及否定词、转折词、比较级、讽刺语气的样本占比是否明显低于真实分布。如果明显偏低,就手动替换几条,保证 64 条不是“均匀随机”的壳子。

2.3 加入控制组:模板语义与标签语义分离

分层抽样之外,我会在 64 条之外单独准备一份控制组,大约 8~12 条。控制组的用途是区分“模型理解了任务指令”和“模型只是对情感词产生了表面联想”。

比如一条真实样本是 “A great movie with a silly plot.”,控制组可以改成 “A fine film with a silly storyline.”,甚至换成 “这部电影还不错,但剧情很傻” 这样的中文表述。如果模型在真实样本上准确率不错,但控制组准确率骤降,说明它依赖的是训练语料里高频触发词,而不是真正在做正负判别。

控制组不计入基线准确率,它的作用是提醒你:微调时需要补充更泛化的数据,而不是堆更多相同句式的样本。

3. 能力基线怎么测:零样本、少样本和置信度

有了 64 条开发样本,接下来就是正经的测量。测量工具很简单:一个生成函数 + 一批固定模板。真正花功夫的是理解测试结果背后代表的能力层次。

3.1 零样本测试的第一句话是模板

零样本测试不是把评论直接丢给模型,而是必须构造合理的提示。同一句话,不同模板可能带来超过 10 个百分点的准确率波动,在小模型上尤其明显。我的建议是:先测一个自己最接近真实部署场景的模板,再测一个“填空式”模板,因为填空往往更稳定。

参考模板:

def build_prompt(text): return ( "请阅读下面这条电影评论,判断它的情感倾向。\n" f"评论:{text}\n" "答案:" )

生成时设置温度 0、贪心解码,限制 max_new_tokens=8,然后截取模型输出的第一个词做映射:

from transformers import AutoModelForCausalLM, AutoTokenizer import torch model_name = "Qwen/Qwen2.5-1.5B-Instruct" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForCausalLM.from_pretrained( model_name, torch_dtype=torch.float16, device_map="auto" ) def predict(text): prompt = build_prompt(text) inputs = tokenizer(prompt, return_tensors="pt").to(model.device) with torch.no_grad(): out = model.generate( **inputs, max_new_tokens=8, do_sample=False, temperature=1.0, ) answer = tokenizer.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True) return answer.strip().lower()

这样跑完 64 条,你会得到原始准确率。注意别在第一次跑的时候就把模板改来改去,模板一改,之前跑的结果就不能直接并列比较。

3.2 少样本演示的边际收益曲线

零样本只是起点,我更看重“少样本演示”的边际收益。从开发集剩余样本里挑 2、4、8 条带正确答案的演示,塞进提示里,然后观察准确率怎么变化:

  • 准确率随 k 上升明显(比如从 60% 到 82%),说明模型底层表示是够的,只是需要被“点拨”一下。这种情况微调成本很低,少量数据就能见到效果。
  • 准确率不升甚至下降,说明模型对任务格式和语义都不太适应。这时候盲目加 LoRA 数据,也只是在强行记住训练集,换一批样本大概率掉点。
  • 演示样本不能从待测的 64 条里选,否则等于开卷考试。我习惯从 dev 剩余样本里随机选,保持和运行环境一致。

少样本演示的收益曲线还能告诉你另一件事:如果你只有几十条标注数据,到底该拿去当“演示样例”还是“训练数据”。如果模型靠 4 条演示就能大幅提升,说明问题出在“触发”而不是“学习”,那你完全可以先不微调,靠提示工程上线。

3.3 logits 不只用来算准确率,还可看“敢于作答”的程度

准确率只是结果,我更关心模型的置信度形态。具体做法是取生成第一个 token 时刻的 logits,看 positive 和 negative 两个候选 token 的概率差:

inputs = tokenizer(build_prompt(text), return_tensors="pt").to(model.device) with torch.no_grad(): logits = model(**inputs).logits[:, -1, :] log_probs = torch.log_softmax(logits[0], dim=-1) pos_prob = log_probs[tokenizer.convert_tokens_to_ids(" positive")].exp().item() neg_prob = log_probs[tokenizer.convert_tokens_to_ids(" negative")].exp().item() conf_gap = abs(pos_prob - neg_prob)

如果一条样本模型答对了,但 positive/negative 概率差只有 0.02,那它本质上是在猜,只是运气好而已。我会把每个样本的置信度记录下来,分别算“答对样本的平均置信度”和“答错样本的平均置信度”。两者差距越大,说明模型确实在区分,不是瞎蒙。两者都很低,说明模板或者基座有问题,即使准确率超过 80% 也要警惕。

下面是一张我用来记录能力基线的示意表格,数字量级贴合常见小模型,但不必对应具体版本:

模型零样本准确率4 条演示后准确率答对平均置信度答错平均置信度
Qwen2.5-1.5B-Instruct84.4%87.5%0.780.55
Llama-3.2-3B-Instruct76.6%81.3%0.610.52
某 7B 基座模型90.6%90.6%0.880.63

注意,准确率接近时,置信度形态能帮你选出更值得微调的基座。

4. 格式基线:小模型在“怎么回答”上比“答得对不对”更容易翻车

能力基线看内容,格式基线看容器。小模型在怎么回答这个问题上,翻车率远高于普通人预期。你要求模型“只输出一个标签”,它偏偏给你输出一整句解释;你要求它输出 JSON,它给你写一段 Markdown 列表。格式基线的目的,就是精确测量这种浪费。

4.1 让模型输出 positive/negative 其实不是格式约束

严格来说,让模型输出 “positive” 或 “negative” 并不算格式约束,因为这两个词在预训练语料里天然高频。真正的格式问题是:模型是否愿意不加修饰、不带标点、不附带解释地把标签词交出来。

我在小模型上经常见到三种输出:

  • “Positive”
  • “positive sentiment”
  • “The sentiment of this review is positive because the movie is great.”

第一种好处理,第二种用前缀匹配也能勉强解析,第三种基本没法在短输出场景里直接使用。这些都在格式基线的统计范围内。

4.2 三种输出约束的通过率对比

在小模型上做格式测试时,我习惯把输出约束分成三个等级,分别统计通过率:

输出约束典型失败形式实际用途
纯标签词带句号、带解释、输出同义词快速推理、低算力部署
JSON 键值键名写错、引号缺失、嵌套多余字段供下游程序直接消费
中文标签输出“正面”和“正面情绪”混用面向中文业务场景

一份 1.5B 模型的格式基线通常长这样:温度 0 时纯标签词通过率 85%,一到采样解码掉到 60%;JSON 通过率可能只有 40%。这说明模型不是不会输出格式,是对格式的记忆太浅,一旦解码引入随机性就露馅。

4.3 格式失败不是小概率事件

很多人把格式失败归因为“prompt 没写清楚”,但在小模型上,这更像是指令遵循能力的边界问题。模型在预训练阶段最擅长的是“续写”,你要求它严格输出标签词时,它仍会顺着语言模型的惯性往解释性文本上走。

格式基线具体测法是:固定同一批 64 条样本,分别用贪心解码和温度 0.7 的采样解码各跑一次,统计三种解析规则下的通过率:精确匹配、前缀匹配、正则抽取。然后记录“同一句话在采样下产生了几种不同输出形式”,比如同一句话第三次输出 “positive”,第四次输出 “positive.”。

这一步做完,你就知道微调数据里“格式约束样本”到底要占多大比例。

5. 基线与 LoRA 微调的衔接:一张决策表搞定起步配置

基线的意义不是好看,而是指导下一步动作。把能力基线和格式基线交叉,可以得到四种组合,每种组合对应的微调策略完全不同。

5.1 决策表:能力基线、格式基线交叉后该做什么

能力基线格式基线我的处理方式
高(>85%)且置信度健康稳定优先尝试提示工程,微调非必需;真要微调就用小秩、小学习率轻量校准
中(65%~85%)且少样本能涨稳定值得微调,重点提升难例,LoRA 秩设 8 左右
中(65%~85%)但少样本不涨不稳定先换模板和基座,微调时在数据里加强格式约束
低(<60%)且少样本不涨乱严肃反思基座选型问题,微调大概率只是记住训练集表面模式

看起来很朴素,但这张表是我频繁踩坑后的经验总结。通常情况下,能力弱 + 格式乱是最难救的,LoRA 会强行让模型输出符合训练集的标签词,但换一批样本很快就掉。此时与其加数据,不如换一个更适合的基座。

5.2 LoRA 参数从哪开始设

如果决策表告诉你“值得微调”,我会用这组参数作为起点:

r: 8 alpha: 16 target_modules: [q_proj, k_proj, v_proj, o_proj] lr: 3e-4 batch_size: 16 epochs: 3 max_length: 128

为什么 SST-2 用 r=8 就够?因为二分类任务需要的适应性变化不多,主要改变的是输出层的偏好和格式记忆。如果你想同时解决格式乱的问题,可以适度把 r 提到 16,给模型更多自由度去模仿你给的格式样本。学习率不建议超过 5e-4,小模型在几百条数据上很容易过拟合,微调后回到开发样本上掉点的情况很常见。

微调数据组织上,我的原则是:如果格式基线不稳,训练样本统一使用“评论:xxx\n答案:标签”的结构,并在开头加一句格式约束;如果格式基线很稳,就少放格式样板,把资源花在难例上。

5.3 微调后必须跑同一份基线

微调跑完之后,评估脚本必须和微调前完全一致。同一批 64 条、同一个模板、同一个种子、同一个解析脚本。然后记录两个数:微调前零样本准确率、微调后准确率。两者之差才是模型真实的增量。

我见过不少项目,微调后拿测试集一测说提升了 20 个点,但根本没跑过零样本对照。真实情况是基座本来就会一半以上,微调只是补齐了模板差异。用 64 条开发样本做基线,最大的价值就在这里:你能给自己的实验一个诚实归因。

6. 64 条样本的局限性:方差、模板敏感和过期习惯

64 条样本不完美,你要清楚它的边界。我自己用它的过程中,有几个坑必须单独说。

6.1 小样本方差会把 1% 的差距变成“看起来的 3%”

64 条样本的二分类准确率是一个带噪声的估计。64 条里答对 54 条是 84.4%,答对 52 条是 81.3%,差的这 2 条会让人误以为两个模型差 3 个百分点。但在统计意义上,这两个结果可能根本没有显著差异。

所以我做小样本评估时,不会因为一两条的差距就换模型。我会在同一批样本上跑多个随机种子、多个演示顺序,取平均结果,再下结论。如果时间允许,让温度 0.7 采样再多跑一遍,观察波动范围。

6.2 同一个模板在另一个模型上不 work

SST-2 的中文提示模板在 Qwen 上很好用,换到 Llama 上可能准确率掉 10 个点;英文模板则反过来。这在小模型上特别明显,因为不同基座的对齐语言和习惯差异很大。

因此,每个模型都应该有一个自己的基线模板版本。我通常把模型名、模板、种子、解码参数打包成一个版本字符串,比如 baseline_v3_qwen15_zh,所有后续对比都基于这个版本。改模板、改种子后的结果不参与历史对比,否则一开始记录的就是一笔糊涂账。

6.3 基线的“保质期”:基座更新后一切重新测

开源小模型迭代很快,今天测的 Qwen2.5 基线,不代表几个月后的 Qwen3 也适用。新基座可能在指令遵循、格式稳定性上有明显变化,也可能在某些能力上反而退化。

我在本地跑量化小模型时也发现,同一个模型在 FP16 和 4bit 量化下,格式通过率会有差异。如果你最终部署的是量化版本,基线就应该在同一个量化版本上测,而不是用全精度结果代替。否则微调完部署上线,格式输出变样了,你根本分不清是模型问题还是量化问题。

6.4 什么时候该把 64 条扩成完整开发集

64 条只解决“该不该微调”“怎么组织微调数据”这两个问题。如果已经决定微调,并且需要评估最终模型效果,就把它扩到完整 dev 集。到那时,你手里已经有可信的 64 条基线做参照,完整开发集上的提升数据才有说服力。

我个人的习惯是:每次拿到新模型、新任务,第一件事不是开训练脚本,而是写一个 eval_baseline.py 跑二十分钟。这个习惯让我砍掉了很多没必要的微调。有两个任务测完发现零样本已经 90% 以上,直接跳过微调上线;还有一个任务表面看着简单,基线只有 51%,后来换基座比调 LoRA 参数有用得多。

64 条样本不神圣,神圣的是你有一个能反复对照、诚实归因的基线。微调前花这二十分钟,比微调后花两天解释那些说不清的提升要划算得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询