ポケモン人気投票データを使って推薦システムを作る
[祝] ポケモン25周年
@tkanayama_ です。先日、ポケモン公式による投票企画「#キミにきめた」が実施されました。これは、Twitter上で以下のようにハッシュタグをつけてツイートすることにより、ポケモンの人気投票を行う企画です。
今回の投票ルールで特筆すべき点は「一人が投票できるポケモンの種類数に制限がない*1」という点です。例えば上の例であれば、@tkanayama_さんは「オタチ」と「ヒノアラシ」の両方に投票したことになります。
このルールを見て私は、ポケモンどうしの共起関係を使ってポケモンの推薦システムを作れるのではないかと考えました。例えば、今回の投票を分析した結果「シャワーズとブースターとサンダースが同一ユーザーによって投票されやすい」ということが分かったとします。もし「シャワーズとブースターが好きだ」という人がいたら、その分析結果を使って「シャワーズとブースターが好きな人は、サンダースというポケモンも好きみたいですよ」とオススメすることができます(現段階ではたかだか900種類程度しかポケモンがいないため、推薦システムの価値は薄いかもしれません。しかし、この25年で900種類のポケモンが発見されたということは、200年後くらいには1万種類のポケモンがいると予想できます。その場合、なかなか全ポケモンを把握するのは難しいので、推薦システムが真価を発揮することでしょう)。
〜推薦システムに詳しい方へ〜
この記事は、簡易的な共起分析とクラスタリングでデータを観察したのち、最後にナイーブなMatrix Factorizationに突っ込んだという内容です。手法的に新しい点は特にありません。なお、「あるポケモンに投票した」という行動を「そのポケモンが好きである」と見なして、implicit feedbackの問題設定を暗に仮定しています。
本編
ステップ1. 基本的な集計をする
今回の企画が企画倒れになる最大のリスクとして、「ルール上は複数のポケモンへの投票が可能だが、実際にはほとんどの人は1種類のポケモンにしか投票しておらず、ポケモンどうしの共起関係が取得できない」という可能性が考えられます。そこで、いきなり推薦システムの学習を行うのではなく、まずは簡単な集計を行なってこの可能性を検証しておきます。
まず、「同一のアカウントが同じポケモンに複数回投票した」という重複を取り除いたところ、ユニークな(ユーザー名, ポケモン名)の組がおよそ15万ペアあることがわかりました。
次に、各ユーザーの投票数の分布を集計しました。
この表は、例えば1種類のポケモンにのみ投票しているユーザー数は52,543人であり、2種類のポケモンに投票しているユーザー数は8,900人である、ということを表しています。やはり単推しが圧倒的に多いですね。中には20種類以上のポケモンに投票している人もいました(この表からは省略しています)。
今回の企画で必要なデータは「2種類以上のポケモンに投票しているユーザー」です。一方、あまりにもたくさんの種類のポケモンに投票しているユーザーは、もしかしたら機械的にポケモンを列挙しているだけかもしれないので、ノイズになるかもしれません(数百匹のポケモンを心から愛している方ももちろんいらっしゃると思いますが!)。それを踏まえて、「2種類以上100種類未満のポケモンに投票しているユーザー」の数を数えたところ、約2万人でした。悪くない規模感です。
これ以降は、この2万人のユーザーによる投票データを扱っていきます。
ステップ2. ポケモンどうしの共起度を計算する
さて、ステップ1によりデータ数不足による企画倒れリスクは無さそうだということがわかりました。次にリスクとして思い浮かぶのは、「実はポケモンの好みは人によってバラバラであり、『このポケモンに投票した人はこのポケモンにも投票しやすい』といった傾向は全く存在しない」という可能性です。この可能性も、簡単な集計によって確認しておきます。
「あるポケモン(ポケモンAと呼びます)に投票したユーザーのうち、あるポケモン(ポケモンBと呼びます)にも投票したユーザーの割合」を、ポケモンBのポケモンAに対する共起度と呼ぶことにします。(例えば、ピカチュウに投票した人が100人いたとして、そのうち20人がライチュウにも投票していたらライチュウのピカチュウに対する共起度は0.2になります。)
この定義に基づいて、共起度を全てのポケモンの組み合わせについて計算し、可視化した結果が下記です。
行列サイズが大きすぎてよくわかりませんね。この行列から、いくつか特徴が現れている箇所を抽出しました。
まず、対角線が全て1になっているのは、共起度の定義から自明です(例えば、イーブイに投票した人のうちイーブイに投票した人の割合は当然100%です)。
次に、上から見ていきます。ブイズ(イーブイ・ブースター・・・ニンフィア)どうしの共起度は、予想通り高いようです。あるブイズに投票した人は、別のブイズにも投票していることが多い、ということです。一方でもう少しよく見ると、ブラッキーとリーフィアの行だけは黒が目立つことがわかります。これは、「ブラッキーやリーフィアに投票した人は、他のブイズにあまり投票していない」ということを意味しています。ブラッキーとリーフィアは他のブイズと一線を画しているようです。(考察:ブラッキーはポケモンバトルでもかなり強いので、ブイズ勢だけではなくポケモンバトル勢などより幅広い層から支持されていることが考えられます。リーフィアはなぜでしょう…?)
次は、ヤナッキー・バオッキー・ヒヤッキーを並べてみました。これらはポケットモンスター ブラック・ホワイトに登場する猿型のポケモンで、三猿と呼ばれています。これらも、セットで扱われることが多いので互いに共起度が高くなっています。しかしよく見ると、先ほどと同様に「バオッキーに投票した人はヤナッキー・ヒヤッキーにあまり投票していない」という非対称性が生じている点が面白いです。(考察:バオッキーは、5年前のポケモン人気投票2016で全720ポケモン中最下位だったという悲しい過去を持っていることがよく知られています。この過去を知っている人が、ヤナッキー・ヒヤッキーではなくバオッキー単体に投票した可能性が考えられます。実際、公式が発表している投票結果を確認したところ、バオッキーはヤナッキーやヒヤッキーと比べて票が多かったようです。)
最後に、ワニノコとメグロコに注目してください。ワニノコは図鑑番号158のみずタイプポケモン、メグロコは図鑑番号551のあく・じめんタイプポケモンなので、属性だけ見ると一見何の関係もありません。しかし、両者はワニとであるという共通点があります。ワニ好きの方々がしっかりワニノコとメグロコ両方に投票していることがわかります。
ステップ3. ポケモンどうしをグループ分けする
ステップ2により、少なくともいくつかの種類のポケモンにおいて、同時に投票されやすい傾向が確認できました。このように局所的にデータを眺める作業もとても楽しいのですが、今度はもう少し大局的に、ポケモンどうしをグルーピングしてみようと思います。
ステップ2で作った行列のそれぞれの行を、そのポケモンの特徴ベクトルであると見なします。この特徴ベクトルを用いて、k-meansアルゴリズムで80個のクラスタに教師なしクラスタリングしてみました(機械学習に馴染みのない方は、「投票傾向が似ているポケモンどうしを自動でグループ分けする方法」だと思ってください><)。
グループ分けの結果全体はこのブログの末尾に貼りました。半分くらいのグループは解釈しにくいグループでしたが、面白い解釈ができるグループもいくつか存在したので、以下に抜粋します。
グループID | グループに属するポケモン(自動分類) | グループの傾向(私の解釈) |
---|---|---|
0 | ランターン, メリープ, モココ, チルタリス | ふわふわ系 |
9 | ミュウツー, ミュウ, スイクン, バンギラス, バシャーモ, ボスゴドラ, ボーマンダ, カイオーガ, グラードン, レックウザ, ガブリアス, ディアルガ, ギラティナ, ゼクロム | 古めの伝説+強そうなやつ |
22 | ワニノコ, オーダイル, メグロコ, ワルビル | ワニ |
72 | ニャース, ウツボット, ベロリンガ, マタドガス, ソーナンス, ドクケイル, サボネア, ハブネーク, チリーン, マネネ, マスキッパ, メガヤンマ | ロケット団 |
グループ0は、メリープ・モココとチルタリスが同じグループに入っているのが面白いです。ふわふわ好きからの支持でしょうか(ランターンが混ざっているのはよくわからず)。
グループ9は、伝説のポケモンとバシャーモ・ボーマンダ・ガブリアスなどポケモンバトルで強いポケモンが同居しているのが面白いです。
グループ22は、ステップ2でも見られたワニたちがきちんと同じグループに入っていることがわかります。
グループ72は、アニメ版のポケモンでロケット団員のムサシとコジロウが使っていたポケモンたちです。アニメを知らないとわからない情報を、機械学習によりデータからうまく抽出できたと言えるかもしれません。
ステップ4. 推薦システムを作る
最後に、実際に推薦システムを作ってみようと思います。
推薦システムは大きく分けて
が存在します(実用上は両者のハイブリッドモデルが用いられることが多いと思います)。
今回は、残念ながら手元にユーザーの属性情報(年齢・性別・居住地 etc.)などを持っていません。一方で、ユーザーとポケモンの相互作用には何か傾向がありそうだということが、ここまでの集計から分かりました。そこで、今回は「2. ユーザーとアイテムの相互作用に基づいて推薦する方法」の代表的な手法である、Matrix Factorizationと呼ばれる手法を適用してみます。
問題設定を簡単に説明します。全ユーザーの80%がtrainingユーザー、20%がtestユーザーとなるようにランダムに割り当てます。次に、testユーザーの投票のうち50%が学習用データ、50%が検証用データになるようにランダムに分割します。最後に、trainingユーザーの全投票データと、testユーザーの学習用データを用いてMatrix Factorizationのモデルを学習します。
さて、このような問題設定でMatrix Factorizationを学習し、検証用データに対して推薦を行なった結果の一部を下記に示します。
ユーザー名 | 実際の投票 | 推薦結果(1位〜10位) |
---|---|---|
@XXXXX | シャワーズ, オーダイル, メグロコ, ワルビル, (ワニノコ) | ウパー, ワニノコ, オーダイル, ナマコブシ, メグロコ, ワルビル, ユキハミ, ヌオー, ウールー, パルキア |
@YYYYY | エーフィ, シャワーズ, グレイシア, ミュウツー, (ニンフィア), (イーブイ), (ブースター), (サンダース), (マッシブーン), (ピカチュウ), (バドレックス) | シャワーズ, エーフィ, サンダース, ブースター, グレイシア, イーブイ, ニンフィア, ブラッキー, ミュウツー, ウインディ |
@ZZZZZ | ヒトモシ, シャンデラ, インテレオン, ライチュウ, (マポイップ), (ランプラー), (ストリンダー), (サーナイト), (コオリッポ), (ドリュウズ) | ピカチュウ, マッシブーン, バドレックス, ニドラン♂, シャンデラ, ミミッキュ, トリトドン, デデンネ, コイル, サニーゴ |
ユーザー@XXXXXさんは、シャワーズ, オーダイル, メグロコ, ワルビル, ワニノコに投票していました。このうち、ワニノコだけは学習から外してあります。つまり、シャワーズ, オーダイル, メグロコ, ワルビルに投票したという情報を使ってワニノコを推薦できればOKです。このとき、推薦結果の上位10位に正しくワニノコが入っていることが確認できました。ウパーやナマコブシなど、水辺にいそうなポケモンも推薦に含まれており、いい感じです。
ユーザー@YYYYYさんは、主にブイズに投票しています。エーフィ, シャワーズ, グレイシア, ミュウツーに投票したという情報から、無事に他のブイズを推薦することができています。しかし、マッシブーンやバドレックスといったポケモンはさすがに推薦することができなかったようです。
最後のユーザー@ZZZZZは、全然うまく推薦できておらず、ほとんど外してしまっています。@ZZZZZさんのように投票傾向が読みにくいユーザーも数多く存在するため、今回の推薦シスエテムは問題設定自体が難しいということがわかります。
最後に、簡単な定量評価としてrecall@k(テストデータにおける各ユーザーの投票うちの何割を、上位k位までの推薦によってカバーできているか?の平均値)を計算しました。
k | recall@k |
---|---|
10 | 0.26 |
30 | 0.40 |
50 | 0.48 |
recall@50が約0.5ですので、50匹推薦すれば投票の半分くらいを当てることができるということになります。
まとめ
今回は、簡単な集計から始めて、最後に実際に推薦システムを学習してみました。推薦性能向上のためには、先ほども少し触れた通りユーザー・ポケモンそれぞれの属性情報をうまく使っていくことが鍵になりそうです。効きそうなユーザー属性としてはぱっと思いつくのは
あたりでしょうか。ただ、どうやってユーザー情報を取得するのかが課題です。基本的なユーザー属性であれば過去のツイートから推定できるかもしれません*2。
また、ポケモンの属性としては
などが考えられます。
最後になりましたが、ポケモン25周年おめでとうございます!これからも、1ファンとして応援しています。
宣伝
他にも関連する技術記事を書いているので、よければ見ていってください。
ポケモン系
推薦システム系
補足など
ツイートの収集
Tweepyというライブラリを用いてTwitterのsearch APIを叩き、収集しました。Tweepyを使えばページネーション関連の実装を自前でする必要がなく、簡単に収集することができました。
(注意:この方法で収集された投票結果は、ポケモン公式が公表している数字と比べて少ないです。これは、Twitterが無料公開している検索APIの仕様によるものだと考えられます。ただ、推薦システムを作る上では必ずしも全データを取得する必要はないので、気にせずこのまま進めます。投票関連の数字はポケモン公式が公表しているものを正としてください。)
consumer_key = os.environ.get('CONSUMER_KEY') consumer_secret = os.environ.get('CONSUMER_SECRET') access_token = os.environ.get('ACCESS_TOKEN') access_token_secret = os.environ.get('ACCESS_TOKEN_SECRET') auth = tweepy.OAuthHandler(consumer_key, consumer_secret) auth.set_access_token(access_token, access_token_secret) api = tweepy.API(auth) def _limit_handled(cursor): while True: try: yield next(cursor) except TweepError as e: time.sleep(15 * 60) def download_votes(name, query): data = [] search_cursor = tweepy.Cursor(api.search, q=query, count=100, result_type='mixed', since_id=SINCE_ID).items() for result in _limit_handled(search_cursor): data.append(dict(user=result.user.screen_name, pokemon=name, time=result.created_at)) return pd.DataFrame(data)
ステップ2の結果一覧
私のポケモンに関する知識を総動員して、グループの傾向を記入してみました。それでも、解釈に苦しむグループもたくさんあったので、もし共通点を見つけられたかたは教えてください。
Matrix Factorizationの実装
LightFM というライブラリを用いて下記のように実装しました。Lossとしてwarpを用いました。何か特別な設定や処理はしていません。
from lightfm import LightFM model = LightFM(loss='warp') model.fit(data['train'], epochs=30, num_threads=2)