LightFMを使ってギターのおすすめカポ位置推薦してみた - 12月 11, 2019 こんにちは、ぐぐりら(@guglilac)です。 今日は最近やっていた自由研究の記事です。 ## 概要 ギターを弾く際に、押さえるのが難しいコードがありますが、 カポタスト(カポ、と呼んだりします)をギターに装着することで難しいコードが出現するのを避け、演奏しやすくしたりします。 今回はこのカポの装着する位置をLightFMというライブラリを用いて推薦するプログラムを書いてみました。 ## ギターのカポとは ギターのコードには押さえるのが比較的難しいコードがあるので、カポをつけてキーを変えることでそのコードが出ないようにします。 例えば、Fコードを鳴らさなければいけない曲を弾きたいが、Fコードがどうしても押さえられない、という時にカポを1フレット目につけると、Eコードの手の形(Fコードよりは簡単)で押さえて弾くと音としてはFコードが鳴る、という優れものです。 このようにして、カポをつけると出てくるコードが変わるので、つける位置をうまく選べば自分の苦手なコードを回避することができます。(もちろんちゃんと練習しないとダメですが!ある程度簡単なコードを選べるということです。) (参考)カポはこんな感じで、ギターに挟んで使います。 カポをつける以外に、ギターの弦の張り方を緩める方法(半音下げチューニング)も、カポはつけていませんが同様の効果が得られるので、今回はこれもカポの位置推薦に含めて考えていきます(-1フレットにつけていると解釈できる) ## 背景 便利なWebサービスに、ギターコード/ウクレレ/ピアノ/バンドスコア見放題 U-フレットというサービスがあり、曲ごとにコード進行が載っているという素晴らしいサイトがあります。コード進行だけではなくオススメのカポの位置も表示されます。 自分もギターを弾くときに参考にさせていただいているのですが、少し気になった点がありました。 オススメのカポの位置が表示されるのですが、それは一つしか表示されません。 一番オススメのカポの位置のみ表示される、という仕様です。 これでも十分ありがたいのですが、たまに不便な時があって * オススメされたカポ位置がしっくりこない時に次の候補が欲しい * レギュラーチューニングしている時に半音下げを推薦されると面倒なのでカポはめる方法で代用したい なんてことがありました。 なので、カポ位置を一つだけ推薦するのではなく、ランキング形式にして複数のカポ位置を推薦するものを作りたい、というモチベが湧きました。 オススメされたものじゃなくても十分弾ける、というのが経験上割とあったので、これで1番目に推薦されたカポ位置がダメでも2番目を見てみよう、ということができるわけです! ## 手法 というわけで、カポ位置を一つ推薦するという問題なら他クラスの分類問題として解けてしまうわけですが、今回はランキングを学習したいので、他の手法を用います。 今回の問題設定は 「各曲に出現したコードからカポ位置をオススメ順に並べる」ので * コードのonehot ベクトルは割とスパース? * ランキング学習に対応したloss * 得られるラベルはrank=1のカポ位置のみ という特性を持つので、Factorization modelが適していると考えました。 また、下二つの項目から、BPRやWARPといったlossがこの条件を満たしていると考えたので、これらを組み合わせたら解けるのでは、と考えました。 Factorization かつBPRやWARPといったlossをサポートしている、そんなpython ライブラリがあればな〜と思って調べると、なんとLightFMというのがありました。 厳密にはFactorization Machinesではないですが、今回の問題設定には適していると感じたのでこれを使用します。 LightFMについてはこちらに書いたので、使ってみよう!と思った方は参考にしていただければと思います。 LightFMをMovielensに適用してみた - Qiita ## データセットと前処理 データセットは、先ほど紹介したギターコード/ウクレレ/ピアノ/バンドスコア見放題 U-フレットのデータを使用します。 全部で5万曲弱取得し、train testで分けました。 取得したコードは全て原キーでのコードにしており、♭と♯の統一、分数コードの表記の統一など表記ゆれを解消するなどの前処理をしました。 前処理後のコードの種類は863種類、各コードの出現頻度は以下のようになっていました。 出現頻度が少ないコードはノイズになりそうなので、学習時にBag of wordsでベクトルを作る時に出現回数の閾値を定めておき、回数が少ないコードはベクトルに算入しないようにしました。 ラベルの方ですが、オススメのカポ位置を見てみると、こうなっています(train) ``` capo0: 15987 capo1: 5044 capo2: 6411 capo3: 5174 capo4: 3845 capo5: 2219 capo6: 2 capo7: 3 half_down: 1224 ``` capo6,7は珍しすぎるので、今回は削除して、残りのカポ位置についてのみ考えることとします。 ## 比較手法と評価指標 * WARP * BPR * Logistic * Baseline WARPとBPR, logisticはLightFMでサポートされているものを用いています。 WARPとBPRはランキング用のlossで、logisticは最適なカポ位置のラベルを正例にとった分類問題用のloss(なはず)です。 WARPとBPRについては論文を読んで記事を書いたのでこちらも参考にしてみてください。 * 【論文紹介】BPR: Bayesian Personalized Ranking from Implicit Feedback (UAI 2009) * 【論文紹介】WSABIE: Scaling Up To Large Vocabulary Image Annotation (IJCAI 2011) Baselineはどのデータが来てもtrainのラベルの割合順にオススメするモデルです。比較用に作りました。 評価はPrecision@3とAUCで行いました。 またembedding_dimは8, bowの閾値は10000で固定して実験しています。 ## 実験結果 ``` (WARP) Precision@3: train 0.33, test 0.23. AUC: train 0.99, test 0.71. (BPR) Precision@3: train 0.33, test 0.16. AUC: train 1.00, test 0.53. (Logistic) Precision@3: train 0.13, test 0.13. AUC: train 0.46, test 0.46. (Baseline) Precision@3: train 0.17, test 0.18. AUC: train 0.55, test 0.55. ``` WARP lossが強いですね。 PrecisionもAUCも他の3つの手法に勝利しています。 ## embeddingの可視化 学習後、カポ位置やコードのembeddingが得られるので、せっかくなので可視化してみます。 embeddingの次元は8にしたので、可視化する際はPCAを用いて2次元に落としています。 C,D,G,Am,EmはGメジャーのダイアトニックコードで、一緒に出現することが多いのですが、図を見ると確かに近くに来ていますね。 同じくA, B, E, C#m,F#mも同様の関係なので、近くに来ています(ちょっとマイナー二つが離れていますが...) 大体の傾向はembeddingとして学習されていそうですね。 word2vecならぬ、chord2vecとでもいいますか、こんな感じのembeddingが得られていました。 最後はおまけ、これは上手くいかなかったのですが、word2vecで有名な「王様 - 男 + 女 = 女王」的なあれをやってみました。 ```python e7=np.array(embeddings["chord"]["C7"]) -np.array(embeddings["chord"]["C"])+ np.array(embeddings["chord"]["E"]) search_similar_embeddings(e7 , embeddings["chord"],10) ``` (search_similar_embeddingsは指定したembeddingとのcos類似度が大きいものを持ってくる自作関数です) 結果 ``` [("E", 0.898935571123925), ("B", 0.8634312971836976), ("Dm", 0.8483122550985608), ("Asus4", 0.8378934594963497), ("Cm7", 0.835664139007935), ("A", 0.8162676974116992), ("C#maj7", 0.7143456388015048), ("Gm7", 0.6423920037019785), ("G#m", 0.6037394770467525), ("G#maj7", 0.5852001749627571)] ``` E7コードが出てきてほしいところでしたが、ダメでした。 今回はembeddingを得るのが本筋ではないため、仕方ないですね。 上手くいかなかった理由としては * BOWベクトルなのでコードの出現順序を考慮していない * 前後だけでなく曲全体で共起しているかをみている(転調とかが悪影響を及ぼす?) * LightFMではchord同士の内積は取られないのでcapoを通してのみchord間の関係を学習している あたりが考えられます。 やはり、分散表現を得たいならまた違った方法が必要なのだろうと思います。 ## おわりに 最後まで読んでいただきありがとうございます。 データセット収集から始まり、使えそうな手法のサーベイ、論文読み、実装、評価、可視化とオールインワンなタスク(ただの自由研究ですが)だったので色々と勉強になりました。 研究自体よりも、その手法で何ができるかを考えるのが楽しいな、と感じるので、こういうのは定期的にやれたらいいですね。 この記事をシェアする Twitter Facebook Google+ B!はてブ Pocket Feedly コメント
コメント
コメントを投稿