轉自:https://blog.csdn.net/heyc861221/article/details/80129310
推薦系統裏面有兩個經典問題:EE和冷啓動。前者涉及到平衡準確和多樣,後者涉及到產品算法運營等一系列。Bandit算法是一種簡單的在線學習算法,常常用於嘗試解決這兩個問題,本文爲你介紹基礎的Bandit算法及一系列升級版,以及對推薦系統這兩個經典問題的思考。
什麼是Bandit算法
爲選擇而生:我們會遇到很多選擇的場景。上哪個大學,學什麼專業,去哪家公司,中午吃什麼等等。這些事情,都讓選擇困難症的我們頭很大。那麼,有算法能夠很好地對付這些問題嗎?
當然有!那就是Bandit算法。Bandit算法來源於歷史悠久的賭博學,它要解決的問題是這樣的:
一個賭徒,要去搖老虎機,走進賭場一看,一排老虎機,外表一模一樣,但是每個老虎機吐錢的概率可不一樣,他不知道每個老虎機吐錢的概率分佈是什麼,那麼每次該選擇哪個老虎機可以做到最大化收益呢?這就是多臂賭博機問題(Multi-armed bandit problem, K-armed bandit problem, MAB)。
怎麼解決這個問題呢?最好的辦法是去試一試,不是盲目地試,而是有策略地快速試一試,這些策略就是Bandit算法。
這個多臂問題,推薦系統裏很多問題都與它類似:
- 假設一個用戶對不同類別的內容感興趣程度不同,那麼我們的推薦系統初次見到這個用戶時,怎麼快速地知道他對每類內容的感興趣程度?這就是推薦系統的冷啓動。
- 假設我們有若干廣告庫存,怎麼知道該給每個用戶展示哪個廣告,從而獲得最大的點擊收益?是每次都挑效果最好那個麼?那麼新廣告如何纔有出頭之日?
- 我們的算法工程師又想出了新的模型,有沒有比A/B test更快的方法知道它和舊模型相比誰更靠譜?
- 如果只是推薦已知的用戶感興趣的物品,如何才能科學地冒險給他推薦一些新鮮的物品?
Bandit算法與推薦系統
在推薦系統領域裏,有兩個比較經典的問題常被人提起,一個是EE問題,另一個是用戶冷啓動問題。
什麼是EE問題?又叫exploit-explore問題。exploit就是:對用戶比較確定的興趣,當然要利用開採迎合,好比說已經掙到的錢,當然要花;explore就是:光對着用戶已知的興趣使用,用戶很快會膩,所以要不斷探索用戶新的興趣才行,這就好比雖然有一點錢可以花了,但是還得繼續搬磚掙錢,不然花完了就得喝西北風。
用戶冷啓動問題,也就是面對新用戶時,如何能夠通過若干次實驗,猜出用戶的大致興趣。
我想,屏幕前的你已經想到了,推薦系統冷啓動可以用Bandit算法來解決一部分。這兩個問題本質上都是如何選擇用戶感興趣的主題進行推薦,比較符合Bandit算法背後的MAB問題。
比如,用Bandit算法解決冷啓動的大致思路如下:用分類或者Topic來表示每個用戶興趣,也就是MAB問題中的臂(Arm),我們可以通過幾次試驗,來刻畫出新用戶心目中對每個Topic的感興趣概率。這裏,如果用戶對某個Topic感興趣(提供了顯式反饋或隱式反饋),就表示我們得到了收益,如果推給了它不感興趣的Topic,推薦系統就表示很遺憾(regret)了。如此經歷“選擇-觀察-更新-選擇”的循環,理論上是越來越逼近用戶真正感興趣的Topic的,
怎麼選擇Bandit算法?
現在來介紹一下Bandit算法怎麼解決這類問題的。Bandit算法需要量化一個核心問題:錯誤的選擇到底有多大的遺憾?能不能遺憾少一些?
王家衛在《一代宗師》裏寄出一句臺詞:
人生要是無憾,那多無趣?
而我說:算法要是無憾,那應該是過擬合了。
所以說:怎麼衡量不同Bandit算法在解決多臂問題上的效果?首先介紹一個概念,叫做累積遺憾(regret)
這個公式就是計算Bandit算法的累積遺憾,解釋一下:
首先,這裏我們討論的每個臂的收益非0即1,也就是伯努利收益。
然後,每次選擇後,計算和最佳的選擇差了多少,然後把差距累加起來就是總的遺憾。
wB(i)是第i次試驗時被選中臂的期望收益, w*是所有臂中的最佳那個,如果上帝提前告訴你,我們當然每次試驗都選它,問題是上帝不告訴你,所以就有了Bandit算法,我們就有了這篇文章。
這個公式可以用來對比不同Bandit算法的效果:對同樣的多臂問題,用不同的Bandit算法試驗相同次數,看看誰的regret增長得慢。
那麼到底不同的Bandit算法有哪些呢?
常用Bandit算法
Thompson sampling算法
Thompson sampling算法簡單實用,因爲它只有一行代碼就可以實現[3]。簡單介紹一下它的原理,要點如下:
- 假設每個臂是否產生收益,其背後有一個概率分佈,產生收益的概率爲p。
- 我們不斷地試驗,去估計出一個置信度較高的“概率p的概率分佈”就能近似解決這個問題了。
- 怎麼能估計“概率p的概率分佈”呢? 答案是假設概率p的概率分佈符合beta(wins, lose)分佈,它有兩個參數: wins,
lose。 - 每個臂都維護一個beta分佈的參數。每次試驗後,選中一個臂,搖一下,有收益則該臂的wins增加1,否則該臂的lose增加1。
- 每次選擇臂的方式是:用每個臂現有的beta分佈產生一個隨機數b,選擇所有臂產生的隨機數中最大的那個臂去搖。
- List item
import numpy as np
import pymc
#wins 和 trials 是一個N維向量,N是賭博機的臂的個數,每個元素記錄了
choice = np.argmax(pymc.rbeta(1 + wins, 1 + trials - wins))
wins[choice] += 1
trials += 1
UCB
UCB算法全稱是Upper Confidence Bound(置信區間上界),它的算法步驟如下:
- 初始化:先對每一個臂都試一遍;
- 按照如下公式計算每個臂的分數,然後選擇分數最大的臂作爲選擇:
3. 觀察選擇結果,更新t和Tjt。其中加號前面是這個臂到目前的收益均值,後面的叫做bonus,本質上是均值的標準差,t是目前的試驗次數,Tjt是這個臂被試次數
這個公式反映一個特點:均值越大,標準差越小,被選中的概率會越來越大,同時哪些被選次數較少的臂也會得到試驗機會。
import numpy as np
T = 1000 # T輪試驗
N = 10 # N個老虎機
true_rewards = np.random.uniform(low=0, high=1, size=N) # 每個老虎機真實的吐錢概率
estimated_rewards = np.zeros(N) # 每個老虎機吐錢的觀測概率,初始都爲0
chosen_count = np.zeros(N) # 每個老虎機當前已經探索的次數,初始都爲0
total_reward = 0
# 計算delta
def calculate_delta(T, item):
if chosen_count[item] == 0:
return 1
else:
return np.sqrt(2 * np.log(T) / chosen_count[item])
# 計算每個老虎機的p+delta,同時做出選擇
def UCB(t, N):
upper_bound_probs = [estimated_rewards[item] + calculate_delta(t, item) for item in range(N)]
item = np.argmax(upper_bound_probs)
reward = np.random.binomial(n=1, p=true_rewards[item])
return item, reward
for t in range(1, T): # 依次進行T次試驗
# 選擇一個老虎機,並得到是否吐錢的結果
item, reward = UCB(t, N)
total_reward += reward # 一共有多少客人接受了推薦
# 更新每個老虎機的吐錢概率
estimated_rewards[item] = ((t - 1) * estimated_rewards[item] + reward) / t
chosen_count[item] += 1
Epsilon-Greedy算法
這是一個樸素的Bandit算法,有點類似模擬退火的思想:
- 選一個(0,1)之間較小的數作爲epsilon;
- 每次以概率epsilon做一件事:所有臂中隨機選一個;
- 每次以概率1-epsilon 選擇截止到當前,平均收益最大的那個臂。
是不是簡單粗暴?epsilon的值可以控制對Exploit和Explore的偏好程度。越接近0,越保守,只想花錢不想掙錢。
樸素Bandit算法
最樸素的Bandit算法就是:先隨機試若干次,計算每個臂的平均收益,一直選均值最大那個臂。這個算法是人類在實際中最常採用的,不可否認,它還是比隨機亂猜要好。
以上五個算法,我們用10000次模擬試驗的方式對比了其效果如圖4,實驗代碼來源 :
算法效果對比一目瞭然:UCB算法和Thompson採樣算法顯著優秀一些。
至於你實際上要選哪一種Bandit算法,你可以選一種Bandit算法來選Bandit算法。
Bandit算法與線性迴歸
UCB算法
UCB算法在做EE(Exploit-Explore)的時候表現不錯,但它是上下文無關(context free)的Bandit算法,它只管埋頭幹活,根本不觀察一下面對的都是些什麼特點的arm,下次遇到相似特點但不一樣的arm也幫不上什麼忙。
UCB解決Multi-armed bandit問題的思路是:用置信區間。置信區間可以簡單地理解爲不確定性的程度,區間越寬,越不確定,反之亦反之。
每個item的回報均值都有個置信區間,隨着試驗次數增加,置信區間會變窄(逐漸確定了到底回報豐厚還是可憐)。每次選擇前,都根據已經試驗的結果重新估計每個Item的均值及置信區間。 選擇置信區間上限最大的那個Item。
“選擇置信區間上界最大的那個Item”這句話反映了幾個意思:
- 如果Item置信區間很寬(被選次數很少,還不確定),那麼它會傾向於被多次選擇,這個是算法冒風險的部分;
- 如果Item置信區間很窄(備選次數很多,比較確定其好壞了),那麼均值大的傾向於被多次選擇,這個是算法保守穩妥的部分;
UCB是一種樂觀的算法,選擇置信區間上界排序,如果時悲觀保守的做法,是選擇置信區間下界排序。
UCB算法加入特徵信息
Yahoo!的科學家們在2010年發表了一篇論文[6],給UCB引入了特徵信息,同時還把改造後的UCB算法用在了Yahoo!的新聞推薦中,算法名叫LinUCB,劉鵬博士在《計算廣告》一書中也有介紹LinUCB在計算廣告中的應用。
單純的老虎機回報情況就是老虎機自己內部決定的,而在廣告推薦領域,一個選擇的回報,是由User和Item一起決定的,如果我們能用Feature來刻畫User和Item這一對CP,在每次選擇Item之前,通過Feature預估每一個arm(item)的期望回報及置信區間,選擇的收益就可以通過Feature泛化到不同的Item上。
爲UCB算法插上了特徵的翅膀,這就是LinUCB最大的特色。
圖5 應用LinUCB算法的Yahoo!首頁
LinUCB算法做了一個假設:一個Item被選擇後推送給一個User,其回報和相關Feature成線性關係,這裏的“相關Feature”就是context,也是實際項目中發揮空間最大的部分。
於是試驗過程就變成:用User和Item的特徵預估回報及其置信區間,選擇置信區間上界最大的Item推薦,觀察回報後更新線性關係的參數,以此達到試驗學習的目的。
LinUCB基本算法描述如下:
對照每一行解釋一下(編號從1開始):
- 設定一個參數\alpha,這個參數決定了我們Explore的程度;
- 開始試驗迭代;
- 獲取每一個arm的特徵向量xa,t;
- 開始計算每一個arm的預估回報及其置信區間;
- 如果arm還從沒有被試驗過,那麼:
- 用單位矩陣初始化Aa;
- 用0向量初始化ba;
- 處理完沒被試驗過的arm;
- 計算線性參數\theta;
- 用\theta和特徵向量xa,t計算預估回報,同時加上置信區間寬度;
- 處理完每一個arm;
- 選擇第10步中最大值對應的arm,觀察真實的回報rt;
- 更新Aat;
- 更新bat;
- 算法結束。
注意到上面的第4步,給特徵矩陣加了一個單位矩陣,這就是嶺迴歸(ridge regression),嶺迴歸主要用於當樣本數小於特徵數時,對迴歸參數進行修正。
對於加了特徵的Bandit問題,正符合這個特點:試驗次數(樣本)少於特徵數。
每一次觀察真實回報之後,要更新的不止是嶺迴歸參數,還有每個arm的回報向量ba。
import numpy as np
class LinUCB:
"""
首先我們設定一些超參數,比如α,正反饋和負反饋的獎勵程度r1,r0,上下文特徵的長度d
我們設定我們的幾個矩陣,比如A和A的逆矩陣,b(x和r的乘積),以及參數矩陣:
"""
def __init__(self):
self.alpha = 0.25
self.r1 = 0.6
self.r0 = -16
self.d = 6 # dimension of user features
self.Aa = {} # Aa : collection of matrix to compute disjoint part for each article a, d*d
self.AaI = {} # AaI : store the inverse of all Aa matrix
self.ba = {} # ba : collection of vectors to compute disjoin part, d*1
self.theta = {}
self.a_max = 0
self.x = None
self.xT = None
"""
初始化矩陣對應上面的4-7步,A設置爲單位矩陣,b設置爲0矩陣,參數也設置爲0矩陣,注意的是,每個arm都有這麼一套矩陣:
"""
def set_articles(self,art):
for key in art:
self.Aa[key] = np.identity(self.d) # 創建單位矩陣
self.ba[key] = np.zeros((self.d,1))
self.AaI[key] = np.identity(self.d)
self.theta[key] = np.zeros((self.d,1))
"""
這對應於上面的12-13步,根據選擇的最優arm,以及得到的用戶反饋,我們更新A和b矩陣:
"""
def update(self,reward):
if reward == -1:
pass
elif reward == 1 or reward == 0:
if reward == 1:
r = self.r1
else:
r = self.r0
self.Aa[self.a_max] += np.dot(self.x,self.xT)
self.ba[self.a_max] += r * self.x
self.AaI[self.a_max] = np.linalg.inv(self.Aa[self.a_max])
self.theta[self.a_max] = np.dot(self.AaI[self.a_max],self.ba[self.a_max])
else:
# error
pass
"""
計算推薦結果對應於上面的8-11步,我們直接根據公式計算當前的最優參數和置信上界,並選擇最大的arm作爲推薦結果。代碼中有個小trick,及對所有的arm來說,共同使用一個特徵,而不是每一個arm單獨使用不同的特徵:
"""
def recommend(self,timestamp,user_features,articles):
xaT = np.array([user_features]) # d * 1
xa = np.transpose(xaT)
AaI_tmp = np.array([self.AaI[article] for article in articles])
theta_tmp = np.array([self.theta[article] for article in articles])
art_max = articles[np.argmax(np.dot(xaT,theta_tmp) + self.alpha * np.sqrt(np.dot(np.dot(xaT,AaI_tmp),xa)))]
self.x = xa
self.xT = xaT
self.a_max = art_max
return self.a_max
Exploit-Explore這一對矛盾一直客觀存在,Bandit算法是公認的一種比較好的解決EE問題的方案。除了Bandit算法之外,還有一些其他的explore的辦法,比如:在推薦時,隨機地去掉一些用戶歷史行爲(特徵)。
解決Explore,勢必就是要冒險,勢必要走向未知,而這顯然就是會傷害用戶體驗的:明知道用戶肯定喜歡A,你還偏偏以某個小概率給推薦非A。
實際上,很少有公司會採用這些理性的辦法做Explore,反而更願意用一些盲目主觀的方式。究其原因,可能是因爲:
- 互聯網產品生命週期短,而Explore又是爲了提升長期利益的,所以沒有動力做;
- 用戶使用互聯網產品時間越來越碎片化,Explore的時間長,難以體現出Explore 的價值;
- 同質化互聯網產品多,用戶選擇多,稍有不慎,用戶用腳投票,分分鐘棄你於不顧;
- 已經成規模的平臺,紅利槓槓的,其實是沒有動力做Explore的。
基於這些,我們如果想在自己的推薦系統中引入Explore機制,需要注意以下幾點:
- 用於Explore的Item要保證其本身質量,縱使用戶不感興趣,也不至於引起其反感;
- Explore本身的產品需要精心設計,讓用戶有耐心陪你玩兒;
- 深度思考,這樣纔不會做出腦殘的產品,產品不會早早夭折,纔有可能讓Explore機制有用武之地。
轉自:https://blog.csdn.net/heyc861221/article/details/80129310