Deep Learningで楽曲認識

様々な曲を聴かせて、そのアーティストが誰なのかを認識させようという試みです。認識部分はDeep Learningに任せます。頼んだ！

イメージとしては学習したモデルを用いて、マイクに向けて直接音楽を流して、「これはミスチルや！」という感じにリアルタイムに推定してくれるシステムを作りました。

githubに公開しました。
なお、学習時はGPUを使っていますが、学習済みモデルを使って推論させるときはGPUではなくCPUを使うコードになっていますが、別にGPUでも動きます。
（単に、推論させるときには別のPCで実行させていて、そのPCにはGPUが載っていなかったからというだけです。）
https://github.com/slowsingle/music_classification

# 「頼んだ！」とは言いつつも...

曲の波形をそのままネットワークに学習させても、訓練誤差は縮まれど、実際に出来上がったモデルで推論させてみるとびっくりするほど上手くいきませんでした。調べてみると、音楽を周波数領域に変換して画像のように扱うと良いとのことなので、その手法を採用しました。

処理の流れとしては、まず数秒程度の音声波形を保存して、それをメル周波数ケプストラムに変換します。

音声をフーリエ変換して得たパワースペクトルの対数をとって、さらにそれに逆フーリエ変換をかけたのがケプストラムです。以下のサイトにケプストラムについてのわかりやすい説明がありました。

[ケプストラム分析 - 人工知能に関する断創録](http://aidiary.hatenablog.com/entry/20120211/1328964624)

ケプストラムに変換すると、その人の声の特徴と、何の言葉を発しているのかという特徴とを区別することができるようです。人の声は声帯の振動から発せられる音源と、声帯から口の外までの声道の特性によって決定されるわけですが、ケプストラムの低次成分がスペクトル包絡に対応し、これが声道特性を表します。高次成分はスペクトルの微細構造に対応し、この中に発話者の基本周波数が隠れています。

今回は、人間の知覚的尺度に基づいたメル尺度を用いてメル周波数ケプストラムに変換します。これでボーカルの特徴が取れるかな、と期待しながらこの特徴量をネットワークの入力としました。

ちなみに、メル周波数ケプストラムに変換するのはlibrosaというモジュールを使うと簡単にできます。

```python:music2melspec.py
import librosa, librosa.display
from scipy.io.wavfile import read
import matplotlib.pyplot as plt

fs, data = read("音楽.wav")
# メル周波数ケプストラムを取得
melspecs = librosa.feature.melspectrogram(y=data, sr=fs,
                                          n_fft=2048, n_mels=128)
# 可視化
librosa.display.specshow(librosa.power_to_db(melspecs, ref=np.max),
                         x_axis='time', y_axis='mel', fmax=fs)
plt.colorbar(format='%+2.0f dB')
plt.show()
```

可視化すると以下のようになります。これはとある楽曲の最初から23秒くらいまでの音声波形をメル周波数ケプストラムに変換したものをログスケールにしたものです。これだけで曲がわかったらすごいもんです。SAOの映画、良かったですね。主題歌も最高でした。

<img src="https://qiita-image-store.s3.amazonaws.com/0/72509/c26e2add-c462-15f2-d012-c8194dd8d464.png" width=70%>

# 学習

wavファイルから数秒ほど切り取って、上記の変換を施して学習させます。ネットワークは画像から犬や猫を識別するような物体認識と同じ構造です。教師信号としては、その曲のアーティストに対応するラベルを与えています。

```python
class MUSIC_NET(chainer.Chain):
    def __init__(self, ):
        super(MUSIC_NET, self).__init__(
            conv1=L.Convolution2D(in_channels=1, out_channels=16,
                                  ksize=(16, 9), stride=4, pad=0,
                                  wscale=0.02 * math.sqrt(16 * 9)),
            conv2=L.Convolution2D(in_channels=16, out_channels=32,
                                  ksize=(5, 3), stride=2, pad=0,
                                  wscale=0.02 * math.sqrt(16 * 5 * 3)),
            conv3=L.Convolution2D(in_channels=32, out_channels=64,
                                  ksize=(3, 3), stride=2, pad=0,
                                  wscale=0.02 * math.sqrt(32 * 3 * 3)),
            fc4=L.Linear(in_size=64 * 14 * 19, out_size=4096,
                         wscale=0.02 * math.sqrt(64 * 14 * 19)),
            fc5=L.Linear(in_size=4096, out_size=7, wscale=0.02 * math.sqrt(4096)),
        )

    def __call__(self, x, t):
        y = self.forward(x)
        loss = F.softmax_cross_entropy(y, t)
        accuracy = F.accuracy(y, t)
        return loss, accuracy

    def forward(self, x):
        conv1 = F.relu(self.conv1(x))
        conv2 = F.relu(self.conv2(conv1))
        conv3 = F.relu(self.conv3(conv2))
        reshape3 = F.dropout(F.reshape(conv3, (-1, 64 * 14 * 19)), ratio=0.5)
        fc4 = F.dropout(F.relu(self.fc4(reshape3)), ratio=0.5)
        fc5 = self.fc5(fc4)
        return fc5

    def predict(self, x):
        y = self.forward(x)
        return F.softmax(y)
```

プーリング層はないほうが精度が高かったですね。カメラ画像とは違って、ちょっとしたズレというのが与える影響が大きいからでしょうか。

ネットワークの記述はまあ、シンプルで良いんですが、データセットをどう与えるかが悩みの種です。データセットをさばく処理をいつも書いている気がします。

```python:dataset.py
import numpy as np
import librosa
from scipy.io.wavfile import read


class READ_DATASET(object):
    def __init__(self, wavfile, chunk, length, expected_fs=None):
        fs, all_data = read(wavfile)
        if expected_fs != None and expected_fs != fs:
            print("It has difference between expected_fs and fs")
            raise AssertionError

        all_data = all_data.astype('float64') - 128.0
        all_data /= 128.0

        self.all_data = all_data
        self.sampling_rate = fs
        self.CHUNK = chunk
        self.length = length

        # ノイズを読み込む
        self.noise = np.load('noise/noise.npy')  # 8bit 16000Hz
        self.noise = self.noise.astype('float32') / 128.0

        # インデックス。初期化時は昇順にしておく
        n_bolcks_all = len(self.all_data) - self.CHUNK * self.length - 1
        self.indexes = np.linspace(0, n_bolcks_all, int(n_bolcks_all / 5.0)).astype(np.int64)
        self.n_blocks = len(self.indexes)

        print("sampling rate is {}".format(fs))

    def shuffle_indexes(self):
        self.indexes = np.random.permutation(len(self.indexes))

    # ノイズの追加
    def _add_noise(self, data, scale=None):
        if scale is None:
            scale = np.random.uniform(low=0.001, high=3.0)
        start_i = np.random.randint(low=0, high=len(self.noise) - len(data))
        noise = self.noise[start_i:(start_i + len(data))]
        data_with_noise = data + noise * scale
        return data_with_noise

    # 音量調整
    def _change_volume(self, data, volume=None):
        if volume is None:
            volume = np.random.uniform(low=0.1, high=1.0)
        data_changed_vol = data * volume
        return data_changed_vol

    # 1個データを取り出す(mel-spec)
    def get_one_melspec(self, index):
        start_i = self.indexes[index]
        data = self.all_data[start_i:(start_i + self.CHUNK * self.length)].copy()

        # データの変形
        data = self._add_noise(data)  # ノイズ追加
        data = self._change_volume(data)  # 音量調節

        melspecs = librosa.feature.melspectrogram(y=data, sr=self.sampling_rate,
                                                  n_fft=2048, n_mels=256)
        return melspecs

    # 複数個データを取り出す(mel-spec)
    def get_batch_melspec(self, indexes):
        melspecs_dataset = list()
        for index in indexes:
            melspecs = self.get_one_melspec(index)
            melspecs_dataset.append(melspecs[np.newaxis, :])
        return np.array(melspecs_dataset)

def main():
    read_dataset = READ_DATASET(wavfile='8bit-16000Hz.wav',
                                chunk=1024, length=160, expected_fs=16000)
    read_dataset.shuffle_indexes()

    melspecs_s = read_dataset.get_batch_melspec(np.arange(10))

if __name__ == "__main__":
    main()
```

8bit、16000Hzの音楽を1024*160の長さの分だけ取り出してメル周波数スペクトラムに変換すると、（256, 321）のサイズの画像ができます。
音楽はwavファイルから、ノイズはpyaudioから取得して.npyファイルに変換したものを用いています。

学習済みモデルを用いて、pyaudioモジュールからリアルタイムで推定をさせることを想定しているので、ノイズのないきれいな音楽ファイルだけではきっと推定が上手く行かないだろうと思い（実際上手くいかない）、ノイズを足し合わせることにしました。

ちなみに、メル周波数スペクトラムに変換したデータはそのままDNNに与えて学習させても良いのですが、隠し味を入れることで推定精度がより上がります。
データをxsで表したとき、以下の関数を挟むだけです。

```py
xs = np.log(xs + 1.0)
```

変数melspec_sの値は0以上なのでエラーは吐きません。大きな値は少しだけ小さな値になります。入力データのスケーリングって、学習においては結構重要なことで、大きすぎる値があると学習がそれに強く引っ張られてしまうので、それを防いでいます。なので、ルートをとっても良かったりします。

それにしても、librosaモジュール素晴らしいですね。
最初は自分でコード書いてましたけど、最後はlibrosaに甘えてしまいました。