การจดจำเสียงอย่างง่าย: การจดจำคำหลัก

ดูบน TensorFlow.org ทำงานใน Google Colab ดูแหล่งที่มาบน GitHub ดาวน์โหลดโน๊ตบุ๊ค

บทแนะนำนี้จะแสดงให้คุณเห็นถึงวิธีสร้างเครือข่ายการรู้จำเสียงพูดขั้นพื้นฐานที่จดจำคำต่างๆ ได้ 10 คำ สิ่งสำคัญคือต้องรู้ว่าระบบรู้จำเสียงพูดและเสียงจริงนั้นซับซ้อนกว่ามาก แต่เช่นเดียวกับ MNIST สำหรับรูปภาพ ระบบควรให้ความเข้าใจพื้นฐานเกี่ยวกับเทคนิคที่เกี่ยวข้องแก่คุณ เมื่อคุณเสร็จสิ้นบทช่วยสอนนี้ คุณจะมีโมเดลที่พยายามจัดประเภทคลิปเสียงหนึ่งวินาทีว่า "ลง" "ไป" "ซ้าย" "ไม่" "ขวา" "หยุด" "ขึ้น" " และใช่".

ติดตั้ง

นำเข้าโมดูลและการอ้างอิงที่จำเป็น

import os
import pathlib

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

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


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

นำเข้าชุดข้อมูลคำสั่งเสียง

คุณจะต้องเขียนสคริปต์เพื่อดาวน์โหลดส่วนหนึ่งของ ชุดข้อมูลคำสั่งเสียง ชุดข้อมูลดั้งเดิมประกอบด้วยไฟล์เสียง WAV กว่า 105,000 ไฟล์ของผู้คนที่พูดคำต่างกันสามสิบคำ ข้อมูลนี้เก็บรวบรวมโดย Google และเผยแพร่ภายใต้ใบอนุญาต CC BY

คุณจะใช้ส่วนหนึ่งของชุดข้อมูลเพื่อประหยัดเวลาในการโหลดข้อมูล แตกไฟล์ mini_speech_commands.zip และโหลดโดยใช้ tf.data API

data_dir = pathlib.Path('data/mini_speech_commands')
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

ตรวจสอบสถิติพื้นฐานเกี่ยวกับชุดข้อมูล

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

แยกไฟล์เสียงลงในรายการและสับเปลี่ยน

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/go/5eb5fc74_nohash_0.wav', shape=(), dtype=string)

แบ่งไฟล์ออกเป็นชุดการฝึก การตรวจสอบ และการทดสอบโดยใช้อัตราส่วน 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

การอ่านไฟล์เสียงและป้ายกำกับ

ในขั้นต้น ไฟล์เสียงจะถูกอ่านเป็นไฟล์ไบนารี ซึ่งคุณจะต้องแปลงเป็นเมตริกซ์ตัวเลข

ในการโหลดไฟล์เสียง คุณจะต้องใช้ tf.audio.decode_wav ซึ่งจะคืนค่าเสียงที่เข้ารหัส WAV เป็น Tensor และอัตราตัวอย่าง

ไฟล์ WAV มีข้อมูลอนุกรมเวลาพร้อมจำนวนตัวอย่างที่กำหนดต่อวินาที แต่ละตัวอย่างแสดงถึงแอมพลิจูดของสัญญาณเสียง ณ เวลาที่กำหนด ในระบบ 16 บิต เช่นเดียวกับไฟล์ใน mini_speech_commands ค่ามีตั้งแต่ -32768 ถึง 32767 อัตราสุ่มสำหรับชุดข้อมูลนี้คือ 16kHz โปรดทราบว่า tf.audio.decode_wav จะทำให้ค่าเป็นมาตรฐานในช่วง [-1.0, 1.0]

def decode_audio(audio_binary):
  audio, _ = tf.audio.decode_wav(audio_binary)
  return tf.squeeze(audio, axis=-1)

ป้ายกำกับสำหรับไฟล์ WAV แต่ละไฟล์คือไดเร็กทอรีหลัก

def get_label(file_path):
  parts = tf.strings.split(file_path, 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]

มากำหนดวิธีการที่จะใช้ในชื่อไฟล์ของไฟล์ 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

ตอนนี้ คุณจะใช้ process_path เพื่อสร้างชุดการฝึกเพื่อแยกคู่ป้ายกำกับเสียงและตรวจสอบผลลัพธ์ คุณจะต้องสร้างชุดการตรวจสอบและทดสอบโดยใช้ขั้นตอนที่คล้ายกันในภายหลัง

AUTOTUNE = tf.data.AUTOTUNE
files_ds = tf.data.Dataset.from_tensor_slices(train_files)
waveform_ds = files_ds.map(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

สเปกโตรแกรม

คุณจะแปลงรูปคลื่นเป็นสเปกโตรแกรม ซึ่งจะแสดงการเปลี่ยนแปลงความถี่เมื่อเวลาผ่านไป และสามารถแสดงเป็นภาพ 2 มิติได้ ซึ่งสามารถทำได้โดยใช้การแปลงฟูเรียร์เวลาสั้น (STFT) เพื่อแปลงเสียงเป็นโดเมนความถี่เวลา

การแปลงฟูริเยร์ ( tf.signal.fft ) แปลงสัญญาณเป็นความถี่ส่วนประกอบ แต่สูญเสียข้อมูลเวลาทั้งหมด STFT ( tf.signal.stft ) แบ่งสัญญาณออกเป็นช่วงเวลาและเรียกใช้การแปลงฟูริเยร์ในแต่ละหน้าต่าง เก็บข้อมูลเวลาบางส่วน และส่งคืนเทนเซอร์ 2 มิติที่คุณสามารถเรียกใช้การบิดแบบมาตรฐานได้

STFT สร้างอาร์เรย์ของจำนวนเชิงซ้อนที่แสดงขนาดและเฟส อย่างไรก็ตาม คุณจะต้องใช้ขนาดสำหรับบทช่วยสอนนี้เท่านั้น ซึ่งสามารถหาได้โดยการใช้ tf.abs กับผลลัพธ์ของ tf.signal.stft

เลือก frame_length และ frame_step พารามิเตอร์ดังกล่าวที่สร้าง spectrogram "ภาพ" เกือบจะเป็นตาราง สำหรับข้อมูลเพิ่มเติมเกี่ยวกับการเลือกพารามิเตอร์ STFT คุณสามารถดู วิดีโอนี้ เกี่ยวกับการประมวลผลสัญญาณเสียง

คุณยังต้องการให้รูปคลื่นมีความยาวเท่ากัน ดังนั้นเมื่อคุณแปลงเป็นภาพสเปกโตรแกรม ผลลัพธ์จะมีขนาดใกล้เคียงกัน ซึ่งสามารถทำได้โดยเพียงแค่ใส่คลิปเสียงที่สั้นกว่าหนึ่งวินาทีเป็นศูนย์

def get_spectrogram(waveform):
  # Padding for files with less than 16000 samples
  zero_padding = tf.zeros([16000] - tf.shape(waveform), dtype=tf.float32)

  # Concatenate audio with padding so that all audio clips will be of the 
  # same length
  waveform = tf.cast(waveform, tf.float32)
  equal_length = tf.concat([waveform, zero_padding], 0)
  spectrogram = tf.signal.stft(
      equal_length, frame_length=255, frame_step=128)

  spectrogram = tf.abs(spectrogram)

  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: go
Waveform shape: (8192,)
Spectrogram shape: (124, 129)
Audio playback

def plot_spectrogram(spectrogram, ax):
  # Convert to frequencies to log scale and transpose so that the time is
  # represented in the x-axis (columns).
  log_spec = np.log(spectrogram.T)
  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()
/home/kbuilder/.local/lib/python3.7/site-packages/ipykernel_launcher.py:4: RuntimeWarning: divide by zero encountered in log
  after removing the cwd from sys.path.
/home/kbuilder/.local/lib/python3.7/site-packages/ipykernel_launcher.py:9: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3.  Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading'].  This will become an error two minor releases later.
  if __name__ == '__main__':

png

ตอนนี้แปลงชุดข้อมูลรูปคลื่นให้มีภาพสเปกโตรแกรมและป้ายกำกับที่เกี่ยวข้องเป็น ID จำนวนเต็ม

def get_spectrogram_and_label_id(audio, label):
  spectrogram = get_spectrogram(audio)
  spectrogram = tf.expand_dims(spectrogram, -1)
  label_id = tf.argmax(label == commands)
  return spectrogram, label_id
spectrogram_ds = waveform_ds.map(
    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(np.squeeze(spectrogram.numpy()), ax)
  ax.set_title(commands[label_id.numpy()])
  ax.axis('off')

plt.show()
/home/kbuilder/.local/lib/python3.7/site-packages/ipykernel_launcher.py:4: RuntimeWarning: divide by zero encountered in log
  after removing the cwd from sys.path.
/home/kbuilder/.local/lib/python3.7/site-packages/ipykernel_launcher.py:9: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3.  Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading'].  This will become an error two minor releases later.
  if __name__ == '__main__':

png

สร้างและฝึกโมเดล

ตอนนี้คุณสามารถสร้างและฝึกโมเดลของคุณได้ แต่ก่อนที่คุณจะทำอย่างนั้น คุณจะต้องทำซ้ำชุดฝึกการประมวลผลล่วงหน้าในชุดตรวจสอบและชุดทดสอบ

def preprocess_dataset(files):
  files_ds = tf.data.Dataset.from_tensor_slices(files)
  output_ds = files_ds.map(get_waveform_and_label, num_parallel_calls=AUTOTUNE)
  output_ds = output_ds.map(
      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() และ prefetch() operation เพื่อลดเวลาแฝงในการอ่านขณะฝึกโมเดล

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

สำหรับโมเดล คุณจะใช้โครงข่ายประสาทเทียมแบบง่าย (CNN) เนื่องจากคุณได้แปลงไฟล์เสียงเป็นภาพสเปกโตรแกรม โมเดลยังมีเลเยอร์การประมวลผลล่วงหน้าเพิ่มเติมดังต่อไปนี้:

  • เลเยอร์การ Resizing เพื่อลดตัวอย่างอินพุตเพื่อให้โมเดลฝึกได้เร็วขึ้น
  • เลเยอร์ Normalization เพื่อทำให้แต่ละพิกเซลในรูปภาพเป็นมาตรฐานตามค่าเฉลี่ยและส่วนเบี่ยงเบนมาตรฐาน

สำหรับเลเยอร์ Normalization วิธี adapt จะต้องเรียกใช้ข้อมูลการฝึกอบรมก่อนเพื่อคำนวณสถิติรวม (เช่น ค่าเฉลี่ยและค่าเบี่ยงเบนมาตรฐาน)

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

norm_layer = preprocessing.Normalization()
norm_layer.adapt(spectrogram_ds.map(lambda x, _: x))

model = models.Sequential([
    layers.Input(shape=input_shape),
    preprocessing.Resizing(32, 32), 
    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)
WARNING:tensorflow:Please add `keras.layers.InputLayer` instead of `keras.Input` to Sequential model. `keras.Input` is intended to be used by Functional model.
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
resizing (Resizing)          (None, 32, 32, 1)         0         
_________________________________________________________________
normalization (Normalization (None, 32, 32, 1)         3         
_________________________________________________________________
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
_________________________________________________________________
model.compile(
    optimizer=tf.keras.optimizers.Adam(),
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=['accuracy'],
)
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 38ms/step - loss: 1.7314 - accuracy: 0.3739 - val_loss: 1.3301 - val_accuracy: 0.5387
Epoch 2/10
100/100 [==============================] - 0s 4ms/step - loss: 1.1985 - accuracy: 0.5773 - val_loss: 0.9609 - val_accuracy: 0.6862
Epoch 3/10
100/100 [==============================] - 0s 4ms/step - loss: 0.9329 - accuracy: 0.6752 - val_loss: 0.7923 - val_accuracy: 0.7350
Epoch 4/10
100/100 [==============================] - 0s 4ms/step - loss: 0.7678 - accuracy: 0.7280 - val_loss: 0.6767 - val_accuracy: 0.7700
Epoch 5/10
100/100 [==============================] - 0s 4ms/step - loss: 0.6529 - accuracy: 0.7670 - val_loss: 0.6235 - val_accuracy: 0.7850
Epoch 6/10
100/100 [==============================] - 0s 4ms/step - loss: 0.5823 - accuracy: 0.7880 - val_loss: 0.5722 - val_accuracy: 0.7987
Epoch 7/10
100/100 [==============================] - 0s 4ms/step - loss: 0.5088 - accuracy: 0.8180 - val_loss: 0.5437 - val_accuracy: 0.8062
Epoch 8/10
100/100 [==============================] - 0s 4ms/step - loss: 0.4687 - accuracy: 0.8342 - val_loss: 0.5667 - val_accuracy: 0.7987
Epoch 9/10
100/100 [==============================] - 0s 4ms/step - loss: 0.4238 - accuracy: 0.8509 - val_loss: 0.5144 - val_accuracy: 0.8350
Epoch 10/10
100/100 [==============================] - 0s 4ms/step - loss: 0.3966 - accuracy: 0.8633 - val_loss: 0.4949 - val_accuracy: 0.8263

มาตรวจสอบกราฟการฝึกอบรมและการสูญเสียการตรวจสอบเพื่อดูว่าโมเดลของคุณได้รับการปรับปรุงระหว่างการฝึกอย่างไร

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: 84%

แสดงเมทริกซ์ความสับสน

เมทริกซ์ความสับสนมีประโยชน์ในการดูว่าโมเดลทำงานได้ดีเพียงใดในแต่ละคำสั่งในชุดทดสอบ

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 ได้อย่างไร