Python吴恩达深度学习作业24 -- 语音识别关键字
在本周的视频中,你学习了如何将深度学习应用于语音识别。在此作业中,你将构建语音数据集并实现用于关键词检测(有时也称为唤醒词或触发词检测)的算法。关键词识别是一项技术,可让诸如Amazon Alexa,Google Home,Apple Siri和Baidu DuerOS之类的设备在听到某个特定单词时回应。对于本练习,我们的触发词将是"Activate."。每次听到你说“激活”时,它都会发出“蜂鸣声
关键字语音识别
在本周的视频中,你学习了如何将深度学习应用于语音识别。在此作业中,你将构建语音数据集并实现用于关键词检测(有时也称为唤醒词或触发词检测)的算法。关键词识别是一项技术,可让诸如Amazon Alexa,Google Home,Apple Siri和Baidu DuerOS之类的设备在听到某个特定单词时回应。
对于本练习,我们的触发词将是"Activate."。每次听到你说“激活”时,它都会发出“蜂鸣声”。作业完成后,你将可以录制自己的讲话片段,并在算法检测到你说"Activate"时触发提示音。
完成此任务后,也许你还可以将其扩展为在笔记本电脑上运行,以便每次你说“Activate”时,它就会启动你喜欢的应用程序,或者打开房屋中的网络连接灯,或触发其他事件?
在本作业中,你将学习:
- 构建语言识别项目
- 合成和处理音频记录以创建训练/开发数据集
- 训练关键词检测模型并做出预测
import numpy as np
from pydub import AudioSegment
import random
import sys
import io
import os
import glob
import IPython
from td_utils import *
%matplotlib inline
d:\vr\virtual_environment\lib\site-packages\pydub\utils.py:165: RuntimeWarning: Couldn't find ffmpeg or avconv - defaulting to ffmpeg, but may not work
warn("Couldn't find ffmpeg or avconv - defaulting to ffmpeg, but may not work", RuntimeWarning)
1 合成数据:创建语音数据集
让我们从为触发词检测算法构建数据集开始。语音数据集在理想情况下应尽可能接近要在其上运行它的应用程序。在这种情况下,你想在工作环境(图书馆,家庭,办公室,开放空间…)中检测到"activate"一词。因此,你需要在不同的背景声音上混合使用positive词(“activate”)和negative词(除activate以外的随机词)来创建录音。让我们看看如何创建这样的数据集。
1.1 试听数据
你的一位朋友正在帮助你完成这个项目,他们去了该地区各地的图书馆,咖啡馆,餐馆,家庭和办公室,以记录背景噪音以及人们说positive/negative词的音频片段。该数据集包括以各种口音讲话的人。
在raw_data目录中,你可以找到原始音频文件的子集,包括正词,负词和背景噪音。你将使用这些音频文件来合成数据集以训练模型。"activate"目录包含人们说"activate"一词的正面示例。"negatives"目录包含人们说"activate"以外的随机单词的否定示例。每个音频记录只有一个字。"backgrounds"目录包含10秒的不同环境下的背景噪音片段。
运行下面的单元格以试听一些示例。
IPython.display.Audio("./raw_data/activates/1.wav")
CSDN不支持播放音频
IPython.display.Audio("./raw_data/negatives/4.wav")
CSDN不支持播放音频
IPython.display.Audio("./raw_data/backgrounds/1.wav")
CSDN不支持播放音频
你将使用这三种类型的记录(positives/negatives/backgrounds)来创建标记的数据集。
1.2 从录音到频谱图
录音到底是什么?麦克风记录随时间变化很小的气压,而这些气压的微小变化也会使你的耳朵感觉到声音。你可以认为录音是一长串数字,用于测量麦克风检测到的气压变化很小。我们将使用以44100Hz(或44100赫兹)采样的音频。这意味着麦克风每秒可以为我们提供44100个号码。因此,一个10秒的音频剪辑由441000个数字表示(= 10 × 44100 10 \times 44100 10×44100)。
从音频的这种“原始”表示中很难弄清是否说了"activate"这个词。为了帮助你的序列模型更轻松地学习检测触发词,我们将计算音频的spectrogram。频谱图告诉我们音频片段在某个时刻存在多少不同的频率。
(如果你曾经在信号处理或傅立叶变换方面上过高级课程,则可以通过在原始音频信号上滑动一个窗口来计算频谱图,并使用傅立叶变换来计算每个窗口中最活跃的频率。如果你不理解前面的句子,也不用担心。)
让我们来看一个例子。
IPython.display.Audio("audio_examples/example_train.wav")
CSDN不支持播放音频
x = graph_spectrogram("audio_examples/example_train.wav")
上图表示在多个时间步长(x轴)上每个频率(y轴)的活跃程度。
图1:录音的频谱图,其中的颜色表示在不同的时间点音频中不同频率出现(响亮)的程度。绿色方块表示某个频率在音频剪辑(扬声器)中更活跃或更活跃。蓝色方块表示较不活跃的频率。
输出频谱图的尺寸取决于频谱图软件的超参数和输入的长度。在此笔记本中,我们将使用10秒的音频剪辑作为训练示例的“标准长度”。频谱图的时间步数为5511。稍后你将看到频谱图将是网络中的输入 x x x,因此 T x = 5511 T_x=5511 Tx=5511。
_, data = wavfile.read("audio_examples/example_train.wav")
print("Time steps in audio recording before spectrogram", data[:,0].shape)
print("Time steps in input after spectrogram", x.shape)
Time steps in audio recording before spectrogram (441000,)
Time steps in input after spectrogram (101, 5511)
现在,你可以定义:
Tx = 5511 # 从频谱图输入到模型的时间步数
n_freq = 101 # 在频谱图的每个时间步输入模型的频率数
请注意,即使将10秒作为我们的默认训练示例长度,也可以将10秒的时间离散化为不同数量的值。你已经看到441000(原始音频)和5511(频谱图)。在前一种情况下,每个步骤代表 10 / 441000 ≈ 0.000023 10/441000 \approx 0.000023 10/441000≈0.000023秒。在第二种情况下,每个步骤代表 10 / 5511 ≈ 0.0018 10/5511 \approx 0.0018 10/5511≈0.0018秒。
对于10秒的音频,你将在此作业中看到的关键值为:
- 441000 441000 441000(原始音频)
- 5511 = T x 5511 = T_x 5511=Tx(频谱图输出,以及神经网络的输入维数)。
- 10000 10000 10000(用pydub模块来合成音频)
- 1375 = T y 1375=T_y 1375=Ty(要构建的GRU输出中的步骤数)。
请注意,这些表示中的每个表示都恰好对应10秒的时间。只是他们在不同程度上离散化了他们。所有这些都是超参数,可以更改(441000除外,这是麦克风函数)。我们选择的值在语音系统使用的标准范围内。
上面的 T y = 1375 T_y=1375 Ty=1375数字意味着对于模型的输出,我们将10s离散为1375个时间间隔(每个时间间隔的长度为 10 / 1375 ≈ 0.0072 10/1375 \approx 0.0072 10/1375≈0.0072秒),并尝试针对每个时间间隔预测是否有人最近说完“activate”。
上面的10000对应于将10秒剪辑离散化为10/10000 = 0.001秒迭代。0.001秒也称为1毫秒或1ms。因此,当我们说要按照1ms的间隔离散时,这意味着我们正在使用10,000个步长。
Ty = 1375 # 我们模型输出中的时间步数
1.3 生成单个训练示例
由于语音数据很难获取和标记,因此你将使用激活,否定和背景的音频片段来合成训练数据。录制很多带有随机"activates"内容的10秒音频剪辑非常慢。取而代之的是,录制许多肯定词和否定词以及分别记录背景噪音(或从免费的在线资源下载背景噪音)会变得更加容易。
要合成一个训练示例,你将:
- 随机选择10秒钟的背景音频剪辑
- 将"activates"的0-4个音频片段随机插入此10秒的片段中
- 将10个否定词的音频剪辑随机插入此10秒剪辑中
因为你已经将"activates"一词合成到了背景剪辑中,所以你确切知道"activates"在10秒剪辑中何时出现。稍后你将看到,这也使得生成标签 y ⟨ t ⟩ y^{\langle t \rangle} y⟨t⟩ 更加容易。
你将使用pydub包来处理音频。Pydub将原始音频文件转换为Pydub数据结构的列表(在此处了解详细信息并不重要)。Pydub使用1毫秒作为离散时间间隔(1毫秒等于1毫秒= 1/1000秒),这也是为什么始终以10,000步表示10秒剪辑的原因。
# 使用pydub加载音频片段
activates, negatives, backgrounds = load_raw_audio()
print("background len: " + str(len(backgrounds[0]))) # 应该是10,000,因为它是一个10秒的剪辑
print("activate[0] len: " + str(len(activates[0]))) # 也许大约1000,因为 "activate" 音频剪辑通常大约1秒(但变化很大)
print("activate[1] len: " + str(len(activates[1]))) # 不同的 "activate" 剪辑可以具有不同的长度
background len: 10000
activate[0] len: 721
activate[1] len: 731
在背景上叠加正/负词:
给定一个10秒的背景剪辑和一个简短的音频剪辑(positive or negative word),你需要能够将单词的简短音频剪辑“添加”或“插入”到背景上。为确保插入背景的音频片段不重叠,你将跟踪以前插入的音频片段的时间。你将在背景中插入多个正/负词剪辑,而又不想在与先前添加的另一个剪辑重叠的位置插入"activate"或随机词。
为了清楚起见,当你在10秒的咖啡馆噪音片段中插入1秒的 “activate” 时,你最终会得到一个10秒的片段,听起来像有人在咖啡馆中说 “activate”,背景咖啡馆噪音中叠加了 “activate” 。注意你没有以11秒的剪辑结尾。稍后你将看到pydub如何帮助你执行此操作。
在叠加的同时创建标签:
还记得标签 y ⟨ t ⟩ y^{\langle t \rangle} y⟨t⟩代表某人是否刚刚说完"activate.“。给定一个背景剪辑,我们可以为所有 t t t初始化 y ⟨ t ⟩ = 0 y^{\langle t \rangle}=0 y⟨t⟩=0,因为该剪辑不包含任何"activates.”。
当插入或覆盖"activate"剪辑时,还将更新
y
⟨
t
⟩
y^{\langle t \rangle}
y⟨t⟩的标签,以便输出的50个步骤现在具有目标标签1。你将训练GRU来检测何时某人完成说"activate"。例如,假设合成的"activate"剪辑在10秒音频中的5秒标记处结束-恰好在剪辑的一半处。回想一下
T
y
=
1375
T_y=1375
Ty=1375,因此时间步长$687 = $ int(1375*0.5)
对应于进入音频5秒的时刻。因此,你将设置
y
⟨
688
⟩
=
1
y^{\langle 688 \rangle} = 1
y⟨688⟩=1。此外,如果GRU在此刻之后的短时间内(在内部)在任何地方检测到"activate",你将非常满意,因此我们实际上将标签
y
⟨
t
⟩
y^{\langle t \rangle}
y⟨t⟩的50个连续值设置为1。我们有
y
⟨
688
⟩
=
y
⟨
689
⟩
=
⋯
=
y
⟨
737
⟩
=
1
y^{\langle 688 \rangle} = y^{\langle 689 \rangle} = \cdots = y^{\langle 737 \rangle} = 1
y⟨688⟩=y⟨689⟩=⋯=y⟨737⟩=1。
这是合成训练数据的另一个原因:如上所述,生成这些标签 y ⟨ t ⟩ y^{\langle t \rangle} y⟨t⟩相对简单。相反,如果你在麦克风上录制了10秒的音频,那么一个人收听它并在 “activate” 完成时准确手动进行标记会非常耗时。
下图显示了标签
y
⟨
t
⟩
y^{\langle t \rangle}
y⟨t⟩,对于我们插入了"activate", “innocent”,activate", "baby"的剪辑,请注意,正标签“1”是关联的只用positive的词。
图2
要实现合成训练集过程,你将使用以下帮助函数。所有这些函数将使用1ms的离散时间间隔,因此将10秒的音频离散化为10,000步。
get_random_time_segment(segment_ms)
在我们的背景音频中获得随机的时间段is_overlapping(segment_time,existing_segments)
检查时间段是否与现有时间段重叠insert_audio_clip(background,audio_clip,existing_times)
使用get_random_time_segment
和is_overlapping
在我们的背景音频中随机插入一个音频片段。insert_ones(y,segment_end_ms)
在我们的标签向量y的"activate"词之后插入1。
函数get_random_time_segment(segment_ms)
返回一个随机的时间段,我们可以在其中插入持续时间为segment_ms
的音频片段。 通读代码以确保你了解它在做什么。
def get_random_time_segment(segment_ms):
"""
获取 10,000 ms音频剪辑中时间长为 segment_ms 的随机时间段。
参数:
segment_ms -- 音频片段的持续时间,以毫秒为单位("ms" 代表 "毫秒")
返回:
segment_time -- 以ms为单位的元组(segment_start,segment_end)
"""
segment_start = np.random.randint(low=0, high=10000-segment_ms) # 确保段不会超过10秒背景
segment_end = segment_start + segment_ms - 1
return (segment_start, segment_end)
接下来,假设你在(1000,1800)和(3400,4500)段插入了音频剪辑。即第一个片段开始于1000步,结束于1800步。现在,如果我们考虑在(3000,3600)插入新的音频剪辑,这是否与先前插入的片段之一重叠?在这种情况下,(3000,3600)和(3400,4500)重叠,因此我们应该决定不要在此处插入片段。
出于此函数的目的,将(100,200)和(200,250)定义为重叠,因为它们在时间步200处重叠。但是,(100,199)和(200,250)是不重叠的。
练习:实现is_overlapping(segment_time,existing_segments)
来检查新的时间段是否与之前的任何时间段重叠。你需要执行2个步骤:
- 创建一个“False”标志,如果发现有重叠,以后将其设置为“True”。
- 循环遍历
previous_segments
的开始和结束时间。将这些时间与细分的开始时间和结束时间进行比较。如果存在重叠,请将(1)中定义的标志设置为True。你可以使用:
for ....:
if ... <= ... and ... >= ...:
...
提示:如果该段在上一个段结束之前开始,并且该段在上一个段开始之后结束,则存在重叠。
# GRADED FUNCTION: is_overlapping
def is_overlapping(segment_time, previous_segments):
"""
检查段的时间是否与现有段的时间重叠。
参数:
segment_time -- 新段的元组(segment_start,segment_end)
previous_segments -- 现有段的元组列表(segment_start,segment_end)
返回:
如果时间段与任何现有段重叠,则为True,否则为False
"""
segment_start, segment_end = segment_time
# 第一步:将重叠标识 overlap 初始化为“False”标志 (≈ 1 line)
overlap = False
# 第二步:循环遍历 previous_segments 的开始和结束时间。
# 比较开始/结束时间,如果存在重叠,则将标志 overlap 设置为True (≈ 3 lines)
for previous_start, previous_end in previous_segments:
if segment_start <= previous_end and segment_end >= previous_start:
overlap = True
return overlap
overlap1 = is_overlapping((950, 1430), [(2000, 2550), (260, 949)])
overlap2 = is_overlapping((2305, 2950), [(824, 1532), (1900, 2305), (3424, 3656)])
print("Overlap 1 = ", overlap1)
print("Overlap 2 = ", overlap2)
Overlap 1 = False
Overlap 2 = True
现在,让我们使用以前的辅助函数在10秒钟的随机时间将新的音频片段插入到背景中,但是要确保任何新插入的片段都不会与之前的片段重叠。
练习:实现insert_audio_clip()
以将音频片段叠加到背景10秒片段上。你将需要执行4个步骤:
- 以ms为单位获取正确持续时间的随机时间段。
- 确保该时间段与之前的任何时间段均不重叠。如果重叠,则返回步骤1并选择一个新的时间段。
- 将新时间段添加到现有时间段列表中,以便跟踪你插入的所有时间段。
- 使用pydub在背景上覆盖音频片段。我们已经为你实现了这一点。
# GRADED FUNCTION: insert_audio_clip
def insert_audio_clip(background, audio_clip, previous_segments):
"""
在随机时间步骤中在背景噪声上插入新的音频片段,确保音频片段与现有片段不重叠。
参数:
background -- 10秒背景录音。
audio_clip -- 要插入/叠加的音频剪辑。
previous_segments -- 已放置的音频片段的时间
返回:
new_background -- 更新的背景音频
"""
# 以ms为单位获取音频片段的持续时间
segment_ms = len(audio_clip)
# 第一步:使用其中一个辅助函数来选择要插入的随机时间段
# 新的音频剪辑。 (≈ 1 line)
segment_time = get_random_time_segment(segment_ms)
# 第二步:检查新的segment_time是否与previous_segments之一重叠。
# 如果重叠如果是这样,请继续随机选择新的 segment_time 直到它不重叠。(≈ 2 lines)
while is_overlapping(segment_time, previous_segments):
segment_time = get_random_time_segment(segment_ms)
# 第三步: 将新的 segment_time 添加到 previous_segments 列表中 (≈ 1 line)
previous_segments.append(segment_time)
# 第四步: 叠加音频片段和背景
new_background = background.overlay(audio_clip, position = segment_time[0])
return new_background, segment_time
np.random.seed(5)
audio_clip, segment_time = insert_audio_clip(backgrounds[0], activates[0], [(3790, 4400)])
audio_clip.export("insert_test.wav", format="wav")
print("Segment Time: ", segment_time)
IPython.display.Audio("insert_test.wav")
Segment Time: (2915, 3635)
CSDN不支持播放音频
# 预期的音频
IPython.display.Audio("audio_examples/insert_reference.wav")
CSDN不支持播放音频
最后,假设你刚刚插入了"activate." ,则执行代码以更新标签
y
⟨
t
⟩
y^{\langle t \rangle}
y⟨t⟩。在下面的代码中,由于
T
y
=
1375
T_y=1375
Ty=1375,所以y
是一个 (1,1375)
维向量。
如果"activate"在时间步骤
t
t
t结束,则设置
y
⟨
t
+
1
⟩
=
1
y^{\langle t+1 \rangle} = 1
y⟨t+1⟩=1以及最多49个其他连续值。但是,请确保你没有用完数组的末尾并尝试更新y[0][1375]
,由于
T
y
=
1375
T_y=1375
Ty=1375,所以有效索引是y[0][0]
至y[0][1374]
。因此,如果"activate" 在1370步结束,则只会得到y[0][1371] = y[0][1372] = y[0][1373] = y[0][1374] = 1
练习:实现insert_ones()
。你可以使用for循环。(如果你是python的slice运算的专家,请随时使用切片对此向量化。)如果段以segment_end_ms
结尾(使用10000步离散化),请将其转换为输出
y
y
y的索引(使用
1375
1375
1375步离散化),我们将使用以下公式:
segment_end_y = int(segment_end_ms * Ty / 10000.0)
# GRADED FUNCTION: insert_ones
def insert_ones(y, segment_end_ms):
"""
更新标签向量y。段结尾的后面50个输出的标签应设为 1。
严格来说,我们的意思是 segment_end_y 的标签应该是 0,而随后的50个标签应该是1。
参数:
y -- numpy数组的维度 (1, Ty), 训练样例的标签
segment_end_ms -- 以ms为单位的段的结束时间
返回:
y -- 更新标签
"""
# 背景持续时间(以频谱图时间步长表示)
segment_end_y = int(segment_end_ms * Ty / 10000.0)
# 将1添加到背景标签(y)中的正确索引
for i in range(segment_end_y + 1, segment_end_y + 51):
if i < Ty:
y[0, i] = 1
return y
arr1 = insert_ones(np.zeros((1, Ty)), 9700)
plt.plot(insert_ones(arr1, 4251)[0,:])
print("sanity checks:", arr1[0][1333], arr1[0][634], arr1[0][635])
sanity checks: 0.0 1.0 0.0
最后,你可以使用insert_audio_clip
和insert_ones
来创建一个新的训练示例。
练习:实现create_training_example()
。你需要执行以下步骤:
- 将标签向量 y y y初始化为维度为 ( 1 , T y ) (1,T_y) (1,Ty)的零numpy数组
- 将现有段的集合初始化为一个空列表
- 随机选择0到4个"activate"音频剪辑,并将其插入10秒剪辑中。还要在标签向量 y y y的正确位置插入标签。
- 随机选择0到2个负音频片段,并将其插入10秒片段中。
# GRADED FUNCTION: create_training_example
def create_training_example(background, activates, negatives):
"""
创建具有给定背景,正例和负例的训练示例。
参数:
background -- 10秒背景录音
activates -- "activate" 一词的音频片段列表
negatives -- 不是 "activate" 一词的音频片段列表
返回:
x -- 训练样例的频谱图
y -- 频谱图的每个时间步的标签
"""
# 设置随机种子
np.random.seed(18)
# 让背景更安静
background = background - 20
# 第一步:初始化 y (标签向量)为0 (≈ 1 line)
y = np.zeros((1, Ty))
# 第二步:将段时间初始化为空列表 (≈ 1 line)
previous_segments = []
# 从整个 "activate" 录音列表中选择0-4随机 "activate" 音频片段
number_of_activates = np.random.randint(0, 5)
random_indices = np.random.randint(len(activates), size=number_of_activates)
random_activates = [activates[i] for i in random_indices]
# 第三步: 循环随机选择 "activate" 剪辑插入背景
for random_activate in random_activates:
# 插入音频剪辑到背景
background, segment_time = insert_audio_clip(background, random_activate, previous_segments)
# 从 segment_time 中取 segment_start 和 segment_end
segment_start, segment_end = segment_time
# 在 "y" 中插入标签
y = insert_ones(y, segment_end_ms=segment_end)
# 从整个负例录音列表中随机选择0-2个负例录音
number_of_negatives = np.random.randint(0, 3)
random_indices = np.random.randint(len(negatives), size=number_of_negatives)
random_negatives = [negatives[i] for i in random_indices]
# 第四步: 循环随机选择负例片段并插入背景中
for random_negative in random_negatives:
# 插入音频剪辑到背景
background, _ = insert_audio_clip(background, random_negative, previous_segments)
# 标准化音频剪辑的音量
background = match_target_amplitude(background, -20.0)
# 导出新的训练样例
file_handle = background.export("train" + ".wav", format="wav")
print("文件 (train.wav) 已保存在您的目录中。")
# 获取并绘制新录音的频谱图(正例和负例叠加的背景)
x = graph_spectrogram("train.wav")
return x, y
x, y = create_training_example(backgrounds[0], activates, negatives)
文件 (train.wav) 已保存在您的目录中。
现在,您可以聆听您创建的训练示例,并将其与上面生成的频谱图进行比较。
IPython.display.Audio("train.wav")
CSDN不支持播放音频
IPython.display.Audio("audio_examples/train_reference.wav")
CSDN不支持播放音频
最后,你可以为生成的训练示例绘制关联的标签。
plt.plot(y[0])
1.4 完整训练集
现在,你已经实现了生成单个训练示例所需的代码。我们使用此过程生成了大量的训练集。为了节省时间,我们已经生成了一组训练示例。
# 加载预处理的训练样例
X = np.load("./XY_train/X.npy")
Y = np.load("./XY_train/Y.npy")
1.5 开发集
为了测试我们的模型,我们记录了包含25个示例的开发集。在合成训练数据的同时,我们希望使用与实际输入相同的分布来创建开发集。因此,我们录制了25个10秒钟的人们说"activate"和其他随机单词的音频剪辑,并手动标记了它们。这遵循课程3中描述的原则,即我们应该将开发集创建为与测试集尽可能相似。这就是为什么我们的开发人员使用真实音频而非合成音频的原因。
# 加载预处理开发集示例
X_dev = np.load("./XY_dev/X_dev.npy")
Y_dev = np.load("./XY_dev/Y_dev.npy")
2 模型
现在,你已经建立了数据集,让我们编写和训练关键字识别模型!
该模型将使用一维卷积层,GRU层和密集层。让我们加载在Keras中使用这些层的软件包。加载可能需要一分钟。
from keras.callbacks import ModelCheckpoint
from keras.models import Model, load_model, Sequential
from keras.layers import Dense, Activation, Dropout, Input, Masking, TimeDistributed, LSTM, Conv1D
from keras.layers import GRU, Bidirectional, BatchNormalization, Reshape
from keras.optimizers import Adam
Using TensorFlow backend.
2.1 建立模型
这是我们将使用的模型架构。花一些时间查看模型,看看它是否合理。
图3
该模型的一个关键步骤是一维卷积步骤(图3的底部附近)。它输入5511步频谱图,并输出1375步,然后由多层进一步处理以获得最终的 T y = 1375 T_y=1375 Ty=1375步输出。该层的作用类似于你在课程4中看到的2D卷积,其作用是提取低级特征,然后生成较小尺寸的输出。
通过计算,一维转换层还有助于加快模型的速度,因为现在GRU只需要处理1375个时间步,而不是5511个时间步。这两个GRU层从左到右读取输入序列,然后最终使用dense+sigmoid层对 y ⟨ t ⟩ y^{\langle t \rangle} y⟨t⟩进行预测。因为 y y y是二进制值(0或1),所以我们在最后一层使用Sigmoid输出来估计输出为1的机率,对应用户刚刚说过"activate"。
请注意,我们使用的是单向RNN,而不是双向RNN。这对于关键字检测确实非常重要,因为我们希望能够在说出触发字后立即检测到触发字。如果我们使用双向RNN,则必须等待记录整个10秒的音频,然后才能知道在音频剪辑的第一秒中是否说了"activate"。
可以通过四个步骤来实现模型:
步骤1:CONV层。使用Conv1D()
和196个滤波器来实现,
滤波器大小为15(kernel_size = 15
),步幅为4。[See documentation.]
步骤2:第一个GRU层。要生成GRU层,请使用:
X = GRU(units = 128, return_sequences = True)(X)
设置return_sequences = True
可以确保所有GRU的隐藏状态都被feed
到下一层。请记住,在Dropout和BatchNorm层之后进行此操作。
步骤3:第二个GRU层。这类似于先前的GRU层(请记住使用return_sequences = True
),但是有一个额外的dropout层。
步骤4:按以下步骤创建一个时间分布的密集层:
X = TimeDistributed(Dense(1, activation = "sigmoid"))(X)
这将创建一个紧随其后的Sigmoid密集层,因此用于密集层的参数对于每个时间步都是相同的。[See documentation.]
练习:实现model(),其架构如图3所示。
# GRADED FUNCTION: model
def model(input_shape):
"""
用 Keras 创建模型的图 Function creating the model's graph in Keras.
参数:
input_shape -- 模型输入数据的维度(使用Keras约定)
返回:
model -- Keras 模型实例
"""
X_input = Input(shape = input_shape)
# 第一步:卷积层 (≈4 lines)
X = Conv1D(196, 15, strides=4)(X_input) # CONV1D
X = BatchNormalization()(X) # Batch normalization 批量标准化
X = Activation('relu')(X) # ReLu activation ReLu 激活
X = Dropout(0.8)(X) # dropout (use 0.8)
# 第二步:第一个 GRU 层 (≈4 lines)
X = GRU(units = 128, return_sequences=True)(X) # GRU (使用128个单元并返回序列)
X = Dropout(0.8)(X) # dropout (use 0.8)
X = BatchNormalization()(X) # Batch normalization 批量标准化
# 第三步: 第二个 GRU 层 (≈4 lines)
X = GRU(units = 128, return_sequences=True)(X) # GRU (使用128个单元并返回序列)
X = Dropout(0.8)(X) # dropout (use 0.8)
X = BatchNormalization()(X) # Batch normalization 批量标准化
X = Dropout(0.8)(X) # dropout (use 0.8)
# 第四步: 时间分布全连接层 (≈1 line)
X = TimeDistributed(Dense(1, activation = "sigmoid"))(X) # time distributed (sigmoid)
model = Model(inputs = X_input, outputs = X)
return model
model = model(input_shape = (Tx, n_freq))
让我们输出模型总结以查看维度。
model.summary()
Model: "model_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 5511, 101) 0
_________________________________________________________________
conv1d_1 (Conv1D) (None, 1375, 196) 297136
_________________________________________________________________
batch_normalization_1 (Batch (None, 1375, 196) 784
_________________________________________________________________
activation_1 (Activation) (None, 1375, 196) 0
_________________________________________________________________
dropout_1 (Dropout) (None, 1375, 196) 0
_________________________________________________________________
gru_1 (GRU) (None, 1375, 128) 124800
_________________________________________________________________
dropout_2 (Dropout) (None, 1375, 128) 0
_________________________________________________________________
batch_normalization_2 (Batch (None, 1375, 128) 512
_________________________________________________________________
gru_2 (GRU) (None, 1375, 128) 98688
_________________________________________________________________
dropout_3 (Dropout) (None, 1375, 128) 0
_________________________________________________________________
batch_normalization_3 (Batch (None, 1375, 128) 512
_________________________________________________________________
dropout_4 (Dropout) (None, 1375, 128) 0
_________________________________________________________________
time_distributed_1 (TimeDist (None, 1375, 1) 129
=================================================================
Total params: 522,561
Trainable params: 521,657
Non-trainable params: 904
_________________________________________________________________
网络的输出为(None,1375,1),输入为(None,5511,101)。Conv1D将步数从频谱图上的5511减少到1375。
2.2 拟合模型
关键词检测需要很长时间来训练。为了节省时间,我们已经使用你上面构建的架构在GPU上训练了大约3个小时的模型,并提供了大约4000个示例的大型训练集。让我们加载模型吧。
model = load_model('./models/tr_model.h5')
你可以使用Adam优化器和二进制交叉熵损失进一步训练模型,如下所示。这将很快运行,因为我们只训练一个epoch,并提供26个例子的小训练集。
opt = Adam(lr=0.0001, beta_1=0.9, beta_2=0.999, decay=0.01)
model.compile(loss='binary_crossentropy', optimizer=opt, metrics=["accuracy"])
model.fit(X, Y, batch_size = 5, epochs=1)
Epoch 1/1
26/26 [==============================] - 10s 381ms/step - loss: 0.0893 - accuracy: 0.9717
2.3 测试模型
最后,让我们看看你的模型在开发集上的表现。
loss, acc = model.evaluate(X_dev, Y_dev)
print("Dev set accuracy = ", acc)
25/25 [==============================] - 1s 37ms/step
Dev set accuracy = 0.9507200121879578
看起来不错!但是,精度并不是这项任务的重要指标,因为标签严重偏斜到0,因此仅输出0的神经网络的精度将略高于90%。我们可以定义更有用的指标,例如F1得分或“精确度/召回率”。但是,我们不要在这里使用它,而只是凭经验看看模型是如何工作的。
3 预测
现在,你已经建立了用于触发词检测的工作模型,让我们使用它来进行预测吧。此代码段通过网络运行音频(保存在wav文件中)。
可以使用你的模型对新的音频片段进行预测。
你首先需要计算输入音频剪辑的预测。
练习:实现predict_activates()。你需要执行以下操作:
- 计算音频文件的频谱图
- 使用
np.swap
和np.expand_dims
将输入调整为(1,Tx,n_freqs)大小 - 在模型上使用正向传播来计算每个输出步骤的预测
def detect_triggerword(filename):
plt.subplot(2, 1, 1)
x = graph_spectrogram(filename)
# 频谱图输出(freqs,Tx),我们想要(Tx,freqs)输入到模型中
x = x.swapaxes(0,1)
x = np.expand_dims(x, axis=0)
predictions = model.predict(x)
plt.subplot(2, 1, 2)
plt.plot(predictions[0,:,0])
plt.ylabel('probability')
plt.show()
return predictions
一旦估计了在每个输出步骤中检测到"activate"一词的可能性,就可以在该可能性高于某个阈值时触发出"chiming(蜂鸣)"声。此外,在说出"activate"之后,对于许多连续值, y ⟨ t ⟩ y^{\langle t \rangle} y⟨t⟩可能接近1,但我们只希望发出一次提示音。因此,每75个输出步骤最多将插入一次铃声。这将有助于防止我们为"activate"的单个实例插入两个提示音。(该作用类似于计算机视觉中的非极大值抑制)
练习:实现chime_on_activate()
。你需要执行以下操作:
- 遍历每个输出步骤的预测概率
- 当预测大于阈值并且经过了连续75个以上的时间步长时,在原始音频剪辑中插入"chime"
使用以下代码将1375步离散化转换为10000步离散化,并使用pydub插入“chime”:
audio_clip = audio_clip.overlay(chime, position = ((i / Ty) * audio.duration_seconds)*1000)
chime_file = "audio_examples/chime.wav"
def chime_on_activate(filename, predictions, threshold):
audio_clip = AudioSegment.from_wav(filename)
chime = AudioSegment.from_wav(chime_file)
Ty = predictions.shape[1]
# 第一步:将连续输出步初始化为0
consecutive_timesteps = 0
# 第二步: 循环y中的输出步
for i in range(Ty):
# 第三步: 增加连续输出步
consecutive_timesteps += 1
# 第四步: 如果预测高于阈值并且已经过了超过75个连续输出步
if predictions[0,i,0] > threshold and consecutive_timesteps > 75:
# 第五步:使用pydub叠加音频和背景
audio_clip = audio_clip.overlay(chime, position = ((i / Ty) * audio_clip.duration_seconds)*1000)
# 第六步: 将连续输出步重置为0
consecutive_timesteps = 0
audio_clip.export("chime_output.wav", format='wav')
3.1 测试开发集
让我们探讨一下我们的模型在开发集中的两个未知的音频剪辑上表现如何。首先让我们听听两个开发集剪辑。
IPython.display.Audio("./raw_data/dev/1.wav")
CSDN不支持播放音频
IPython.display.Audio("./raw_data/dev/2.wav")
CSDN不支持播放音频
现在,让我们在这些音频剪辑上运行模型,看看在"activate"之后它是否添加了提示音!
filename = "./raw_data/dev/1.wav"
prediction = detect_triggerword(filename)
chime_on_activate(filename, prediction, 0.5)
IPython.display.Audio("./chime_output.wav")
CSDN不支持播放音频
filename = "./raw_data/dev/2.wav"
prediction = detect_triggerword(filename)
chime_on_activate(filename, prediction, 0.5)
IPython.display.Audio("./chime_output.wav")
CSDN不支持播放音频
这是你应该记住的:
- 数据合成是创建针对语音问题(尤其是触发词检测)大型训练集的有效方法。
- 在将音频数据传递到RNN,GRU或LSTM之前,使用频谱图和可选的1D转换层是常见的预处理步骤。
- 可以使用端到端的深度学习方法来构建非常有效的触发词检测系统。
4 试试你自己的例子!
在此笔记本的此可选练习中,你可以在自己的音频剪辑上尝试使用你的模型!
录制一个10秒钟的音频片段,说"activate"和其他随机单词,然后将其作为myaudio.wav
上传到Coursera hub。确保将音频作为WAV文件上传。如果你的音频以其他格式(例如mp3)录制,则可以在线找到免费软件以将其转换为wav。如果你的录音时间不是10秒,则下面的代码将根据需要修剪或填充该声音,以使其达到10秒。
# 将音频预处理为正确的格式
def preprocess_audio(filename):
# 将音频片段修剪或填充到 10000ms
padding = AudioSegment.silent(duration=10000)
segment = AudioSegment.from_wav(filename)[:10000]
segment = padding.overlay(segment)
# 将帧速率设置为 44100
segment = segment.set_frame_rate(44100)
# 导出为wav
segment.export(filename, format='wav')
将音频文件上传到Coursera后,将文件路径放在下面的变量中。
your_filename = "audio_examples/my_audio.wav"
preprocess_audio(your_filename)
IPython.display.Audio(your_filename) # 听你上传的音频
CSDN不支持播放音频
最后,使用该模型预测在10秒的音频剪辑中何时说了"activate"并触发提示音。如果没有适当添加哔声,请尝试调整chime_threshold。
chime_threshold = 0.5
prediction = detect_triggerword(your_filename)
chime_on_activate(your_filename, prediction, chime_threshold)
IPython.display.Audio("./chime_output.wav")
CSDN不支持播放音频
更多推荐
所有评论(0)