Repository navigation
SenseVoice-Small 增量微调:保留普通话+粤语的前提下新增潮汕话识别,如何避免灾难性遗忘? #3388
Description
Activity
- addedquestionFurther information is requestedFurther information is requestedneeds triageNeeds maintainer triage and routingNeeds maintainer triage and routing
on Jul 23, 2026 先给结论:没有旧域约束时,任何全量或部分参数微调都不能保证普通话/粤语零退化。当前最稳妥的路线是“旧模型伪标注/公开数据 replay + 小学习率分阶段解冻 + 三套独立验证集”,而不是只用潮汕话单域继续训练。
1. 防遗忘
拿不到官方原始数据不等于不能做 replay。建议先用当前线上 SenseVoice-Small 给你们已有的、无标注的普通话/粤语生产音频生成伪标签,过滤低置信度/异常长短样本,再补充许可允许的公开 zh/yue 数据。训练前固定三套不参与训练的验证集:普通话、粤语、潮汕话,并始终用原始线上模型作为 retention baseline。
目前没有官方固定配比。可以先把下面三档作为实验起点,不是官方保证值:
- replay:new = 1:1
- replay:new = 2:1
- replay:new = 3:1
replay 内再按线上普通话/粤语流量或业务重要性分层采样。选择“潮汕话达到目标,同时 zh/yue 相对 CER/WER 退化不超过你们业务阈值”的最小新域占比;不要只看混合总指标。若连 3:1 replay 仍明显遗忘,继续增加 replay、降低学习率/训练步数,或加强 teacher-student 约束。
2. 冻结参数
SenseVoice-Small 没有单独文档化的 adapter/LoRA recipe,但 FunASR 通用训练入口已经支持按参数名前缀冻结。
funasr/bin/train_ds.py会读取freeze_param,所以可以先在现有命令加:++freeze_param="encoder" \ ++optim_conf.lr=0.00002这会冻结整个
encoder.*,只训练语言查询 embedding 和 CTC/output 部分,适合先做短暂 head warm-up;但潮汕话的声学差异较大,长期完全冻结 encoder 往往容量不够。更建议第二阶段只冻结前部层并放开后部层,例如通过逗号分隔的精确前缀冻结:++freeze_param="encoder.encoders0,encoder.encoders.0,encoder.encoders.1,encoder.encoders.2" \ ++optim_conf.lr=0.00002实际冻结多少层要靠三套验证集选择。启动后请检查日志里的
Setting ... requires_grad = False和 model summary,确认冻结范围符合预期。仓库里的 LoRA 基础设施和示例目前主要接在 Paraformer/SANM 的特定模块上;SenseVoice-Small 的构造路径没有把 LoRA 配置完整接通。因此不建议把
lora_only当作现成的 SenseVoice 官方方案,否则可能没有可训练的 LoRA 参数。3. 语言 token
当前 SenseVoice-Small 的 language id 是闭集:
<|zh|>、<|en|>、<|yue|>、<|ja|>、<|ko|>。训练数据集假定text_language恰好编码成一个特殊 token,模型内部的lid_int_dict/lid_dict也只认识这些固定 id。因此请不要直接把
<|teochew|>、<|nan|>等字符串写进 JSONL:现有 tokenizer 会把它们切成多个普通 token,语言查询会回退到 auto,而且后续text[:, 4:]的目标位置也会错位。在不 fork 模型结构的前提下,如果输出目标是标准中文,先用
<|zh|>是当前可运行方案。它确实与普通话共享条件,因此 replay 和独立 zh 验证集是必要的。新增真正的自定义 language token 不是数据侧一行配置:需要同时修改 SentencePiece 模型、vocab/CTC 输出维度、lid_dict、lid_int_dict、language embedding、旧权重迁移,以及导出/推理代码;目前仓库没有这条官方示例。建议的最小实验顺序
- 先保存当前线上模型在固定 zh/yue/chaozhou 验证集上的基线。
- 用
<|zh|>,replay:new=2:1,冻结整个 encoder,低学习率做短 warm-up。 - 保留 replay,逐步解冻 encoder 后部层;每个 checkpoint 同时评估三套集合。
- 若需要严格区分潮汕话条件,再单独设计 custom-LID 模型 fork,不要在这轮数据微调里混做。
为了把建议收敛成更具体的配置,麻烦补充:潮汕话/可用于 replay 的普通话/粤语各有多少小时和说话人数,以及当前线上模型在三套验证集上的 CER/WER(没有潮汕话基线也可以先给前两套)。
- addedneeds feedbackWaiting for reporter feedback or retest resultsWaiting for reporter feedback or retest resultsand removedneeds triageNeeds maintainer triage and routingNeeds maintainer triage and routing
on Jul 23, 2026 数据量(当前值,潮汕话与普/粤 后续都会持续增多)
- 潮汕话:8.45小时,说话人数约20人
- 可 replay 的普/粤:自有录音共约 75 小时(普通话约 100h、粤语约10h),无人工标注,仅音频 + 当前模型 ASR 输出(拟作伪标签,过滤低置信/异常长短),说话人数约240人。
当前线上模型三套验证集基线 CER(离线 SenseVoice) - 普通话(FLEURS test,300 句):8.9%
- 粤语(ASCEND 口语对话,314 句):9.1%
- 潮汕话(自有数据域 held-out,300 句):93.3%(现模型基本不会潮汕话,为起点)
感谢补充数据。按你们当前的 8.45 小时潮汕话数据,建议先跑下面这一档:
- 每个 epoch:潮汕话 8.45h + replay 16.9h(
replay:new=2:1)。 - replay 第一版可用普通话约 10.1h、粤语约 6.8h。这个 60:40 是为了显式保护粤语的实验起点,不是官方固定比例;不要按 100h:10h 原始池比例直接采样,否则粤语容易被淹没。
- 每个 epoch 从旧域池轮换采样,不需要把全部旧域音频重复塞入每轮。你回复中的“共约 75h”与“普通话约 100h + 粤语约 10h”不一致,请在生成 manifest 时以逐条统计结果为准,但这不影响每轮 16.9h 的 replay 预算。
- 三套验证集先固定,并确保与训练/replay 说话人不重叠。保留当前基线:zh 8.9%、yue 9.1%、Teochew 93.3%;提前定义 zh/yue 可接受的相对退化阈值。
- 第一阶段用
<|zh|>,冻结整个 encoder,lr=2e-5,先跑 3–5 epoch,并频繁保存/评估,不要直接沿用默认 50 epoch、2e-4。 - 若 zh/yue 保持住但潮汕话 CER 停滞,再从最佳 checkpoint 放开 encoder 后部层;若任一旧语种超出阈值,则增加 replay、提高粤语最低占比、降低学习率或提前停止。
- 选 checkpoint 时先淘汰所有违反 zh/yue 保持阈值的模型,再在剩余模型里选潮汕话 CER 最低者。
我已经把完整的中英文配方、冻结参数和 1:1 / 2:1 / 3:1 实验矩阵合入仓库:
你们可以先按 2:1 跑第一轮,把每个 checkpoint 的 zh/yue/Teochew CER 表贴回来,我们再根据三条曲线收敛第二阶段解冻层数。
- 每个 epoch:潮汕话 8.45h + replay 16.9h(
当前问题已有可执行的 2:1 replay、分阶段冻结/解冻和三语种验证方案,对应中英文持续微调指南也已通过 #3396 合入 main。先按已提供方案将 issue 标记为 completed,避免开放队列长期停留在等待反馈状态。后续跑出 zh / yue / 潮汕话各 checkpoint 的 CER 表后,可以直接重新打开本 issue;届时我们再根据三条曲线一起收敛 replay 比例、学习率和解冻层数。
重新打开这个 issue。之前因为已经给出训练方案和指南就标记 completed,但方案尚未经过你们的三语种 checkpoint/CER 结果验证,这不等于实际问题已经解决。
请按方便的节奏继续实验;有 zh / yue / 潮汕话的 CER 表或训练曲线后直接贴在这里。即使暂时没有结果,本 issue 也会保持为等待反馈,而不是因为队列整理而关闭。
抱歉隔了很久才回来。这段时间我们大部分精力花在语料采集上,所以拖到现在才回复。
也谢谢您 8-29 主动重开这个 issue、并说明可以按我们的节奏推进。您当时当天就给出了完整配方,
还把中英文持续微调指南合进了主仓(#3396),对我们帮助很大。先说一个必须坦白的情况,它决定了这条回复能提供什么、不能提供什么。
一、为什么我不贴潮汕话那一列的 CER
我们按您的一阶段配方完整跑完了,但事后发现当时的潮汕话测试集本身不可信:
它的参考文本与训练标签同源,而这批标签究竟是逐字转写、还是书面语意译,
我们当时根本没有验证过。后来另建了一套与训练标签不同源的独立测试集,
才发现旧尺子量出来的数字没有参考价值。所以早期那些潮汕话 CER 我就不贴了——贴出来只会误导您对 replay 配比的判断。
这一条本身可能是最值得写进指南的教训:给一个全新语种做持续微调时,如果测试集的
参考文本与训练标签同源、且标注规范未经独立核验,那么该语种上的所有 CER 都不能用来
选 checkpoint,也不能用来判断某个配置好不好。 我们在这上面浪费了相当长的时间,
而它不报错、不异常,从曲线上完全看不出来。二、旧域保持这一侧的结果(这部分尺子是干净的)
旧域四格用的是 FLEURS-cmn / FLEURS-yue 等公开测试集,参考文本没有上面那个问题。
这里只给相对退化量——我们的绝对基线后来因为 CER 口径修订变动过,绝对值不宜跨期比较:配置 旧域四格最差退化 备注 <|zh|>、冻结整个 encoder、lr 2e-5(按指南)+1.66pp 普通话退化最明显(普·朗读 +1.40pp) <|minnan|>、冻结 encoder、更小 lr+0.97pp 普通话两格 ±0.0 <|minnan|>、解冻 + teacher-student 约束 + WiSE-FT+0.17pp 旧域达标 一个可能对指南有用的观察:共享
<|zh|>会显式干扰普通话。
有<|yue|>隔离的粤语几乎不动,而普通话明显退化;改用词表中已经存在的<|minnan|>
(单 token,不属于您提醒的"自定义字符串被切碎"那种情形)之后,普通话退化归零。
如果指南里能提示一句"新语种优先复用词表中已有的空闲语种 token,而不是共享<|zh|>",
应该能帮到后来的人。至于潮汕话本身,目前已经从"基本不识别"(我们 7-23 贴过的起点:held-out 集 CER 93.3%,
后来更换测试集后,训前甚至因为大量插入而超过 100%)推进到了业务可用的水平。
具体数字我们还在用那套独立测试集复核,等稳定了再完整汇报。
我们这边的后续实验也还在进行中,很抱歉这一轮不能在 replay 配比的收敛上帮到指南。三、一个可能值得修的问题,和两条来自部署侧的提醒
1.(可能值得修)
model.pt.best对纯 CTC 的 SenseVoice 是静默失效的funasr/train_utils/trainer.py里avg_keep_nbest_models_type默认取"acc",
而选优判据用的是cur_acc >= best_acc——是>=,平手也会覆盖。
SenseVoice-Small 是纯 CTC,验证集acc恒为0.0000,于是0.0 >= 0.0恒成立
⇒model.pt.best必然等于最后一个 checkpoint(我们做过 md5 比对确认)。它不报错也不告警,使用者会以为自己拿到了最优点,实际拿到的是最后一个点。
建议:CTC-only 模型下默认改用loss,或者在检测到acc恒为 0 时打一条 WARNING;
持续微调指南里也建议加一句"请勿依赖model.pt.best,按验证集逐点实测选点"。2.(部署侧提醒)新增语种 tag 后,要检查下游有没有对语种做闭集校验
这一条不是 FunASR 本身的问题,是我们在自己的部署链路上踩到的,但我觉得值得写进指南:
我们的服务端对识别结果的语种 tag 做了闭集校验,遇到闭集之外的 tag 就判为异常、
丢弃离线结果——最终端点只返回一个句号,而模型其实识别得好好的,日志里只有一行 WARNING。结果就是:模型训好了,端到端仍然是空输出,而且很难往模型之外的地方去想。
指南里如果能提示一句"新增语种 tag 后,需同步检查下游/部署侧是否存在语种校验或白名单",
应该能省掉后来者不少排查时间。3.(现象反馈)发射速率被压低 / CTC blank 偏置
上次提到的这个现象仍然存在。我们从解码侧验证过:对 blank 施加惩罚无法挽回——
把输出长度拉回 1.0 时精确率同步崩掉,说明底下并没有可用的分布,属训练侧现象而非解码侧。
持续学习场景下若官方有推荐的对策(length_normalized_loss、blank 相关正则、
新域 loss 权重等),也希望能写进指南。四、两个想请教的问题
-
SenseVoice-Small 预训练时,粤语数据里朗读与自发口语的大致配比是什么量级?
我们观察到在持续微调中,粤语这两种体裁的稳健性差别很大——朗读几乎不动,
自发口语明显更脆弱。想确认这是否与预训练分布本身有关。 -
embed表里各语种槽位在持续微调中的梯度问题。 如果某个语种在新的训练数据里
占比很低,它对应的槽位在整个微调过程中几乎拿不到梯度、基本保持原样。
官方对这种情况有建议吗——显式冻结、特殊初始化,还是不必处理?
-
谢谢这次把可信结果和不可信结果分开说明,尤其是独立测试集的标注口径问题。这比继续用旧 CER 调配比更有价值。你们的旧域退化表会作为这一实验的反馈保留,不会写成通用保证,也不要求你补交已经失效的潮汕话数字。
我核对了当前源码:
Trainer默认确实用avg_keep_nbest_models_type="acc",且平手会覆盖 best。对于你描述的验证 acc 始终为 0 的运行,这个指标没有选点能力。后续新运行可以在现有训练命令中明确加入:++train_conf.avg_keep_nbest_models_type=loss
这是当前已有参数,不是尚未发布的修复;它只使 best 按验证 loss 选取,不保证是最低 CER 或旧域保持最好的 checkpoint。仍需保留多个候选并在独立的新域、普通话/粤语朗读及自发口语集上逐点比较。我会把 CTC 场景的默认指标/告警和相应回归测试作为单独修复处理,本条回复不代表问题已经修好。
关于另外两问:
- 我目前没有可核验的 SenseVoice-Small 预训练粤语朗读/自发口语配比,不能据此给出一个比例,也不能把你们的差异直接归因于该配比。
- 低频语种槽位建议先保留预训练初始化,记录实际采样次数、对应 embedding 行的梯度和参数变化,再做冻结/低学习率对照。槽位本身变化小也不代表该语种能力不会退化,因为共享 encoder 仍可能变化;梯度置零也不一定阻止 AdamW 等优化器的 weight decay。
<|minnan|>的实验结果值得记录,但“词表里已有”不能自动等同于“空闲、可任意复用”。指南需要区分你们的具体对照结果与通用建议,并加入 tokenizer/模型映射/服务端白名单的端到端检查。独立标注规范、best 选点边界、下游语种校验这三点也会纳入后续指南修订。issue 保持开放。这次已把选点问题做成独立修复并合入 #3677,同时更新了 SenseVoice 的微调脚本和中英文持续微调指南。
关键原因不是
>=本身,而是当前 SenseVoice 的验证统计没有 ASRacc,只有acc_rich;此前 trainer 把缺失的acc当成了 0。修复后,缺失或非有限指标不再进入 best、保留数量筛选和 checkpoint 平均;真正的数值 0 仍是合法指标。普通 trainer 和 DeepSpeed trainer 的保存/恢复路径也同步修正,写入的 best 指针与排名对应当前保存结果。维护脚本现在显式使用
++train_conf.avg_keep_nbest_models_type=loss。新指南强调:这是模型总验证 loss,不等于最低 CER,也不保证旧域保持;平均后的模型同样需要独立评估。改变选点指标请使用新的输出目录,旧运行中已经写入的虚假 0 排名不会自动修复,已删掉的候选权重也无法恢复。本次最终提交在 ind-gpu8 上通过 97 项 CPU 回归,包括真实双进程 Gloo、缺失/非有限指标、保存/恢复、筛选/平均、写入失败时不提前删除旧候选,以及文档契约。DeepSpeed 的保存接口使用模拟引擎边界测试,不是完整 DeepSpeed/GPU 声学训练验证。
你提出的独立标注来源/逐字转写口径、旧域体裁分开评估、tokenizer/模型映射/服务端语种白名单检查,已经加入 中文指南 和 英文指南。已撤下可能误导后续选点的旧 CER 表,没有把
<|minnan|>写成任意可复用的空闲槽位。本次是源码修复,尚未包含在 PyPI 1.4.14 中。它解决的是选点与保存逻辑,不代表灾难性遗忘或 CTC blank 偏置已经解决。issue 继续保持开放,后续按你们独立测试集的复核结果推进。
更新发布状态:#3677 的 trainer 选点与保存修复现在已包含在 PyPI FunASR 1.4.15 中,不再需要仅为此修复安装开发分支。
训练脚本与指南请同时参考固定版本的中文持续微调指南;
pip升级不会自动替换你复制到工作目录里的训练脚本。更换选点指标请使用新输出目录,旧运行中错误的排名或已删除权重不会自动恢复。这次发布解决的是有效指标选点和 checkpoint 保存边界,不代表 CER、遗忘或 blank 偏置已解决。issue 保持开放,后续继续依据你们的独立测试集结果判断。
背景
我们线上用 SenseVoice-Small 做普通话 +粤语识别(已部署),输出标准中文。现在想在不损失现有普通话/粤语识别能力的前提下,新增潮汕话识别。
难点:潮汕话不在 SenseVoice 现有语种里;而且我们拿不到官方普通话/粤语的原始训练数据,担心直接微调会导致普/粤能力灾难性遗忘。
想请教的问题
如何防遗忘:在没有原始 zh/yue 训练数据的情况下,微调新增一个语种,有没有官方推荐做法避免遗忘?是否只能靠混入其他 zh/yue
数据"复习"?新增语种数据 vs 复习数据的推荐配比大概是多少?
能否部分参数微调:SenseVoice-Small 微调是否支持冻结部分参数(比如冻结encoder、只训练部分层)用于低资源/防遗忘?文档里我只看到全量微调,请问有没有冻结/adapter 的官方做法?
语种标签冲突:SenseVoice 的语种 token 是闭集(
<|zh|> <|en|> <|yue|> <|ja|> <|ko|>),没有潮汕话槽位。我打算把潮汕话标成<|zh|>(因为输出目标是标准中文)——这样会不会干扰原有普通话(同为<|zh|>)的识别?能否新增一个自定义语种 token?如何添加?有没有增量扩展新语种的官方示例或建议?
我已经尝试/了解的
finetune.sh与 jsonl 数据格式(key/source/target/text_language等)。<|zh|>是否会干扰普通话。计划使用的命令
计划用
examples/industrial_data_pretraining/sense_voice/finetune.sh+ jsonl(潮汕话样本text_language标为<|zh|>)。尚未开跑,想先确认方向是否正确。