2018-02-22 追記
コード類をGitHubにあげました。プルリクお待ちしております!
https://github.com/nekoumei/lyric_visualizer_with_wordcloud
はじめに
凛として時雨とは
僕が大好きな日本の3ピースバンドです。
プログレッシブな曲展開が魅力の凛として時雨ですが、歌詞のセンスが独特でおもしろいです。
ちなみに、作詞作曲はすべてギターボーカルのTKが行っています。
最近、ニューアルバム「#5」を出したり、Apple Music等サブスクリプションサービスに過去音源が配信されたりしているので、まだ聴いたことのない方は是非。是非。
本記事のイシュー
凛として時雨のアルバムの特徴として、アルバムごとのコンセプト等は明確に設定されていません。
このことは本人たちがインタビューで明言しています。
(参考:https://www.cinra.net/interview/201802-lingtositesigure?page=2)
しかし、時雨好きの方なら分かるとおり、時期によって歌詞によく出てくる言葉は変わっているように聴こえます。
(初期はSadisticとかが好きだし徐々にプラスチックとかイリュージョンとかが入ってきたり)
そこで、アルバムごとに歌詞の傾向の違いをpythonをつかって可視化しようと思います。
やったこと
- Webスクレイピング
- 歌詞サイトから歌詞をスクレイピングします。
- 形態素解析
- 歌詞から単語を抽出します。
- TF-IDF
- アルバムごとに出現単語をベクトル化し、各アルバムの出現単語のTF-IDF値を算出します。
- WordCloudで可視化
- 冒頭の画像がいわゆるWordCloudです。これを用いてアルバムごとにWordCloudを描画します。
環境
OS : MacOS High Sierra 10.13.3
python : 3.6.1
その他、記事中で使うパッケージのインストール方法などはぐぐってください。
また、コードはすべてJupyter Notebook上で実行しています。
0. パッケージのインポート
import requests
from bs4 import BeautifulSoup
import pandas as pd
import re
from time import sleep
import sys
import MeCab
import numpy as np
from PIL import Image
from sklearn.feature_extraction.text import TfidfVectorizer
import matplotlib.pyplot as plt
%matplotlib inline
1. スクレイピング
今回は下記サイトから歌詞をスクレイピングします。
https://www.uta-net.com/
利用規約およびrobots.txtを確認しましたが、特にスクレイピングについては制限されていないようなので、気を遣いつつ情報を取得します。
1.1 曲の一覧及びURLを取得する
上記サイトの「凛として時雨」一覧ページから下記情報を取得します。
https://www.uta-net.com/artist/7840/
- 曲名
- 曲のURL
- 歌手名
- 作詞者名
- 作曲者名
作詞者名や作曲者名は、今回はすべてTKなのであまり意味はありませんが、他のアーティストに拡張した際に作詞者ごとに傾向を可視化するなどできそうなので取得しておきます。
def scraping_web_page(url):
sleep(5)
html = requests.get(url)
soup = BeautifulSoup(html.content, 'html.parser')
return soup
#曲一覧ページをスクレイピングする
soup = scraping_web_page('https://www.uta-net.com/artist/7840/')
#htmlをパースして曲名、各曲URL、アーティスト名、作詞、作曲者名を取得する
contents = []
contents.append(soup.find_all(href=re.compile('/song/\d+/$')))
contents.append(soup.find_all(href=re.compile('/song/\d+/$')))
contents.append(soup.find_all(class_=re.compile('td2')))
contents.append(soup.find_all(class_=re.compile('td3')))
contents.append(soup.find_all(class_=re.compile('td4')))
infomations = []
for i, content in enumerate(contents):
tmp_list = []
for element in content:
if i == 0:
tmp_list.append(element.get('href'))
else:
tmp_list.append(element.string)
infomations.append(tmp_list)
#DataFrameにする
artist_df = pd.DataFrame({
'URL' : infomations[0],
'SongName' : infomations[1],
'Artist' : infomations[2],
'Lyricist' : infomations[3],
'Composer' : infomations[4]})
#URLにホストネームを付加
artist_df.URL = artist_df.URL.apply(lambda x : 'https://www.uta-net.com' + x)
Webサイトに迷惑をかけないよう、sleepでスクレイピングする間を空けるようにしています。
(本節では1回しか見に行かないので大丈夫ですが、次節でわさっといきます)
これで、こんな感じのDataFrame(表)ができます。
この表をもとに、各曲の歌詞をスクレイピングしていきます。
1.2 各曲の歌詞、発売日、商品番号を取得する
残念ながらWebページにアルバム名が記載されていないので、商品番号を取得して後でアルバム単位にまとめるために使います。
#各曲のページをスクレイピングする
contents_list = []
for i, url in artist_df.URL.iteritems():
contents_list.append(scraping_web_page(url))
#歌詞、発売日、商品番号をdataframeに格納する
lyrics = []
sales_dates = []
cd_nums = []
for contents in contents_list:
lyrics.append(contents.find(id='kashi_area').text)
sales_dates.append(contents.find(id='view_amazon').text[4:14])
cd_nums.append(contents.find(id='view_amazon').text[19:28])
artist_df['Lyric'] = lyrics
artist_df['Sales_Date'] = sales_dates
artist_df['CD_Number'] = cd_nums
これで下記のDataFrameができます。(歌詞は画像を加工して消しています)
商品番号で、曲をアルバムごとに区別できますが、わかりにくいのでアルバム名を付与します。
cd_num_name_dict = {
'AICL-3481' : '#5',
'ANTX-1009' : 'Inspiration is DEAD',
'AICL-3382' : 'DIE meets HARD',
'AICL-2174' : 'still a Sigre virgin?',
'AICL-2014' : 'just A moment',
'ANTX-1006' : 'Feeling your UFO',
'ANTX-1002' : '#4',
'AICL-2804' : 'Best of Tornado',
'AICL-2451' : 'abnormalize',
'AICL-2949' : 'es or s',
'AICL-2761' : 'Enigmatic Feeling',
'AICL-2526' : "i'm perfect",
'ANTX-1011' : 'Telecastic fake show',
'AICL-2795' : 'Who What Who What',
'AICL-1985' : 'moment A rhythm'
}
artist_df['Album_Name'] = artist_df.CD_Number.apply(lambda x : cd_num_name_dict[x])
これで目的のDataFrameができました。
1.3 ちょっと前処理
DataFrameを眺めていると、ちょっと気になる所がありました。
2015年に出たベストアルバム、「Best of Tornado」の収録曲の一部が、ベスト盤のみに紐付けられ、オリジナル・アルバムからは省かれているようです。
どうやらUta-Netさんの仕様として、ひとつの曲はひとつのアルバムとのみ紐付けられているようです。
今回の目的にそぐわないため、これら4曲はそれぞれ元のオリジナルアルバムに紐付けなおします。
artist_df.at[16,'CD_Number'] = 'ANTX-1002'
artist_df.at[16,'Album_Name'] = cd_num_name_dict['ANTX-1002']
artist_df.at[46,'CD_Number'] = 'ANTX-1002'
artist_df.at[46,'Album_Name'] = cd_num_name_dict['ANTX-1002']
artist_df.at[61,'CD_Number'] = 'ANTX-1002'
artist_df.at[61,'Album_Name'] = cd_num_name_dict['ANTX-1002']
artist_df.at[41,'CD_Number'] = 'ANTX-1006'
artist_df.at[41,'Album_Name'] = cd_num_name_dict['ANTX-1006']
本当は曲名の(2015 mix)をとったり、発売日も修正すべきですが、今回は関係ないので無視します。
また、アルバム「just A moment」の「a over die」がインスト曲にもかかわらず、なぜか同アルバム「Telecastic fake show」の歌詞が入っていました。
https://www.uta-net.com/song/180777/
これはUta-Netさんのページが間違っているので、この曲はDataFrameから削除します。
artist_df.drop(12,inplace=True)
artist_df.reset_index(drop=True,inplace=True)
2. 形態素解析 + TF-IDF
def get_word_list(lyric_list):
#普通のipadicを使うときはこっち
#m = MeCab.Tagger ("-Ochasen")
#neologdを使うときはこっち
m = MeCab.Tagger ("-Ochasen -d /hogehoge/mecab/dic/mecab-ipadic-neologd")
lines = []
keitaiso = []
for text in lyric_list:
keitaiso = []
m.parse('')
ttt = m.parseToNode (re.sub('\u3000',' ',text))
while ttt:
#print(ttt.surface,ttt.feature)
#辞書に形態素を入れていく
tmp = {}
tmp['surface'] = ttt.surface
tmp['base'] = ttt.feature.split(',')[-3] #base
tmp['pos'] = ttt.feature.split(',')[0] #pos
tmp['pos1'] = ttt.feature.split(',')[1] #pos1
#文頭、文末を表すBOS/EOSは省く
if 'BOS/EOS' not in tmp['pos']:
keitaiso.append(tmp)
ttt = ttt.next
lines.append(keitaiso)
#baseが存在する場合baseを、そうでない場合surfaceをリストに格納する
word_list = []
for line in lines:
for keitaiso in line:
if (keitaiso['pos'] == '名詞') |\
(keitaiso['pos'] == '動詞') |\
(keitaiso['pos'] == '形容詞') :
if not keitaiso['base'] == '*' :
word_list.append(keitaiso['base'])
else:
word_list.append(keitaiso['surface'])
return word_list
MecabとNEologdを使って、各歌詞を形態素解析し、名詞・動詞・形容詞のみ抽出します。
品詞の原型が存在する場合は原型を、存在しない場合はその単語をそのまま抽出します。
#アルバム単位で歌詞を結合する
lyrics = np.array( [] )
for cd_number in artist_df.CD_Number.unique():
album = artist_df[artist_df.CD_Number == cd_number].copy()
lyrics = np.append(lyrics, ' '.join(get_word_list(album.Lyric.tolist())))
#TF-IDFでベクトル化する
vectorizer = TfidfVectorizer(use_idf=True, token_pattern=u'(?u)\\b\\w+\\b')
vecs = vectorizer.fit_transform(lyrics)
words_vectornumber = {}
for k,v in sorted(vectorizer.vocabulary_.items(), key=lambda x:x[1]):
words_vectornumber[v] = k
#各アルバムの各単語のスコアリングをDataFrameにする
vecs_array = vecs.toarray()
albums = []
for vec in vecs_array:
words_album = []
vector_album = []
for i in vec.nonzero()[0]:
words_album.append(words_vectornumber[i])
vector_album.append(vec[i])
albums.append(pd.DataFrame({
'words' : words_album,
'vector' : vector_album
}))
scikit-learnのTfidfVectorizerを使って、アルバムごとに単語のベクトルを算出します。
TF-IDFについて詳細はググっていただけると良いのですが、ざっくり言うと、
アルバムごとに、「あるアルバムを特徴づける単語」が何なのかを数値化するために今回は用いています。
どうでもいいんですが、Vectorizerってちょっと時雨みがありますね。
3. WordCloudで可視化
いよいよWordCloudを使って可視化します。
今回は下記のライブラリを用いてWordCloudを描画します。
https://github.com/amueller/word_cloud
def draw_wordcloud(df,col_name_noun,col_name_quant,fig_title,masking=True):
word_freq_dict = {}
stop_words = set(['いる','する','れる','てる','なる','られる','よう','の','いく','ん','せる','いい','ない','ある','しまう','・','さ'])
for i, v in df.iterrows():
if v[col_name_noun] not in stop_words:
word_freq_dict[v[col_name_noun]] = v[col_name_quant]
from wordcloud import WordCloud
#text = ' '.join(words)
if masking:
tele_mask = np.array(Image.open('/Users/hogehogehogehoge/telecaster.png'))
else:
tele_mask = None
wordcloud = WordCloud(background_color='white',
font_path = '/hogehoge/Fonts/ヒラギノ角ゴシック W3.ttc',
mask=tele_mask,
min_font_size=15,
max_font_size=200,
width=1000,
height=1000
)
wordcloud.generate_from_frequencies(word_freq_dict)
plt.figure(figsize=[20,20])
plt.imshow(wordcloud,interpolation='bilinear')
plt.axis("off")
plt.title(fig_title,fontsize=25)
3.1 まず、全曲の出現単語をWordCloudする
一番最初に記載した、すべての曲の歌詞を使ってWordCloudを書きます。
これは、アルバムは関係なく全曲の歌詞から単語を抽出し、頻出語が大きくなるように描いています。
また、下記フリー素材サイトさんから、ギターのシルエット画像を入手し、マスキングしています。
https://www.silhouette-ac.com/
word_list = get_word_list(artist_df.Lyric.tolist())
word_freq = pd.Series(word_list).value_counts() #pandasのSeriesに変換してvalue_counts()
words_df = pd.DataFrame({'noun' : word_freq.index,
'noun_count' : word_freq.tolist()})
draw_wordcloud(words_df,'noun','noun_count','all songs',True)
「君」「僕」が頻出していることがよく分かります。
「刺す」とか「殺す」とかの物騒な言葉は意外と少なく(初期は多かったように思っていた)、「消える」や「見える」、「世界」「記憶」「幻」などふわっとした言葉が多いんですね。
結構、肌感覚と合った結果が得られました。
たのしい。
3.2 アルバムごとにWordCloudを描く
本題です。
for i,album in enumerate(albums):
fig_title = cd_num_name_dict[artist_df.CD_Number.unique().tolist()[i]]
draw_wordcloud(album,'words','vector',fig_title,True)
先程は単純に単語の出現頻度が言葉の大きさにそのまま反映されるように描画しましたが、今回はアルバムごとにTF-IDFのスコアリングが高い単語が大きく描画されるようにしました。
では、時系列ごとに見ていきましょう。
※上記コードを実行するとシングルも含めてすべてのCD単位で描画されますが、下記ではアルバム・ミニアルバムのみ載せます。
#4 (2005年)
「ナイフ」「殺人」「切り裂く」など物騒なワードが散見されますね。また、「君」だけではなく「キミ」が強いですね。
(本当は正規化すべきかもしれないが、時雨の歌詞におけるキミと君の役割は異なる、という仮説からそのままにしています)
Feeling your UFO (2006年)
Inspiration is DEAD (2007年)
君と僕がかなり大きいです。リズム、揺れる、など音楽への言及が他アルバムと比較して多いように思います。
just A moment (2009年)
phase、know、plusなど英単語が多様化してきています。
still a Sigure virgin? (2010年)
君、僕に加えて「今」が台頭してきました。また、「宇宙」がこれまでで一番大きくなっているのも気になりますね。
i'mperfect (2013年)
「sitai」が大きいのは「sitai miss me」で初めて出てきたワードだからでしょうか。他のアルバムではあまり聞き覚えがなかったので新鮮だったことを思い出しました。
また、firmなどカメラ用語がこの頃から出てきていますね。
es or s (2015年)
このアルバムでは君、僕がとても小さいです。karmaやplastic、fake、悲しみなどややダウナーなワードが並びます。
#5 (2018年)
曲全体の雰囲気としてはかなり原点回帰色が強い今作(とてもよい・・・)ですが、ワードとしてはheart、minorityなど真新しい語句も見受けられます。分館とは・・・?
また、僕よりも君の方が大きくでていることも注目すべき点でしょう。
4. まとめ・展望
以上、凛として時雨のアルバムごとの特徴をWordCloudで可視化しました。
正直に言うと、僕自身が普段音楽を聞くとき歌詞の意味を考えながら聴くことはほぼなくて、刹那的フレーズのかっこよさくらいしか聴いていないので、考察が極めて稚拙になってしまいました。
しかしながら、時系列ごとに見ていくとTKの中で表現したい言葉の移り変わりがなんとなく見えたような気がします。
今後の展望としては、せっかくvectorizeできるようになったので、アルバムごと、あるいは曲ごとにtfidf vectorizeしてk-meansとかでクラスタリングしてみても楽しいかもしれません。気が向いたら今度やります。
以上です。ありがとうございました。