Bandit 算法简介

MAB的全称是 Multi-armed bandit problem(多臂老虎机问题)。它的背景是当赌场中有一排老虎机,每一台老虎机中奖的概率不同,有没有一种最优的策略来在各个老虎机之间分配自己的资金以实现收益的最大化。

我们可以把这个问题翻译成一个典型的推荐问题,有若干个item,每个item的曝光点击率不同,有没有一种最优的策略来分配item的曝光,从而让总体点击率最高。下面叙述中,我会统一采用推荐问题的术语,而不在用老虎机的术语。

当我们面对没有先验知识的item时,只能每个都去尝试一下,但很快我们就会遇到 Exploitation-Exploration(E&E) 两难的问题:

  • 只把流量花在历史点击率高的item上,会错过其他可能会有更好的点击率表现的item,这个方法推到极端就是 winner gets all;
  • 把流量花在到处探索上,就难免有较高的机会成本,不能积累到可观的收益,这个方法推到极端就是随机推荐。

bandit 算法是一类解决Exploitation-Exploration 问题的方法,根据是否考虑上下文特征,算法分为 context-free bandit 和 contextual bandit 两大类。

本篇主要介绍 context-free bandit 算法,这类算法可以认为是推荐系统中“千人一面”的算法,不考虑不同用户的差异性。

1、基本原理

(1)Epsilon-Greedy算法

首先,介绍一种最简单的算法。

选一个(0,1)之间较小的数epsilon,生成一个随机数,如果小于epsilon,则在所有item中选一个推荐,如果大于epsilon,则返回目前为止点击率最高的item。

这里epsilon的值可以控制对exploit和explore的偏好程度。

(2)Thompson sampling算法

想了解 Thompson sampling,就绕不开beta分布。知乎帖子(https://www.zhihu.com/question/30269898) 对beta分布有比较详细的介绍,这里会参照它进行介绍。

用一句话来说,beta分布可以看作一个二项分布期望值的概率分布。

举一个简单的例子,是用一个棒球运动员击中的球数除以击球的总数,就是击球率,我们一般认为0.266是正常水平的击球率,而如果击球率高达0.3就被认为是非常优秀的。

如果有一个棒球运动员,他的过往历史击球率是0.27,新赛季的第一场比赛,他击球10次,命中5次,我们可以说他现在的击球率是0.5吗?好像不太合理,那么用过往的0.27呢?好像也不太合理。

实际上,“他现在的击球率”这个概率值,也服从某个概率分布,对于二项分布来说,这个概率值的概率分布就是beta分布。

beta分布有 \alpha\beta 两个参数,可以表示成 Beta(\alpha, \beta) ,这两个参数分别代表在历史击球中,成功次数和失败次数。

\frac{\alpha}{\alpha + \beta}=0.27

我们可以取 \alpha = 81, \beta = 219,beta分布的概率密度图是这样的,可以看到期望为0.27

beta.png

这位球员在新赛季击球10次中5次,那么我们就可以更新Beta分布的参数了 Beta(81+5, 219+10),期望变成了0.273。

如果这位球员继续延续高水平的表现,击球200次中100次,那么他的击球率的分布就变成了 Beta(81+100, 219+200),期望变成了0.303,并且我们可以从概率密度图看到,新的击球率分布变窄了,这也和我们直观上的感觉是一致的。

beta2.png

那么Thompson sampling算法的基本原理就是,每一个item的点击率都服从一个Beta分布,Beta(alpha, beta),这个\alpha是点击次数, \beta 是曝光未点击的次数。

每个item都根据自己的Beta分布生成一个随机数,选择随机数最大的那个item。

(3)UCB算法

UCB是 Upper Confidence Bound 的简称。对于每个item的点击率p来说,假设历史观测的点击率是p’,那么我们可以认为,p实际是在p‘的邻域内的,我们可以假设

p' - \Delta <= p <= p' + \Delta

UCB算法就是使用真实值的上界 p‘+\Delta 来进行乐观预估。

我们会有一些观察:

  • 如果曝光的次数越多,\Delta应该越小,p'会越趋向于真实值p
  • 如果一个item在多次曝光中总是不被选中,那么它的delta应该逐渐增大,以保证它后面有机会曝光

UCB算法中选择

\Delta = \sqrt{\frac{2log(T)}{n}}

其中,T是总实验次数,n是选中该item的次数。这么选择的数学原理可以参考这篇知乎帖子(https://zhuanlan.zhihu.com/p/32356077) ,这里直接略过。

2、代码

代码结构:

 - item.py
 - mab_algo.py
 - mab_simulator.py

首先,我们抽象一个Item的类,它代表了推荐问题中的一个item,它会有一个自己真实的点击率(click_rate),会记录在实验中自己的曝光次数和点击次数,以及实验中观测到的点击率。

# item.py

import random

'''
定义被曝光的item
item有一个属性是点击率
每次被调用exposure函数时,都会随机返回1和0,返回1的概率是点击率
'''
class Item(object):
  def __init__(self, click_rate):
    self.click_rate = click_rate
    self.click_cnt = 0
    self.exposure_cnt = 0

  def exposure(self):
    rnd = random.random()
    self.exposure_cnt = self.exposure_cnt + 1
    if rnd <= self.click_rate:
      self.click_cnt = self.click_cnt + 1
      return 1
    else:
      return 0
  
  def get_ctr(self):
    if self.exposure_cnt == 0:
      return 0.0
    else:
      return self.click_cnt / self.exposure_cnt


if __name__ == "__main__":
  item = Item(0.1)
  exposure_cnt = 10000
  click_cnt = 0
  for i in range(exposure_cnt):
    item.exposure()
  print('exposure: %d click: %d click_rate: %4.2f' 
        %(item.exposure_cnt, item.click_cnt, 1.0*item.click_cnt/item.exposure_cnt) )

然后,我们根据上面的介绍,分别实现三种 bandit 算法,并实现了一个随机算法作为对比:

# mab_algo.py
import random
import numpy as np

# UCB算法
# p = p' + np.sqrt(2*np.log(T) / N)
# T是总实验次数,n是选中该item的次数
def ucb_algo(item_list):
  total_exposure_cnt = 0
  for item in item_list:
    total_exposure_cnt = total_exposure_cnt + item.exposure_cnt
  upper_bound_probs = [item.get_ctr() + np.sqrt(2*np.log(1+total_exposure_cnt) / max(1, item.exposure_cnt)) \
                      for item in item_list]
  return np.argmax(upper_bound_probs)

# Thompson sampling算法
def thompson_sampling_algo(item_list):
  max_item_idx = 0
  max_rand = -1
  for idx, item in enumerate(item_list):
    # beta(a, b), a should be larger than 0
    rand = np.random.beta(1+item.click_cnt, 1+item.exposure_cnt - item.click_cnt)
    if rand > max_rand:
      max_item_idx = idx
      max_rand = rand
  return max_item_idx

# epsilon的概率是随机返回
# 1-epsilon的概率是选择当前效果最好的一个
def epsilon_greedy_algo(item_list):
  epsilon = 0.2
  rnd = random.random()
  if rnd < epsilon:
    # explore
    return random.randint(0, len(item_list) - 1)
  else:
    # exploit
    max_item_idx = 0
    max_ctr = -1
    for idx, item in enumerate(item_list):
      if item.get_ctr() > max_ctr:
        max_ctr = item.get_ctr()
        max_item_idx = idx
    return max_item_idx

# 随机返回
def random_algo(item_list):
  return random.randint(0, len(item_list) - 1)

最后,我们写一个模拟器,来看一下不同算法的实际分配曝光的表现情况。

# mab_simulator.py
import random
from item import Item
from mab_algo import *

def get_exposure_item_idx(item_list, algo):
  return algo(item_list)

def demo():
  algo_list = [random_algo, epsilon_greedy_algo, thompson_sampling_algo, ucb_algo]
  click_rate_list = [0.05, 0.06, 0.1, 0.12, 0.2]
  
  exposure_cnt = 100000
  for algo in algo_list:
    print(algo)
    item_list = []
    for click_rate in click_rate_list:
      item = Item(click_rate)
      item_list.append(item)

    for i in range(exposure_cnt):
      idx = get_exposure_item_idx(item_list, algo)
      item_list[idx].exposure()

    for i in range(len(item_list)):
      print('item #%d click_rate=%4.2f%%\texpo=%d click=%d real_click_rate=%4.2f%%'
        %(I, 
          100*item_list[i].click_rate, 
          item_list[i].exposure_cnt, 
          item_list[i].click_cnt, 
          100.0*item_list[i].click_cnt/item_list[i].exposure_cnt))

if __name__ == "__main__":
  demo()

3、实验

(1)实验设置

我们设定5个item,#0到#4,它们的真实点击率分别是5%、6%、10%、12%和20%。

共进行10万次实验(也就是10万次曝光机会),记录在不同算法下,每个item的曝光次数。

(2)实验结果

先看随机算法的结果,不出意外,每个item的曝光次数差不多。

item #0 click_rate=5.00%    expo=20041 click=980 real_click_rate=4.89%
item #1 click_rate=6.00%    expo=19813 click=1231 real_click_rate=6.21%
item #2 click_rate=10.00%   expo=19946 click=1988 real_click_rate=9.97%
item #3 click_rate=12.00%   expo=20193 click=2358 real_click_rate=11.68%
item #4 click_rate=20.00%   expo=20007 click=3924 real_click_rate=19.61%

epsilon_greedy算法,实验中设置epsilon=0.2,也就是会有20%的曝光(2万次)会作为探索分配到各个item,实验结果也符合预期。

item #0 click_rate=5.00%    expo=3995 click=180 real_click_rate=4.51%
item #1 click_rate=6.00%    expo=3984 click=240 real_click_rate=6.02%
item #2 click_rate=10.00%   expo=3995 click=396 real_click_rate=9.91%
item #3 click_rate=12.00%   expo=3889 click=455 real_click_rate=11.70%
item #4 click_rate=20.00%   expo=84137 click=16898 real_click_rate=20.08%

thompson_sampling算法将几乎全部的曝光机会都给了点击率最高的 item #4。这是因为实验设置的item之间的点击率差异还是很大的,在beta分布中体现了很明显的差距。

item #0 click_rate=5.00%    expo=81 click=4 real_click_rate=4.94%
item #1 click_rate=6.00%    expo=167 click=16 real_click_rate=9.58%
item #2 click_rate=10.00%   expo=202 click=21 real_click_rate=10.40%
item #3 click_rate=12.00%   expo=313 click=40 real_click_rate=12.78%
item #4 click_rate=20.00%   expo=99237 click=19934 real_click_rate=20.09%

UCB算法也将大部分的曝光都给了 item #4,但没有thompson_sampling算法那么极端。

item #0 click_rate=5.00%    expo=730 click=29 real_click_rate=3.97%
item #1 click_rate=6.00%    expo=918 click=54 real_click_rate=5.88%
item #2 click_rate=10.00%   expo=1773 click=183 real_click_rate=10.32%
item #3 click_rate=12.00%   expo=1946 click=211 real_click_rate=10.84%
item #4 click_rate=20.00%   expo=94633 click=19115 real_click_rate=20.20%

4、总结

context-free bandit 算法是一类非常简单的算法,它可以有效解决E&E问题,并且计算过程非常简单,在实际应用中,因为它天生应对冷启动,容易做到实时反馈的特点,在很多场景中依然是很有用的工具。

为了解决 context-free 无法做到千人千面的问题,有很多可以结合上下文的 bandit 算法,其中LinUCB,就是比较有代表性的一种,它来自于雅虎的新闻推荐算法:https://arxiv.org/pdf/1003.0146.pdf

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,635评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,628评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,971评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,986评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,006评论 6 394
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,784评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,475评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,364评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,860评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,008评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,152评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,829评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,490评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,035评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,428评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,127评论 2 356