このページは Cloud Translation API によって翻訳されました。
Switch to English

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

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によって収集され、CC BYライセンスの下でリリースされました。自分の声を5分間提供することで、データの改善に役立てることができます。

データセットの一部を使用して、データの読み込みにかかる時間を節約します。 mini_speech_commands.zipを抽出し、 mini_speech_commands.zipを使用してtf.dataます。

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 [==============================] - 2s 0us/step

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

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

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

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/fde2dee7_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を使用しtf.audio.decode_wav 。これは、WAVでエンコードされたオーディオをテンソルおよびサンプルレートとして返します。

WAVファイルには、1秒あたりのサンプル数が設定された時系列データが含まれています。各サンプルは、その特定の時間におけるオーディオ信号の振幅を表します。 mini_speech_commandsのファイルのように、16ビットシステムでは、値の範囲はmini_speech_commandsです。このデータセットのサンプルレートは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)を適用して、オーディオを時間-周波数領域に変換することで実行できます。

フーリエ変換( 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: yes
Waveform shape: (13375,)
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]
  X = np.arange(16000, step=height + 1)
  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()
/tmpfs/src/tf_docs_env/lib/python3.6/site-packages/ipykernel_launcher.py:4: RuntimeWarning: divide by zero encountered in log
  after removing the cwd from sys.path.
/tmpfs/src/tf_docs_env/lib/python3.6/site-packages/ipykernel_launcher.py:8: 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.
  

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()
/tmpfs/src/tf_docs_env/lib/python3.6/site-packages/ipykernel_launcher.py:4: RuntimeWarning: divide by zero encountered in log
  after removing the cwd from sys.path.
/tmpfs/src/tf_docs_env/lib/python3.6/site-packages/ipykernel_launcher.py:8: 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.
  

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レイヤー。

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 [==============================] - 16s 47ms/step - loss: 1.9142 - accuracy: 0.2855 - val_loss: 1.2969 - val_accuracy: 0.5938
Epoch 2/10
100/100 [==============================] - 0s 4ms/step - loss: 1.2597 - accuracy: 0.5581 - val_loss: 0.8918 - val_accuracy: 0.7237
Epoch 3/10
100/100 [==============================] - 0s 4ms/step - loss: 0.9525 - accuracy: 0.6647 - val_loss: 0.7742 - val_accuracy: 0.7337
Epoch 4/10
100/100 [==============================] - 0s 4ms/step - loss: 0.7793 - accuracy: 0.7288 - val_loss: 0.6514 - val_accuracy: 0.7850
Epoch 5/10
100/100 [==============================] - 0s 4ms/step - loss: 0.6715 - accuracy: 0.7639 - val_loss: 0.5986 - val_accuracy: 0.8000
Epoch 6/10
100/100 [==============================] - 0s 4ms/step - loss: 0.5647 - accuracy: 0.7968 - val_loss: 0.5901 - val_accuracy: 0.8075
Epoch 7/10
100/100 [==============================] - 0s 4ms/step - loss: 0.5122 - accuracy: 0.8093 - val_loss: 0.5526 - val_accuracy: 0.8087
Epoch 8/10
100/100 [==============================] - 0s 4ms/step - loss: 0.4780 - accuracy: 0.8298 - val_loss: 0.5166 - val_accuracy: 0.8163
Epoch 9/10
100/100 [==============================] - 0s 4ms/step - loss: 0.4216 - accuracy: 0.8544 - val_loss: 0.4806 - val_accuracy: 0.8438
Epoch 10/10
100/100 [==============================] - 0s 4ms/step - loss: 0.3976 - accuracy: 0.8510 - val_loss: 0.4848 - val_accuracy: 0.8388

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

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