先生、人生相談です。TF-IDFで歌詞の分析はできますか？（ヨルシカらしさを求めて Part.1）

# 1. はじめに

皆さん「ヨルシカ」をご存じですか？　私はファンクラブの会員になるほど大好きです。最近はFFBEやNEWS23にヨルシカの楽曲が起用されていることもあるので、名前を知っている人も多いのではないでしょうか？　ちなみに私のおすすめ曲は「心に穴が空いた」です。「夜紛い」のアンサーソングとしての意味合いもあるので、是非とも一緒に聞いてみてください。

そこで、今回は**ヨルシカの楽曲同士で比較して、各楽曲でどういう歌詞が多く使われているのか？**について、TF-IDFで分析していこうと思います！

# 2. 環境
- Windows 10
- Python 3.7.9 (Anaconda)
- mecab-python3 (Ver. 1.0.3)
- BeautifulSoup4 (Ver. 4.9.3)
- scikit-learn (Ver. 0.23.2)

# 3. まず、TF-IDFとは？（簡単に）

> tf-idfは、文書中に含まれる単語の重要度を評価する手法の1つであり、主に情報検索やトピック分析などの分野で用いられている。([Wikipedia](https://ja.wikipedia.org/wiki/Tf-idf)より)

TF-IDF値は以下の式で計算されます。

```math
tfidf(t_{a}, d_{b}) = tf(t_{a}, d_{b}) \cdot idf(t_{a})
```

ここで，$d_b$をある文書として，そこに出現する単語を$t_a$とします。
TF値、IDF値はそれぞれ以下の式で計算されます。

```math
tf(t_{a}, d_{b}) = \frac{n_{a, b}}{\sum_{k} n_{k,b}} \\
idf(t_{a}) = \mathrm{log}\frac{N}{df(t_{a})}
```
ここで、$n_{a, b}$はある単語$t_a$の文書$d_b$内での出現回数、Nは全文書数、$df(t_{a})$は単語$t_{a}$が出現する文書の数とします。そうすることで、TF値は文書中で単語がどの程度の頻度で出現したかを、IDF値は単語がどれくらいそれぞれの文書で出現しやすいかを表現します。そして、それらを掛け合わせるたTF-IDF値は、ある単語が他の文書と比較してどれだけ重要であるかを表現できます。

# 4. コード
まず、[歌詞検索J-Lyric.net](https://j-lyric.net/)からのアーティストページから楽曲の歌詞ページをとってきます、そして、それぞれの歌詞ページから歌詞を持ってきて、分かち書きしたテキストに変換します。
今回はbase_listに分かち書きした歌詞の文字列のリストを入れています。

```Python
import requests
import re
from bs4 import BeautifulSoup
import MeCab
import sys
from sklearn.feature_extraction.text import TfidfVectorizer
import time

DOMAIN = "https://j-lyric.net"
YORUSHIKA = "/artist/a05cf40/"

def get_soup_from_artist_page(url):
    response = requests.get(url)
    soup = BeautifulSoup(response.text, 'html.parser')
    time.sleep(1)
    return soup

def get_lyric_page_from_artist_page(soup):
    """
    歌詞ページへのリンク部分のデータを取得する。

    Parameters
    ----------
    soup : bs4.BeautifulSoup
        アーティストページをBSでパースしたデータ。
    Returns
    -------
    soup.find(id="cnt").find(id="mnb").find(class_="cnt").find_all(id=re.compile("ly")) : list
        歌詞へのリンク、タイトルを含むデータのリスト。
    """
    return soup.find(id="cnt").find(id="mnb").find(class_="cnt").find_all(id=re.compile("ly"))

def get_lyric_path(page):
    """
    歌詞ページへのパスを取得する。

    Parameters
    ----------
    page : bs4.BeautifulSoup
        歌詞ページへのリンク部分のデータ。
    Returns
    -------
    page.find(class_="ttl").find("a").get("href") : string
        歌詞ページへのパス。
    """
    return page.find(class_="ttl").find("a").get("href")

def get_lyric_title(page):
    """
    歌詞タイトルを取得する。

    Parameters
    ----------
    page : bs4.BeautifulSoup
        歌詞ページへのリンク部分のデータ。
    Returns
    -------
    page.find(class_="ttl").find("a").contents[0] : string
        歌詞のタイトル。
    """
    return page.find(class_="ttl").find("a").contents[0]

def get_lyrics_textlist_from_page(lyrics_url):
    """
    URLから歌詞を持ってくる。

    Parameters
    ----------
    lyrics_url : string
        歌詞ページのURL。
    Returns
    -------
    lyric_textlist : list
        歌詞ページから持ってきた歌詞の文字列。
        ex. [..., "誰かのために強くなれるなら", "ありがとう、悲しみよ", ...]
    """
    response = requests.get(lyrics_url)
    soup = BeautifulSoup(response.text, 'html.parser')
    time.sleep(1)
    lyric_list = soup.find(id="Lyric").contents
    lyric_textlist =[x for x in lyric_list if getattr(x, 'name', None) != 'br']
    # 全角空白を「、」にする
    lyric_textlist =[x.replace("\u3000", "、") for x in lyric_textlist] 
    return lyric_textlist


def get_base_str(lyric, mecabTagger):
    """
    歌詞のリストをMecabで分かち書きして、そのテキストを返す。

    Parameters
    ----------
    lyric : list
        歌詞のリスト。
        ex. [..., "誰かのために強くなれるなら", "ありがとう、悲しみよ", ...]
    mecabTagger : MeCab.Tagger
        MeCabのTagger。
    Returns
    -------
    base_str : string
        分かち書きしたテキスト。
        ex. "... 誰 ため 強い なれる ありがとう 悲しみ ..."
        
    """
    base_str = ""
    for t in lyric:
        node = mecabTagger.parseToNode(t)
        base_str_each_line = ""
        while node:
            feature = node.feature.split(",")
            if feature[0] != "BOS/EOS" and feature[0] != "助詞" and feature[0] != "助動詞" and feature[0] != "補助記号" and len(feature) > 8:
                base = feature[7]
                if "-" in base:
                    base = base.split("-")[0] # 原型
                base_str_each_line += (base + " ") # res: "いちご  食べる "
            node = node.next
        base_str += base_str_each_line # res: "いちご 食べる ラーメン 美味しい ... "
    return base_str

soup = get_soup_from_artist_page(DOMAIN + YORUSHIKA)
title_list = []
mecabTagger = MeCab.Tagger("-Owakati")
# アーティストページから歌詞ページを含むsoupを持ってくる
page_list = get_lyric_page_from_artist_page(soup)
base_list = []
for p in page_list:
    lyric_path = get_lyric_path(p)
    lyric_title = get_lyric_title(p)
    lyric_url = DOMAIN + lyric_path
    lyrics_text_list = get_lyrics_textlist_from_page(lyric_url)
    title_list.append(lyric_title)
    base_str_each_song = get_base_str(lyrics_text_list, mecabTagger)
    base_list.append(base_str_each_song)
```
とりあえずこれで分かち書きした歌詞テキストのリストを取得できたので、これをTF‐IDFで計算していきます。
計算するコードは[こちら](https://tex2e.github.io/blog/python/tf-idf)より引用させてもらいました。

```Python
vectorizer = TfidfVectorizer(max_df=0.9) # tf-idfの計算
X = vectorizer.fit_transform(base_list)
words = vectorizer.get_feature_names()
i = 0
for doc_id, vec in zip(range(len(base_list)), X.toarray()):
    print('doc_id:', doc_id)
    cnt = 0
    print('title: ', title_list[i])
    for w_id, tfidf in sorted(enumerate(vec), key=lambda x: x[1], reverse=True):
        if cnt > 10: break
        lemma = words[w_id]
        print('\t{0:s}: {1:f}'.format(lemma, tfidf))
        cnt += 1
    i += 1
```
これで各楽曲ごとにTF-IDF値の高い語が順に11個出力されます。

# 5. 結果
全部結果を載せると大変なので一部抜粋して見ていきましょう。

```
doc_id: 31
title:  爆弾魔
	爆破: 0.736169
	為る: 0.245904
	吹き飛ぶ: 0.226513
	爆弾: 0.226513
	狡い: 0.226513
	片手: 0.152953
	全部: 0.150287
	此の: 0.144266
	青春: 0.131620
	日々: 0.117569
	さようなら: 0.104170
```
流石爆弾魔といったところでしょうか。タイトルの通り、サビでも爆破という言葉を反復しているので、値が0.73まで到達するのも納得です。（余談ですが、[3rd Full Album「盗作」](https://sp.universal-music.co.jp/yorushika/tousaku/)に収録されているRe-recording版もとても良かったので是非聞いてみてね）

```
doc_id: 24
title:  盗作
	足りる: 0.570575
	未だ: 0.356744
	満たす: 0.332895
	知る: 0.248869
	盗む: 0.240515
	美しい: 0.186341
	本当: 0.134581
	此の: 0.134035
	ああ: 0.131671
	切っ掛け: 0.120257
	或る: 0.120257
```
わかる人にはわかるでしょう。助動詞がないのでちょっとわかりにくいですが、この曲は「足りない」「満たされない」「知りたい」と自らの欲への渇望を謳った曲なのです。改めてこうやって歌詞を見返してみるとやはり心に来るものがありますね。ヨルシカ最高。盗作はいいぞ。Music Videoも観てね。[ヨルシカ - 盗作（OFFICIAL VIDEO）
](https://youtu.be/CS4f3jawFxY)

```
doc_id: 22
title:  ただ君に晴れ
	大人: 0.335331
	追い付く: 0.287321
	ポケット: 0.230888
	記憶: 0.230888
	成る: 0.213277
	上る: 0.207875
	出す: 0.201199
	咲く: 0.193791
	乾く: 0.178883
	俯く: 0.178883
	其れ: 0.169299
```
Youtube再生回数1億回越えおめでとう。ただ君に晴れです。個人的には大人とか追いつくの値が高くてへぇ～～ってなりました。やっぱりサビで反復が多いとスコアに乗りやすいんですかね。こっちのMusic Videoもぜひ見てね。[ヨルシカ - ただ君に晴れ (MUSIC VIDEO)
](https://www.youtube.com/watch?v=-VKIqrvVOpo)

# 6. 気になったところ
MeCabで形態素解析した結果が一部怪しかったです。「覚束ぬままに」の「おぼつかぬ」を正しく認識できていなかったり、「マシンガン」を「マシン」と「ガン」に分けて認識していたりしました。MeCabの方で辞書とかいじればもうちょっといい感じにできるんでしょうか？

# 7. 最後に
今回はヨルシカの各楽曲の歌詞について調べていきました。単純な手法でしたが、結構頻度の高い語を持ってこれていたのでびっくりしました。
一応これはPart.1なので、もしかしたらPart.2が出るかもしれないし出ないかもしれないです。気持ち的には頑張って出してヨルシカの布教をしたいなぁと思っています。