コード進行認識AIを作るうえで工夫したこと2

https://qiita.com/anime_magic_2/items/65fc0f3f247912f04b21

この記事を書いてから一年以上が経過しました。
私はまだコード認識AIの開発を続けていて、日々いろいろなことを試しています。
今では過去のAIよりも高精度で予測することができるようになりました。
その間に試したこと、改善したこと、うまくいかなかったことなどを紹介したいと思います。

今回はまだ開発中のためAIは公開していません。

- [改善したこと](#改善したこと)
    - [入力の系列長を長くする](#入力の系列長を長くする)
    - [マスキングを行う](#マスキングを行う)
    - [ClassBalancedLossを利用する](#ClassBalancedLossを利用する)
    - [相対的なコードを予測する](#相対的なコードを予測する)
    - [音源分離を利用する](#音源分離を利用する)
    - [Modelsoupsを利用する](#Modelsoupsを利用する)

- [うまくいかなかったこと](#うまくいかなかったこと)
    - [プーリングで時間軸を圧縮する](#プーリングで時間軸を圧縮する)
    - [Transformerを利用する](#Transformerを利用する)


# 改善したこと
## 入力の系列長を長くする
音楽データは、Aメロ、Bメロなどの区間での短期間の繰り返しや、1番、2番での長期間の繰り返し構造があります。
こういった繰り返し構造をとらえるために、より長い系列をモデルに入力します。
そうすることで長い期間での依存関係をとらえることができます。
具体的には、モデルに入力する系列長を512から8192にまで増やしました。
時間に換算すると5.12秒から81.92秒になります。

## マスキングを行う
入力の系列長を長くしたことで、切り出し位置や、楽曲の長さによっては8192に満たない場合があります。
その場合はパディングをし長さを合わせるのですが、そのままだとパディング部分が損失や精度に影響してしまいます。
パディング部分がわかるようにパディング部分の値を-1.0とします。
以下のように損失関数でパディング部分が影響しないようにマスキングを行います。
```python
def tversky(y_true, y_pred, alpha=0.5, smooth=1e-6):
    y_true_boolean_mask = tf.not_equal(y_true[:, :, 0], -1)

    y_true = tf.boolean_mask(y_true, y_true_boolean_mask)
    y_pred = tf.boolean_mask(y_pred, y_true_boolean_mask)

    # flatten label and prediction tensors
    y_true = K.flatten(y_true)
    y_pred = K.flatten(y_pred)

    epsilon = K.epsilon()
    y_pred = K.clip(y_pred, epsilon, 1. - epsilon)

    # True Positives, False Positives & False Negatives
    TP = K.sum(y_true * y_pred, axis=-1)
    FP = K.sum((1 - y_true) * y_pred, axis=-1)
    FN = K.sum(y_true * (1 - y_pred), axis=-1)
    
    return (TP + smooth) / (TP + alpha * FP + (1 - alpha) * FN + smooth)
```
one-hotエンコードされる場合は以下のように本来のクラス数+1とします。
```python
# ベースの正解のデータ
bass = to_categorical(BASS_DATA, 13 + 1)

def focal_loss(y_true, y_pred):
    mask = tf.not_equal(y_true[:, :, -1], 1)
    y_true = y_true[:, :, :-1]

    epsilon = K.epsilon()
    y_pred = K.clip(y_pred, epsilon, 1.0 - epsilon)
    
    cross_entropy = -y_true * K.log(y_pred)
    loss = K.pow((1 - y_pred), gamma) * cross_entropy
    
    # masking
    loss = tf.ragged.boolean_mask(loss, mask)
    loss = K.mean(K.sum(tf.reduce_mean(loss, axis=1), axis=-1))
    return loss
```

評価関数もマスキングを行い、正しい精度が計算できるようにしました。

```python
def maskacc(y_true, y_pred):
    y_true_boolean_mask = tf.not_equal(y_true[:, :, -1], 1)
    y_true = y_true[:, :, :-1]

    y_true = tf.boolean_mask(y_true, y_true_boolean_mask)
    y_pred = tf.boolean_mask(y_pred, y_true_boolean_mask)

    return tf.keras.metrics.categorical_accuracy(y_true, y_pred)

def maskbacc(y_true, y_pred):
    y_true_boolean_mask = tf.not_equal(y_true[:, :, 0], -1)
    y_true = tf.boolean_mask(y_true, y_true_boolean_mask)
    y_pred = tf.boolean_mask(y_pred, y_true_boolean_mask)

    return tf.keras.metrics.categorical_accuracy(y_true, y_pred)
```

上記のマスキングを行っていなかったがために、異常に高い精度が出てしまっていました。

## ClassBalancedLossを利用する
前回の記事ではコードのデータは不均衡のため重みを調節していると書きましたが、すべて手動で行っていました。
手動で行うのは効率的ではないし、データが変わればそのたびに調整する必要があります。
そこでClass-Balanced Lossを利用し、クラスのサンプル数から重みを自動で計算できるようにしました。

下記はその実装です。
```python
with open("chord_samples_num.json") as f:
    chord_cls_num = np.array(list(json.load(f).values())) / 1000

def categorical_focal_loss(alpha=None, gamma=1.333, beta=0.9999):
    alpha_fix = alpha
    if alpha is not None:
        effective_num = 1.0 - np.power(beta, alpha)
        per_cls_weights = (1.0 - beta) / np.array(effective_num)
        per_cls_weights = per_cls_weights / np.sum(per_cls_weights) * len(alpha)
        weights = tf.convert_to_tensor(per_cls_weights, dtype=tf.float32)
    def focal_loss(y_true, y_pred):
        mask = tf.not_equal(y_true[:, :, -1], 1)
        y_true = y_true[:, :, :-1]

        epsilon = K.epsilon()
        y_pred = K.clip(y_pred, epsilon, 1.0 - epsilon)
        
        cross_entropy = -y_true * K.log(y_pred)
        if alpha_fix is not None:
            loss = weights * K.pow((1 - y_pred), gamma) * cross_entropy
        else:
            loss = K.pow((1 - y_pred), gamma) * cross_entropy
        
        loss = tf.ragged.boolean_mask(loss, mask)
        loss = K.mean(K.sum(tf.reduce_mean(loss, axis=1), axis=-1))
        return loss
    return focal_loss
```
クラスのサンプル数はフレームごとにカウントしたためかなり大きな数となってしまい、そのままではすべての重みが1.0になってしまいました。なので1000という適当な数字で割っています。

## 相対的なコードを予測する
前回の記事でコード進行のルールや音の関係性などを学習させるためにキーを予測すると書きましたが、実際に予測しているコードはすべて絶対的なもので、キーから見た相対的なコードではありませんでした。
これではそのコードがそのキーでどのような役割を持つか学習できないのではと考えました。
具体的な例でいうと、CキーでのGというコードとGキーでのGというコードでは役割が全く異なります。
そういったキーによる役割を学習させるために、キーから見た相対的なコードを予測するようにしました。

## 音源分離を利用する
コードを予測するときに、ドラムやボーカルといったものはコードを予測するのに関係ない音になります。
こういったノイズを少なくするために、音源分離を利用し、楽器ごとに音を分離します。

具体的には、音源分離モデルを学習させ、分離したスペクトログラムをコード予測モデルに入力します。
学習はステップごとに分け、音源分離モデルを学習させ、コード予測モデルを学習させます。
分離したものは音源に復元する必要はないので、音源分離モデルにはコード予測モデルに入力するスペクトログラムをそのまま入力します。

## Modelsoupsを利用する
複数のモデルの重みを平均化して精度を上げる手法らしいです。それなりに精度が向上しました。

# うまくいかなかったこと
## プーリングで時間軸を圧縮する
RunLengthEncodingを思いつくまでは、プーリングで時間軸を圧縮していました。
学習自体は問題なくできたのですが、短いコードの予測ができなくなってしまいました。プーリングで細かい情報が欠落してしまったのだと思います。
またこの方法では圧縮、復元に多くのレイヤーを利用するためリソースが多く使われてしまっていました。

## Transformerを利用する
長期の依存関係を捉えることができるといわれるTransformerですが、ただ単純にLSTMの代わりとして利用してもうまくいきませんでした。Attentionの重みは崩壊し、同じ出力を返すのみになってしまいました。[^3]

[^1]: Transformerではそのような問題は起こりにくい可能性はありますが、8192もの系列長を入力するとリソースが足りなくります。
[^2]: ちなみにこの方法はRun-Length Encodingというらしいです。手法を思いついた後にChat-GPTにこの手法に名前があるかどうか聞いたら教えてくれました。
[^3]: 使用メモリー的にも多くのレイヤーを重ねられなかったです。