Word2vecを使ってOfficial髭男dismに「恋」とは何なのかを聞いてみた
Word2vecの使い方がまあまあわかってきたので、Word2vecの勉強を兼ねて少し遊んでみたいと思ったのかこの投稿に至っています。
以下二つの投稿をたまたま見つけたことをきっかけに、割と最近流行りのアーティストでやってみようと思い、去年からPretenderをきっかけに流行り始めたOfficial髭男dismの曲をを分析してみようと思いました。
「Official髭男dism」とは
知らない方はいないと思いますが、一応補足説明です。5年くらい前からデビューしている男性4人組のバンドです。
去年の2019年にPretenderというラブソングをきっかけに大きくブレイクした気がします。ちなみに自分はそこまで詳しくはないですが、他の曲も割と恋愛に関連する曲は多いです。
そのため今回の分析は恋愛ソング界隈では大御所の西野カナさんとは違った意味で、「恋」とは何なのかを聞くには良さそうな人達です。
どうやって「恋」とは何なのかを聞くか
当然直接会って髭男の方々に「恋とは何なんですか」と聞くわけにはいかないので、髭男の全曲の歌詞を解析して恋とは何なのかを探ります。
上記二つのURLに貼ってあるやり方と同じで、Word2vecを用いて「恋」という単語と近い距離の単語は何なのかを探っていきます。
Word2vecを使った物は以前にも投稿しました。
具体的な方法は以下の手順になります。
1. 髭男の全曲の歌詞をスクレイピングして取得する
2. 歌詞を形態素解析で分かち書きにして品詞を抽出
3. Word2vecに流し込んでモデルを作成
4. モデルに「恋」とは何かを聞いてみる
使用技術
今回の歌詞の解析に使うのは以下のツール類です。環境構築方法は調べれば載っています。上記の「Word2vecを用いて久保建英選手の評価を分析してみる」で環境構築方法を載せています。
・Python 3.8.0
・Word2vec
・Scrapy
スクレイピングを行うためのクローリング用フレームワーク
・BeautifulSoup
スクレイピングしたHTMLやXMLをパースしたりするためのモジュール
・Mecab
形態素解析を行うためのツール
今回はスクレイピングに勉強も兼ねてScrapyを使用しましたが、王道のrequests+BeautifulSoupでもかまいません。以前こんな記事を書きました。
https://note.com/shimakaze_soft/n/n262b9133dc1d
全曲の歌詞を取得する
それではscrapyを使って全曲の歌詞を取得します。歌詞の取得先は定番でもあるUta-Netです。まずはscrapyでプロジェクトを作成します。
$ scrapy startproject utanet_crawl
すると以下のようなディレクトリとファイルが作成されます。これでクローリングのためのscrapyプロジェクトは作成されました。
utanet_crawl
├── scrapy.cfg
└── utanet_crawl
├── __init__.py
├── __pycache__
├── items.py
├── middlewares.py
├── pipelines.py
├── settings.py
└── spiders
├── __init__.py
└── __pycache__
次にサイト毎のクローリングのためのスパイダーというものを作成していきます。ディレクトリの中に入り、genspiderコマンドでスパイダーを作成します。
$ cd utanet_crawl
$ scrapy genspider hige_dan www.uta-net.com
これでspidersディレクトリ以下にhige_dan.pyというファイルが作成されます。こちらのファイルを編集していきます。
次にitemsを定義します。同じディレクトリ上にitems.pyというファイルがあるため、items.pyに以下のクラスを作成します。
class SongCrawlItem(scrapy.Item):
title = scrapy.Field() # 曲のタイトル
url = scrapy.Field() # 曲のURL
lyric = scrapy.Field() # 歌詞
それではいよいよhige_dan.pyは編集していきます。parseメソッドの中にtime.sleep(1)を加えることで1秒間停止しています。クローリング先のuta-netに負荷をかけないための配慮です。
import re
import time
import scrapy
from utanet_crawl.items import SongCrawlItem
from bs4 import BeautifulSoup
class HigeDanSpider(scrapy.Spider):
name = 'hige_dan'
allowed_domains = ['www.uta-net.com']
start_urls = ['http://www.uta-net.com/artist/18093']
def parse(self, response):
# 髭男dismのアーティストID
for song in response.css('tbody').css('tr'):
item = SongCrawlItem()
song_title = song.css('td.td1 a::text').extract_first()
song_path = song.css('td.td1 a::attr(href)').extract_first()
item['title'] = song_title
song_url = 'http://' + self.allowed_domains[0] + song_path
# 1秒間停止する
time.sleep(1)
yield scrapy.Request(
song_url,
callback=self.parse_lyrics,
meta={'item': item}
)
def parse_lyrics(self, response):
# 歌詞自体を抽出する
item = response.meta['item']
item['url'] = response.url
# text
text = response.css('div#kashi_area').extract_first()
soup = BeautifulSoup(text, 'html')
soup = soup.find('div', itemprop='text')
song_lyrics = soup.getText()
# テキストのクリーニング
song_lyrics = self.text_cleaning(song_lyrics)
# 歌詞
item['lyric'] = song_lyrics
yield item
def text_cleaning(self, text):
song_lyrics = text.replace('\n', '')
song_lyrics = song_lyrics.replace(' ', '')
# 英数字の排除
song_lyrics = re.sub(r'[a-zA-Z0-9]', '', song_lyrics)
# 記号の排除
song_lyrics = re.sub(
r'[ <>♪`‘’“”・…_!?!-/:-@[-`{-~]', '', song_lyrics
)
# 注意書きの排除
song_lyrics = re.sub(r'注意:.+', '', song_lyrics)
return song_lyrics
上記のクローリングの実行は以下になります。特にエラーなども表示されなければ成功です。
$ scrapy crawl hige_dan
歌詞をテキストファイルに保存する
scrapyでクローリングした上記の歌詞を一つのテキストファイルの中に保存します。やり方はいろいろありますが、今回はscrapyの中にあるパイプラインという物をいじってlyrics.txtというテキストファイルに保存します。
同じディレクトリ上にあるpipelines.pyというファイルがあるので、そちらを編集します。
$ ls
$ __init__.py items.py middlewares.py pipelines.py settings.py spiders
pipelines.pyを以下に書き換えてください。lyrics.txtというテキストファイルに一行ずつ書き足されていきます。
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
# useful for handling different item types with a single interface
# from itemadapter import ItemAdapter
class UtanetCrawlPipeline:
def open_spider(self, spider):
write_path = 'lyrics.txt'
self.file = open(write_path, 'w')
def close_spider(self, spider):
self.file.close()
def process_item(self, item, spider):
line = item['lyric'] + '\n'
self.file.write(line)
print(line)
print('----' * 20)
return item
scrapyの初期の状態ではパイプラインが動かないため、こちらを動けるように設定します。同じディレクトリ上いsettings.pyというファイルがあるため、そのファイルの中にある以下の部分のコメントアウトを解除します。
# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
'utanet_crawl.pipelines.UtanetCrawlPipeline': 300,
}
これでもう一度クローラーを動かすと、lyrics.txtというファイルが作成されるはずです。
$ scrapy crawl hige_dan
$ ls
lyrics.txt scrapy.cfg utanet_crawl
形態素解析で分かち書きした歌詞をテキストファイルに保存
次にMecabを使って歌詞を形態素解析してからwakati.txtというファイルに保存します。save_wakati.pyというファイルを作成しています。
以下がsave_wakati.pyです。
import re
import MeCab
import requests
from bs4 import BeautifulSoup
# 文章を読み込む
def read_doc(path='./utanet_crawl/lyrics.txt'):
text = ""
with open(path, 'r', errors='ignore') as f:
text += f.read()
return text
# データの前処理
def preprocessing(text):
# 英数字の削除
text = re.sub("[a-xA-Z0-9_]", "", text)
# 記号の削除
text = re.sub("[!-/:-@[-`{-~*]", "", text)
# 空白・改行の削除
text = re.sub(u'\n\n', '\n', text)
text = re.sub(u'\r', '', text)
return text
# ストップワードリストの生成
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
# MeCab による単語への分割関数 (名詞のみ残す)
def split_text_only_noun(text):
stop_word = create_stop_word()
option = '-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd'
# option = ''
tagger = MeCab.Tagger("-Ochasen " + option)
tagger.parse('')
# Execute class analysis
node = tagger.parseToNode(text)
words = []
while node:
# word = node.surface.upper()
# 基本形を使用する
word = node.feature.split(',')[6]
word = word.upper()
class_feature = node.feature.split(',')[0]
sub_class_feature = node.feature.split(',')[1]
# features = ['名詞', '動詞', '形容詞']
features = ['名詞', '動詞', '形容詞', '形容動詞']
if class_feature in features:
if sub_class_feature not in ['空白', '*']:
# ストップワードに該当しない瀕死を保存する
if word not in stop_word:
words.append(word)
node = node.next
return words
# 分かち書きしたデータをファイルに保存
def save_wakati_file(wakati_list, save_path='wakati.txt', add_flag=False):
# 新規保存か追加保存かの選択
mode = 'w'
if add_flag:
mode = 'a'
# 分かち書きしたデータをファイルに保存
with open('./' + save_path, mode=mode, encoding='utf-8') as f:
f.write(' '.join(wakati_list))
if __name__ == "__main__":
# 歌詞を読み込む
lyrics = read_doc()
# データの前処理 クリーニング作業
lyrics = preprocessing(lyrics)
# 形態素解析
list_text = split_text_only_noun(lyrics)
# 分かち書きしたものを保存
save_wakati_file(list_text)
手順としては以下の内容です。
1. lyrics.txtを読み込む
2. 前処理で不要な文字を削除する
3. 形態素解析で品詞部分を抜き出してリストに格納
4. wakati.txtというファイルに半角スペースを区切って保存
$ python save_wakati.py
# wakati.txtというテキストファイルが作成されます.
Word2vecのモデルを作成する
先ほど作成したwakati.txtを元にword2vecのモデルを作成します。ファイル名はsave_word2vec.pyです。
from gensim.models import word2vec
def save_word2vec_model(load_path, save_path):
# 分かち書きしたテキストデータからコーパスを作成
sentences = word2vec.LineSentence(load_path)
# ベクトル化
model = word2vec.Word2Vec(
sentences,
sg=1, # Skip-Gram Modelを使用
size=200, # ベクトルの次元数
min_count=4, # 単語の出現回数によるフィルタリング
window=5, # 対象単語からの学習範囲,
iter=40,
hs=1
)
# 階層化ソフトマックスを使用
model.save(save_path)
# 作成したモデルをファイルに保存
if __name__ == "__main__":
load_path = 'wakati.txt'
save_model_path = 'save.model'
save_word2vec_model(load_path, save_model_path)
上記のsave_word2vec.pyを実行すれば、save.modelというモデルが作成されます。
いよいよ恋とは何なのかを聞いてみる
それではいよいよ恋とは何なのかを聞いてみます。面倒くさい準備作業はこれで終わりになります。
それではsynonym.pyというファイルを作成して実行してみます。
from gensim.models import word2vec
def most_similar(load_model_path, positive=None, negative=None):
model = word2vec.Word2Vec.load(load_model_path)
results = []
if positive and negative:
if positive in model.wv and negative in model.wv:
results = model.wv.most_similar(
positive=[positive],
negative=[negative]
)
elif positive:
if positive in model.wv:
results = model.wv.most_similar(
positive=[positive]
)
return results
if __name__ == "__main__":
inputs = list(input().rstrip().split(' '))
save_model_path = 'save.model'
positive = inputs[0] if len(inputs) >= 1 else None
negative = inputs[1] if len(inputs) >= 2 else None
results = most_similar(save_model_path, positive, negative)
print('results', results)
半角スペースを入れることでnegativeも入れられます。
$ python synonym.py
恋 心
それでは「恋」という単語を与えてみて、一番近い類義語をいくつか出してもらいます。
$ python synonym.py
恋
「恋」
('去る', 0.5954979062080383),
('勝率', 0.5825579166412354),
('込む', 0.5584472417831421),
('見積もる', 0.5289913415908813),
('進める', 0.5264034271240234),
('差す', 0.5098958611488342),
('あて', 0.5063321590423584),
('雑', 0.4968462586402893),
('迷う', 0.47684425115585327),
('撮る', 0.4723493158817291)
なかなか現実的なワードが出てきました。「去る」「勝率」「見積もる」というのがなんとも現実的。「勝率」、「見積もる」というのも何ともシビアな内容です。
Pretenderの結末?とも言える歌詞の内容にもある通り、「君の運命のヒトは僕じゃない。辛いけど歪めない。」と「去る」ことも大事なのかしれません。まさしくグッバイな内容です。
こちらの「西野カナ」先生、「aiko」先生とは全く違った教えになりました。
次にPretenderの歌詞の一番の盛り上がりどこでもある「グッバイ」に一番類義語を出してみます。
$ python synonym.py
グッバイ
そのまんまの意味の「GOOD-BYE」が取り出されてしまいました。恐らくどこかで「グッバイ」と「GOOD-BYE」が形態素解析あたりの段階で取り出されてしまったのかも。
「グッバイ」
('GOOD-BYE', 0.7168291211128235),
('いやいや', 0.6614043116569519),
('痛', 0.5685068964958191),
('相思相愛', 0.5599619150161743),
('サンキュー。', 0.5376338362693787),
('ヒト', 0.5306980013847351),
('触れる', 0.5301403999328613),
('髪', 0.5231151580810547),
('答え', 0.5201852321624756),
('否める', 0.5138511657714844)
それでも中々に髭男らしい内容なのかもしれません。「グッバイ」することは確かに「いやいや」で「痛」ものですね。ただ「相思相愛」というのは少し謎です。
次に「恋」と同じように使われそうな「愛」とは何かを聞いてみます。
$ python synonym.py
愛
「愛」
('怖がる', 0.5298768281936646),
('合える', 0.4761654734611511),
('つなぐ', 0.4489273428916931),
('疑う', 0.42714470624923706),
('余地', 0.42705923318862915),
('もたげる', 0.42530012130737305),
('欲張り', 0.4207506775856018),
('たか', 0.42058151960372925),
('完璧', 0.4116225242614746),
('相', 0.4062730371952057)
「髭男」先生的には「愛」とは怖がり、疑い、欲張るものでもあるらしい。中々に怖い。
君の運命の人は僕じゃないなら、運命から僕が無くなったらどうなるのか
歌詞にもある「君の運命の人は僕じゃない」とありますが、「運命」から「僕」が無くなったらどうなるのかが少し気になります。
Word2vecの機能でもありますが、単語間は距離でもあるため、引いたりすることもできます。それでは「運命 - 僕」を引いてみたらどんな結果になるでしょうか。
$ python synonym.py
運命 僕
('ヒト', 0.46188387274742126),
('否める', 0.4060783386230469),
('ページ', 0.39936089515686035),
('終える', 0.39716243743896484),
('孤独', 0.3562673330307007),
('GOOD-BYE', 0.3529985547065735),
('僕は待ってる', 0.31914806365966797),
('今度', 0.31594714522361755),
('暮らし', 0.2980244755744934),
('間違う', 0.2869136929512024)]
「人」ではなく「ヒト」というのもPretenderの歌詞の中にも出てくる内容です。「ヒト」に関してはよくわかりませんが、「運命から僕がいなくなった場合」は「歪める」「終える」「孤独」と中々に暗い内容です。
「僕待ってる」で軽く恐怖を感じた。
ついでにWordCloudで画像に出力
ついでではありますが、WordCloudで画像を生成してみました。以下を出力すればwordcloud_sample.pngというファイルの画像が生成できます。
from wordcloud import WordCloud
if __name__ == "__main__":
path = 'wakati.txt'
list_text = []
with open(path, encoding="utf-8") as f:
list_text = f.readlines()
text = ' '.join(list_text)
wordcloud = WordCloud(
background_color="white",
font_path='./NotoSansCJKjp-Regular.otf',
width=900,
height=500
).generate(text)
wordcloud.to_file("./wordcloud_sample.png")
まとめ
個人的に面白い結果が出てきたので満足な内容です。今回使用したソースコードは以下になります。
最近ではWord2vec以外にもfastTextを使うと精度が良くなるそうです。時間がある時に試してみたいです。
全体的にネガティブ的な内容になっちゃいましたが、決してOfficial髭男dismをディスってるわけではないです!あくまでもネタな内容です!
髭男は割と最近聴いているので好きです!(だから髭男ファンの方怒らないで。。。)
参考資料
この記事が気に入ったらサポートをしてみませんか?