构建模型

已完成

现在波形已转换为光谱图张量,可以训练卷积神经网络(CNN)。 CNN 适用于光谱图分类,因为光谱图是具有跨时间和频率的局部模式的二维表示形式。

本单元中的代码使用以下在上一单元中创建的对象:

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

如果在完整模块流之外运行代码,请先运行上一单元中的安装程序和预处理代码。

检查模型输入

在创建模型之前,请检查一个批次以获取光谱图的输入形状和标签的数量。

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)

预期输出: 光谱图输入形状应与上一单元中生成的形状匹配,标签应和 noyes

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

创建模型

模型从调整 大小层 开始,该层将每个 (124, 129, 1) 光谱图 (32, 32, 1)向下采样。 较小的输入以频率和时间分辨率为代价使训练保持快速;此权衡适用于本模块中的二进制任务。 规范化层随之而来。 规范化层通过调用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()

预期输出: 模型摘要列出了输入层、调整大小和规范化层、两个卷积层、池化层、拖放层、平展层和密集输出层。 最终的密集层有两个输出,每个输出对应一个类。

编译和训练模型

使用 Adam 优化器稀疏分类交叉熵。 尽管任务有两个类别,但该模型使用两个输出对数值,一个用于no,一个用于yes,而不是一个 sigmoid 输出。 该设计与在上一单元中用 label_mode="int" 创建的整数标签相匹配。 模型的最后 Dense 一层输出原始对数(无 softmax 激活),因此设置 from_logits=True 以便损失函数在内部应用数值稳定的 softmax。

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 每纪元打印一行,其中包含训练损失、训练准确性、验证损失和验证准确性。 由于硬件和随机初始化的不同,精确值会有所不同,但对于这个二分类问题,准确性应该在最初的几个时期内提高,并且验证准确性最终会比随机猜测好得多。

绘制训练历史记录

绘制损失和准确性曲线,以检查模型在训练过程中是否改进。

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 输出最终的测试丢失和测试准确性。 确切值会有所不同,但结果应远远高于对均衡的双类数据集进行随机猜测所需的 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 正确时,大多数计数应出现在对角线上。

在一个音频文件上运行推理

若要对单个 WAV 文件进行分类,请使用用于训练的相同预处理加载该文件:一个通道、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 文件测试模型。 录制自己说“是”和“否”的简短剪辑。 使每个剪辑保持接近一秒,并最大程度地减少背景噪音。 将文件导出为 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}")

预期输出: 如果文件尚不存在,代码将输出要创建的路径。 创建文件后,代码会为每个自定义文件打印一个预测。 你自己的语音的准确性可能低于测试集的准确性,因为模型是在语音命令录制中训练的,而不是在录制环境或麦克风上训练的。