MLコミュニティデーは11月9日です! TensorFlow、JAXからの更新のために私たちに参加し、より多くの詳細をご覧ください

シンプルな音声認識:キーワードの認識

TensorFlow.orgで表示 GoogleColabで実行 GitHubでソースを表示 ノートブックをダウンロード

このチュートリアルでは、10個の異なる単語を認識する基本的な音声認識ネットワークを構築する方法を示します。実際の音声および音声認識システムははるかに複雑であることを知っておくことが重要ですが、画像のMNISTと同様に、関連する手法の基本的な理解が得られるはずです。このチュートリアルを完了すると、1秒のオーディオクリップを「下」、「移動」、「左」、「いいえ」、「右」、「停止」、「上」に分類しようとするモデルができあがります。 " はい"。

設定

必要なモジュールと依存関係をインポートします。

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)

音声コマンドデータセットをインポートする

あなたはの部分ダウンロードするためのスクリプト記述します音声コマンドデータセットを。元のデータセットは、30の異なる単語を言っている人々の105,000を超えるWAVオーディオファイルで構成されています。このデータはGoogleによって収集され、CCBYライセンスの下でリリースされました。

データセットの一部を使用して、データの読み込みにかかる時間を節約します。抽出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
182091776/182082353 [==============================] - 1s 0us/step

データセットに関する基本的な統計を確認してください。

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

オーディオファイルをリストに抽出し、シャッフルします。

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/right/a8e25ebb_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でエンコードされたオーディオを返します。

WAVファイルには、1秒あたりのサンプル数が設定された時系列データが含まれています。各サンプルは、その特定の時間におけるオーディオ信号の振幅を表します。 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

スペクトログラム

波形をスペクトログラムに変換します。スペクトログラムは、時間の経過に伴う周波数の変化を示し、2D画像として表すことができます。これは、短時間フーリエ変換(STFT)を適用して、オーディオを時間-周波数領域に変換することで実行できます。

Aフーリエ変換( tf.signal.fft )その成分周波数に信号を変換するが、全ての時間情報を失います。 STFT( tf.signal.stft )時間のウィンドウに信号を分割し、フーリエ変換が各ウィンドウ上で変換動作し、いくつかの時間情報を保存し、あなたは上の標準畳み込みを実行することができ、2Dテンソルを返します。

STFTは、大きさと位相を表す複素数の配列を生成します。しかし、あなただけ適用することにより導出することができ、このチュートリアルのための大きさが必要ですtf.absの出力にtf.signal.stft

選択しframe_lengthframe_step生成スペクトログラム「画像」は、ほぼ正方形であるというようなパラメータを。 STFTパラメータの選択の詳細については、を参照することができ、この映像音声信号処理に。

また、波形の長さを同じにして、スペクトログラム画像に変換したときに結果の寸法が同じになるようにする必要があります。これは、1秒より短いオーディオクリップをゼロパディングするだけで実行できます。

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

次に、データを調べます。データセットからの1つの例の波形、スペクトログラム、および実際のオーディオを比較します。

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: right
Waveform shape: (16000,)
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). An epsilon is added to avoid 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()
/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: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)

データセットの追加cache()し、 prefetch()モデルを訓練しながら、操作は読み取り待ち時間を短縮します。

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)
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 [==============================] - 13s 40ms/step - loss: 1.7503 - accuracy: 0.3659 - val_loss: 1.3361 - val_accuracy: 0.5600
Epoch 2/10
100/100 [==============================] - 0s 4ms/step - loss: 1.1948 - accuracy: 0.5809 - val_loss: 0.9340 - val_accuracy: 0.7225
Epoch 3/10
100/100 [==============================] - 0s 4ms/step - loss: 0.9253 - accuracy: 0.6773 - val_loss: 0.7738 - val_accuracy: 0.7588
Epoch 4/10
100/100 [==============================] - 0s 4ms/step - loss: 0.7543 - accuracy: 0.7341 - val_loss: 0.6993 - val_accuracy: 0.7650
Epoch 5/10
100/100 [==============================] - 0s 4ms/step - loss: 0.6473 - accuracy: 0.7719 - val_loss: 0.6500 - val_accuracy: 0.7800
Epoch 6/10
100/100 [==============================] - 0s 4ms/step - loss: 0.5746 - accuracy: 0.7977 - val_loss: 0.6049 - val_accuracy: 0.7975
Epoch 7/10
100/100 [==============================] - 0s 4ms/step - loss: 0.5181 - accuracy: 0.8155 - val_loss: 0.5967 - val_accuracy: 0.8200
Epoch 8/10
100/100 [==============================] - 0s 4ms/step - loss: 0.4493 - accuracy: 0.8402 - val_loss: 0.5678 - val_accuracy: 0.8100
Epoch 9/10
100/100 [==============================] - 0s 4ms/step - loss: 0.4292 - accuracy: 0.8491 - val_loss: 0.5531 - val_accuracy: 0.8175
Epoch 10/10
100/100 [==============================] - 0s 4ms/step - loss: 0.3914 - accuracy: 0.8598 - val_loss: 0.5204 - val_accuracy: 0.8275

トレーニングと検証の損失曲線をチェックして、トレーニング中にモデルがどのように改善されたかを確認しましょう。

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で畳み込みニューラルネットワークを使用して簡単な音声分類を行う方法を示しました。