第4章 音频人工智能:让模型识别声音
本章技术主题: 音频分类——把连续声音转换为可学习的频谱特征。
本章技术实验: 训练声音分类模型,观察频谱、类别分数和未知状态。
家庭项目: “制作一个家庭声音提醒器”是本章主项目。
跨章位置: 保存声音模型、阈值和独立测试,第12章可把它接入低风险互动反馈。
技术主线: 声音片段 → 波形 → 频谱图 → 模型训练 → 类别概率 → 测试与使用判断。
学习目标
学完本章,你将能够:
- 说明采样率、声音窗口、波形和频谱图分别表示什么;
- 完成声音样本采集、标签确认、训练集与测试集划分,并运行一个Keras/TensorFlow声音分类模型;
- 解释模型为什么输出类别概率,以及
other类别和unknown状态为何不同; - 用噪声、距离、陌生声音和故障文件测试模型,比较误报、漏报与人工复核量;
- 判断一个课堂声音分类器适合做什么、不适合做什么。
项目导入
声音分类是音频人工智能的一项基础任务。它不要求模型把声音完整转写成文字,而是判断一段声音更接近哪些已知类别。例如,家庭环境中的门铃、烧水提示和宠物叫声,教室环境中的上课铃、掌声和桌椅移动声,都可以成为分类对象。
声音并不是一个稳定不变的符号。同一个门铃在近处和隔门处的音量不同;同一个上课铃在安静教室和走廊噪声中具有不同背景;手机提示音还可能与门铃相似。如果训练材料只来自一个设备、一个距离和一个房间,模型可能记住录音条件,而没有学会需要识别的声音模式。
本章正式项目是一个低风险的“家庭声音提醒器”。它默认在本地读取已获授权的一秒WAV文件并完成模型推理,输出各类别分数以及“已知类别”或“未知”的判断,再把结果记录下来;原始录音不上传。作品不进行连续监听,不保存谈话内容,不连接门锁、电源、炉具和报警设备,也不替代火灾报警器等专业安全装置。
本章成果
- 一个可以训练、测试和推理的本地声音分类程序;
- 一张“声音—波形—频谱图—模型—类别概率—规则—人”的原理图;
- 一份覆盖正常、边界、陌生和故障声音的测试记录;
- 一次只改变置信度阈值的对照实验;
- 一张说明适用环境、误报漏报和停止条件的使用判断卡。
考点提示
本章不要求推导傅里叶变换公式,但要能解释声音为什么需要采样、为什么要切成固定窗口,以及频谱图怎样同时表示时间和频率。还要分清训练与推理、类别分数与现实正确性、模型输出与应用动作。训练集、验证集和测试集应按录制批次或环境分开;同一长录音切出的相邻片段不能分别放入训练集和测试集,否则会造成数据泄漏。
第一节 采集声音并训练第一个声音分类模型
一、把声音任务定义为可检验的分类问题
首版任务使用四个已知类别:doorbell(门铃)、kettle(烧水提示)、class_bell(教室铃声)和other(其他常见声音)。每个输入都是一秒、16 kHz、单声道WAV。模型输出四个类别的相对分数,普通程序再根据最高分和前两名分差决定接受该类别还是输出unknown。
other与unknown不能混为一谈。other是训练时已经收集和标注的类别,例如普通敲击声和拉椅子声;unknown不是一个训练标签,而是应用在证据不足时作出的拒绝判断。强迫模型把每段声音都分到四个已知类别,会使陌生声音也得到一个看似确定的名称。
最小成功样例是:输入独立录制的清晰门铃,程序显示波形、频谱图和全部类别分数;输入拍手、混合声或分数接近的声音时,程序保留分数并输出unknown。项目只记录低风险事件,不因任何分类结果自动控制设备。
二、采集、标注和划分声音数据
数据目录按用途和标签组织:
ch04-sound-project/
├─ input/audio/
│ ├─ train/doorbell|kettle|class_bell|other/
│ ├─ valid/doorbell|kettle|class_bell|other/
│ ├─ test/doorbell|kettle|class_bell|other/
│ └─ target.wav
├─ src/main.py
├─ tests/cases.json
├─ output/
└─ evidence/dialogue.md、change.diff、regression.md、decision-card.md
每个样本的数据卡至少记录类别、录制批次、房间、距离、设备、背景声和授权情况。训练集用于调整模型参数,验证集用于观察训练过程和选择配置,测试集只在模型和规则基本确定后使用。划分时应把同一录制时段或同一长录音的切片整体放在一侧,不能随机拆到训练和测试两边。
四类数量应大致均衡,并尽量覆盖近距离、远距离、不同音量和不同背景。标签不确定的声音进入“待确认”区,不应为了凑数量随意归类。涉及谈话、姓名或其他可识别个人的信息不进入课堂数据集。
采集数量相同也不代表数据已经平衡。如果门铃样本全部在安静近距离录制,而other样本都来自嘈杂走廊,模型可能把背景安静程度当成类别线索。检查数据时要同时比较数量、设备、距离和背景分布,并保留至少一个训练阶段没有出现的环境作为泛化测试。
三、与AI协同形成首版工程
本章的应用中的AI是声音分类模型,它在程序运行时接收频谱图并输出类别分数。开发与学习AI帮助学生澄清数据格式、生成和解释程序、分析错误日志、提出限定修改;它不替学生决定录音是否获得授权、标签是否正确以及错误后果是否可以接受。
与开发与学习AI协同时,可以先提交下面的任务说明:
目标:把一秒WAV分为doorbell、kettle、class_bell、other;证据不足时输出unknown。
数据:train、valid、test来自不同录制批次,target.wav用于单次观察。
格式:16 kHz单声道;不足一秒补零,超过一秒截取。
实现:Python与Keras/TensorFlow;显示波形、频谱图和全部类别分数。
边界:只记录低风险事件,不连续监听,不连接门锁、电源和报警器。
验收:保存模型、测试明细、观察图和结果JSON;测试静音、陌生声、混合声和坏文件。
请先检查数据泄漏、标签顺序和测试划分,再给出完整程序。
自然语言说明不是运行证据。学生仍需检查AI生成代码是否真正使用独立测试集、预处理是否一致、标签顺序是否固定,并亲自运行程序保存首轮结果。
第二节 从波形、频谱图到类别概率
一、声音如何变成模型可以处理的数据
麦克风把空气振动转换成随时间变化的数值。采样率16 kHz表示每秒记录16000个振幅值。一秒声音因此可以表示为长度16000的一维波形。波形容易观察声音何时出现、持续多久和大致强弱,却不容易直接分辨音色。
程序把波形切成许多有重叠的短窗口,再计算每个窗口中不同频率成分的强弱,得到频谱图。频谱图的横向表示时间,纵向表示频率区间,每个位置的数值表示该时间和频率附近的能量。对幅度作对数变换,可以让较弱但有辨别价值的成分更容易观察。
频谱图是一种二维表示,因此可以用小型卷积网络学习局部形状。卷积层寻找短促提示音、持续嗡鸣或重复节奏等模式,最后的Softmax层为四个已知类别分配相对分数。最高分只表示模型在当前标签集合中更偏向该类,并不表示现实中一定是该声音。
二、完整核心程序
下面的src/main.py读取三个独立数据目录,完成频谱转换、模型训练、验证、测试、单个目标声音推理和结果保存。运行前需在项目环境中安装TensorFlow、NumPy和Matplotlib,并按前述目录放入WAV文件。
from pathlib import Path
import json
import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf
ROOT = Path(__file__).resolve().parents[1]
AUDIO = ROOT / "input" / "audio"
OUT = ROOT / "output"
OUT.mkdir(exist_ok=True)
RATE = 16000
LENGTH = 16000
SEED = 42
CONFIDENCE = 0.70
MARGIN = 0.15
NAMES = ["class_bell", "doorbell", "kettle", "other"]
tf.keras.utils.set_random_seed(SEED)
def load_wave(path):
"""读取16 kHz单声道WAV,并统一为一秒。"""
wave, rate = tf.audio.decode_wav(
tf.io.read_file(path), desired_channels=1
)
tf.debugging.assert_equal(rate, RATE, message="采样率必须为16 kHz")
wave = tf.squeeze(wave, axis=-1)[:LENGTH]
wave = tf.pad(wave, [[0, LENGTH - tf.shape(wave)[0]]])
wave.set_shape([LENGTH])
return wave
def spectrogram(wave):
"""把一维波形转换为对数频谱图。"""
value = tf.signal.stft(
wave, frame_length=400, frame_step=160, fft_length=512
)
return tf.math.log1p(tf.abs(value))[..., tf.newaxis]
def make_dataset(split, shuffle=False):
paths, labels = [], []
for label, name in enumerate(NAMES):
files = sorted((AUDIO / split / name).glob("*.wav"))
if not files:
raise ValueError(f"{split}/{name}中没有WAV文件")
paths.extend(str(path) for path in files)
labels.extend([label] * len(files))
dataset = tf.data.Dataset.from_tensor_slices((paths, labels))
if shuffle:
dataset = dataset.shuffle(len(paths), seed=SEED)
dataset = dataset.map(
lambda path, label: (spectrogram(load_wave(path)), label),
num_parallel_calls=tf.data.AUTOTUNE,
)
return dataset.batch(16).prefetch(tf.data.AUTOTUNE)
def choose(scores, confidence=CONFIDENCE, margin=MARGIN):
order = np.argsort(scores)[::-1]
first, second = int(order[0]), int(order[1])
top = float(scores[first])
gap = float(scores[first] - scores[second])
decision = NAMES[first] if top >= confidence and gap >= margin else "unknown"
return decision, top, gap
def audit(model, dataset):
rows = []
for features, labels in dataset:
predictions = model.predict(features, verbose=0)
for scores, truth in zip(predictions, labels.numpy()):
decision, top, gap = choose(scores)
rows.append(
{
"truth": NAMES[int(truth)],
"decision": decision,
"top_score": top,
"top_two_margin": gap,
}
)
return {
"correct": sum(row["decision"] == row["truth"] for row in rows),
"wrong": sum(
row["decision"] not in {row["truth"], "unknown"} for row in rows
),
"unknown": sum(row["decision"] == "unknown" for row in rows),
"rows": rows,
}
train = make_dataset("train", shuffle=True)
valid = make_dataset("valid")
test = make_dataset("test")
input_shape = tuple(spectrogram(tf.zeros(LENGTH)).shape)
model = tf.keras.Sequential(
[
tf.keras.layers.Input(input_shape),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Conv2D(16, 3, activation="relu"),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.Conv2D(32, 3, activation="relu"),
tf.keras.layers.GlobalAveragePooling2D(),
tf.keras.layers.Dense(len(NAMES), activation="softmax"),
]
)
model.compile(
optimizer="adam",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)
model.fit(train, validation_data=valid, epochs=10, verbose=2)
_, test_accuracy = model.evaluate(test, verbose=0)
model.save(OUT / "sound_model.keras")
test_report = audit(model, test)
test_report["test_accuracy"] = float(test_accuracy)
(OUT / "test_decisions.json").write_text(
json.dumps(test_report, ensure_ascii=False, indent=2), encoding="utf-8"
)
target = load_wave(str(AUDIO / "target.wav"))
target_spec = spectrogram(target)
scores = model.predict(target_spec[tf.newaxis, ...], verbose=0)[0]
decision, top, gap = choose(scores)
figure, axes = plt.subplots(2, 1, figsize=(8, 5))
axes[0].plot(np.arange(LENGTH) / RATE, target.numpy())
axes[0].set(title="Waveform", xlabel="Time (s)", ylabel="Amplitude")
axes[1].imshow(
tf.squeeze(target_spec).numpy().T, origin="lower", aspect="auto"
)
axes[1].set(title="Log spectrogram", xlabel="Time frame", ylabel="Frequency bin")
figure.tight_layout()
figure.savefig(OUT / "target.png", dpi=150)
plt.close(figure)
result = {
"probabilities": {name: float(score) for name, score in zip(NAMES, scores)},
"top_score": top,
"top_two_margin": gap,
"decision": decision,
"test_accuracy": float(test_accuracy),
"notice": "只用于低风险声音分类学习,不替代安全报警装置。",
}
(OUT / "result.json").write_text(
json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8"
)
print(json.dumps(result, ensure_ascii=False, indent=2))
阅读程序时先沿数据形状追踪:WAV成为(16000,)波形,波形成为二维频谱图,批量频谱图进入模型,模型输出四个分数。CONFIDENCE和MARGIN位于模型之外,它们不改变模型参数,只决定应用何时接受候选。test_decisions.json保留每个测试声音的真实标签、最终判断、最高分和分差,便于以后使用同一批样本比较规则。
三、只改变置信度阈值
保持模型、测试集、预处理和分差规则不变,只把CONFIDENCE从0.55改为0.80。运行前先预测:提高阈值通常减少确定分类和误报,但可能增加未知状态或漏报。运行后记录正确判断、错误判断、未知数量,以及每个变化样本的具体声音条件。
阈值取舍必须联系错误后果。把桌椅声误报成上课铃会产生不必要提醒;把一次低风险门铃判为未知会失去辅助提醒。两种错误都需要记录,但代价可能不同。高风险场景不能通过“继续调阈值”转交给本课堂模型负责。
这个实验只说明同一个模型在两种接受规则下的表现,不能证明阈值0.80普遍优于0.55。如果同时更换模型、声音和预处理,就不能把输出差异归因于阈值。
第三节 测试噪声、距离、陌生声音与误报漏报
一、建立能够暴露失败的测试集
只测试安静环境中的清晰声音,会高估作品能力。固定测试集应覆盖:
| 类型 | 示例 | 观察重点 | 期望行为 |
|---|---|---|---|
| 正常 | 独立批次的清晰门铃或上课铃 | 基本分类是否可完成 | 输出全部分数并给出候选类别 |
| 边界 | 隔门、低音量、只有半段提示音 | 分数是否接近阈值 | 证据不足时输出unknown |
| 陌生 | 拍手、钥匙碰撞、未训练手机铃声 | 是否被强行归类 | 保留分数,不触发确定提醒 |
| 故障 | 损坏WAV、错误采样率、文件缺失 | 能否安全停止 | 显示原因,不生成事件结论 |
还应把同一目标声放在近距离、远距离、不同房间和背景噪声下测试。测试材料必须来自独立录制批次,不能把为调试反复使用的声音继续称为“从未见过的测试证据”。
误报是目标事件没有发生却给出目标提醒;漏报是目标事件发生却没有相应提醒。混淆矩阵可以显示错误从哪个类别流向哪个类别,精确率回答“报出的目标中有多少是真的”,召回率回答“真实目标中有多少被发现”。样本很少时,应同时报告实际错误条数,不用一个百分数制造普遍性。
二、根据错误进行一次限定修改
首版只依据最高分是否超过阈值。一段门铃与电视声混合的声音得到doorbell=0.61、other=0.57,仍被确定为门铃。问题不在于最高分过低,而在于前两类几乎无法区分。可以要求开发与学习AI只修改“分数转判断”的函数:
实际行为:混合声最高两类分数接近,仍输出doorbell。
期望行为:最高分达到阈值且前两名差值不少于0.15时才接受,否则输出unknown。
允许修改:choose函数。
禁止修改:训练数据、模型结构、预处理、原始分数和固定测试集。
完成后:重跑正常、边界、陌生和故障样例,给出文件差异。
修改时不得把失败声音偷偷加入训练集,也不得删除原始分数。增加未知状态可能减少误报,也可能把原来正确的远距离门铃改为未知,因此必须回归全部样例,而不是只重跑已修好的混合声。
三、形成使用判断
判断卡至少回答以下问题:使用的模型、设备、房间和输入格式是什么;哪些声音经过独立测试;误报与漏报各有多少;低分、类别竞争和故障时怎样停止;谁对录音授权和最终使用负责。
合理结论可以是:作品只在类别有限、录音已授权、设备和环境接近测试条件时辅助记录低风险事件;默认在本地推理,低置信度、前两类接近、陌生声音或文件故障时输出未知;模型或录音设备改变后重新测试;不连续监听,不上传家庭或教室谈话,不连接真实设备,不替代专业报警装置。
在这些边界内,学生还可以把模型迁移为声音互动作品:经过人工确认的类别只改变屏幕卡片、虚拟指示灯或播放短提示音,unknown和故障状态保持不行动。这个反馈层由普通程序实现,不是模型“自动理解了该做什么”。界面和反馈方式可以由学生创意设计,但必须保留原始分数、确认步骤和停止条件,不得扩展为真实门锁、电源、炉具或报警控制。
如果只需识别一种固定电子提示音,还应比较简单的频率规则。若规则已经稳定满足目标,就没有必要为了使用更复杂的模型而增加数据、算力和维护负担。
本章小结
声音先被采样为波形,再按短窗口转换成频谱图。声音分类模型从带标签的频谱图中学习局部模式,并为已知类别输出相对分数。训练集用于学习参数,验证集用于观察训练,独立测试集用于形成使用证据。
模型不知道世界上所有声音。other是训练过的类别,unknown是应用拒绝在证据不足时强行分类。置信度阈值和前两名分差属于模型外规则。噪声、距离、房间和设备变化会暴露模型泛化边界,误报、漏报与未知数量必须结合实际后果解释。
应用中的AI承担频谱图到类别分数的映射,开发与学习AI协助形成和修改程序,人负责授权、标签、测试和采用决定。一个可靠的课堂作品不仅能在正常样例上运行,还能在陌生输入和故障出现时明确停止。
关键术语
- 采样率:每秒记录声音振幅的次数。
- 声音窗口:从连续声音中截取的固定时长片段。
- 波形:振幅随时间变化的一维序列。
- 频谱图:表示各时间窗口中不同频率成分强弱的二维数据。
- 声音分类模型:把声音特征映射到已知类别分数的模型。
- 数据泄漏:训练信息以不恰当方式进入测试过程,使评价结果虚高。
- 误报:目标事件没有发生,系统却给出目标提醒。
- 漏报:目标事件已经发生,系统却没有给出相应提醒。
- 未知状态:应用认为现有证据不足以接受任何已知类别时的输出。
目标测试
一、单项选择题
- 频谱图更直接呈现的是( )。
A.录音文件名 B.时间和频率成分的强弱 C.声音事件的真实名称 D.设备权限 - 把同一长录音的相邻切片随机分到训练集和测试集,最可能造成( )。
A.类别增多 B.数据泄漏 C.采样率下降 D.声音变短 - 最高两个类别分数非常接近时,较合适的处理是( )。
A.永远选择第一名 B.删除第二名 C.保留分数并输出未知 D.直接触发设备 - 保持模型不变而提高置信度阈值,通常会使( )。
A.模型自动重训 B.确定分类减少 C.采样率变化 D.标签数量增加
二、判断并改错
- 测试准确率较高的课堂声音分类器可以直接替代家庭安全报警器。请判断并改正。
other类别和unknown状态含义完全相同。请判断并改正。
三、简答与操作题
- 不使用公式,说明一秒WAV从波形、频谱图到类别概率和最终判断的过程。
- 设计一条边界声音和一条陌生声音,写出采集条件、期望行为和通过标准。
- 设计一次只把置信度阈值从0.55改为0.80的实验,说明固定项、记录项和结论边界。
- 说明应用中的AI、开发与学习AI、普通规则、低风险反馈层和人在本章项目中的不同职责。
答案编号:A1-4-01—A1-4-10。完整答案和评价要点统一放入书后“目标测试参考答案”。