Help us understand the problem. What is going on with this article?

Pythonでつくる彼女入門 ~Tinder自動化プロジェクト~ 第5話

More than 1 year has passed since last update.

目次

やったこと 主な出来事
第1話 自動右スワイプ
第2話 自動メッセージ送信 女性とマッチした
第3話 ライブラリ化 マッチした女性とLINEを交換した
第3.5話 アクセストークンの再取得 これまでのコードではトークンが取得できなくなっていた
第4話 データ収集 LINEの返信が来なくなった
第5話 データ分析 プロフィール文編 仲良くなった人から情報商材を勧められた
第6話 データ分析 画像編 リアルの知り合い女子から最近やたらと深夜に電話がかかってくる(?)

コードはGitHubから閲覧できます。

前回までのあらすじ

  • TinderのAPIを簡単に叩くためのライブラリを作成した
  • マッチする相手を探すべく、データの収集を行った

近況

学会の準備で忙殺されており、気がついたら前回の記事から2ヶ月以上経っていました。とはいえその間クローラーはずっと働いていたので、前回から集め始めたデータがだいぶ溜まっています。
相変わらず彼女はできません。

データ分析

随分データが溜まりました。
スワイプした女性が10632人。うちマッチしたのが72人。思ったよりもマッチしないものですね。
前回はテーブルデータをスプレッドシートに、画像データをGoogleDriveに保存しましたので、まずそれらをダウンロードするところからスタートです。スプレッドシートをダウンロードする際はファイル形式がいくつか選択できますが、csvやtsvでダウンロードするとプロフィール文中の改行や、海外の人がプロフィール文に書いたカンマなどが悪さをして無駄に面倒くさかったので、.xlsx形式で保存しておきます。
あとプロフィール画像が25000枚くらいあったので、ダウンロードにやたらと時間がかかりました。
分析はjupyter notebook上で行っています。

なお、このデータは、僕のプロフィールをもとに集めたデータを検証したものなので、みなさんが実行すると結果が異なる可能性があります。1ご注意ください。

データを眺めてみる

まずはマッチした人のデータを眺めてみましょう。

analytics.py
import pandas as pd
import numpy as np
from tqdm.notebook import tqdm
import matplotlib.pyplot as plt
import seaborn as sns
from collections import Counter


filePath="data/tinder.xlsx"
df=pd.read_excel(filePath)
df.set_index("id", inplace=True)

match = df[df["match"]==1]

なんか見覚えのない人がいるな??

次にマッチしなかった人のデータです。

analytics.py
unmatch = df[df["match"]==0]

パット見た雰囲気ですが、マッチした人のほうがしっかりとプロフィール文を記入しているような気がします。確認してみましょう。
「しっかりと記入されたプロフィール文」の定義が難しいですが、とりあえず単純にプロフィール文の文字数を調べてみます。

analytics.py
%matplotlib inline
sns.distplot(unmatch["bio"].apply(lambda w:len(str(w))), color="b", bins=30)
sns.distplot(match["bio"].apply(lambda w:len(str(w))), color="r", bins=30)

結果は以下の通り。赤がマッチした人、青がマッチしなかった人です。
bio-length.png

やっぱり青と比べて赤はゼロ文字付近が少ない気がします。というかマッチしなかった人に、プロフィールを1文字も書いていないアカウントが多いみたいですね。
プロフィールが空白のアカウントは、右スワイプしてもマッチする確率が低いので、スワイプしないほうが良さそうです。

プロフィール文を詳しく見る

形態素解析

まずは、プロフィール文にふくまれる単語をチェックしたいと思います。プロフィール文に対して、形態素解析エンジンMeCab[1]と拡張辞書mecab-ipadic-NEologd[2]を用いて形態素解析を行います。

インストール

MeCabのインストールは、pip install mecab-python3で行うことができます。
mecab-ipadic-NEologdのインストール方法は、公式文章[3]がとても良くまとまっているので、そちらを参考にしてください。
オプション等が様々選択できますが、本当に面倒くさい人は

$git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git ~/neologd
$echo yes | ~/neologd/bin/install-mecab-ipadic-neologd -n -a

とするとインストールできます。

単語分割

mecabをPythonから呼び出して、プロフィール文を単語毎に分割します。
mecabを単に呼び出すと標準の辞書が使用されるので、オプションでNEologdを指定します。辞書の場所はecho `mecab-config --dicdir`"/mecab-ipadic-neologd"で取得できます。

mecab.py
import subprocess
import MeCab

cmd = 'echo `mecab-config --dicdir`"/mecab-ipadic-neologd"'
path = (subprocess.Popen(cmd, stdout=subprocess.PIPE,
                           shell=True).communicate()[0]).decode('utf-8')
m = MeCab.Tagger("-d {0}".format(path))

print(m.parse("彼女はペンパイナッポーアッポーペンと恋ダンスを踊った。"))
#>>
#彼女 名詞,代名詞,一般,*,*,*,彼女,カノジョ,カノジョ
#は    助詞,係助詞,*,*,*,*,は,ハ,ワ
#ペンパイナッポーアッポーペン 名詞,固有名詞,一般,*,*,*,Pen-Pineapple-Apple-Pen,ペンパイナッポーアッポーペン,ペンパイナッポーアッポーペン
#と    助詞,並立助詞,*,*,*,*,と,ト,ト
#恋ダンス   名詞,固有名詞,一般,*,*,*,恋ダンス,コイダンス,コイダンス
#を    助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
#踊っ 動詞,自立,*,*,五段・ラ行,連用タ接続,踊る,オドッ,オドッ
#た    助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
#。    記号,句点,*,*,*,*,。,。,。
#EOS

マッチする人がよく使う言葉

これを用いて、マッチした人、マッチしなかった人それぞれから、プロフィール文にふくまれている単語を抜き出します。

analytics.py
def getWord(df):
    retval = []
    for bio in df.bio:
        parse = m.parse(str(bio)).strip().split("\n")
        for p in parse:
            if ("\t" in p) == False:
                continue
            word, desc = p.split("\t")
            if desc.split(",")[0] in ("名詞", "動詞", "形容詞", "形容動詞", "連体詞", "副詞", "接続詞", "感動詞", "記号"): # 助詞と助動詞を除きたかった
                retval.append(word)
    return retval

bio_match = getWord(match)
bio_unmatch = getWord(unmatch)

得られた単語のリストを出現頻度順に並べて表示します。まずはマッチした人から。

analytics.py
df_bio_match = pd.DataFrame.from_dict(
    Counter(bio_match), orient="index").reset_index().rename(columns={"index":"word",0:"count"})
sns.barplot(data=df_bio_match.sort_values(
    "count", ascending=False)[:20], x="word", y="count")
plt.xticks(rotation="vertical")

bio-match.png

発生している豆腐は、空白スペースのようななにかです。多分thinspかな?
∇(ナブラ)を誰が使っているか気になったので確認したら、( ・∇・)みたいな顔文字に使われていました。
続いて、マッチしなかった人です。

analytics.py
df_bio_unmatch = pd.DataFrame.from_dict(
    Counter(bio_unmatch), orient="index").reset_index().rename(columns={"index":"word",0:"count"})
sns.barplot(data=df_bio_unmatch.sort_values(
    "count", ascending=False)[:20], x="word", y="count")
plt.xticks(rotation="vertical")

bio-unmatch.png

なにか傾向に差があるような、そうでもないような...
例えば、マッチする人は、文章に句点をつけない傾向にあることが見て取れます。
また、漢字で「好き」と書く人はマッチの有無を問わず多数いる一方で、ひらがなで「すき」と書く人とはことごとくマッチしていません。地雷なのかな?
正直、マッチした人のサンプル数が少ないので誤差の範囲な気がしないでもないですが、覚えておいて損はないかもしれません。

文章のベクトル化

最後に、Doc2Vecを用いてプロフィール文をベクトル化してみます。
何年か前にWord2Vecという単語をベクトル化するDNNがNLP界隈で大きな話題になりましたが、それを単語ではなく文章に対して適用したアルゴリズムがDoc2Vecです。Word2Vecの解説は[4]が、Doc2Vecの解説は[5][6]あたりが参考になりました。
実装にはgensim[7]というライブラリを使用します。pip install gensimでインストールしておいてください。
具体的なコードは、[8]を参考にさせていただきました。

analytics.py
# データを訓練データとテストデータに分割
df_train, df_test = train_test_split(df, random_state=8888)

# MeCabを用いてプロフィール文を単語に分割
m_wakati = MeCab.Tagger("-d {0} -Owakati".format(path)) # MeCabのオプションに -Owakati をつけることで、品詞等を出力せず単語をスペースで区切ってくれます。
bios=[]
for bio in df_train.bio:
    bio = m_wakati.parse(str(bio)).strip()
    bios.append(bio)

# データをgensimで処理できる形式に変換
trainings = [TaggedDocument(words = data.split(),tags = [i]) for i,data in enumerate(bios)]

# doc2vecを学習
doc2vec = Doc2Vec(documents=trainings, dm=1, vector_size=300, window=4, min_count=3, workers=4)

# 訓練データのベクトルを取得
X_train = np.array([doc2vec.docvecs[i] for i in range(df_train.shape[0])])

# 訓練データの正解ラベルを取得
y_train = df_train["match"]

# テストデータのベクトルと、正解ラベルを取得
X_test = np.array([doc2vec.infer_vector(m.parse(str(bio)).split(" ")) for bio in df_test.bio])
y_test = df_test["match"]

ベクトル化した文章を、PCAを用いて可視化してみます。まずは訓練データから。

analytics.py
from sklearn.decomposition import PCA

pca = PCA()
X_reduced = pca.fit_transform(X_train)

plt.scatter(X_reduced[y_train==0][:,0], X_reduced[y_train==0][:,1], c="b", label="No Match")
plt.scatter(X_reduced[y_train==1][:,0], X_reduced[y_train==1][:,1], c="r", label="Match")
plt.legend()

pca_train.png
マッチしなかった人については、第二主成分が大きい人が一定数いるのに対し、マッチした人の第二主成分は概ね0付近に固まっています。
テストデータも見てみましょう。

analytics.py
X_test_reduced = pca.transform(X_test)

plt.scatter(X_test_reduced[y_test==0][:,0], X_test_reduced[y_test==0][:,1], c="b", label="No Match")
plt.scatter(X_test_reduced[y_test==1][:,0], X_test_reduced[y_test==1][:,1], c="r", label="Match")
plt.legend()

pca_test.png

マッチした人は第二主成分が0付近に集まっている様子が見て取れます。

機械学習

svmを用いた分類

文章をベクトル化したので、機械学習にかけて分類をしてみましょう。
サポートベクターマシンを用いて、プロフィール文ベクトルを分類します。
今回扱うデータは極めて偏った不均衡データのため、素直に決定境界を引くとすべてのプロフィールが「マッチしない」と判定されてしまいます。これでは使い物になりません。
そもそも、今回は何をしたかったのかを思い返すと、機械学習の精度を上げたいのではなく、彼女が欲しかったのでした。混同行列の各項目に着目すると、

説明 備考
TP 実際にマッチする人をマッチすると判定 これこそ求めているもの
TN 実際にマッチしない人をマッチしないと判定 無駄な右スワイプを削減できる
FP 実際にはマッチしない人をマッチすると判定 右スワイプが一回無駄になる
FN 実際にはマッチする人をマッチしないと判定 運命の人と出会えなくなる

明らかにFNが発生することが最悪で、これをなんとしても避けたいです。一方、FPは発生しないことが望ましいですが、少しくらい発生してもどうということはありません。
というわけで、今回のタスクにおいては、なるべくrecallが高いことが求められます。一方、precisionやF値は低くても許容範囲です。もちろん、すべてのケースを「マッチする」と予測すれば、precisionやF値の壊滅と引き換えに高いrecallを得ることが可能2ですのでそれを回避すべく機械学習を導入したわけですが、そのためにrecallが下がるようでは本末転倒と言わざるを得ません。
そこで今回は、マッチする確率をRegressorによって推定させ、かなり低い閾値を設定することで「明らかにマッチしなそうなヤバイやつ」だけを排除する戦略をとります。疑わしきは右スワイプです。
評価指標としてはaucを用います。

analytics.py
from sklearn.svm import SVR
from sklearn.metrics import roc_auc_score

model = SVR(C=100.0)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
print(roc_auc_score(y_test, y_pred))
#>>0.6196

auc0.6超え!これはなかなか良い結果ではないでしょうか。具体的な閾値の設定は、画像の分析も終えたあとに行う予定です。
記事が長くなってしまったので、本日はこのへんで。
次回のプロフィール画像編も、ご期待ください。

第6話はこちら

参考文献

[1]https://taku910.github.io/mecab/
[2]https://github.com/neologd/mecab-ipadic-neologd
[3]https://github.com/neologd/mecab-ipadic-neologd/blob/master/README.ja.md
[4]斎藤康毅, ゼロから作るDeep Learning ❷ ―自然言語処理編
[5]https://kitayamalab.wordpress.com/2016/12/10/doc2vecparagraph-vector-のアルゴリズム/
[6]https://deepage.net/machine_learning/2017/01/08/doc2vec.html
[7]https://radimrehurek.com/gensim/index.html
[8]https://qiita.com/asian373asian/items/1be1bec7f2297b8326cf


  1. イケメンと僕とで、同じ実験を行った場合にどのような差がでるかについても検証してみたい。いや、やっぱり見たくないかも。 

  2. そしてそれを実現していたのが、全右スワイプというこれまで実装していたスワイプ戦略でした。 

Fulltea
( ; ; ) ← 無限ループに泣かされている人の顔文字です。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away