【自然言語処理】乃木坂46は10年間何を歌ってきたのか【歌詞分析】
乃木坂46の結成10周年を記念して(?)、ここまでの全楽曲の歌詞を自然言語処理的なアプローチで分析してみる。
分析といっても個人的に使ってみたかった手法を適用してみる題材として歌詞のテキストデータを使おう、というところから始まっているので、その結果に対して分析的な解釈は与えられていないかもしれない。
したがって、タイトル負けというか「何を歌ってきたか」に対して解を与える内容になっていないかもしれないということは悪しからず。
歌詞のテキストデータは歌詞サイトからスクレイピングしてきた。
1つディレクトリを作成して曲ごとにtxtで保存する。
※スクレイピングした歌詞は著作権のあるものなので私的な情報解析目的にとどめる
※スクレイピング対象サイトに過度な負荷をかけないようにアクセス間隔を数秒空ける
work_dir/ ┗ nogizaka46_lyrics_text/ ┣ 13日の金曜日.txt ┣ 2度目のキスから.txt ┣ 4番目の光.txt ┣ Against.txt ┣ ...
スクレイピングに使ったコードは載せない。
28thシングル「君に叱られた」までのソロ曲を除く全206楽曲の歌詞データを用意した。
Word Cloudをつくる
まずは基本的なアプローチとしてWord Cloudを作ってみる。
Janomeで形態素解析して名詞、動詞、連体詞、形容詞、副詞、感動詞のみ分かち書きにし、適当にstop wordを指定して描画する。
効果があるかは別として、一応最新のNEologdの辞書を参照するようにしている。
# coding: utf-8 import os from janome.tokenizer import Tokenizer from janome.analyzer import Analyzer from janome.charfilter import * from janome.tokenfilter import * from wordcloud import WordCloud char_filters = [UnicodeNormalizeCharFilter(), RegexReplaceCharFilter('<.*?>', '')] keep_pos = ['名詞', '動詞', '連体詞', '形容詞', '副詞', '感動詞'] token_filters = [POSKeepFilter(keep_pos), LowerCaseFilter(), ExtractAttributeFilter('base_form')] a = Analyzer(char_filters=char_filters, token_filters=token_filters) lyrics_dir = 'nogizaka46_lyrics_text' all_tokens = list() for file_name in os.listdir(lyrics_dir): with open(os.path.join(lyrics_dir, file_name), 'r', encoding='utf-8') as f: lyrics = f.read().strip() tokens = [token for token in a.analyze(lyrics)] all_tokens.extend(tokens) font_file = 'NotoSansCJKjp-Bold.otf' stop_words = [ 'もの', 'こと', 'とき', 'そう', 'たち', 'これ', 'よう', 'これら', 'それ', 'ん', 'てる', 'いる', 'ある', 'する', 'の', 'さ', 'れる', 'せる', 'なる', 'ない', 'その'] word_chain = ' '.join(all_tokens) wordcloud = WordCloud(background_color='white', font_path=font_file, width=900, height=500, max_words=1000, stopwords=set(stop_words_ja)).generate(word_chain) wordcloud.to_file('nogizaka46_wordcloud.png')
なんとなく「君」と「僕」と「誰」かの「今」の「何」かを歌ってそう。
ただ「君」「僕」「誰」「何」あたりはポップソングならどんな曲にでもよく出てくる単語。
特に「君」「僕」あたりはアイドルソングには頻出であろう。
というわけで、これらの目立つ単語をstop wordに追加して再実行してみる。
stop_words += ['僕', '君', '何', '今', '誰', '自分', 'wow', 'いい', 'どこ']
やはりアイドルソングらしく「愛」とか「恋」を歌っているようだ。
また、さきほどより「私」が目立つようになった。「あなた」もある。
一人称・二人称が「僕・君」が多い一方、「私・あなた」のときもあるようだ。
曲の意味や文脈によって変わるのだろうか。
乃木坂の曲の歌詞は語り手が女性目線のものと男性目線のものがはっきり分かれるものもあるので、それにもよるかもしれない。
「生きる」という単語が浮かび上がっているのも特徴的かもしれない。 使われているのは例えば、
「人生を考えたくなる」
これから何をする? どう生きて行くか? しあわせを考えてしまう
とか、
「命は美しい」
何のために生きるのか?何度問いかけてはみても暗闇が黙り込む
あたりだろうか。
女性アイドルグループとしてこんなに「生きる」について歌っているのは珍しいのではないだろうか。
感情ポジネガ分析
次に曲ごとにどんな感情を歌っているのか、ポジティブ/ネガティブの度合いを見てみる。
ここではGoogle CloudのNatural Language APIを使う。
Natural Language APIの感情分析APIではテキストの意味する内容や背後にある感情をscoreとして-1.0~1.0の範囲で数値化できる。
これを使って各曲のscoreがどのくらいなのか、また全体としてのscoreの分布がどうなっているのかを可視化してみる。
Natural Language APIのPythonクライアントをインストールして、GCPでサービスアカウントキーを発行しておく。
pip install google-cloud-language
特にテキストの前処理はせず、1曲ずつAPIに投げて結果を辞書形式で一時保存してDataFrameに読み込む。
# coding: utf-8 import os import re import json import pandas as pd from google.oauth2 import service_account from google.cloud import language_v1 as language client = language.LanguageServiceClient() lyrics_dir = 'nogizaka46_lyrics_text' sentiments = list() for file_name in os.listdir(lyrics_dir): song_name = lyrics.replace('.txt', '') with open(os.path.join(lyrics_dir, file_name), 'r') as f: lyrics = f.read().strip() document = language.Document(content=lyrics, type_=language.Document.Type.PLAIN_TEXT) result = client.analyze_sentiment(request={'document': document}) sentiment = result.document_sentiment sentiments.append({'name': song_name, 'sentiment': {'score': sentiment.score, 'magnitude': sentiment.magnitude}, 'lyrics': lyrics}) sentiment_df = pd.json_normalize(sentiments)
scoreの分布をbox plotに出力した上で、scoreに基づく位置に曲名を重ねてみる。
import matplotlib.pyplot as plt from matplotlib.font_manager import FontProperties from adjustText import adjust_text import seaborn as sns plt.figure(figsize=(20,12)) sns.set(palette='Blues') sns.boxplot(x=sentiment_df['sentiment.score'] , palette='Blues') fp = FontProperties(fname='NotoSansCJKjp-Regular.otf', size=11) annotate = [plt.text(score, 0, name, fontproperties=fp) for name, score in zip(sentiment_df['name'], sentiment_df['sentiment.score'])] adjust_text(annotate, autoalign='y', only_move={'points':'y', 'text':'y'}) plt.savefig('nogizaka46_sentiment')
(なるべく曲名が重ならないようにしているが、いかんせん曲数が多いので見づらい...)
左側(負方向)がネガティブな曲、右側(正方向)がポジティブな曲となる。
「4番目の光」「人間という楽器」「毎日がBrand new day」などポジティブっぽい歌詞の曲が右側に来ていたり、
「不眠症」「嫉妬の権利」などネガティブっぽい歌詞の曲が左側に来ているのが見て取れる。
一方、「口ほどにもないkiss」がそれほどnegative scoreか?とか思ったりもする。
「僕は僕を好きになる」は最終的には自分を好きになるけど、途中に結構ネガティブなセンテンスが多いためか結構negative scoreになっている。
box plotを見ると全体の分布としては多少ネガティブ側に寄っていることがわかる。
乃木坂はネガティブなセンテンスを歌いがち、なのかもしれない。
先のWord Cloudでも生き方や人生についての詞という側面が見えたが、やはりどこか若者的な人生や恋愛における葛藤を歌っていたり、内省的な歌詞の曲が多いような気はする。
BERTクラスタリング
次は曲を歌詞の意味的なまとまりにクラスタリングしてみたい。
日本語BERTの学習済みモデルを使って曲ごとに歌詞の文章の多次元分散表現を得て、これを平面にマッピングしてみる。
少し実装が手間になるがやっていく。
今回は今回は京都大学の黒橋・褚・河原研究室が作成し公開しているBERT日本語Pretrainedモデルを利用させていただく。 nlp.ist.i.kyoto-u.ac.jp
形態素解析器Juman++をインストールしておく。
最新版をwgetしてきて解凍し、ビルドする。
wget http://lotus.kuee.kyoto-u.ac.jp/nl-resource/jumanpp/jumanpp-1.02.tar.xz tar Jxfv jumanpp-1.02.tar.xz cd jumanpp-1.02.tar.xz ./configure make sudo make install
また、PythonでJumanを利用するためのPyKNPと、PyTorch、transformersをインストールしておく。
pip install pyknp pip install torch pip install transformers
学習済みモデルは最新版かついちばんサイズが大きく語彙が大きいものをダウンロードし解凍して使う。
Japanese_L-24_H-1024_A-16_E-30_BPE_WWM_transformers.zip
unzip Japanese_L-24_H-1024_A-16_E-30_BPE_WWM_transformers.zip
1曲ずつ埋め込んでいき、得られたNumpyのベクトルをnpyファイルに一時保存する。
このLargeのモデルでは入力したテキストを1024次元の分散表現として埋め込むことができる。
# coding: utf-8 import os import numpy as np import torch from transformers import BertTokenizer, BertModel from pyknp import Juman bert_path = 'Japanese_L-24_H-1024_A-16_E-30_BPE_WWM_transformers' vocab_file_name='vocab.txt' bert_model = BertModel.from_pretrained(bert_path) bert_tokenizer = BertTokenizer(os.path.join(bert_path, vocab_file_name), do_lower_case=False, do_basic_tokenize=False) def juman_tokenize(text): result = Juman(command='jumanpp').analysis(text) return [mrph.midasi for mrph in result.mrph_list()] def embed_lyrics(text): text = text.replace(' ', '') tokens = juman_tokenize(text) bert_tokens = bert_tokenizer.tokenize(' '.join(tokens)) ids = bert_tokenizer.convert_tokens_to_ids(['[CLS]'] + bert_tokens[:126] + ['[SEP]']) tokens_tensor = torch.tensor(ids).reshape(1, -1) bert_model.eval() with torch.no_grad(): layers, _ = bert_model(tokens_tensor) embedding = layers[-2].cpu().numpy()[0] return np.mean(embedding, axis=0) lyrics_dir = 'nogizaka46_lyrics_text' for file_name in os.listdir(lyrics_dir): song_name = file_name.replace('.txt', '') with open(os.path.join(lyrics_dir, file_name), 'r', encoding='utf-8') as f: text = f.read().strip() vector = embed_lyrics(text) np.save(os.path.join('nogizaka46_lyrics_vector', song_name, vector)
保存した埋め込みベクトルのnpyファイルをNumpyのarrayに読み込んでいく。
この時この後の可視化用にファイル名から曲名の配列を作っておく。
import os import numpy as np lyrics_vec_dir = 'nogizaka46_lyrics_vector' song_names = list() lyrics_vector = np.empty((0, 1024)) for file_name in os.listdir(lyrics_dir): song_name = file_name.replace('.npy', '') lyrics_vector = np.append(data, np.array([np.load(os.path.join(lyrics_dir, file_name))]), axis=0) song_names.append(song_name)
埋め込みベクトルは1024次元になっているが、これを2次元に削減してscatter plotしたい。
次元削減にはUMAPを使う。t-SNEよりも高速で的確に削減できる傾向にある。
pip install umap-learn
目標次元数n_components=2
、距離関数metric='euclidean'
を指定しあとのパラメータは適当に調整した。
パラメータの合わせ方のTipsもあるのかもしれないが、ベクトルのサイズなどからあたりを付け、結果を見ながら微調整した。
import umap fit = umap.UMAP( n_neighbors=3, min_dist=0.19, spread= 1.9, n_epochs=100, n_components=2, metric='euclidean', random_state=42) u = fit.fit_transform(lyrics_vector)
インタラクティブな可視化がしたかったため、Plotlyを使って描画する。
pip install plotly
削減後の埋め込みベクトルをもとに平面にscatter plotする。
曲名をラベルすることでマウスオーバーで描画した点に対応する曲名が出てくるようにする。
(直接曲名をplotすると重なりすぎて見えなくなる未来が見えたため)
import plotly.graph_objs as go trace = go.Scatter( x=u[:, 0], y=u[:, 1], mode='markers', marker={ 'size': 10, 'opacity': 0.8, }, text=song_names) data = [trace] go.Figure(data).show()
結果は図のようになった。
インタラクティブに見れるようにHTML出力してみた。
Nogizaka46 Lyrics 2-dim Embeddings - Interactive Scatter Plot
結論から言うと、あんまり意味的なまとまりを見出すことはできなかった。
なんとなく「別れ」とか「うまくいかない恋」を歌ったような曲が近くにまとまっている感じも見受けられる。(そもそもそういう曲がちょっと多い?)
また、同じあるいは似た意味の単語を含む曲が近くにきているのも散見される。
ただ、詞の要旨でまとまりができて「何を歌っているのか」が明確に分けられるほどではなかった。
多様な言葉を多様な使い方で組み込んでおり、いろいろな心情や状況を歌ってきたのだろう。
こうしてみてみると、乃木坂楽曲は(秋元康の詞がというべきかもしれないが)心の中の思いをそのまま文章にしたようなものや、目に映る風景を事細かに言語化してところどころにちりばめて心情を強調するような、極めて「詩的」な文章表現が多い。
そのため、比喩としての用法も含めて多種多様な単語が混ざっており、言葉尻だけで詞の意味を捉えるのは確かに難しいなと思った。
まとめ
乃木坂楽曲は、
- 女性アイドルらしく恋とか愛を歌ったものが多い
- 一方、人生・生き方についての若者的な葛藤を歌っていたり内省的なものが目立つ
- 全体としてネガティブな心情や状況を交えて歌っているものが多い
といったことが分析と改めて詞をよく読んでみた結果浮かび上がってきた。
今回は、Google Cloud Natural Language APIと日本語BERTの文章埋め込みを試せたので満足。
アーティストごとの比較とかをやってみるともっと興味深い違いが見出せたりもするのかもしれない。
それはまた別の機会に。