使用基于 YAMNet 的迁移学习进行环境声音分类#
YAMNet 是一种预训练的深度神经网络,可以预测 521 个类的音频事件,例如笑声、吠叫或警笛声。
在本教程中,您将学习如何:
加载并使用 YAMNet 模型进行推断。
使用 YAMNet 嵌入向量构建一个新模型来对猫和狗的声音进行分类。
评估并导出模型。
导入 TensorFlow 和其他库#
首先安装 TensorFlow I/O,这将使您更轻松地从磁盘上加载音频文件。
!pip install -q "tensorflow==2.11.*"
# tensorflow_io 0.28 is compatible with TensorFlow 2.11
!pip install -q "tensorflow_io==0.28.*"
import os
from IPython import display
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import tensorflow as tf
import tensorflow_hub as hub
import tensorflow_io as tfio
关于 YAMNet#
YAMNet 是一种采用 MobileNetV1 深度可分离卷积架构的预训练神经网络。它可以使用音频波形作为输入,并对 AudioSet 语料库中的 521 个音频事件分别进行独立预测。
在内部,模型会从音频信号中提取“帧”并批量处理这些帧。此版本的模型使用时长为 0.96 秒的帧,每 0.48 秒提取一帧。
模型会接受包含任意长度波形的一维 float32 张量或 NumPy 数组,表示为 [-1.0, +1.0]
区间内的单通道(单声道)16 kHz 样本。本教程包含帮助您将 WAV 文件转换为受支持格式的代码。
模型会返回 3 个输出,包括类分数、嵌入向量(将用于迁移学习)和对数梅尔语谱图。您可以在此处找到更多详细信息。
YAMNet 的一种特定用途是作为高级特征提取器 - 1,024 维嵌入向量输出。您将使用基础 (YAMNet) 模型的输入特征并将它们馈送到由一个隐藏的 tf.keras.layers.Dense
层组成的浅层模型中。然后,您将在少量数据上训练网络进行音频分类,而不需要大量带标签的数据和端到端训练。(这类似于使用 TensorFlow Hub 进行图像分类迁移学习,请参阅以了解更多信息。)
首先,您将测试模型并查看音频分类结果。然后,您将构建数据预处理流水线。
从 TensorFlow Hub 加载 YAMNet#
您将使用来自 TensorFlow Hub 的预训练 YAMNet 从声音文件中提取嵌入向量。
从 TensorFlow Hub 中加载模型非常简单:选择模型,复制其网址,然后使用 load
函数。
注:要阅读模型的文档,请在浏览器中使用模型网址。
yamnet_model_handle = 'https://tfhub.dev/google/yamnet/1'
yamnet_model = hub.load(yamnet_model_handle)
加载模型后,您可以遵循 YAMNet 基本使用教程并下载 WAV 样本文件以运行推断。
testing_wav_file_name = tf.keras.utils.get_file('miaow_16k.wav',
'https://storage.googleapis.com/audioset/miaow_16k.wav',
cache_dir='./',
cache_subdir='test_data')
print(testing_wav_file_name)
您将需要用于加载音频文件的函数,稍后在处理训练数据时也将使用该函数。(请参阅简单音频识别以详细了解如何读取音频文件及其标签。)
注:从 load_wav_16k_mono
返回的 wav_data
已经归一化为 [-1.0, 1.0]
区间内的值(有关更多信息,请参阅 TF Hub 上的 YAMNet 文档)。
# Utility functions for loading audio files and making sure the sample rate is correct.
@tf.function
def load_wav_16k_mono(filename):
""" Load a WAV file, convert it to a float tensor, resample to 16 kHz single-channel audio. """
file_contents = tf.io.read_file(filename)
wav, sample_rate = tf.audio.decode_wav(
file_contents,
desired_channels=1)
wav = tf.squeeze(wav, axis=-1)
sample_rate = tf.cast(sample_rate, dtype=tf.int64)
wav = tfio.audio.resample(wav, rate_in=sample_rate, rate_out=16000)
return wav
testing_wav_data = load_wav_16k_mono(testing_wav_file_name)
_ = plt.plot(testing_wav_data)
# Play the audio file.
display.Audio(testing_wav_data, rate=16000)
加载类映射#
务必加载 YAMNet 能够识别的类名。映射文件以 CSV 格式记录在 yamnet_model.class_map_path()
中。
class_map_path = yamnet_model.class_map_path().numpy().decode('utf-8')
class_names =list(pd.read_csv(class_map_path)['display_name'])
for name in class_names[:20]:
print(name)
print('...')
运行推断#
YAMNet 提供帧级类分数(即每帧 521 个分数)。为了确定剪辑级预测,可以按类跨帧聚合分数(例如,使用平均值或最大值聚合)。这是通过 scores_np.mean(axis=0)
以如下方式完成的。最后,要在剪辑级找到分数最高的类,您需要在 521 个聚合分数中取最大值。
scores, embeddings, spectrogram = yamnet_model(testing_wav_data)
class_scores = tf.reduce_mean(scores, axis=0)
top_class = tf.math.argmax(class_scores)
inferred_class = class_names[top_class]
print(f'The main sound is: {inferred_class}')
print(f'The embeddings shape: {embeddings.shape}')
注:模型正确推断出动物的声音。您在本教程中的目标是提高模型针对特定类的准确率。此外,请注意该模型生成了 13 个嵌入向量,每帧 1 个。
ESC-50 数据集#
ESC-50 数据集 (Piczak, 2015) 是一个包含 2,000 个时长为 5 秒的环境录音的带标签集合。该数据集由 50 个类组成,每个类有 40 个样本。
下载并提取数据集。
_ = tf.keras.utils.get_file('esc-50.zip',
'https://github.com/karoldvl/ESC-50/archive/master.zip',
cache_dir='./',
cache_subdir='datasets',
extract=True)
探索数据#
每个文件的元数据均在 ./datasets/ESC-50-master/meta/esc50.csv
下的 csv 文件中指定
所有音频文件均位于 ./datasets/ESC-50-master/audio/
您将创建支持映射的 pandas DataFrame
,并使用它来更清晰地查看数据。
esc50_csv = './datasets/ESC-50-master/meta/esc50.csv'
base_data_path = './datasets/ESC-50-master/audio/'
pd_data = pd.read_csv(esc50_csv)
pd_data.head()
过滤数据#
现在,数据存储在 DataFrame
中,请应用一些转换:
过滤掉行并仅使用所选类 -
dog
和cat
。如果您想使用任何其他类,则可以在此处进行选择。修改文件名以获得完整路径。这将使后续加载更加容易。
将目标更改到特定区间内。在此示例中,
dog
将保持为0
,但cat
将改为1
,而非其原始值5
。
my_classes = ['dog', 'cat']
map_class_to_id = {'dog':0, 'cat':1}
filtered_pd = pd_data[pd_data.category.isin(my_classes)]
class_id = filtered_pd['category'].apply(lambda name: map_class_to_id[name])
filtered_pd = filtered_pd.assign(target=class_id)
full_path = filtered_pd['filename'].apply(lambda row: os.path.join(base_data_path, row))
filtered_pd = filtered_pd.assign(filename=full_path)
filtered_pd.head(10)
加载音频文件并检索嵌入向量#
在这里,您将应用 load_wav_16k_mono
并为模型准备 WAV 数据。
从 WAV 数据中提取嵌入向量时,您会得到一个形状为 (N, 1024)
的数组,其中 N
为 YAMNet 找到的帧数(每 0.48 秒音频一帧)。
您的模型将使用每一帧作为一个输入。因此,您需要创建一个新列,每行包含一帧。您还需要展开标签和 fold
列以正确反映这些新行。
展开的 fold
列会保留原始值。您不能混合帧,因为在执行拆分时,最后可能会将同一个音频拆分为不同的部分,这会降低您的验证和测试步骤的效率。
filenames = filtered_pd['filename']
targets = filtered_pd['target']
folds = filtered_pd['fold']
main_ds = tf.data.Dataset.from_tensor_slices((filenames, targets, folds))
main_ds.element_spec
def load_wav_for_map(filename, label, fold):
return load_wav_16k_mono(filename), label, fold
main_ds = main_ds.map(load_wav_for_map)
main_ds.element_spec
# applies the embedding extraction model to a wav data
def extract_embedding(wav_data, label, fold):
''' run YAMNet to extract embedding from the wav data '''
scores, embeddings, spectrogram = yamnet_model(wav_data)
num_embeddings = tf.shape(embeddings)[0]
return (embeddings,
tf.repeat(label, num_embeddings),
tf.repeat(fold, num_embeddings))
# extract embedding
main_ds = main_ds.map(extract_embedding).unbatch()
main_ds.element_spec
拆分数据#
您需要使用 fold
列将数据集拆分为训练集、验证集和测试集。
ESC-50 被排列成五个大小一致的交叉验证 fold
,这样,源自同一来源的剪辑就始终位于同一 fold
中 - 请参阅 ESC: Dataset for Environmental Sound Classification 论文以了解更多信息。
最后一步是从数据集中移除 fold
列,因为您在训练期间不会用到它。
cached_ds = main_ds.cache()
train_ds = cached_ds.filter(lambda embedding, label, fold: fold < 4)
val_ds = cached_ds.filter(lambda embedding, label, fold: fold == 4)
test_ds = cached_ds.filter(lambda embedding, label, fold: fold == 5)
# remove the folds column now that it's not needed anymore
remove_fold_column = lambda embedding, label, fold: (embedding, label)
train_ds = train_ds.map(remove_fold_column)
val_ds = val_ds.map(remove_fold_column)
test_ds = test_ds.map(remove_fold_column)
train_ds = train_ds.cache().shuffle(1000).batch(32).prefetch(tf.data.AUTOTUNE)
val_ds = val_ds.cache().batch(32).prefetch(tf.data.AUTOTUNE)
test_ds = test_ds.cache().batch(32).prefetch(tf.data.AUTOTUNE)
创建模型#
大部分工作已经完成!接下来,请定义一个非常简单的序贯模型,其中包含一个隐藏层和两个输出,以便通过声音识别猫和狗。
my_model = tf.keras.Sequential([
tf.keras.layers.Input(shape=(1024), dtype=tf.float32,
name='input_embedding'),
tf.keras.layers.Dense(512, activation='relu'),
tf.keras.layers.Dense(len(my_classes))
], name='my_model')
my_model.summary()
my_model.compile(loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
optimizer="adam",
metrics=['accuracy'])
callback = tf.keras.callbacks.EarlyStopping(monitor='loss',
patience=3,
restore_best_weights=True)
history = my_model.fit(train_ds,
epochs=20,
validation_data=val_ds,
callbacks=callback)
让我们对测试数据运行 evaluate
方法,以避免过拟合。
loss, accuracy = my_model.evaluate(test_ds)
print("Loss: ", loss)
print("Accuracy: ", accuracy)
做得很棒!
测试模型#
接下来,仅使用 YAMNet 基于之前测试中的嵌入向量尝试您的模型。
scores, embeddings, spectrogram = yamnet_model(testing_wav_data)
result = my_model(embeddings).numpy()
inferred_class = my_classes[result.mean(axis=0).argmax()]
print(f'The main sound is: {inferred_class}')
保存可直接将 WAV 文件作为输入的模型#
使用嵌入向量作为输入,您的模型即可工作。
在实际场景中,您需要使用音频数据作为直接输入。
为此,您需要将 YAMNet 与您的模型组合成一个模型,从而导出用于其他应用。
为了便于使用模型的结果,最后一层将为 reduce_mean
运算。使用此模型进行应用时(您将在本教程后续内容中了解),您将需要最后一层的名称。如果未定义,TensorFlow 会自动定义递增式名称,这会使得使其难以测试,因为它会在您每次训练模型时不断变化。使用原始 TensorFlow 运算时,您无法为其分配名称。为了解决这个问题,您将创建一个应用 reduce_mean
的自定义层并将其称为 'classifier'
。
class ReduceMeanLayer(tf.keras.layers.Layer):
def __init__(self, axis=0, **kwargs):
super(ReduceMeanLayer, self).__init__(**kwargs)
self.axis = axis
def call(self, input):
return tf.math.reduce_mean(input, axis=self.axis)
saved_model_path = './dogs_and_cats_yamnet'
input_segment = tf.keras.layers.Input(shape=(), dtype=tf.float32, name='audio')
embedding_extraction_layer = hub.KerasLayer(yamnet_model_handle,
trainable=False, name='yamnet')
_, embeddings_output, _ = embedding_extraction_layer(input_segment)
serving_outputs = my_model(embeddings_output)
serving_outputs = ReduceMeanLayer(axis=0, name='classifier')(serving_outputs)
serving_model = tf.keras.Model(input_segment, serving_outputs)
serving_model.save(saved_model_path, include_optimizer=False)
tf.keras.utils.plot_model(serving_model)
加载您保存的模型以验证它能否按预期工作。
reloaded_model = tf.saved_model.load(saved_model_path)
最终测试:给定一些声音数据,您的模型能否返回正确的结果?
reloaded_results = reloaded_model(testing_wav_data)
cat_or_dog = my_classes[tf.math.argmax(reloaded_results)]
print(f'The main sound is: {cat_or_dog}')
如果您想在应用环境中尝试您的新模型,可以使用 ‘serving_default’ 签名。
serving_results = reloaded_model.signatures['serving_default'](testing_wav_data)
cat_or_dog = my_classes[tf.math.argmax(serving_results['classifier'])]
print(f'The main sound is: {cat_or_dog}')
(可选)更多测试#
模型已准备就绪。
让我们基于测试数据集将它与 YAMNet 进行比较。
test_pd = filtered_pd.loc[filtered_pd['fold'] == 5]
row = test_pd.sample(1)
filename = row['filename'].item()
print(filename)
waveform = load_wav_16k_mono(filename)
print(f'Waveform values: {waveform}')
_ = plt.plot(waveform)
display.Audio(waveform, rate=16000)
# Run the model, check the output.
scores, embeddings, spectrogram = yamnet_model(waveform)
class_scores = tf.reduce_mean(scores, axis=0)
top_class = tf.math.argmax(class_scores)
inferred_class = class_names[top_class]
top_score = class_scores[top_class]
print(f'[YAMNet] The main sound is: {inferred_class} ({top_score})')
reloaded_results = reloaded_model(waveform)
your_top_class = tf.math.argmax(reloaded_results)
your_inferred_class = my_classes[your_top_class]
class_probabilities = tf.nn.softmax(reloaded_results, axis=-1)
your_top_score = class_probabilities[your_top_class]
print(f'[Your model] The main sound is: {your_inferred_class} ({your_top_score})')
后续步骤#
您已创建可对狗或猫的叫声进行分类的模型。利用相同的想法和不同的数据集,您可以尝试构建诸如基于鸟鸣的鸟类声学识别模型。
在社交媒体上与 TensorFlow 团队分享您的项目吧!