使用基于 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 中,请应用一些转换:

  • 过滤掉行并仅使用所选类 - dogcat。如果您想使用任何其他类,则可以在此处进行选择。

  • 修改文件名以获得完整路径。这将使后续加载更加容易。

  • 将目标更改到特定区间内。在此示例中,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 团队分享您的项目吧!