類似本検索システムを作りたい

ということで、本を検索すると類似している本のリストを出力するサイトを試作してみました。

https://bookrecommendst.herokuapp.com/

(herokuを他の用途で使うまではアクセス可能な予定です。)

youtu.be

こんな感じで、ある検索した本に対して類似度の高い本を出力してくれます。

作成にあたり、こちらのサイトを参考にさせていただいています。

実施したこと

1. データの取得

ブクログ様のサイトの情報を使用しています。 ブクログではユーザ毎にどの本を読んでいるかという情報を閲覧できるようになっているので、その情報をスクレイピングにより取得しました。 全くのランダムにユーザをピックアップするということができませんでしたので、人気タグの1ページ目にあるタグが付与されている人のIDを6万人程取得し、その各ユーザに対し登録されている本の情報を取得しました。

2. データの前処理、整形

取得した情報より、下記のようなユーザ*本の行列を作ります。

本A 本B 本C ...
ユーザ1 0 0 1 ...
ユーザ2 1 1 0 ...
... ... ... ...

スパースな行列になるので、メモリの爆発を防ぐためにscipyのlilmatrixを使用しています。 また、本の登録者が10冊未満のユーザおよび、全体で10冊未満しか登録されていない本についてはあらかじめ除外をしています。これは、レコメンドの安定性の向上および、特定のユーザが結果に大きな影響を与えるような結果を防ぐための処理になります。 この除外処理により、ユーザ数は6万→5万、ユニークな本の冊数は38万→2万となりました。 本については想定以上に多様性が大きく、今回の取得したデータ量だと結構対象外となるものが多くなってしまいました。

3. 次元削減

経験的にはスパース行列のまま類似度計算を行うよりも、次元削減をした方が意味のある特徴量になることが多いです。また、計算時間の削減にも寄与するため次元削減を行いました。 次元削減手法はSVDとNMFを選択できるようにしています。選定理由はスパース行列に対応している次元削減手法だったからです。 この処理により列数を2万→100に削減しています。

4. 類似度計算

コサイン類似度を計算します。 最初は全入力パターンを事前に計算して結果を持っておこうと思っていましたが、 下記サイトを参考に行列計算すればかなり高速にできることがわかったので、入力毎に毎回計算しています。

https://stackoverflow.com/questions/41905029/create-cosine-similarity-matrix-numpy

5. wepアプリ実装

上記の処理をstreamlitを用いてwebアプリ化し、それをherokuに実装しました。 検索は単純な部分一致検索です。herokuは無料枠です。

サンプルの確認

いくつか自分で検索してみた結果を示します。すべてSVDでの結果になります。

  • know 野崎まど

f:id:rmizutaa:20200713204628p:plain

順当にSFっぽいものが上位にきている気がします。 ホラーがちらほらあるのが少し気になりますね。

f:id:rmizutaa:20200713204701p:plain

ちゃんと下巻がトップにでてきました。 ラインナップを見る限り、割と妥当という印象を受けます。

f:id:rmizutaa:20200713204718p:plain

もう少し同名タイトルの別巻が並ぶかと思いましたがそうでもなかったです。 こういうサイトで漫画をきっちり登録している人はあまり多くない印象なので、 なんとなくですが漫画の入力に対する結果はよくなさそうです。

f:id:rmizutaa:20200713204644p:plain

もっと尖った結果を期待しましたが、 普通にビジネスマンが読むような本が出てきてしまいました。 こういう本を読んでいるような人はそもそも読書量が多く雑食ぎみなので、 共通項をとるとこんな感じになってしまうのでしょうか。

まとめ

ブクログで取得した情報を元に、本を検索すると類似している本のリストを出力するサイトを試作しました。 今回取得したデータ量はそれほど多くはなかったので、対象外となってしまった本も多いですし、入力に対して疑問符がつく結果となるケースもそれなりにありました。 ただ、自分が読んだことある本に対して検索をかけられるのは個人的には楽しめました。

どうしても今回のようなやりかただとみんなが共通して読んでいるものが上位に出てきてしまうので、 よりマイナーで尖っている本を探したい場合は、あえて広く皆に読まれている本を除外しておくような、 また違ったアプローチが必要になりそうかなと思いました。

今回使用したコードは下記になります。

github.com