构建模型
现在波形已转换为光谱图张量,可以训练卷积神经网络(CNN)。 CNN 适用于光谱图分类,因为光谱图是具有跨时间和频率的局部模式的二维表示形式。
本单元中的代码使用以下在上一单元中创建的对象:
train_spectrogram_dsval_spectrogram_dstest_spectrogram_dslabel_namesget_spectrogramBINARY_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 矩阵。 当模型分类 no 并 yes 正确时,大多数计数应出现在对角线上。
在一个音频文件上运行推理
若要对单个 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}")
预期输出: 如果文件尚不存在,代码将输出要创建的路径。 创建文件后,代码会为每个自定义文件打印一个预测。 你自己的语音的准确性可能低于测试集的准确性,因为模型是在语音命令录制中训练的,而不是在录制环境或麦克风上训练的。