自然言語処理の手法を用いてコード進行から作曲者を推論した

# 前置き　
　初投稿となります。普段は理系大学院生で原子力関連の研究室に所属しており、現在は深層学習を用いた研究を行っています。また研究の影響で深層学習に興味を持ち、趣味で自然言語処理の勉強をしております。

　ライブ観戦が趣味で先日も『[RAISE A SUILEN](https://bushiroad-music.com/artists/raise-a-suilen)』の現地ライブに参加し、今度は『[Roselia](https://bushiroad-music.com/artists/roselia)』のライブにも参戦予定です。この記事は『Roselia』の曲を聞いていた際に、音楽のコード進行が自然言語処理に通じるものがあると気づき、実装したものとなります。

# 概要
　『Roselia』曲からいくつか選び、コード進行をword2vecで学習し曲をベクトル化。その後コサイン類似度によって曲同士の類似度を計算し、作曲者の推論をした。

# コード進行とは
　**コード進行とは和音（chord)の流れ**です。有名なものはカノン進行とか王道進行とかです。正確な説明は私にはできないので[SoundQuest](https://soundquest.jp/about/)をご参考ください。

さてこのコード進行ですが、ある程度秩序立ったものとなります。例えば
```
Dm -> G7 -> C -> A7
```
のような進行は非常に多くの曲に見られ、とても安定感がある進行です。一方
```
A7 -> G7 -> Dm -> C
```
のような進行はあまり見られないし、おそらく違和感を抱く方もいらっしゃると思います。このように登場するコードは同じですが、前後関係によって印象が全く変わるのがコード進行の特徴となります。

　今回とり上げる『Roselia』曲は、
```
Am -> F -> C -> G
```
```
Am -> Em -> F -> G -> C
```
の二つの進行が多い印象です。前者は『[Neo-Aspect](https://www.youtube.com/watch?v=cbRDLXki6Wo)』や『[Song I am.](https://www.youtube.com/watch?v=Hrwm_Pkdgkg)』など[藤永龍太郎(Elements Garden)](https://twitter.com/RyutaroFujinaga)さんが作曲した曲、後者は『[FIRE BIRD](https://www.youtube.com/watch?v=Qf87EzO2lxw)』や『[ROZEN HORIZON](https://www.youtube.com/watch?v=6NE_VeL0ouI)』など[上松範康(Elements Garden)](https://twitter.com/agematsu)さんが作曲した曲によく見られます。

# word2vecとは
　自然言語処理で用いられる手法の一つで、文章から単語のベクトル表現を獲得するものです。単語をベクトル表現することで各種演算ができるようです。代表的なものだと、
```
女　＋　王国　- 男　＝　王女
```
が例でよく見られます。word2vec自体は以下の記事がとても分かりやすいです。
- [Word2Vecを理解する](https://qiita.com/g-k/items/69afa87c73654af49d36)
- [絵で理解するWord2vecの仕組み](https://qiita.com/Hironsan/items/11b388575a058dc8a46a)

　本記事ではコード進行を「文章」、コードを「単語」とみなし、ベクトルを獲得することを試みます。前章で述べたようにC/Amキーでは例えば「G」コードの後には「C」コードが来ることが多く、なんとなく言語と似た構造が見られます。また作曲者によって使われるコード、コード進行に特徴が見られるのも、話し方や使う単語に各人の特徴が現れるのと同様に思われます。

　ここで実際にword2vecでコードのベクトル表現を獲得してみましょう。以下では『Roselia』の人気曲『ROZEN_HORIZON』のコード進行を[gensim](https://radimrehurek.com/gensim/models/word2vec.html)のword2vecで処理しました。
```python3
from bs4 import BeautifulSoup
from urllib.request import urlopen
import gensim

html = urlopen("https://gakufu.gakki.me/m/index2.php?p=N15970&k=p5#r")
bs = BeautifulSoup(html, "html.parser")

chord = bs.find_all("span",class_="cd_fontpos")
chord = [c.text for c in chord if c.text != "N.C."]
# print(chord)  # ['Am', 'G', 'Am', 'FM7', 'G', 'Am', ...]

num_features = 10
sentences = [c.split() for c in chord]
model = gensim.models.word2vec.Word2Vec(sentences, vector_size=num_features, min_count=1, window=1)
for key, value in model.wv.most_similar('C'):
    print("chord : {}, score : {}".format(key, value))
```

```
chord : C＃dim, score : 0.5916882753372192
chord : Gsus4, score : 0.5115842819213867
chord : Am7, score : 0.4619681239128113
chord : A7, score : 0.38148820400238037
chord : Fadd9, score : 0.30411967635154724
chord : B, score : 0.2914133369922638
chord : EonG＃, score : 0.27556225657463074
chord : FM7, score : 0.24953822791576385
chord : Em7, score : 0.22384946048259735
chord : Em7onB, score : 0.17258583009243011
```
　まず曲のコード進行から`gensim.models.word2vec.Word2Vec`で10次元ベクトルを返すモデルを学習します 。今回C/Amキーに移調させたデータで学習しています。その後「C」コードと類似するコードとそのコサイン類似度を表示しました。

　解釈が難しいですが、音楽理論としては「C#dim」コードや「Am7」コードがこの中では「C」コードと類似性が高いと私は認識しており、それらが上位に来ていることからword2vecによってコード進行を分析することは有効であると示唆されました。

# 当記事で行ったこと
　上述の通りコード進行やコードにはある程度作曲者の特徴が現れます（少なくとも私はそう考えております）。それゆえword2vecで獲得した曲のベクトル表現も、同一作曲家のもの同士は類似度が高く出ると考えました。以下では『Roselia』曲を多く手掛ける[藤永龍太郎(Elements Garden)](https://twitter.com/RyutaroFujinaga)、[上松範康(Elements Garden)](https://twitter.com/agematsu)の二人の作曲者に注目してこれを検証します。

　具体的な手法は以下の通りです。
1. 作曲者問わず『Roselia』曲についてコード進行をgensimで学習させ、コードのベクトル表現を返す汎用的なモデルを獲得する。ただし後述の検証で用いる曲は学習データに含めない。
2. 検証用の曲について、二人が作曲した曲と類似度を計算し比較する。


# 補足
　自然言語処理の手法をコード進行に応用した例は多くの記事で挙げられています。当記事も以下の記事を参考に致しました。
- [python実験-コード進行を自然言語処理してみた-](https://ameblo.jp/lesser-papakichi/entry-12697778613.html)
- [ディープラーニングによるコード進行の予測](https://qiita.com/tanikawa/items/8f1a5b3a33f24ed0a984)

ただし、作曲者に注目したものは私が調べた限り見つかりませんでした。そこで記事にまとめ共有しようと思った次第です。

# 実装
　以下実装パートになります。コード自体は[GitHub](https://github.com/bokuranosenjou/roselia_chord)に公開してますので、ぜひ興味を持たれた際はご自身でもお試しください。以下のコマンドで実行できるかと思います。

```
$ pip3 install -r requirements.txt
$ python3 scraping.py
$ python3 chord.py
```

### 環境
- macOS Ventura ver13.3.1
- Python 3.8.13
- スクレイピングにbs4、ベクトル表現の作成にgensimを使用
- その他必要なライブラリはrequirements参考

### スクレイピング
　[楽器.me](https://gakufu.gakki.me/)からデータを拝借致しました。当サイトは国内最大級の歌詞コードサイトとなっており、実に70000を超える曲データが公開されています。

　スクレイピングには[BeautifulSoup](https://www.crummy.com/software/BeautifulSoup/bs4/doc/#)を使用しました。以下実装例となります。

```python3:scraping.py(一部抜粋)
from urllib.request import urlopen
from bs4 import BeautifulSoup

html = urlopen("hoge")
bs = BeautifulSoup(html, "html.parser")

chord = bs.find_all("span", class_="cd_fontpos")
chord = [c.text for c in chord]
print(chord)  # ['Am', 'F', 'C', 'G']
```
　実際は`roselia_dict.py`に曲とリンク先のhtmlを登録してあり、`chord.py`でそれらをスクレイピングし、テキストに書き出しています。

### 学習
　学習データには転調がない曲のうち、キーをC/Amに移調したものを用いました。これはCメジャーキーとDメジャーキーでは同じ「C」コードであっても、前後に現れることの多いコードが異なるためです。また「N.C.（ノンコード）」は除きました。
```python
chord = [c.text for c in chord if c.text != "N.C."]
```
　上記の条件を満たす曲は[14曲](https://github.com/bokuranosenjou/roselia_chord/blob/main/roselia_songs.py)あり、そのうち学習には『[Neo-Aspect](https://www.youtube.com/watch?v=cbRDLXki6Wo)』および『[ROZEN HORIZON](https://www.youtube.com/watch?v=6NE_VeL0ouI)』を除いた12曲を用いました。この二曲については検証用の曲となります。

　学習については前述した方法と同様に10次元ベクトルを返すようにgensimで学習しました。
```python
import gensim
num_features = 10
model = gensim.models.word2vec.Word2Vec(sentences, vector_size=num_features, min_count=1, window=1)
```

### 評価
　まず曲の全体のベクトル表現を以下の関数で求めます。[Word2Vecを使った文章間の類似度算出（簡易版）](https://qiita.com/yoppe/items/512c7c072d08c64afa7e)を参考にしました。
```python
def avg_feature_vector(chord, model, num_features=num_features):
    feature_vec = np.zeros((num_features,), dtype="float32") # 特徴ベクトルの入れ物を初期化
    for ichord in chord:
        feature_vec = np.add(feature_vec, model.wv[ichord])
    if len(chord) > 0:
        feature_vec = np.divide(feature_vec, len(chord))
    return feature_vec
```
`chord`には各曲のコード進行、`model`には先ほど学習したモデルが入ります。また`num_features`は前述の通り10です。

　次に以下の10曲について上記の関数でベクトルを計算しました。
- 軌跡
- 陽だまりロードナイト
- Determination Symphony
- Re:birth day
- Neo-Aspect
- BLACK SHOUT
- 熱色スターマイン
- ONENESS
- R
- ROZEN HORIZON
この内上5曲は藤永龍太郎作曲、下5曲は上松範康作曲です。また前述の通り検証用の曲は学習データに含めていません。

　全体は省略しますが、以下のような関数で比較します。
```python
song_dict = {}  # 曲とそれに対応するベクトルを管理
song_composer_pair = {} # 曲とその作曲者を管理

for composer, dict_ in roselia_dict.items():
    for song, _ in dict_.items():
        # 実装では”/fujinaga/Neo-Aspect.txt”のようなファイルを生成しています。
        PATH = "./{}/{}.txt".format(composer, song) 

        with open(PATH, mode="r") as f:
            chord = [s.rstrip() for s in f.readlines()]
            chord = [c for c in chord if c in model.wv]

        song_dict[song] = avg_feature_vector(chord, model)
        song_composer_pair[song] = composer

def validate_target(targets):
    for target in targets:
        print("=============================================")
        print("target :: {} (composer : {})".format(target, song_composer_pair[target]))
        scores = {
            "fujinaga" : [],
            "agematsu" : [],
        }
        results = []
        for song, vector in song_dict.items():
            if song == target : 
                continue
            results.append([1 - spatial.distance.cosine(vector, song_dict[target]), song])
        results.sort(reverse=True)
        for val, song in results:
            if song_composer_pair[song] not in scores : continue
            print("song : {}, composer : {}, score : {}".format(song, song_composer_pair[song], val))
                scores[song_composer_pair[song]].append(val)
            
        print()
        for key, value in scores.items():
            print("Average_score", key, np.mean(np.array(value))
```
　ベクトル同士の比較にはコサイン類似度を用います。実装は`scipy.spatial`にベクトル間の距離を求める関数がありますので、1から距離を引いて類似度を求めます。

### 結果と考察
　前節の関数で二つの検証曲について他曲との類似度をそれぞれ求めました。
```python
targets = ["Neo-Aspect", "ROZEN_HORIZON"]
validate_target(targets)
```

　結果は以下の通りです。まず藤永作の『Neo-Aspect』について、以下の出力が得られました。
```
=============================================
target :: Neo-Aspect (composer : fujinaga)
song : Hidamari_Rhodonite, composer : fujinaga, score : 0.9963401556015015
song : Determination_Symphony, composer : fujinaga, score : 0.9852861166000366
song : ONENESS, composer : agematsu, score : 0.978401780128479
song : Kiseki, composer : fujinaga, score : 0.9446823596954346
song : Re_birth_day, composer : fujinaga, score : 0.9224499464035034
song : ROZEN_HORIZON, composer : agematsu, score : 0.9209639430046082
song : BLACK_SHOUT, composer : agematsu, score : 0.868392825126648
song : Nesshoku_Starmine, composer : agematsu, score : 0.8675056099891663
song : R, composer : agematsu, score : 0.8477595448493958

Average_score fujinaga 0.962189644575119
Average_score agematsu 0.8966047406196594
```
　情報が多いですね。まず比較対象となった他9曲のうち、藤永作の曲が1,2,4,5番目に位置しています。上松作の『ONENESS』が3番目に位置しているのが面白いですね。この二つの曲が類似している印象はあまりなかったです。藤永さんはとても多彩な曲を作られる印象があるので、他作曲者の曲にも類似しているとgensimは推論したのでしょうか。

　またそれぞれの作曲家についての類似度の平均に関しては藤永さん（正解）が高く出ました。word2vecによって上手く作曲者の推論ができているようです。

　続いて上松作の『ROZEN HORIZON』です。

```
=============================================
target :: ROZEN_HORIZON (composer : agematsu)
song : ONENESS, composer : agematsu, score : 0.9605984091758728
song : BLACK_SHOUT, composer : agematsu, score : 0.9590064287185669
song : Nesshoku_Starmine, composer : agematsu, score : 0.9522596597671509
song : R, composer : agematsu, score : 0.9327542185783386
song : Determination_Symphony, composer : fujinaga, score : 0.9280744194984436
song : Neo-Aspect, composer : fujinaga, score : 0.9209639430046082
song : Hidamari_Rhodonite, composer : fujinaga, score : 0.9129042625427246
song : Kiseki, composer : fujinaga, score : 0.8692155480384827
song : Re_birth_day, composer : fujinaga, score : 0.8644245266914368

Average_score fujinaga 0.8991165399551392
Average_score agematsu 0.9511546790599823
```
　こちらは上位に全て上松作の曲が位置しています。上松さんは『BanG Dream!』シリーズ以外でも人気の曲が多く、所々に「上松節」がある気がします。それをword2vecは上手く捉えられたのでしょうか。一方スコア自体は『Neo-Aspect』ほどは高くありませんね。個人的に『FIRE BIRD』と『ROZEN HORIZON』が結構似ていると思っており、そちらのデータが得られた後にまた検証してみたいです。

　なお平均スコア自体は上松さん（正解）が高く出ました。こちらも作曲者の推論ができているようです。

# 結論
- word2vecでコード進行を学習し、曲のベクトル表現を得た。
- 同じ作曲者の曲同士はベクトルのコサイン類似度は高くなり、word2vecが作曲者の推論に有効である可能性が示唆された。

# 今後の展望
　まず学習データを増やしたいと考えています。『Roselia』以外の『BanG Dream!』シリーズの曲を入れた場合に同じように作曲者を推論できるかが気になります。また今回転調する曲は省いていますがそれらも学習データに使う方法を考えたいです。その他高度な言語モデルを用いた場合に同じような結果が得られるか検証したいです。

# 後書き
　初投稿で拙い記事ですが、ここまで閲覧くださりありがとうございます。私の趣味全開の記事となってしまいましたが、少しでも『Roselia』、音楽理論、自然言語処理等にご興味を持っていただければ幸いです。