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

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

恋:異性に愛情を寄せること、その心。
ほほう。愛情とは?
愛情:相手にそそぐ愛の気持。
ほほう。愛とは?
愛:そのものの価値を認め、強く引きつけられる気持。

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

f:id:nktng117:20180613141607p:plain

どうやって聞くか?

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

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

読者対象
  • 自然言語処理で何が出来るか知りたい人
  • 恋に悩んでいる人
使う技術
  • 自然言語処理(Janome, word2vec)
  • スクレイピング(BeautifulSoup)
  • Python

1.データの抽出

まず始めに、こちら(https://www.uta-net.com/search/?Aselect=1&Keyword=%E8%A5%BF%E9%87%8E%E3%82%AB%E3%83%8A&Bselect=3&x=0&y=0)から西野カナの歌全167曲の歌詞をスクレイピングで抽出します。
ライブラリはBeautifulSoupを使います。

import requests
from bs4 import BeautifulSoup
base_url = "https://www.uta-net.com"
target_url = 'https://www.uta-net.com/search/?Aselect=1&Keyword=%E8%A5%BF%E9%87%8E%E3%82%AB%E3%83%8A&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に全曲の歌詞が入りました。

2.データの前処理

歌詞には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)

3.形態素解析

次に歌詞データに対して形態素解析を行います。
形態素解析とは日本語を最小単位に分割し、品詞などを同定する行為です。
形態素解析なんて難しそーと思われるかもしれませんが、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 発音

他にも関係なさそうな単語を省くためストップワードリストを作成します。
ストップワードは以下を参考に、いくつか自分で追加しました。
http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt

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

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

4.word2vecで学習

word2vecは、大量のテキストデータを解析し、各単語の意味をベクトル表現化する手法です。Word2Vecを使うことで、単語と単語の関係性を簡単に表現でき、
「王様」 – 「男」+ 「女」 = 「女王」
「パリ」 – 「フランス」 + 「日本」 = 「東京」
のような 単語同士の 演算が出来たり、単語同士の類似度を計算することができます。
では歌詞データに使われている単語の関係性をword2vecに学習させましょう。

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

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

size ベクトルの次元数
min_count n回未満登場する単語を破棄
window 学習に使う前後の単語数

5.類似度の計算

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

.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)
# counter = {}
# 品詞を取り出し「名詞、動詞、形容詞、形容動詞」のリスト作成
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)
# for wo in word:
#     if not wo in counter: counter[wo] = 0
#     counter[wo] += 1
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
9 為 0.8328136801719666
10 噂 0.830684244632721

一番近かったのは「追いかける」でした。西野先生いわく、恋とは追いかけるものらしいです。
てっきり「震える」が出ると思ったら出ませんでしたね。
これらの結果から無理やり定義すると
恋:追いかけて、時には待ち、つらい思いもするが生まれ変われるもの
という結果になりました。
なるほど、さすが西野先生。納得しました!

せっかくなのでaiko先生にも聞いてみたいと思います。

f:id:nktng117:20180613141743p:plain

出力結果
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先生は恋はわりとネガティブなモノと考えているみたいですね。

おまけ

恋についてもっと知りたくなったので、いろいろな演算を行っていましょう!
演算は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

仕方ない見たいです笑
よかったですね、ヤリモクのみなさん。

コードはこちら
github.com

課題

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