זיהוי שמע פשוט: זיהוי מילות מפתח

הצג באתר TensorFlow.org הפעל בגוגל קולאב צפה במקור ב-GitHub הורד מחברת

מדריך זה מדגים כיצד לעבד מראש קבצי אודיו בפורמט WAV ולבנות ולהכשיר מודל בסיסי של זיהוי דיבור אוטומטי (ASR) לזיהוי עשר מילים שונות. תשתמש בחלק ממערך הנתונים של פקודות דיבור ( Warden, 2018 ), המכיל קטעי אודיו קצרים (באורך שנייה אחת או פחות) של פקודות, כגון "למטה", "לך", "שמאלה", "לא", " נכון", "עצור", "למעלה" ו"כן".

מערכות זיהוי דיבור ואודיו בעולם האמיתי הן מורכבות. אבל, כמו סיווג תמונות עם מערך הנתונים של MNIST , מדריך זה אמור לתת לך הבנה בסיסית של הטכניקות המעורבות.

להכין

ייבוא ​​מודולים ותלות נחוצים. שים לב שאתה תשתמש ב- seaborn להדמיה במדריך זה.

import os
import pathlib

import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
import tensorflow as tf

from tensorflow.keras import layers
from tensorflow.keras import models
from IPython import display

# Set the seed value for experiment reproducibility.
seed = 42
tf.random.set_seed(seed)
np.random.seed(seed)

ייבא את מערך הנתונים המיני של פקודות דיבור

כדי לחסוך זמן עם טעינת נתונים, תעבוד עם גרסה קטנה יותר של מערך הנתונים של פקודות דיבור. מערך הנתונים המקורי מורכב מיותר מ-105,000 קובצי אודיו בפורמט קובץ האודיו WAV (Waveform) של אנשים האומרים 35 מילים שונות. נתונים אלה נאספו על ידי Google ושוחררו תחת רישיון CC BY.

הורד וחלץ את הקובץ mini_speech_commands.zip המכיל את מערכי הנתונים הקטנים יותר של פקודות דיבור עם tf.keras.utils.get_file :

DATASET_PATH = 'data/mini_speech_commands'

data_dir = pathlib.Path(DATASET_PATH)
if not data_dir.exists():
  tf.keras.utils.get_file(
      'mini_speech_commands.zip',
      origin="http://storage.googleapis.com/download.tensorflow.org/data/mini_speech_commands.zip",
      extract=True,
      cache_dir='.', cache_subdir='data')
Downloading data from http://storage.googleapis.com/download.tensorflow.org/data/mini_speech_commands.zip
182083584/182082353 [==============================] - 1s 0us/step
182091776/182082353 [==============================] - 1s 0us/step

קטעי האודיו של מערך הנתונים מאוחסנים בשמונה תיקיות המתאימות לכל פקודת דיבור: no , yes , down , go , left , up , right stop :

commands = np.array(tf.io.gfile.listdir(str(data_dir)))
commands = commands[commands != 'README.md']
print('Commands:', commands)
Commands: ['stop' 'left' 'no' 'go' 'yes' 'down' 'right' 'up']

חלץ את קטעי האודיו לרשימה שנקראת filenames , וערבב אותה:

filenames = tf.io.gfile.glob(str(data_dir) + '/*/*')
filenames = tf.random.shuffle(filenames)
num_samples = len(filenames)
print('Number of total examples:', num_samples)
print('Number of examples per label:',
      len(tf.io.gfile.listdir(str(data_dir/commands[0]))))
print('Example file tensor:', filenames[0])
Number of total examples: 8000
Number of examples per label: 1000
Example file tensor: tf.Tensor(b'data/mini_speech_commands/yes/db72a474_nohash_0.wav', shape=(), dtype=string)

פיצול filenames לקבוצות אימון, אימות ובדיקות תוך שימוש ביחס של 80:10:10, בהתאמה:

train_files = filenames[:6400]
val_files = filenames[6400: 6400 + 800]
test_files = filenames[-800:]

print('Training set size', len(train_files))
print('Validation set size', len(val_files))
print('Test set size', len(test_files))
Training set size 6400
Validation set size 800
Test set size 800

קרא את קובצי השמע והתוויות שלהם

בחלק זה תעבדו מראש את מערך הנתונים, ותצרו טנסורים מפוענחים עבור צורות הגל והתוויות המתאימות. ציין זאת:

  • כל קובץ WAV מכיל נתונים מסדרת זמן עם מספר מוגדר של דגימות בשנייה.
  • כל דגימה מייצגת את משרעת אות השמע באותו זמן ספציפי.
  • במערכת של 16 סיביות , כמו קבצי ה-WAV במערך הנתונים המיני של פקודות דיבור, ערכי המשרעת נעים בין -32,768 ל-32,767.
  • קצב הדגימה עבור מערך נתונים זה הוא 16kHz.

צורת הטנזור המוחזרת על ידי tf.audio.decode_wav היא [samples, channels] , כאשר channels הם 1 עבור מונו או 2 עבור סטריאו. מערך הנתונים המיני של פקודות דיבור מכיל רק הקלטות מונו.

test_file = tf.io.read_file(DATASET_PATH+'/down/0a9f9af7_nohash_0.wav')
test_audio, _ = tf.audio.decode_wav(contents=test_file)
test_audio.shape
TensorShape([13654, 1])

כעת, בואו נגדיר פונקציה שמעבדת מראש את קובצי ה-WAV הגולמיים של מערך הנתונים לטנסור אודיו:

def decode_audio(audio_binary):
  # Decode WAV-encoded audio files to `float32` tensors, normalized
  # to the [-1.0, 1.0] range. Return `float32` audio and a sample rate.
  audio, _ = tf.audio.decode_wav(contents=audio_binary)
  # Since all the data is single channel (mono), drop the `channels`
  # axis from the array.
  return tf.squeeze(audio, axis=-1)

הגדר פונקציה שיוצרת תוויות באמצעות ספריות האב עבור כל קובץ:

  • פצל את נתיבי הקבצים ל- tf.RaggedTensor s (טנסורים בעלי ממדים מרופטים - עם פרוסות שעשויות להיות בעלות אורך שונה).
def get_label(file_path):
  parts = tf.strings.split(
      input=file_path,
      sep=os.path.sep)
  # Note: You'll use indexing here instead of tuple unpacking to enable this
  # to work in a TensorFlow graph.
  return parts[-2]

הגדר פונקציית מסייעת נוספת - get_waveform_and_label - שמחברת את הכל ביחד:

  • הקלט הוא שם קובץ האודיו WAV.
  • הפלט הוא טאפל המכיל את טנסור האודיו והתוויות מוכן ללמידה בפיקוח.
def get_waveform_and_label(file_path):
  label = get_label(file_path)
  audio_binary = tf.io.read_file(file_path)
  waveform = decode_audio(audio_binary)
  return waveform, label

בנה את ערכת ההדרכה כדי לחלץ את צמדי תוויות האודיו:

אתה תבנה את ערכות האימות והבדיקה באמצעות הליך דומה בהמשך.

AUTOTUNE = tf.data.AUTOTUNE

files_ds = tf.data.Dataset.from_tensor_slices(train_files)

waveform_ds = files_ds.map(
    map_func=get_waveform_and_label,
    num_parallel_calls=AUTOTUNE)

בואו נשרטט כמה צורות גל שמע:

rows = 3
cols = 3
n = rows * cols
fig, axes = plt.subplots(rows, cols, figsize=(10, 12))

for i, (audio, label) in enumerate(waveform_ds.take(n)):
  r = i // cols
  c = i % cols
  ax = axes[r][c]
  ax.plot(audio.numpy())
  ax.set_yticks(np.arange(-1.2, 1.2, 0.2))
  label = label.numpy().decode('utf-8')
  ax.set_title(label)

plt.show()

png

המרת צורות גל לספקטרוגרמות

צורות הגל במערך הנתונים מיוצגות בתחום הזמן. לאחר מכן, תמיר את צורות הגל מאותות תחום הזמן לאותות תחום הזמן על ידי חישוב טרנספורמציה פורייה קצרת זמן (STFT) כדי להמיר את צורות הגל כספקטרוגרמות , אשר מציגות שינויים בתדר לאורך זמן ויכולות להיות מיוצג כתמונות דו-ממדיות. אתה תזין את תמונות הספקטרוגרמה לרשת העצבית שלך כדי לאמן את המודל.

טרנספורמציה של פורייה ( tf.signal.fft ) ממירה אות לתדרים המרכיבים שלו, אך מאבדת את כל המידע. לשם השוואה, STFT ( tf.signal.stft ) מפצל את האות לחלונות זמן ומריץ טרנספורמציה של פורייה בכל חלון, תוך שמירה על מידע זמן והחזרת טנזור דו-ממדי שניתן להריץ עליו פיתולים סטנדרטיים.

צור פונקציית עזר להמרת צורות גל לספקטרוגרמות:

  • צורות הגל צריכות להיות באותו אורך, כך שכאשר אתה ממיר אותן לספקטרוגרמות, לתוצאות יש ממדים דומים. ניתן לעשות זאת פשוט על ידי ריפוד אפס של קטעי האודיו שהם קצרים משנייה אחת (באמצעות tf.zeros ).
  • בעת קריאה tf.signal.stft , בחר את הפרמטרים frame_length ו- frame_step כך ש"תמונה" הספקטרוגרמה שנוצרה תהיה כמעט מרובעת. למידע נוסף על בחירת פרמטרי STFT, עיין בסרטון Coursera זה על עיבוד אותות אודיו ו-STFT.
  • ה-STFT מייצר מערך של מספרים מרוכבים המייצגים גודל ופאזה. עם זאת, במדריך זה תשתמש רק בגודל, שאתה יכול להסיק על ידי החלת tf.abs על הפלט של tf.signal.stft .
def get_spectrogram(waveform):
  # Zero-padding for an audio waveform with less than 16,000 samples.
  input_len = 16000
  waveform = waveform[:input_len]
  zero_padding = tf.zeros(
      [16000] - tf.shape(waveform),
      dtype=tf.float32)
  # Cast the waveform tensors' dtype to float32.
  waveform = tf.cast(waveform, dtype=tf.float32)
  # Concatenate the waveform with `zero_padding`, which ensures all audio
  # clips are of the same length.
  equal_length = tf.concat([waveform, zero_padding], 0)
  # Convert the waveform to a spectrogram via a STFT.
  spectrogram = tf.signal.stft(
      equal_length, frame_length=255, frame_step=128)
  # Obtain the magnitude of the STFT.
  spectrogram = tf.abs(spectrogram)
  # Add a `channels` dimension, so that the spectrogram can be used
  # as image-like input data with convolution layers (which expect
  # shape (`batch_size`, `height`, `width`, `channels`).
  spectrogram = spectrogram[..., tf.newaxis]
  return spectrogram

לאחר מכן, התחל לחקור את הנתונים. הדפס את הצורות של צורת הגל המתוחה של דוגמה אחת והספקטרוגרמה המתאימה, והשמע את האודיו המקורי:

for waveform, label in waveform_ds.take(1):
  label = label.numpy().decode('utf-8')
  spectrogram = get_spectrogram(waveform)

print('Label:', label)
print('Waveform shape:', waveform.shape)
print('Spectrogram shape:', spectrogram.shape)
print('Audio playback')
display.display(display.Audio(waveform, rate=16000))
Label: yes
Waveform shape: (16000,)
Spectrogram shape: (124, 129, 1)
Audio playback

כעת, הגדר פונקציה להצגת ספקטרוגרמה:

def plot_spectrogram(spectrogram, ax):
  if len(spectrogram.shape) > 2:
    assert len(spectrogram.shape) == 3
    spectrogram = np.squeeze(spectrogram, axis=-1)
  # Convert the frequencies to log scale and transpose, so that the time is
  # represented on the x-axis (columns).
  # Add an epsilon to avoid taking a log of zero.
  log_spec = np.log(spectrogram.T + np.finfo(float).eps)
  height = log_spec.shape[0]
  width = log_spec.shape[1]
  X = np.linspace(0, np.size(spectrogram), num=width, dtype=int)
  Y = range(height)
  ax.pcolormesh(X, Y, log_spec)

צייר את צורת הגל של הדוגמה לאורך זמן ואת הספקטרוגרמה המתאימה (תדרים לאורך זמן):

fig, axes = plt.subplots(2, figsize=(12, 8))
timescale = np.arange(waveform.shape[0])
axes[0].plot(timescale, waveform.numpy())
axes[0].set_title('Waveform')
axes[0].set_xlim([0, 16000])

plot_spectrogram(spectrogram.numpy(), axes[1])
axes[1].set_title('Spectrogram')
plt.show()

png

כעת, הגדר פונקציה שהופכת את מערך הנתונים של צורות הגל לספקטרוגרמות והתוויות המתאימות להן כמזהי מספרים שלמים:

def get_spectrogram_and_label_id(audio, label):
  spectrogram = get_spectrogram(audio)
  label_id = tf.argmax(label == commands)
  return spectrogram, label_id

מפה get_spectrogram_and_label_id על פני רכיבי מערך הנתונים באמצעות Dataset.map :

spectrogram_ds = waveform_ds.map(
  map_func=get_spectrogram_and_label_id,
  num_parallel_calls=AUTOTUNE)

בדוק את הספקטרוגרמות עבור דוגמאות שונות של מערך הנתונים:

rows = 3
cols = 3
n = rows*cols
fig, axes = plt.subplots(rows, cols, figsize=(10, 10))

for i, (spectrogram, label_id) in enumerate(spectrogram_ds.take(n)):
  r = i // cols
  c = i % cols
  ax = axes[r][c]
  plot_spectrogram(spectrogram.numpy(), ax)
  ax.set_title(commands[label_id.numpy()])
  ax.axis('off')

plt.show()

png

בנה ואימון המודל

חזור על העיבוד המקדים של ערכת ההדרכה על ערכות האימות והבדיקות:

def preprocess_dataset(files):
  files_ds = tf.data.Dataset.from_tensor_slices(files)
  output_ds = files_ds.map(
      map_func=get_waveform_and_label,
      num_parallel_calls=AUTOTUNE)
  output_ds = output_ds.map(
      map_func=get_spectrogram_and_label_id,
      num_parallel_calls=AUTOTUNE)
  return output_ds
train_ds = spectrogram_ds
val_ds = preprocess_dataset(val_files)
test_ds = preprocess_dataset(test_files)

אצווה את ערכות ההדרכה והאימות לאימון מודלים:

batch_size = 64
train_ds = train_ds.batch(batch_size)
val_ds = val_ds.batch(batch_size)

הוסף פעולות Dataset.cache ו- Dataset.prefetch כדי להפחית את זמן האחזור לקריאה בזמן אימון המודל:

train_ds = train_ds.cache().prefetch(AUTOTUNE)
val_ds = val_ds.cache().prefetch(AUTOTUNE)

עבור המודל, תשתמש ברשת עצבית קונבולוציונית פשוטה (CNN), מכיוון שהפכת את קבצי האודיו לתמונות ספקטרוגרמה.

מודל tf.keras.Sequential שלך ישתמש בשכבות העיבוד המקדים הבאות של Keras:

  • tf.keras.layers.Resizing : להקטין דגימה של הקלט כדי לאפשר למודל להתאמן מהר יותר.
  • tf.keras.layers.Normalization .נורמליזציה : לנרמל כל פיקסל בתמונה על סמך הממוצע וסטיית התקן שלו.

עבור שכבת Normalization , תחילה יהיה צורך לקרוא לשיטת adapt שלה על נתוני האימון על מנת לחשב סטטיסטיקה מצטברת (כלומר, הממוצע וסטיית התקן).

for spectrogram, _ in spectrogram_ds.take(1):
  input_shape = spectrogram.shape
print('Input shape:', input_shape)
num_labels = len(commands)

# Instantiate the `tf.keras.layers.Normalization` layer.
norm_layer = layers.Normalization()
# Fit the state of the layer to the spectrograms
# with `Normalization.adapt`.
norm_layer.adapt(data=spectrogram_ds.map(map_func=lambda spec, label: spec))

model = models.Sequential([
    layers.Input(shape=input_shape),
    # Downsample the input.
    layers.Resizing(32, 32),
    # Normalize.
    norm_layer,
    layers.Conv2D(32, 3, activation='relu'),
    layers.Conv2D(64, 3, activation='relu'),
    layers.MaxPooling2D(),
    layers.Dropout(0.25),
    layers.Flatten(),
    layers.Dense(128, activation='relu'),
    layers.Dropout(0.5),
    layers.Dense(num_labels),
])

model.summary()
Input shape: (124, 129, 1)
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 resizing (Resizing)         (None, 32, 32, 1)         0         
                                                                 
 normalization (Normalizatio  (None, 32, 32, 1)        3         
 n)                                                              
                                                                 
 conv2d (Conv2D)             (None, 30, 30, 32)        320       
                                                                 
 conv2d_1 (Conv2D)           (None, 28, 28, 64)        18496     
                                                                 
 max_pooling2d (MaxPooling2D  (None, 14, 14, 64)       0         
 )                                                               
                                                                 
 dropout (Dropout)           (None, 14, 14, 64)        0         
                                                                 
 flatten (Flatten)           (None, 12544)             0         
                                                                 
 dense (Dense)               (None, 128)               1605760   
                                                                 
 dropout_1 (Dropout)         (None, 128)               0         
                                                                 
 dense_1 (Dense)             (None, 8)                 1032      
                                                                 
=================================================================
Total params: 1,625,611
Trainable params: 1,625,608
Non-trainable params: 3
_________________________________________________________________

הגדר את מודל Keras עם אופטימיזציית Adam ואובדן האנטרופיה הצולבת:

model.compile(
    optimizer=tf.keras.optimizers.Adam(),
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=['accuracy'],
)

אמן את הדגם במשך 10 עידנים למטרות הדגמה:

EPOCHS = 10
history = model.fit(
    train_ds,
    validation_data=val_ds,
    epochs=EPOCHS,
    callbacks=tf.keras.callbacks.EarlyStopping(verbose=1, patience=2),
)
Epoch 1/10
100/100 [==============================] - 6s 41ms/step - loss: 1.7503 - accuracy: 0.3630 - val_loss: 1.2850 - val_accuracy: 0.5763
Epoch 2/10
100/100 [==============================] - 0s 5ms/step - loss: 1.2101 - accuracy: 0.5698 - val_loss: 0.9314 - val_accuracy: 0.6913
Epoch 3/10
100/100 [==============================] - 0s 5ms/step - loss: 0.9336 - accuracy: 0.6703 - val_loss: 0.7529 - val_accuracy: 0.7325
Epoch 4/10
100/100 [==============================] - 0s 5ms/step - loss: 0.7503 - accuracy: 0.7397 - val_loss: 0.6721 - val_accuracy: 0.7713
Epoch 5/10
100/100 [==============================] - 0s 5ms/step - loss: 0.6367 - accuracy: 0.7741 - val_loss: 0.6061 - val_accuracy: 0.7975
Epoch 6/10
100/100 [==============================] - 0s 5ms/step - loss: 0.5650 - accuracy: 0.7987 - val_loss: 0.5489 - val_accuracy: 0.8125
Epoch 7/10
100/100 [==============================] - 0s 5ms/step - loss: 0.5099 - accuracy: 0.8183 - val_loss: 0.5344 - val_accuracy: 0.8238
Epoch 8/10
100/100 [==============================] - 0s 5ms/step - loss: 0.4560 - accuracy: 0.8392 - val_loss: 0.5194 - val_accuracy: 0.8288
Epoch 9/10
100/100 [==============================] - 0s 5ms/step - loss: 0.4101 - accuracy: 0.8547 - val_loss: 0.4809 - val_accuracy: 0.8388
Epoch 10/10
100/100 [==============================] - 0s 5ms/step - loss: 0.3905 - accuracy: 0.8589 - val_loss: 0.4973 - val_accuracy: 0.8363

בואו נשרטט את עקומות אובדן האימון והאימות כדי לבדוק כיצד המודל שלך השתפר במהלך האימון:

metrics = history.history
plt.plot(history.epoch, metrics['loss'], metrics['val_loss'])
plt.legend(['loss', 'val_loss'])
plt.show()

png

הערך את ביצועי המודל

הפעל את הדגם על ערכת הבדיקה ובדוק את ביצועי הדגם:

test_audio = []
test_labels = []

for audio, label in test_ds:
  test_audio.append(audio.numpy())
  test_labels.append(label.numpy())

test_audio = np.array(test_audio)
test_labels = np.array(test_labels)
y_pred = np.argmax(model.predict(test_audio), axis=1)
y_true = test_labels

test_acc = sum(y_pred == y_true) / len(y_true)
print(f'Test set accuracy: {test_acc:.0%}')
Test set accuracy: 85%

הצג מטריצת בלבול

השתמש במטריצת בלבול כדי לבדוק עד כמה המודל הצליח לסיווג כל אחת מהפקודות בערכת הבדיקה:

confusion_mtx = tf.math.confusion_matrix(y_true, y_pred)
plt.figure(figsize=(10, 8))
sns.heatmap(confusion_mtx,
            xticklabels=commands,
            yticklabels=commands,
            annot=True, fmt='g')
plt.xlabel('Prediction')
plt.ylabel('Label')
plt.show()

png

הפעל מסקנות על קובץ שמע

לבסוף, אמת את פלט החיזוי של המודל באמצעות קובץ שמע קלט של מישהו שאומר "לא". עד כמה הדגם שלך מתפקד?

sample_file = data_dir/'no/01bb6a2a_nohash_0.wav'

sample_ds = preprocess_dataset([str(sample_file)])

for spectrogram, label in sample_ds.batch(1):
  prediction = model(spectrogram)
  plt.bar(commands, tf.nn.softmax(prediction[0]))
  plt.title(f'Predictions for "{commands[label[0]]}"')
  plt.show()

png

כפי שהפלט מרמז, הדגם שלך היה צריך לזהות את פקודת האודיו כ"לא".

הצעדים הבאים

מדריך זה הדגים כיצד לבצע סיווג אודיו פשוט/זיהוי דיבור אוטומטי באמצעות רשת עצבית קונבולוציונית עם TensorFlow ו-Python. למידע נוסף, שקול את המשאבים הבאים: