trigger word detection چیست؟
Trigger word کلمه ای است که با گفتن آن شروع به صحبت با voice assistant می کنیم. در حقیقت voice assistant ها در حال شنیدن صدا هستند و زمانی که ما یک trigger word خاص را می گوییم فعال می شوند.
به عنوان مثال با گفتن “?Hey Siri, what’s the weather like today”، ابزار siri متعلق به شرکت apple سعی در پیدا کردن وضعیت هوای امروز را می کند. در حقیقت ما در اینجا به نحوه ی trigger شدن اینگونه assistant ها می پردازیم.
برخی از voice assistant ها و trigger word های مربوط به آن ها را در زیر می بینیم:
قبل از شروع نگاهی به کتابخانه های مورد نیاز می اندازیم:
Pydub
Pydub کتابخانه کار با صوت پایتون است. در زیر برخی اعمال پایه ای که می توان با این کتابخانه انجام داد را می بینیم:
1 |
from pydub import AudioSegment |
Open a file
1 |
song = AudioSegment.from_wav("never_gonna_give_you_up.wav") |
یک فایل صوتی را میتوان لود کرد(با فرمت های مختلف) در بالا فایل صوتی با فرمت wav را لود کردیم.
Slice audio
1 2 3 4 5 6 |
# pydub does things in miliseconds ten_seconds = 10 * 1000 first_10_seconds = song[:10000] last_5_seconds = song[-5000:] |
کتابخانه pydub با واحد میلی ثانیه کار می کند.
Make the beginning louder and the end quieter
1 2 3 |
beginning = first_10_seconds + 6 end = last_5_seconds - 3 |
با – یا + می توان volume صدا را کم یا زیاد کرد(بر حسب db).
(Concatenate audio (add one file to the end of another
1 |
without_the_middle = beginning + end |
ادغام دو بخش صوت(که در اینجا 10 ثانیه اول و 5 ثانیه آخر را ادغام کردیم).
AudioSegments are immutable
1 2 |
# song is not modified backwards = song.reverse() |
می توان صوت را برعکس کرد.
Repeat
1 2 |
# repeat the clip twice do_it_over =backwards * 2 |
با * میتوان تعداد تکرار را مشخص کرد.
(Save the results (again whatever ffmpeg supports
1 |
do_it_over.export("mashup.mp3", format="mp3") |
می توان خروجی را با هر فرمتی که ffmpeg پشتیبانی می کند، ذخیره کرد.
Trigger Word Detection
کد ها برگرفته از نوت بوک مربوط به کورس sequence models سایت coursera می باشد که از این آدرس قابل دسترس است.
همچنین ویدیو های مربوط به این آموزش را میتوانید از آدرس های 1 و 2 مشاهده کنید.
1 2 3 4 5 6 7 8 9 |
from pydub import AudioSegment import random import sys import io import os import glob import IPython from td_utils import * %matplotlib inline |
کتابخانه های لازم را import می کنیم.
1 2 3 |
Ipython.display.Audio("./raw_data/activates/1.wav") Ipython.display.Audio("./raw_data/negatives/4.wav") Ipython.display.Audio("./raw_data/backgrounds/1.wav") |
( )Ipython.display.Audio تابع jupyter notebook برای نمایش فایل صوتی است.
Dataset
دیتاست شامل 3 پوشه است:
• پوشه صوت های trigger شدن که در اینجا کلمه activate است.
• پوشه صوت های negative برای negative sampling که شامل کلمات غیر trigger است.
• پوشه صوت های پس زمینه(background)
صوت های لازم را خودمان می سازیم، یعنی مثلا یک background را برداشته و روی آن کلمات trigger و negative را سوار می کنیم. (نمونه آن را در فایل صوتی “audio_examples/example_train.wav” می بینیم)
From audio recordings to spectrograms
ضبط صدا دقیقا چیست؟
فرض کنید میکروفون شما فرکانس نمونه برداری 44100 هرتز دارد(یعنی در هر ثانیه 44100 نمونه بر میدارد)، اگر 10 ثانیه صحبت کنید 441000 نمونه برداشته می شود.(بر اساس فشار هوای وارد شده بر میکروفون)
در این نوع representation دیتا ما تشخیص کلمه trigger سخت است و ما آن را به spectrogram تبدیل می کنیم.
1 |
x = graph_spectrogram("audio_examples/example_train.wav") |
با این تابع spectrogram صوت sample ما را بدست می آورد(محور x به ما time-step و محور y به ما فرکانس را نشان می دهد)
خروجی آن چنین است:
فرکانس ها را بازه بندی کردیم(مثلا در اینجا سبز نشانه شدت زیاد فرکانس و آبی نشانه شدت کم فرکانس است).
مقدار time-step به مقدار hyper parameter های spectrogram وابسته است. در اینجا 10 ثانیه طول صوت داشتیم و time-step ما مقدار𝑇𝑥=5511 دارد.(یک mapping بین time مورد نظر و time-step وجود دارد مثلا برای بدست آوردن ثانیه 5 ام میتوان 𝑇𝑥 را نصف کرد)
1 2 3 |
_, 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) |
خروجی
1 2 |
Time steps in audio recording before spectrogram (441000,) Time steps in input after spectrogram (101, 5511) |
قبل از تبدیل به spectrogram به اندازه 441000 نمونه داشتیم و پس از تبدیل dimension ما به (5511, 101) تغییر کرد. چون 5511 مقدار time-step ماست و در هر کدام 101 مقدار فرکانس داریم.
1 2 |
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 ثانیه ای مقادیر زیر را داریم:
- 441000 تعداد نمونه هایی که میکروفون ذخیره می کند.
- 5511=𝑇𝑥 خروجی spectrogram و تعداد time-step هایی است که به ورودی شبکه می دهیم.
- n_freq تعداد فرکانسی است که در هر time-step است.
- 1375=𝑇𝑦 خروجی شبکه
Generating a single training example
مراحل ساخت داده training:
-
یک صوت background ده ثانیه ای به صورت رندوم برمیداریم.
-
به صورت رندوم 0-4 صوت activate برداشته و به جاهای مختلف بدون همپوشانی اضافه(overlay) می کنیم.
-
به صورت رندوم 0-2 صوت negative برداشته و به جاهای مختلف بدون همپوشانی اضافه(overlay) می کنیم.
1 2 3 4 5 |
# 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 |
تابع ()load_raw_audio از توابع فایل td_utils.py، فایل های Wav درون فولدر activates, negatives و background را می خواند و در با فرمت datastructure خود pydub در لیست هایی به همین نام ها append می کند.
در این صوت 10 ثانیه ای لیبل خروجی را صفر میگذاریم، فقط در هر جایی کلمه activate تمام می شود 50 time-step خروجی را 1 میکنیم.
Helper Functions
1 2 3 4 5 6 |
def get_random_time_segment(segment_ms): segment_start = np.random.randint(low=0, high=10000-segment_ms) segment_end = segment_start + segment_ms - 1 return (segment_start, segment_end) |
این تابع یک time segment رندوم برای ما جدا می کند. به این ترتیب که ما به عنوان ورودی طول segment خود را برحسب میلی ثانیه وارد می کنیم. خروجی یک tuple است که شامل نقطه شروع و پایان segment است.(نقطه ی شروع طوری انتخاب می شود که اگر با طول segment آرگمان جمع شود از 10 ثانیه طول کلی بیشتر نشود)
1 2 3 4 5 6 7 8 9 10 11 |
def is_overlapping(segment_time, previous_segments): segment_start, segment_end = segment_time overlap = False for previous_start, previous_end in previous_segments: if segment_start <= previous_end and segment_end >= previous_start: overlap = True return overlap |
این تابع وضعیت همپوشانی segment time را با لیستی از segment time های قبلی چک می کند. در صورتی که همپوشانی داشته باشد، True برمیگرداند.(یادآوری: segment time یک tuple از نقطه ی شروع و پایان یک segment است)
1 2 3 4 5 6 7 8 9 10 11 12 |
def insert_audio_clip(background, audio_clip, previous_segments): segment_ms = len(audio_clip) segment_time = get_random_time_segment(segment_ms) while is_overlapping(segment_time, previous_segments): segment_time = get_random_time_segment(segment_ms) previous_segments.append(segment_time) new_background = background.overlay(audio_clip, position = segment_time[0]) return new_background, segment_time |
این تابع سه ورودی یک background(صوت 10 ثانیه ای)، یک audio_clip(یک صوت activate/negative) و time segment های قبلی را می گیرد و audio_clip را روی background قرار می دهد. به این شکل که ابتدا طول صوت audio_clip را می گیرد و در جایی از background که امکان قرار دادن آن وجود داشته باشد را پیدا کرده و روی آن overlay می کند. Time segment مربوط به این audio_clip را به time segment اضافه میکند(که اگر صوت جدیدی را بخواهیم overlay کنیم مشکلی پیش نیاید)
1 2 3 4 5 6 7 8 9 |
def insert_ones(y, segment_end_ms): segment_end_y = int(segment_end_ms * Ty / 10000.0) for i in range(segment_end_y+1, segment_end_y+51): if i < Ty: y[0, i] = 1.0 return y |
بعد هر بار کلمه activate ما 50 لیبل خروجی را یک بگذاریم. این تابع با توجه به نقطه ی پایان segment در فاصله 10 ثانیه نقطه ی متناظر با آن را در طول بردار خروجی(1375) پیدا می کند و 50 تای بعد آن 1 میگذارد(شرط if برای این است که اگر مثلا نقطه انتهایی در بردار خروجی 1370 بود، تا 1374 را یک کند)
یک نمونه از plot اجرای این تابع می بینیم:
1 2 |
arr1 = insert_ones(np.zeros((1, Ty)), 9700) plt.plot(insert_ones(arr1, 4251)[0,:]) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
def create_training_example(background, activates, negatives): np.random.seed(18) background = background – 20 y = np.zeros((1, Ty)) previous_segments = [] 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] for random_activate in random_activates: background, segment_time = insert_audio_clip(background, random_activate, previous_segments) segment_start, segment_end = segment_time y = insert_ones(y, segment_end) 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("File (train.wav) was saved in your directory.") x = graph_spectrogram("train.wav") return x, y |
به کمک این تابع training set خود را می سازیم.این تابع یک صوت background، یک لیست از صوت های activate و یک لیست از صوت های negative می گیرد. صدای background را کم کرده و لیست 1375 تایی خروجی می سازد.به صورت رندوم بین 0-4 صوت activate و همچنین 0-2 صوت negative را انتخاب کرده و بر روی background به کمک helper function های ذکر شده overlay می کند. در صورت overlay کردن activate ها 50 لیبل خروجی باید یک شود.(تابع match_target_amplitude نیز برای استاندارد سازی volume صوت نهایی بکار میرود)پس خروجی نیز spectrogram صوت ورودی و لیبل های منتاظر است.
Full training set
برای ایجاد training set می توان تابع بالا را به میزان دلخواه ران کرد. در اینجا ما از یک دیتا preprocess شده استفاده میکنیم.
ورودی ما دارای shape روبروست: (101, 5511, 26)
یعنی ما 26 تا صوت داریم.
1 2 |
X = np.load("./XY_train/X.npy") Y = np.load("./XY_train/Y.npy") |
Development set
برای development set بهتر است دیتا را synthesize نکنیم(یعنی مثل روشی که برای درست کردن training set استفاده شد، کلمات را روی background سوار نکنیم). در اینجا این دیتاست جمع آوری شده واقعا صدای ضبط شده ی 10 ثانیه ای است که لیبل زده شده است. دلیل آن هم این است که به test set و دیتای واقعی نزدیک تر است.
1 2 |
X_dev = np.load("./XY_dev/X_dev.npy") Y_dev = np.load("./XY_dev/Y_dev.npy") |
shape آن (101, 5511, 25) است یعنی 25 صوت داریم.
Model
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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 def model(input_shape): X_input = Input(shape = input_shape) X = Conv1D(196, 15, strides=4)(X_input) X = BatchNormalization()(X) X = Activation('relu')(X) X = Dropout(0.8)(X) X = GRU(units = 128, return_sequences=True)(X) X = Dropout(0.8)(X) X = BatchNormalization()(X) X = GRU(units = 128, return_sequences=True)(X) X = Dropout(0.8)(X) X = BatchNormalization()(X) X = Dropout(0.8)(X) X = TimeDistributed(Dense(1, activation = "sigmoid"))(X) model = Model(inputs = X_input, outputs = X) return model |
ساختار و summary شبکه بصورت زیر است:
در ابتدا به ورودی شبکه که 5511 تاست(یعنی خروجی spectrogram) یک کانولوشن 1 بعدی میزنیم با پارامتر های filter_size = 15 و stride = 4 که تعداد فیلتر ها 196 تا است.
طبق فرمول، بعد خروجی کانولوشن اینگونه حساب میشود:
(input-filter_size)/stride+1 = (5511-16)/4+1 = 1375
سپس دیتا را نرمال سازی می کنیم(در تمام لایه ها این روال را داریم) سپس به تابع ReLU پاس می دهیم و پس از آن به تابع dropout با مقدار 0.8 پاس می دهیم(در تمام لایه ها این روال را داریم).
در مرحله بعد به (128 تایی)GRU حالت stack شده پاس می دهیم(با پارامتر return_sequences=True).
در آخر نیز دیتا را به یک لایه dense و activation سیگموید میدهیم.
لازم به ذکر است که لایه dense بصورت TimeDitributed است که برای هر time step یک dense جدید تعریف نشود بلکه کلا یک dense داریم که پارمتر ها share شده اند(طول این لایه 129 تا است).
1 |
model = model(input_shape = (Tx, n_freq)) |
مدل را می سازیم.
Fit the model
می توان مدل را train کرد ولی در انجا ما از مدل pre-trained استفاده می کنیم.
1 2 3 4 5 6 |
model = load_model('./models/tr_model.h5') 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) |
مدل را با AdamOptimizer کامپایل کرده و یک epoch میزنیم.
Test the model
1 2 |
loss, acc = model.evaluate(X_dev, Y_dev) print("Dev set accuracy = ", acc) |
روی developent set به Accuray 95 درصد میرسیم.
Making Predictions
برای prediction از دو تابع زیر استفاده می کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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 |
این تابع یک فایل صوتی می گیرد و spectrogram آن را رسم می کند و پس از تغییر محور ها به (shape (5511, 101 , تغییر کرده و dimension آن را expand میکنیم(چون بصورت minibatch به شبکه می دهیم) و آن را به عنوان ورودی شبکه میدهیم.
خروجی تابع predict بصورت (1, 1375, 1) است(اولی بدلیل minibatch و آخری بدلیل لایه سیگموید است).
در آخر مقادیر predict شده را رسم می کند.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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] consecutive_timesteps = 0 for i in range(Ty): consecutive_timesteps += 1 if predictions[0,i,0] > threshold and consecutive_timesteps > 75: audio_clip = audio_clip.overlay(chime, position = ((i / Ty) * audio_clip.duration_seconds)*1000) consecutive_timesteps = 0 audio_clip.export("chime_output.wav", format='wav') |
این تابع یک صدای بوق را load می کند و درجایی که predict کرده که صدای activate را شنیده، این صدای بوق را overlay می کند.
این تابع دو هایپرپارامتر threshold و consecutive_timestep دارد که اولی آستانه ای برای خروجی سیگموند برای قبول کردن شرط activate بودن است و دومی به منظور این است که چندبار پشت هم صدای بوق را overlay نکنیم(که پس از overlay کردن آن را صفر می کنیم).
Test on dev examples
برای تست کردن فایل 2.wav را از پوشه development set بر میداریم.
1 2 3 4 |
filename = "./raw_data/dev/2.wav" prediction = detect_triggerword(filename) chime_on_activate(filename, prediction, 0.5) IPython.display.Audio("./chime_output.wav") |
در این فایل دوبار صورت activate شنیده شده است که دو بار peak داریم.
شبکه های اجتماعی