モダンなメタルバンド名を自動生成したい

# はじめに
皆さんは「自分たちのオリジナルバンドの名前が決まらない...」という経験をしたことはないでしょうか？誰しもが一度は悩んだことのある経験だと思います。私も現在、バンドのリーダーに「次の練習までに一人2案は考えてきて」という宿題を課されて絶賛悩んでいる最中です。

最近のアーティストのバンド名を見ていると非常にユニークで複雑な名前が増えてきているように感じます。自分たちのオリジナルバンドを組むに当たって、バンド名を考えることは非常に重要なイベントです。バンド数の増加により、バンド名の多様化が進んでいる昨今の音楽業界において、オリジナリティのある「いいバンド名」を決めるのに苦労している方も多いのではないでしょうか？

そこで今回は上記のような課題を解決するべく、簡単な自然言語処理を使って自動で生成するというアプローチで本記事の取り組みを始めました(結局自然言語処理の技術はあまり使いませんでしたが)。

今回作成したスクリプトおよび、収集したメタルバンド名のリストは[こちら](https://github.com/Pu-of-Parari/metal_bandname_generator)。

# 作ったもの
## 概要(エレベーターピッチ)

- [自分たちのバンドの名前を決め]たい
- [最近オリジナルバンドを結成したバンドマン]向けの
- [バンド名ジェネレータ]というツールは、
- [pythonのスクリプト]です。
- これは、[指定した条件からオリジナルのバンド名を自動生成すること]ができ、
- [自分たちで云々言いながら考えること]とは違って、
- [既存のバンドの情報を基にそれっぽいバンド名を自動で複数生成する機能]が備わっています。

## 生成されたバンド名
何度か自動生成して、個人的にいいと思ったものをピックアップしました。

```
Bring Funeral Against You
Devil Brings Sacred
Burden Minus Trooper Hollow
Brat Murphy Thrice Hearts
Brides Macabre Tigers Hotel
Project Farewell My Eyes
Glass Cloud Nothings Black
Necropsy Rites Of Today
```


# モダンメタルバンドのバンド名の傾向とそれに対するアプローチ
昨今のモダンなメタルバンドの名前は非常に複雑化してきています。
以下はその例です。

- Animal as Leaders
- After the Burial
- Attila
- Bring Me The Horizon
- Carnifex
- Oceans Ate Alaska
- Periphery
- Thy Art is Murder
- Within Destruction



自分なりに分析した結果、以下のような傾向にある気がしました。

- 3-4の英単語列
- 厨二単語の乱用
- 深い意味がありそう(~~で実はない~~)な名前
- 造語、ラテン語など英語以外の単語


また、バンド関係者の話を聞いてみるとバンド名を決める際には以下のことに留意しておくべきという情報も得られました。

- 略したときにキャッチーさが残る
  - 各単語の頭文字を取って略すことが多い -> BMTH(Bring Me The Horizon), KsE(Killswitch Engage), A7X(Avenged Sevenfold)など
- (当たり前だが)オリジナリティを出す
  - 検索エンジンで調べる際に他の情報と被って埋もれてしまうのはよくない。
  - 既に同じバンド名がある場合は論外。
- 自分たちで造語を作ったら上記二点は満たしやすい(が、難しい)。


分析した傾向の中でも造語以外については、バンド名として使われる単語を収集してそこから何かしらの処理で自動生成できるのではないかと考えました。
本記事では以下の3つのアプローチで自動生成を試みました。
1. 使いたい単語1つと生成したい単語数を指定して、単語リストの中からランダムで残りの単語数分抽出する
2. 頭文字を指定して、その頭文字に沿った単語を単語リストの中からランダムで抽出する
3. 生成したい単語数を指定して、マルコフ連鎖を用いて生成する

# 補足：マルコフ連鎖について
ざっくりと説明すると、マルコフ連鎖とは「時刻`t+1`の状態は、現在の時刻`t`の状態のみに依存する(それより過去の時刻`t-i (i<0)`の状態には依存しない)」という考え(マルコフ性)をもつ確率過程です。

文生成における「状態」は文字や単語、文節にあたります。(今回のバンド名生成においては単語)

例えば、
- 「The Rolling Stones」
- 「The Rolling KoroKoros」
- 「The Rolling GoroGoros」
- 「Led Zeppelin」

という4つのバンド名をサンプルとした場合、
「The」の後ろは0.75(=3/4)の確率で「Rolling」が出現し、「Rolling」の次は0.33(=1/3)の等確率で「Stones」、「Korokoros」、「GoroGoros」が出現する。「Led」の後ろは1.00(=1/1)の確率で必ず「Zeppelin」が出現する、
というふうな感じで次の状態(=次の単語)を決めることができます。サンプル数を増やすことでより生成バリエーションの多いモデルを作ることができるというわけですね。

マルコフ連鎖の詳しい説明、pythonによる実装は[こちらの記事](https://qiita.com/k-jimon/items/f02f実装したモード別にae75e853a9c02127)がわかりやすく説明されています。

# 実装
## 実装環境

```
OS: Ubuntu 18.04.2 LTS
Python: 3.6
```

## バンド名収集
Wikipedia各ジャンルのバンド名リストから収集し、1行1バンド形式のtxtファイルを作成しました。
こちらについてはgithubのレポジトリにもアップしました！

``` :今回収集したジャンル
- Christian hardcore
- deathcore
- djent
- mathcore
- metalcore
- hardcore punk
- heavy metal
```

最終的に1,730バンド収集することができました。

## コード

```python:generate.py
import numpy as np
import random
import re
from collections import defaultdict
import pprint

def make_by_word(seed_word, num_word, init_dict):
    """指定した単語を使って生成"""
    result_words = [seed_word]
    seed_word = seed_word.lower()
    for i in range(num_word-1):
        chosen_word = random.choice(tokenized_text)
        result_words.append(chosen_word)

    bandname = result_words
    if "the" in bandname or "The" in bandname:
        for i, res in enumerate(result_words):
            if res.lower() == "the":
                bandname[0], bandname[i] = bandname[i], bandname[0]
    else:
        random.shuffle(bandname)

    print(" ".join(bandname))

def make_by_initial_name(seed, init_dict):
    """指定したイニシャルを基に生成"""
    seed = seed.lower()
    result_words = []
    for initial in seed:
        #print(initial)
        #print(init_dict[initial])
        choices = init_dict[initial]
        chosen_word = random.choice(choices)
        result_words.append(chosen_word.capitalize())

    print(" ".join(result_words))

def walk_graph(seed, graph, distance=5, start_node=None):
    """単語数を指定してマルコフ連鎖を用いて生成"""
    if distance <= 0:
        return []
    #print(seed)
    # If not given, pick a start node at random.
    if not start_node:
        start_node = random.choice(list(graph.keys()))


    weights = np.array(
        list(markov_graph[start_node].values()),
        dtype=np.float64)
    # Normalize word counts to sum to 1.
    weights /= weights.sum()

    # Pick a destination using weighted distribution.
    choices = list(markov_graph[start_node].keys())
    #print(choices)

    chosen_word = np.random.choice(choices, None, p=weights)

    result_words = [chosen_word] + walk_graph(seed,
        graph, distance=distance-1,
        start_node=chosen_word)

    return result_words


if __name__ == '__main__':
    # preprocess
    ## Read text from file and tokenize.
    path = './band_list/band_list.txt'

    with open(path) as f:
      text = f.read()
    #print(text)
    tokenized_text = [
        word
        for word in re.split('\W+', text)
        if word != ''
    ]

    init_dict = defaultdict(list)
    for word in set(tokenized_text):
      word_ = word.lower()
      init_dict[word_[0]].append(word_)

    ## Create graph.
    markov_graph = defaultdict(lambda: defaultdict(int))

    last_word = tokenized_text[0].lower()
    for word in tokenized_text[1:]:
        word = word.lower()
        markov_graph[last_word][word] += 1
        last_word = word


    # main process
    print("""\
    mode:
    1: generate the words you want to use and the num of words you set(e.g. bring 4)
    2: set the acronym you want to use(e.g. BMTH)
    3: set hte word count and generate a Markov chain model(e.g. 4)""")
    mode = int(input("mode? >> "))

    if mode == 1:
        seed = input("seed? >> ")
        num_word = int(input("word num? >> "))
        print("--------------------------")
        for i in range(10):
            make_by_word(seed, num_word, init_dict)

    elif mode == 2:
        seed = input("initial? >> ")
        print("--------------------------")
        for i in range(10):
            make_by_initial_name(seed, init_dict)

    else:
        seed = int(input("word num? >> "))
        print("--------------------------")
        for i in range(10):
            #print(' '.join(walk_graph(
            #      markov_graph, distance=len(seed))), '\n')
            count = 0
            res = walk_graph(seed, markov_graph, distance=seed)
            print(" ".join(res))
```

前処理として、
・バンド名リストを読み込み、
・バンド名を辞書に格納
・マルコフグラフを作成
を行なったあと、メインの機能であるバンド名生成を行います。

今回は、上記でも触れた3つのアプローチで実装しました。

1. 使いたい単語1つと生成したい単語数を指定して、単語リストの中からランダムで残りの単語数分抽出する(`make_by_word()`)
2. 頭文字を指定して、その頭文字に沿った単語を単語リストの中からランダムで抽出する(`make_by_initial_name()`)
3. 生成したい単語数を指定して、マルコフ連鎖を用いて生成する(`walk_graph()`)





# 実際にバンド名を生成する
以下を実行してください。

`python generate.py`

以下が出力されるのでモードを選んでください。

```
mode:
1: generate the words you want to use and the num of words you set(e.g. bring 4)
2: set the acronym you want to use(e.g. BMTH)
3: set hte word count and generate a Markov chain model(e.g. 4)
mode? >>
```

モード別の概要は以下のようになっています。

```
1: 使いたい単語と生成したい単語数を指定してバンド名生成を行います。(例：bring 4)
2: 生成したいバンド名の頭文字を指定してバンド名生成を行います。(例：BMTH)
3: 生成したい単語数を指定しマルコフ連鎖モデルを使用してバンド名生成を行います。
```
モードごとに聞かれる値を設定することで10個のバンド名を自動で生成します。



## モード1: 使いたい単語と単語数を指定

これが使いたいという単語があればこのモードを使いましょう。存在しない単語を指定することができます。
(標準出力の下線部より下が生成されたバンド名です。)

```
mode:
1: generate the words you want to use and the num of words you set(e.g. bring 4)
2: set the acronym you want to use(e.g. BMTH)
3: set hte word count and generate a Markov chain model(e.g. 4)
mode? >> 1
seed? >> Bring
word num? >> 4
--------------------------
Bring Funeral Against You
Bring Deftones Airlines Kids
Hesitation Dire Bring Psyopus
Lions Barren Bring Dismemberment
Rorschach Bring Hand Bloodshed
Bring Pereo Hound This
Take Remains Bring Skycamefalling
Bring Sick Bunchofuckingoofs Annisokay
Rock Bring Fall Drivers
Devil Bring s Sacred
```



## モード2: 生成したいバンド名の頭文字を指定

昨今のバンド名はイニシャルで略されることが多いため実装したモードです。
(標準出力の下線部より下が生成されたバンド名です。)


```
mode:
1: generate the words you want to use and the num of words you set(e.g. bring 4)
2: set the acronym you want to use(e.g. BMTH)
3: set hte word count and generate a Markov chain model(e.g. 4)
mode? >> 2
initial? >> BMTH
--------------------------
Bullet Minutemen Thee Hoax
Burst Meat Treason Hand
Burden Minus Trooper Hollow
Blitzkrieg Mantis Treatment Horse
Botch Me Troy Hat
Beasts Mad Tattoo Hacktivist
Break Moxy They Headpins
Brat Murphy Thrice Hearts
Blue Mountain They Houses
Brides Macabre Tigers Hotel
```

略した時のかっこよさからトップダウンで決めることができます。


## モード3: 生成したい単語数のみを指定しマルコフ連鎖モデルで生成

単語数のみを指定してマルコフ連鎖モデルを用いた生成を行います。
(標準出力の下線部より下が生成されたバンド名です。)


```
mode:
1: generate the words you want to use and the num of words you set(e.g. bring 4)
2: set the acronym you want to use(e.g. BMTH)
3: set hte word count and generate a Markov chain model(e.g. 4)
mode? >> 3
word num? >> 4
--------------------------
venomous concept messiah prophet
wept rapeman pagan altar
hat band opprobrium p
x cries hannah head
lvl maroon alexisonfire mallory
for mine anti cimex
ambassadors of slumber kerber
project farewell my eyes
glass cloud nothings black
necropsy rites of today
```

こちらは単語数以外の指定はないため、使いたい単語等も決まってないときに使うことができます。
マルコフ連鎖を使っているおかげで語の並びなどは今回の3モードの中で最もそれっぽいものになっているはずです。


# おわりに
今回は、既存のバンド名に使われている単語を基にそれっぽいバンド名を自動生成するツールを作ってみました。
これを使って何度か試行し、気に入ったものをバンド名に提案しようと思います。
需要は非常に低いかもですが、皆さんも同じような課題にぶつかったときは活用してみてください。


(後日談)<br>自動生成したバンド名は却下となってしまいましたが、他のメンバーが考えてきた案を全員でブラッシュアップして無事に決めることができました！

# 参考
- [[Python]N階マルコフ連鎖で文章生成](https://qiita.com/k-jimon/items/f02fae75e853a9c02127)
- [Build a Markov Chain Sentence Generator in 20 lines of Python](https://www.jeffcarp.com/posts/2019/markov-chain-python/)
- バンド名収集系
  - [List of deathcore artists](https://en.wikipedia.org/wiki/List_of_deathcore_artists)
  - [List of mathcore bands](https://en.wikipedia.org/wiki/List_of_mathcore_bands)
  - [List of metalcore bands](https://en.wikipedia.org/wiki/List_of_metalcore_bands)
  - [List of Christian hardcore bands](https://en.wikipedia.org/wiki/List_of_Christian_hardcore_bands)
  - [List of hardcore punk bands](https://en.wikipedia.org/wiki/List_of_hardcore_punk_bands)
  - [List of heavy metal bands](https://en.wikipedia.org/wiki/List_of_heavy_metal_bands)
  - [List of djent bands](https://en.wikipedia.org/wiki/List_of_djent_bands)