触发词检测

实验题目

  • 触发词检测

实验内容

  • 本次实验我们了解如何将深度学习应用于语音识别,我们将构建语音数据集并实现触发词检测算法(有时也称为关键字检测或唤醒词检测)。触发词检测是一项技术,可以让 Amazon Alexa、Google Home、Apple Siri 和百度 DuerOS 等设备在听到某个词时唤醒。

触发词检测

  • 本练习的触发词将是“激活”。当我们每次听到说“激活”时,都会发出“叮咚”的声音。在本次作业结束时,我们也可以录制自己说话的片段,并让算法在检测到说“激活”时触发提示音。

  • 在本次作业中我们将学习:

    • 构建语音识别项目
    • 合成和处理录音以创建训练/开发数据集
    • 训练触发词检测模型并进行预测

实验步骤

导入实验所需的相关库

# Keras==2.2.5 tensorflow==1.15.0
!pip install pydub
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

1 - 数据合成:创建语音数据集

  • 首先我们为触发词检测算法构建一个数据集。理想情况下,语音数据集应尽可能接近希望在其上运行的应用程序。在这种情况下,我们希望在工作环境(图书馆、家、办公室、开放空间……)中检测“激活”一词。因此,需要在不同的背景声音中混合使用正面词(“激活”)和负面词(激活以外的随机词)来创建录音。

1.1 - 收听数据

  • 在 raw_data 目录中,我们可以找到正面词、负面词和背景噪音的原始音频文件的子集。我们将使用这些音频文件合成数据集来训练模型。 “activate”目录包含人们说“activate”这个词的正面例子。 “否定”目录包含人们说“激活”以外的随机词的否定示例。每个录音有一个词。 “背景”目录包含 10 秒不同环境中的背景噪音剪辑,我们将使用这三种类型的记录(正/负/背景)来创建标记数据集,下面我们可以听一下录音示例:

    IPython.display.Audio("./raw_data/activates/1.wav")
    

触发词检测

1.2 - 从录音到频谱图

  • 什么才是真正的录音?随着时间的推移,麦克风记录的气压变化很小,而正是这些气压的微小变化,我们的耳朵也会将其感知为声音。我们可以将录音视为一长串数字,用于测量麦克风检测到的微小气压变化。我们将使用以 44100 Hz(或 44100 赫兹)采样的音频。这意味着麦克风每秒为我们提供 44100 个数字。因此,一个 10 秒的音频剪辑由 441000 个数字表示(= $10 \times 44100 $)。

  • 很难从音频的这种“原始”表示中确定是否说了“激活”这个词。为了帮助我们的序列模型更轻松地学习检测触发词,我们将计算音频的频谱图。频谱图告诉我们在某一时刻音频剪辑中存在多少不同的频率,下面我们生成一段音频的图谱:

    x = graph_spectrogram("audio_examples/example_train.wav")
    
  • 实现效果如下

    触发词检测

  • 上图表示每个频率(y 轴)在多个时间步长(x 轴)上的活跃程度。

    触发词检测

    **图 1**:音频记录的频谱图,其中颜色显示不同时间点音频中不同频率的出现(响亮)程度。 绿色方块表示某个频率在音频剪辑中更活跃或更频繁(更响亮); 蓝色方块表示较不活跃的频率。

1.3 - 生成单个训练示例

  • 由于语音数据难以获取和标记,因此我们将使用激活、底片和背景的音频剪辑来合成训练数据。录制大量带有随机“激活”的 10 秒音频剪辑非常慢。相反,更容易记录大量正面和负面的词,并分别记录背景噪音(或从免费在线资源下载背景噪音)。

  • 要合成单个训练示例,我们将:

    • 选择一个随机的 10 秒背景音频剪辑
    • 在这个 10 秒的剪辑中随机插入 0-4 个“激活”的音频剪辑
    • 在这个 10 秒的剪辑中随机插入 0-2 个否定词的音频剪辑
  • 由于我们已将“激活”一词合成到背景剪辑中,因此我们确切地知道在 10 秒剪辑中“激活”出现的时间。稍后我们将看到,这也使生成标签 y ⟨ t ⟩ y^{\langle t \rangle} y⟨t⟩ 变得更加容易。

  • 我们将使用pydub包来操作音频。 Pydub 将原始音频文件转换为 Pydub 数据结构列表(了解这里的细节并不重要)。 Pydub 使用 1ms 作为离散化间隔(1ms = 1/1000 秒),这就是为什么 10 秒剪辑总是使用 10,000 步表示的原因。

    # Load audio segments using pydub 
    activates, negatives, backgrounds = load_raw_audio()
    
    print("background len: " + str(len(backgrounds[0])))    # Should be 10,000, since it is a 10 sec clip
    print("activate[0] len: " + str(len(activates[0])))     # Maybe around 1000, since an "activate" audio clip is usually around 1 sec (but varies a lot)
    print("activate[1] len: " + str(len(activates[1])))     # Different "activate" clips can have different lengths 
    
    # background len: 10000 
    # activate[0] len: 721 
    # activate[1] len: 731
    

在背景上叠加正面/负面词

  • 给定一个 10 秒的背景剪辑和一个简短的音频剪辑(正面或负面的单词),我们需要将单词的简短音频剪辑“添加”或“插入”到背景上。为确保插入到背景中的音频片段不重叠,我们将跟踪先前插入的音频片段的时间。我们将在背景上插入多个正面/负面词的剪辑,并且不想在与之前添加的另一个剪辑重叠的地方插入“激活”或随机词。

  • 为清楚起见,当我们将 1 秒的“激活”插入 10 秒的咖啡馆噪音片段时,最终会得到一个 10 秒的剪辑,听起来就像有人在咖啡馆里说“激活”,而“激活”叠加在背景咖啡馆噪音上,没有得到一个 11 秒的剪辑,后面我们将看到 pydub 如何执行此操作。

在叠加的同时创建标签

  • 还记得标签 y ⟨ t ⟩ y^{\langle t \rangle} y⟨t⟩ 表示某人是否刚刚说完“激活”。给定一个背景剪辑,我们可以为所有 t t t 初始化 y ⟨ t ⟩ = 0 y^{\langle t \rangle}=0 y⟨t⟩=0,因为该剪辑不包含任何“激活”。

  • 当我们插入或覆盖“激活”剪辑时,还将更新 y ⟨ t ⟩ y^{\langle t \rangle} y⟨t⟩ 的标签,以便输出的 50 步现在具有目标标签 1。我们将训练 GRU 以检测何时有人完成说“激活”。例如,假设合成的“激活”剪辑在 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 在此时刻之后的短时间内检测到“激活”,我们会非常满意,因此我们实际上将标签 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 秒的音频,那么当“激活”完成时,一个人要听它并准确地手动标记会非常耗时。

  • 下面是一个说明标签 y ⟨ t ⟩ y^{\langle t \rangle} y⟨t⟩ 的图,对于我们插入了“激活”、“无辜”、“激活”、“宝贝”的剪辑。请注意,正标签“1”是相关联的只有积极的话。

触发词检测

**图 2**
  • 要实现训练集合成过程,我们将使用以下辅助函数。所有这些函数都将使用 1ms 的离散化间隔,因此 10 秒的音频总是被离散化为 10,000 步。

    • 1.get_random_time_segment(segment_ms)在我们的背景音频中获取一个随机的时间段
    • 2.is_overlapping(segment_time, existing_segments) 检查时间段是否与现有段重叠
    • 3.insert_audio_clip(background, audio_clip, existing_times) 使用 get_random_time_segmentis_overlapping 在我们的背景音频中随机插入一个音频片段
    • 4.insert_ones(y, segment_end_ms) 在单词“activate”之后将 1 插入到我们的标签向量 y 中
  • 函数** get_random_time_segment(segment_ms) **返回一个随机时间段,我们可以在其中插入持续时间为“segment_ms”的音频剪辑。

    def get_random_time_segment(segment_ms):
        """
        Gets a random time segment of duration segment_ms in a 10,000 ms audio clip.
        
        Arguments:
        segment_ms -- the duration of the audio clip in ms ("ms" stands for "milliseconds")
        
        Returns:
        segment_time -- a tuple of (segment_start, segment_end) in ms
        """
        
        segment_start = np.random.randint(low=0, high=10000-segment_ms)   # Make sure segment doesn't run past the 10sec background 
        segment_end = segment_start + segment_ms - 1
        print("segment_time is [%d,%d]"%(segment_start,segment_end))
        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 个步骤:

    • 1.创建一个“False”标志,如果我们发现有重叠,稍后将设置为“True”。
    • 2.循环previous_segments 的开始和结束时间。将这些时间与段的开始和结束时间进行比较。如果存在重叠,则将 (1) 中定义的标志设置为 True。
    # GRADED FUNCTION: is_overlapping
    
    def is_overlapping(segment_time, previous_segments):
        """
        Checks if the time of a segment overlaps with the times of existing segments.
        
        Arguments:
        segment_time -- a tuple of (segment_start, segment_end) for the new segment
        previous_segments -- a list of tuples of (segment_start, segment_end) for the existing segments
        
        Returns:
        True if the time segment overlaps with any of the existing segments, False otherwise
        """
        
        segment_start, segment_end = segment_time
        
        ### START CODE HERE ### (≈ 4 line)
        # Step 1: Initialize overlap as a "False" flag. (≈ 1 line)
        overlap = False
        
        # Step 2: loop over the previous_segments start and end times.
        # Compare start/end times and set the flag to True if there is an overlap (≈ 3 lines)
        for previous_start, previous_end in previous_segments:
            if segment_start<=previous_end and segment_end>=previous_start:
                overlap = True
        ### END CODE HERE ###
    
        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) # False
    print("Overlap 2 = ", overlap2) # True
    
  • 现在,让我们使用之前的辅助函数在 10 秒的背景中随机插入一个新的音频片段,但要确保任何新插入的片段不与之前的片段重叠。

  • 练习:实现 insert_audio_clip() 将音频剪辑叠加到背景 10 秒剪辑上。 我们将需要执行 4 个步骤:

      1. 获取以毫秒为单位的正确持续时间的随机时间段。
      1. 确保该时间段不与之前的任何时间段重叠。 如果重叠,则返回步骤 1 并选择一个新的时间段。
      1. 将新的时间段添加到现有时间段列表中,以便跟踪我们插入的所有时间段。
      1. 使用 pydub 将音频剪辑覆盖在背景上。
    # GRADED FUNCTION: insert_audio_clip
    
    def insert_audio_clip(background, audio_clip, previous_segments):
        """
        Insert a new audio segment over the background noise at a random time step, ensuring that the 
        audio segment does not overlap with existing segments.
        
        Arguments:
        background -- a 10 second background audio recording.  
        audio_clip -- the audio clip to be inserted/overlaid. 
        previous_segments -- times where audio segments have already been placed
        
        Returns:
        new_background -- the updated background audio
        """
        
        # Get the duration of the audio clip in ms
        segment_ms = len(audio_clip)
        
        ### START CODE HERE ### 
        # Step 1: Use one of the helper functions to pick a random time segment onto which to insert 
        # the new audio clip. (≈ 1 line)
        segment_time = get_random_time_segment(segment_ms)
        
        # Step 2: Check if the new segment_time overlaps with one of the previous_segments. If so, keep 
        # picking new segment_time at random until it doesn't overlap. (≈ 2 lines)
        while is_overlapping(segment_time,previous_segments):
            segment_time = get_random_time_segment(segment_ms)
    
        # Step 3: Add the new segment_time to the list of previous_segments (≈ 1 line)
        previous_segments.append(segment_time)
        ### END CODE HERE ###
        
        # Step 4: Superpose audio segment and background
        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)
    
  • 最后,实现代码来更新标签 y ⟨ t ⟩ y^{\langle t \rangle} y⟨t⟩,假设我们刚刚插入了一个“激活”。在下面的代码中,y 是一个 (1,1375) 维向量,因为 T y = 1375 T_y = 1375 Ty​=1375。

  • 如果“激活”在时间步 t t t 结束,则设置 y ⟨ t + 1 ⟩ = 1 y^{\langle t+1 \rangle} = 1 y⟨t+1⟩=1 以及最多 49 个附加连续值。但是,请确保不会超出数组的末尾并尝试更新y[0][1375],因为有效索引是y[0][0]y[0][1374], 因为 T y = 1375 T_y = 1375 Ty​=1375。因此,如果“激活”在步骤 1370 结束,我们将只得到 y[0][1371] = y[0][1372] = y[0][1373] = y[0][1374] = 1

  • 练习:实现insert_ones(),可以使用 for 循环。如果一个段以 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):
        """
        Update the label vector y. The labels of the 50 output steps strictly after the end of the segment 
        should be set to 1. By strictly we mean that the label of segment_end_y should be 0 while, the
        50 followinf labels should be ones.
        
        
        Arguments:
        y -- numpy array of shape (1, Ty), the labels of the training example
        segment_end_ms -- the end time of the segment in ms
        
        Returns:
        y -- updated labels
        """
        
        # duration of the background (in terms of spectrogram time-steps)
        segment_end_y = int(segment_end_ms * Ty / 10000.0)
        
        # Add 1 to the correct index in the background label (y)
        ### START CODE HERE ### (≈ 3 lines)
        for i in range(segment_end_y + 1, segment_end_y + 1 + 50):
            if i < Ty:
                y[0, i] = 1
        ### END CODE HERE ###
        
        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])
    

触发词检测

  • 最后,我们可以使用 insert_audio_clipinsert_ones 创建一个新的训练示例。

  • 练习:实现create_training_example(),我们需要执行以下步骤:

    • 1.将标签向量 y y y 初始化为一个由零和形状 ( 1 , T y ) (1, T_y) (1,Ty​) 组成的 numpy 数组。
    • 2.将现有段集初始化为空列表。
    • 3.随机选择0到4个“激活”的音频片段,插入到10秒片段中。 还要在标签向量 y y y 中的正确位置插入标签。
    • 4.随机选择0到2个负片音频,插入10sec片段。
    # GRADED FUNCTION: create_training_example
    
    def create_training_example(background, activates, negatives):
        """
        Creates a training example with a given background, activates, and negatives.
        
        Arguments:
        background -- a 10 second background audio recording
        activates -- a list of audio segments of the word "activate"
        negatives -- a list of audio segments of random words that are not "activate"
        
        Returns:
        x -- the spectrogram of the training example
        y -- the label at each time step of the spectrogram
        """
        
        # Set the random seed
        np.random.seed(18)
        
        # Make background quieter
        background = background - 20
    
        ### START CODE HERE ###
        # Step 1: Initialize y (label vector) of zeros (≈ 1 line)
        y = np.zeros((1, Ty))
    
        # Step 2: Initialize segment times as empty list (≈ 1 line)
        previous_segments = []
        ### END CODE HERE ###
        
        # Select 0-4 random "activate" audio clips from the entire list of "activates" recordings
        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]
        
        ### START CODE HERE ### (≈ 3 lines)
        # Step 3: Loop over randomly selected "activate" clips and insert in background
        for random_activate in random_activates:
            # Insert the audio clip on the background
            background, segment_time = insert_audio_clip(background,random_activate,previous_segments)
            # Retrieve segment_start and segment_end from segment_time
            segment_start, segment_end = segment_time
            # Insert labels in "y"
            y = insert_ones(y,segment_end_ms=segment_end)
        ### END CODE HERE ###
    
        # Select 0-2 random negatives audio recordings from the entire list of "negatives" recordings
        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]
    
        ### START CODE HERE ### (≈ 2 lines)
        # Step 4: Loop over randomly selected negative clips and insert in background
        for random_negative in random_negatives:
            # Insert the audio clip on the background 
            background, _ = insert_audio_clip(background,random_negative,previous_segments)
        ### END CODE HERE ###
        
        # Standardize the volume of the audio clip 
        background = match_target_amplitude(background, -20.0)
    
        # Export new training example 
        file_handle = background.export("train" + ".wav", format="wav")
        print("File (train.wav) was saved in your directory.")
        
        # Get and plot spectrogram of the new recording (background with superposition of positive and negatives)
        x = graph_spectrogram("train.wav")
        
        return x, y
    
    x, y = create_training_example(backgrounds[0], activates, negatives)
    
  • 实现效果如下

触发词检测

  • 现在我们可以聆听创建的训练示例并将其与上面生成的频谱图进行比较。

    IPython.display.Audio("train.wav")
    IPython.display.Audio("audio_examples/train_reference.wav")
    
  • 最后,我们可以为生成的训练示例绘制相关标签如下

    plt.plot(y[0])
    

    触发词检测

1.4 - 完整的训练集

  • 我们现在已经实现了生成单个训练示例所需的代码,接下来我们使用这个过程来生成一个大的训练集。为了节省时间,我们已经生成了一组训练示例,直接调用

    # Load preprocessed training examples
    X = np.load("./XY_train/X.npy")
    Y = np.load("./XY_train/Y.npy")
    

1.5 - 开发集

  • 为了测试我们的模型,我们记录了一个包含 25 个示例的开发集。 虽然我们的训练数据是合成的,但我们希望使用与实际输入相同的分布来创建一个开发集。 因此,我们录制了 25 个 10 秒的音频片段,这些片段是人们说“激活”和其他随机词,并手工标记它们。 这遵循了之前描述的原则,即我们应该创建与测试集分布尽可能相似的开发集,这就是我们的开发集使用真实音频而不是合成音频的原因,接下来我们加载预处理的开发集示例

    # Load preprocessed dev set examples
    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
    

2.1 - 构建模型

  • 这是我们将使用的架构

    触发词检测

  • 该模型的一个关键步骤是一维卷积步骤(靠近图 3 的底部)。它输入5511步谱图,输出1375步输出,再经过多层进一步处理,得到最终的 T y = 1375 T_y=1375 Ty​=1375步输出。该层的作用类似于 2D 卷积,即提取低级特征,然后可能生成更小维度的输出。

  • 在计算上,一维卷积层也有助于加速模型,因为现在 GRU 只需要处理 1375 个时间步而不是 5511 个时间步。两个 GRU 层从左到右读取输入序列,然后最终使用密集 + sigmoid 层对 y ⟨ t ⟩ y^{\langle t \rangle} y⟨t⟩ 进行预测。因为 y y y 是二进制值(0 或 1),我们在最后一层使用 sigmoid 输出来估计输出为 1 的机会,对应于刚刚说“激活”的用户。

  • 请注意,我们使用单向 RNN 而不是双向 RNN。这对于触发词检测非常重要,因为我们希望能够在触发词被说出后几乎立即检测到。如果我们使用双向 RNN,我们将不得不等待整个 10 秒的音频被记录下来,然后才能判断音频剪辑的第一秒中是否说了“激活”。

  • 模型的实现可以分为四个步骤:

    步骤 1:CONV 层,使用 Conv1D() 来实现,有 196 个过滤器,过滤器大小为 15(kernel_size=15),步长为 4。[参见文档。]

    步骤2:第一个 GRU 层。要生成 GRU 层,可以使用:
    X = GRU(单位 = 128,return_sequences = True)(X)
    设置 return_sequences=True 确保所有 GRU 的隐藏状态都被馈送到下一层。请记住在 Dropout 和 BatchNorm 层中遵循这一点。

    步骤 3:第二个 GRU 层。这类似于之前的 GRU 层(记住使用 return_sequences=True),但是有一个额外的 dropout 层。

    步骤4:创建一个时间分布的密集层如下:
    X = TimeDistributed(Dense(1, activation = "sigmoid"))(X)
    这会创建一个密集层,然后是一个 sigmoid,这样密集层使用的参数对于每个时间步都是相同的。 [参见文档。]

  • 练习:实现model(),架构如上图所示,实现代码如下:

    # GRADED FUNCTION: model
    
    def model(input_shape):
        """
        Function creating the model's graph in Keras.
        
        Argument:
        input_shape -- shape of the model's input data (using Keras conventions)
    
        Returns:
        model -- Keras model instance
        """
        
        X_input = Input(shape = input_shape)
        
        ### START CODE HERE ###
        
        # Step 1: CONV layer (≈4 lines)
        X = Conv1D(filters=196, kernel_size=15, strides=4)(X_input)     # CONV1D
        X = BatchNormalization()(X)                                     # Batch normalization
        X = Activation('relu')(X)                                       # ReLu activation
        X = Dropout(0.8)(X)                                             # dropout (use 0.8)
    
        # Step 2: First GRU Layer (≈4 lines)
        X = GRU(units=128, return_sequences=True)(X)                    # GRU (use 128 units and return the sequences)
        X = Dropout(0.8)(X)                                             # dropout (use 0.8)
        X = BatchNormalization()(X)                                     # Batch normalization
        
        # Step 3: Second GRU Layer (≈4 lines)
        X = GRU(units=128, return_sequences=True)(X)                    # GRU (use 128 units and return the sequences)
        X = Dropout(0.8)(X)                                             # dropout (use 0.8)
        X = BatchNormalization()(X)                                     # Batch normalization
        X = Dropout(0.8)(X)                                             # dropout (use 0.8)
        
        # Step 4: Time-distributed dense layer (≈1 line)
        X = TimeDistributed(Dense(1, activation = "sigmoid"))(X)        # time distributed  (sigmoid)
    
        ### END CODE HERE ###
    
        model = Model(inputs = X_input, outputs = X)
        
        return model  
    
  • 生成模型

    model = model(input_shape = (Tx, n_freq))
    
  • 让我们打印模型摘要以跟踪形状

    model.summary()
    
  • 网络的输出形状为 (None, 1375, 1),而输入形状为 (None, 5511, 101)。 Conv1D 将频谱图的步数从 5511 减少到 1375。

2.2 - Fit the model

  • 触发词检测需要很长时间来训练。 为了节省时间,我们已经使用上面构建的架构在 GPU 上训练了大约 3 小时的模型,以及大约 4000 个示例的大型训练集,让我们加载模型

    model = load_model('./models/tr_model.h5')
    
  • 我们可以使用 Adam 优化器和二元交叉熵损失进一步训练模型,如下所示。 这将运行得很快,因为我们仅针对一个时期进行训练,并使用 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)
    
  • 可以看到训练过程如下

    触发词检测

2.3 - 测试模型

  • 最后,让我们看看您的模型在开发集上的表现如何

    loss, acc = model.evaluate(X_dev, Y_dev)
    print("Dev set accuracy = ", acc)
    
    # Dev set accuracy =  0.9451636075973511
    
  • 这看起来很不错! 然而,准确度并不是这项任务的重要指标,因为标签严重偏向于 0,因此仅输出 0 的神经网络将获得略高于 90% 的准确度。 我们可以定义更有用的指标,例如 F1 分数或 Precision/Recall。 但我们不要在这里操心,而只是凭经验看看模型是如何做的。

3 - 做出预测

  • 现在我们已经为触发词检测构建了一个工作模型,让我们用它来进行预测。 此代码片段通过网络运行音频(保存在 wav 文件中)

    def detect_triggerword(filename):
        plt.subplot(2, 1, 1)
    
        x = graph_spectrogram(filename)
        # the spectogram outputs (freqs, Tx) and we want (Tx, freqs) to input into the model
        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
    
  • 一旦我们估计了在每个输出步骤中检测到“激活”一词的概率,您就可以在概率高于某个阈值时触发“钟声”声音播放。 此外,在说“激活”之后,对于连续许多值, y ⟨ t ⟩ y^{\langle t \rangle} y⟨t⟩ 可能接近 1,但我们只想鸣响一次。 所以我们最多每 75 个输出步插入一次提示音。 这将有助于防止我们为单个“激活”实例插入两个钟声(这起到类似于计算机视觉中的非最大抑制的作用)

    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]
        # Step 1: Initialize the number of consecutive output steps to 0
        consecutive_timesteps = 0
        # Step 2: Loop over the output steps in the y
        for i in range(Ty):
            # Step 3: Increment consecutive output steps
            consecutive_timesteps += 1
            # Step 4: If prediction is higher than the threshold and more than 75 consecutive output steps have passed
            if predictions[0,i,0] > threshold and consecutive_timesteps > 75:
                # Step 5: Superpose audio and background using pydub
                audio_clip = audio_clip.overlay(chime, position = ((i / Ty) * audio_clip.duration_seconds)*1000)
                # Step 6: Reset consecutive output steps to 0
                consecutive_timesteps = 0
            
        audio_clip.export("chime_output.wav", format='wav')
    

3.1 - Test on dev examples

  • 让我们探索一下我们的模型如何处理来自开发集的两个看不见的音频片段。 让我们先听听两个开发集剪辑。

    IPython.display.Audio("./raw_data/dev/1.wav")
    IPython.display.Audio("./raw_data/dev/2.wav")
    
  • 现在让我们在这些音频剪辑上运行模型,看看它是否在“激活”后添加了提示音!

    filename = "./raw_data/dev/1.wav"
    prediction = detect_triggerword(filename)
    chime_on_activate(filename, prediction, 0.5)
    IPython.display.Audio("./chime_output.wav")
    

    触发词检测

    filename  = "./raw_data/dev/2.wav"
    prediction = detect_triggerword(filename)
    chime_on_activate(filename, prediction, 0.5)
    IPython.display.Audio("./chime_output.wav")
    

触发词检测

4 - 试试自己的例子

  • 录制一段 10 秒的音频片段,让您说出“激活”这个词和其他随机词,然后将其作为“myaudio.wav”上传到 Coursera 中心。 请务必将音频上传为 wav 文件。 如果您的音频以不同的格式(例如 mp3)录制,您可以在网上找到免费软件将其转换为 wav。 如果您的录音不是 10 秒,下面的代码将根据需要修剪或填充它以使其为 10 秒。

    # Preprocess the audio to the correct format
    def preprocess_audio(filename):
        # Trim or pad audio segment to 10000ms
        padding = AudioSegment.silent(duration=10000)
        segment = AudioSegment.from_wav(filename)[:10000]
        segment = padding.overlay(segment)
        # Set frame rate to 44100
        segment = segment.set_frame_rate(44100)
        # Export as wav
        segment.export(filename, format='wav')
    
  • 将音频文件上传到 Coursera 后,将文件路径放在下面的变量中。

    your_filename = "audio_examples/my_audio.wav"
    preprocess_audio(your_filename)
    IPython.display.Audio(your_filename) # listen to the audio you uploaded 
    
  • 最后,使用模型预测在 10 秒音频剪辑中说“激活”的时间,并触发提示音。 如果未正确添加哔声,请尝试调整 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")
    

    触发词检测

上一篇:暂时


下一篇:Zerocoin: Anonymous Distributed E-Cash from Bitcoin