ハローラスク

時間から自分を無理に引き離すこのような安定性あるいは確実性に到達するためには、時間が必要である。時間なしですませるためには、時間が必要である。時間のなかで時間を従わせ、みずから時間に従わなければならない。

tf-idfでアイドルマスターシンデレラガールズの楽曲の歌詞を分析してみる

はじめに

大学の授業でtf-idfというのを知ったので, それに関するメモと, 実際のデータを使った応用例を書きます.

BoWについて

tf-idfは主に文書データに対して用いられる手法です. そこで, tf-idfの話に入る前に, そもそも文書をデータ化するとはどういうことか?を説明します.
BoW(Bag of Words)は文書をベクトルとして表現するモデルです. これは非常に単純で,

  • 文書に登場する全ての語彙を  T_1, T_2, ... , T_n として, 文書中に含まれる T_i の個数が  i 番目の要素の値になるようなベクトルを文書ごとに作る

というものです. 例えば

  • A: "This is a pen."
  • B: "He is a teacher."

の場合を考えてみましょう. 出てくる語彙はThis, is, a, pen, He, teacherの6つです. AはThis, is, a, pen, He, teacherをそれぞれ1個, 1個, 1個, 1個, 0個, 0個含むので, BoWは  (1, 1, 1, 1, 0, 0) となります. 同様にBのBoWは  (0, 1, 1, 0, 1, 1) となります.
このように, BoWは単語の順番を無視した素朴なモデルなので, 文書の細かい意味を判別するのは難しいですが, 文書のトピックが何かを類推する程度の実用性はあります. また, BoWは文書に限らず, ある人の行動履歴や, サイトの閲覧履歴のデータなど, 幅広く応用することができます.

なぜtf-idfが必要なのか?

このBoWですが, そのままの状態だと分析に使う上ではある欠点があります.
例えば機械学習で論文をリンゴに関する論文とゴリラに関する論文とラッパに関する論文に分けることを考えましょう. 当然, BoWを作ると, リンゴに関する論文のベクトルは"apple"の項の数値が大きく, ゴリラに関する論文のベクトルは"gorilla"の項の数値が大きく, ラッパに関する論文のベクトルは"trumpet"の項の数値が大きいと推測できます.
ところが, どのベクトルにも"a"や"the"のような定冠詞の項の数値もかなり含まれてしまいます. 機械学習では, 分類に大きな影響を与える項に重みを与えて学習させた方がうまく分類できるので, こういった「どのような文章にもありふれた単語」の存在は邪魔です.
Pythonの機械学習ライブラリscikit-learnのユーザーマニュアルのtf-idfの章の冒頭にも, 以下のような文章が掲載されています.

In a large text corpus, some words will be very present (e.g. “the”, “a”, “is” in English) hence carrying very little meaningful information about the actual contents of the document. If we were to feed the direct count data directly to a classifier those very frequent terms would shadow the frequencies of rarer yet more interesting terms.


そこで用いられるのがtf-idf(term frequency-inverse document frequency)という変換です. これを使うと, 特定の文書にだけ頻繁に現れる単語に大きな重みが与えられ, どの文書にも現れる単語にはあまり重みを与えない, ということができます.
具体的には, 以下のようなことを行います.

  • ベクトルのそれぞれの単語の要素ごとに  tf idf の積を計算する. ただし,  tf は対象の文章中に単語が出てくる回数である. また,  idf は以下の通りである(これとは違う計算方法の流儀もある).
 idf = \log\bigg(\dfrac{N+1}{df+1}\bigg) + 1
 N は全ての文書の個数,  df はその単語が出てくる文書の個数)

  • 全ての単語の要素に対して計算を終えたら, ベクトルをL2正則化する. 平たく言うと, ベクトルを正規化する(大きさを1にする)ということである(これによりそれぞれの文書の長さの違いによる影響を無くすことができる).

実際に例で確認してみましょう.

単語1 単語2 単語3
文書A 3 0 1
文書B 2 0 0
文書C 3 0 0
文書D 4 0 0
文書E 3 2 0
文書F 3 0 2

文書AのBoW (3, 0, 1) をtf-idfで変換してみましょう. 単語1について,

 tf = 3, idf = \log\bigg(\dfrac{6+1}{6+1}\bigg) + 1
 \therefore tf\cdot idf = 3

単語2について,

 tf = 0, idf = \log\bigg(\dfrac{6+1}{1+1}\bigg) + 1
 \therefore tf\cdot idf = 0

単語3について,

 tf = 1, idf = \log\bigg(\dfrac{6+1}{2+1}\bigg) + 1
 \therefore tf\cdot idf \fallingdotseq 1.8473

最後に,  (3, 0, 1.8473) を正規化して,  (0.8515, 0, 0.5243) ということになります.
元の (3, 0, 1) と値を比較してみると, どの文書にもありふれて存在している単語1の値が大きく減っている一方で, 希少性の高い単語3の値がそれほど減っていないことが確認できます.

scikit-learnでtf-idf

Python機械学習ライブラリscikit-learnを使えば, このような計算は自動で行ってくれます.

>>> from sklearn.feature_extraction.text import TfidfTransformer
>>> transformer = TfidfTransformer()
>>> data = [[3, 0, 1], [2, 0, 0], [3, 0, 0], [4, 0, 0], [3, 2, 0], [3, 0, 2]]
>>> transformer.fit_transform(data).toarray()
array([[ 0.85151335,  0.        ,  0.52433293],
       [ 1.        ,  0.        ,  0.        ],
       [ 1.        ,  0.        ,  0.        ],
       [ 1.        ,  0.        ,  0.        ],
       [ 0.55422893,  0.83236428,  0.        ],
       [ 0.63035731,  0.        ,  0.77630514]])

それぞれの単語のtf-idfの最大値もmax(axis=0)でこの通り. なお, ravel()というのは多重配列を一次元の配列にしている以上の意味はありません.

>>> transformer.fit_transform(data).max(axis=0).toarray().ravel()
array([ 1.        ,  0.83236428,  0.77630514])

さらに, 正規化する前のidfのデータだけを取り出すことも簡単にできます(計算方法を見ればわかるのですが, idfは文書に依存せず一定値をとります).

>>> transformer.idf_
array([ 1.        ,  2.25276297,  1.84729786]) 

歌詞におけるtf-idfとは?

 tf はその単語が文書に現れる回数であり,  idf は(だいたい)単語の希少性を示す数値でした. すなわち, ある単語の tf-idf の最大値が大きいということは, その単語が特定の文書にのみ頻繁に登場するということを意味します. ということは, 楽曲の歌詞に現れる単語の中でtf-idfの最大値が大きいものは, その曲固有のキャッチーな内容の単語なのではないか, と推測できます.
一方, idfが小さいということは, 希少性が低い, どの文書にも現れる内容だということを意味します(ここでは「tf-idfの最大値が小さい」とはならないことに注意してください. tf-idf の場合, たとえ現れる文書の種類が多くても, 現れ方に偏りがあれば値は大きくなってしまいます). ということは, 楽曲の歌詞に現れる単語の中でidfが小さいものは, どの曲にも現れる普遍的な内容の単語なのではないか, と推測できます.

というわけで, これをアイドルマスターシンデレラガールズの楽曲で確かめようというのが以下の内容です.

アイドルマスターシンデレラガールズの楽曲の歌詞を分析する

歌詞からBoWを作る

英語と違って日本語は単語間の空白区切りがありません. そこで, まず歌詞を形態素解析するところから始まります. これはMeCabというものを使ってやります. 以下の記事を参考にしてMeCabPythonでも使えるようにします.

qiita.com

MeCabを使うとこんなことができます.

>>> import MeCab
>>> mecab = MeCab.Tagger("-Owakati")
>>> print(mecab.parse("日本国民は、正義と秩序を基調とする国際平和を誠実に希求し、国権の発動たる戦争と、武力による威嚇又は武力の行使は、国際紛争を解決する手段としては、永久にこれを放棄する。"))
日本 国民 は 、 正義 と 秩序 を 基調 と する 国際 平和 を 誠実 に 希求 し 、 国権 の 発動 たる 戦争 と 、 武力 による 威嚇 又は 武力 の 行使 は 、 国際 紛争 を 解決 する 手段 として は 、 永久 に これ を 放棄 する 。

歌詞のデータは, https://ameblo.jp/someokasanlove/entry-12302877232.html を参考にさせていただきました. 2018年4月23日閲覧時点で155曲の歌詞が掲載されていたので, 上から順にコピペして song1.txt, song2.txt, ..., song155.txt と保存させていただきました. ただし『(キャラ名)』のようなパート分けの部分は手動で削除しました.
(ところで, 歌詞を個人ブログに掲載するのは著作権違反だと思っていたのですが, http://www.jasrac.or.jp/info/network/ugc.html によると, アメブロはOKらしいです. )

採取された語彙を見てみると, 歌詞とは言いにくいような記号がたくさん含まれていたので削除することにしました. また, ひらがな1文字, カタカナ1文字は頻度が高い割に意味がないので('て', 'に', 'を', 'は' とか)削除することにしました. そうすると, 5795種類の語彙が残るので, 155曲それぞれについて, 要素数5795のベクトルを作ります.

tf-idf が大きい単語は?

さて, その曲固有のキャッチーな内容の単語だと予想されるtf-idfの最大値が大きい単語100個を取り出してみましょう.

# tf-idf変換する
from sklearn.feature_extraction.text import TfidfTransformer
transformer = TfidfTransformer()
transformer.fit_transform(data).toarray()

# tf-idfが大きいもの100個
sorted_tfidf = transformer.fit_transform(data).max(axis=0).toarray().ravel().argsort()
for i in sorted_tfidf[-1:-101:-1]:
    print("\'{}\'".format(voc[i]), end=" ")

結果は以下の通りになります.
'chu' 'GOIN' 'cherry' 'Chu' 'はい' 'ミンミンミン' 'Tiny' 'Da' 'Jewel' 'メーデー' 'foo' 'ミツボシ' 'Fight' 'Dan' 'ウサミン' 'cha' 'コスモス' '今夜' 'Wing' 'ニャ' 'ding' 'dong' 'ボク' 'きちん' 'こんち' 'スキ' 'ニャン' '夏' 'あん' 'NUDIE' 'NEW' 'WORLD' '走れ' 'きら' 'いつも' 'はじまれ' '先' 'venus' 'Doki' '絶対' 'Fu' 'はにかん' '強く' 'ぼく' '特権' 'Duwa' '命' 'わん' 'きみ' 'Loving' 'of' '新た' 'life' 'one' 'キミ' 'チュ' 'la' 'Come' 'Hello' 'yeah' 'きらり' '流れ星' '花見' 'un' 'パーリラパリラハイ' 'merry' 'ココニアル' 'たくさん' 'ドキ' '君' 'オトメ' 'ススメ' 'Right' '恋せよ' 'まし' 'GO' 'ハイ' '会い' 'Precious' 're' 'friends' 'Hey' 'deux' 'trois' 'キラッ' 'シンガロング・ナウ' 'ナナナナナ' 'ロマンティック' '燃やし' 'story' 'Go' 'キュン' 'Dong' 'Ding' 'Dang' 'eau' 'de' 'secret' 'toilette' 'ぷにぷに'
皆さんの予想通りだったでしょうか. 個人的には『絶対特権主張しますっ!』の'特権'とかは分かりやすいなあと思いました.
実際に上位の単語がどのくらい使われているのかを確認してみます.

tf-idf が大きい単語の特徴としては,

といった感じでしょうか.

idf が小さい単語は?

次に, どの曲にも現れる普遍的な内容の単語だと予想されるidfが小さい単語300個を取り出してみましょう.

# idfを取り出す
transformer.idf_

# idfが小さいもの300個
sorted_idf = transformer.idf_.argsort()
for i in sorted_idf[:300]:
    print("\'{}\'".format(voc[i]), end=" ")

結果は以下の通りになります.
'ない' 'から' 'たい' 'この' 'てる' 'たら' '夢' '今' 'そう' 'だけ' 'まで' 'よう' '時' 'いる' '私' '空' '笑顔' '心' 'じゃ' '見' '手' 'でも' '日' 'けど' '人' 'あなた' 'きっと' 'ずっと' '胸' '目' '未来' 'もう' '想い' 'その' 'もっと' 'いつも' '世界' 'って' 'なく' '中' 'いい' 'いつ' 'みんな' 'あの' '何' '誰' '一' 'こと' 'だって' '風' 'なら' '今日' 'なる' '気持ち' '君' '自分' '光' 'なっ' '明日' '信じ' '前' 'ある' 'どこ' 'ちゃう' 'みたい' 'まだ' 'ほら' '声' '星' '一緒' 'なんて' '好き' 'どんな' 'する' '涙' 'いつか' 'それ' '夜' 'られ' '色' '大好き' 'くれ' 'ハート' '届け' 'でしょ' 'より' 'たく' 'ここ' '待っ' 'そっと' 'ちゃ' 'かけ' '先' '道' '勇気' 'ドキドキ' '恋' '言葉' '愛' 'ちょっと' '全部' 'まま' 'です' 'ゆく' 'そんな' '抱きしめ' 'もの' '魔法' '見つめ' 'たち' 'また' 'キミ' 'あげる' '行こ' 'ため' 'ねぇ' '時間' '輝く' 'キラキラ' 'ちゃっ' '言え' '少し' '遠く' 'とき' 'たり' '見せ' '顔' 'さあ' 'いく' '見つけ' '二' '伝え' '瞳' '気' 'そば' '瞬間' 'すぐ' '感じ' 'you' '場所' '花' '大切' 'わたし' '虹' '上げ' '行く' '街' '輝き' '同じ' '強く' 'しよ' 'あれ' 'どう' 'くれる' 'ます' '幸せ' 'いっぱい' '音' '止め' '希望' 'まるで' '最高' 'けれど' '楽しい' 'I' '探し' '止まら' '1' 'なれ' '海' 'ありがとう' '見え' 'なり' 'たくさん' 'Yeah' '笑っ' 'だっ' 'ほど' '季節' 'ほしい' '秘密' '本当' 'ひとつ' 'あっ' '触れ' 'あと' '見上げ' '不安' '歌' '永遠' '光る' '素敵' '出す' '鼓動' '雨' 'ココロ' '運命' '隣' 'まだまだ' '泣い' '言わ' 'ステージ' 'ただ' '背中' 'ちゃんと' 's' '見る' '優しく' '新しい' '来' 'いま' 'られる' '様' 'こんな' '教え' '次' '度' '知っ' 'ダメ' 'なれる' '歩' '景色' 'チャンス' '波' '言っ' '忘れ' '大' 'もん' '照らし' '太陽' '向い' 'とか' 'これ' 'すぎ' 'リズム' '息' 'くらい' '知ら' '歌う' '大事' '夢見' '感じる' 'だから' '続け' 'ホント' 'キス' '欲しい' '開け' '変わら' '不思議' 'いっ' 'そこ' 'キモチ' 'ながら' 'そして' '月' '夏' '2' 'ふたり' 'のに' '生き' '指先' '向こう' '集め' '変わっ' '呼ん' '夜空' 'my' '小さな' '変え' 'ワクワク' 'でき' '咲く' 'らしく' '憧れ' 'ひとり' 'パワー' '大丈夫' 'ミラクル' '奇跡' 'さぁ' '乙女' '的' '本気' '踊る' '最後' '事' '力' '溢れ' '日々' '見える' '毎日' 'あげ' 'すべて' '溶け' '気づい' '越え' 'こんなに' '揺れる' '両手'

こちらは先ほどよりも予想が付きやすかったかもしれません. 詳しく見てみると

  • キラキラ系('夢', '笑顔', '未来', '想い', '世界')
  • 身体系('見', '手', '心', '胸', '目')
  • 景色系('空', '風', '星', '道', '街')
  • 恋愛系('好き', 'ハート', 'ドキドキ', '恋', '愛')
  • 5W1H系('いつも', 'いつ', 'どこ', 'どんな', 'いつか')
  • 逆接接続詞系('でも', 'けど', 'まだ')
  • 苦難系('涙', '不安', '泣い')
  • 光系('光', '輝く', 'キラキラ')

など, いくつかパターンがあるような気がして面白いです.
実際に上位の単語がどのくらい使われているのかを確認してみます.

'夢' が歌詞に含まれる曲がほぼ半数もあるのはかなりすごいですね.

おわりに

今回の記事ではtf-idfを使った楽曲の歌詞の分析を紹介してみました. 皆さんも, もしこれからデレマス曲の作詞のお仕事をされる場合は, tf-idf が大きい単語のような口ずさみやすい単語を効果的に繰り返し用いつつ, idf が小さい単語のような単語を要所要所で入れていくことで, 良い作詞ができるのではないでしょうか.
また, 今記事を書きながら思いついたことですが, 歌詞をデータ化することにより, キュート曲, クール曲, パッション曲の分類を機械学習に行わせることもできそうな気がします. いつか気が向いたらやってみようかなと思います.
今回の分析の全体のデータは

github.com

にまとめてあります.
それでは, ここまで読んでくださった方はありがとうございました.