简介:面向音乐信息检索(MIR)学习者和人声分析爱好者的一份尖叫声样本数据集,源自 NTHU MIR 课程项目,旨在弥补极端人声在 MIR 研究中关注不足的问题,可用于音频特征提取、分类建模与声音事件分析等练习。压缩包共 63 个文件,约 4.83MB,包含 48 个标注文本、8 张特征可视化图、3 个 Python 脚本、2 个 CSV 数据表及 PDF 报告和说明文档,覆盖样本标注、特征统计、SVM 基线分类与结果对比等环节。数据集从 16 首歌曲中切分出 565 个尖叫片段,便于读者直接开展特征提取与分类实验,也能借助可视化图和报告理解实验设计、评估流程与局限。目前已有 719 人学习,适合具备一定 Python 与音频处理基础、希望快速进入极端人声研究或课程复现的读者。
1. 尖叫声音数据集能解决什么:从一段音频到可训练样本
做音频事件检测的人多半碰过这种尴尬:模型对正常说话声判断得很准,一遇到尖叫、喊叫这类高能量短促音就开始翻车,要么漏报,要么把拖长的元音当成异常。根子不在分类器,而在训练样本——通用语音数据集里尖叫片段占比极低,标签还粗,模型压根没见过足够的正样本。这份尖叫声音样本数据集就是冲这个缺口来的:它把尖叫与人声、环境噪声切成独立音频片段,正负样本分开存放,配套的思路可以直接接进 Python 的 SVM 分类器做二分类。适合做异常声音告警、园区音频监测,以及想拿真实音频练手特征工程的从业者。它不是开箱即训的成品,采样率、时长、标签分布都得先摸一遍,下面按拆包到验证的顺序走。
2. 数据集结构与音频规格:拆包先看采样率、时长与标签分布
拿到压缩包别急着写模型。音频数据集最常见的翻车点,是训练跑完指标漂亮,换一段现场录音就崩,回头查才发现正负样本来自不同录音设备,采样率和增益都不一致。所以第一步永远是把文件系统扫一遍,把标签、分组、格式、时长这几个维度先统计清楚,再决定特征怎么提。
2.1 目录组织与标签解析
发布方常见的组织方式是按类别分文件夹,比如尖叫一个目录、人声和噪声各一个目录;也有按采集场景分一级、按类别分二级的。标签能不能从路径直接推断,决定后面要不要单独维护一张映射表。我一般先写个扫描脚本把结构打印出来,而不是靠肉眼在文件管理器里数。
from pathlib import Path from collections import Counter AUDIO_EXT = {".wav", ".flac", ".ogg", ".mp3", ".m4a"} def scan_dataset(root): root = Path(root) records = [] for p in root.rglob("*"): if p.suffix.lower() not in AUDIO_EXT: continue rel = p.relative_to(root) parts = rel.parts # 第一层目录当作粗标签,适配「一类一个文件夹」的常见发布习惯 label = parts[0] if len(parts) > 1 else "unknown" # 第二层目录留作分组键,比如说话人、采集场景、设备编号 group = parts[1] if len(parts) > 2 else label records.append({"path": str(p), "label": label, "group": group}) return records recs = scan_dataset("./eva_audio") print(Counter(r["label"] for r in recs)) print(Counter(r["group"] for r in recs))逻辑说明:rglob递归遍历整个目录树,后缀过滤掉说明文档、标注表和可能混进来的图片。标签取相对路径第一段,是为了兼容大多数按类别分目录的发布方式;group取第二段,作用是后面做分组交叉验证,避免同一段原始录音切出来的多个片段同时落进训练集和验证集——这种泄漏在音频数据集里极其常见,指标能虚高十几个点。
参数说明:AUDIO_EXT按实际格式增删,如果包里混了.aac要补上,否则会静默漏样本;group在目录只有一层时会退化成label,这时候分组交叉验证就等价于普通分层抽样,得自己另找分组依据,比如从文件名前缀里解析设备号。
统计结果出来后重点看两件事:正负样本比例,以及每个分组的样本数。如果尖叫类只占一成不到,后面 SVM 的class_weight就必须开;如果某个分组的样本数只有个位数,交叉验证的折数要相应减少,不然某一折验证集里可能一个正样本都没有。
2.2 音频参数统一与静音裁剪
采样率不统一是音频数据集的头号坑。有的片段 44.1k,有的 16k,直接提 MFCC 维度虽然一样,但频率轴根本不对齐,模型学到的其实是采样率差异而不是尖叫特征。我一般统一重采样到 16k、单声道、PCM 16bit。尖叫的能量主要集中在 1k 到 4k 之间,16k 采样率的上限 8k 完全够覆盖,再高只是徒增计算量。
import librosa import numpy as np import soundfile as sf SR = 16000 MIN_DUR = 0.3 # 低于 300ms 的片段信息量不足,直接丢弃 MAX_DUR = 5.0 # 超过 5s 的做中心裁剪,避免长尾样本主导训练 def load_and_fix(path, sr=SR): y, _ = librosa.load(path, sr=sr, mono=True) # 去直流偏移,否则 MFCC 第 0 维会被整体抬高,掩盖能量差异 y = y - np.mean(y) # 静音裁剪,top_db 是相对峰值门限,给太松会把尖叫尾音削掉 y, _ = librosa.effects.trim(y, top_db=30) dur = len(y) / sr if dur < MIN_DUR: return None if dur > MAX_DUR: start = (len(y) - int(MAX_DUR * sr)) // 2 y = y[start:start + int(MAX_DUR * sr)] # 峰值归一化,消除不同录音设备的增益差异 peak = np.max(np.abs(y)) + 1e-8 y = y / peak * 0.95 return y def export_unified(recs, out_dir): Path(out_dir).mkdir(parents=True, exist_ok=True) kept = 0 for r in recs: y = load_and_fix(r["path"]) if y is None: continue name = f"{r['label']}_{kept:05d}.wav" sf.write(Path(out_dir) / name, y, SR, subtype="PCM_16") kept += 1 return kept逻辑说明:librosa.load直接传sr让内部完成重采样,比先读原文件再调resample少一次内存拷贝。去直流和峰值归一化是为了让不同设备录的样本落在同一量纲上,否则 SVM 的 RBF 核会被大幅值特征主导,标准化也救不回来。统一导出成 wav 而不是每次训练时现场解码,是为了一次性把解码开销和格式兼容问题解决掉,后面调特征、调参都只读这一份。
参数说明:top_db=30比默认的 60 激进得多,原因是尖叫的起音和尾音衰减很快,门限太松会把有效段当成静音削掉;但如果数据集里本身噪声底很高,30 可能把整段裁没,这时候要回调到 35 到 40 之间,先抽十条样本看一眼裁前裁后的时长比再定。MIN_DUR和MAX_DUR不要拍脑袋,先跑一遍时长直方图:正常语音片段大多在 1 到 3 秒,尖叫往往更短更集中,如果MIN_DUR定到 0.5 秒,可能把最有判别力的短促尖叫全滤掉了。
提示:导出阶段顺手把原始路径和导出文件名的对应关系写成 csv。后面发现某类样本被误裁,能凭这张表回溯到原文件,不至于整批重跑。
3. 特征工程:MFCC、过零率与谱对比度怎么算
SVM 吃的是定长向量,而音频是变长序列,中间这层映射就是特征工程。选得好,几十维特征加 RBF 核就能跑出可用结果;选得糙,堆到几百维也只是把噪声喂给模型。尖叫的判别信息不在单帧,而在能量突变和频谱宽带分布上,所以帧级特征之后必须做统计池化。
3.1 用 librosa 抽帧级特征并池化
帧长 25ms、帧移 10ms 是音频分类里比较稳的起点,既能捕捉起音的瞬态,又不至于让帧数爆炸。特征组合我一般用 13 维 MFCC 加一阶差分,再叠加过零率、谱质心、谱带宽、谱滚降、谱对比度和 RMS 能量。MFCC 抓音色包络,一阶差分抓变化速度,后几个抓频谱形状和能量,正好对应尖叫「高频多、能量陡、变化快」的三个特点。
import numpy as np import librosa N_MFCC = 13 HOP = 160 # 10ms @ 16k WIN = 400 # 25ms @ 16k def extract_features(y, sr=16000): mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=N_MFCC, n_fft=512, hop_length=HOP, win_length=WIN) d_mfcc = librosa.feature.delta(mfcc, width=9) zcr = librosa.feature.zero_crossing_rate(y, frame_length=WIN, hop_length=HOP) cen = librosa.feature.spectral_centroid(y=y, sr=sr, n_fft=512, hop_length=HOP) bw = librosa.feature.spectral_bandwidth(y=y, sr=sr, n_fft=512, hop_length=HOP) roll = librosa.feature.spectral_rolloff(y=y, sr=sr, n_fft=512, hop_length=HOP, roll_percent=0.85) contrast = librosa.feature.spectral_contrast(y=y, sr=sr, n_fft=512, hop_length=HOP) rms = librosa.feature.rms(y=y, frame_length=WIN, hop_length=HOP) feats = np.vstack([mfcc, d_mfcc, zcr, cen, bw, roll, contrast, rms]) # 帧级统计池化:均值刻画整体分布,标准差刻画调制强度 return np.concatenate([feats.mean(axis=1), feats.std(axis=1)])逻辑说明:池化同时取均值和标准差,是因为尖叫和普通语音的差别很多时候不在平均值上,而在波动幅度上——尖叫的能量包络起伏远大于平稳说话声,标准差这一半特征贡献的判别力往往不比均值小。用vstack把不同函数返回的二维特征堆起来,保证每一行是一个特征、每一列是一帧,最后按行做统计,维度才对得上。
参数说明:n_fft=512对应 32ms 窗,和win_length=400搭配是为了留出补零空间,帧边界更平滑;roll_percent=0.85表示累计能量到 85% 处的频率,尖叫的高频成分重,这个值会比普通语音明显偏高,如果数据里尖叫偏沉闷可以调到 0.9 增强区分度。维度算下来是 (13+13+1+1+1+1+7+1)×2 = 76 维,样本量在几百到几千级别时这个维度比较合适,再高就要考虑降维。
另外提醒一句,spectral_contrast默认返回 7 个频带,如果n_fft或采样率改过,频带数可能变化,拼特征前最好用feats.shape打一行日志确认维度,不然训练时才会报维度不匹配,排查起来很费时间。
3.2 特征标准化与防泄漏
标准化这件事看着简单,实际是音频分类里最容易埋雷的地方。常见错误的写法是先对全量数据fit_transform,再切分训练验证,特征均值里已经混进了验证集的信息,交叉验证分数会虚高。正确做法是把StandardScaler塞进 Pipeline,让它在每一折内部只 fit 训练部分。
from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC from sklearn.model_selection import cross_val_score, StratifiedKFold pipe = Pipeline([ ("scaler", StandardScaler()), # 每折内部单独 fit,避免泄漏 ("svm", SVC(kernel="rbf", class_weight="balanced", random_state=42)), ]) cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores = cross_val_score(pipe, X, y, cv=cv, scoring="f1")逻辑说明:Pipeline 保证fit时 scaler 只看到当折的训练数据,predict时用同一组均值和方差变换验证数据,这是交叉验证可信的前提。StratifiedKFold保证每折的正负比例和全集一致,小样本下这点尤其重要,否则可能出现某一折验证集里几乎没有正样本,F1 直接失效。
参数说明:shuffle=True配合固定random_state保证结果可复现,但要注意如果数据本身按时间或设备排过序,随机打乱反而会把同源样本打散到各折,这时候应该改用GroupKFold并按group字段分组。scoring="f1"而不是默认的 accuracy,理由在下一章展开。
注意:如果先做了上采样或 SMOTE 再进交叉验证,必须把采样步骤也放进 Pipeline,否则合成样本会跨折泄漏,分数同样不可信。
4. SVM 分类器:核函数选择与参数搜索
特征定下来之后,模型这层反而简单。几十维的音频统计特征、样本量在千级以内,SVM 是非常合适的选择:决策边界清楚、小样本下不容易过拟合、调参维度只有两个。真正需要花心思的是核函数和参数区间怎么定,以及不平衡数据下的阈值怎么选。
4.1 用 Pipeline 搭 SVM 并做网格搜索
RBF 核在这类问题上通常比线性核稳,因为尖叫和普通语音的边界在特征空间里往往不是超平面能切开的。搜索空间先粗后细,C 从 0.1 扫到 100,gamma 从scale扫到 1,五折分层交叉验证配合 F1 打分。
from sklearn.model_selection import GridSearchCV from sklearn.svm import SVC from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.model_selection import StratifiedKFold pipe = Pipeline([ ("scaler", StandardScaler()), ("svm", SVC(kernel="rbf", class_weight="balanced", probability=True, random_state=42)), ]) param_grid = { "svm__C": [0.1, 1, 10, 100], "svm__gamma": ["scale", 0.01, 0.1, 1], } cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) grid = GridSearchCV(pipe, param_grid, scoring="f1", cv=cv, n_jobs=-1, verbose=1, return_train_score=True) grid.fit(X, y) print(grid.best_params_, grid.best_score_)逻辑说明:probability=True会让 SVM 内部多做一轮交叉验证来拟合概率输出,训练时间大约翻倍,但后面调阈值和滑动窗口聚合都依赖predict_proba,这笔开销值得。return_train_score=True是为了看训练分和验证分的差距,差距过大说明 C 或 gamma 选得太激进,过拟合了。
参数说明:C 控制间隔宽度和容错,C 越大越倾向于把所有训练点分对,小样本下很容易过拟合;gamma 是 RBF 核的影响半径,值越大单个样本影响范围越小,边界越曲折。"scale"等于 1/(特征数×方差),是小样本下的合理默认值。搜索结果出来后不要直接取最优,把 C 和 gamma 的二维分数表打出来看一眼,如果最优解落在网格边缘,说明区间没覆盖到,得往外扩。
如果样本量过千,网格搜索的全量组合会跑很久,我一般先用RandomizedSearchCV抽几十组定位大致区间,再在小范围里做精细网格,能省掉大半时间。
4.2 类别不平衡与阈值调整
class_weight="balanced"解决的是训练时的权重倾斜,但预测时默认的 0.5 阈值仍然偏向多数类。尖叫检测这类场景,漏报的代价通常高于误报,工作点应该往召回方向压。做法是在验证集上取predict_proba,扫一遍阈值,画 PR 曲线找 F1 最大的位置。
import numpy as np from sklearn.metrics import precision_recall_curve proba = grid.best_estimator_.predict_proba(X_val)[:, 1] prec, rec, thr = precision_recall_curve(y_val, proba) # thr 比 prec/rec 少一个元素,索引要错开一位对齐 f1 = 2 * prec * rec / (prec + rec + 1e-8) best_idx = int(np.argmax(f1[:-1])) print(f"threshold={thr[best_idx]:.3f} precision={prec[best_idx]:.3f} " f"recall={rec[best_idx]:.3f} f1={f1[best_idx]:.3f}")逻辑说明:precision_recall_curve返回的阈值数组长度比精确率、召回率少一,因为最后一个点对应的是全部判正和全部判负两个极端,没有阈值。用f1[:-1]对齐阈值数组,否则argmax取到的索引会错位,选出来的阈值偏小,实际部署时召回虚高。
参数说明:如果业务上更怕误报,就把选择准则从 F1 换成满足召回下限的前提下取精确率最高的点,写成循环判断即可。选出来的阈值要固化进配置文件,不要每次推理都重新扫——重扫的前提是输入分布没变,而现场音频的分布和验证集往往不完全一致,频繁重扫会让告警行为忽松忽紧。
提示:验证集上定好的阈值,上线后第一周建议记录实际打分分布,和验证集对比一下。如果中位数明显偏移,说明录音链路和训练数据不一致,先查增益和采样率,别急着调阈值。
5. 常见问题排查:静音污染、标签混叠与过拟合
前四章走完,模型通常能跑出一个不错的交叉验证分数,但真正的血泪经验都集中在这一步:分数好不代表能用。下面几条是我在音频数据集上反复踩过的坑,按现象、原因、解决三段说清楚,遇到同类问题时可以对着排查。
5.1 数据层面的三个坑
现象一:验证集 F1 很高,现场录音几乎全判成尖叫。原因多半是负样本里混了大量低能量静音片段,模型学到的其实是「有没有声音」而不是「是不是尖叫」,一旦现场有持续底噪,静音这个捷径失效,全部往正类倒。解决办法是统计每个样本裁剪后的有效时长和 RMS 均值,把 RMS 低于全集 5 分位的负样本剔掉,同时补一批带底噪的真实负样本进去。判断标准很简单:如果负样本的平均 RMS 只有正样本的三分之一以下,这个数据集就有污染嫌疑。
现象二:同一段原始录音切出来的片段,一半在训练集一半在验证集。这是音频数据集最隐蔽的泄漏,交叉验证分数能虚高十到二十个点,上线后原形毕露。原因在于发布方常按固定长度切片,片段之间高度相关。解决办法是按文件名前缀或目录里的分组信息做GroupKFold,保证同一来源的所有片段落在同一折。第 2 章扫描脚本里那个group字段就是干这个用的,别嫌麻烦,一定要填。
现象三:训练时维度报错或频率轴对不上。原因是有少数文件的采样率或声道数没被统一,重采样漏了。解决办法是在导出阶段加一行断言,assert sr == 16000,并且对每个文件打印原始librosa.get_samplerate的返回值做一次聚合计,出现非 16k 的直接列出来单独处理。不要指望librosa.load静默处理一切,它对某些容器格式的重采样行为并不可靠。
5.2 训练层面的三个坑
现象四:训练集 F1 接近 1,验证集只有 0.6 左右。典型的过拟合,原因通常是 C 或 gamma 选太大,加上特征维度相对样本量偏高。解决办法是把网格往小值方向扩,C 从 100 降到 1 甚至 0.1 再试一遍,同时看return_train_score的训练分和验证分差距。差距超过 0.15 就要警惕。另一个手段是砍掉贡献小的特征组,比如先只留 MFCC 加 RMS 跑一版基线,再逐个加回其他特征,看验证分是否真的提升。
现象五:只报 accuracy,看起来 90% 以上,实际一个正样本都没检出来。正负比例 9:1 的时候,全判负也能拿到 90% 的准确率,这个指标在异常检测里基本没有参考价值。解决办法是固定用 F1、召回率、PR 曲线三件套汇报,混淆矩阵也要打出来,看清楚错误集中在哪个方向。如果召回高但精确率极低,多半是负样本不够多样;反过来精确率高召回低,通常是阈值定高了或者正样本特征覆盖不全。
现象六:本地验证挑好的阈值,部署到另一批设备上完全失效。原因是各设备的录音增益和频响不同,特征分布整体偏移,固定的概率阈值失去意义。解决办法有两条:一是推理前做和第 2 章一致的峰值归一化与重采样,保证输入分布尽量对齐;二是把绝对阈值换成相对策略,比如对一段长音频做滑动窗口,按窗口打分的分位数判定,而不是对单窗概率卡死。前者治本,后者兜底,通常两个一起上。
注意:排查顺序建议从数据到特征再到模型,别一上来就怀疑分类器。音频任务里九成的问题出在数据侧,把时长分布、RMS 分布、采样率分布三张图打出来,很多问题看一眼就清楚了。
6. 验证与进阶:混淆矩阵、分组交叉验证与滑窗推理
模型在验证集上跑出分数只是起点,真正决定能不能用的是三件事:验证方式是否可信、错误集中在哪、以及怎么把片段级的模型用到连续音频上。分组交叉验证前面提过,这里补一个实操细节——GroupKFold的折数受最小分组样本数限制,如果某个分组只有两条样本,折数开 5 就会报错,稳妥做法是先统计分组样本数的分布,折数取最小分组样本数,通常落在 3 到 5 之间。
混淆矩阵要按分组维度拆开看,而不是只看总体。同一批数据里,来自安静环境和嘈杂环境的负样本错误率可能差好几倍,总体混淆矩阵会把这个问题抹平。我的习惯是跑完交叉验证后,把每折的错误样本路径导出来,按分组字段聚合,哪一组错误率高,就回去查那一组的录音条件。这一步花十分钟,往往比调半天参数有用。
滑动窗口推理是把片段级模型用起来的关键。长音频先按固定窗长切,窗移取窗长的一半保证重叠,每窗提特征打一个概率,最后用中位数或分位数聚合,避免单窗误判导致整段告警。
import numpy as np import librosa def sliding_predict(model, path, sr=16000, win_sec=2.0, hop_sec=1.0): y, _ = librosa.load(path, sr=sr, mono=True) y = y / (np.max(np.abs(y)) + 1e-8) * 0.95 win, hop = int(win_sec * sr), int(hop_sec * sr) scores = [] for start in range(0, max(len(y) - win, 0) + 1, hop): seg = y[start:start + win] if len(seg) < win * 0.5: # 尾部不足半窗直接丢弃 continue seg = np.pad(seg, (0, win - len(seg))) # 不足一窗右侧补零 feat = extract_features(seg, sr).reshape(1, -1) scores.append(model.predict_proba(feat)[0, 1]) if not scores: return 0.0 # 取 90 分位而非均值,短促尖叫不会被长段静音稀释 return float(np.percentile(scores, 90))逻辑说明:窗长 2 秒、窗移 1 秒是折中值,窗太短特征统计不稳定,太长则短促尖叫会被稀释。聚合用 90 分位而不是均值,是因为尖叫在整段音频里往往只占很小比例,均值会被大量低分窗拉平,分位数能保住峰值信息。补零而不是丢弃尾部短窗,是为了不遗漏恰好落在结尾的尖叫。
参数说明:hop_sec取窗长一半是常见做法,追求更高时间分辨率可以降到 0.25 秒,代价是推理耗时翻倍;分位数从 90 调到 95 会更保守,误报减少但可能漏掉强度偏弱的尖叫,按业务容忍度定。
最后说个教训。早些年我做过一版异常声音检测,交叉验证 F1 到 0.92,兴冲冲部署到现场,第一周误报率高得离谱。回头查了两天才发现,验证集里的负样本全是安静办公室录的,现场却是带空调底噪的走廊,特征分布压根不是一回事。从那以后我每次拿到音频数据集,都强制先跑一遍时长、RMS、采样率三张分布图,并且坚持按来源分组做交叉验证,宁可分数难看一点,也不要一个骗自己的数字。希望帮到你。
本文还有配套的精品资源,点击获取