モデルを構築する

完了

波形がスペクトログラムテンソルに変換されたので、畳み込みニューラル ネットワーク (CNN) をトレーニングできます。 スペクトログラムは時間と周波数を越えてローカル パターンを持つ 2 次元表現であるため、CNN はスペクトログラムの分類に適しています。

このユニットのコードでは、前のユニットで作成した次のオブジェクトを使用します。

  • train_spectrogram_ds
  • val_spectrogram_ds
  • test_spectrogram_ds
  • label_names
  • get_spectrogram
  • BINARY_DATASET_PATH

完全なモジュール フローの外部でコードを実行する場合は、最初に前のユニットのセットアップと前処理コードを実行します。

モデル入力を検査する

モデルを作成する前に、1 つのバッチを調べて、スペクトログラムの入力図形とラベルの数を取得します。

import pathlib

import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.keras import layers
from tensorflow.keras import models

for example_spectrograms, example_labels in train_spectrogram_ds.take(1):
    input_shape = example_spectrograms.shape[1:]
    break

num_labels = len(label_names)

print("Input shape:", input_shape)
print("Number of labels:", num_labels)
print("Labels:", label_names)

予想される出力: スペクトログラム入力図形は、前のユニットで生成された図形と一致する必要があり、ラベルは no し、 yesする必要があります。

Input shape: (124, 129, 1)
Number of labels: 2
Labels: ['no' 'yes']

モデルを作成する

モデルは、サイズ変更レイヤーから始まり、各スペクトログラムをにダウンサンプリングします。 より小さい入力は頻度および時間解像度を犠牲にして訓練を高速化します。このトレードオフは、このモジュールのバイナリタスクで機能します。 正規化レイヤーは次のとおりです。 正規化レイヤーは、adapt を呼び出してトレーニングを開始する前にトレーニング スペクトログラムの平均と標準偏差を学習します。

normalization_layer = layers.Normalization()
normalization_layer.adapt(
    data=train_spectrogram_ds.map(lambda spectrogram, label: spectrogram)
)

model = models.Sequential([
    layers.Input(shape=input_shape),
    layers.Resizing(32, 32),
    normalization_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()

予想される出力: モデルの概要には、入力レイヤー、サイズ変更と正規化レイヤー、2 つの畳み込みレイヤー、プール、ドロップアウト、フラット化、高密度出力レイヤーが一覧表示されます。 最終的な高密度レイヤーには、クラスごとに 1 つずつ、2 つの出力があります。

モデルをコンパイルしてトレーニングする

Adam オプティマイザースパース カテゴリークロスエントロピーを使用します。 タスクには 2 つのクラスがありますが、このモデルでは no 用と yes 用に 2 つの出力 logit が使用され、1 つの sigmoid 出力ではなくなっています。 このデザインは、前のユニットで label_mode="int" で作成された整数ラベルと一致します。 モデルの最終的な Dense 層は生のロジットを出力するので(ソフトマックスアクティベーションなし)、損失が内部的に数値的に安定したソフトマックスを適用するように from_logits=True 設定します。

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

history = model.fit(
    train_spectrogram_ds,
    validation_data=val_spectrogram_ds,
    epochs=10,
    callbacks=[
        tf.keras.callbacks.EarlyStopping(
            monitor="val_loss",
            patience=2,
            restore_best_weights=True,
        )
    ],
)

予想される出力: TensorFlow では、エポックごとに 1 行が出力され、トレーニングの損失、トレーニングの精度、検証の損失、検証の精度が表示されます。 正確な値はハードウェアとランダムな初期化によって異なりますが、この 2 クラスの問題では、最初のいくつかのエポックよりも精度が向上し、検証の精度はランダムな推測よりもはるかに優れているはずです。

トレーニング履歴をプロットする

トレーニング中にモデルが改善されたかどうかを確認するために、損失曲線と精度曲線をプロットします。

metrics = history.history

plt.figure(figsize=(12, 4))

plt.subplot(1, 2, 1)
plt.plot(history.epoch, metrics["loss"], label="Training loss")
plt.plot(history.epoch, metrics["val_loss"], label="Validation loss")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.legend()

plt.subplot(1, 2, 2)
plt.plot(history.epoch, metrics["accuracy"], label="Training accuracy")
plt.plot(history.epoch, metrics["val_accuracy"], label="Validation accuracy")
plt.xlabel("Epoch")
plt.ylabel("Accuracy")
plt.legend()

plt.show()

予想される出力: 損失プロットは、一般的に下向きに傾向を示す必要があります。 精度プロットは、一般に上昇傾向にあるはずです。 検証の精度が悪くなる間にトレーニングの精度が向上し続ける場合、モデルはオーバーフィットします。

テスト セットで評価する

トレーニングが完了した後にのみ、テスト セットを使用します。 これにより、トレーニングまたは検証中に見られなかったデータに対するモデルのパフォーマンスをより適切に見積もることができます。

test_metrics = model.evaluate(test_spectrogram_ds, return_dict=True)
print(test_metrics)

予想される出力: TensorFlow は、最終的なテスト損失とテスト精度を出力します。 正確な値は異なりますが、結果は、バランスの取れた 2 クラス データセットでのランダムな推測から予想される 50% の精度を大きく上回る必要があります。

混同行列を調べて、モデルが混同するクラスを確認することもできます。

predicted_batches = []
true_batches = []

for spectrograms, labels in test_spectrogram_ds:
    logits = model(spectrograms, training=False)
    predicted_batches.append(tf.argmax(logits, axis=1))
    true_batches.append(labels)

predicted_labels = tf.concat(predicted_batches, axis=0)
true_labels = tf.concat(true_batches, axis=0)

confusion_matrix = tf.math.confusion_matrix(
    true_labels,
    predicted_labels,
    num_classes=num_labels,
)
print(confusion_matrix.numpy())

予想される出力: 出力は 2 x 2 マトリックスです。 ほとんどのカウントは、モデルが noyes を正しく分類しているときに対角線に表示されます。

1 つのオーディオ ファイルで推論を実行する

1 つの WAV ファイルを分類するには、トレーニングに使用されるのと同じ前処理 (1 つのチャネル、16,000 サンプル、STFT の大きさ、チャネル ディメンション) で読み込みます。

def load_waveform(file_path):
    audio_binary = tf.io.read_file(str(file_path))
    waveform, sample_rate = tf.audio.decode_wav(
        audio_binary,
        desired_channels=1,
        desired_samples=16000,
    )
    waveform = tf.squeeze(waveform, axis=-1)
    return waveform, sample_rate


sample_file = next((BINARY_DATASET_PATH / "no").glob("*.wav"))

sample_waveform, sample_rate = load_waveform(sample_file)
sample_spectrogram = get_spectrogram(sample_waveform)

logits = model(sample_spectrogram[tf.newaxis, ...], training=False)
predicted_index = tf.argmax(logits[0]).numpy()
predicted_label = label_names[predicted_index]

print("Sample file:", sample_file)
print("Predicted label:", predicted_label)

予想される出力: 通常、予測は、選択したサンプル ファイルのフォルダー名と一致します。 この例では、 no フォルダーからファイルを選択するため、通常、モデルは noを予測します。 特にモデルが完全に収束する前、またはトレーニング中に誤って分類されたクリップに対して、予測が間違っている場合があります。

省略可能: 独自の音声をテストする

独自の WAV ファイルを使用してモデルをテストできます。 「はい」と「いいえ」と言っている自分の短いクリップを記録します。 各クリップを 1 秒近くに保ち、バックグラウンド ノイズを最小限に抑えます。 ファイルを 16 kHz モノラル 16 ビット PCM WAV ファイルとしてエクスポートし、トレーニング データと一致し、 tf.audio.decode_wavでデコードできるようにします。 記録ツールで別のサンプル レートをエクスポートする場合は、このコードを使用する前に、ファイルを 16 kHz に再サンプリングします。 desired_samples=16000引数はサンプルをパッドまたはトリミングします。44.1 kHz または 48 kHz の録音を 16 kHz オーディオに変換しません。 custom_files内のパスを、作成したファイルと一致するように更新します。

def load_voice_sample(file_path):
    audio_binary = tf.io.read_file(str(file_path))
    waveform, sample_rate = tf.audio.decode_wav(
        audio_binary,
        desired_channels=1,
        desired_samples=16000,
    )

    if int(sample_rate.numpy()) != 16000:
        raise ValueError("Use a 16 kHz WAV file, or resample the audio to 16 kHz before inference.")

    waveform = tf.squeeze(waveform, axis=-1)
    return waveform


custom_files = {
    "no": pathlib.Path("data/myvoice/no.wav"),
    "yes": pathlib.Path("data/myvoice/yes.wav"),
}

missing_files = [file_path for file_path in custom_files.values() if not file_path.exists()]

if missing_files:
    print("Create these WAV files before running the optional custom-voice example:")
    for file_path in missing_files:
        print(file_path)
else:
    for expected_label, file_path in custom_files.items():
        waveform = load_voice_sample(file_path)
        spectrogram = get_spectrogram(waveform)
        logits = model(spectrogram[tf.newaxis, ...], training=False)
        predicted_label = label_names[tf.argmax(logits[0]).numpy()]

        print(f"Expected: {expected_label}; predicted: {predicted_label}")

予想される出力: ファイルがまだ存在しない場合、コードは作成するパスを出力します。 ファイルを作成した後、コードはカスタム ファイルごとに 1 つの予測を出力します。 モデルは録音環境やマイクではなく Speech Commands の録音でトレーニングされているため、自分の音声の精度はテストセットの精度よりも低くなる可能性があります。