☰
Kaldi split_scp.pl报错:说话人数不足导致分割失败的原因与解决方法
2026/10/14 15:07:35 网站建设 项目流程

别慌,这个报错我在Kaldi上见过好几次了。第一次碰到的时候是深夜跑的流程,日志刷出一行utils/split_scp.pl: Refusing to split data because number of speakers 2 is less than the,后面直接中断退出,当时我对着屏幕愣了半天——数据明明准备了,怎么就不让分了呢。后来把这条报错的来龙去脉摸清楚之后才发现,问题往往出在数据准备阶段,而且有好几种不同的触发原因,对应的处理方式也完全不一样。

这篇就专门聊这个报错:它什么时候会出现、底层判断逻辑是什么、怎么排查、哪些情况下可以直接改配置绕过去,哪些情况下必须回头补数据。如果你正在用Kaldi做音识别相关的实验或者工程落地,尤其是刚开始接触说话人相关任务、数据的说话人数量又不多的时候,这篇文章应该能帮你在几分钟内定位到问题。

1. 先搞清楚这个报错的真实含义

1.1 报错在哪个阶段最容易冒出来

用过Kaldi的人都知道,Kaldi有一套完整的shell工具链,各种.pl脚本承担着数据划分、特征提取、对齐、训练之前的准备工作。split_scp.pl就是这套流程里用得极其频繁的一个脚本:把某个scp文件(一般是特征文件列表或者音频文件列表)按指定的份数切分,为后续多机多卡并行或者并行任务分发做准备。

在实际项目里,这条报错最常见的出现位置是数据准备阶段跑局部脚本的时候,比如:

  • 跑steps/make_mfcc.sh或steps/make_fbank.sh时指定--nj参数,脚本内部需要把wav.scp或feats.scp分割成与nj对应数量的份数。
  • 跑utils/fix_data_dir.sh之后继续执行utils/split_data.sh分割数据目录,想把训练集分成若干shard时。
  • 训练前跑steps/train_mono.sh、steps/train_dnn.sh这类脚本,内部常数会用utils/split_data.sh按说话人切分训练子集。

报错出现时,后面的脚本会直接因为分割失败而终止,导致整个流程卡死。

1.2 报错的完整语义拆解

这条报错的关键信息是number of speakers 2 is less than the。把整条日志补全,实际是:

utils/split_scp.pl: Refusing to split data because number of speakers 2 is less than the number of splits 数据份数

这个判断逻辑用大白话说就是:脚本发现你的数据里只有2个不同的说话人,而你要把数据拆成比如5份,脚本认为这样拆分会导致某些份里面压根没有完整的说话人信息,后续做说话人相关处理时会出现映射缺失,所以坚决拒绝拆分。

Kaldi里对“说话人”这个概念的使用频率远超很多新手想象。不仅仅在说话人识别、说话人确认任务里需要spk2utt和utt2spk,即使是普通的ASR训练流程,模型训练脚本也会依赖说话人来组织数据分段,比如做fMLLR、说话人自适应,或者在神经网络训练里给每个说话人分配embedding。如果某个子集里没有某个说话人的任何语音,就会产生不一致性。

1.3 这个报错最常见的两个场景区分

有很多人一看到这个报错就慌,以为自己的数据全部损坏了。但根据我的经验,绝大多数情况其实只分两类:

一类是你确实只有极少数量的有效说话人,这类问题需要扩充数据来源或者调整分割份数。

另一类是你的原始数据本来有几十个说话人,但经过某一步处理后,spk2utt或者utt2spk文件出了问题,导致Kaldi认为你的有效说话人只剩2个。这类情况属于数据准备bug,排查修复即可,不需要重新采集数据。

这两者的处理方向完全不一样,所以动手改任何东西之前,先做一次全面的数据目录体检是很有必要的。很多资料上来就告诉你“把--nj改小一点”,但这只是掩耳盗铃,如果数据目录有问题,你调小--nj也许能跑通这一步,后面train阶段还会炸。

2. 核心机制拆解:split_scp.pl 的说话人判断逻辑

2.1 脚本本身怎么统计说话人数

split_scp.pl本身是一个Perl脚本,它的职责是读入一个数据列表,然后按行分配到多个输出文件。单纯的split_scp.pl其实不关心说话人数量,但 Kaldi 的分割框架里还有另一个关键脚本utils/split_data.sh。

split_data.sh会把Kaldi数据目录里的多个文件(wav.scp、text、utt2spk、spk2utt等)一起按某种规则分割。在这个脚本内部,它会构造临时的说话人性映射,然后调用类似这样的逻辑:

# 伪代码示意,实际脚本逻辑更复杂 num_speakers=$(wc -l < data/spk2utt) if [ "$num_speakers" -lt "$num_split_jobs" ]; then echo "utils/split_scp.pl: Refusing to split data because number of speakers $num_speakers is less than the number of splits $num_split_jobs" exit 1 fi

所以,报错里提到的“speakers 2”,是脚本从你的数据目录的spk2utt文件读取到的行数。spk2utt是Kaldi里“说话人ID到话语ID列表”的映射文件,每一行代表一个说话人及其对应的所有语音片段ID。行数就是说话人的数量。

理解这一点很重要:脚本判断的是有效映射条目数,不是你录了多少个文件夹,也不是wav.scp里有多少条音频。

2.2 为什么Kaldi要把这个判断写得这么“死板”

从使用者的角度,很多人会觉得这个限制不合理:我只有2个说话人,想分成4份跑并行,为什么就不行?每个说话人的语音分开放在不同份里不就好了?

实际上Kaldi这么做有自己的设计理由。Kaldi的并行训练和数据准备框架里,后续很多步骤会按“份”来统计统计量、做归一化、计算全局CMVN之类的东西,并且在训练时需要保证每个speaker的数据尽量完全落在同一份里,避免同一说话人的不同语音段被切到不同worker,造成特征统计不一致或者说话人信息泄漏。

如果每个说话人的数据被拆散到多份,那么说话人级别的统计就乱了。而如果只有一个说话人,怎么拆都会出现某几份完全缺失该说话人的数据,训练出的模型在这些份上就学不到完整的说话人特征。

当然,实际脚本里也提供了--per-speaker之类的选项,让用户可以选择尽量按说话人边界拆分,但即便按这个方式拆分,脚本也同样会先检查说话人数是否小于分割份数,防止出现空份。所以这就是一个“宁可保守,也不要出错”的设计。

2.3 数据目录里哪些文件会影响统计结果

既然脚本统计的是spk2utt,那数据目录里跟说话人相关的几个文件就必须时刻保持一致。下面这几个文件是Kaldi数据目录的核心:

  • wav.scp:音频路径列表,格式为uttID 实际路径或者管道命令
  • text:转录标注,格式为uttID 文本内容
  • utt2spk:话语到说话人的映射,格式为uttID 说话人ID
  • spk2utt:说话人到话语列表的映射,格式为说话人ID uttID列表(通常由utt2spk排序生成)

这几个文件必须同步更新。很多人改了wav.scp,却忘了改utt2spk和spk2utt,后面一跑utils/validate_data_dir.sh就会开始各种扯皮。

而spk2utt通常是Kaldi脚本通过utils/utt2spk_to_spk2utt.pl由utt2spk生成的。所以很多情况下,问题根源最后会回溯到utt2spk是否干净。

3. 遇到报错后的完整排查思路

3.1 第一步:做数据目录体检,确认有效说话人数量

收到这个报错时,第一步不是改参数,而是看数据目录当前的真实状态。

先看统计信息:

# 进入数据目录,假设置于 data/train cd data/train # 查看说话人数量 wc -l spk2utt # 查看话语总数 wc -l wav.scp # 查看utt2spk是否完整 wc -l utt2spk # 用Kaldi自带校验脚本 utils/validate_data_dir.sh --no-feats data/train

如果validate_data_dir.sh能顺利通过,说明目录结构本身没问题,那么你的有效说话人确实只有2个。如果校验报错,则需要先顺着提示修复数据目录。

3.2 第二步:确认是否有“假说话人”干扰

还有一种容易被忽略的情况:spk2utt里的说话人ID可能包含大量噪声。比如说话人ID是直接从文件名里截取的,而有些文件名里带了错误的扩展名,或者路径解析时混入了一些特殊字符,导致实际说话人数量统计异常。

此时可以这样快速检查:

# 看看说话人ID长什么样 head -n 10 spk2utt # 统计speaker ID总数量 awk '{print $1}' spk2utt | sort -u | wc -l

如果统计结果仍然为2,那么再检查是不是所有说话人都真的有音频支撑:

# 检查是否有说话人没有任何对应的utt awk '{print $1}' utt2spk | sort | uniq -c | awk '$1==0'

一般来说,只要utt2spk的每个说话人都至少在wav.scp里对应了一条有效条目,问题就锁定在数据量太少。

3.3 第三步:判读到底是真缺数据,还是有无效音频被过滤

有时候你的原始数据里有10个说话人,但某个预处理脚本只跑通了2个人的特征提取,其他8个说话人的特征文件为空或生成失败,后续生成spk2utt时Kaldi会只保留那些有特征成功对应的语音段,所以从结果来看,有效说话人就只剩2个。

这种问题常见于:

  • 音频格式不规范,部分语音解码失败
  • 音频时长过短,在remove_short_utterances阶段被过滤
  • 标注文本为空,在清洗阶段被丢弃

此时需要回到原始音频集合做一次全面检查。如果是时长太短导致的,可以通过查看时长分布来确认:

# 如果已经生成了utt2dur awk '{print $2}' utt2dur | sort -n | tail -n 20

如果发现有大量超短音频,可以调整过滤阈值或者移除过滤步骤,把更多说话人保留下来。

3.4 第二步与第三步的交叉验证逻辑

在实践中,我自己一般会先看wc -l spk2utt,再看validate_data_dir.sh,如果这两个都通过,就跑到原始数据文件夹里数一数有多少个说话人文件夹。如果原始文件夹数量远超2,那一定是中间的过滤逻辑误伤了大量数据。如果原始文件夹确实只有2个,那就不用纠结数据目录问题,直接进入下一节的处理方案。

4. 实操解决:当有效说话人确实太少时怎么办

4.1 方法一:把多个说话人合并为一个“伪说话人”

如果这个报错出现在传统的ASR训练流程里,而且你的任务本身并不关心说话人身份,一个很实用的办法是通过修改utt2spk,把多个真实说话人映射到同一个说话人ID。

比如原始数据只有2个录音人,但每条音频内容差异很大,你想让流程跑通,可以生成合并后的utt2spk:

# 将所有人的utt都归入一个speaker awk '{print $1, "global_speaker"}' data/train/utt2spk > data/train/utt2spk.tmp mv data/train/utt2spk.tmp data/train/utt2spk # 重建 spk2utt utils/utt2spk_to_spk2utt.pl data/train/utt2spk > data/train/spk2utt # 再次校验 utils/validate_data_dir.sh --no-feats data/train

这样就变成了1个说话人,看起来“更少”了,但某些流程确实能继续跑。不过要注意,这种做法只适用于完全不做任何说话人自适应、不做说话人嵌入特征的任务。如果后续训练脚本要基于说话人做统计(比如某些无监督预训练、说话人感知的增强模型),这种合并会让模型学不到区分说话人的能力,性能大打折扣。

这种方法本质上是在骗流程,看起来不太体面,但对于跑通demo或者快速验证发音质量分析等任务,可以救急。

4.2 方法二:把“各个音频片段”视为不同的说话人

另一个更常见的操作是“数据增强 + 会话切分”,把同一个人的不同录音session当成不同说话人。比如原始数据里有2个人,但每个人录了5个不同批次的音频,我们可以用批次信息来生成更细粒度的spkID:

# 假设每个utt id的格式为:sessionX_speakerID_utteranceID awk '{split($1, arr, "_"); print $1, arr[1]"_"arr[2]}' data/train/utt2spk > new_utt2spk

这样如果每个真实说话人有多个session,speaker数量就会成倍增加,超过分割阈值不再是问题。

从学术角度讲,这种做法相当于把“发言人”重定义为“录音会话”,对于某些声学建模任务是合理的,因为同一个说话人在不同session、不同环境下的声学特征本身就有差异。但对于说话人验证任务来说,这就是明显的错误标注,会让评测指标失真。

所以,这个方法要看具体任务来用。在做ASR声学模型训练的数据准备阶段,我经常这么处理采集来源复杂的语音数据。

4.3 方法三:通过数据增强扩充说话人多样性

如果数据量确实少,而且你希望后续模型有更好的泛化性,最值得做的是系统性的数据增强。Kaldi生态里常用的扩充手段包括:

  • 加噪:给原始音频叠加不同类型的背景噪声(如白噪声、环境噪声、音乐噪声)
  • 变速:基于SoX或FFmpeg改变语速,生成快慢变体
  • 音量扰动:随机调整音频增益,模拟不同录音距离和音量

这里给一个基于SoX的增强脚本思路:

# 原始音频目录:orig_wavs # 增强后输出目录:aug_wavs mkdir -p aug_wavs/train/spk1 mkdir -p aug_wavs/train/spk2 # 生成带噪声的样本 for spk in spk1 spk2; do for wav in orig_wavs/train/$spk/*.wav; do base=$(basename "$wav" .wav) # 原始样本 cp "$wav" "aug_wavs/train/$spk/${base}_clean.wav" # 加背景噪声(生成15秒白噪声,再叠加) sox -n -r 16000 -c 1 noise_sn15.wav whitenoise trim 0.0 15.0 sox "$wav" "aug_wavs/train/$spk/${base}_noise.wav" \ noise_sn15.wav vol 0.02 mix # 变速处理:0.9倍速和1.1倍速 sox "$wav" "aug_wavs/train/$spk/${base}_speed0.9.wav" tempo 0.9 sox "$wav" "aug_wavs/train/$spk/${base}_speed1.1.wav" tempo 1.1 # 音量扰动:0.8倍与1.2倍 sox "$wav" "aug_wavs/train/$spk/${base}_vol0.8.wav" vol 0.8 sox "$wav" "aug_wavs/train/$spk/${base}_vol1.2.wav" vol 1.2 done done

生成完增强数据之后,再重新构建整个Kaldi数据目录:

# 生成wav.scp find aug_wavs/train -name "*.wav" | \ awk -F'/' '{id=$NF; sub(/.wav$/, "", id); print id, $0}' | \ sort > data/train/wav.scp # 生成utt2spk:这里从路径里解析说话人id(路径前两级就是spkID) find aug_wavs/train -name "*.wav" | \ awk -F'/' '{id=$NF; sub(/.wav$/, "", id); spk=$3; print id, spk}' | \ sort > data/train/utt2spk # 生成spk2utt utils/utt2spk_to_spk2utt.pl data/train/utt2spk > data/train/spk2utt

增强之后,说话人数量从2变成了至少6(每个真实说话人被拆成clean、noise、speed0.9、speed1.1、vol0.8、vol1.2六个“变体身份”)。这明显让分割脚本满意了。

但注意,如果你把同一个原始说话人的多个变体当成不同说话人,在某些需要真实说话人语义的场景下会有问题。做增强之前先想清楚你的下游任务是否需要真实身份标签。

4.4 方法四:直接调低分割份数

如果只是临时想让流程跑下去,而且并行资源也不太够,可以直接修改脚本调用时传入的--nj参数,把它降到小于等于当前说话人数。比如:

# 原来是 --nj 5 # 改成 --nj 2

但这样做有两个潜在副作用:一是并行度降低,训练速度变慢;二是如果训练脚本内部对最小份数有硬性要求,调低之后可能引发其他错误。比如某些训练脚本要求训练份数必须大于等于某个值,否则会报train_nnet.sh: expected --num-jobs to be at least 1以外的其他错误。

所以,这个方法适合救急,不适合作为长期方案。如果你的目标是工程化落地,数据量迟早要补上来。

5. 实操:如何正确重新组织数据目录让流程跑通

5.1 完整修复流程(以真实项目为蓝本)

下面给出一套我在实际项目X里验证过的修复流程,适用于“数据目录出现问题导致有效说话人数异常减少”的情况。某次我在处理一批真实录制音频时,发现原始文件夹里有12个发音人,但Kaldi数据目录最后只认了1个有效说话人,报错时显示number of speakers 1 is less than the。

排查后发现,生成utt2spk时所用的文件名解析规则出了问题:某些文件名的中间带空格和特殊符号,脚本解析后把多个文件错误地归到同一个speaker。

修复步骤如下:

第一,回到原始数据源头,把文件名统一规范化:

# 把带有空格和特殊符号的文件名改为纯下划线形式 for f in raw_audio/*/*.wav; do dir=$(dirname "$f") base=$(basename "$f") newbase=$(echo "$base" | tr ' ' '_' | tr -d '[]()') mv "$f" "$dir/$newbase" done

第二,按目录层级确定说话人ID。设计目录结构为raw_audio/speakerID/utteranceID.wav,每个speaker一个文件夹:

find raw_audio -name "*.wav" | \ awk -F'/' '{print $3"_"$4, $2, $0}' | \ awk '{print $1, $3, $2}' > data/train/wav_utt2spk_tmp.list

第三,生成wav.scp和utt2spk:

# wav.scp: utteranceID 完整路径 awk '{print $1, $3}' data/train/wav_utt2spk_tmp.list > data/train/wav.scp # utt2spk: utteranceID speakerID awk '{print $1, $2}' data/train/wav_utt2spk_tmp.list > data/train/utt2spk

第四,重建spk2utt:

utils/utt2spk_to_spk2utt.pl data/train/utt2spk > data/train/spk2utt

第五,重新校验并测试分割:

utils/validate_data_dir.sh --no-feats data/train # 手动测试分割成5份 utils/split_data.sh data/train 5

如果此时能顺利生成data/train/split5目录,问题就解决了。

5.2 重新生成特征并重试

修复数据目录后,还需要把特征重新生成一遍,确保feats.scp里没有旧文件的残留:

# 清空旧特征目录 rm -rf data/train/feats.scp data/train/cmvn.scp # 重新提取MFCC steps/make_mfcc.sh --nj 5 data/train steps/compute_cmvn_stats.sh data/train # 再次校验 utils/validate_data_dir.sh data/train

这里有个细节:validate_data_dir.sh在存在特征文件的情况下会检查feats.scp与utt2spk的对应关系,如果特征文件缺失,它也会主动过滤一些无效utt。所以特征生成完成后再跑一次校验才能反映真实状态。

5.3 当你有多个数据来源时的合并策略

实际工程里,很少只靠一个数据源撑起整个训练集。当你手上有多个来源的数据时,可以把它们合并到同一个Kaldi数据目录。需要注意的是:合并之后utt2spk里的说话人ID可能会冲突,比如两个来源都用了spk1这样的命名。

推荐的做法是在生成数据目录之前就给每个来源的说话人ID加上前缀:

# 来源A:spkA_001, spkA_002 # 来源B:spkB_001, spkB_002 # 来源C:spkC_001

前缀保证了全局唯一性,也让后续排查问题时光靠ID就能知道数据来自哪里。我见过不少项目因为ID冲突,合并后说话人被“吞并”,最后报错时只显示极少数说话人,排查半天才发现在合并脚本里ID重了。

6. 常见问题速查与避坑经验

6.1 常见触发场景与对应解法速查表

触发场景典型报错补充信息根本原因推荐解法
只有2个真实说话人,却要分割成10份number of speakers 2 is less than the number of splits 10说话人数低于分割份数增强数据、合并伪说话人、调低nj
12个说话人但最后只统计到2个同上文件名解析异常或过滤逻辑误删检查utt2spk、重新生成映射
音频解码失败导致特征为空同上格式不支持或通道数异常统一转成16k单声道wav
超短音频被过滤掉大量说话人同上时长阈值设置过高查看时长分布、降低阈值
spk2utt与utt2spk不一致同上手动修改其中一个文件后未同步执行utils/utt2spk_to_spk2utt.pl重建

6.2 避坑心得一:永远不要把spk2utt当成独立文件来维护

spk2utt是由utt2spk推导出来的,我见过有人想省事,直接手动改了spk2utt,结果和utt2spk不一致,Kaldi跑着跑着就会很奇怪。最稳妥的姿势永远是:

# 改完utt2spk之后,必须执行这一行 utils/utt2spk_to_spk2utt.pl data/train/utt2spk > data/train/spk2utt # 再跑校验 utils/validate_data_dir.sh data/train

这一条是Kaldi社区里最常见的新手坑,也是这个报错的主要来源之一。

6.3 避坑心得二:分割份数要和资源匹配,不要盲目追求大nj

很多人有个误解,以为--nj越大速度越快。实际上Kaldi的数据分割是按说话人边界来的,如果--nj设置过大,反而可能因为说话人数不足而报错,或者分割后某些份数据量太少,导致训练时某些GPU节点空转。

根据我的经验,在数据量只有几十小时的情况下,--nj设置在4到8之间比较合适。当总说话人数在10到20时,设置--nj=10通常问题不大。如果说话人数上百,再考虑--nj=20或更多。先小步测试,确认分割无误,再去追并行度。

6.4 避坑心得三:用增强手段救急要记录好变体与真实说话人的对应关系

如果使用了变速、加噪等方式把2个说话人扩充成更多“伪说话人”,一定要在项目内部维护一张映射表。不然等模型训练完成、做分析报告时,你会发现所有说话人ID都变了,完全无法回溯到原始录音人。

我的做法是在数据目录下放一个real_speaker_map.txt,每一行:

伪说话人ID 真实说话人ID 增强类型 原始文件

这一张表在后续做错误分析、录制质量分析时非常救命。没有这张表,增强数据只会成为一团乱麻。

6.5 排查这段经验过程中我最大的体会

处理过几次之后,我发现在Kaldi里跑实验,最耗时间的往往不是模型训练本身,而是数据准备阶段这些细枝末节的文件一致性问题。split_scp.pl这个脚本虽然脾气倔,但它实际上在帮你做数据质量检查。它拒绝分割,不是因为它没能力分割,而是它认为你当前的数据条件不适合后续流程。尊重这个判断,先回头整理数据,比强行绕过它要省心得多。

最后再说一个我在实操里养成的小习惯:每次动完数据目录里的任何映射文件,我都会顺手跑一遍validate_data_dir.sh再继续下一步。这个习惯帮我挡掉了至少一半的数据准备阶段报错。如果你现在正被这个报错卡住,按上面的顺序检查一遍数据目录、重新生成spk2utt、再做一次增强或者调整分割份数,大概率就能顺利推进下去。

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

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

立即咨询