bandit算法原理及Python實現

Bandit算法是在線學習的一種,一切通過數據收集而得到的概率預估任務,都能通過Bandit系列算法來進行在線優化。這裏的“在線”,指的不是互聯網意義上的線上,而是隻算法模型參數根據觀察數據不斷演變。

以多臂老虎機問題爲例,首先我們假設每個臂是否產生收益,其背後有一個概率分佈,產生收益的概率爲p

我們不斷地試驗,去估計出一個置信度較高的概率p的概率分佈就能近似解決這個問題了。

怎麼能估計概率p的概率分佈呢? 答案是假設概率p的概率分佈符合beta(wins, lose)分佈,它有兩個參數: wins, lose。

每個臂都維護一個beta分佈的參數。每次試驗後,選中一個臂,搖一下,有收益則該臂的wins增加1,否則該臂的lose增加1。

初始化beta參數 勝率和敗率都爲0.5  
  prior_a = 1. # aka successes 
    prior_b = 1. # aka failures
    estimated_beta_params = np.zeros((K,2))
    estimated_beta_params[:,0] += prior_a # allocating the initial conditions
    estimated_beta_params[:,1] += prior_b
beta參數要在後面的計算中不斷更新的。

-------------------------------------------------------------------------------------------

對於硬幣或者骰子這樣的簡單實驗,我們事先能很準確地掌握系統成功的概率。然而通常情況下,系統成功的概率是未知的。爲了測試系統的成功概率p,我們做n次試驗,統計成功的次數k,於是很直觀地就可以計算出p=k/n。然而由於系統成功的概率是未知的,這個公式計算出的p只是系統成功概率的最佳估計。也就是說實際上p也可能爲其它的值,只是爲其它的值的概率較小。

例如有某種特殊的硬幣,我們事先完全無法確定它出現正面的概率。然後拋10次硬幣,出現5次正面,於是我們認爲硬幣出現正面的概率最可能是0.5。但是即使硬幣出現正面的概率爲0.4,也會出現拋10次出現5次正面的情況。因此我們並不能完全確定硬幣出現正面的概率就是0.5,所以p也是一個隨機變量,它符合Beta分佈。

Beta分佈是一個連續分佈,由於它描述概率p的分佈,因此其取值範圍爲0到1。
Beta分佈有αβ兩個參數,其中α爲成功次數加1,β爲失敗次數加1。

連續分佈用概率密度函數描述,下面繪製實驗10次,成功4次和5次時,系統成功概率p的分佈情況。可以看出k=5時,曲線的峯值在p=0.5處,而k=4時,曲線的峯值在p=0.4處。

n = 10
k = 5
p = np.linspace(0, 1, 100)
pbeta = stats.beta.pdf(p, k+1, n-k+1)
plot(p, pbeta, label="k=5", lw=2)

k = 4
pbeta = stats.beta.pdf(p, k+1, n-k+1)
plot(p, pbeta, label="k=4", lw=2)
xlabel("$p$")
legend(loc="best");
----------------------------------------------------------------------------------------------------------------

幾個bandit算法:

我們先從最簡單的開始,先試幾次,每個臂都有了均值之後,一直選均值最大那個臂。這個算法是我們人類在實際中最常採用的,不可否認,它還是比隨機亂猜要好。

 def naive(estimated_beta_params,number_to_explore=100):
    totals = estimated_beta_params.sum(1) # totals
    if np.any(totals < number_to_explore): # if have been explored less than specified
        least_explored = np.argmin(totals) # return the one least explored
        return least_explored
    else: # return the best mean forever
        successes = estimated_beta_params[:,0] # successes
        estimated_means = successes/totals # the current means
        best_mean = np.argmax(estimated_means) # the best mean
        return best_mean

下一個,Thompson sampling算法:

簡單介紹一下它的原理:

每次選擇臂的方式是:用每個臂現有的beta分佈產生一個隨機數b,選擇所有臂產生的隨機數中最大的那個臂去搖。

以上就是Thompson採樣,用python實現就一行:

np.argmax(pymc.rbeta(1 + successes, 1 + totals - successes))

第三個是UCB算法:

UCB算法全稱是Upper Confidence Bound(置信區間上界),算法的具體步驟如下:

先對每一個臂都試一遍

之後,每次選擇以下值最大的那個臂

其中加號前面是這個臂到目前的收益均值,後面的叫做bonus,本質上是均值的標準差,置信區間可以簡單地理解爲不確定性的程度,區間越寬,越不確定,反之亦反之。t是目前的試驗次數,Tjt是這個臂被試次數。

這個公式反映:均值越大,標準差越小,被選中的概率會越來越大,起到了exploit的作用;同時哪些被選次數較少的臂也會得到試驗機會,起到了explore的作用。

每個item的回報均值都有個置信區間,隨着試驗次數增加,置信區間會變窄(逐漸確定了到底回報豐厚還是可憐)。

每次選擇前,都根據已經試驗的結果重新估計每個item的均值及置信區間。

選擇置信區間上限最大的那個item。

“選擇置信區間上界最大的那個item”這句話反映了幾個意思:

  1. 如果item置信區間很寬(被選次數很少,還不確定),那麼它會傾向於被多次選擇,這個是算法冒風險的部分;
  2. 如果item置信區間很窄(備選次數很多,比較確定其好壞了),那麼均值大的傾向於被多次選擇,這個是算法保守穩妥的部分;
  3. UCB是一種樂觀的算法,選擇置信區間上界排序,如果時悲觀保守的做法,是選擇置信區間下界排序。
def UCB(estimated_beta_params):
    t = float(estimated_beta_params.sum()) # total number of rounds 
    totals = estimated_beta_params.sum(1)  #The number of experiments per arm
    successes = estimated_beta_params[:,0]
    estimated_means = successes/totals # earnings mean
    estimated_variances = estimated_means - estimated_means**2    
    UCB = estimated_means + np.sqrt( np.minimum( estimated_variances + np.sqrt(2*np.log(t)/totals), 0.25 ) * np.log(t)/totals )
    return np.argmax(UCB)
第四個,Epsilon-Greedy算法:

選一個(0,1)之間較小的數epsilon

每次以概率epsilon(產生一個[0,1]之間的隨機數,比epsilon小)做一件事:所有臂中隨機選一個。否則,選擇截止當前,平均收益最大的那個臂。

是不是簡單粗暴?epsilon的值可以控制對Exploit和Explore的偏好程度。越接近0,越保守,只想花錢不想掙錢。

代碼:

from arm import Arm
import random
import numpy as np


def mean(values):
    return sum(values)*1.0/len(values)

class EpsilonGreedyAlgorithm(object):

    def __init__(self, arms, epsilon):
        self.epsilon = epsilon
        self.arms = arms
        self.values = [[] for i in arms]

    def select_arm(self):
        if random.random() > self.epsilon:
            arm_idx = self.get_best_arm_idx()
        else:
            arm_idx = self.get_random_arm_idx()

        arm = self.arms[arm_idx]
        reward = arm.pull()
        self.update(arm_idx, reward)

    def update(self, arm_idx, reward):
        self.values[arm_idx].append(reward)

    def get_best_arm_idx(self):
        max_yhat = 0.0
        max_idx = None
        for i, values in enumerate(self.values):
            yhat = 0.0 if len(values) == 0 else mean(values)
            if yhat > max_yhat:
                max_yhat = yhat
                max_idx = i

        if max_idx is None:
            return self.get_random_arm_idx()
        else:
            return max_idx

    def get_random_arm_idx(self):
        return random.randrange(len(self.arms))


if __name__=="__main__":
	epsilon = 0.1
	ps = [random.random() for i in range(random.randrange(2, 8))]
            arms = [Arm(p) for p in ps]
            algo = EpsilonGreedyAlgorithm(arms, epsilon=epsilon)
            for i in range(100):
                algo.select_arm()
            total_reward = 0
            for i, vals in enumerate(algo.values):
                total_reward += sum(vals)
    print "reward:", total_reward, "\t epsilon:", epsilon
發佈了11 篇原創文章 · 獲贊 13 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章