Tweetの #名刺代わりの小説10選 から機械学習を使わずレコメンド
導入・目的
Twitterでは#名刺代わりの小説10選
というタグが存在し、2018 ~ 2020/10/25までにおおよそ8000ツイートをちまちまと集めることができました。
このタグから、レコメンドを行いたいと思います。データ数的に、けんすうさんが制作したMNMほどデータを集めることができなかったので、機械学習のアプローチを用いずにスモールデータでもワークするアプローチでレコメンドを行いたいと思います。
#名刺代わりの小説10選
— てでーべあー (@kumanikamareru) October 19, 2020
未来のイヴ/リラダン
ライ麦畑でつかまえて/サリンジャー
アルジャーノンに花束を/ダニエル・キイス
箱男/安部公房
砂の女/同上
風の歌を聴け/村上春樹
羊をめぐる冒険/同上
スプートニクの恋人/同上
海と毒薬/遠藤周作
機械の中の幽霊/ケストラー
方法
どんなレコメンドであっても基本は共起を用いたアプローチを取ることが多いです。これは行列分解を行うMatrix Factorizationなどや類似のアルゴリズムは共起を基本としたアルゴリズムほとんどに言うことができます。
一般には機械学習をベースとしたアプローチが主流ですが、次元圧縮等を伴わない場合(つまり、汎化を気にしない場合)、テーブル操作のみで同等のことが行えます。
#名刺代わりの小説10選
のツイート構造が好きな10冊の本のタイトルを紹介するもので、ある一冊に着目し、周辺に共起となるタイトルが散らばっていると考えることができます。
あるタイトルの周辺に散らばっているタイトルをうまく集計していくことで、どのような本がある本の近くに現れやすいのかを定量化することができます。
前処理
ツイートは自然言語で記述されており、基本は"本のタイトル"/"著者"
で構成されていますが、例外も多く、『』
でタイトルや本が指定されているもの、|
で分画されているもの、タイトルと著者が逆転しているものと様々です。
基本的にはヒューリスティックにそこそこパースできるプログラムを目指すのですが、以下のようなコードになってしまいます。
import re
import mojimoji
total = []
for tweet in df["tweet"]:
if not isinstance(tweet, str):
continue
block = []
tweet = mojimoji.zen_to_han(tweet, kana=False)
for line in tweet.split("\n"):
line = line.strip()
if "#" in line:
continue
if line == "":
continue
if "「" in line and "」" in line:
a = re.search("「(.*?)」", line).group(1)
b = re.sub("「.*?」", "", line)
block.append(a)
block.append(b)
elif "『" in line and "』" in line:
a = re.search("『(.*?)』", line).group(1)
b = re.sub("『.*?』", "", line)
block.append(a)
block.append(b)
elif "(" in line and ")" in line:
a = re.search("\((.*?)\)", line).group(1)
b = re.sub("\(.*?\)", "", line)
block.append(a)
block.append(b)
elif "/" in line and line.count("/") == 1:
a, b = line.split("/")
block.append(a)
block.append(b)
elif "|" in line and line.count("|") == 1:
a, b = line.split("|")
block.append(a)
block.append(b)
elif re.split("\s{1,}", line).__len__() == 2:
a, b = re.split("\s{1,}", line)
block.append(a)
block.append(b)
else:
pass
total.append([x.strip() for x in block])
精度は64%で、64%のツイートでパースすることに成功しました。
著者情報
また、Wikipediaの小説家一覧より、小説家をすべて定義します。そうすることで、タイトルだけをユニークに取り出すことができ、著者とタイトルを分離できます。
# 著者の名前が記されたcsvを読み込み
names = pd.read_csv("var/auths.csv")["name"].tolist()
# titleの抽出
total = [[x for x in block if x not in names and x in titles ] for block in total ]
total = [x for x in total if x.__len__() >= 1]
人気の本、著者
本筋からはずれますが、みなさんがよく上げる10冊の中にあるタイトルには、以下のようなタイトルがあることが分かりました。
この中からだと、私は星の王子さま
と虐殺器官
が好きです。
また作家ランキングだと以下のようになります。
共起を計算する
total
というlistを共起するタイトルをtitle_cos
変数の中にどんどん追加していき、すべて追加し終わったあとに、最大値で割り込むことでnormalizeした各タイトル(title)ごとに共起しやすいタイトル(co)を一覧で表現することができます。
title_cos = {}
for block in total:
for title in block:
title_cos[title] = title_cos.get(title, []) + block
tmps = []
for title, cos in list(title_cos.items()):
cos = Counter(cos)
max_ = max(cos.values())
tmp = pd.DataFrame({"co": list(cos.keys()), "val": list(cos.values())})
tmp["title"] = title
tmp = tmp[tmp.val >= 2]
tmp["val"] /= max_
tmps.append(tmp.sort_values(by=["val"], ascending=False)[:300])
#print(title)
ret = pd.concat(tmps)
ret = ret[["title", "co", "val"]]
ret
例えば、夜は短し歩けよ乙女
を見てみるとそれらしく出ています。
頻出するものは重要でない仮説を導入する
自然言語処理などでよく使われる理論にtfidf
等が存在しますが、idf
部分はレアリティが高いものを高いスコアに、頻出するものは低いスコアにするヒューリスティックです。
この仮説は割とどこでも適応可能で、今回のようなケースにも実際に適応可能でした。
具体的にはTOP 300以内によく登場する本はあまり重要でない仮説を導入します。
w = ret.groupby(by=["co"]).agg(val_sum=("val", "sum")).reset_index()
df = pd.merge(ret, w, on=["co"], how="left")
df["val_norm"] = df["val"]/np.log(df["val_sum"]+np.e)
tmps = []
for title, s in df.groupby(by=["title"]):
tmps.append(s.sort_values(by=["val_norm"], ascending=False))
df = pd.concat(tmps)
その上で、夜は短し歩けよ乙女
を見てみると微妙に内容的に遠いのでは?と考えていた、図書館戦争
や人間失格
が有名だから共起していましたが、この改良版ではランクが下がりました。
結果
作家の粒度でも簡単に実行でき、村上春樹だと以下のようになります。
Google Spreadsheetで誰でも見れるようにおいてあるのでご自身の好きなタイトルや作家さんを探してみましょう(作者名フィルタで落としきれなかったノイズも多少あり、これは手動でフィルタを追加するしかなさそうです)
私はハーモニー
に関連する本をいくつか読んで自分で定性的に評価したのですが、かなり良かったです。紫色のクオリア
とか淵の王
とはちゃんと出るのは良さがあります。
考察
データ数がそんなになくとも、pandasでデータをゴニョゴニョしてテーブル操作するだけで、割とまともな結果を得ることができました。
MatrixFactorizationなどをこのような課題設定で使うことも可能なのですが、そのまま適応してしまうと、メジャーなタイトルに寄せられてしまう問題やハイパラであるどういう性質がある本にどういう割引を行うか、などを機械学習のブラックボックス性故に少し感覚値がつかみにくいなどがあります。しかし、今回の方法だと目で見て感覚を掴みながら調整できるので、どのへんが良さそうなのかすぐ探索できます。
まとめ
個人でさっとバズったTwitterのハッシュタグを集めて分析して、自分のQoLを上げるのは良さそうです。
今回、集めた8000近くのツイートやjupyterのクソコード等はGitHubで公開していますので、ご自由にどうぞ。。。