Aidemy Tech Blog

機械学習・ディープラーニング関連技術の活用事例や実装方法をまとめる、株式会社アイデミーの技術ブログです。

TinderAPIで女の子の顔写真を集めて、加工アリorナシを自動で判定してみた

こんにちは、Aidemy研修生のタットヤムです。

みなさん、かわいい女の子は好きですか?僕は大好きです。

     f:id:kirraria:20180628002323p:plain

今どきはマッチングアプリでいつでもかわいい女の子を見放題なのでいい時代ですね。中でも「Tinder」はアメリカを中心に世界的に流行ってる、相手の写真を見てアリなら右、ナシなら左にスワイプするだけでマッチングするという、かわいい子の顔を見て癒されるのにうってつけなアプリです。

しかしTinderでスワイプして写真を眺めていると、「この子かわいいけど加工してるっぽい?」なんてことありますよね。

Aidemyの研修でせっかくディープラーニングを学んだので、今回はTinderのPythonAPIを使って日本各地の女の子の顔写真を集めて、ディープラーニングで加工がされているかどうかを自動で判定してみようと思います。

だいたい手順はこんな感じになります。そのまま目次にしてしまいますね。

 

0.環境

主な使用ライブラリなどを載せておきます。

・Python 3.6.4    プログラミング言語

・Anaconda 1.6.9    プログラミングする環境を管理する

・Jupyter 4.4.0           ノートにそのまま書くみたいにプログラミングができる

・numpy 1.14.0          数学でやった行列みたいなのを上手に扱える

・matplotlib 2.1.2       グラフを描いたりしてくれる

・OpenCV 3.4.1.15    写真を上手に扱える

・Keras 2.2.0             機械学習のプログラムがたくさん入ったパッケージ的なもの

・Pynder 0.0.13    TinderをPythonで動かすためのAPI

 

インポート部分は全部でこんな感じになってます。

import os
import re
import random
import matplotlib.pyplot as plt
import cv2
import numpy as np
import urllib.request
from urllib.parse import quote
import httplib2
import pynder
from keras import optimizers
from keras.applications.vgg16 import VGG16
from keras.layers import Dense, Dropout, Flatten, Input
from keras.models import Model, Sequential, load_model
from keras.utils.np_utils import to_categorical

 

加えていくつか関数を定義しておきます。

def get_image(img_url): # ウェブ上の画像をダウンロードする
    opener = urllib.request.build_opener()
    http = httplib2.Http(".cache")
    response, content = http.request(img_url)            
    return content

def aidemy_imshow(name, img): # Jupyter notebook上で画像を表示
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    plt.imshow(img)
    plt.show()
cv2.imshow = aidemy_imshow

def jpg_count(folder_name): # ディレクトリ内のjpgファイルを数える
    files = os.listdir("./"+folder_name)
    jpgcount = 0
    for file in files:
        index = re.search(".jpg", file)
        if index:
            jpgcount += 1
    return jpgcount

def image_save(img,folder_name): # 画像を保存する
    # ディレクトリがなければ作る
    if not os.path.exists(folder_name):
        os.mkdir(folder_name)
    jpgcount = jpg_count(folder_name)
    # カウント数+1でファイル名をつけて保存
    w_pass = "./{}/{}.jpg".format(folder_name,jpgcount)
    cv2.imwrite(w_pass,img)

1.写真を集める

はじめに、Tinderから写真をたくさん集めます。コードは各章の下に貼っておきます。

PynderでTinderにアクセスするためには、アクセストークンというものを調べないといけません。アクセストークンはこのWebページ(Tinderface)に書いてあるとおりにやると調べることができます。

なおディープラーニングの学習に使う写真は、札幌・秋田・宇都宮・東京・横浜・名古屋・京都・大阪・福岡・沖縄を回るようにして集めました。

こんな感じで各都市で300人ずつスワイプして画像を集めました。 

f:id:kirraria:20180703203212p:plain

# 写真を保存するディレクトリを作成する
if not os.path.exists("images"):
        os.mkdir("images")

AccessToken = "ここにアクセストークンを貼ってね"
session = pynder.Session(AccessToken)
friends = session.get_fb_friends()

# 探索する都市の緯度と経度を指定
Locations = [["Sapporo", 43.055248, 141.345505],
             ["Akita", 40.824589, 140.755203],
             ["Utunomiya", 36.554241, 139.897705],
             ["Tokyo", 35.680909, 139.767372],
             ["Yokohama", 35.465786, 139.622313],
             ["Nagoya", 35.154919, 136.920593],
             ["Kyoto", 35.009129, 135.754807],
             ["Osaka", 34.702509, 135.496505],
             ["Hukuoka", 33.579788, 130.402405],
             ["Okinawa", 26.204830, 127.692398]
            ]

# 各都市で取得する人数の上限
Limit = 300

for location in Locations:
    session.update_location(location[1], location[2])
    users = session.nearby_users()
    count = 0
    for user in users:
        count += 1
        if count > Limit:
            break
        photo_urls = user.get_photos(width="640")
        for url in photo_urls:
            img_buf = np.fromstring(get_image(url), dtype='uint8')
            img = cv2.imdecode(img_buf, 1)
            image_save(img,"./images/{}".format(location[0]))
    print(location[0] + " is done")

2.顔の部分を抽出する

次に、集めた写真の顔の部分だけを抽出して、ディープラーニングが学習しやすいようにします。しかし何千枚もの写真を1つ1つ加工していくのは大変なので、ここではHaar-like特徴分類器というプログラムを使います。

仕組みを簡単に説明すると、人の顔はだいたい両目のところが暗く鼻のところは明るいとか、目が暗くてその下の頬は明るいとか、明暗でいくつかパターンがあって、そのパターンがあるものを人の顔として認識するという仕組みです。詳しくは下の記事を読んでみてください。

qiita.com

 結果を見てみると、わりとしっかりと顔に反応して抽出してくれていることが分かります。ディープラーニングとはまた違いますが、一見単純な仕組みでもちゃんと人の顔を抽出してくれるっていうのが何というかロマンがありますよね。

ただし顔スタンプなど、顔に似ているものもけっこう拾ってしまうので、これは次の手順のときに、一緒に分別しないといけません。

f:id:kirraria:20180703203219p:plain

# HAAR分類器の特徴量
cascade_path = "D:\Anaconda3\envs\Conda_AITrade\Lib\site-packages\cv2\data\haarcascade_frontalface_alt.xml"

face_num = [0, 0] # [0]が顔認識数、[1]が顔認識されなかったファイル数(経過表示)

if not os.path.exists("faces"):
        os.mkdir("faces")

for location in Locations:
    # 都市ディレクトリ内のjpgファイル数を読み込み
    imageN = jpg_count("./images/{}".format(location[0]))
    print(location[0]," : ",imageN)
    
    # ディレクトリ内のファイルをリストに格納
    dir_pass = "./images/{}/".format(location[0])
    file_names = os.listdir(dir_pass)
    for file_name in file_names:
        print(file_name, face_num)
        # 画像を読み込み、顔認識分類器にかける
        img = cv2.imread(dir_pass + file_name)
        # 画像ファイルが読み込めないとき
        if img is None:
            print("img is none.")
            continue
        img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        cascade = cv2.CascadeClassifier(cascade_path)
        faces_poses = cascade.detectMultiScale(img_gray, scaleFactor=1.1, minNeighbors=2, minSize=(64,64))
        # 顔が検出されたとき
        if len(faces_poses) > 0:
            for fp in faces_poses:
                img_face = img[fp[1]:fp[1]+fp[3], fp[0]:fp[0]+fp[2]]
                img_face = cv2.resize(img_face, (64, 64))
                image_save(img_face,"faces/{}".format(location[0]))
                face_num[0] += 1
        # 顔が検出されなかったとき
        else:
            face_num[1] += 1

3.重複した顔写真を削除する

次に、集めた顔写真の中で重複しているものを削除します。

同じ顔写真が複数あるとディープラーニングの学習に偏りが生じてしまう可能性があるので、しっかり消していきましょう。

今回はある写真が複数あるかどうかを調べるために、写真のヒストグラムの類似率を計算しました。カラー画像を構成する3色それぞれの濃淡をヒストグラムとして計算し、ヒストグラムが一定以上類似している写真を重複として判定して削除します。

類似度の閾値は、画像の一覧を目視してだいたい重複がなくなるぐらいに設定しましたが、もうちょっといい設定方法があったかもしれません。

顔写真はこれまでの処理で削られた結果、全部で1294枚となりました。

# パラメータ定義 ####################################
# 類似度の閾値
criret = 0.999

# 処理の実行 #######################################
indir_pass = "./faces/"
outdir_pass = "./faces_dd"
file_names = os.listdir(indir_pass)
passhist_list = []

for file_name in file_names:
    # 画像を読み込む
    img = cv2.imread(indir_pass + file_name)
    # 画像ファイルが読み込めないとき
    if img is None:
        print("img is none.")
        continue
    # RGBのヒストグラムを計算しリストに格納
    img_hist = []
    img_hist.append(cv2.calcHist([img], [0], None, [256], [0, 256]))
    img_hist.append(cv2.calcHist([img], [1], None, [256], [0, 256]))
    img_hist.append(cv2.calcHist([img], [2], None, [256], [0, 256]))
    # 参照ヒストグラムとの類似率を計算し、閾値以下であれば新規顔画像として保存する
    for pass_hist in passhist_list:
        if cv2.compareHist(img_hist[0],pass_hist[0],cv2.HISTCMP_CORREL) > criret:
            break
        if cv2.compareHist(img_hist[1],pass_hist[1],cv2.HISTCMP_CORREL) > criret:
            break
        if cv2.compareHist(img_hist[2],pass_hist[2],cv2.HISTCMP_CORREL) > criret:
            break
    else: # すべての参照ヒストグラムとの類似率が閾値以下だったとき
        passhist_list.append(img_hist)
        image_save(img, outdir_pass)

4.顔写真を普通の写真と加工写真に分ける

次に、集めた顔写真を普通の写真と、プリクラの写真、SNOWで加工した写真の3つに分けます。

実はこの作業が一番大変で、何千の画像データを1つ1つ見て確認してディレクトリに振り分けていくのは気が遠くなるような作業でした。またプリクラならまだわかりやすいのですが、SNOWで加工した写真は一見普通に見えるようになっている写真もあるので判定に困ることも多かったです。

しかしここの振り分けのゆらぎが、のちのモデルの性能に大きな影響を与えることもあるので、しっかりと自分の中で軸を定めて分けていきましょう。

5.ディープラーニングモデルに学習させる

いよいよ振り分けた顔写真を使って、ディープラーニングの学習を行っていきます。ディープラーニングをするためには、学習モデルを設計しないといけないのでそれから始めましょう。

5.1.学習モデル

今回の学習モデルはこんな感じに設計しました。今回はVGG16モデルを転移学習として使用し、その後2層の全結合層を経て出力層にて3クラスの分類を行います。

- 入力 (64x64×3 : 64×64のカラー画像)
- VGG16モデル(16層の転移学習モデル)
- 全結合層1(64, sigmoid関数, dropout=0.5)
- 全結合層2(32, sigmoid関数, dropout=0.5)
- 出力層(3, softmax関数)

5.2.転移学習

ちなみに2行目にあるVGG16モデルというのは、転移学習と呼ばれるもので、簡単にいうとほかの人があらかじめ学習までさせてくれた学習モデルをそっくり借りてくるというものです。

人間が物体を認識するとき、たとえばこんな風になりますよね。

→映像が見えた
→なんか丸っこいものが見えた、顔かな
→なんか目が大きいからSNOWっぽい

この「映像が見えた」ところから「なんか目が大きいからSNOWっぽい」まですべての工程を学習させるのがしんどいので、「映像が見えた」から「なんか丸っこいものが見えた、顔かな」ってところまでをほかの人が学習させてくれたものを使い、最後の「なんか目が大きいからSNOWっぽい」で加工されているかどうか判定するところだけを自分のお好みで学習させるという感じです。

5.3.データの水増し

今回はデータの80%をトレーニングデータ、20%をテストデータとしました。この2つを分ける理由は後述します。

それとモデル学習の前にもう一つ、データの水増しというのを行います。トレーニングデータの顔写真1035枚だとちょっと少ないので、画像を左右反転させたり、ぼかしたりして一つの画像を何枚にも増やします。今回は一つの画像が2×2×2=8枚になるように水増ししたので、データは8280枚となりました。

5.4.学習の実行

最後にモデルの学習を実行します。そのまま学習を進めてしまうとVGG16モデルのパラメータも変わってしまうので、そこだけ変わらないように固定します。今回はミニバッチ=32のepoch=100で学習を実行しました。

学習の経過を下のグラフに示します。青線がトレーニングデータに対する正答率で赤線がテストデータに対する正答率です。

f:id:kirraria:20180703184325p:plain

結果としてトレーニングデータに対してテストデータの正答率の上がり方が悪いので、若干過学習気味といった感じになってしまいました。最終的な正答率は約90%でした。原因としては、ラベリング時の振り分け基準があいまい、使ったモデルが適当でなかった等が考えられます。

# フォルダを生成
if not os.path.exists("result"):
        os.mkdir("result")

# Pynderを使いTinderにログイン
session = pynder.Session(AccessToken)

# モデルの読み込み
model = load_model("my_model.h5")

for location in Locations:
    session.update_location(location[1], location[2])
    users = session.nearby_users()
    count = 0
    for user in users:
        count += 1
        if count > Limit:
            break
        photo_urls = user.get_photos(width="640")
        for un, url in enumerate(photo_urls):
            img_buf = np.fromstring(get_image(url), dtype='uint8')
            img = cv2.imdecode(img_buf, 1)
            img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            cascade = cv2.CascadeClassifier(cascade_path)
            faces_poses = cascade.detectMultiScale(img_gray, scaleFactor=1.1, minNeighbors=2, minSize=(64,64))
            # 顔が検出されたとき
            if len(faces_poses) > 0:
                for fp in faces_poses:
                    # 顔を囲む枠を生成
                    cv2.rectangle(img, tuple(fp[0:2]), tuple(fp[0:2]+fp[2:4]), (0, 0, 255), thickness=3)
                    # 学習したモデルでスコアを計算する
                    img_face = img[fp[1]:fp[1]+fp[3], fp[0]:fp[0]+fp[2]]
                    img_face = cv2.resize(img_face, (64, 64))
                    score = model.predict(np.expand_dims(img_face, axis=0))
                    # 最も高いスコアを書き込む
                    score_argmax = np.argmax(np.array(score[0]))
                    text =  "{0} {1:.3}% ".format(Labels[score_argmax], score[0][score_argmax]*100)
                    cv2.putText(img, text, (fp[0],fp[1]+fp[3]+30), cv2.FONT_HERSHEY_DUPLEX, 1, (0,0,255), 2)
                # 画面に表示
                cv2.imshow("{}_{}".format(location[0], count), img)
                plt.show()
                image_save(img,"result")
            # 顔が検出されなかったとき
            else:
                continue

6.実際にTinderの写真に適用してみる

それでは実際に、新規にTinderから取得した写真をモデルに入力し、正しくラベリングできているかどうかを見ていきましょう。

まずは普通の写真から、高いパーセンテージで普通とラベリングされています。

f:id:kirraria:20180703200241j:plain
f:id:kirraria:20180703200207j:plain

 

次にプリクラの写真です。左の写真はちょっとパーセンテージが低いですね。ほかの写真でも実行した結果、プリクラの写真はたまに普通の写真としてラベリングされてしまうようです。

f:id:kirraria:20180703200343j:plain
f:id:kirraria:20180703200425j:plain

 

最後にSNOWで加工された写真です。SNOWの写真もプリクラの写真と同様に、基本的には拾ってくれるのですが、たまに普通の写真としてラベリングされてしまいました。

f:id:kirraria:20180703200820j:plain
f:id:kirraria:20180703200842j:plain

 

これらは間違ってしまっているものです。ぼやけていたりノイズが入ったりしていると誤った判定になりやすいなと感じました。

f:id:kirraria:20180703202753j:plain
f:id:kirraria:20180703202805j:plain
# フォルダを生成
if not os.path.exists("result"):
        os.mkdir("result")

# Pynderを使いTinderにログイン
session = pynder.Session(AccessToken)

# モデルの読み込み
model = load_model("my_model.h5")


for location in Locations:
    session.update_location(location[1], location[2])
    users = session.nearby_users()
    count = 0
    for user in users:
        count += 1
        if count > Limit:
            break
        photo_urls = user.get_photos(width="640")
        for un, url in enumerate(photo_urls):
            img_buf = np.fromstring(get_image(url), dtype='uint8')
            img = cv2.imdecode(img_buf, 1)
            img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            cascade = cv2.CascadeClassifier(cascade_path)
            faces_poses = cascade.detectMultiScale(img_gray, scaleFactor=1.1, minNeighbors=2, minSize=(64,64))
            # 顔が検出されたとき
            if len(faces_poses) > 0:
                for fp in faces_poses:
                    # 顔を囲む枠を生成
                    cv2.rectangle(img, tuple(fp[0:2]), tuple(fp[0:2]+fp[2:4]), (0, 0, 255), thickness=3)
                    # 学習したモデルでスコアを計算する
                    img_face = img[fp[1]:fp[1]+fp[3], fp[0]:fp[0]+fp[2]]
                    img_face = cv2.resize(img_face, (64, 64))
                    score = model.predict(np.expand_dims(img_face, axis=0))
                    # 最も高いスコアを書き込む
                    score_argmax = np.argmax(np.array(score[0]))
                    text =  "{0} {1:.3}% ".format(Labels[score_argmax], score[0][score_argmax]*100)
                    cv2.putText(img, text, (fp[0],fp[1]+fp[3]+30), cv2.FONT_HERSHEY_DUPLEX, 1, (0,0,255), 2)
                # 画面に表示
                cv2.imshow("{}_{}".format(location[0], count), img)
                plt.show()
                image_save(img,"result")
            # 顔が検出されなかったとき
            else:
                continue

7.まとめ

「TinderAPIで女の子の顔写真を集めて、加工アリorナシを自動で判定してみた」はいかがだったでしょうか?

私は、TinderのAPIで取得できる情報は写真以外にもいろいろあるみたいなので、ソーシャル系のデータ解析としていろいろ取り組めることがあるかもしれないなあと感じています。

またプログラミングの課題で扱うような整理されたデータでは考慮しないようなところをいろいろ考えなくてはならないことを身をもって体感することができたことはとてもよい経験でした。正直ディープラーニングの学習モデルの出来としてはまだまだだと思いますが、モデルの構築理論などをしっかり勉強してより良いモデルが作れるように精進していきたいと思います。

最後までお付き合いくださり、ありがとうございました!

 

#Aidemynote エントリー作品のまとめ

「Aidemyユーザーにアウトプットの機会を持ってもらいたい」
そんな気持ちで生まれた#Aidemynote企画。今回は、なかでも秀逸だったものを分類してまとめました!

アジェンダは以下の通りです。

-【Aidemy初心者向け】Aidemyで勉強してみたnote 
-【Aidemy一通り勉強した人向け】実装してみたnote
-エヴァンジェリストの活動note

 

 

【Aidemy初心者向け】Aidemyで勉強してみたnote 

 

-「AIプログラミングを学んでみよう」
まずはこれを読んでみよう!Aidemyを使うメリットってなに?

note.mu


-Aidemyをやってみた感想
Aidemyの"ヤバい"点4つをわかりやすく解説してくれました。

note.mu


-「有料化してもAidemyは利用価値ありか!?」

Aidemy課金すべきかしないべきか!?を考察した記事

https://note.mu/z2_mrs/n/n3c702680a86c


-「Aidemyを利用したらマッハで課金してしまった話。」

課金ユーザーの方の記事。この記事のようにツイートと何か良いことがあるかも?

note.mu

 

-「Aidemyをやってみた① Python入門」

コースごとにnoteを書いてくれました!

Python入門が気になっている方必読の記事。

note.mu


-「Aidemyをやってみた② 機械学習概論」

機械学習概論って実際どうなの?

note.mu


-「Aidemyをやってみた③ ブロックチェーン基礎」

話題のブロックチェーンを実装しながら学べるのはAidemyだけ!

率直な感想を読むことができます!

note.mu


-Aidemyやってみたけど次何したら良いの?を他サービスと比較しつつ解説しています。

note.mu


-「Aidemy基本コース→Udemy学習でPython入門を学ぶと道が開ける」

ブロックチェーン講座について紹介している記事です。

note.mu

 

 

【Aidemy一通り勉強した人向け】実装してみたnote

ここからは、Aidemyで学んだことをアウトプットした記事です。

やってみたいなと思った人で、Aidemyのどのコースを受講すれば良いかわからない人のために、受講ルートを3つに分けたので参考にしてくださいね!

 

Python入門→NumPyを用いた数値計算→Matplotlibによるデータの可視化データクレンジング機械学習概論教師あり学習(分類)ディープラーニング基礎→CNNを用いた画像認識

Python入門→NumPyを用いた数値計算→Matplotlibによるデータの可視化→Pandasを用いたデータ処理データクレンジング機会学習概論教師あり学習(回帰)時系列解析

-「画像認識で『綾鷹を選ばせる』AIを作る」

言わせたい、AIに、「選ばれたのは綾鷹でした」

これをやってみたい人が学ぶといいAidemyの講座:CNNを用いた画像認識

qiita.com


-「ディープラーニングで動画に自動でモザイクをかける『ディープモザイク』作ってみました」

モザイクをかけたい動画が手元にある、、!そんなあなたにディープラーニング!

これをやってみたい人が学ぶといいAidemyの講座:ディープラーニング基礎

karaage.hatenadiary.jp

 

-「【Aidemy × Bio】Aidemyのコースを応用して、遺伝子解析データを学習させてみた」
ハーバード大発。機械学習でデータを要約、その特徴を掴む手綱とする

これをやってみたい人が学ぶといいAidemyの講座:教師なし学習

note.mu


-「【Aidemy × Bio】機械学習はがん細胞を見分けられるか?:遺伝子解析データをもとに教師あり学習(分類〉を行ってみる!」

では、機械学習はがん細胞と正常の細胞を見分けることが出来るのか?

これをやってみたい人が学ぶといいAidemyの講座:教師あり学習(分類)

note.mu


-「【Aidemy × MIT】MITのチュートリアルの教材を利用して、ワインの特性データのクラスタリングを深めてみる!」
ワインのデータを元に、教師なし学習
これをやってみたい人が学ぶといいAidemyの講座:教師なし学習

note.mu

 

--「【Aidemy × MIT】MITのチュートリアルの教材を利用して、ワインの特性データの分類(classification)を行ってみる!」
ワインのデータをもとに教師あり学習を行う

これをやってみたい人が学ぶといいAidemyの講座:教師あり学習(分類)

note.mu


- 「【MITの機械学習講座】回帰編をつかって、エイズウイルスの研究をしてみよう!」
エイズウイルスの薬耐性について回帰を行う

これをやってみたい人が学ぶといいAidemyの講座:教師あり学習(回帰〉

note.mu

 

-「ナスDがセネガルの監督に似ているか確かめるために、似ているワールドカップ出場者を検索するAIをつくった」

自分はどのワールドカップ出場選手に似ているか判別してくれるAI

これをやってみたい人が学ぶといいAidemyの講座:CNNを用いた画像認識

qiita.com


-「AIで日経平均を予言!?2018年末に26,000園(Prophetで楽々予測)」

Facebookの時系列予測ツールPrpophetを活用して投資判断をしてみる

これをやってみたい人が学ぶといいAidemyの講座:時系列解析
startupangels.jp


-「Deep Learningを用いた樹皮画像による樹種同定(Keras,CNN,転移学習,VGG16)」

ディープラーニングでコナラとイチョウを見分ける

これをやってみたい人が学ぶといいAidemyの講座:CNNを用いた画像認識

www.asanohatake.com

 -「女性モデルの分類を主成分分析で挑戦」

どんな体型の人がモデルに向いているのか?

これをやってみたい人が学ぶといいAidemyの講座:教師なし学習

note.mu

 

-「機械学習を使って焼き肉で肉が焼けたかどうかを区別してみた」

焼き肉の焼き加減を区別するAI

これをやってみたい人が学ぶといいAidemyの講座:CNNを用いた画像認識

qiita.com

 

 

エヴァンジェリストの活動
-Aidemyエヴァンジェリストがある一日をシェアしてくれました。

note.mu



 

 

 

Aidemy公式Facebookページでは、こういったアウトプットブログや新講座の情報、さらにセール情報も発信しています。

 

 

 

 

安倍首相は常にネガティブ!?!?

   実行結果
f:id:ryo0927:20180627204643p:plain
初めまして、Aidemy研修生の加賀美です。
今回は、自然言語処理(MeCab)とTwitter APIを使い、安倍首相のツイートを感情分析してみました。

・対象者

 PythonでTwitter APiを使ってみたい人

 MeCabを使って日本語の感情分析をやってみたい人

では、早速Tweitter APIを利用して感情分析をしてみましょう!!

目次

実装

Tweitter APIのkey取得

Twitterデータは、APIを使って取得します。APIを使用するために必要なkeyは以下の4つです。

 ・Consumer Key
 ・Consumer Secret
 ・Access Token
 ・Access Token Secret

これらのkeyを取得する手順の仕方については、以下記事でわかりやすくまとめられていますので、ご参照ください。
www.randpy.tokyo

ツイートを取得する

以下のコードで、特定のユーザのツイートを取得します。

#今回使用するモジュール
import neologdn
import re
import json
from requests_oauthlib import OAuth1Session
import requests
import MeCab
import pandas as pd
import re
from statistics import mean
from bs4 import BeautifulSoup
from datetime import datetime
import matplotlib.pyplot as plt
%matplotlib inline

#カンマの間に取得したkeyを入れてください
consumer_key = ''
consumer_secret = ''
access_token = ''
access_token_secret = ''

#APIの認証
twitter = OAuth1Session(consumer_key,consumer_secret,access_token,access_token_secret)

#ユーザのツイートを取得
url = 'https://api.twitter.com/1.1/statuses/user_timeline.json'

#取得するユーザを指定 @の後の名前
screen_name = "AbeShinzo"

#パラメータ設定 max_id: どのツイートから取得するか決める
params = {'screen_name': screen_name,'count': 100,
         'max_id':1008645666061500419,'include_rts':False,
          'exclude_replies':True,'contributor_details':False}

#リクエストを投げる
res = twitter.get(url,params = params)

#テキスト・時間・IDを追加する場所
tweet_list = []
time_ = []
tweet_id = []

#ツイートを取得 今回は制限に引っかからないようにしていますが、念の為、制限まで行ったらストップするようにしています。’if limit == 1の数字を変更するとより多くのツイートを取得できます。’
for j in range(1):
    res = twitter.get(url,params = params)
    if res.status_code == 200:
        #APIの残りを表示
        limit = res.headers['x-rate-limit-remaining']
        print ("API remain: " + limit)
        if limit == 1:
            #15分間停止させる
            sleep(60*15)
        
        timeline = json.loads(res.text)
        #各ツイートの本文を'tweet_list'に追加
        #各ツイートの投稿日を'time_'に追加
        for i in range(len(timeline)):
            if i != len(timeline)-1:
                tweet_list.append(timeline[i]['text']+'\n')
                time_.append(timeline[i]['created_at'])
                tweet_id.append(timeline[i]['id'])
            else:
                tweet_list.append(timeline[i]['text']+'\n')
                time_.append(timeline[i]['created_at'])
                tweet_id.append(timeline[i]['id'])
                
                params['max_id'] = timeline[i]['id']-1
# 日付の整形 Twitter APIから得られる日付は米国の時間のため日本時間に直す
time_list = []
def make_convert_date_format(src_format, dst_format):
    def convert_date_format(s):
        return datetime.strftime(datetime.strptime(s, src_format),dst_format)

    return convert_date_format

convert_date_format = make_convert_date_format('%a %b %d %H:%M:%S %z %Y', '%Y-%m-%d')
for i in range(len(time_)):
    time_list.append(convert_date_format(time_[i]))
    
# ツイートからURLを削除、不要な文字を消す
def format_text(text):
    text = re.sub(r"(https?|ftp)(:\/\/[-_\.!~*\'()a-zA-Z0-9;\/?:\@&=\+\$,%#]+)", "", text)
    text = re.sub("\d", "", text)
    text = re.sub("\s", "", text)
    text = re.sub("\n","",text)
    text = re.sub("、","",text)
    text = re.sub("#","",text)
    text = re.sub("[()]","",text)
    text = re.sub("・","",text)
    text = re.sub("。","",text)
    text = re.sub("「","",text)
    text = re.sub("」","",text)
    text = re.sub("[a-zA-Z0-9]","",text)
    text = re.sub("@","",text)
    return text

neo_tweet = []
tweet_c = []
for i in range(len(tweet_list)):
    neo_tweet.append(neologdn.normalize(tweet_list[i]))
    tweet_c.append(format_text(neo_tweet[i]))

データフレーム にして確認。paramsのmax_idを変更するとそのツイートよりも前のツイートを取得します。

#データフレームを作成
df = pd.DataFrame({'tweet_list':tweet_list,'time':time_list,'tweet_id':tweet_id})
print(df.head())

f:id:ryo0927:20180621001903p:plain
実行結果
 

ツイートを形態素解析する

先に、以下のリンク先から単語感情極性対応表をダウンロードしてローカルに保存してください。
PN Table

#PN Tableを読み込み
pn_df = pd.read_csv('pn_ja.dic.txt',sep=':',encoding='shift-jis',
                    names=('Word','Reading','POS', 'PN'))
# MeCabインスタンス作成
m = MeCab.Tagger('')  # 指定しなければIPA辞書

# PN Tableをデータフレームからdict型に変換しておく
word_list = list(pn_df['Word'])
pn_list = list(pn_df['PN'])  # 中身の型はnumpy.float64
pn_dict = dict(zip(word_list, pn_list))

# テキストを形態素解析して辞書のリストを返す関数
def get_diclist(text):
    parsed = m.parse(text)      # 形態素解析結果(改行を含む文字列として得られる)
    lines = parsed.split('\n')  # 解析結果を1行(1語)ごとに分けてリストにする
    lines = lines[0:-2]         # 後ろ2行は不要なので削除
    diclist = []
    for word in lines:
        l = re.split('\t|,',word)  # 各行はタブとカンマで区切られてるので
        d = {'Surface':l[0], 'POS1':l[1], 'POS2':l[2], 'BaseForm':l[7]}
        diclist.append(d)
    return(diclist)

# 形態素解析結果の単語ごとdictデータにPN値を追加する関数
def add_pnvalue(diclist_old):
    diclist_new = []
    for word in diclist_old:
        base = word['BaseForm']        # 個々の辞書から基本形を取得
        if base in pn_dict:
            pn = float(pn_dict[base])
        else:
            pn = 'notfound'            # その語がPN Tableになかった場合
        word['PN'] = pn
        diclist_new.append(word)
    return(diclist_new)

# 各ツイートのPN平均値をとる関数
def get_pnmean(diclist):
    pn_list = []
    for word in diclist:
        pn = word['PN']
        if pn != 'notfound':
            pn_list.append(pn)  # notfoundだった場合は追加もしない            
    if len(pn_list) > 0:        # 「全部notfound」じゃなければ
        pnmean = mean(pn_list)
    else:
        # 全部notfoundならゼロにする
        pnmean = 0
    return(pnmean)

# 各ツイートをPN値に変換
pnmeans_list = []
for tw in tweet_c:
    dl_old = get_diclist(tw)
    dl_new = add_pnvalue(dl_old)
    pnmean = get_pnmean(dl_new)
    pnmeans_list.append(pnmean)

PN値に欠損値がないか確認

PN値とは、そのツイートがPositiveかNegativeかどうかを判断する数値です。この数値は+1 ~ -1の範囲で表現されます。+1に近ければポジティブ、-1に近ければネガティブと判断することができます。

a = pd.Series(pnmeans_list)
print(a.head(25))

f:id:ryo0927:20180623033225p:plain
22、23行目に0がありましたこれを確認してみます。
安倍首相のツイートをクリックして後ろの数字をTweet_IDの数字に変更すると該当するツイートに飛ぶことができます。

tn1 = tweet_id[22]
tn2 = tweet_id[23]
print('Tweet_ID 22行目: '+str(tn1))
print('Tweet_ID 23行目: '+str(tn2))

f:id:ryo0927:20180621004243p:plain
22行目のツイート
f:id:ryo0927:20180621004339p:plain
23行目のツイート
f:id:ryo0927:20180621004429p:plain
なぜPN値が0になってしまうのかというと、MeCabという日本語の形態素解析エンジンを使用しているため、英語に対応していないからです。

英語のツイートを除外する

PN値が0の、PN値・ツイート・ツイートID・投稿日を除外して別のリストに保存する

# PN値が0のものを削除
pnl = []
twl = []
tl = []
il = []
for i in range(len(pnmeans_list)):
    if pnmeans_list[i] != 0:
        pnl.append(float('{:.4}'.format(pnmeans_list[i])))
        twl.append(tweet_c[i])
        tl.append(time_list[i])
        il.append(tweet_id[i])

データフレーム にして22、23行目を確認

df1 = pd.DataFrame({'tweet':twl,'time':tl,'tweet_id':il,'PN値':pnl})
print(df1.iloc[22])
print(df1.iloc[23])

f:id:ryo0927:20180623033937p:plain
上の画像を見ると、英語のツイートがなくなっていることが確認できます。

PN値を見て、ポジティブかネガティブか判断する

さて、ここからが本題です。PN値の平均を出して、普段ポジティブとネガティブのツイートどちらが多いのか確認していきます。

#データフレームの作成
df = pd.DataFrame({'tweet':twl,'time':tl,'tweet_id':il,'PN値':pnl})

# 全体のPN値の平均を計算
_mean = df['PN値'].mean()
print('平均: '+str('{0:.4}'.format(_mean)))
pnl_max = max(pnl)
print('最大値: '+str(pnl_max))
pnl_min = min(pnl)
print('最小値: '+str(pnl_min))

print()
# PN値が0以上のものを抜き出す
for i in range(len(il)):
    if pnl[i] >= 0:
        print('PN値0以上: '+str(df.iloc[i]))
print()

# PN値が最小のものを抜き出す
for i in range(len(pnl)):
    if pnl[i] == pnl_min:
        print('PN値最小:'+str(df.iloc[i]))
print()

#ツイート毎にPN値をプロットする
plot_df = pd.DataFrame(pnl,index=tl)
plot_df.plot.bar(figsize=(20,10))
plt.tick_params(labelsize = 15)
plt.tight_layout()
plt.show()

f:id:ryo0927:20180623035750p:plain
f:id:ryo0927:20180627203500p:plain
ほう、グラフにするとかなりわかりやすいですね。12/20日以外は常にネガティブなツイートをしているようですね(笑)

よし、完成!じゃないんです。まだ、正規化を行っていません。
上で定義した、「def get_diclist(text)」を下記のように変更してください。

# ストップワードを取得
def stopwords():
    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_=str(soup).split()
    return stop_

# 正規化
def dictionary_words(text):
    stop_word = stopwords()
    parsed = m.parse(text)# 形態素解析結果(改行を含む文字列として得られる)
    lines = parsed.split('\n')  # 解析結果を1行(1語)ごとに分けてリストにする
    lines = lines[0:-2]         # 後ろ2行は不要なので削除
    diclist = []
    for word in lines:
        l = re.split('\t|,',word)  # 各行はタブとカンマで区切られている
        d = {'Surface':l[0], 'POS1':l[1], 'BaseForm':l[7]}
        if d['POS1'] == '名詞' or '動詞' or '形容詞' or '副詞':
            if not d['BaseForm'] in stop_word:
                diclist.append(d)
    return (diclist)

そして、下記も変更してください

# 各ツイートをPN値に変換
pnmeans_list = []
for tw in tweet_c:
    dl_old = dictionary_words(tw) #ここを変更
    dl_new = add_pnvalue(dl_old)
    pnmean = get_pnmean(dl_new)
    pnmeans_list.append(pnmean)

すると,,,,
f:id:ryo0927:20180623040014p:plain
f:id:ryo0927:20180627204643p:plain
グラフの見え方とPN値が変わりましたね。
正規化で「名詞・動詞・形容詞・副詞」だけを抽出することによって、それ以外の品詞にPN値が引っ張られないようにすることができます。

PN値が最大、最小のツイートを確認する

PN値が最大のツイートを見てみましょう
f:id:ryo0927:20180621033503p:plain
すごいポジティブ!!(笑)
「新しい」、「創っていってくれる」、「全力」、「支援」というキーワードがPN値を上げてくれる要因になったのかなと思いました。

次にPN値が最小のツイートを見てみます
f:id:ryo0927:20180621033642p:plain
いや、どんだけブルガリアに行きたくないんだ(笑)
「凍った」、「昼食後」がPN値を下げているのかなと思いました。

感想

今回は特定のユーザのツイートを取得してみましたが、次はキーワードやハッシュタグを取得して感情分析を行って行きたいと思っています。
自分でプログラムを組むと勉強したことの復習や、勉強で出てこなかった部分がとても勉強になりました。
「もう少しコードが短くなるよ!」「こんなやり方があるよ!」などありましたらぜひ教えてください!

Udacityのデータセットを深層学習させて、自動運転をしてみた

f:id:ryoryo4138:20180611185223j:plain

Aidemy研修生 岡前です。

Using Deep Learning to Predict Steering Angles

なんか英語の方が響きがいい気がする。

やはり、"かっこいい"こと、"ワクワク"する事(大抵、激しく難しい)をやって、基礎のお勉強がやりたくてしょうがなくなりますよね。という事で、"自動運転"というビックワードに背伸びしてみました。 

対象者:

深層学習(CNN)で何ができるか知りたい人(とか)

記事を読了することで期待される成果:

なんか深層学習すごくね?って思える。(pythonとUnityも)

深層学習の具体的な使い方の一例を知ることができる。

概要:

UdacityのデータセットのPhase 2 dataCh2_002を使って、

画像とハンドルの正解データからハンドルの角度を回帰で求める。

次に、Unityでシュミレーションを作り、自動車を自動運転してみる。

github.com

目次:

  1. Modelを理解する
  2. データの前処理
  3. 深層学習(python)
  4. シュミレーション(Unity)
  5. 結果
  6. 発展
  7. 感想
  8. 環境

サクッと見たい方は4.深層学習から見てください。その後戻って1.2.3を見る形で大丈夫です。

1. Modelを理解する

まずはどのように機械学習をするモデルなのかを見てみましょう。

f:id:ryoryo4138:20180611183446p:plainf:id:ryoryo4138:20180611183454p:plain

2700万個ノードが繋がっていて、25万パラメータあるそうです。

これがEnd to End Learningの論文です。

https://arxiv.org/pdf/1604.07316.pdf

以下に、論文の内容をまとめたので軽く目を通すと、

4.以降のコードがわかりやすくなると思います。

1.論文名,著者,会社,出版年

End to End Learning for Self-Driving Cars,Mariusz Bojarski,NVIDIA,2016

2.論文の内容

自動車の走行時の正面の写真とハンドルの角度のみの正解データを使って、道路の特徴を学習して、舗装されていない道を含めた全ての道のハンドルの角度を回帰するモデルを提案している

3.研究背景

Computer Vision and Pattern Recognition(パターン認識)の自立走行の分野

4.研究目的

より少ないトレーニングデータと小さなシステムで、より精度の高いハンドルの角度を算出すること。

5.新規性

車線認識や、径路計画(自立移動技術)などは道の環境を場合分けして学習する必要があるが、end-to-end systemでは、同時に舗装されていない道や駐車場などのこれまで学習が難しかった全ての道をまとめて学習できる点。

6.結果

自動運転を実験した結果、高速道路や住宅街などの違いだけでなく、天気などの様々な条件で運転するのに、end-to-end systemは100時間未満のデータで十分であった。

 

つまり、このモデルを使うと、限られたデータしかなくても運転できるって事です!

なので、今回はハンドルのデータのみを使います。

 

3. データの前処理

2.1 インストール

先ほどのUdacityのURLの中の、

Phase 2 dataCh2_002をインストールします。

2.2 RAG BAGファイルをpngに変換する

rag bagファイルをpngに変換します。参考文献を載せておきます。

ja/rosbag/Tutorials/Exporting image and video data - ROS Wiki

2.3 画像を深層学習のモデルに合うように加工
# 画像の加工
def roi(img):
    #img =img[55:140,30:290]
    # img =img[40:img.shape[0]-25
    #img = img[60:140,40:280]
    return cv2.resize(img,(200,66), interpolation=cv2.INTER_AREA)

#画像を読み込む
def image_generator(path):
    fname = os.path.basename(path)
    img = plt.imread("center/"+fname)

    #Crop and Resize the image
    img = roi(img)
    #Reshape the image
    img = np.reshape(img,(3,66,200))
    return img

#画像を逐次処理
def batch_generator(file, batch_size=32, *args, **kwargs):
    images = []
    steering = []
    df = pd.read_csv(file)
    while True:
        for key, row in df.iterrows():
            try:
                # Reset generator if over bounds
                img = image_generator(row.fullpath.replace('jpg', 'png'))
            except Exception as e:
                # print(e)
                continue
            ster = row.angle
            images += [img.tolist()]
            steering += [ster]
            if len(images) == batch_size:
                yield np.array(images), np.array(steering)
                images = []
                steering = []

4. 深層学習

私は、3.に多くの時間を費やしました。初めてデータを扱う人は、多くの人が3.に時間がかかると思います。理由は、勉強する内容が次から次へと出てくるためです。私は、次回以降も含めた勉強だと思い、何日か頑張りました。(助けてもらいながらやりました。)

いよいよ、上記の構造を用いて,以下のコードでモデルをトレーニングします。

github https://github.com/RyosukeHonda/Behavioral-Cloning 

model.pyを参考にさせていただきました。

#Make dataset
train_data = batch_generator("train_round1.csv", batch_size=5)
val_data = batch_generator("evaluation.csv", batch_size=5)
#Nvidia Model
model = Sequential()
#model.add(Lambda(lambda x: x/255.0-0.5,input_shape=(3,66,200),name="Normalization"))
model.add(Conv2D(24, (5, 5), strides=(2,2), activation='relu', data_format='channels_first', name='Conv1', input_shape=(3, 66, 200)))
model.add(Conv2D(36, (5, 5), strides=(2,2), activation='relu', data_format='channels_first', name='Conv2'))
model.add(Conv2D(48, (5, 5), strides=(2,2), activation='relu', data_format='channels_first', name='Conv3'))
model.add(Conv2D(64, (3, 3), activation='relu', data_format='channels_first', name='Conv4'))
model.add(Conv2D(64, (3, 3), activation='relu', data_format='channels_first', name='Conv5'))
model.add(Flatten())
model.add(Dropout(0.4))
model.add(Dense(100, activation='relu', name='FC1'))
model.add(Dropout(0.5))
model.add(Dense(50, activation='relu', name='FC2'))
model.add(Dropout(0.5))
model.add(Dense(10, activation='relu', name='FC3'))
model.add(Dropout(0.5))
model.add(Dense(1, name='output'))
model.summary()
opt = Adam(lr=0.0001)

いざ終わって見ると、簡潔なコードですね。

5. シュミレーション(Unity)

ここからは、深層学習ではなく結果を確かめる段階です。これを元にUnityで自分で道を作ります。今回はシュミレーションをUnityで作るのは趣旨と外れてしまうので行わないことにしました。Youtubeに載っていた結果だけ引用します。

 

github.com

 

www.youtube.com

 

6. 結果

ハンドルの角度のみを正解データから、回帰してハンドルの角度を予測できるようになりました。

7. 発展

今回は、ハンドルの角度のみを正解データとしました。自動運転には他に速度やノイズ、自動車の位置などの様々なデータを分析します。そのモデルは、線形非線形の問題で重積分をしなくてはいけないため、理論が難しくなり、今回の趣旨である深層学習を使って"かっこいい"こと、"ワクワク"する事をやりたいというのと外れるため、やりませんでした。という事で発展として、3termで$2400のUdacityのコースをあげておきます。nano degreeが手に入るといえ、日本の学習への常識からすると高価だと思いました。

eu.udacity.com

8. 感想

深層学習はモデルがたくさんGit Hubにあるため、最初は大変ですが、慣れれば多くの面白いことができるのだと夢を膨らせました。一方で、論文を見て、そのモデルが作られた目的や先行研究などを理解していくことは今後に繋がりますし、モデルを理解することも結局エラーがどこにあるのかを発見するのに役立った部分もありました。

また、私自身が初めての深層学習のアウトプットであったことから、勉強時間を「初心者のため前提知識や背景を学ぶために投下している時間」と「自動運転をするために投下している時間」に分ける事で、毎日の勉強を振り返る事ができる、と初心者らしい気づきを書いて締めさせていただきます。

読んでいただきありがとうございました。

 

7. 環境

MacBook Pro (13-inch, 2016, Four Thunderbolt 3 Ports)

何を使えばいいのかを使用したコードを使って示しておきます。

import keras
from keras.layers.normalization import BatchNormalization
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation, Flatten,Lambda
from keras.layers.normalization import BatchNormalization
from keras.layers import Conv2D
from keras.layers.pooling import MaxPooling2D
from keras.preprocessing.image import load_img,img_to_array
from keras.optimizers import SGD, Adam, RMSprop
from keras.callbacks import EarlyStopping,History
from keras.preprocessing.image import load_img
from keras.models import load_model
import cv2
import numpy as np
import pandas as pd
import os
from numpy.random import randint
import matplotlib as plt

 

 

機械学習で探すJ-POPあるある

はじめまして。aidemy研修生の川島と申します。

 

さて、いきなり個人的な近況報告で申し訳ないのですが、最近いろんな音楽を積極的に聞こうとしています。なかでもJ-POPは知っている曲の幅を広げようと頑張っております。

J-POP、僕は好きです(ここ大事)。で、これは僕がJ-POPを好きだという前提で聞いてもらいたいのですが、J-POPの歌詞って臭いのが多くないですか?

あいつらはすぐ誰かを愛しますし、手をつないで歩きますし、多分君のことが世界で一番大事ですし。これ、少しは感じたことのある人も多いはずです。

この件に関しては、専門家(?)の非常に有用な先行研究がありました。パーマ大佐さん(以下敬称略)という芸人の方の、「J-POPの歌詞あるあるの歌」(以下「あるあるの歌」)というものが存在します。


パーマ大佐 - 「J-POPの歌詞あるあるの歌」 Music Video

 

こちらがその、パーマ大佐の「あるあるの歌」です。暇な人はぜひ見てみてください。暇じゃなければ見なくてもいいです(でも面白いと思うよ)。

 

ここから先、少しだけ「あるあるの歌」のネタバレが入ることもありますので、ご了承ください。

 

この歌、ある程度J-POPを聞いたことのある人なら、ニヤッとさせられたりクスッと来たりする場面が多々あると思います。でも、「よくよく考えると具体的なのがすぐには思いつかないな……。本当にあったっけ?」ってこともないでしょうか。……僕はいくつかありました(多分J-POPに精通していないせい)。というわけで前置きが長くなりましたが、今回はpythonを用いてJ-POPの歌詞によく見られるフレーズを自分なりに分析していきたいと思います。

 

0.目次

 

1.環境

主なものを載せておきます。

・python 3.6.5

・anaconda 1.6.14

・jupyter 4.4.0

・numpy 1.14.3

・Janome 0.3.6

・gensim 3.4.0

・scikit-learn 0.19.1

2.フレーズから見る「あるある」

最初は、J-POPにおける「あるある」なフレーズを探していくのを目標にしたいと思います。

 

2-1.スクレイピングによる歌詞の取得

まずはスクレイピングにより、分析対象となる歌詞の一覧を引っ張ってきましょう。

今回は、Uta-Netさんのこちらのリンクから引っ張ってきました。楽曲ごとの月間アクセス数です。2018年6月9日現在、米津玄師、RADWIMPS、WANIMAなどのアーティストが多く見られます。

www.uta-net.com

 

 

コードは以下です。引数urlにより上記のURLを渡すと、4ページ目で50曲ずつ、計200曲を取り出してきてくれます。改行ごとに区切ったりアーティスト名や曲名を取得したりといった作業は分析には使いませんが、あとあと分析結果の表示の際に使うので一緒にやっちゃっています。

また、たまに同じ曲が2回ランクインしている場合があるので、それもあらかじめ抜いておきます。同じ曲が2回ってどういうこと?と思われるかもしれませんが、今回だと以下の2組がそれに該当します。

・打上花火(米津玄師) と 打上花火(DAOKO×米津玄師)

・スパークル [original ver.](RADWIMPS) と スパークル (movie var.)(RADWIMPS)

実は最初はこれがあるのに気づかず、バグかと思いずっとバグを探していました……。

 

from time import sleep
import urllib.request
from bs4 import BeautifulSoup
import re

#歌詞ページのURLから歌詞本体を取得
#関数scrapingから呼び出される
def song_scraping(url):
    s_html = urllib.request.urlopen(url)
    s_soup = BeautifulSoup(s_html, "lxml")
    lyric = s_soup.find("div", id="kashi_area")
    
    #HTML上で</br>区切りで改行されているのを用い、正規表現で行ごとの文を取得
    #行ごとに分ける必要がなければ lyric.text でも良い
    lines = re.findall(r'>.*?<', str(lyric))
    lines_cut = []
    for line in lines:
        #何もない行を除く
        if len(line) > 2:
            #空白が"\u3000"になっているので置換
            line = line.replace("\u3000", " ")
            lines_cut.append(line[1:len(line)-1])
    return lines_cut
            

#曲の一覧のurlが渡される
def scraping(url):
    lyrics = []   
    songs = []
    artists = []
    url_list = []
    for i in range(1,5):
        url_list.append(url + "?p=" + str(i))
    #それぞれのページについて
    for u in url_list:
        html = urllib.request.urlopen(u)
        soup = BeautifulSoup(html, "lxml")
        links = soup.find_all("a")

        for link in links:
            link_content = link.get("href")
            if link_content[0:5] == '/song':
                #曲へのリンクを見つけた場合そこに飛ぶ
                song_url = 'https://www.uta-net.com' + link_content
                lyrics.append(song_scraping(song_url))
                songs.append(link.text)
            elif link_content[0:7] == '/artist':
                #アーティストへのリンクを見つけた場合そのアーティスト名を保存
                artists.append(link.text)
        sleep(0.5)
    return lyrics, songs, artists

#曲のリストから歌詞の1行目を参照して同一と思われる曲を削除
def cut_same_song(lyrics, songs, artists):
    cut = []
    for i in range(len(songs)):
        for j in range(i+1, len(songs)):
            if lyrics[i][0] == lyrics[j][0]:
                cut.append(j)
    cut.sort()
    for k in range(len(cut)):
        del lyrics[cut[k]-k]
        del songs[cut[k]-k]
        del artists[cut[k]-k]
    return lyrics, songs, artists

url ="https://www.uta-net.com/user/ranking/monthly.html"
lyrics, songs, artists = scraping(url) lyrics, songs, artists = cut_same_song(lyrics, songs, artists)

2-2.形態素解析を用いた歌詞の分析 

上記の作業により、二重配列lyricsには配列で各曲の歌詞が、さらに配列で行ごとに分けられて入っています。これを基に歌詞によく出てくるフレーズを分析していきましょう。具体的な手順としては、シンプルですが以下のようなものです。フレーズはおおよそ2単語の組み合わせにより作られるという考えに基づいています。

・歌詞から形容詞、形容動詞、名詞、動詞を順に抜き出す。

・曲ごとに、ある程度近くにあった2単語に組に加点をする。

・一定数以上の曲にあった(一定以上の点を取った)単語の組を抽出する。

 

歌詞から形容詞、形容動詞、名詞、動詞を抜き出すのは、janomeを用いて形態素解析をすることで可能となります。形態素解析とは、文を単語ごとに抜き出して品詞や活用形などを特定することを指します。さらに形態素解析では、動詞などの原形を取得したり品詞の細かい分類を取得できたりします。便利……!

今回のコードではこれを利用し、細分類が「非自立」の単語は抜いておきました。「知ってる」の「てる」や「君のこと」の「こと」が非自立に含まれます。大雑把に言えば特に意味のない単語というわけですね。ただ、それでもたまに不要な単語が抽出されるので、手作業で不要な単語を登録しそれは抽出しないようにしておきます。しかし今回の場合は不要な単語が大きく分けて2種類存在します。具体的には、

・意味のあまりない単語

・異常に出現するけどそこまで興味のない単語

です。なぜこれを分けるのかは少しあとの方で説明するとして、以下が無視する単語リストです。

 

trash = [ "(", ")", "(", ")",  "'", ".", ",", "&", "&", ":", ";", "!", "?", "…", "-", "!", "?", "a", "m", "s", "t", \
         "I", "me", "you", "You", "we", "We","the", "that", "it", "is", "be", "are", "Are", "to", "ll", "re", "ve", \
         "say", "in", "for", "of", "and", "one", \
         "oh", "yeah", "wow", "baby", "Baby", \
         "さ", "ら", "す", "かな", "かん", \
          "人", "あと", "たち", "もと", "方", "先", "前", "度", "次", "気", "最後", "すべて", "場所", "全て", "的", "底", \
         "れる", "られる", "なる", "する", "いる", "ある", "くる", "いく", "せる", "ない", "いい", \
          "一", "二", "2", "2"]
         
block = ["それ", "そう", "どれ", "どこ", "ここ", "これ", "誰", "何", "なん", \
         "僕", "私", "君", "あなた", "自分", "ぼく", "僕ら", "僕たち", "今", \
         "いま", "いつ", "いつか", "今日", "明日", "日々", \
         "夢",  "幸せ", "気持ち", "恋", "愛", "言葉", "心", "好き", "想い", "未来", "勇気", "笑顔", "道", \
         "思う", "言う", "見る", "行く", "笑う", "愛す", "信じる", \
         "知る", "わかる", "できる", "出来る", "会う", "分かる", "なれる", "見える", \
          "愛しい", "優しい"]

 

前者trashが「意味のあまりない単語=ほかの助動詞などと同様にすっ飛ばす単語」、後者blockが「異常に出現するけど興味のない単語=ほかの名詞などと同様にカウントされるが加点のランキングに参加しない単語」です。

 

このストップワードリストについては少し前の記事「Pythonで国会議事録から、話題の政治ワードを抽出してみた」でも触れられていますが、そことは少し違う語録になっていると思います。だってJ-POPのやつら、やたら笑うしやたら愛するし、好きだの勇気だの未来だの笑顔だの、びっくりするくらい出現するんで……。あと、サビにめっちゃ英語になるんで、英語もある程度リストに入れておきました。「あるあるの歌」にある「間が埋められなくなるとすぐLaLaLaとかHeyとかBabyって言う」っていうの、まさにですね。

さて、trashとblockをわざわざ分けた意味について少しだけ触れておきます。もしblockをtrashと同様にすっ飛ばしてしまうと、何連続にも単語がすっ飛ばされ、その結果あまり近くにいない単語の組に加点してしまうことがしばしば起こるのです。そのため、 blockの単語は「加点には参加しないがすっ飛ばしはしない単語」ということでtrashと分けたのです。

また、加点対象となる単語の近さの基準ですが、これは前後2単語までとしました。例えば「暗闇であなたの背をなぞった」(米津玄師 Lemon)という歌詞では、

「暗闇」「あなた」「背」「なぞる」が抽出され、「あなた」はblockの対象なので「あなた」を含む組には加点されず、「暗闇」「背」や「背」「なぞる」といった2単語の組に加点され、「暗闇」「なぞる」の組は遠すぎるとみなし加点されません。

少し説明が長くなりましたが、コードは以下です。

 

import numpy as np
from janome.tokenizer import Tokenizer

#単語の組み合わせごとの出現頻度を調査
#lines: 歌詞が行ごとにリストで渡される
#dic_id: 今までに登場した単語一覧とそれらのIDの入った辞書
#dic_len: 引数として渡された時点でのdic_idの長さ
#mat: 二次元配列。それぞれの単語の組について、点数を保存している。
#例えば mat[0][3]==1 なら、dic_idでIDが1の単語と3の単語の組み合わせが存在した曲が1曲あったことになる。
#trash: 無視する単語リスト
#block: 単語として意味はあるが無駄に多く登場するので点数をつけない単語リスト
#dic_id, dic_len, mat を更新して返す
def analyze(lines, dic_id, dic_len, mat, trash, block):
    #1つ前、2つ前の単語
    pre = '\0'
    pre2 = '\0'
    #1つ前、2つ前の単語がblockに含まれていないかをここに保存。含まれていたら1が立つ。
    pre_block = [0, 0]
    #この曲で現れた単語の組み合わせはすべてこの二次元配列に保存する。
    mat_temp = np.reshape(np.zeros(len(mat)**2, dtype='uint8'), (len(mat), len(mat)))
    
    t = Tokenizer()
    for line in lines:
        #各行を単語に分ける(形態素解析)
        tokens = t.tokenize(line)  
        for token in tokens:
            #品詞分類
            part_of_speech = token.part_of_speech.split(",")[0]
            part_of_speech_sub = token.part_of_speech.split(",")[1]
            #原形を取得
            base_form = token.base_form


            if (part_of_speech == "名詞" or part_of_speech == "動詞" or part_of_speech == "形容詞" or part_of_speech == "形容動詞")\
                    and part_of_speech_sub != "非自立" and not base_form in trash:
                if not base_form in block:
                    #ここからカウントの対象とみなされた単語に対して行う操作
                    if not base_form in dic_id:
                        #新出の単語ならそれのために dic_id, mat_temp を拡張
                        dic_id[base_form] = len(dic_id)
                        if len(mat)==0:
                            mat = np.array([[0]], dtype='uint8')
                            mat_temp = np.array([[0]], dtype='uint8')
                        else:
                            mat = np.append(mat, np.reshape([np.zeros(len(mat), dtype='uint8')], (-1, 1)), axis=1)
                            mat = np.append(mat, [np.zeros(len(mat)+1, dtype='uint8')], axis=0)
                            mat_temp = np.append(mat_temp, np.reshape([np.zeros(len(mat_temp), dtype='uint8')], (-1, 1)), axis=1)
                            mat_temp = np.append(mat_temp, [np.zeros(len(mat_temp)+1, dtype='uint8')], axis=0)
                    #単語の組に加点
                    if not pre == '\0' and pre_block[0]==0:
                        i = dic_id[base_form]
                        j = dic_id[pre]
                        if i>j:
                            i, j = j, i
                        mat_temp[i][j] = 1
                    if not pre2 == '\0' and pre_block[1]==0:
                        i = dic_id[base_form]
                        j = dic_id[pre2]
                        if i>j:
                            i, j = j, i
                        mat_temp[i][j] = 1
                    #pre_blockの更新
                    pre_block[1] = pre_block[0]
                    pre_block[0] = 0
                else:
                    pre_block[1] = pre_block[0]
                    pre_block[0] = 1

                pre2 = pre
                pre = base_form
    #この曲で加点された結果を足して上書き
    mat = mat + mat_temp
    return dic_id, len(dic_id), mat


dic_len = 0
dic_id = {}
mat = []

for lyric in lyrics:
    dic_id, dic_len, mat = analyze(lyric, dic_id, dic_len, mat, trash, block)

 

これが終われば、二次元配列matにはそれぞれの2単語の組の点数が保存されています。

 

2-3.結果の表示

さて、分析作業はこれで終わりなので、出力はこの関数にお願いします。1曲分の歌詞を渡すと対象となった単語の登場部分を探索し、もしあればその該当付近の行、アーティスト、曲名を見せてくれるものです。

 

#歌詞と2つの単語を受け取り、その2つの単語の組み合わせを見つけ次第表示する
def show_example(lines, song, artist, word1, word2, trash, block):
    #今何行目にいるか
    now_line = 0
    #1つ目の探すべき単語が直近にあるかどうか。
    #現在見ている単語が探すべき単語なら1、1つ前の単語が探すべき単語なら2、
    #2つ前の単語が探すべき単語なら4を示す。
    flag1 = 0
    #同様に、2つ目の探すべき単語が直近にあるかどうか。
    flag2 = 0
    pre = "\0"
    pre2 = "\0"
    t = Tokenizer()
    for line in lines:
        tokens = t.tokenize(line)
        for token in tokens:
            part_of_speech = token.part_of_speech.split(",")[0]
            part_of_speech_sub = token.part_of_speech.split(",")[1]
            base_form = token.base_form


            if (part_of_speech == "名詞" or part_of_speech == "動詞" or part_of_speech == "形容詞" or part_of_speech == "形容動詞")\
                    and part_of_speech_sub != "非自立" and not base_form in trash and not base_form in block:
                if base_form == word1:
                    flag1 = 1
                if base_form == word2:
                    flag2 = 1
                if flag1+flag2 == 3 or flag1+flag2 == 5:
                    #この場合は見つかったので表示
                    print(song, artist, ':')
                    if now_line == 0:
                        print(lines[now_line])
                    else:
                        print(lines[now_line-1]+lines[now_line])
                    return True
                pre2 = pre
                pre = base_form
                flag1 *= 2
                flag2 *= 2
                flag1 %= 8
                flag2 %= 8
        now_line += 1
    #見つからなかった場合
    return False

 

そしてこの関数を以下のように実行します。

 

#「あるある」の可能性がある単語の組の表示
for i in range(len(mat)):
    word1 = [key for key, v in dic_id.items() if v==i][0]
    for j in range(i+1, len(mat)):
        word2 = [key for key, v in dic_id.items() if v==j][0]
       #今回は3回以上登場した組を抽出してみる
        if mat[i][j] >= 3:
            print('「' + word1 + '」「' + word2 + '」にかんする' + str(mat[i][j]) + '個の「あるある」が検出されました。')
            print()
            found_num = 0
            #歌詞の探索
            for lyric, song, artist in zip(lyrics, songs, artists):
                found = show_example(lyric, song, artist, word1, word2, trash, block)
                if found==True:
                    found_num += 1
                #表示は最大4曲までとする
                if found_num == 4:
                    break
            print()

 

とりあえず前半はこれで作業終了です。あとは実行して寝るだけ。おやすみなさい。

 

2-4.実行結果

さて、結果を見てみましょう。例えば、こんな感じで表示されます。9個はかなり多い方ですね。

 

「手」「伸ばす」にかんする9個の「あるある」が検出されました。

春雷 米津玄師 :
あなたにはこの世界の彩りが どう見えるのか知りたくて今頬に手を伸ばした 壊れそうでただ怖かった
スパークル [original ver.] RADWIMPS :
運命だとか未来とかって 言葉がどれだけ手を伸ばそうと届かない 場所で僕ら恋をする
ロメオ LIP×LIP :
潤んだ瞳 嘆く唇に触れたいと手を伸ばした
HANABI Mr.Children :
もう一回 もう一回僕はこの手を伸ばしたい

 

ということで、4曲以上に登場した「あるある」をいくつか以下に抽出しました。ついでに、「あるあるの歌」に似たようなもの、関連がありそうなものがあれば一緒に載せています。参考にしてください(何の?)。ちなみに3曲以上、2曲以上という条件にすれば、もっと「あるあるの歌」とかぶるものが出てきます。

4曲以上だと、思っていた以上に「あるある」っぽいものが的確に抽出されているように思います。3曲だとやはり、「何だこれは」というのもたまに入っていたりするのですが。当然、2曲以上という条件で抽出してみるとそれなりの頻度で変なのが入ってきました。偶然かぶるというのはわりとありますからね……。

 

あるある 抽出された単語 登場曲数 「あるあるの歌」対応箇所
やたら胸が痛む 「胸」
「痛い」
5  
大事なものは
すぐ遠く離れる
「離れる」
「遠い」
5 「何故J-POPの歌詞っていつも
大切な人がすぐいなくなるの」
すぐ涙を流す 「涙」
「流す」
8 「すぐに涙を流す」
すぐ風が吹く 「風」
「吹く」
11 「すぐ風になる」
(惜しい……)
すぐ夜が明ける 「夜」
「明ける」
5  
空がやたら青い 「青い」
「空」
7  
めっちゃ空を
見上げる
「空」
「見上げる」
5 「どうせ愛している君は今日も
同じ空の下にいるのだろう」
生きる意味を
やけに考える
「生きる」
「意味」
5  
すぐ目を閉じる 「目」
「閉じる」
11 「どうせ愛している君は僕の
瞼の裏に潜んでいるのだろう」
なぜか夏ばっかり
終わりが来る
「夏」
「終わる」
4  
すぐ花が咲く 「花」
「咲く」
6 「すぐ桜咲く」
(※「桜」「舞う」は3曲)
やたら手をつなぐ 「手」
「繋ぐ」
10  
しかも手を掴む 「掴む」
「手」
7  

 

わりと「言われてみれば……!」というのが多く感じるのではないでしょうか(もし感じなかったとしても、僕は多く感じましたので許してください)。

 

3.単語から見る「あるある」

さて、ここから後半です。先ほど配列blockにより「やたら登場するけど興味のない単語」を抜いておきました。単体であまりに頻繁に登場するし他の単語との組み合わせも興味がないので抜いたわけですが、よくよく考えてみればこれらの単語自身が単体で「あるある」です。ということで、このような単語が登場する箇所を見たい気持ちがあります。……とは言っても、単純に抽出して表示するとやばいんです。というのも、例えば「笑う」だと198曲中64曲に登場するのです。そんなにたくさん表示されると流石に見ていられないというのが率直な気持ちです。じゃあいくつかピックアップすればいいじゃないかという話ですが、いろんな文脈で「笑う」が使われるわけで、ランダムにピックアップしてもあまり面白い結果は得られません。というわけでここでは、「対象の単語がある程度似たような雰囲気で使われている箇所だけに絞ってピックアップ」ということを、pythonの機械学習にかんするライブラリの力を借りて行いたいと思います。

 

3-1.Doc2Vecを利用した準備

まずは取得した歌詞を用いて、Doc2Vecに学習させモデルを作成します。Doc2Vecは、大雑把に言えば文書をベクトル化することにより、文書の類似度などを定量的に示すことを可能にしてくれるものです。自然言語を処理しやすい数値に変換してくれるという点で、とても便利な機能なわけです。学習は以下のように行います。

from gensim.models.doc2vec import Doc2Vec
from gensim.models.doc2vec import TaggedDocument
from janome.tokenizer import Tokenizer

#スクレイピングで取得した1曲分の歌詞を学習できるように成形する
def make_training_doc(num, lines):
    t = Tokenizer()
    #改行で区切っていたのを結合する
    lyric = ''.join(lines)
    #janomeの形態素解析の分かち書きを利用する
    training_token = t.tokenize(lyric, wakati=True)
    training_doc = TaggedDocument(words=training_token, tags=[num])
    return training_doc

training_docs = [] for num, lyric in enumerate(lyrics): training_docs.append(make_training_doc(num, lyric)) #学習 model = Doc2Vec(documents=training_docs, min_count=1)

 

一方で、対象となる単語が出てくる箇所は予め抽出しておきます。対象の単語が「笑う」の場合、以下のようになります。

 

#対象の単語が含まれる部分を抽出
#見つかったかどうかと上書きしたlines_includingを返す
def pickup(lines, word, lines_including):
    t = Tokenizer()
    for i, line in enumerate(lines):
        tokens = t.tokenize(line)
        for token in tokens:
            base_form = token.base_form
            if (base_form == word):
                #見つかった場合
                if i==0:
                    lines_including.append(line+lines[i+1])
                elif i==len(lines)-1:
                    lines_including.append(lines[i-1]+line)
                else:
                    lines_including.append(lines[i-1]+line+lines[i+1])
                return lines_including, True      
    return lines_including, False


#対象の単語が含まれる付近の分をリストに格納
lines_including = []
#対象の単語が含まれる曲番号を順に格納
index_including = []
#対象の単語
word = "笑う"

for i, lyric in enumerate(lyrics):
    lines_including, found = pickup(lyric, word, lines_including)
    if found:
        index_including.append(i)

 

3-2.Doc2Vecによる対象箇所のベクトル化

これで、モデルと調べる対象が作成されました。作成したモデルに、「笑う」が出てくる近辺の文をベクトル化してもらいます。ここでは、特に引数を指定していないので、それぞれの文が100次元ベクトルで表されます。

 

#ベクトル化した結果を格納
vecs_including = np.array([])
t = Tokenizer()

#文書のベクトル化
for line_including in lines_including:
    token_including = t.tokenize(line_including, wakati=True)
    if len(vecs_including) == 0:
        vecs_including = np.array([model.infer_vector(token_including)])
    else:
        vecs_including = np.append(vecs_including, np.array([model.infer_vector(token_including)]), axis=0)

 

というわけで、言語がベクトルに置換されました。ここからは、ベクトルの値がなるべく近いものを集め、最も近かったものたちを表示します。これにより、「笑う」という単語が歌詞の中では主にどのように使われるのかが見られるわけです。

 

3-3.クラスタリングによる「あるある」の抽出

では、どのようにベクトル値が近いものを集めるかという話ですが、ここではクラスタリングを利用します。クラスタリングは教師なし学習の一つであり、点の集合を指定されたグループの数に、なるべく近いものどうしでグループ分けしてくれるものです。pythonではsklearnというライブラリにこのアルゴリズムを行ってくれるものがあるので、これを使うことにします。今回の例では、64個の100次元空間の点のクラスタリングを行うことになります。以下がそのコードです。

今回は最終的に10個弱の例を表示すればいいかなということで、全体を8で割った数をグループ数にしています。

 

from sklearn.cluster import KMeans

#いくつのグループに分けるか
groups = len(vecs_including)//8
#学習
km = KMeans(n_clusters=groups, max_iter=500, tol=1e-10)
#それぞれのベクトルの分類先のグループのIDを配列に格納
group_id = km.fit_predict(vecs_including)

これで、ベクトルがグループ分けされました。それぞれのグループにおける分散を求め、一番分散が小さいグループの歌詞を表示することにします。分散が小さいということはそれだけ文書の意味がグループ内で似ているということなので、最も「あるある」らしき歌詞群が得られるわけです。

 

#それぞれのグループの分散を求める
vars_including = []
for i in range(groups):
    vars_including.append(np.linalg.norm(np.var(vecs_including[group_id==i], axis = 0)))
    
#分散が小さい順になるようにインデックスをソート
vars_argsort = np.argsort(vars_including)

i=0
#表示されるグループのID
group_shown = 0

while(True):
    #曲が1つか2つしかグループに含まれていない場合は飛ばす
    if np.sum(group_id==vars_argsort[i]) > 2:
        group_shown = vars_argsort[i]
        break
    else:
        i+=1
    if i == groups:
        print("error.")
        break
    
for i in range(len(group_id)):
    if group_id[i]==group_shown:
        print(songs[index_including[i]], artists[index_including[i]])
        print(lines_including[i])
        print()

 

3-4.実行結果

これまでのコードを実行すると、以下のような歌詞が表示されます。とりあえず眺めてみてください。

 

君はロックを聴かない あいみょん
君は気づくのかい?なぜ今笑うんだい?嘘みたいに泳ぐ目

ピースサイン 米津玄師
口をついて叫んだあの日から変わっていく僕を笑えばいい独りが怖い僕を

真赤 My Hair is Bad
なぜか甘えてしまう格好つかないよなって笑ってた合鍵を返して

サザンカ SEKAI NO OWARI
思い出して つまずいたならいつだって物語の主人公は笑われる方だ人を笑う方じゃないと僕は思うんだよ

君に届け flumpool
その小さな手も上手く笑えない君が 笑えばあの日見た夢がまた一つ 叶う

僕の名前を back number
いらない思い出だらけの僕の頭を君は笑って抱きしめてくれた今 君の手を握って出来るだけ目を見て

手紙 back number
離れていても守られているんだあなたはずっと手を振って笑ってくれた帰り道迷わないように

アイラブユー 西野カナ
あたりまえに今日も隣で笑ってるけどやっぱり私はキミでよかった

愛をこめて花束を Superfly
理由なんて訊かないでよね今だけすべて忘れて 笑わないで受けとめて照れていないで

幻 My Hair is Bad
昨日見た夢のことずっと思い出してるあの日みたいに笑ってたあの時みたいに話してた

これだけ見せられてもちゃんと抽出されているのか分からないと思うので、比較としてほかの歌詞を載せておきます。これらは、一つや二つだけの歌詞でグループを形成しているものであり、いわば外れ値です。全体的に上の歌詞と雰囲気が違うものになっていることが分かるかと思います。というか2曲目はもはやよく分からないですが……。

 

今勝ち上がるためのお勉強 朗らかな表情踊る阿呆に見る阿呆 我らそれを端から笑う阿呆デカイ自意識抱え込んではもう 磨耗 すり減って残る酸っぱい葡萄

(LOSER 米津玄師)

ねぇね 千賀は (ねぇね!) 化粧水な 11本は多すぎ!ヘマしても笑ってLet's go! Come on! Come on! Hey!キタ ガヤ タマ ヨコ ミヤ ニカ セン

 (We are キスマイ! Kis-my-Ft2)

 

こういった外れ値が抜かれたことを考えれば、ある程度は洗練されて抽出されていることが分かるかと思います。とは言っても、見てもらうとわかる通り、完全にすべて同じような意味とはいかないようです。というのも同じ単語が含まれている時点で外れ値があるとは言え基本的に近いベクトル値であるみたいで、いろいろ試してはみたのですがそれらの分類は難しかったです。今回は、表示する歌詞の数の都合上このグループ数にしたわけですが。

今回は高々200曲ほどですので、何千、何万のオーダーで歌詞を読み取ればもう少し細かな分類も可能になってくるかもしれません。また、分析の対象となった歌詞を見ると、歌詞ならではの文脈が分かりにくいような言葉遣いが多く存在したので、それも分類を難しくする一因となっているのではないかと思います。

 何はともあれ、おおよその傾向として、J-POPでは「笑う」が登場する場合は大概君が笑っているのであったりということはわかるかと思います。あとは、人に笑われるパターンのものがいくつか入っていますね。

 

他の例として、「愛」(含まれるのは全55曲)のものを載せておきます。幸せいっぱいかと思いきや、案外ややグロテスクな表現や寂しい表現が多いですね。他のグループを除くと、幸せそうな歌詞が集まっているところもありました。例としてはこちらの方が上手くいったかもしれないです……。

ブルースカイ ブルー 西城秀樹
おそれなどまるで感じないではげしさが愛と信じた立ちどまることも許さずに

キセキ GReeeeN
君に巡り合えた それって『奇跡』2人寄り添って歩いて 永久の愛を形にしていつまでも君の横で 笑っていたくて

ゴーゴー幽霊船 米津玄師
今日も映画みたいな夢うつつ愛も絶え絶えの景色だそこでどんな夢見てもしょうがない

MAD HEAD LOVE 米津玄師
どんどろりんと言葉が溶けていくもう愛から愛へ愛されて愛まで脳みそ全部そんな感じ

さよならエレジー 菅田将暉
はじめてのキスを繰り返して欲しくて愛が僕に噛みついて 離さないと言うけれどさみしさのカタチは変わらないみたいだ

StaRt Mrs. GREEN APPLE
幸せな時間をどれだけ過ごせるかは…微々たるものでも愛に気づけるか。さあ 試されよう

愛をこめて花束を Superfly
ここに来られて本当に良かったわこの込み上がる気持ちが愛じゃないなら何が愛かわからないほど

二人セゾン 欅坂46
見過ごしちゃもったいない愛を拒否しないで君はセゾン

ガラスを割れ! 欅坂46
(OH OH OH OH OH…)愛の鎖引きちぎれよ歯向かうなら背中向けるな

I NEED U (Japanese Ver.) BTS (防弾少年団)
Girl, いっそ、いっそ教えて欲しいよGirl, 君と、君との愛が終わりと oh僕には出来ないよ

ハナウタ [ALEXANDROS]×最果タヒ
痛みだけが指にふれる愛おしさばかり打ち寄せだれにも触れたくないのに

サヨナラの意味 乃木坂46
守りたかった愛に代わるもの電車が通過する

魔法って言っていいかな? 平井堅
最後は君が背中をさすってくれたね愛という曖昧なものに君はその指で輪郭を描いてくれた

One Love 嵐
それぞれ描く 幸せのかたちは重なり今 大きな愛になるずっと二人で 生きてゆこう

遥か GReeeeN
「誰かに嘘をつくような人に なってくれるな」 父の願いと「傷ついたって 笑い飛ばして 傷つけるより全然いいね」 母の愛あの空 流れる雲 思い出す あの頃の僕は

4.おわりに

 いかがだったでしょうか。個人的には、上手くいくかかなり不安だったので、思ったよりはそれらしい結果が出てわりと安心しているところはあります。今回いろいろ試してみたわけですが、pythonの機械学習ライブラリを用いてあとは簡単なアルゴリズムを適用するだけで、自然言語とかいうよくわからないものを定量的に扱えるようになることに感動しました。しかし一方で、歌詞ならではの難しさのようなものも後半では浮かび上がってきたように思います。

今回は自然言語処理の技術の一部を利用したわけですが、調べれば調べるほど興味深い分野ですので、興味がある方はぜひ勉強してみてほしいです(とはいえ僕も勉強を始めたばかりですが)。

だらだらと書いてしまいましたが、このあたりで筆を置きたいと思います。ここまでお読みいただきありがとうございました。駄文、失礼しました。

OpenCVで乃木坂46秋元真夏と銀シャリ鰻和弘の類似度を調べてみた

初めまして、アイデミー研修生のだっちー(@dacciinfo)です。

仮想通貨のブログを書いているので「仮想通貨が大暴落したときのTwitter負の感情分析」みたいなことやろうかなーとざっくり思っていたのですが、ツイートの取得が1週間前までしか無理みたいなので断念しました。

 

そこで今回は好きな乃木坂46を取り上げたいと思います!

いきなりですが、このふたりをご存知でしょうか。

左が乃木坂46の秋元真夏さん、右が銀シャリの鰻和弘さんです。

f:id:h25e28:20180610190151p:plainf:id:h25e28:20180610151811p:plain


以前から、このふたりが似てるとネットで話題となっていました。

このふたりもブログやTwitterでコメントしています。

f:id:h25e28:20180610143603p:plain

鰻さんと真夏さんヽ(。・ω・。)ノ546 | 乃木坂46 秋元真夏 公式ブログ


確かに似てる感じしますね笑

そこで今回はOpenCVを用いて顔の類似点を調べ、どのくらい似ているのか調べたいと思います!

 

今回、プログラム書くにあたり参考とさせていただいたサイトは以下になります。

ありがとうございました。

OpenCVを使って誰の顔なのかを推定する(Eigenface, Fisherface, LBPH)

OpenCVを使った顔認識(Haar-like特徴分類器)

Fisherfaces


目次

使用するアルゴリズムと環境

今回、顔の類似度を調べるにあたり、使用したアルゴリズムは
Fisherfacesです。これはEigenfaceの改良版で、照明や角度の違いに影響されにくいといった特徴があります。
使用する画像からHaar-like特徴分類器を使用し、顔領域を抽出します。
その抽出した顔画像をFisherfaceで学習させ、未学習の画像と比較し、類似度を調べます。

環境は
Python 3.6.5
opencv 3.3.1

秋元さん15枚の画像を学習データとし、未学習の画像である鰻さんの画像をテストデータとします。鰻さんの比較対象として乃木坂メンバー数人の画像もテストデータとして用意します。
f:id:h25e28:20180614185515p:plain

f:id:h25e28:20180614191006p:plain

プログラム

import cv2
import os
import numpy as np
from PIL import Image

#トレーニング画像
train_path = './train_images'

#テスト画像
test_path = './test_images'

#Haar-like特徴分類器
cascadePath = "/Users/ユーザー名/Downloads/opencv-3.4.1/opencv-3.4.1/data/haarcascades/haarcascade_frontalface_default.xml"
faceCascade = cv2.CascadeClassifier(cascadePath)

#FisherFace
#recognizer = cv2.face_FisherFaceRecognizer.create()
recognizer = cv2.face_LBPHFaceRecognizer.create()

#pathしたフォルダ内の画像を習得
def get_images_and_labels(path):
    #画像を格納する配列
    images = []
    #ラベルを格納する配列
    labels = []
    #ファイル名を格納する配列
    files = []
    for f in os.listdir(path):
        #画像のパス
        image_path = os.path.join(path, f)
        #グレースケールで読み込み
        image_pil = Image.open(image_path).convert('L')
        #Numpyの配列に格納
        image = np.array(image_pil, 'uint8')
        #Haar-like特徴分類器で顔を検知
        faces = faceCascade.detectMultiScale(image)
        #検出した画像の処理
        for(x, y, w, h) in faces:
            #200×200にリサイズ
            roi = cv2.resize(image[y: y + h, x: x + w], (200, 200), interpolation=cv2.INTER_LINEAR)
            #画像を配列に格納
            images.append(roi)
            #ファイル名からラベルを取得
            labels.append(int(f[7:9]))
            #ファイル名を配列に格納
            files.append(f)
    
    return images, labels, files

#トレーニング画像を取得
images, labels, files = get_images_and_labels(train_path)

#トレーニング実施
recognizer.train(images, np.array(labels))

#テスト画像を取得
test_images, test_labels, test_files = get_images_and_labels(test_path)

i=0
while i < len(test_labels):
    #テスト画像に対して予測実施
    label, confidence = recognizer.predict(test_images[i])
    #予測結果をコンソール出力
    print("Test Image: {}, Predicted Label: {}, Confidence: {}".format(test_files[i], label, confidence))
    #テスト画像表示
    cv2.imshow("test image", test_images[i])
    cv2.waitKey(300)
    i += 1

#終了処理
cv2.destroyAllWindows()

実行結果はこちらです。

Test Image: subject01.akimoto.png, Predicted Label: 1, Confidence: 48.77426502706794
Test Image: subject01.asuka.jpg, Predicted Label: 1, Confidence: 54.37620231697899
Test Image: subject01.hori.jpg, Predicted Label: 1, Confidence: 53.24049819391969
Test Image: subject01.ikuta.jpg, Predicted Label: 1, Confidence: 56.262042555618166
Test Image: subject01.nishino.png, Predicted Label: 1, Confidence: 50.30127169485847
Test Image: subject01.shiraishi.jpg, Predicted Label: 1, Confidence: 72.06184327769823
Test Image: subject01.unagi.png, Predicted Label: 1, Confidence: 63.37511081588498

subject01の「01」をラベルに設定し、学習しています。複数の画像を学習させる際は、この数値を変更してください。学習した画像は今回秋元さんのみなのでラベルも1のみです。
分散に基づいて計算されているため、Confidence(確度)は0に近い方が確度が高くなります。
学習に使用した画像をテストで使用すると特徴点が一致するため、Confidenceは0です。

未学習の秋元さんの画像は48.77、もちろんこの中で一番近い値です。
類似度順に並び替えると

f:id:h25e28:20180614192751j:plainf:id:h25e28:20180610151811p:plainf:id:h25e28:20180614192801j:plainf:id:h25e28:20180614192754j:plainf:id:h25e28:20180614193458p:plainf:id:h25e28:20180614192805p:plainf:id:h25e28:20180610190151p:plain
白石<鰻<生田<齋藤<堀<西野<秋元

のようになりました。ちなみにニャンちゅうは顔認識されず対象外となってしまいました。

学習データを変更

こちらは秋元さんが番組で平野ノラさんのモノマネをしたときのものです。

f:id:h25e28:20180614194754j:plain


この番組でのモノマネした秋元さんの画像をできるだけ集めて学習させます。あとは先ほどと同様です。
f:id:h25e28:20180614195153p:plain


実行結果

Test Image: subject01.akimoto.png, Predicted Label: 1, Confidence: 64.72162690757675
Test Image: subject01.asuka.jpg, Predicted Label: 1, Confidence: 70.898033127977
Test Image: subject01.hori.png, Predicted Label: 1, Confidence: 71.68987057398674
Test Image: subject01.ikuta.jpg, Predicted Label: 1, Confidence: 72.3624149846466
Test Image: subject01.nishino.png, Predicted Label: 1, Confidence: 71.81215441739869
Test Image: subject01.shiraishi.jpg, Predicted Label: 1, Confidence: 78.99918092527798
Test Image: subject01.unagi.png, Predicted Label: 1, Confidence: 65.89760962981548

f:id:h25e28:20180614192751j:plainf:id:h25e28:20180614192801j:plainf:id:h25e28:20180614192805p:plainf:id:h25e28:20180614192757j:plainf:id:h25e28:20180614192754j:plainf:id:h25e28:20180610151811p:plainf:id:h25e28:20180610190151p:plain
白石<生田<西野<堀<齋藤<鰻<秋元

という結果になりました。
学習データを鰻さんに似ているモノマネした秋元さんの画像に変更することでより近い結果となりました。

学習データの追加

次は、先ほどの乃木坂メンバー白石、生田、西野、堀、齋藤の画像10枚学習させます。
もちろん、テストデータとは違うものです。

f:id:h25e28:20180615211357p:plain

ラベルは01が秋元、02が齋藤、03が堀、04が生田、05が白石、06が西野、07が鰻です。

実行結果

Test Image: subject01.akimoto.jpg, Predicted Label: 1, Confidence: 21.786044627006834
Test Image: subject02.asuka.jpg, Predicted Label: 2, Confidence: 51.351808634017296
Test Image: subject03.hori.jpg, Predicted Label: 3, Confidence: 44.75424378786359
Test Image: subject04.ikuta.jpg, Predicted Label: 4, Confidence: 45.70965779143789
Test Image: subject05shiraishi.jpg, Predicted Label: 5, Confidence: 49.08969544979982
Test Image: subject06nishino.jpg, Predicted Label: 6, Confidence: 43.72499865726888
Test Image: subject07.unagi.png, Predicted Label: 1, Confidence: 63.37511081588498

各乃木坂メンバーはsubjectとPredicted Labelが同じであるため、自分の顔の学習データとテストデータが一致していることが分かります。
鰻さんは学習させていないですが、Predicted Labelが1となりました。これは学習データの中から秋元さんの顔に近いと判断され、ラベル1に振り分けられました。

考察と感想

今回は、Haar-like特徴分類器を使って顔領域を抽出、Fisherfaceで学習、テスト、結果表示という流れで類似度を調べました。ブログに載せた以外にもAKAZE特徴量を使用した特徴点マッチングで類似度を試みましたが上手くいきませんでした。
学習データを変更、追加することでより近い結果になったと思います。今回は比較対象として乃木坂の数人を挙げましたが、少なくともこのメンバーの中では秋元さんが一番鰻さんに似ていることが分かりました。
Pythonにふれてまだ3週間で、まだまだ未熟ではありますがこれから自分で色々実装してみたいと思います。

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

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

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

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

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

課題

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