読者です 読者をやめる 読者になる 読者になる

ユーザーのデータをランダムに選んでまとめる機能について考える

サービスの機能を考える時などに、「ユーザのデータをいい感じにランダムに選んで、こちらが自動的にまとめて表示してあげたい」みたいな会話がされるけど、この場合のランダムというのは純粋なランダムではない場合が多い。

例えば、CGM系のサービスでユーザの投稿をランダムに5件、抜き出してまとめて表示するというような機能を考えた時には、実際に本当にランダムに取得してしまうと、よくない結果が得られてしまう。

純粋なランダム選択をRuby(Rails)で書くなら、こう。

posts = Post.where(user_id: user_id).all
random_posts = post.sample(5)

これだと、こういう事が起こる。

f:id:ainame:20150325184346p:plain

投稿が数年間に行われたのに、ランダムに選択した結果、はじめの方と最後の方に偏ってしまったパターン。これはこれでランダムなんだから別にいいじゃんって考えもあるけど、出来るかぎり全体の時系列をまとめたものが欲しいと期待されてしまう。

まとめる対象が文章とかだったらまだ良いかもしれないけど、これが例えば写真や動画をまとめる機能だとすると、時系毎にそこに写る被写体の様子がガラリと変わるので、選択のロジックがコンテンツの質に直結してしまう。

よりよい選択を得るために考えたロジックがあるのでここにまとめる。 モンテカルロ法を使ったアプローチだ。モンテカルロ法はよく円周率の計算とか、最近だと囲碁プログラムの指し手を思考する部分に使われたりする。

たとえばさっきのようなコードも、1回だけしか実行しなかったら上図のような結果になってしまうかもしれない。しかし、それが何千、何万回と実行された時により良い結果が現れることもあるはず。その試行結果の中からもっとも良いと思われる結果を取り出せば、それなりに妥当な結果が得られる。

posts = Post.where(user_id: user_id).all

samples = []
10000.times do 
  samples << post.sample(5)
end

most_valuable_sample = samples.sort_by {|sample| Evaluator.call(sample) }.last

さっきと同じ、sampleをただひたすら並べていって、結果の良し悪しを評価するEvaluatorによって評価値をもとめてその値を元にソートし、一番評価が高かったやつを採用する。Evaluatorは、選びたい内容によって適宜変えるとよさ気。

ユーザの投稿をそれなりに等間隔かつランダムに出してあげたいって時なんかは、選んだ投稿の列から、投稿ごとの作成日の変化量を取得して、変化量の標準偏差を取ってあげると、標準偏差が小さい(= ある投稿から次の投稿の間隔のばらつきが少ない状態)ものを選ぶことが出来たりする。

ただ、これだけだとうまくいかないこともあるとは思うので、適当なヒューリスティックを混ぜて利用すると良い。たとえば、20個ランダムに選ぶうち先頭と最後のデータはサービスを開始した初月と直近1ヶ月のデータをそれぞれ選ぶとか。

この書き方のメリットとして、ランダムに選ぶ部分をただひたすら繰り返すだけでそれなりに妥当な結果を得られてしまうので、プログラムが偉くシンプルになって秘伝のタレみたいなコードが生まれなくて良い。デメリットとしては、(この書き方をしなくても)ランダム性が期待されるコードはそもそもテストが難しいというのと、実行に若干時間がかかるので、Webのリクエスト時に処理するにはそんなに向かないということだ。

そんな感じで、昨日ランダムまとめ機能を作ったりしたので日記を書いてみた。 まとめとしては言葉でランダムって言うのは簡単だけど、人間が期待するランダムと数学的なランダムは違うので、プログラマーのみなさんめげずに頑張りましょうってことです。ちなみに、ガチャの確率については知見無いので特に語れません。