在 GitHub 上执行或查看/下载此 notebook
语音活动检测 (Voice Activity Detection)
语音活动检测 (VAD) 的目标是检测音频录音中包含语音的片段。
如下图所示,VAD 的输入是音频信号(或其对应的特征)。输出可以是一个序列,其中包含语音的时间帧为“1”,非语音时间帧为“0”。
或者,VAD 可以在输出中提供检测到语音活动的边界。例如
segment_001 0.00 2.57 NON_SPEECH
segment_002 2.57 8.20 SPEECH
segment_003 8.20 9.10 NON_SPEECH
segment_004 9.10 10.93 SPEECH
segment_005 10.93 12.00 NON_SPEECH
segment_006 12.00 14.40 SPEECH
segment_007 14.40 15.00 NON_SPEECH
VAD 有什么用?
VAD 在许多语音处理流程中扮演着关键角色。它用于当我们只想将处理算法应用于音频录音中的语音部分时。
例如,它经常被用作语音识别、语音增强、说话人分割和许多其他系统的预处理步骤。
为什么具有挑战性?
区分语音和非语音信号对人类来说非常自然。然而,对于机器来说,这棘手得多。一个好的 VAD 即使在嘈杂和混响条件下也应该精确地检测语音活动。在现实生活条件下,可能的噪声源数量巨大(例如,音乐、电话铃声、警报等),这使得这个问题对机器来说充满挑战。
此外,一个好的 VAD 应该能够处理短录音和非常长录音(例如会议),并且理想情况下不应该计算成本太高。
流程描述
鲁棒语音活动检测几十年来一直是一个非常活跃的研究领域。如今,深度学习在这个问题上也扮演着关键角色。
在本教程中,我们使用一个神经网络,它为每个输入帧提供语音/非语音预测。然后对帧级别的后验概率进行后处理,以检索最终的语音边界,如下图所示
更精确地说,我们这里计算标准的 FBANK 特征,并将其输入到 CRDNN 模型中(该模型结合了卷积层、循环层和全连接层)。输出经过 sigmoid 处理,进行二分类。网络使用二元交叉熵进行训练。对于语音帧,预测值将接近一;对于非语音帧,预测值将接近零。
在推理时,对二分类预测进行后处理。例如,我们在其上应用一个阈值来识别候选语音区域。之后,我们可以应用其他类型的后处理,例如合并邻近的片段或移除过短的片段。我们将在推理部分详细描述这一点。
现在,让我们简要讨论如何使用 SpeechBrain 训练这样的模型。
训练
SpeechBrain 有一个使用 LibriParty 数据集训练 VAD 的 recipe。这是我们为 VAD 训练等任务创建的数据集。它包含几个模拟声学场景,其中语音和噪声序列周期性地活跃(单独或同时)。
除此之外,训练 recipe 还会使用 Musan(包含语音、噪声和音乐信号)、CommonLanguage(包含 48 种语言的语音)和 open-rir(包含噪声和脉冲响应)动态地创建其他几个模拟声学场景。
动态模拟的声学场景探索了不同的情况,例如噪声+语音、语音到噪声转换、噪声到语音转换等。
与其它 SpeechBrain recipe 类似,可以使用以下命令训练模型
cd recipes/LibriParty/VAD
python train.py hparams/train.yaml
请按照 recipe 中提供的 README 文件进行操作,并在开始训练前确保已下载所有数据。
除了大量使用语音增强/污染外,该 recipe 没有特别之处。因此,让我们更多地关注依赖于一些自定义组件的推理部分。
推理
现在我们可以专注于推理部分。推理部分比平时稍微复杂一些,因为我们将其设计为适用于非常长的录音,并支持多种技术来后处理网络预测。
我们将处理所有上述方面。但首先,让我们安装 speechbrain
%%capture
# Installing SpeechBrain via pip
BRANCH = 'develop'
!python -m pip install git+https://github.com/speechbrain/speechbrain.git@$BRANCH
%%capture
!wget -O /content/example_vad_music.wav "https://www.dropbox.com/scl/fi/vvffxbkkuv79g0d4c7so3/example_vad_music.wav?rlkey=q5m5wc6y9fsfvt43x5yy8ohrf&dl=1"
让我们读取一个语音信号
import torch
import torchaudio
import matplotlib.pyplot as plt
audio_file = "/content/example_vad_music.wav"
signal, fs = torchaudio.load(audio_file)
signal = signal.squeeze()
time = torch.linspace(0, signal.shape[0]/fs, steps=signal.shape[0])
plt.plot(time, signal)
from IPython.display import Audio
Audio(audio_file)
<IPython.lib.display.Audio object>

现在我们可以通过以下方式使用上一步训练好的 VAD
from speechbrain.inference.VAD import VAD
VAD = VAD.from_hparams(source="speechbrain/vad-crdnn-libriparty", savedir="pretrained_models/vad-crdnn-libriparty")
boundaries = VAD.get_speech_segments(audio_file)
VAD.save_boundaries(boundaries)
segment_001 0.00 0.23 NON_SPEECH
segment_002 0.23 5.58 SPEECH
segment_003 5.58 10.90 NON_SPEECH
segment_004 10.90 16.63 SPEECH
如你所见,两个语音部分都被正确检测到。音乐部分被正确分类为非语音片段,而音乐和语音同时活跃的部分被分类为语音。
推理流程 (详细)
检测语音片段的流程如下
计算帧级别的后验概率。
对后验概率应用阈值。
在此基础上得出候选语音片段。
在每个候选片段内应用能量 VAD (可选)。
合并距离过近的片段。
移除过短的片段。
复核语音片段 (可选)。
为了使调试更容易、接口更模块化和透明,我们允许用户访问这些中间步骤的输出。
在某些情况下,并非所有这些步骤都是必需的。用户可以自由定制流程及其超参数,以提高在其数据上的性能。
让我们从后验概率计算开始。
1- 后验概率计算
神经网络模型提供的输出对于非语音帧应接近零,对于语音帧应接近一。
时间分辨率取决于特征提取部分和所采用的神经网络模型。在这种情况下,我们每 10 毫秒进行一次预测(这在语音处理中是相当标准的)。
要计算后验概率,可以使用以下命令
prob_chunks = VAD.get_speech_prob_file(audio_file)
plt.plot(prob_chunks.squeeze())
[<matplotlib.lines.Line2D at 0x7f301f0de980>]

如预期的那样,语音区域的值较高,音乐区域的值较低。
get_speech_prob_file 函数
旨在处理长音频录音。它在大块(例如 30 秒)上计算后验概率,这些大块被顺序读取以避免在内存中存储长信号。每个大块又被分割成更小的块(例如 10 秒),这些小块并行处理。
你可以根据内存限制调整 large_chunk_size
和 small_chunk_size
。如果你有足够的内存,可以使用它来存储更大块的信号(例如 5 分钟)。这可以通过增加 large_chunk_size
来实现,并会使 VAD(稍微)更快。
2- 应用阈值
现在,我们可以通过应用阈值来检测候选语音片段。
要做到这一点,可以使用以下函数
prob_th = VAD.apply_threshold(prob_chunks, activation_th=0.5, deactivation_th=0.25).float()
plt.plot(prob_th.squeeze())
[<matplotlib.lines.Line2D at 0x7f301f131450>]

除了应用单个阈值(例如 0.5)之外,我们允许用户设置两个不同的阈值,一个用于决定何时开始语音片段 (activation_th
),另一个用于检测何时停止语音片段 (deactivation_th
)。
根据我们的经验,将 activation_th
设置得高于 deactivation_th
是有意义的(例如,activation_th=0.5
,deactivation_th=0.25
)。
然而,用户可以通过调整这些超参数来使 VAD 或多或少地具有选择性。
3- 获取边界
现在,我们可以从经过阈值处理的后验概率中得出语音片段的边界
boundaries = VAD.get_boundaries(prob_th)
VAD.save_boundaries(boundaries, audio_file=audio_file)
segment_001 0.00 0.23 NON_SPEECH
segment_002 0.23 5.58 SPEECH
segment_003 5.58 10.90 NON_SPEECH
segment_004 10.90 16.63 SPEECH
segment_005 16.63 20.00 NON_SPEECH
segment_006 20.00 20.43 SPEECH
segment_007 20.43 20.45 NON_SPEECH
boundaries
张量包含每个语音片段的开始和结束秒数。save_boundaries
方法可用于以人类可读的格式绘制边界,和/或将它们保存到文件中(使用 save_path
参数)。
4- 基于能量的 VAD (可选)
训练好的神经网络 VAD 倾向于检测较大的语音片段,其中相互接近的较小语音片段会被合并。
如果用户想要更高的分辨率,一种可能的方法是在检测到的语音片段中应用基于能量的 VAD。基于能量的 VAD 使用滑动窗口处理语音片段,计算每个分块内的能量。能量轮廓被标准化,使其具有 0.5 和 +-0.5 的标准差。然后我们应用阈值并将原始语音片段分割成更小的片段。
操作如下
boundaries = VAD.energy_VAD(audio_file,boundaries, activation_th=0.8, deactivation_th=0.0)
VAD.save_boundaries(boundaries, audio_file=audio_file)
segment_001 0.00 1.66 NON_SPEECH
segment_002 1.66 2.13 SPEECH
segment_003 2.13 2.70 NON_SPEECH
segment_004 2.70 4.03 SPEECH
segment_005 4.03 4.27 NON_SPEECH
segment_006 4.27 5.26 SPEECH
segment_007 5.26 11.37 NON_SPEECH
segment_008 11.37 11.94 SPEECH
segment_009 11.94 12.63 NON_SPEECH
segment_010 12.63 13.12 SPEECH
segment_011 13.12 13.26 NON_SPEECH
segment_012 13.26 14.28 SPEECH
segment_013 14.28 14.99 NON_SPEECH
segment_014 14.99 15.67 SPEECH
segment_015 15.67 15.79 NON_SPEECH
segment_016 15.79 16.06 SPEECH
segment_017 16.06 16.30 NON_SPEECH
segment_018 16.30 16.42 SPEECH
segment_019 16.42 20.02 NON_SPEECH
segment_020 20.02 20.10 SPEECH
segment_021 20.10 20.18 NON_SPEECH
segment_022 20.18 20.18 SPEECH
segment_023 20.18 20.22 NON_SPEECH
segment_024 20.22 20.22 SPEECH
segment_025 20.22 20.29 NON_SPEECH
segment_026 20.29 20.35 SPEECH
segment_027 20.35 20.37 NON_SPEECH
segment_028 20.37 20.37 SPEECH
segment_029 20.37 20.42 NON_SPEECH
segment_030 20.42 20.42 SPEECH
segment_031 20.42 20.45 NON_SPEECH
用户可以通过调整 activation_th
和 deactivation_th
来使 VAD 或多或少地具有选择性。
与神经网络 VAD 不同,能量 VAD 倾向于过度分割输入。我们将通过后处理边界来改进这一点,如下所示。
5- 合并邻近片段
用户可能需要选择 VAD 的所需分辨率(最佳粒度级别可能取决于任务)。
例如,合并彼此距离过近的片段可能是有意义的。
这是通过以下方法完成的
# 5- Merge segments that are too close
boundaries = VAD.merge_close_segments(boundaries, close_th=0.250)
VAD.save_boundaries(boundaries, audio_file=audio_file)
segment_001 0.00 1.66 NON_SPEECH
segment_002 1.66 2.13 SPEECH
segment_003 2.13 2.70 NON_SPEECH
segment_004 2.70 5.26 SPEECH
segment_005 5.26 11.37 NON_SPEECH
segment_006 11.37 11.94 SPEECH
segment_007 11.94 12.63 NON_SPEECH
segment_008 12.63 14.28 SPEECH
segment_009 14.28 14.99 NON_SPEECH
segment_010 14.99 16.42 SPEECH
segment_011 16.42 20.02 NON_SPEECH
segment_012 20.02 20.42 SPEECH
segment_013 20.42 20.45 NON_SPEECH
在这种情况下,我们合并了距离小于 250 毫秒的片段。用户可以调整 close_th
并根据自己的需求进行设置。
6- 移除短片段
移除可能被错误分类为语音的短孤立片段也可能是有意义的
# 6- Remove segments that are too short
boundaries = VAD.remove_short_segments(boundaries, len_th=0.250)
VAD.save_boundaries(boundaries, audio_file=audio_file)
segment_001 0.00 1.66 NON_SPEECH
segment_002 1.66 2.13 SPEECH
segment_003 2.13 2.70 NON_SPEECH
segment_004 2.70 5.26 SPEECH
segment_005 5.26 11.37 NON_SPEECH
segment_006 11.37 11.94 SPEECH
segment_007 11.94 12.63 NON_SPEECH
segment_008 12.63 14.28 SPEECH
segment_009 14.28 14.99 NON_SPEECH
segment_010 14.99 16.42 SPEECH
segment_011 16.42 20.02 NON_SPEECH
segment_012 20.02 20.42 SPEECH
segment_013 20.42 20.45 NON_SPEECH
在这种情况下,我们移除长度小于 250 毫秒的片段。请注意,我们首先合并了邻近的片段,然后才移除短片段。这有助于仅移除短的“孤立”片段。
7- 复核语音片段 (可选)
此时,我们可以获取后处理的语音片段,并复核它们是否真正包含语音。操作如下
# 7- Double-check speech segments (optional).
boundaries = VAD.double_check_speech_segments(boundaries, audio_file, speech_th=0.5)
VAD.save_boundaries(boundaries, audio_file=audio_file)
segment_001 0.00 1.66 NON_SPEECH
segment_002 1.66 2.13 SPEECH
segment_003 2.13 2.70 NON_SPEECH
segment_004 2.70 5.26 SPEECH
segment_005 5.26 11.37 NON_SPEECH
segment_006 11.37 11.94 SPEECH
segment_007 11.94 12.63 NON_SPEECH
segment_008 12.63 14.28 SPEECH
segment_009 14.28 14.99 NON_SPEECH
segment_010 14.99 16.42 SPEECH
segment_011 16.42 20.45 NON_SPEECH
该方法再次对检测到的语音片段使用神经网络 VAD。如果片段内的平均后验概率大于 speech_th
(在本例中,speech_th=0.5
),则确认该语音片段。否则,将其移除。
可视化
我们还实现了一些实用程序来帮助用户可视化 VAD 的输出
upsampled_boundaries = VAD.upsample_boundaries(boundaries, audio_file)
此函数创建一个与原始音频录音维度相同的“VAD 信号”。这样,就可以一起绘制它们
time = torch.linspace(0, signal.shape[0]/fs, steps=signal.shape[0])
plt.plot(time, signal)
plt.plot(time, upsampled_boundaries.squeeze())
[<matplotlib.lines.Line2D at 0x7f301f0b5fc0>]

要了解更多细节,还可以上采样并可视化 VAD 得分
upsampled_vad_scores = VAD.upsample_VAD(prob_chunks, audio_file)
plt.plot(time, signal)
plt.plot(time, upsampled_boundaries.squeeze())
plt.plot(time, upsampled_vad_scores.squeeze())
[<matplotlib.lines.Line2D at 0x7f301f105330>]

或者,用户可以保存 VAD 文件,并使用像 audacity 这样的音频可视化软件与原始文件一起打开。
就这样!VAD 愉快!
附录:关于使用基于能量的 VAD
如果使用了基于能量的 VAD,合并、移除、复核操作的顺序很重要。如果在基于能量的 VAD 后立即使用 double_check_speech_segments
,然后再使用 merge_close_segments
,会丢弃一些语音帧。
# plotted boundaries may be scaled down to compare many at once
def plot_boundaries(b, color):
upsampled = VAD.upsample_boundaries(b, audio_file)
plt.plot(time, upsampled.squeeze(), color)
# first figures - from CRDNN VAD to energy VAD
fig, axs = plt.subplots(3, 3, figsize=(16, 12))
plt.sca(axs[0, 0])
plt.title('1a. CRDNN VAD scores')
plt.plot(time, signal)
plt.plot(time, upsampled_vad_scores.squeeze(), 'green')
# CRDNN boundaries
plt.sca(axs[1, 0])
plt.title('1b. CRDNN VAD boundaries')
plt.plot(time, signal)
boundaries = VAD.get_boundaries(prob_th)
plot_boundaries(boundaries, 'orange')
# energy VAD boundaries
plt.sca(axs[2, 0])
plt.title('1c. Energy VAD boundaries based on CRDNN')
plt.plot(time, signal)
boundaries_energy = VAD.energy_VAD(audio_file, boundaries, activation_th=0.8, deactivation_th=0.0)
plot_boundaries(boundaries_energy, 'purple')
# second figure - double-check, then merge
plt.sca(axs[0, 1])
plt.title('2a. Energy VAD (same as 1c)')
plt.plot(time, signal)
plot_boundaries(boundaries_energy, 'purple')
# double-check
plt.sca(axs[1, 1])
plt.title('2b. Double-check (too early)')
plt.plot(time, signal)
boundaries = VAD.double_check_speech_segments(boundaries_energy, audio_file, speech_th=0.5)
plot_boundaries(boundaries, 'red')
# merge (too late)
plt.sca(axs[2, 1])
plt.title('2c. Merge short segments (too late)')
plt.plot(time, signal)
boundaries = VAD.merge_close_segments(boundaries, close_th=0.250)
plot_boundaries(boundaries, 'black')
# third figure - merge, remove, double-check
plt.sca(axs[0, 2])
plt.title('3a. Energy VAD (same as 1c)')
plt.plot(time, signal)
plot_boundaries(boundaries_energy, 'purple')
# merge
plt.sca(axs[1, 2])
plt.title('3b. Merge short segments (as above)')
plt.plot(time, signal)
boundaries = VAD.merge_close_segments(boundaries_energy, close_th=0.250)
plot_boundaries(boundaries, 'black')
# merge
plt.sca(axs[2, 2])
plt.title('3c. Remove short segments & double-check (as above)')
plt.plot(time, signal)
boundaries = VAD.remove_short_segments(boundaries, len_th=0.250)
boundaries = VAD.double_check_speech_segments(boundaries, audio_file, speech_th=0.5)
plot_boundaries(boundaries, 'red')
Exception ignored in: <function _xla_gc_callback at 0x7f3035dbd750>
Traceback (most recent call last):
File "/usr/local/lib/python3.10/dist-packages/jax/_src/lib/__init__.py", line 97, in _xla_gc_callback
def _xla_gc_callback(*args):
KeyboardInterrupt:
引用 SpeechBrain
如果你在研究或商业中使用 SpeechBrain,请使用以下 BibTeX 条目进行引用
@misc{speechbrainV1,
title={Open-Source Conversational AI with {SpeechBrain} 1.0},
author={Mirco Ravanelli and Titouan Parcollet and Adel Moumen and Sylvain de Langen and Cem Subakan and Peter Plantinga and Yingzhi Wang and Pooneh Mousavi and Luca Della Libera and Artem Ploujnikov and Francesco Paissan and Davide Borra and Salah Zaiem and Zeyu Zhao and Shucong Zhang and Georgios Karakasidis and Sung-Lin Yeh and Pierre Champion and Aku Rouhe and Rudolf Braun and Florian Mai and Juan Zuluaga-Gomez and Seyed Mahed Mousavi and Andreas Nautsch and Xuechen Liu and Sangeet Sagar and Jarod Duret and Salima Mdhaffar and Gaelle Laperriere and Mickael Rouvier and Renato De Mori and Yannick Esteve},
year={2024},
eprint={2407.00463},
archivePrefix={arXiv},
primaryClass={cs.LG},
url={https://arxiv.org/abs/2407.00463},
}
@misc{speechbrain,
title={{SpeechBrain}: A General-Purpose Speech Toolkit},
author={Mirco Ravanelli and Titouan Parcollet and Peter Plantinga and Aku Rouhe and Samuele Cornell and Loren Lugosch and Cem Subakan and Nauman Dawalatabad and Abdelwahab Heba and Jianyuan Zhong and Ju-Chieh Chou and Sung-Lin Yeh and Szu-Wei Fu and Chien-Feng Liao and Elena Rastorgueva and François Grondin and William Aris and Hwidong Na and Yan Gao and Renato De Mori and Yoshua Bengio},
year={2021},
eprint={2106.04624},
archivePrefix={arXiv},
primaryClass={eess.AS},
note={arXiv:2106.04624}
}