1. 程式人生 > >吳恩達Coursera深度學習課程 deeplearning.ai (5-3) 序列模型和注意力機制--程式設計作業(二):觸發字檢測

吳恩達Coursera深度學習課程 deeplearning.ai (5-3) 序列模型和注意力機制--程式設計作業(二):觸發字檢測

Part 2: 觸發字檢測

關鍵詞語音喚醒

觸發字檢測

歡迎來到這個專業課程的最終程式設計任務!

在本週的視訊中,你瞭解瞭如何將深度學習應用於語音識別。在本作業中,您將構建一個語音資料集並實現觸發字檢測演算法(有時也稱為關鍵字檢測或喚醒檢測)。觸發字檢測技術,可以讓亞馬遜Alexa,Google Home,Apple Siri和百度DuerOS等裝置在聽到某個詞語時進行喚醒。

本練習中,我們的觸發詞將是“Activate”。每當它聽到你說“Activate”時,它就會發出“chiming”聲音。在此作業結束時,您將能夠錄製自己正在講話的片段,並在演算法檢測到您說出“chiming”時讓演算法觸發一次鐘聲。

完成作業後,也許你還可以擴充套件到膝上型電腦上執行,這樣每當你說“chiming”它啟動你最喜歡的應用程式,或開啟你家的網路連線燈,或觸發一些其他事件。

image

本作業中,你將學到

  • 構建一個語音識別專案
  • 合成和處理音訊記錄以建立訓練/開發測試集
  • 訓練觸發字檢測模型並進行預測

導包

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 資料合成:建立語音資料集

首先為觸發字檢測演算法構建一個數據集。 理想情況下,語音資料集儘可能接近您希望執行的應用程式。 在這種情況下,您希望在工作環境(圖書館,家庭,辦公室,開放空間等)中檢測到“activate”一詞。 因此,您需要在不同的背景聲音中混合使用正面詞語(“activate”)和負面詞語(除activate以外的隨機詞)。 我們來看看如何建立這樣一個數據集。

1.1 聆聽資料

你的朋友正在幫助你完成這個專案,並且他們已經去過遍佈該地區的圖書館,咖啡館,餐館,家庭和辦公室,以記錄背景噪音,以及人們說正面/負面詞彙的片段的片段。 這個資料集包括以各種口音說話的人。

在raw_data目錄中,您可以找到正面單詞,負面單詞和背景噪音的原始音訊檔案的子集。 您將使用這些音訊檔案合成數據集來訓練模型。 “activate”目錄包含說“activate”的人的正面例子。 “negatives”目錄包含說除“activate”以外的隨機單詞的反面例子。 每個音訊記錄有一個詞。 “backgrounds”目錄包含步同環境下的背景噪音的10s的剪輯。

聆聽樣例資料

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

你將使用這三種類型的音訊資料建立標籤資料集。

1.2 從錄音到聲譜圖

什麼是錄音? 麥克風隨著時間的推移記錄氣壓的微小變化,正是這些氣壓的微小變化讓你的耳朵感覺到了聲音。 你可以想象一個錄音是一個長長的數字列表,用於測量麥克風檢測到的微小氣壓變化。 我們將使用以44100赫茲取樣的音訊。 這意味著麥克風每秒給我們44100個數字。 因此,10秒音訊剪輯由441000個數字(= 10×44100)表示。

從這個音訊的原始資料表示中找出是否包含“activate”這個詞是相當困難的。 為了幫助你的序列模型更容易學習檢測觸發字,我們將計算音訊的譜圖。 頻譜圖告訴我們一段時間內音訊片段中存在多少不同的頻率。

(如果你曾經學習過訊號處理或傅立葉變換上的課程,頻譜的計算時通過在原始音訊訊號上滑動視窗計算的,並使用傅立葉變換計算每個視窗中最活躍的頻率。 如果你不理解前面的句子,也不用擔心。)

讓我們看一個例子:

IPython.display.Audio("audio_examples/example_train.wav")
x = graph_spectrogram("audio_examples/example_train.wav")

image

上面的圖表表示每個頻率(y軸)在各個時間步(x軸)上的活動情況。

image

上圖是音訊記錄的頻譜圖,其中顏色顯示的是不同時間點音訊不同頻率的程度。 綠色方塊意味著音訊片段中的某個頻率更加活躍(聲音更響亮); 藍色方塊表示較少的活動頻率。

輸出譜圖的維度取決於譜圖軟體的超引數和輸入的長度。 在本文中,我們將使用10秒音訊剪輯作為我們培訓示例的“標準長度”。 頻譜圖的時間步數將為5511.稍後你會看到頻譜圖作為輸入X給帶網路中,因此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 # The number of time steps input to the model from the spectrogram
n_freq = 101 # Number of frequencies input to the model at each time step of the spectrogram

注意: 即使10秒作為我們預設的訓練示例長度,也可以將10秒的時間離散為不同的數值。 你已經看過441000(原始音訊)和5511(頻譜圖)。 在前一種情況下,每個時間步代表10/441000≈0.000023秒。 在第二種情況下,每個時間步代表10/5511≈0.0018秒。

對於10s的音訊,關鍵的值有:

  • 441000(原始音訊)
  • 5511=Tx(頻譜圖輸出,也是神經網路輸入的維度)
  • 10000(由pydub模組用於合成的音訊)
  • 1375=Ty(即將構建的GRU的輸出時間步的數量)

注意: 每一個樣本恰好10秒的時間,被不同型別進行離散化。這些都是超引數,可以更改(除了441000,這是麥克風的功能)。 這裡選擇了語音系統標準範圍內的值。

比如Ty = 1375意味著對於模型的輸出,我們將10秒離散成1375個時間間隔(每個長度為10 /1375≈0.0072秒),並嘗試預測這些時間間隔是否最近有人說過“activate”。

又如上面的10000這個數字,將10秒剪輯離散化為10/10000 = 0.001秒的間隔。 0.001秒也被稱為1毫秒 所以當我們說按照1ms間隔進行離散化時,這意味著正在使用10,000步。

Ty = 1375 # The number of time steps in the output of our model

1.3 生成一個訓練示例

由於語音資料很難獲取和標記,因此您將使用正向、反向和背景的音訊剪輯合成訓練資料。 記錄大量10秒的隨機“activates”音訊剪輯是很慢的。 相反,記錄大量正向和反向詞彙,並單獨記錄背景噪音(或從免費線上渠道下載背景噪音)更容易。

為了合成一個訓練樣本,你需要:

  • 隨機選擇一個10秒的背景音訊剪輯
  • 隨機將0-4個正向音訊片段插入此10秒剪輯中
  • 隨機將0-2個反向音訊片段插入此10秒剪輯中

因為您已將“activates”一詞合成到背景剪輯中,所以您確切知道10秒剪輯中何時出現“activates”。 稍後您會看到,這樣也更容易生成標籤y⟨t⟩。

您將使用pydub軟體包來處理音訊。 Pydub將原始音訊檔案轉換為Pydub資料結構列表(這裡瞭解細節並不重要)。 Pydub使用1ms作為離散化間隔(1ms是1毫秒= 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: 916
# activate[1] len: 1579

在背景上覆蓋正面/負面的詞語

給定一個10秒的背景剪輯和一個短的音訊剪輯(正面或負面的單詞),您需要能夠將單詞的短片段“新增”或“插入”背景。 為確保插入到背景上的音訊片段不重疊,需要跟蹤以前插入的音訊片段的時間。 您將在背景中插入多個正面/負面單詞剪輯,並且不希望插入有重疊。

為了清楚起見,當您在咖啡廳噪音的10秒剪輯中插入1秒“activate”時,最終會出現10秒的剪輯,聽起來像某人在咖啡廳中說“activate”。你不會以11秒的剪輯結束。 稍後你會看到pydub如何讓你做到這一點。

在插入的同時建立標籤

回想一下,標籤y⟨t⟩表示某人是否剛說完“activate”。給定一個背景剪輯,我們可以初始化所有t的y⟨t⟩= 0,因為該剪輯不包含任何“activate”。

當你插入或覆蓋“activate”剪輯時,您還將更新yt的標籤,以便輸出的50個步驟具有目標標籤1.您將訓練GRU以檢測某人何時說完“啟用”。 例如,假設合成的“activate”剪輯在10秒音訊中的5秒處結束 - 恰好在剪輯的中途。 回想一下Ty = 1375,所以時間步長687 = int(1375 * 0.5)對應於5秒進入音訊的時刻。 所以,你會設定y688= 1。 此外,如果GRU在短時間內在任何時間內檢測到“activate”,那麼在此時刻之後,您會非常滿意,所以我們實際上將標籤yt的50個連續值設定為1.具體來說,我們有y688=y689==y737=1

這是合成訓練資料的另一個原因:上面描述的生成這些標籤y⟨t⟩比較簡單;相反,如果在麥克風上錄製了10秒的音訊,那麼聽到該音訊並且在“activate”完成時手動標記是非常耗時的。

下面是一張插圖,展示了插入“activate”,“innocent”,“activate”,“baby” 的剪輯的標籤y⟨t⟩。請注意,正面標籤“1”僅與正面字詞相關。

image

要實現訓練集合成過程,您將使用以下輔助函式。 所有這些功能將使用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_segment 和 is_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

    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)

檢查新的時間片是否與之前的任意時間片有重疊。這需要兩步:
1. 建立”false”標籤,稍後如果有重疊則置為”true”
2. 瀏覽之前插入時間片的開始和結束時間,比較與新時間片是否有重疊,如果有則將標籤置為”true”。

for ....:
        if ... <= ... and ... >= ...:
            ...

提示:如果新的時間片在上一個時間片結束之前開始,或者在下一個時間片開始之後結束,都是有重疊。

# 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)
print("Overlap 2 = ", overlap2)

Overlap 1 =  False
Overlap 2 =  True

期待的輸出

key value
Overlap 1 False
Overlap 2 True

現在,我們隨機將一個新的音訊片段插入到10秒的背景中,但要確保任何新插入的片段都不會與之前的片段重疊。

練習:實現 insert_audio_clip()

將一個新的音訊片段插入到10秒的背景中,你需要完成4步:

  1. 以毫秒為單位獲取隨機時間段。
  2. 確保時間段與前面的任何時間段都不重疊;如果重疊,則返回步驟1並選擇新的時間段。
  3. 將新時間段新增到現有時間段列表中,以跟蹤插入的所有時間段。
  4. 使用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:  (2254, 3169)

期待的輸出

key value
Segment Time (2254, 3169)
# Expected audio
IPython.display.Audio("audio_examples/insert_reference.wav")

最後,假設你剛剛插入一個“activate”,實現程式碼來更新標籤yt。在下面的程式碼中,y是一個(1,1375)維向量,因為Ty = 1375。

如果“activate”在時間步驟t結束,則設定yt+1=y<t+2>=y<t+50>個連續值,但是Ty = 1375,注意 y<t+m> 不能越界。

練習:實現 insert_ones()

你可以使用for迴圈。
(如果你是python的slice操作的專家,也可以使用切片來將其向量化)。
如果一個段在segment_end_ms處結束(使用10000步離散化),將其轉換為輸出y的索引(使用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+51):
        if i < Ty:
            y[0, i] = 1.0
    ### 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])

# sanity checks: 0.0 1.0 0.0

image

最後,你可以使用insert_audio_clip 和 insert_ones 建立一個新的訓練樣本。

練習:實現 create_training_example()
  1. 將標籤向量y初始化為零值的(1,Ty)numpy陣列
  2. 將已存在時間片集合初始化為空列表。
  3. 隨機選擇0至4個“activate”音訊剪輯,並將其插入10秒剪輯,記著將標籤插入標籤向量y中的正確位置
  4. 隨機選擇0到2個負面音訊片段,並將它們插入10秒片段。
# 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)
    ### 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, _ = background, segment_time = 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)

# File (train.wav) was saved in your directory.

image

現在你可以聽一下你建立的新樣本,和上面的頻譜比較一下。

IPython.display.Audio("train.wav")

期待的輸出

IPython.display.Audio("audio_examples/train_reference.wav")

最後,你可以將生成的訓練樣本的相關標籤繪製成圖。

plt.plot(y[0])

image

1.4 全部訓練集

你現在已經實現了生成單個訓練樣本所需的程式碼。 我們將使用這個過程來生成一個大的訓練集。 為了節省