会話の内容にあわせてBGMを自動選曲する

#１．はじめに
　Sonyとレッジが企画する第２弾となるAI開発コンテスト「[Neural Network Console Challenge](https://nnc-challenge.com/)」に再挑戦。Audiostockの音声（BGM）データや書誌（曲の説明文）を解析して、**自由課題「日常会話の内容にあわせてBGMを自動選曲するプレーヤーを作る」**に取り組んでいきます。
　Google Homeなどのスマートスピーカーが、部屋にいる人の会話内容にあわせてBGMを自動再生していくシステムを考えます（プライバシーの観点から実用化のハードルは高そうですが…）。

★突き抜けたチャレンジが評価され（異端？笑）、「Ledge.ai賞」を頂きました！！→→[結果はこちら](https://ledge.ai/nnc-challenge-audiostock-report/)

#２．実験環境＆データ
・Google Colaboratory(python3)
・Neural Network Console(Windows版)
※参加に当たりクラウド版のGPU利用申請はしましたが、結局ローカル版で十分な処理量でしたので、アウトプットはWindows版での結果となりました
・学習用データ提供：Audiostock 約1万曲と書誌。書誌は以下のような一行説明とタグがついています。

| 作品No.| データ名| 一行説明 | タグ |
|:-:|:-:|:-:|:-:|
| 42554	  | audiostock_42554.wav  | オープニングに最適な曲です  | オープニング  |
| 42555	  | audiostock_42555.wav  | ボサノバな曲です  | ボサノバ  |
| 42556	  | audiostock_42556.wav  | ほのぼのとしたイージーリスニング コミカル  | コミカル,かわいい,温かい,ほのぼの,イージーリスニング  |
| 42557	  | audiostock_42557.wav  | 変拍子な曲です  | 変拍子 |

#３．BGMの自動分類
　BGMの音声データ（WAV）を使い、自動分類をするためのモデルをNNCを活用して作成します。時間の都合上、今回は３クラス分類ができるモデルを構築しました。

### ３－１．アノテーションと学習データ
　どのようなクラスを作ることが望ましいか調査するために、まず、テキストデータを統計的に分析できる「KHcoder」を使って、上記の「一行説明」に含まれる単語を調査。上位の結果は以下のようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590631/9ab3c2ed-9d70-4f87-b657-a86947fdcf29.png)			
　これらの中から、実際にBGMを聞いてみつつ分類できそうな（テンポや音色などが違う）
"ロック", "ポップ", "バラード"のいづれかが含まれる曲を学習データにすることに。学習データ１４６８件、評価データ１０５件を作成しました。また、作成にあたり効果音のような音源（ジングル）は、曲の長さが短いので対象外としました。

### ３－２．メル周波数ケプストラム係数に変換
　BGMのWAVデータをメル周波数ケプストラム係数に変換し、４０次元のベクトルに落とし込んでいきます（詳しくは割愛しますが、[このページ](https://qiita.com/martin-d28jp-love/items/34161f2facb80edd999f)に詳しく書いてありました）。縦軸の音の高さ毎に平均を取り(1,40)の配列にして、学習用データとしました。

```python:Wav_to_Mel.py
import pandas as pd
import numpy as np
import librosa

y, sr = librosa.load(file_name)
#４０次元で特徴量抽出
mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=40)
#縦軸で平均を計算し、出力
S_A = np.mean(mfcc, axis = 1)
np.savetxt(output_filename, S_A.reshape(1, -1), delimiter=',', fmt="%s")
```

### ３－３．NNCで分類モデルを作る
　NNCでベクトルを分類するモデルを学習させてました。CNNでの解法が一般的なようですが、様々なネットワークや活性化関数を試してみたところ、結局以下の設定が最も精度が高いという結果に。この辺は時間があれば、さらに実験を繰り返したいところです。
　ちなみにNNCの利点でいうと、関数を変えてみるなどの試行錯誤がGUIが整備されているのですごい楽です。どのようなネットワークになっているかも直感的に分かりますし、Google Colabなどと比較した場合の魅力の一つだと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590631/6033a799-37c0-6be3-8205-1a43cbae76b9.png)
低次元のベクトルを学習させたので、今回はCPU(Windows版)で十分な処理量でしたが、ほぼ同設定で学習したクラウド版での学習結果も、一応Publishing予定。３０エポック学習させ、学習曲線は以下のようになりました（Best Validationは９エポック目でした）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590631/46237136-26ad-197e-fbe9-11056f531721.png)
　次に、作成したモデルを用い、テストデータで評価し、精度をはかってみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590631/0eb66693-7f0b-542f-0683-0ab704b0ba57.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590631/4d7c6bc6-90d5-db15-1e70-879384d416e8.png)
３分類問題とはいえ、Accuracyは0.8とある程度の特徴はとれていそうです。平均適合率も約８割以上となり、適したBGMを選曲するという課題に対しては価値のあるモデルとなっていそうです。

#４．日常会話を分析する
　Bertの学習済みモデルを活用し、会話内容と近しいBGMを一行説明から選び出します。会話（テキスト）のベクトルを算出し、コサイン類似度で最も近い一行説明をもつBGMを選出します。まずはテキストベースで相応しいBGMを探し、その後は３．でつくったBGMを元にした分類に基づく選曲に繫げていきます。NNCでBERTの実装が思いつかなかったので、知見のあるGoogle colabとtransformersで処理しました（個人的にはNNCは画像分野は充実しているので、次は自然言語周りの強化をしてくれると仕事的にも嬉しいですね）。

```python:Conversation_to_BGM.py
import pandas as pd
import numpy as np
import torch
import transformers

from transformers import BertJapaneseTokenizer
from tqdm import tqdm
tqdm.pandas()

class BertSequenceVectorizer:
    def __init__(self):
        self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
        self.model_name = 'cl-tohoku/bert-base-japanese-whole-word-masking'
        self.tokenizer = BertJapaneseTokenizer.from_pretrained(self.model_name)
        self.bert_model = transformers.BertModel.from_pretrained(self.model_name)
        self.bert_model = self.bert_model.to(self.device)
        self.max_len = 128
            
    def vectorize(self, sentence : str) -> np.array:
        inp = self.tokenizer.encode(sentence)
        len_inp = len(inp)

        if len_inp >= self.max_len:
            inputs = inp[:self.max_len]
            masks = [1] * self.max_len
        else:
            inputs = inp + [0] * (self.max_len - len_inp)
            masks = [1] * len_inp + [0] * (self.max_len - len_inp)

        inputs_tensor = torch.tensor([inputs], dtype=torch.long).to(self.device)
        masks_tensor = torch.tensor([masks], dtype=torch.long).to(self.device)
        
        seq_out, pooled_out = self.bert_model(inputs_tensor, masks_tensor)

        if torch.cuda.is_available():    
            return seq_out[0][0].cpu().detach().numpy()
        else:
            return seq_out[0][0].detach().numpy()

if __name__ == '__main__':
    #元データの読み込み
    df_org = pd.read_csv('./drive/NNC/BGMデータ一覧.csv')
    #学習データにある曲のみに絞る
    df_org = df_org.dropna(subset=["一行説明"])
    df_org = df_org[~df_org['一行説明'].str.contains("ジングル")]
    df_org = df_org[~df_org['タグ'].str.contains("ジングル")]
    df_org = df_org.head(5000)
    word = ["ロック", "ポップ", "バラード"]
    df = df_org.iloc[0:0]
    for w in word:
      df_detect = df_org[df_org["一行説明"].str.contains(w)]
      df = pd.concat([df, df_detect])
    df = df.reset_index(drop=True)
    BSV = BertSequenceVectorizer()
    #書誌から特徴ベクトルを計算
    df['text_feature'] = df['一行説明'].progress_apply(lambda x: BSV.vectorize(x))
    #入力テキストから類似ベクトル（BGM）を探る
    nn = NearestNeighbors(metric='cosine')
    nn.fit(df["text_feature"].values.tolist())
    vec = BSV.vectorize("おはよう。今日は良い天気だね。そうだね。一日晴れみたいだよ。")
    ##コサイン類似度を計算
    dists, result = nn.kneighbors([vec], n_neighbors=1)
    print(df["データ名"][r], df["一行説明"][r])

###出力結果
audiostock_45838.wav
Name: データ名, dtype: object 188    
忙しくも楽しいポップ/ロック
Name: 一行説明, dtype: object
```

#５．実験
　それでは想定される会話文を入力として、どのような曲が選ばれるか試してみます。最終的に選ばれるBGMは、学習、評価に用いず、かつ一行説明に"ロック"、"ポップ"、"バラード"という単語が含まれない３００曲をランダムに選びにしました。全体像は図のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590631/235db983-2b43-3ce4-f2b7-70cb41943cff.png)
　最終的に選び出す曲は、予測確率が高いものをNNCが出力するファイル「output_result.csv」から順に流すことにしました（NNCは学習時の評価と最終的な評価で異なるデータを設定できるんですね）。では様々なケースで選曲してみます。

**ケース１）**
**◆会話文：**
おはよう。今日は良い天気だね。そうだね。一日晴れみたいだよ。
**◆類似度が高い一行説明：**
忙しくも楽しいポップ/ロック（audiostock_45838.wav）→ ラベル「ロック」
※説明内に「ポップ」もありますが、ラベルは「ロック」に分類しました
**◆選曲結果：**
・audiostock_44217  ハード&ヘヴィーなスポーツ系オープニング
・audiostock_44540  パワフルなハード・ブギー
・audiostock_46435  激闘マイナー・ファンク・メタル
・audiostock_43382  若さの暴走POP ROCK
・audiostock_44100  躍動感とスピード感の溢れる曲

朝っぽい曲かどうかは置いておいて、「パワフル」、「躍動感」といった元気になりそうな曲を選ぶことができました！エレキギターを使ったメタル風の曲が選ばれる傾向にありそうです。

***ケース２）***
**◆会話文：**
週末は山梨でキャンプの予定。久々に湖畔で静かに過ごせるよ。だいぶ涼しくなってきたから気をつけて。
**◆類似度が高い一行説明：**
ふと遠い日の親の愛情を懐かしく思うポップ（audiostock_45997.wav）→ラベル「ポップ」
**◆選曲結果：**
・audiostock_45254  怪談話に背筋が凍る趣のある純邦楽
・audiostock_44771  恐怖のドキュメント・タッチのBGM
・audiostock_46760  旅情 懐かしい 哀愁 さびしい 黄昏
・audiostock_46657  さわやか 希望 ドライブ 軽快 前進
・audiostock_44331  南国カリブのほのぼのミュージック

１、２曲目は明らかにまずい選択結果になってしまいましたが（怪談…）、４、５曲目は旅にぴったりのポップなBGMが選べています。また、３曲目の曲説明はもの悲しい雰囲気がありますが、タグに「ポップ」が入っているBGMで、実際に聞いてみるとそこまで暗い曲ではありませんでした。このことからもポップ調の曲を自動選択できる傾向はあると言えそうです。

***ケース３）***
**◆会話文：**
あのドラマ、感動モノって聞いたんだけど見た？。切なくて悲しい話。ラストは泣いたよ。
**◆類似度が高い一行説明：**
ウォーミングなバラード、ティーンの気持ち（audiostock_43810.wav）→ラベル「バラード」
**◆選曲結果：**
・audiostock_46013　瑞々しく神秘的でゆったりとした環境
・audiostock_44891　夜の星のリラクゼーション系アンビエント
・audiostock_44575　童話の世界が広がる優しいアンビ風サウンド
・audiostock_45599　ひんやりとした朝の雰囲気の神秘的な環境
・audiostock_45452　庭園に芸術的な気品漂う優美なクラシック

神秘的でゆったりとしたバラード調の曲やクラシックといった静かなBGMをうまく抽出することができています。

　３分類ともBGMの特徴量のみで自動選択しましたが、こちらがほぼ意図した通りの曲を抽出できていそうです！選択しなかった（予測した確率が低かった）BGMを見てみると、「バラエティ番組タイトルBGM」（audiostock_43840）、「ラテン風味のユーロ・ハウス調」（audiostock_42921）、「多国籍 アフリカ 神秘 紀行 おしゃれ」（audiostock_46146）などとなっており、適さないBGMの見分けもついているモデルとなっていることが確認できました。

#６．まとめと考察
　日常会話の内容に併せてBGMを自動選曲するプレーヤーを作る、という課題に対して、

* "ロック", "ポップ", "バラード"という３分類ができるモデルをNNCで構築。精度、適合率ともに80％を超えるモデルを作ることができた
* 自然言語モデルと組み合わせることで、BGMの自動選曲が実現できるスキーマを考案
* 最終的には曲の特徴から自動選曲するので、Audiostockの投稿者がこちらが意図する説明文やタグを付けなくとも、適したBGMを選び出すことができる

ということが実現できました。今回は計１６００曲程度と学習データが小さいものでのモデル作成しかできませんでしたが、アノテーションとデータ数をさらに精査することで、精度の向上がさらに見込めたり、３つ以上の分類クラスも作れたりするはずです。BGMの特徴量の出し方にも研究の余地がさらにありそうです。
　スマートスピーカーを想定したサービス提案でしたが、それに限らずSNSでタグやテキストから曲をつけて投稿したり、動画編集で字幕データからBGMを自動で選曲したりするなど、将来性のある提案になりそうです。

#７．参考文献
* [khcoder](https://khcoder.net/)
* [Kerasを使って音楽をジャンル分けしてみた](https://qiita.com/1plus4/items/e8a102b349b5ae16f97d)
* [BERT で簡単に日本語の文章の特徴ベクトルを取得できるクラス作った](https://kaeru-nantoka.hatenablog.com/entry/2020/05/29/144745)