![]() | ![]() | ![]() | ![]() |
このチュートリアルでは、WAV形式のオーディオファイルを前処理し、10個の異なる単語を認識するための基本的な自動音声認識(ASR)モデルを構築およびトレーニングする方法を示します。 「down」、「go」、「left」、「no」、「no」などのコマンドの短い(1秒以下)オーディオクリップを含む音声コマンドデータセット( 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)
ミニ音声コマンドデータセットをインポートする
データの読み込みにかかる時間を節約するために、音声コマンドデータセットの小さいバージョンで作業します。元のデータセットは、35の異なる単語を話す人々のWAV(Waveform)オーディオファイル形式の105,000を超えるオーディオファイルで構成されています。このデータはGoogleによって収集され、CCBYライセンスの下でリリースされました。
tf.keras.utils.get_fileを使用して、小さい音声コマンドデータセットを含む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
データセットのオーディオクリップは、各音声コマンドに対応する8つのフォルダに保存されます: 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ファイルには、1秒あたりのサンプル数が設定された時系列データが含まれています。
- 各サンプルは、その特定の時間におけるオーディオ信号の振幅を表します。
- ミニ音声コマンドデータセットのWAVファイルのような16ビットシステムでは、振幅値の範囲は-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
に分割します(不規則な次元のテンソル-スライスの長さが異なる場合があります)。
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
音声とラベルのペアを抽出するためのトレーニングセットを作成します。
- 前に定義した
get_waveform_and_label
を使用して、Dataset.from_tensor_slices
とDataset.map
を使用してtf.data.Dataset
を作成します。
後で同様の手順を使用して、検証セットとテストセットを作成します。
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()
波形をスペクトログラムに変換する
データセット内の波形は、時間領域で表されます。次に、短時間フーリエ変換(STFT)を計算して波形をスペクトログラムに変換することにより、時間領域信号から時間周波数領域信号に波形を変換します。スペクトログラムは、時間の経過に伴う周波数変化を示します。 2D画像として表されます。スペクトログラム画像をニューラルネットワークにフィードして、モデルをトレーニングします。
フーリエ変換( tf.signal.fft
)は、信号をその成分周波数に変換しますが、すべての時間情報を失います。比較すると、STFT( tf.signal.stft
)は、信号を時間のウィンドウに分割し、各ウィンドウでフーリエ変換を実行して、時間情報を保持し、標準の畳み込みを実行できる2Dテンソルを返します。
波形をスペクトログラムに変換するためのユーティリティ関数を作成します。
- 波形は同じ長さである必要があるため、スペクトログラムに変換すると、結果の寸法は同じになります。これは、1秒より短いオーディオクリップをゼロパディングするだけで実行できます(
tf.zeros
を使用)。 -
tf.signal.stft
を呼び出すときは、生成されたスペクトログラム「画像」がほぼ正方形になるように、frame_length
パラメーターとframe_step
パラメーターを選択します。 STFTパラメータの選択の詳細については、オーディオ信号処理とSTFTに関するこのCourseraビデオを参照してください。 - 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
次に、データの調査を開始します。 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: (16000,) Spectrogram shape: (124, 129, 1) Audio playbackプレースホルダー19
次に、スペクトログラムを表示するための関数を定義します。
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()
次に、波形データセットをスペクトログラムとそれに対応するラベルに整数IDとして変換する関数を定義します。
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()
モデルを構築してトレーニングする
検証セットとテストセットでトレーニングセットの前処理を繰り返します。
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)
プレースホルダー26モデルトレーニングのトレーニングセットと検証セットをバッチ処理します。
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 _________________________________________________________________
Adamオプティマイザーとクロスエントロピー損失を使用してKerasモデルを構成します。
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()
モデルのパフォーマンスを評価する
テストセットでモデルを実行し、モデルのパフォーマンスを確認します。
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%プレースホルダー37
混同行列を表示する
混同行列を使用して、モデルがテストセット内の各コマンドをどの程度適切に分類したかを確認します。
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()
オーディオファイルで推論を実行する
最後に、「いいえ」と言っている人の入力オーディオファイルを使用して、モデルの予測出力を確認します。モデルのパフォーマンスはどれくらいですか?
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()
出力が示すように、モデルはオーディオコマンドを「no」として認識しているはずです。
次のステップ
このチュートリアルでは、TensorFlowとPythonを使用した畳み込みニューラルネットワークを使用して、簡単な音声分類/自動音声認識を実行する方法を示しました。詳細については、次のリソースを検討してください。
- YAMNetチュートリアルによるサウンド分類は、オーディオ分類に転送学習を使用する方法を示しています。
- KaggleのTensorFlow音声認識チャレンジのノートブック。
- TensorFlow.js-転送学習コードラボを使用した音声認識では、音声分類用の独自のインタラクティブなウェブアプリを構築する方法を学びます。
- arXivでの音楽情報検索のディープラーニングに関するチュートリアル(Choi et al。、2017)。
- TensorFlowには、独自のオーディオベースのプロジェクトを支援するためのオーディオデータの準備と拡張の追加サポートもあります。
- librosaライブラリ(音楽とオーディオの分析用のPythonパッケージ)の使用を検討してください。