環境音分類のためのYAMNetによる転移学習

TensorFlow.orgで表示GoogleColabで実行GitHubで表示 ノートブックをダウンロードTFハブモデルを参照してください

YAMNetは、事前にトレーニングされたディープニューラルネットワークであり、笑い声、吠え声、サイレンなど、 521のクラスからの音声イベントを予測できます。

このチュートリアルでは、次の方法を学習します。

  • 推論のためにYAMNetモデルをロードして使用します。
  • YAMNet埋め込みを使用して新しいモデルを作成し、猫と犬の音を分類します。
  • モデルを評価してエクスポートします。

TensorFlowおよびその他のライブラリをインポートする

TensorFlow I / Oをインストールすることから始めます。これにより、オーディオファイルをディスクから簡単にロードできるようになります。

pip install tensorflow_io
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秒ごとに1つのフレームを抽出します。

このモデルは、 [-1.0, +1.0]の範囲のシングルチャネル(モノラル)16 kHzサンプルとして表される、任意の長さの波形を含む1次元float32TensorまたはNumPy配列を受け入れます。このチュートリアルには、WAVファイルをサポートされている形式に変換するのに役立つコードが含まれています。

モデルは、クラススコア、埋め込み(転送学習に使用)、およびログメルスペクトログラムを含む3つの出力を返します。詳細については、こちらをご覧ください。

YAMNetの特定の用途の1つは、高レベルの特徴抽出器(1,024次元の埋め込み出力)としての使用です。ベース(YAMNet)モデルの入力フィーチャを使用して、1つの非表示のtf.keras.layers.Denseレイヤーで構成される浅いモデルにフィードします。次に、大量のラベル付きデータを必要とせずに、オーディオ分類のために少量のデータでネットワークをトレーニングし、エンドツーエンドでトレーニングします。 (これは、詳細については、TensorFlow Hubを使用した画像分類の転移学習に似ています。)

まず、モデルをテストし、オーディオの分類結果を確認します。次に、データ前処理パイプラインを構築します。

TensorFlowハブからのYAMNetの読み込み

Tensorflow Hubから事前にトレーニングされたYAMNetを使用して、サウンドファイルから埋め込みを抽出します。

TensorFlow Hubからモデルをロードするのは簡単です。モデルを選択し、そのURLをコピーして、 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)
Downloading data from https://storage.googleapis.com/audioset/miaow_16k.wav
221184/215546 [==============================] - 0s 0us/step
229376/215546 [===============================] - 0s 0us/step
./test_data/miaow_16k.wav

オーディオファイルをロードする機能が必要になります。この機能は、後でトレーニングデータを操作するときにも使用されます。 (オーディオファイルとそのラベルの読み取りについて詳しくは、シンプルオーディオ認識をご覧ください。

# 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)
2022-01-26 08:07:19.084427: W tensorflow_io/core/kernels/audio_video_mp3_kernels.cc:271] libmp3lame.so.0 or lame functions are not available
WARNING:tensorflow:Using a while_loop for converting IO>AudioResample
WARNING:tensorflow:Using a while_loop for converting IO>AudioResample

png

クラスマッピングをロードする

YAMNetが認識できるクラス名をロードすることが重要です。マッピングファイルは、 yamnet_model.class_map_path()にCSV形式で存在します。

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('...')
Speech
Child speech, kid speaking
Conversation
Narration, monologue
Babbling
Speech synthesizer
Shout
Bellow
Whoop
Yell
Children shouting
Screaming
Whispering
Laughter
Baby laughter
Giggle
Snicker
Belly laugh
Chuckle, chortle
Crying, sobbing
...

推論を実行する

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.argmax(class_scores)
inferred_class = class_names[top_class]

print(f'The main sound is: {inferred_class}')
print(f'The embeddings shape: {embeddings.shape}')
The main sound is: Animal
The embeddings shape: (13, 1024)

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)
Downloading data from https://github.com/karoldvl/ESC-50/archive/master.zip
645103616/Unknown - 47s 0us/step

データを調べる

各ファイルのメタデータは、。 ./datasets/ESC-50-master/meta/esc50.csvのcsvファイルで指定されます。

すべてのオーディオファイルは./datasets/ESC-50-master/audio/にあります

マッピングを使用してパンダの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 )のみを使用します。他のクラスを使用したい場合は、ここで選択できます。
  • フルパスを持つようにファイル名を修正します。これにより、後でロードしやすくなります。
  • ターゲットを特定の範囲内に変更します。この例では、 dog0のままですが、 catは元の値の5ではなく1になります。
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秒ごとに1つ)です。

モデルは、各フレームを1つの入力として使用します。したがって、行ごとに1つのフレームを持つ新しい列を作成する必要があります。また、これらの新しい行を適切に反映するには、ラベルと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
(TensorSpec(shape=(), dtype=tf.string, name=None),
 TensorSpec(shape=(), dtype=tf.int64, name=None),
 TensorSpec(shape=(), dtype=tf.int64, name=None))
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
WARNING:tensorflow:Using a while_loop for converting IO>AudioResample
WARNING:tensorflow:Using a while_loop for converting IO>AudioResample
(TensorSpec(shape=<unknown>, dtype=tf.float32, name=None),
 TensorSpec(shape=(), dtype=tf.int64, name=None),
 TensorSpec(shape=(), dtype=tf.int64, name=None))
# 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
(TensorSpec(shape=(1024,), dtype=tf.float32, name=None),
 TensorSpec(shape=(), dtype=tf.int64, name=None),
 TensorSpec(shape=(), dtype=tf.int64, name=None))

データを分割する

fold列を使用して、データセットをトレーニング、検証、およびテストセットに分割します。

ESC-50は、5つの均一なサイズの交差検定foldに配置されているため、同じ元のソースからのクリップは常に同じfoldになります。詳細については、 ESC:環境サウンド分類のデータセットを参照してください。

最後のステップは、トレーニング中に折り畳み列を使用しないため、データセットから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)

モデルを作成する

あなたはほとんどの仕事をしました!次に、音から猫と犬を認識するために、1つの隠れ層と2つの出力を持つ非常に単純なシーケンシャルモデルを定義します。

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()
Model: "my_model"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 dense (Dense)               (None, 512)               524800    
                                                                 
 dense_1 (Dense)             (None, 2)                 1026      
                                                                 
=================================================================
Total params: 525,826
Trainable params: 525,826
Non-trainable params: 0
_________________________________________________________________
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)
Epoch 1/20
15/15 [==============================] - 6s 49ms/step - loss: 0.7811 - accuracy: 0.8229 - val_loss: 0.4866 - val_accuracy: 0.9125
Epoch 2/20
15/15 [==============================] - 0s 18ms/step - loss: 0.3385 - accuracy: 0.8938 - val_loss: 0.2185 - val_accuracy: 0.8813
Epoch 3/20
15/15 [==============================] - 0s 18ms/step - loss: 0.3091 - accuracy: 0.9021 - val_loss: 0.4290 - val_accuracy: 0.8813
Epoch 4/20
15/15 [==============================] - 0s 18ms/step - loss: 0.5354 - accuracy: 0.9062 - val_loss: 0.2074 - val_accuracy: 0.9125
Epoch 5/20
15/15 [==============================] - 0s 18ms/step - loss: 0.4651 - accuracy: 0.9333 - val_loss: 0.6857 - val_accuracy: 0.8813
Epoch 6/20
15/15 [==============================] - 0s 18ms/step - loss: 0.2489 - accuracy: 0.9167 - val_loss: 0.3640 - val_accuracy: 0.8750
Epoch 7/20
15/15 [==============================] - 0s 17ms/step - loss: 0.2020 - accuracy: 0.9292 - val_loss: 0.2158 - val_accuracy: 0.9125
Epoch 8/20
15/15 [==============================] - 0s 16ms/step - loss: 0.4550 - accuracy: 0.9208 - val_loss: 0.9893 - val_accuracy: 0.8750
Epoch 9/20
15/15 [==============================] - 0s 17ms/step - loss: 0.3434 - accuracy: 0.9354 - val_loss: 0.2670 - val_accuracy: 0.8813
Epoch 10/20
15/15 [==============================] - 0s 17ms/step - loss: 0.2864 - accuracy: 0.9208 - val_loss: 0.5122 - val_accuracy: 0.8813

過剰適合がないことを確認するために、テストデータに対してevaluateメソッドを実行してみましょう。

loss, accuracy = my_model.evaluate(test_ds)

print("Loss: ", loss)
print("Accuracy: ", accuracy)
5/5 [==============================] - 0s 9ms/step - loss: 0.2526 - accuracy: 0.9000
Loss:  0.25257644057273865
Accuracy:  0.8999999761581421

できたね!

モデルをテストする

次に、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}')
The main sound is: cat

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)
WARNING:tensorflow:Compiled the loaded model, but the compiled metrics have yet to be built. `model.compile_metrics` will be empty until you train or evaluate the model.
WARNING:tensorflow:Compiled the loaded model, but the compiled metrics have yet to be built. `model.compile_metrics` will be empty until you train or evaluate the model.
2022-01-26 08:08:33.807036: W tensorflow/python/util/util.cc:368] Sets are not currently considered sequences, but this may change in the future, so consider avoiding using them.
INFO:tensorflow:Assets written to: ./dogs_and_cats_yamnet/assets
INFO:tensorflow:Assets written to: ./dogs_and_cats_yamnet/assets
tf.keras.utils.plot_model(serving_model)

png

保存したモデルをロードして、期待どおりに機能することを確認します。

reloaded_model = tf.saved_model.load(saved_model_path)

そして最後のテストでは、いくつかのサウンドデータが与えられた場合、モデルは正しい結果を返しますか?

reloaded_results = reloaded_model(testing_wav_data)
cat_or_dog = my_classes[tf.argmax(reloaded_results)]
print(f'The main sound is: {cat_or_dog}')
The main sound is: cat

新しいモデルをサービング設定で試してみたい場合は、「serving_default」シグニチャを使用できます。

serving_results = reloaded_model.signatures['serving_default'](testing_wav_data)
cat_or_dog = my_classes[tf.argmax(serving_results['classifier'])]
print(f'The main sound is: {cat_or_dog}')
The main sound is: cat

(オプション)さらにいくつかのテスト

モデルの準備が整いました。

テストデータセットの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)
./datasets/ESC-50-master/audio/5-214759-A-5.wav
WARNING:tensorflow:Using a while_loop for converting IO>AudioResample
WARNING:tensorflow:Using a while_loop for converting IO>AudioResample
Waveform values: [ 3.2084468e-09 -7.7704687e-09 -1.2222010e-08 ...  2.2788899e-02
  1.0315948e-02 -3.4766860e-02]

png

# Run the model, check the output.
scores, embeddings, spectrogram = yamnet_model(waveform)
class_scores = tf.reduce_mean(scores, axis=0)
top_class = tf.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.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})')
[YAMNet] The main sound is: Silence (0.500638484954834)
[Your model] The main sound is: cat (0.9981643557548523)

次のステップ

犬や猫の音を分類できるモデルを作成しました。同じアイデアと異なるデータセットを使用して、たとえば、鳥の歌声に基づいて鳥の音響識別子を作成することを試みることができます。

プロジェクトをソーシャルメディアでTensorFlowチームと共有してください!