和音をベクトルにするChord2Vecを作った話 〜 "A7" - "A" + "C" = ? 〜 - 4月 12, 2020 こんにちは、ぐぐりら(@guglilac)です。 今回は、Word2VecならぬChord2Vecを作って遊んでみましたという記事です。 Word2Vecは文章中に登場する単語を、共起関係をもとにベクトルにする手法で、広く使われています。 単語の意味を考慮してベクトルに埋め込むことができます。 Word2Vecを使うと、単語どうしの足し算や引き算がある程度できるようになります。 「王様」 - 「男」 + 「女」 = 「女王」 のような感じです。 Word2Vecについての詳細な説明は他に譲ります。 * [絵で理解するWord2vecの仕組み -Qiita](https://qiita.com/Hironsan/items/11b388575a058dc8a46a) * [word2vec(Skip-Gram Model)の仕組みを恐らく日本一簡潔にまとめてみたつもり](https://www.randpy.tokyo/entry/word2vec_skip_gram_model) 今回は、これを単語ではなく音楽のコードのベクトル表現を得たいと思います。 最終的には、タイトルにもあるように、 ``` "A7" - "A" + "C" = "C7" ``` のような計算ができるようなコードのベクトル表現を得ることが目標です。 ## なぜやったのか この記事を執筆するに至った経緯ですが、 [前回](https://www.smartbowwow.com/2019/12/lightfm.html)、lightFMという協調フィルタリング系のライブラリを用いて、楽曲のコード進行から最適なカポの位置を推薦しました。 その際に、予測の副産物として各コードのベクトル表現が得られたので可視化してみたり、Word2Vecのように足し算や引き算ができるかどうかを試したのですが、うまくいきませんでした。 lightFMはあくまで副産物としてベクトルが得られ、そこでは良い表現を得るということに焦点を当てていなかったので、今回は、そのリベンジです。 ## どうやったのか といっても、gensimというライブラリを使うとWord2Vecは簡単に使えるので、自分でやったことは学習用のコーパスを用意することだけです。 データは前回の記事で取得した約5万曲のコード進行を使います。 表記ゆれの統一などの前処理は前回の記事と同じです。 自然言語でのgensimの使用では、文章を分かち書きしてコーパスを作るといった工程が必要ですが、今回はすでにコードの列となっているので分かち書きする必要もなく、簡単です。 コーパスの中身はこんな感じ. ```python [ ['G#m', 'D#7', 'Emaj7', 'Bmaj7', 'C#m', 'D#7', 'G#m', 'C#m', 'G#m', 'B', 'D#m', 'E', 'B', 'D#m', 'C#m', 'E', 'G#m', 'C#m', 'G#m', 'C#m', 'E', 'D#7', 'C#m', 'G#m', 'E', 'D#m', 'G#m', 'G#m', 'C#m', 'G#m', 'B', 'D#m', 'E', 'B', 'D#m', 'C#m', 'E', 'G#m', 'C#m', 'G#m', 'C#m', 'E', 'D#7', 'C#m', 'G#m', 'E', 'D#m', 'G#m', 'B', 'E', 'F#7', 'B', 'E', 'F#7', 'B', 'G#m', 'C#m', 'G#m', 'B', 'D#m', 'E', 'B', 'D#m', 'C#m', 'E', 'G#m', 'C#m', 'G#m', 'C#m', 'E', 'D#7', 'C#m', 'G#m', 'E', 'D#m', 'G#m', 'C#m', 'G#m', 'C#m', 'E', 'D#7', 'C#m', 'G#m', 'E', 'D#m', 'G#m', 'G#m', 'D#7', 'Emaj7', 'Bmaj7', 'C#m', 'D#7', 'G#m'], ['C#m', 'E', 'E', 'D#m', 'G#m', 'C#m', 'C#m', 'B', 'B9', 'E', 'E', 'D#m', 'G#m', 'C#m', 'C#m', 'B', 'E', 'B', 'A', 'B', 'E', 'B', 'A', 'B', 'E', 'F#', 'G#m', 'F#', 'B', 'E', 'F#', 'C#m', 'C#m', 'D#', 'E', 'D#m', 'C#m', 'D#m', 'E', 'D#m', 'D#', 'C#m', 'E', 'E', 'E', 'D#m', 'G#m', 'C#m', 'C#m', 'B', 'E', 'E', 'D#m', 'G#m', 'C#m', 'C#m', 'B', 'E', 'B', 'A', 'B', 'E', 'B', 'A', 'B', 'E', 'F#', 'G#m', 'F#', 'B', 'E', 'F#', 'C#m', 'C#m', 'D#', 'E', 'D#m', 'C#m', 'D#m', 'E', 'D#m', 'D#', 'C#m', 'E', 'E', 'E', 'D#m', 'G#m', 'C#m', 'C#m', 'B', 'E', 'E', 'D#m', 'G#m', 'C#m', 'C#m', 'B', 'E', 'E', 'D#m', 'G#m', 'C#m', 'C#m', 'B', 'E', 'E', 'D#m', 'G#m', 'C#m', 'C#m', 'E', 'B', 'E', 'B', 'E'], .... ] ``` これをgensimのword2vecに入力します。 ```python from gensim.models import word2vec embedding_dim=100 min_count=5 window_size=5 iter_num=500 model = word2vec.Word2Vec(corpus, size=embedding_dim, min_count=min_count, window=window_size, iter=iter_num) ``` `size`は特徴量の数、`min_count`は無視する単語の頻度の閾値、 `window_size`は前後window幅、`iter_num`分繰り返し計算します。 これで訓練できました。試しに ```python model.wv["C"] ``` とすると、Cコードのベクトルが取得できます。 ## 実験結果 まずは、Cコードに似ているコードを取得しましょう。 ```python model.wv.most_similar(positive=["C"]) ``` とやると、Cコードのベクトルとのコサイン類似度が大きいベクトルを出力してくれます。 結果は ```python [('C/B', 0.6145074367523193), ('Cadd9', 0.596329927444458), ('Am', 0.5841001272201538), ('Em', 0.5790020227432251), ('G/B', 0.5402282476425171), ('G', 0.4994836449623108), ('C/F', 0.48254910111427307), ('F', 0.47810545563697815), ('G/C', 0.4444805085659027), ('Cmaj9', 0.43892884254455566)] ``` Cコードのルートだけ変わった分数コードや、Cキーの曲でよく出てくるコードが並んでいます。学習できていそうです。 前回もやった、ベクトルをPCAにかけて二次元に落として可視化したものがこちら。 前回は内積によってベクトルを学習していたので、何度か回すと共起するベクトルなのに遠いところに配置されるケースも見られたのですが、今回は何度か回しても、似たような位置に来てくれました。 やはりベクトルを得る目的ならこちらの方が良さそうです。 では最後、今回の目標だったコードの足し算引き算をやってみます。 ```python # A7 - A + C = ? model.wv.most_similar(positive=["A7","C"],negative=["A"]) ``` 結果は...!! ```python [('C7', 0.48901692032814026), ('C/E', 0.44288498163223267), ('Am7', 0.4260050058364868), ('G7', 0.4138847589492798), ('Gm7/C', 0.4089568257331848), ('C/F', 0.40648773312568665), ('Dm7/G', 0.40488165616989136), ('Cadd9', 0.40399545431137085), ('G7sus4', 0.3991324305534363), ('C7sus4', 0.39309656620025635)] ``` やったね!C7が一番上にきています。 他のもやってみます。sus4とかもできるかな? ```python # Asus4 - A + C = ? model.wv.most_similar(positive=["Asus4","C"],negative=["A"]) ``` 結果 ```python [('Csus4', 0.5615717172622681), ('Gsus4', 0.48594969511032104), ('Am', 0.4580533504486084), ('C/B', 0.436903715133667), ('G/B', 0.4276440441608429), ('F', 0.4121154248714447), ('C/A#', 0.4093543291091919), ('Em', 0.4037439823150635), ('Am/G', 0.3950252830982208), ('Fadd9', 0.3900836110115051)] ``` できてますね! sus4とかセブンスコードとかじゃなくて、何度上みたいな関係も学習してくれているのか?と思ったので ```python # C - F = ? - G (Dになってほしい) model.wv.most_similar(positive=["C","G"],negative=["F"]) ``` とやってみると ```python ('Em', 0.715512216091156), ('Cadd9', 0.6208784580230713), ('D', 0.5858040452003479), ('D/G', 0.5396372079849243), ('Bm', 0.5011054277420044), ('G/B', 0.4714002013206482), ('C/B', 0.4668537676334381), ('Cmaj9', 0.42411768436431885), ('D/C', 0.41762834787368774), ('Em/D', 0.4074915945529938)] ``` Fから見たCはGから見たDなので、Dと出て欲しいですが、一番上には来てないですね。 ただ、上位10個の中にD, D/G, D/Cなどが入っているので、悪くはなさそう? 似た種類のコードが複数あるために登場回数が分散してしまい、一番上にこなかったのかもしれません。 分数コードも表記ゆれとして統一すると、Dが一番上でもおかしくなさそうですね。 ## 今後 せっかくコードのベクトルが得られたので、これを入力に用いて何かしらの予測をしたいです。 実はさくっと適当にlightGBMに得られたベクトルの平均を入れたらBOWよりも精度が下がってしまったので、モデルの方での使い方も工夫の余地がありそうです。(そもそもこういうのlightGBMにいれて良いものなのかも怪しい) Deepとかだとembedding layerの初期値に使ったりできそう。そのあたりができたらまた記事にしたいです。 ## おわりに gensimに入れるだけで簡単にWord2Vecを試せるのはいいですね。 結果も思ったのと近く、満足です。 言語に限らず系列データはいろいろ考えられるので、気になったらすぐ試せるライブラリがあるのはありがたいです。 今後の展望の内容を書いた続編もご期待ください!(未来の自分がんばれ) ありがとうございました。 この記事をシェアする Twitter Facebook Google+ B!はてブ Pocket Feedly コメント
コメント
コメントを投稿