誰にも頼まれていないデータ分析
エンタメ

西野カナに「恋」とは何か聞いてみた

  • 自然言語処理
  • Word2Vec
  • Python

この記事で分かること

  • 歌詞から「恋」の意味を読み解く手順
  • 西野カナとaikoで「恋」の使われ方はどう違うのか
  • Word2Vecで単語を足し引きするとどうなるのか

いきなりですが、みなさん恋してますか? 僕は出来てないです。というか恋が何か分かってません(笑)。 好きは分かるけど、恋って何か重い感じがする。「恋してる」何て言ったことないし、恋って何だーと思ったので、定義を調べてみると、

恋:異性に愛情を寄せること、その心。

ほほう。愛情とは?

愛情:相手にそそぐ愛の気持。

ほほう。愛とは?

愛:そのものの価値を認め、強く引きつけられる気持。

うーん….よく分からないので、よく恋をしてそうな西野カナ先生に聞いてみましょう!

1.どうやって聞くか?

会って直接!とはいかないので、今回は西野カナ先生の歌に対して自然言語処理をして、先生が恋をどのように表現しているか調べてみます。言い換えると、

「西野カナ先生に恋とは何かを聞く」 → 「西野カナ先生の歌詞から辞書を作り、恋と類似度の高い言葉は何かを解析する」

2.読者対象

  • 自然言語処理で何が出来るか知りたい人
  • 恋に悩んでいる人

3.使う技術

  • 自然言語処理(Janome, word2vec)
  • スクレイピング(BeautifulSoup)
  • Python

4.データの抽出

まず始めに、こちらから西野カナの歌全167曲の歌詞をスクレイピングで抽出します。 ライブラリはBeautifulSoupを使います。

歌ネット(uta-net.com)歌詞を検索できるサイト。アーティスト名や曲名、歌い出しなどから楽曲の歌詞を探せます。uta-net.com
import requests
from bs4 import BeautifulSoup

base_url = "https://www.uta-net.com"
target_url = 'https://www.uta-net.com/search/?Aselect=1&Keyword=...&Bselect=3&x=0&y=0'
music_num = 167

r = requests.get(target_url)

soup = BeautifulSoup(r.text, "html.parser")
url_list = []
#曲一覧から各曲のURLを取り出してリストに入れる
for i in range(music_num):
   href = soup.find_all("td", attrs={"class": "side td1"})[i].contents[0].get("href")
   url_list.append(href)

kashi = ""
#曲ごとにRequestを送り歌詞を抽出する
for i in range(music_num):
   target_url = base_url + url_list[i]
   r = requests.get(target_url)
   soup = BeautifulSoup(r.text, "html.parser")

   for string in soup.find_all("div", attrs={"id": "kashi_area"})[0].strings:
       kashi += string

with open('kashi.txt', mode = 'w', encoding = 'utf-8') as fw:
   fw.write(kashi)

これでkashi.txtに全曲の歌詞が入りました。

5.データの前処理

歌詞にはLoveやWowといった英語も含まれており、英数字、記号を正規表現で削除し、日本語のみの歌詞にします。

import re
# 英数字の削除
kashi = re.sub("[a-xA-Z0-9_]","",kashi)
# 記号の削除
kashi = re.sub("[!-/:-@[-`{-~]","",kashi)
# 空白・改行の削除
kashi = re.sub(u'\n\n', '\n', kashi)
kashi = re.sub(u'\r', '', kashi)

6.形態素解析

次に歌詞データに対して形態素解析を行います。 形態素解析とは日本語を最小単位に分割し、品詞などを同定する行為です。 形態素解析なんて難しそーと思われるかもしれませんが、Janomeという形態素解析を行ってくれるライブラリがあるのでそれを使います。

def tokenize(text):
    t = Tokenizer()
    tokens = t.tokenize(text)
    word = []
    stop_word = create_stop_word()
    for token in tokens:
        part_of_speech = token.part_of_speech.split(",")[0]
        if part_of_speech == "名詞":
            if not token.surface in stop_word:
                word.append(token.surface)
        if part_of_speech == "動詞":
            if not token.base_form in stop_word:
                word.append(token.base_form)
        if part_of_speech == "形容詞":
            if not token.base_form in stop_word:
                word.append(token.base_form)
        if part_of_speech == "形容動詞":
            if not token.base_form in stop_word:
                word.append(token.base_form)

    return word

結果はこのようになります。

(入力文の例)「窓の外を眺めながら考えてる」
窓   名詞,一般,*,*,*,*,窓,マド,マド
の   助詞,連体化,*,*,*,*,の,ノ,ノ
外   名詞,一般,*,*,*,*,外,ソト,ソト
を   助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
眺め 動詞,自立,*,*,一段,連用形,眺める,ナガメ,ナガメ
ながら 助詞,接続助詞,*,*,*,*,ながら,ナガラ,ナガラ
考え 動詞,自立,*,*,一段,連用形,考える,カンガエ,カンガエ
てる 動詞,非自立,*,*,一段,基本形,てる,テル,テル

「は」や「の」といった助詞・助動詞は今回は必要ないので、形態素解析を行った後に、名詞・動詞・形容詞・形容動詞だけを取り出しています。 動詞・形容詞・形容動詞に関しては、活用されているものは基本形に直します。基本形は.base_formで取り出せます。

(例)眺め/て → 眺める/て

記述取り出せるもの
surface表層形
infl_type活用型
infl_form活用形
base_form原形
print token.reading読み
print token.phonetic発音

他にも関係なさそうな単語を省くためストップワードリストを作成します。 ストップワードは以下を参考に、いくつか自分で追加しました。

def create_stop_word():
    target_url = 'http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt'
    r =requests.get(target_url)
    soup=BeautifulSoup(r.text, "html.parser")
    stop_word=str(soup).split()
    #自分で追加
    my_stop_word=['いる','する','させる','の','色','真夏','身体','最初','知る','られる']
    stop_word.extend(my_stop_word)
    return stop_word

これで辞書が完成しました。

7.word2vecで学習

word2vecは、大量のテキストデータを解析し、各単語の意味をベクトル表現化する手法です。Word2Vecを使うことで、単語と単語の関係性を簡単に表現でき、

「王様」 – 「男」+ 「女」 = 「女王」

「パリ」 – 「フランス」 + 「日本」 = 「東京」

のような 単語同士の 演算が出来たり、単語同士の類似度を計算することができます。 では歌詞データに使われている単語の関係性をword2vecに学習させましょう。

model = word2vec.Word2Vec(sentence, size=200, min_count=4, window=4, iter=50)

パラメータはそれぞれ以下を表しています。

パラメータ意味
sizeベクトルの次元数
min_countn回未満登場する単語を破棄
window学習に使う前後の単語数

8.類似度の計算

さあようやく準備が出揃いました。 後は西野カナ先生が恋をどのように表現しているのかを調べるだけです。 どうやって調べるかというと、恋という単語に対して類似度の高い単語を出力します。学習したモデルに対し、

.most_similar(positive=["単語"])

という風にmost_similarメソッドを使うことでその単語と類似度の高い単語が出力されます。

from gensim import corpora
from janome.tokenizer import Tokenizer
from gensim.models import word2vec
import matplotlib.pyplot as plt
from wordcloud import WordCloud
import re
import requests
from bs4 import BeautifulSoup

with open("kashi.txt", "r", encoding="utf-8") as f:
    kashi = f.read()

# 英数字の削除
kashi = re.sub("[a-xA-Z0-9_]","",kashi)
# 記号の削除
kashi = re.sub("[!-/:-@[-`{-~]","",kashi)
# 空白・改行の削除
kashi = re.sub(u'\n\n', '\n', kashi)
kashi = re.sub(u'\r', '', kashi)
# 品詞を取り出し「名詞、動詞、形容詞、形容動詞」のリスト作成
def tokenize(text):
    t = Tokenizer()
    tokens = t.tokenize(text)
    word = []
    stop_word = create_stop_word()
    for token in tokens:
        part_of_speech = token.part_of_speech.split(",")[0]
        if part_of_speech == "名詞":
            if not token.surface in stop_word:
                word.append(token.surface)
        if part_of_speech == "動詞":
            if not token.base_form in stop_word:
                word.append(token.base_form)
        if part_of_speech == "形容詞":
            if not token.base_form in stop_word:
                word.append(token.base_form)
        if part_of_speech == "形容動詞":
            if not token.base_form in stop_word:
                word.append(token.base_form)

    return word

def create_stop_word():
    target_url = 'http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt'
    r =requests.get(target_url)
    soup=BeautifulSoup(r.text, "html.parser")
    stop_word=str(soup).split()
    #自分で追加
    my_stop_word=['いる','する','させる','の','色','真夏','身体','最初','知る','られる']
    stop_word.extend(my_stop_word)
    return stop_word

sentence = [tokenize(kashi)]
model = word2vec.Word2Vec(sentence, size=200, min_count=4, window=4, iter=50)
print(model.wv.most_similar(positive=[u"恋"], topn=10))

topnで上位表示数を指定してます。

出力結果

順位単語類似度
1追いかける0.8969742059707642
2待てる0.8759297728538513
3生まれ変わる0.8688281178474426
4押す0.8627504110336304
5つらい0.8446334004402161
6失う0.8399721384048462
7逃す0.8399657011032104
8送る0.833713710308075
90.8328136801719666
100.830684244632721

一番近かったのは「追いかける」でした。

西野先生いわく、恋とは追いかけるものらしいです。 てっきり「震える」が出ると思ったら出ませんでしたね。

これらの結果から無理やり定義すると

恋:追いかけて、時には待ち、つらい思いもするが生まれ変われるもの

という結果になりました。 なるほど、さすが西野先生。納得しました! せっかくなのでaiko先生にも聞いてみたいと思います。

出力結果

順位単語類似度
1堕ちる0.9168707132339478
2さよなら0.915056347846984
3全て0.9063875675201416
4嫌い0.9030067920684814
5怒る0.889123797416687
6並べる0.8881044983863831
7上げる0.8861263990402222
8我慢0.885418713092804
9勇気0.8848025798797607
10痺れる0.8845231533050537

「さよなら」「嫌い」「怒る」「我慢」等。 aiko先生は恋はわりとネガティブなモノと考えているみたいですね。

9.おまけ

恋についてもっと知りたくなったので、いろいろな演算を行っていましょう! 演算はpositiveに足す単語を、negativeに引く単語をいれることで出来ます。

model.wv.most_similar(positive=[u"単語1", u"単語2"], negative=[u"単語3"], topn=10)

この場合 単語1 + 単語2 – 単語3 になります。

恋という漢字の下に心がつくように、恋とは心あってのものだと思います。 では心のない恋とは存在するのでしょうか?恋の概念から心の概念を引いてみました。

model.wv.most_similar(positive=[u"恋"], negative=["心"], topn=3)
順位単語類似度
1友達0.7440136075019836
2れる0.7216039896011353
3思う0.7072246074676514

友達になっちゃいました。心が動かないとただの友達ということでしょうか。

恋に関して嘘ついたことありませんか?好きでもないのに、好きって言ってみたり。←特に男性! 自分の気持ちに嘘ついたり。 西野先生いわく、恋で嘘つくことは・・・

model.wv.most_similar(positive=[u"恋", u"嘘"], topn=3)
順位単語類似度
1仕方0.9253218173980713
2辛い0.922753095626831
3追いかける0.9220635890960693

仕方ないみたいです笑

10.まとめ

  • 西野カナの歌詞では、「恋」に最も近い言葉は「追いかける」だった
  • 結果をつなぐと「追いかけて、時には待ち、つらい思いもするが生まれ変われるもの」になる
  • aikoでは「堕ちる」「さよなら」「嫌い」が上位で、恋の捉え方がかなり違う
  • 恋から心を引くと「友達」、恋に嘘を足すと「仕方」が出てくる

パラメーターによって結果にばらつきがでたので、 今後はパラメーターを変えて結果がどう変わっていくかを調査できればなと思います。

関連する記事