ヨルシカとはコンポーザーの”n-buna”がボーカル”suis”を迎えて結成したバンドです。コンポーザーのn-bunaは2012年から自身の曲をオンライン上に公開しており、私も中学生の頃から彼の音楽を聴き続けてきています。
n-bunaは2017年からボーカルのsuisと共にユニットとしてヨルシカを結成。2019年には1st、2ndアルバムとして「だから僕は音楽を辞めた」と「エルマ」を立て続けにリリースしました。「だから僕は音楽を辞めた」では、音楽を辞めることになった青年"エイミー"の詩を基にした楽曲14曲を収録され、「エルマ」では、前作の主人公の青年から送られてきた手紙に影響を受けたエルマが手掛けた楽曲14曲が収録されています。
今回は、この2つのアルバムについてその歌詞を自然言語処理を用いて色々と分析をしてみましたので、その過程や結果をここにまとめます。
分析は以下の手順で行いました。
- データの取得
- データの前処理
- 機械学習を用いたモデリング
0.物語の背景知識
分析に入る前に、このアルバムの世界観について説明します(おそらく、背景理解がないと分析内容を理解するのはかなり厳しいかと思います)。物語の登場人物は天才音楽家のエイミーと彼の才覚に畏敬の念を抱く凡人エルマの2人。ある時エルマはエイミーの前から姿を消してしまい、エルマは彼の後を追って北欧に旅立つという物語です。物語は前半と後半に分かれており、アルバム「だから僕は音楽を辞めた」に収録されている楽曲はエイミーの書き連ねた詩を元にエルマがそれを音楽としたもの。後半の「エルマ」では、エルマが自身の旅の中で書き連ねた詩を自分自身で音楽にしたものが収録されています。
1.データの取得
データの取得には、webスクレイピングを用いて、https://www.uta-net.com/lyricist/12614/ のページから歌詞データを取得しました (2020年11月27日時点で全52曲のデータを取得しています)。
取得したデータはpandasを用いて以下の様に表データとして保持します (著作権諸々の話が恐いので、今回は具体的なコードの掲載は控えさせていただきます。また、下図において歌詞の部分は伏せてあります)。
また、今回はエルマとエイミーに着目した分析を行うため、この2人が登場するアルバム「だから僕は音楽を辞めた」と「エルマ」以外の歌詞データは分析の対象から除外します。
2. データの前処理
さて、このままでは分析が行えないためデータの前処理を行う必要があります。なぜ前処理を行う必要があるのかというと、例えば、ある文書からその文書における重要な用語を検出する際にはその後の出現頻度が重要度を決める大きな要因となります。しかしながら、前処理を行わないと出現頻度を巧く算出できません。
例えば、「すもももももももものうち」という早口言葉があります。この文章において、「すもも」は1回、「もも」は2回登場するが、接続詞の「も」も2回登場する。この文章における重要語は明らかに「すもも」や「もも」です(なぜなら、この文書では李や桃の話をしているからだ!)。しかし、単純に頻度に着目してしまうと桃と接続詞の「も」の重要度が同じになってしまう(そんなはずはない!)。
また別の例を挙げると、市販の受験用英単語帳には"入試での出題頻度が高い単語を集めました!"みたいなうたい文句が掲載されている場合がありますが、真に出題頻度の高いaやanやtheが単語として登録されていません。
このように、前処理を行わないと文章の意味に寄与しない(もしくは寄与レベルが低い)単語がモデルに大きな影響を与えてしまうことがあり、精度の高いモデルを作ることができなくなってしまいます。そのため、文章の意味に関与しない後はあらかじめ前処理として分析対象から省いておく必要があります。今回は、名詞、動詞、形容詞、代名詞、形状詞-一般のみを分析対象としました。(文書の内容によっては代名詞や形状詞も含めない場合もありますが、今回は「君」や「僕」など、歌詞に代名詞が多発することが予想されるため、代名詞もモデルに組み込んでいます)。
また、今回は特徴後である、マシンガン、神様、蜃気楼などはMeCabのユーザー辞書に別途登録を行い、強制抽出を行っております (強制抽出を行わない場合は、マシン+ガン、神+様、蜃気+楼などと言葉の最小単位で分割されてしまいます)。
さて、文意への寄与度の低い助詞や助動詞を省くためには、歌詞の品詞分解を行い、各単語の品詞を特定する必要があります。この品詞分解は日本語には語の区切りを明示する記号が無い分、単語間にスペースを挟む英語などの言語と比べてやや難しいです(この品詞分解する作業を形態素解析と呼びます)。しかし、工藤拓さんによって開発されたMecabという最高にクールな形態素解析エンジンがあるのでそれを用いると簡単に分類を行うことができます!
このMecabでは以下のことを行ってくれます
・単語への分かち書き (tokenization)
・活用語処理 (stemming, lemmatization)
・品詞同定 (part-of-speech tagging)
実際には、MeCabを用いて品詞分解を行い不要語を削除する関数を定義し、その関数を入手したデータに用いるという形で以下の様にコードを書きました。
# MeCabをインポートします
import MeCab
# MeCabを使って処理を行う関数を定義します
def preprocessing_by_mecab(text):
tagger = MeCab.Tagger('/usr/local/lib/mecab/dic/mecab-ipadic-neologd/')
tagger.parse('')
node = tagger.parseToNode(text)
word_list = []
#while文で各単語が指定した品詞の場合だけリストに入れます
while node:
pos0 = node.feature.split(",")[0]
pos1 = node.feature.split(",")[1]
if pos0 in ['名詞', '動詞', '形容詞', '代名詞']:
word = node.surface
word_list.append(word)
if pos0 in ['形状詞']:
if pos1 in ['一般']:
word = node.surface
word_list.append(word)
node = node.next
#単語間にスペースを入れます
return ' '.join(word_list)
# 定義した関数を入手した歌詞に実行し、品詞分解を行い不要語を削除します
df['前処理済み'] = df['歌詞'].apply(lambda x:preprocessing_by_mecab(x))
これで、先ほどの表データの一番右に「前処理済み」という列が追加され、前処理の行われたしたデータが格納されました。
最後に、これらの単語をリストに格納して前処理は完了です。
# 空白を除去する関数定義
def remove_non_word(lyric):
return [word for word in lyric if len(word)]
# 「ストップワード除去済み」列のデータをとりあえずリストに入れてしまう
lyrics = list(df['ストップワード除去済'])
# 単語間の間に空白が入っているが、これを空白毎に分割してリストに格納する
lyrics = [liryc.split(' ') for liryc in lyrics]
# 先ほど定義した関数を用いて空白を除く
lyrics = [remove_non_word(liryc) for liryc in lyrics]
3. 機械学習を用いたモデリング
さて、ここから実際に分析を進めていきます。今回はアルバムに収められている歌詞を各単語ごとにベクトル化し、ある単語のベクトルと類似のベクトルを持つ単語を見つけることで類似語を検知したり、あるベクトル同士を足し合わせたり弾いたりしたベクトルと類似のベクトルを持つ単語を見つけることで"意味の足し算・引き算"を行った結果どのような言葉が見つかるのかを見ていきます。(例えば、ヨルシカの歌詞における、エルマというベクトルと似ているベクトルを検知すれば、エルマがいったい何者なのかの手掛かりになります。)
今回、単語ベクトルの学習においてはword2vecというモデルを用いました。word2vecとはGoogleのTomas Mikolovらによって提案されたニューラルネットワークの手法です。word to vectorというその名の通り、単語をベクトルにするのに役立つモデルであり、これを使えば細かい理論については理解していなくてもモデルを作ることができます(勿論、理解した方が良い……)。詳しい理論は難しいのでここでの説明は一旦割愛しますが、開発者のTomas Mikolovらの下記の論文を参考としてリンクを張っておきますので、興味のある方は読んでみてください。
・Distributed Representations of Words and Phrases and their Compositionality
・Efficient Estimation of Word Representations in Vector Space
英語だし論文を読むのはちょっと難しそう……という方は、以下の記事が分かりやすいです。
・絵で理解するWord2vecの仕組み
さて、それでは実際にモデルを作りましょう。word2vecでモデルを作成する際にはいくつかパラメータを指定するのですが、今回はsize(ベクトルの次元数)=45, min_count(n回以上登場した単語のみを考慮する)=2, window(ウィンドウ幅)=5, iter(学習の回数)=500でやってみました。何故このくらいにしたのかというと、まあ色んな数字で試行錯誤してみた結果ですね。
# gensimをインポート
import gensim
# word2vecを用いてモデル化 (ベクトルの次元は45、最小で2回以上登場する単語を対象、iterは500回)
model = gensim.models.word2vec.Word2Vec(lyrics, size=45, min_count=2, window=5, iter=500, seed=10)
これで無事モデルが完成しました! (コードはたんとたった1行!)
類似度は、ある単語とある単語とのそれぞれのベクトルが近ければ近いほど類似度が高いと判断できます。ベクトルの類似度をどう計算するかはコサイン類似度という方法を用います。高校2年生のベクトルの知識があれば理解できるかと思いますが、忘れてしまった方は以下の図をご参照下さい。
とりあえず、類似度を計算し、ある単語と類似度の高い単語を列強する関数を定義し、色々なワードを入れて類似語を見ていきましょう。
# 類似度の高い単語を抽出する関数を定義
def similor_words(model, word):
results = model.wv.most_similar(positive=word, topn=10)
df_result = pd.DataFrame(results, columns=['単語', '類似度'])
return df_result
それでは、エルマ、エイミーそれぞれのベクトルと類似度が高いベクトルを持つ語を見ていきましょう。下図の左側がエルマ、右側がエイミーの結果です。
similor_words(model, 'エルマ')
similor_words(model, 'エイミー')
分析において重要なのはその結果よりもむしろその解釈・意味合いです。まずは、エルマの方から見ていきましょう。基本的に「エルマ」という語はエイミーが作詞した曲にしか出てこないため、ここから読み解けるのは、"エイミーにとっての"エルマの情報になります。
さて、特徴的なのは「暮れ」「字」「さよなら」と、何かの終わりを指す言葉が多く出てきている点です。「暮れ」は1日の終わりを指し、「さよなら」は別れの挨拶です。「字」は一見分かりにくいですが、これらのアルバムにおいて「字」は「あと80字」「この歌の歌詞は360字」などと残り期間を示す際に使われる言葉になっています。では、何の終わりかというと、「エルマ」の最終曲「ノーチラス」のMVおいてエイミーは最後に毒である花緑青を飲み海に身を投げてその人生を終えたことが映されているため、エルマの人生の終盤と捉えて差し支えないでしょう。エイミーは人生の終盤にエルマに思いを馳せながらその命を絶ったことは想像に難くありません。エイミーが旅の途中に書いた詩を、彼の後を追いかけ北欧を旅したエルマが楽曲にしたという背景を考えると、「だから僕は音楽を辞めた」の詩は、人生の終盤に差し掛かったエイミーがエルマを思って書き連ねた詩であるということが自然言語処理の観点からも確認できるのではないかと思います。エイミーはエルマが「神様」だととらえるほどの偉大な作曲家でありながら、死を目前にしてその心はずっと一般人であるエルマに向けられていたのは興味深いです。
一方で、エイミーについてはエルマよりもずっと厳しいです。これ分析を進めていて途中で気づいたのですが、両アルバムにおいてエイミーが登場する歌というのが非常に少ないです(元々は最低3回以上登場する単語のみでモデルを作成しようとしましたが、エイミーが2回しか登場しないので最低登場回数を2回以上に変更しました)。
さて、一方でエルマにとってのエイミーですが、その生き方を真似するほどに心の穴を開けられた存在であったということが、エイミーの類似語から見て取れます。ただ、やはりエイミーに関してはモデル化するに足る十分なデータ量があったとはいえず、この物語においてエイミーの存在は脇役にすぎなかったのではないかと思わせられます。
前半がエイミーの作った詩、後半がエルマの書いた詩と、2人の重要度は半々であるかのように見せかけておきながら、結局楽曲にしたのはエルマですし、結局この物語は最初から最後まで天才の後を追う凡人エルマの物語であったのかもしれません。
4. おまけ (n-bunaの楽曲全てを使ってモデルを作成してみた)
今回は「だから僕は音楽を辞めた」と「エルマ」に収録されている曲からモデルを作りましたが、同様の手法によりn-bunaが作詞を行った曲すべてでモデルを作っていました。word2vecならぬn-buna2vecです。(スクレイピングで取得できなかった非商業作品の歌詞は含めておりません)
# ベクトルの次元は90、最小で3回以上登場する単語を対象、iterは250回
model = gensim.models.word2vec.Word2Vec(lyrics, size=90, min_count=3, window=5, iter=250, seed=10)
ここで、歌詞に頻出する"夏"と"歌"の類似度の高い語についてみてみましょう。(左が"夏"、右が"歌"です)
夏に関しては"匂い""想い出"など情景を表す語が多いです。夏は"頬に残る"という表現も印象的です。
5. おまけ2 (n-bunaの楽曲に使われる単語の頻度)
前処理をし、ストップワードを除いた歌詞に使われている高頻度の単語(トップ20単語)は以下の様になりました。x軸が単語で、y軸が登場回数を表しています。左が今回取り上げたアルバムにおける頻度分布で、右のグラフがn-bunaが作詞した歌詞全体の頻度分布です。
コードは以下です。
# Counterを用いて、"ストップワード除去済"列に入っている単語をカウントする
from collections import Counter
text = ' '.join(df["ストップワード除去済"]) #各曲を長い一つのリストに統合
words = [w for w in text.split(" ")] #textをスペース区切りでリストに格納
word_count = Counter(words)
# 描画を行うmatplotlibは日本語に対応していないので、日本語表示が可能なフォントを指定
import os
os.chdir("C:\Windows\Fonts")
import matplotlib
from matplotlib.font_manager import FontProperties
font_path = './TakaoPGothic.ttf'
font_prop = FontProperties(fname=font_path)
import matplotlib.pyplot as plt
%matplotlib inline
# pandasを用いて表データに
df_count = pd.DataFrame.from_dict(word_count,orient='index').reset_index()
df_count.columns=["単語","頻度"]
df_count.sort_values("頻度",ascending=False,inplace=True)
df_count=df_count.iloc[1:,:]
# matplotlibを用いてグラフ化
plt.figure()
ax=df_count.iloc[:20,:].plot.bar()
ax.set_ylabel("Frequency")
ax.set_xticklabels(df_count.iloc[:20,:]["単語"],fontdict = {"fontproperties": font_prop})
ax.legend_.remove()
plt.show()
6. おわりに
今回は、word2vecを用いて歌謡曲の歌詞の分析を行ってみました。以下、反省点と、今後やってみたいことです。
反省点
「エルマ」や「エイミー」と、直接的に名前を明示する単語の出現頻度は意外なほど低く、モデルとしてまわすには不適切だったかなと思います(本来であれば事前に頻度を確認しておくべきでした)。彼らは「僕」や「君」と代名詞で呼ばれていたので、「だから僕は音楽を辞めた」と「エルマ」で母集団を分割してモデルを作り、その中から「君」、「僕」のベクトルを見るべきだったかなと思います。
今後やりたいこと
今後は別のアーティスト同士の比較や、和歌のベクトル化などもやってみたいです。あとは、純粋な頻度だけではなく、TF-IDF法などを用いて重みづけを行った単語の頻度とかも見てみたいですね。また、今回はベクトル同士の足し算引き算については結果を乗せていない(例えば"僕"-"音楽"など)ので、それに関しての意味合いもどこかで書ければなと思います。
さいごに
筆者は大学でコンピュータサイエンスを勉強したわけでもなければ職業エンジニアでもなく、仕事では1行もコードを書かないような人なので、不慣れなところもあり分析内容などの間違いがあるかもしれません。もし、何かお気づきの点や感想などありましたらコメントなどに書き込んでいただけると助かります。
(これはwordcloudでn-bunaのアイコンに頻出単語を詰め込んだ図)