Nhận dạng âm thanh đơn giản: Nhận dạng từ khóa

Xem trên TensorFlow.org Chạy trong Google Colab Xem nguồn trên GitHub Tải xuống sổ ghi chép

Hướng dẫn này trình bày cách xử lý trước các tệp âm thanh ở định dạng WAV và xây dựng và đào tạo mô hình nhận dạng giọng nói tự động (ASR) cơ bản để nhận dạng mười từ khác nhau. Bạn sẽ sử dụng một phần của tập dữ liệu Speech Commands ( Warden, 2018 ), chứa các đoạn âm thanh ngắn (một giây trở xuống) của các lệnh, chẳng hạn như "xuống", "đi", "trái", "không", " phải "," dừng "," lên "và" có ".

Hệ thống nhận dạng âm thanh và giọng nói trong thế giới thực rất phức tạp. Tuy nhiên, giống như phân loại hình ảnh với bộ dữ liệu MNIST , hướng dẫn này sẽ cung cấp cho bạn hiểu biết cơ bản về các kỹ thuật liên quan.

Thành lập

Nhập các mô-đun và phụ thuộc cần thiết. Lưu ý rằng bạn sẽ sử dụng seaborn để hình dung trong hướng dẫn này.

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)

Nhập tập dữ liệu Lệnh nói nhỏ

Để tiết kiệm thời gian tải dữ liệu, bạn sẽ làm việc với một phiên bản nhỏ hơn của tập dữ liệu Speech Commands. Tập dữ liệu gốc bao gồm hơn 105.000 tệp âm thanh ở định dạng tệp âm thanh WAV (Dạng sóng) của những người nói 35 từ khác nhau. Dữ liệu này được Google thu thập và phát hành theo giấy phép CC BY.

Tải xuống và giải nén tệp mini_speech_commands.zip chứa bộ dữ liệu Speech Commands nhỏ hơn với 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

Các đoạn âm thanh của tập dữ liệu được lưu trữ trong tám thư mục tương ứng với mỗi lệnh thoại: no , yes , down , go , left , up , rightstop :

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']

Trích xuất các đoạn âm thanh thành một danh sách được gọi là filenames và trộn nó:

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)

Tách filenames thành các bộ đào tạo, xác thực và kiểm tra theo tỷ lệ 80:10:10, tương ứng:

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

Đọc các tệp âm thanh và nhãn của chúng

Trong phần này, bạn sẽ xử lý trước tập dữ liệu, tạo ra các tenxơ được giải mã cho các dạng sóng và các nhãn tương ứng. Lưu ý rằng:

  • Mỗi tệp WAV chứa dữ liệu chuỗi thời gian với số lượng mẫu được đặt trên giây.
  • Mỗi mẫu đại diện cho biên độ của tín hiệu âm thanh tại thời điểm cụ thể đó.
  • Trong hệ thống 16 bit , giống như các tệp WAV trong tập dữ liệu mini Speech Commands, các giá trị biên độ nằm trong khoảng từ -32,768 đến 32,767.
  • Tốc độ mẫu cho tập dữ liệu này là 16kHz.

Hình dạng của tensor được trả về bởi tf.audio.decode_wav[samples, channels] , trong đó channels1 đối với đơn âm hoặc 2 đối với âm thanh nổi. Tập dữ liệu mini Speech Commands chỉ chứa các bản ghi đơn âm.

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])

Bây giờ, hãy xác định một chức năng xử lý trước các tệp âm thanh WAV thô của tập dữ liệu thành bộ căng âm thanh:

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)

Xác định một hàm tạo nhãn bằng cách sử dụng các thư mục mẹ cho mỗi tệp:

  • Chia các đường dẫn tệp thành các tf.RaggedTensor (tensor có kích thước rách nát — với các lát cắt có thể có độ dài khác nhau).
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]

Xác định một chức năng trợ giúp get_waveform_and_label hợp tất cả lại với nhau:

  • Đầu vào là tên tệp âm thanh WAV.
  • Đầu ra là một bộ tuple chứa bộ căng âm thanh và nhãn sẵn sàng cho việc học có giám sát.
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

Xây dựng bộ đào tạo để trích xuất các cặp nhãn âm thanh:

Sau này, bạn sẽ xây dựng bộ xác thực và kiểm tra bằng quy trình tương tự.

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)

Hãy vẽ một vài dạng sóng âm thanh:

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

Chuyển đổi dạng sóng thành quang phổ

Các dạng sóng trong tập dữ liệu được biểu diễn trong miền thời gian. Tiếp theo, bạn sẽ biến đổi các dạng sóng từ tín hiệu miền thời gian thành tín hiệu miền tần số thời gian bằng cách tính toán phép biến đổi Fourier trong thời gian ngắn (STFT) để chuyển đổi các dạng sóng thành dạng quang phổ , hiển thị các thay đổi tần số theo thời gian và có thể được biểu diễn dưới dạng hình ảnh 2D. Bạn sẽ cung cấp các hình ảnh quang phổ vào mạng nơ-ron của mình để huấn luyện mô hình.

Một phép biến đổi Fourier ( tf.signal.fft ) chuyển đổi một tín hiệu thành các tần số thành phần của nó, nhưng mất tất cả thông tin thời gian. Trong khi đó, STFT ( tf.signal.stft ) chia tín hiệu thành các cửa sổ thời gian và chạy một biến đổi Fourier trên mỗi cửa sổ, lưu giữ một số thông tin thời gian và trả về một tensor 2D mà bạn có thể chạy tích chập tiêu chuẩn.

Tạo một chức năng tiện ích để chuyển đổi dạng sóng thành quang phổ:

  • Các dạng sóng cần phải có cùng độ dài để khi bạn chuyển đổi chúng thành quang phổ, các kết quả có kích thước tương tự. Điều này có thể được thực hiện bằng cách không đệm các đoạn âm thanh ngắn hơn một giây (sử dụng tf.zeros ).
  • Khi gọi tf.signal.stft , hãy chọn các tham số frame_step frame_length cho "hình ảnh" quang phổ được tạo ra gần như là hình vuông. Để biết thêm thông tin về lựa chọn tham số STFT, hãy tham khảo video Coursera này về xử lý tín hiệu âm thanh và STFT.
  • STFT tạo ra một mảng các số phức biểu thị độ lớn và pha. Tuy nhiên, trong hướng dẫn này, bạn sẽ chỉ sử dụng độ lớn mà bạn có thể lấy được bằng cách áp dụng tf.abs trên đầu ra của 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

Tiếp theo, bắt đầu khám phá dữ liệu. In các hình dạng của dạng sóng căng thẳng của một ví dụ và biểu đồ quang phổ tương ứng, và phát âm thanh gốc:

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

Bây giờ, hãy xác định một hàm để hiển thị một biểu đồ quang phổ:

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)

Vẽ biểu đồ dạng sóng của ví dụ theo thời gian và biểu đồ quang phổ tương ứng (tần số theo thời gian):

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

Bây giờ, hãy xác định một hàm biến đổi tập dữ liệu dạng sóng thành các biểu đồ quang phổ và các nhãn tương ứng của chúng dưới dạng ID số nguyên:

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

Ánh get_spectrogram_and_label_id qua các phần tử của tập dữ liệu với Dataset.map :

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

Kiểm tra các biểu đồ quang phổ để biết các ví dụ khác nhau của tập dữ liệu:

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

Xây dựng và đào tạo mô hình

Lặp lại quá trình xử lý trước tập huấn luyện trên các tập xác thực và thử nghiệm:

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)

Hàng loạt các bộ đào tạo và xác nhận cho đào tạo mô hình:

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

Thêm các hoạt động Dataset.cacheDataset.prefetch để giảm độ trễ đọc trong khi đào tạo mô hình:

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

Đối với mô hình, bạn sẽ sử dụng một mạng nơ-ron phức hợp đơn giản (CNN), vì bạn đã chuyển đổi các tệp âm thanh thành hình ảnh quang phổ.

Mô hình tf.keras.Sequential của bạn sẽ sử dụng các lớp tiền xử lý Keras sau:

Đối với lớp Normalization , phương pháp adapt của nó trước tiên cần được gọi trên dữ liệu huấn luyện để tính toán thống kê tổng hợp (nghĩa là giá trị trung bình và độ lệch chuẩn).

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
_________________________________________________________________

Định cấu hình mô hình Keras với trình tối ưu hóa Adam và sự mất mát entropy chéo:

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

Huấn luyện mô hình hơn 10 kỷ nguyên cho mục đích trình diễn:

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

Hãy vẽ biểu đồ các đường cong mất mát trong quá trình đào tạo và xác thực để kiểm tra xem mô hình của bạn đã được cải thiện như thế nào trong quá trình đào tạo:

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

png

Đánh giá hiệu suất mô hình

Chạy mô hình trên bộ thử nghiệm và kiểm tra hoạt động của mô hình:

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%

Hiển thị một ma trận nhầm lẫn

Sử dụng ma trận nhầm lẫn để kiểm tra xem mô hình đã phân loại từng lệnh trong bộ kiểm tra tốt như thế nào:

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

Chạy suy luận trên tệp âm thanh

Cuối cùng, xác minh kết quả dự đoán của mô hình bằng cách sử dụng tệp âm thanh đầu vào của người nào đó nói "không". Mô hình của bạn hoạt động tốt như thế nào?

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

Như đầu ra gợi ý, mô hình của bạn phải nhận ra lệnh âm thanh là "không".

Bước tiếp theo

Hướng dẫn này đã trình bày cách thực hiện phân loại âm thanh đơn giản / nhận dạng giọng nói tự động bằng cách sử dụng mạng nơ-ron phức hợp với TensorFlow và Python. Để tìm hiểu thêm, hãy xem xét các tài nguyên sau: