アイドルのコンセプトは歌詞にも現れているのか？





# ベロティカってアイドル知ってますか...？

 ベロティカっていうアイドルが半年ぐらい前デビューしたんですけど（記事を書いてるのは2024/2/28です）、そのアイドルのコンセプトが「エロティシズム」なんですよ。~~（エロティシズムという言葉は初めて知りました）~~
  
 :::note info

 
 「エロティシズム」とはなにかをChatGPTにきいてみました
 
 「エロティシズム」は、美や愛に対する強い興奮や魅力を意味する言葉です。これはしばしば性的な要素を含むものであり、芸術、文学、映画、写真などの表現の中で見られることがあります。エロティシズムは個人の感性や文化によって異なる解釈を受けることがあり、時には挑発的であったり、芸術的であったりすることがあります。

一般的には、エロティシズムはセクシュアリティや肉体美に焦点を当て、その美的な側面を強調します。これは単なる性的な描写だけでなく、感性や情熱、美学に対する深い理解や表現も含むことがあります。

:::



アイドルの歌う歌詞が「エロティシズム」を表しているような歌詞であれば、更にコンセプトがしっかりしているということになるのではないか？と思ったので、調べてみました。

ベロティカのtwitter：https://twitter.com/verotica_info

# どうやって調べるん？

ベロティカの歌っている歌詞を形態素解析してLDAを用いてトピックモデルを生成します。その後、ワードクラウドを用いて「エロティシズム」っぽい単語が出てくるのかを調べてみました。（エロティシズムっぽい歌詞の判定は私の主観になってしまいます）

# 手法の流れ
今回の調べ方の流れは以下のようになっています。

1. 歌詞をネットからスクリプトで入手
1. 歌詞の不要な文字列を削除
1. 歌詞を形態素解析を行い、単語ごとにする
1. LDAを用いて、トピックモデルの生成
1. 結果の確認

上記の順番で記事を書いていきます。

コードはgithubにあります。
https://github.com/NeoSolleil/The-concept-of-an-idol..git

# 歌詞をネットからスクリプトで入手

まずは歌詞を手に入れましょう。
歌詞は表示されているブラウザから直接コピーすることはできないので、スクリプトで入手します。

```linkco_kasi_toridasi.py

import os
import re
import bs4
import time
import requests
import pprint

def load(url):
    res = requests.get(url)
    res.raise_for_status()

    return res.text

def pickup_tag(html, find_tag):
    soup = bs4.BeautifulSoup(str(html), 'html.parser')
    paragraphs = soup.find_all(find_tag)

    return paragraphs

def parse(html):
    soup = bs4.BeautifulSoup(str(html), 'html.parser')
    # htmlタグの排除
    kashi_row = soup.getText()
    kashi_row = kashi_row.replace('\n', '')
    kashi_row = kashi_row.replace('　', '')

    # 英数字の排除
    kashi_row = re.sub(r'[a-zA-Z0-9]', '', kashi_row)
    # 記号の排除
    kashi_row = re.sub(r'[ ＜＞♪`‘’“”・…_！？!-/:-@[-`{-~]', '', kashi_row)
    # 注意書きの排除
    kashi = re.sub(r'注意：.+', '', kashi_row)

    return kashi


def extract_text_from_string(text, start_string, end_string):
    start_index = text.find(start_string)
    if start_index == -1:
        print(f"指定された開始文字列 '{start_string}' が見つかりませんでした。")
        return None

    end_index = text.find(end_string, start_index)
    if end_index == -1:
        print(f"指定された終了文字列 '{end_string}' が見つかりませんでした。")
        return None

    extracted_text = text[start_index + len(start_string):end_index]
    return extracted_text

start_string = '<div class="lyric_text"><p>'
end_string = '</p></p></p>'


site_url='https://linkco.re/t667vaEg/songs/2472069/lyrics'

# 曲のurlを格納
musics_url = []
musics_url.append(site_url)



""" 歌詞の取得 """
for i, page in enumerate(musics_url):

    html = load(page)
    for div in pickup_tag(html, 'div'):

        div = str(div)
        result = extract_text_from_string(div, start_string, end_string)
        if result is not None:
            #print(result)
            #print('')
            cleaned_text = result.replace('\u3000', ' ').replace('<p><p>', ' ').replace('<p>', ' ')
            #cleaned_text = result.replace('\u3000', ' ').replace('<p>', ' ').replace('<p><p>', ' ')
            print(cleaned_text)
            break
            
```

上記のコードを使用することで歌詞だけを取り出せます。
コードの
```
site_url='https://linkco.re/t667vaEg/songs/2472069/lyrics'
```
の部分を適宜変えてください。
このコードでは、「超NEW WORLD」という曲の歌詞を取得するコードになっています。


# 歌詞の不要な文字列を削除
取得したテキストファイルをそのまま使用すると「\u3000」「＜p＞」「＜p＞＜p＞」（本当は半角ですが表示できないので、表示するために全角にしました）のような不要な文字列が含まれているので、取り除きます。
最初は歌詞を取り出したときにもう一度コードを書いて処理していましたが、面倒くさかったので`linkco_kasi_toridasi.py`のコードに組み込みました。~~（最初からやっておけ）~~
下記の部分です。
```            
cleaned_text = result.replace('\u3000', ' ').replace('<p><p>', ' ').replace('<p>', ' ')
```
:::note warn
注意

```  
#cleaned_text = result.replace('\u3000', ' ').replace('<p>', ' ').replace('<p><p>', ' ')
```  
こちらのコードのようにｐのreplaceの順番が逆になれば、置き換えたときに空白が２つになってしまう場合あります。
:::


ここで形態素解析するなら空白とかを削除して純粋なテキストファイルにしたほうがいいんじゃないか？と思った方がいるかもしれません。
**しかし、歌において歌詞の切れ目は意味があるものだと私は考えています。歌の切れ目となる場所は歌詞カードにおいて空白や改行になっているので、なぜそこで歌詞を切るのか？という含みを持たせるために必要だと考えました。よって、空白や改行はなるべく残しておきたいと考えました。**（今回は改行を空白に置き換えています。）


# 歌詞を形態素解析を行い、単語ごとにする

linkco_kasi_toridasi.pyで生成したテキストファイルを用いて、分かち書きを行います。
sudachiを使用しました。

```wakati_sudachi.py

import os
from sudachipy import tokenizer, dictionary

def tokenize_text(text):
    tokenizer_obj = dictionary.Dictionary().create()
    tokens = tokenizer_obj.tokenize(text)
    result = ' '.join([m.surface() for m in tokens])
    return result

def tokenize_files_in_folder(folder_path, output_folder):
    if not os.path.exists(output_folder):
        os.makedirs(output_folder)

    tokenizer_obj = dictionary.Dictionary().create()

    for filename in os.listdir(folder_path):
        if filename.endswith(".txt"):
            file_path = os.path.join(folder_path, filename)
            output_path = os.path.join(output_folder, f"{filename}_tokenized.txt")

            with open(file_path, 'r', encoding='utf-8') as file:
                content = file.read()

            tokens = tokenize_text(content)

            with open(output_path, 'w', encoding='utf-8') as output_file:
                output_file.write(tokens)

# 使用例
input_folder_path = 'linkco_kasi_toridasi.pyで生成したテキストファイルが入っているフォルダのPATH'
output_folder_path = '分かち書きをしたテキストファイルを保存するフォルダのPATH'

tokenize_files_in_folder(input_folder_path, output_folder_path)

``` 
上記のコードを使用すると、分かち書きされたテキストを入手することができます。

しかし、linkco_kasi_toridasi.pyで生成したものにはそもそもで空白が入ってしまっている。
このままでは、「空白を分かち書きする」部分が存在してしまう。（空白を単語として分かち書きするので空白の前後に空白がつく）
```linkco_kasi_toridasi.py　で取得した歌詞
感情線転がって 膨らんじゃった妄想に揺られ
```

```wakati_sudachi.py　で分かち書きしたテキスト
感情 線 転がっ て   膨らん じゃっ た 妄想 に 揺られ
```
「て」の後ろが半角の空白が３つついてしまっているんです。これを取り除かないと上手くLDAができないかもしれないので、3つの空白の部分を1つの空白に置き換えます。


```kuhaku_3wo1.py

file_path = 'wakati_sudachi.pyで分かち書きしたテキストへのPATH'  # テキストファイルのパスを適切に指定してください

# ファイルを開いて中のデータを読み込む
with open(file_path, 'r') as file:
    lines = file.readlines()

cleaned_text = lines[0].replace('   ', ' ')#.replace('\n', '')
print(cleaned_text)
```
上記のコードを使用すると、3つの空白の部分を1つの空白に置き換えられます。~~（`wakati_sudachi.py`に組み込むの忘れてた。）~~

```kuhaku_3wo1.py　で空白を置き換えたテキスト
感情 線 転がっ て 膨らん じゃっ た 妄想 に 揺られ
```

3つの空白が無くなっていることが確認できますね。


# LDAを用いて、トピックモデルの生成

やっと「エロティシズム」のコンセプトが歌詞にも現れているのかを知ることができますね。

作成した歌詞を1つのテキストファイルにまとめてLDAのトピックモデルを生成します。
LDAでトピックモデルを作成し、それを用いてワードクラウドを生成します。
今回は、6つの楽曲をまとめたものを使用するので、トピックモデルのトッピクス数は6つに設定しました。（なんとなくで決めたので、特に深い理由はありません笑）

以下のコードを使用してワードクラウドを生成します。  




```word_cloud.py
from wordcloud import WordCloud
import matplotlib.pyplot as plt
import MeCab
import gensim
from gensim import corpora, models, similarities
import ipadic

def token(text_list,wvs):
    CHASEN_ARGS = r' -F "%m\t%f[7]\t%f[6]\t%F-[0,1,2,3]\t%f[4]\t%f[5]\n"'
    CHASEN_ARGS += r' -U "%m\t%m\t%m\t%F-[0,1,2,3]\t\t\n"'
    t = MeCab.Tagger(ipadic.MECAB_ARGS + CHASEN_ARGS)
    for text in text_list:
        pos = t.parseToNode(text)
        word_vector = []
        while pos:
            if "名詞" in pos.feature:
                if "数" not in pos.feature and "非自立" not in pos.feature and "接尾" not in pos.feature and "代名詞" not in pos.feature:
                    word_vector += [pos.surface]
            pos = pos.next
        wvs += [word_vector]
    return text_list

def get_topic(wvs):
    #print(wvs)
    # 辞書作成
    dictionary = corpora.Dictionary(wvs)
    #dictionary.filter_extremes(no_below=2, no_above=0.3)
    dictionary.save_as_text('dict.txt')
    # コーパスを作成
    corpus = [dictionary.doc2bow(text) for text in wvs]
    corpora.MmCorpus.serialize('cop.mm', corpus)
    # 辞書(dict.txt)を読み込んで、コーパス(cop.mm)を作成
    dictionary = gensim.corpora.Dictionary.load_from_text('dict.txt')
    corpus = corpora.MmCorpus('cop.mm')
    # LDAの実行
    topic_N = 6
    lda = gensim.models.ldamodel.LdaModel(corpus=corpus, num_topics=topic_N, id2word=dictionary)
    return lda, corpus


def read_file(filename):
    data = []
    stopwords = [""]
    with open(filename, 'r') as f:
      text = f.read()
    word_list = text.split()

    return word_list


def check_lda(lda, corpus):
    for topics_per_document in lda[corpus]:
        high_topic_score = 0.0
        for topic in topics_per_document:
            topic_index = topic[0]
            topic_score = topic[1]
            if topic_score > high_topic_score:
                high_topic_score = topic_score
                high_topic_index = topic_index

def word_cloud(lda, corpus):
    plt.figure(figsize=(20,20))
    for t in range(lda.num_topics):
        plt.subplot(4,3,t+1)
        x = dict(lda.show_topic(t, 50))
        fpath = "SourceHanSansJP-Normal.otfへのPATH"
        im = WordCloud(background_color="white",font_path=fpath, width=900, height=500).generate_from_frequencies(x)
        plt.imshow(im)
        plt.axis("off")
        plt.title("Topic_" + str(t))
    plt.show()


if __name__ == '__main__':
    wvs = []
    filename = '歌詞を1つにまとめたテキストファイルへのPATH'
    data = read_file(filename)
    token_jpn = token(data,wvs)
    lda, corpus = get_topic(wvs)
    check_lda(lda, corpus)
    word_cloud(lda, corpus)

```


コードの以下の3つを適宜変更してください。

```トピック数を変更したいとき.
topic_N = 6
```
```フォントの変更をしたいとき.
fpath = "SourceHanSansJP-Normal.otfへのPATH"
```

```歌詞をパスを変更したいとき.
filename = '歌詞を1つにまとめたテキストファイルへのPATH'
```


# 結果
:::note info
word_cloud.pyのコードの中で名詞のみを使用するようにしています。
:::
![topic6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718719/0f6c1a0a-4122-0c46-e225-10fd77f94ff7.png)

# 考察

- トピック０（左上）とトピック4（下段中央）において、ChatGPTが回答した「美や愛に対する強い興奮や魅力を意味する言葉です。これはしばしば性的な要素を含むもの」という「エロティシズム」に沿ったものが生成できていると考える
- しかし、同じ単語が他のトピックにも入っているため、トピックとして分類ができていないと考える
- 特に英単語を上手く分類することはできておらず、理由として、基本的に歌詞は日本語となっているので、データが少ないということが考えられる
- そもそもで楽曲が6つしかないので、トピックとして分類するほどの単語数がなかったのではないかと考える

# 感想

NEWとWORLDの主張強すぎですね。笑
結構エロティシズムな言葉が出てきましたね。服装やダンスだけでなく、歌詞もエロティシズムな感じなんですね。~~（エロティシズムをよく分かってない）~~
やってる途中に、データが少ないかも...と感じたので、上手くいかないかな思ってましたが、意外と上手くいきましたね。笑
でも、同じ単語が他のトピックにも出てきてるので、明らかにデータ数が少ないですよね。


しっかりとしたコンセプトがあって楽曲の多いアーティストって誰がいますかね？
「コンセプトを持っているアーティスト」となるとアイドルしか思いつきません。笑

ベロティカが長い期間活動してくれれば、もっと楽曲が増えるので、更に分類がしやすくなりますね。
この記事を見て、少しでもベロティカについて興味が出てきたなら、ぜひ[twitter](https://twitter.com/verotica_info)や[Youtube](https://www.youtube.com/@verotica_info)を見てみてください。







ここまで読んでいただきありがとうございます。
また書こうと思います。
また逢う日まで。