見出し画像

会話の内容に合わせてBGMを自動選曲するエンジン作ってみた【Ledge.ai賞受賞しました】

初めまして。8月までICTRADメンバーだった山崎啓介です。コロナ禍の在宅勤務拡大でできた通勤分の時間を有効活用すべく、AIコンサルなどを行うレッジが主催するAI開発コンテスト「Neural Network Console Challenge sponsored by Audiostock(NNC-Challenge)」に挑戦したところ、Ledge.ai賞を受賞しました!

ここでは、応募した作品について紹介したいと思います。

1.はじめに

Sonyとレッジが企画する第2弾となるAI開発コンテスト「Neural Network Console Challenge」に挑戦。Audiostockの音声(BGM)データや書誌(曲の説明文)を解析して、自由課題「日常会話の内容にあわせてBGMを自動選曲するプレーヤーを作る」に取り組みました。

例えば、Google Homeなどのスマートスピーカーが、部屋にいる人の会話内容にあわせてBGMを自動再生していくシステムを考えます(プライバシーの観点から実用化のハードルは高そうですが…)。

2.実験環境&データ

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

山崎さん図

3.BGMの自動分類

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

3-1.アノテーションと学習データ
どのようなクラスを作ることが望ましいか調査するために、まず、テキストデータを統計的に分析できる「KHcoder」を使って、上記の「一行説明」に含まれる単語を調査。上位の結果は以下のようになりました。

画像2

これらの中から、実際にBGMを聞いてみつつ分類できそうな(テンポや音色などが違う)"ロック", "ポップ", "バラード"のいずれかが含まれる曲を学習データにすることに。学習データ1468件、評価データ105件を作成しました。また、作成にあたり効果音のような音源(ジングル)は、曲の長さが短いので対象外としました。

3-2.メル周波数ケプストラム係数に変換
BGMのWAVデータをメル周波数ケプストラム係数に変換し、40次元のベクトルに落とし込んでいきます(詳しくは割愛しますが、このページに詳しく書いてありました)。縦軸の音の高さ毎に平均を取り(1,40)の配列にして、学習用データとしました。

import pandas as pd
import numpy as np
import librosa

y, sr = librosa.load(file_name)
#40次元で特徴量抽出
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")

3-3.NNCで分類モデルを作る
NNCでベクトルを分類するモデルを学習させました。CNNでの解法が一般的なようですが、様々なネットワークや活性化関数を試してみたところ、結局以下の設定が最も精度が高いという結果に。この辺は時間があれば、さらに実験を繰り返したいところです。

ちなみにNNCの利点でいうと、関数を変えるなどの試行錯誤できるGUIが整備されているのですごく楽です。どのようなネットワークになっているかも直感的に分かりますし、Google Colabなどと比較した場合の魅力の一つだと思います。

画像3

低次元のベクトルを学習させたので、今回はCPU(Windows版)で十分な処理量でしたが、ほぼ同設定で学習したクラウド版での学習結果も、一応Publishing予定。30エポック学習させ、学習曲線は以下のようになりました(Best Validationは9エポック目でした)。

画像4

次に、作成したモデルを用い、テストデータで評価し、精度をはかってみます。

画像5

画像6

3分類問題とはいえ、Accuracyは0.8とある程度の特徴はとれていそうです。平均適合率も約8割以上となり、適したBGMを選曲するという課題に対しては価値のあるモデルとなっていそうです。

4.日常会話を分析する

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

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

5.実験

それでは想定される会話文を入力として、どのような曲が選ばれるか試してみます。最終的に選ばれるBGMは、学習、評価に用いず、かつ一行説明に"ロック"、"ポップ"、"バラード"という単語が含まれない300曲をランダムに選ぶようにしました。全体像は図のようになります。

画像7

最終的に選び出す曲は、予測確率が高いものをNNCが出力するファイル「output_result.csv」から順に流すことにしました(NNCは学習時の評価と最終的な評価で異なるデータを設定できるんですね)。では様々なケースで選曲してみます。

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

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

ケース2)
◆会話文:

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

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

ケース3)
◆会話文:

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

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

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

6.まとめと考察

日常会話の内容に合わせてBGMを自動選曲するプレーヤーを作る、という課題に対して、

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

ということが実現できました。今回は計1600曲程度と学習データが小さいものでのモデル作成しかできませんでしたが、アノテーションとデータ数をさらに精査することで、精度の向上がさらに見込めたり、3つ以上の分類クラスも作れたりするはずです。BGMの特徴量の出し方にも研究の余地がさらにありそうです。

スマートスピーカーを想定したサービス提案でしたが、それに限らずSNSでタグやテキストから曲をつけて投稿したり、動画編集で字幕データからBGMを自動で選曲したりするなど、将来性のある提案になりそうです。

7.参考文献

khcoder
Kerasを使って音楽をジャンル分けしてみた
BERT で簡単に日本語の文章の特徴ベクトルを取得できるクラス作った

(山崎啓介)