Pythonではじめる音響解析！TensorFlow.Kerasの便利な機能「DataSets」を使ってつくるスペクトログラムを用いて音声認識をするニューラルネットワークの解説

<font color="red">**本記事は、都度ソースコードの抜粋を掲載していますが、実装全文は記事の末尾にまとめてありますので必要に応じてご活用ください。**</font>

私がこれまでニューラルネットワークについて紹介してきた記事では、画像を分類するニューラルネットワークが主でした。 <font color="blue">**今回ははじめて音声を扱うニューラルネットワークについて紹介します。**</font> TensorFlow.kerasで音声認識する方法は、公式からチュートリアルとして以下の記事が公開されています。本記事では、公式のチュートリアルに沿って実装を紹介しながら、関連する技術記事をまとめます。 **なお、モデルの精度を上げるためにニューラルネットワークは独自のもの定義しています。** 

https://www.tensorflow.org/tutorials/audio/simple_audio?hl=ja

![announcer-8726227_640.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/5bad4977-5f80-2aea-202e-8dc83a952e0c.jpeg)

本記事は、 <font color="blue">**16bit/16kHzでサンプリングされた1秒の音声が「なんと言っているのか」を当てるニューラルネットワークを実装します。**</font> 入力データは音声データですので、画像と異なり、1次元の振幅データです。 **ですが、音声認識では音声データに対して短時間フーリエ変換を加え「スペクトログラム」と呼ばれる横軸が時間軸、縦軸が周波数軸の、周波数の強弱を示すデータを扱います。これはグレースケール画像の取り扱いとよく似ています。**

#### 音声データ

![スクリーンショット 2025-02-01 16.46.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/9708ae76-e52d-05ec-4564-5bd032481c5e.png)

#### スペクトログラム

![スクリーンショット 2025-02-01 16.47.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/9d222c53-460a-4922-a2b0-0e647a40fe9d.png)

## スペクトログラムを扱う

スペクトログラムについての知識はさまざまなサイトから入手することが出来ます。 **スペクトログラムとは横軸に時間軸を、縦軸に周波数軸をとり、各点に周波数の強度を保持した2次元グレースケール構造のデータ形式です。その瞬間にどの周波数が発話されたかを可視化することができます。そして可視化が出来れば分類することが可能です。** 本記事のネットワークでは **「短時間フーリエ変換(STFT)」** を利用して、16kHzでサンプリングした1秒間の音声を、256点ずつまとめて128点のストライドで高速フーリエ変換(FFT)し、その時々の周波数の強度を算出し、スペクトログラムを生成します。 **スペクトログラムに変換することにより、特徴量を、以下のように1次元の音声データから、2次元の画像データとして扱うことが可能となります。**

![スクリーンショット 2025-02-01 16.46.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/dcf6f1b0-569d-c6dc-aa4c-b7cd79cf2e7e.png)

#### 参考にした技術記事

https://shinshoji01.hatenablog.com/entry/2019/06/30/173422

https://gochikika.ntt.com/Visualization_and_EDA/spectral_visualization.html

https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.spectrogram.html

https://qiita.com/purple_jp/items/7c91a05c547615e9ab89

https://www.gavo.t.u-tokyo.ac.jp/~mine/japanese/nlp+slp/lecture-02.pdf

https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.pcolormesh.html

![スクリーンショット 2025-02-01 16.47.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/9d222c53-460a-4922-a2b0-0e647a40fe9d.png)

### STFTの前処理を実装するには

TensorFlow.kerasを使って音声データを加工する方法は、利用シーンに応じて、いくつも紹介されています。例えば、今回は利用していませんが、 **STFTの前処理として、ノイズの除去や、フェードイン・フェードアウト処理を行う方法について** は、以下の記事が参考になります。

https://www.tensorflow.org/io/tutorials/audio?hl=ja

https://stackoverflow.com/questions/77442410/what-is-the-purpose-of-the-dtype-argument-in-tensorflow-io-audio-audioiotensor

#### 高速フーリエ変換(FFT)

STFTのベースとなる **「FFT」** については下記のサイトの解説が参考になりました。

https://qiita.com/kotai2003/items/577c89b20909b989dbf7

https://qiita.com/kotai2003/items/f5528f70bffa51816c78

https://nsr-9.hatenablog.jp/entry/2021/09/21/120000


## 開発環境を準備する

それではまず、環境を準備します。
作業用ディレクトリを作成しましょう。

```bash
# 作業用ディレクトリを作成する
$ mkdir example_sound_proc_01
$ cd example_sound_proc_01/
```

次に、Pythonの仮想環境を定義します。ここでは **sound_proc** というPythonの仮想環境を定義しました。今回はVisual Studio Codeで実装するため、仮想環境を定義した後に、 **Visual Studio Codeから作成したPythonの仮想環境を選択します。** これでVisual Studio Codeにより実装したPythonスクリプトからTensorFlowを呼び出せるようになります。

https://qiita.com/xtrizeShino/items/be8f405be8c3f72b10d9

```bash
# 開発に利用するPython開発環境を定義する
$ conda create -n sound_proc python=3.10
$ conda activate sound_proc

# 開発に使うライブラリをインストールする
$ pip install tensorflow-io[tensorflow]
$ pip install ipykernel
$ pip install numpy
$ pip install matplotlib
$ pip install pillow
$ pip install opencv-python
$ pip install scikit-learn
$ pip install seaborn
```

![スクリーンショット 2025-02-01 18.56.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/c732b0c0-721c-032f-d0f6-32c68468a79f.png)


## 音声データを取得する

最初に実装するソースコードは、 **音声データ「mini_speech_commands」** を入手するためのコードです。mini_speech_commandsは以下のような **「down」「go」「left」「no」「right」「stop」「up」「yes」** から構成される約1秒(16,000点)の音声データのデータセットです。

![スクリーンショット 2025-02-01 18.56.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/9c648b74-7ea7-c329-a836-28f47748c7cd.png)

フルサイズのデータセットとして **「Speech Commands Dataset」** もあります。こちらのほうがバリエーションに富んでいますので、より多くの言葉を認識できますが、AIモデルが大きくなり、学習時間も長くなります。 **本記事は「mini_speech_commands」を扱います。**


https://www.tensorflow.org/tutorials/audio/simple_audio?hl=ja

https://research.google/blog/launching-the-speech-commands-dataset/?m=1

学習する際のラベル(カテゴリ)は、音声ファイルが格納されているディレクトリが、ひとつのカテゴリとして扱われます。README等のデータも混ざっていますので、 **ファイル名は除いて、ディレクトリ名だけを抽出して** 学習に利用しましょう。

https://note.nkmk.me/python-listdir-isfile-isdir/

https://note.nkmk.me/python-os-exists-isfile-isdir/

本プログラムでは、音声を含むディレクトリが存在するかを確認した後、存在しない場合にkerasのutilsを使ってデータセットをダウンロードします。**「keras.utils.get_file」はダウンロードから圧縮ファイルの展開までこなしてくれる便利なAPI** です。以下のコードでは、音声データの有無を確認した後に、存在しなければ音声データをダウンロードし、最後にディレクトリ名からラベル(カテゴリ)の個数を算出しています。

```python
# ...

######
### データセットがない場合はダウンロードする
######
data_dir = pathlib.Path(DATASET_PATH)
if not data_dir.exists():
    tf.keras.utils.get_file(
        'mini_speech_commands.zip',
        origin="http://storage.googleapis.com/download.tensorflow.org/data/mini_speech_commands.zip",
        extract=True,
        cache_dir='.', 
        cache_subdir='data'
    )

######
### 認識に利用するカテゴリを列挙する
######
commands = np.array(os.listdir(str(data_dir)))
# ディレクトリ名がカテゴリになっているので、ディレクトリ名のみを抽出する
commands = [
    f for f in commands if os.path.isdir(os.path.join(str(data_dir), f))
]
# カテゴリの総数を取得する
NUM_OF_LABELS = len(commands)
# カテゴリを表示する
print('Commands:', commands)

# ...
```


## DataSetsを定義する

TensorFlow.kerasの **「DataSets」** は非常に便利なデータ管理機能です。DataSetsは多くの場合、学習用データセットや検証用データセットとして機能します。 **DataSetsはデータとして存在している音声ファイルを順次読み込んでくれるだけでなく、map指定子を利用することによりデータの読み出し中に加工を加えることが出来ます。** 本記事では、DataSetsを以下の図のように利用します。まず16kHz1秒間のデータを **「audio_dataset_from_directory」** により (16000x1) のテンソルとして読み出した後に、 **「255点/ストライド128点のSTFT」** によりスペクトログラム (124x129) に変換します。最後にTensorFlowのネットワークのshapeにあうように **「expand_dim」** を行い (124x129x1) に拡張します。

![スクリーンショット 2025-02-01 17.26.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/e67babb1-30f2-85ba-1068-bda3c42a44de.png)

#### DataSetsの実装で参考にしたサイト

https://www.tensorflow.org/guide/data?hl=ja

https://qiita.com/typecprint/items/3d10e77e76e74db6e9e9

https://qiita.com/Suguru_Toyohara/items/820b0dad955ecd91c7f3

https://qiita.com/exy81/items/d1388f6f02a11c8f1d7e

https://www.tensorflow.org/datasets?hl=ja

https://deepage.net/tensorflow/2017/07/18/tensorflow-dataset-api.html

https://stackoverflow.com/questions/48771502/is-there-a-way-to-stack-two-tensorflow-datasets

### 音声データを取り出すDataSetsを実装する

まず、keras.utilsの **「audio_dataset_from_directory」** を利用して、データセットのディレクトリから音声データのファイルリストを取得します。DataSetsは、はじめてデータが取り出されようとしたときに、はじめて音声データをメモリ上に展開しますので、 **大きなデータセットを扱ってもメモリ消費はバッチサイズの大きさとプリフェッチの設定により抑えることができます。** また「audio_dataset_from_directory」は **「validation_split」** に指定により学習用データと検証用データを分離することが出来ます。

```python
# ...

######
### データセットを定義する
### -->> 学習用 train_ds, 検証/テスト用 val_ds
######
train_ds, val_ds = tf.keras.utils.audio_dataset_from_directory(
    directory=data_dir,
    batch_size=BATCH_SIZE,
    validation_split=VALIDATION_RATE,
    seed=SEED_FOR_RANDOM,
    output_sequence_length=NUM_OF_AUDIOLEN,
    subset='both')

# データセットからラベル名のリストを取得する
label_names = np.array(train_ds.class_names)
# ラベル名のリスト数とカテゴリ名の個数が一致しているか確認する
# -->> 一致していない場合はロードミス(終了)
if len(label_names) != NUM_OF_LABELS:
    print("ERROR : invalid data lenght (Dataset = {} / Labels = {})"
          .format(len(label_names), NUM_OF_LABELS))
    quit()

# ...
```

### 音声データを加工するDataSetsを定義する

続いて、16kHz/16bitの音声データを、スペクトログラムへ変換するDataSetsを定義します。 **前節で定義したDataSetsを利用することにより、以下のように音声データを読み出すことが出来ますので、このDataSetsにmapを設定** してデータを加工していきます。

![スクリーンショット 2025-02-01 16.46.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/9708ae76-e52d-05ec-4564-5bd032481c5e.png)

スペクトログラムへ変換する関数 **「get_spectrogram」** を実装し、ひとつの音声データを加工してみると、以下のように加工することが出来ます。これをDataSetsのmapに設定することで、ニューラルネットワークの学習時にモデルがDataSetsからデータを読み出そうとすると、 **DataSetsが自動的にファイルシステムから音声ファイルを開き、さらに自動的に音声データをスペクトログラムへと変換し、学習の入力へ手渡してくれます。**

![スクリーンショット 2025-02-01 16.46.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/dcf6f1b0-569d-c6dc-aa4c-b7cd79cf2e7e.png)

結果として、以下がニューラルネットワークの学習時の入力となります。

![スクリーンショット 2025-02-01 16.47.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/9d222c53-460a-4922-a2b0-0e647a40fe9d.png)

https://www.tensorflow.org/api_docs/python/tf/expand_dims

以上を実装したものが、下記のソースコードです。

```python
# ...

######
### 軸をひとつ除外して
### ステレオ音源からモノラル音源にする関数を定義して
### データセットに接続する
######
def squeeze(audio, labels):
  audio = tf.squeeze(audio, axis=-1)
  return audio, labels

# データセットをステレオから
# モノラルへと変換する関数を　DataSetに登録する
train_ds = train_ds.map(squeeze, tf.data.AUTOTUNE)
val_ds = val_ds.map(squeeze, tf.data.AUTOTUNE)

# ...

######
### 波形データを 時間 - 周波数 のスペクトログラムに変換する関数を定義する
######
def get_spectrogram(waveform):
    # Convert the waveform to a spectrogram via a STFT.
    spectrogram = tf.signal.stft(waveform, frame_length=255, frame_step=128)
    # Obtain the magnitude of the STFT.
    spectrogram = tf.abs(spectrogram)
    # Add a `channels` dimension, so that the spectrogram can be used
    # as image-like input data with convolution layers (which expect
    # shape (`batch_size`, `height`, `width`, `channels`).
    spectrogram = spectrogram[..., tf.newaxis]
    return spectrogram

# ...

######
### 音声データからスペクトログラムへ変換する
### -->> map関数を定義し、データセットの出力を
### -->> 波形からスペクトログラムに変換して、軸を追加する
######
def make_spec_ds(ds):
    return ds.map(
        map_func=lambda audio,label: (get_spectrogram(audio), label),
        num_parallel_calls=tf.data.AUTOTUNE)

# mapでスペクトログラムへ変換する関数を登録し
# 新しいデータセットを定義し直す
train_spectrogram_ds = make_spec_ds(train_ds)
val_spectrogram_ds = make_spec_ds(val_ds)
test_spectrogram_ds = make_spec_ds(test_ds)

# ...
```

### DataSetsのキャッシュを有効化する

DataSetsを利用することにより、 **必要なタイミングでデータをメモリ上へ読み出すことができるため、メモリ消費量を削減できますが、次に使いたいデータを先に読み出しておきたいことも多いでしょう。** この場合、下記の **「cache().prefetch(tf.data.AUTOTUNE)」** 属性を加えることで、次に読み出される音声データを先にファイルシステムへアクセスして取り出しておくDataSetsを定義することができます。

```python
# ...

######
### DataSetのキャッシュを有効にする
######
# スペクトログラムのデータセットに
# cache と prefetch を指定してモデルのトレーニング時の読み取りレイテンシを短縮する
train_spectrogram_ds = train_spectrogram_ds.cache().shuffle(10000).prefetch(tf.data.AUTOTUNE)
val_spectrogram_ds = val_spectrogram_ds.cache().prefetch(tf.data.AUTOTUNE)
test_spectrogram_ds = test_spectrogram_ds.cache().prefetch(tf.data.AUTOTUNE)

# ...
```

## モデルを定義する

最後に、 **スペクトログラム (124x129x1) を入力** として、 **ラベル (カテゴリ) に分類** するニューラルネットワークを定義します。入力データにResizeを加えて (128x128x1) のスペクトログラムへと変換した後に、画像認識とほぼ同等のネットワークを使って分類しました。このネットワークでは、まず、画像サイズで畳み込みを行った後に、MaxPoolingを利用して解像度を半分に削減し、さらに畳み込みを行い、解像度を削減することを繰り返し、最終的に全結合層へと接続して、カテゴリを導出します。

![スクリーンショット 2025-02-01 17.26.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/8d6576e0-2716-bafe-0495-b2d268e57ad5.png)

https://www.tensorflow.org/api_docs/python/tf/keras/layers/Conv2D

https://www.tensorflow.org/guide/keras/preprocessing_layers?hl=ja

```python
# ...

# 入出力のshapeを確認する
input_shape = example_spectrograms.shape[1:]
print('Input shape:', input_shape)
num_labels = len(label_names)
print('Output shape:', num_labels)

# 入力データを正規化するためのレイヤーを定義する
norm_layer = layers.Normalization()
norm_layer.adapt(data=train_spectrogram_ds.map(map_func=lambda spec, label: spec))

# ネットワークを定義する
model = models.Sequential([
    layers.Input(shape=input_shape),
    # Downsample the input.
    layers.Resizing(128, 128),
    # Normalize.
    norm_layer,
    layers.Conv2D(8, 3, activation='relu', padding='same'),
    layers.Conv2D(16, 3, activation='relu', padding='same'),
    layers.MaxPooling2D(),
    layers.Conv2D(32, 3, activation='relu', padding='same'),
    layers.Conv2D(64, 3, activation='relu', padding='same'),
    layers.MaxPooling2D(),
    layers.Conv2D(64, 3, activation='relu', padding='same'),
    layers.Conv2D(128, 3, activation='relu', padding='same'),
    layers.MaxPooling2D(),
    layers.Conv2D(128, 3, activation='relu', padding='same'),
    layers.Dropout(0.25),
    layers.Flatten(),
    layers.Dense(128, activation='relu'),
    layers.Dropout(0.5),
    layers.Dense(num_labels),
])

# ネットワークを表示する
model.summary()

######
### ネットワークをコンパイルする
######
model.compile(
    optimizer=tf.keras.optimizers.Adam(),
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=['accuracy'],
)

# ...
```

生成されたネットワークは以下の通りです。

![スクリーンショット 2025-02-01 16.46.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/5139b24d-294a-8d5f-8b99-2b9fa6e9334f.png)

## モデルを学習する

定義したネットワークとDataSetsを利用し、モデルの学習をします。学習結果の可視化にはTensorBoardも利用しました。DataSetsに設定した処理が自動的に行われるため、fitでは、特にスペクトログラムであることを意識して...といったような実装はありません。

https://www.tensorflow.org/tensorboard/get_started?hl=ja

```python
# ...

######
### 学習する
######
# TensorBoard出力用のコールバックを定義する
tensorboard_callback = tf.keras.callbacks.TensorBoard(
        log_dir=LOG_FILE_DIR, histogram_freq=1)

# 学習を実行
history = model.fit(
    train_spectrogram_ds,
    validation_data=val_spectrogram_ds,
    epochs=EPOCHS,
    callbacks=[tensorboard_callback],
)

######
### 学習結果を描画する
######
metrics = history.history
plt.figure(figsize=(16,6))
plt.subplot(1,2,1)
plt.plot(history.epoch, metrics['loss'], metrics['val_loss'])
plt.legend(['loss', 'val_loss'])
plt.ylim([0, max(plt.ylim())])
plt.xlabel('Epoch')
plt.ylabel('Loss [CrossEntropy]')

plt.subplot(1,2,2)
plt.plot(history.epoch, 100*np.array(metrics['accuracy']), 100*np.array(metrics['val_accuracy']))
plt.legend(['accuracy', 'val_accuracy'])
plt.ylim([0, 100])
plt.xlabel('Epoch')
plt.ylabel('Accuracy [%]')

plt.savefig(SAVE_IMAGE_ACC_AND_LOSS)
plt.close()
plt.clf()

# 学習が終了した後、TensorBoardを起動するコマンドを表示する
print("### Exec for TensorBoard: tensorboard --logdir={} --port 6006".format(LOG_FILE_DIR))

# 終了
quit()
```

## 学習結果を確認する

学習を実施した結果、学習データセットに対してはAccuracy 97%、 **検証用データセットに対してはAccuracy 94%** と、非常に高い精度の音声認識を実現することが出来ました。

![スクリーンショット 2025-02-01 20.37.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/0c6605ca-0c1b-3c8e-f026-c1ec82f1b60f.png)

![スクリーンショット 2025-02-01 16.51.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/93b792fa-4c19-f9f9-00ca-11ce839365dc.png)

#### TensorBoardによる表示

![スクリーンショット 2025-02-01 16.53.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/e01b55d9-48a0-0d7f-df0b-4a7653b696e4.png)

----

今回は音声認識と呼ばれる、音声データのカテゴリを当てる問題を紹介しました。今回は1次元のデータとして音声を扱いましたが産業用途では **振動を扱うアプリケーションとして、障害予知(異常検知)など様々なものがあります。** そういったアプリケーションについても、順次紹介していきたいと思いますので、是非ご期待ください。

ここまでお付き合い頂き、ありがとうございました！

----

# ソースコード全文

最後に、本記事にて実装したソースコードの全文を掲載します。

```python
import os
import pathlib

import tensorflow as tf
import tensorflow_io as tfio
from tensorflow.keras import layers
from tensorflow.keras import models

import matplotlib.pyplot as plt
import numpy as np

######
### 乱数のシードを決めておく
######
SEED_FOR_RANDOM = 42
tf.random.set_seed(SEED_FOR_RANDOM)
np.random.seed(SEED_FOR_RANDOM)

######
### 実行に使うファイルパスを設定する
######
# データセットを格納するディレクトリを指定する
DATASET_PATH = 'data/mini_speech_commands_extracted/mini_speech_commands'
# 検証用データと学習用データの比率
VALIDATION_RATE = 0.2
# おんせの長さを指定する
NUM_OF_AUDIOLEN = 16000
# 学習のバッチサイズを指定する
BATCH_SIZE = 64
# 学習回数
EPOCHS = 20
# ラベルの総数
NUM_OF_LABELS = 0 # 後から代入する
# 波形を保存するファイル名
SAVE_IMAGE_WAVES = 'input_waves.png'
# スペクトログラムと波形を保存する
SAVE_IMAGE_WAVE_AND_SPEC = 'input_waves_and_spec.png'
# スペクトログラムを複数表示する
SAVE_IMAGE_SPECS = 'input_specs.png'
# 学習結果を描画する
SAVE_IMAGE_ACC_AND_LOSS = 'graph_acc_and_loss.png'
# TensorBoardのログ出力用ファイルパス
LOG_FILE_DIR = "logs/fit/" 
os.makedirs(LOG_FILE_DIR, exist_ok=True)

######
### データセットがない場合はダウンロードする
######
data_dir = pathlib.Path(DATASET_PATH)
if not data_dir.exists():
    tf.keras.utils.get_file(
        'mini_speech_commands.zip',
        origin="http://storage.googleapis.com/download.tensorflow.org/data/mini_speech_commands.zip",
        extract=True,
        cache_dir='.', 
        cache_subdir='data'
    )

######
### 認識に利用するカテゴリを列挙する
######
commands = np.array(os.listdir(str(data_dir)))
# ディレクトリ名がカテゴリになっているので、ディレクトリ名のみを抽出する
commands = [
    f for f in commands if os.path.isdir(os.path.join(str(data_dir), f))
]
# カテゴリの総数を取得する
NUM_OF_LABELS = len(commands)
# カテゴリを表示する
print('Commands:', commands)

######
### データセットを定義する
### -->> 学習用 train_ds, 検証/テスト用 val_ds
######
train_ds, val_ds = tf.keras.utils.audio_dataset_from_directory(
    directory=data_dir,
    batch_size=BATCH_SIZE,
    validation_split=VALIDATION_RATE,
    seed=SEED_FOR_RANDOM,
    output_sequence_length=NUM_OF_AUDIOLEN,
    subset='both')

# データセットからラベル名のリストを取得する
label_names = np.array(train_ds.class_names)
# ラベル名のリスト数とカテゴリ名の個数が一致しているか確認する
# -->> 一致していない場合はロードミス(終了)
if len(label_names) != NUM_OF_LABELS:
    print("ERROR : invalid data lenght (Dataset = {} / Labels = {})"
          .format(len(label_names), NUM_OF_LABELS))
    quit()

######
### 軸をひとつ除外して
### ステレオ音源からモノラル音源にする関数を定義して
### データセットに接続する
######
def squeeze(audio, labels):
  audio = tf.squeeze(audio, axis=-1)
  return audio, labels

# データセットをステレオから
# モノラルへと変換する関数を　DataSetに登録する
train_ds = train_ds.map(squeeze, tf.data.AUTOTUNE)
val_ds = val_ds.map(squeeze, tf.data.AUTOTUNE)

# 検証/テスト用データセットを
# 検証用とテスト用データセットに2分割する
test_ds = val_ds.shard(num_shards=2, index=0)
val_ds = val_ds.shard(num_shards=2, index=1)

######
### データの形状を確認する
######
for example_audio, example_labels in train_ds.take(1):  
    print("DataSet AUDIO Shape =", str(example_audio.shape))
    print("DataSet LABEL Shape =", str(example_labels.shape))

######
### 波形を描画する
######
plt.figure(figsize=(16, 10))
rows = 3
cols = 3
n = rows * cols
for i in range(n):
    plt.subplot(rows, cols, i+1)
    audio_signal = example_audio[i]
    plt.plot(audio_signal)
    plt.title(label_names[example_labels[i]])
    plt.yticks(np.arange(-1.2, 1.2, 0.2))
    plt.ylim([-1.1, 1.1])

plt.savefig(SAVE_IMAGE_WAVES)
plt.close()
plt.clf()

######
### 波形データを 時間 - 周波数 のスペクトログラムに変換する関数を定義する
######
def get_spectrogram(waveform):
    # Convert the waveform to a spectrogram via a STFT.
    spectrogram = tf.signal.stft(waveform, frame_length=255, frame_step=128)
    # Obtain the magnitude of the STFT.
    spectrogram = tf.abs(spectrogram)
    # Add a `channels` dimension, so that the spectrogram can be used
    # as image-like input data with convolution layers (which expect
    # shape (`batch_size`, `height`, `width`, `channels`).
    spectrogram = spectrogram[..., tf.newaxis]
    return spectrogram

######
### スペクトログラムを描画する
######
def plot_spectrogram(spectrogram, ax):
    if len(spectrogram.shape) > 2:
        assert len(spectrogram.shape) == 3
        spectrogram = np.squeeze(spectrogram, axis=-1)
    # Convert the frequencies to log scale and transpose, so that the time is
    # represented on the x-axis (columns).
    # Add an epsilon to avoid taking a log of zero.
    log_spec = np.log(spectrogram.T + np.finfo(float).eps)
    height = log_spec.shape[0]
    width = log_spec.shape[1]
    X = np.linspace(0, np.size(spectrogram), num=width, dtype=int)
    Y = range(height)
    ax.pcolormesh(X, Y, log_spec)

######
### 最初のデータの波形とスペクトログラムを描画する
######
label = label_names[example_labels[0]]
waveform = example_audio[0]
spectrogram = get_spectrogram(waveform)

fig, axes = plt.subplots(2, figsize=(12, 8))
timescale = np.arange(waveform.shape[0])
axes[0].plot(timescale, waveform.numpy())
axes[0].set_title('Waveform')
axes[0].set_xlim([0, 16000])

plot_spectrogram(spectrogram.numpy(), axes[1])
axes[1].set_title('Spectrogram')
plt.suptitle(label.title())
plt.savefig(SAVE_IMAGE_WAVE_AND_SPEC)
plt.close()
plt.clf()

######
### 音声データからスペクトログラムへ変換する
### -->> map関数を定義し、データセットの出力を
### -->> 波形からスペクトログラムに変換して、軸を追加する
######
def make_spec_ds(ds):
    return ds.map(
        map_func=lambda audio,label: (get_spectrogram(audio), label),
        num_parallel_calls=tf.data.AUTOTUNE)

# mapでスペクトログラムへ変換する関数を登録し
# 新しいデータセットを定義し直す
train_spectrogram_ds = make_spec_ds(train_ds)
val_spectrogram_ds = make_spec_ds(val_ds)
test_spectrogram_ds = make_spec_ds(test_ds)

# データセットから先頭のスペクトログラムを取得する
for example_spectrograms, example_spect_labels in train_spectrogram_ds.take(1):
    break

######
### スペクトログラムの先頭3x3個を描画する
######
rows = 3
cols = 3
n = rows * cols
fig, axes = plt.subplots(rows, cols, figsize=(16, 9))

for i in range(n):
    r = i // cols
    c = i % cols
    ax = axes[r][c]
    plot_spectrogram(example_spectrograms[i].numpy(), ax)
    ax.set_title(label_names[example_spect_labels[i].numpy()])

plt.savefig(SAVE_IMAGE_SPECS)
plt.close()
plt.clf()

######
### DataSetのキャッシュを有効にする
######
# スペクトログラムのデータセットに
# cache と prefetch を指定してモデルのトレーニング時の読み取りレイテンシを短縮する
train_spectrogram_ds = train_spectrogram_ds.cache().shuffle(10000).prefetch(tf.data.AUTOTUNE)
val_spectrogram_ds = val_spectrogram_ds.cache().prefetch(tf.data.AUTOTUNE)
test_spectrogram_ds = test_spectrogram_ds.cache().prefetch(tf.data.AUTOTUNE)

# 入出力のshapeを確認する
input_shape = example_spectrograms.shape[1:]
print('Input shape:', input_shape)
num_labels = len(label_names)
print('Output shape:', num_labels)

# 入力データを正規化するためのレイヤーを定義する
norm_layer = layers.Normalization()
norm_layer.adapt(data=train_spectrogram_ds.map(map_func=lambda spec, label: spec))

# ネットワークを定義する
model = models.Sequential([
    layers.Input(shape=input_shape),
    # Downsample the input.
    layers.Resizing(128, 128),
    # Normalize.
    norm_layer,
    layers.Conv2D(8, 3, activation='relu', padding='same'),
    layers.Conv2D(16, 3, activation='relu', padding='same'),
    layers.MaxPooling2D(),
    layers.Conv2D(32, 3, activation='relu', padding='same'),
    layers.Conv2D(64, 3, activation='relu', padding='same'),
    layers.MaxPooling2D(),
    layers.Conv2D(64, 3, activation='relu', padding='same'),
    layers.Conv2D(128, 3, activation='relu', padding='same'),
    layers.MaxPooling2D(),
    layers.Conv2D(128, 3, activation='relu', padding='same'),
    layers.Dropout(0.25),
    layers.Flatten(),
    layers.Dense(128, activation='relu'),
    layers.Dropout(0.5),
    layers.Dense(num_labels),
])

# ネットワークを表示する
model.summary()

######
### ネットワークをコンパイルする
######
model.compile(
    optimizer=tf.keras.optimizers.Adam(),
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=['accuracy'],
)

######
### 学習する
######
# TensorBoard出力用のコールバックを定義する
tensorboard_callback = tf.keras.callbacks.TensorBoard(
        log_dir=LOG_FILE_DIR, histogram_freq=1)

# 学習を実行
history = model.fit(
    train_spectrogram_ds,
    validation_data=val_spectrogram_ds,
    epochs=EPOCHS,
    callbacks=[tensorboard_callback],
)

######
### 学習結果を描画する
######
metrics = history.history
plt.figure(figsize=(16,6))
plt.subplot(1,2,1)
plt.plot(history.epoch, metrics['loss'], metrics['val_loss'])
plt.legend(['loss', 'val_loss'])
plt.ylim([0, max(plt.ylim())])
plt.xlabel('Epoch')
plt.ylabel('Loss [CrossEntropy]')

plt.subplot(1,2,2)
plt.plot(history.epoch, 100*np.array(metrics['accuracy']), 100*np.array(metrics['val_accuracy']))
plt.legend(['accuracy', 'val_accuracy'])
plt.ylim([0, 100])
plt.xlabel('Epoch')
plt.ylabel('Accuracy [%]')

plt.savefig(SAVE_IMAGE_ACC_AND_LOSS)
plt.close()
plt.clf()

# 学習が終了した後、TensorBoardを起動するコマンドを表示する
print("### Exec for TensorBoard: tensorboard --logdir={} --port 6006".format(LOG_FILE_DIR))

# 終了
quit()
```

記事は以上となります。