Bandit 演算法#
Bandit 演算法(Multi-Armed Bandit,多臂老虎機)解決的是 探索與利用(Exploration vs Exploitation) 問題。
在推薦系統中,它用於回答:「該繼續推薦用戶已知喜歡的內容,還是冒險試試新東西?」
Bandit 在推薦的三大典型場景:
- 冷啟動:新用戶或新物品還沒資料,用 Bandit 邊推邊學
- 多策略融合:首頁有多種推薦策略(熱門、CF、深度模型)競爭曝光
- 多素材選擇:同一個推薦位有多張圖 / 多個標題候選
問題背景#
多臂老虎機問題#
想像一排老虎機,每台的中獎機率未知。目標是在有限次嘗試中拿到最大累積收益。
策略 = 利用(選歷史表現最好的)+ 探索(試不確定的)推薦場景的對應#
- 每個 arm = 一個候選物品 / 策略 / 素材
- 拉一次 = 推給用戶看一次
- 收益 = 點擊、停留、購買
只利用不探索 ➡️ 陷入「資訊繭房」、永遠錯過更好的選項;只探索不利用 ➡️ 用戶體驗崩掉。Bandit 的核心是「隨經驗累積動態調整探索比例」。
探索與利用的核心直覺#
在挑演算法之前,先建立幾個會反覆用到的直覺——它們決定了「該探索多少」。
時間區間決定探索比例#
探索與利用的比例,本質上由剩餘時間決定:
- 剛搬到一座城市(前面還有好幾年)➡️ 該多試新餐廳。
- 下週就要搬走 ➡️ 回到已知最愛,別再冒險。
對推薦的啟示:新用戶生命週期長,應積極探索找出興趣;臨近流失的用戶,應專注已驗證的內容。所以「探索強度」不是固定常數,而該隨用戶所處的時間區間調整。
後悔:衡量探索的代價#
後悔(Regret) = 「事後諸葛、從頭就一直選最優 arm 的累積收益」減去「實際累積收益」。它把「探索犯的錯」量化成一個數字。
理論結果(Lai-Robbins):最佳策略的後悔只以 對數速率 成長——意思是「前期犯的錯,比後期多得多」。後悔雖然永遠不會歸零,但好的策略讓每年新增的後悔越來越少。這解釋了為何所有好 Bandit(UCB、Thompson)前期都拚命試、後期才穩定下來。
未知的價值(Gittins 指數)#
一個完全沒試過(0 勝 0 敗)的選項,到底值多少?Gittins 指數給了量化答案:在標準設定下,全新選項的指數約為 0.70,比一個已知勝率 70% 的選項還高。
關鍵洞察是「探索紅利消退得很慢」:即使新選項第一次就失敗(0 勝 1 敗),它的指數通常仍高於 50%,值得再給一次機會。剩餘時間越長,未知選項越值錢——這與「時間區間」的直覺完全一致。
樂觀面對不確定性#
UCB 與 Thompson 共享同一種態度:對不確定的選項給予樂觀的估計,先假設它可能很好,直到資料證明相反。
決策依據 = 已知表現 + 不確定性帶來的「樂觀加分」- 樣本少 ➡️ 不確定性大 ➡️ 樂觀加分大 ➡️ 自然被選中去探索。
- 樣本多 ➡️ 估計變穩 ➡️ 加分縮小 ➡️ 自動轉向利用。
這就是為何「樂觀」在長期反而是最小化後悔的理性策略:它保證每個選項都被充分試過,不會因為運氣差的前幾次就被永久埋沒。下面的 UCB 與 Thompson Sampling 正是這個直覺的兩種具體實作。
基礎 Bandit 演算法#
ε-Greedy#
最簡單的策略:
以機率 ε 隨機探索
以機率 1-ε 選擇當前最優實作程式碼
import numpy as np
def epsilon_greedy(Q, epsilon=0.1):
"""
Q: 每個 arm 的估計價值
epsilon: 探索機率
"""
if np.random.random() < epsilon:
return np.random.randint(len(Q))
else:
return np.argmax(Q)ε-Greedy 的探索是盲目的:對「明顯爛」與「不確定可能很好」的 arm 一視同仁。後面 UCB 與 Thompson Sampling 都在改進這點。
湯普森採樣(Thompson Sampling)#
利用貝葉斯思想,為每個 arm 維護一個機率分佈,每次決策時從分佈中採樣:
1. 為每個 arm 維護 Beta(α, β) 分佈
- α:成功次數 + 1
- β:失敗次數 + 1
2. 每次決策:
- 從每個 arm 的分佈中採樣
- 選採樣值最大的 arm
3. 觀察結果後更新分佈實作程式碼
import numpy as np
class ThompsonSampling:
def __init__(self, n_arms):
self.alpha = np.ones(n_arms)
self.beta = np.ones(n_arms)
def select_arm(self):
samples = np.random.beta(self.alpha, self.beta)
return np.argmax(samples)
def update(self, arm, reward):
if reward == 1:
self.alpha[arm] += 1
else:
self.beta[arm] += 1Thompson Sampling 在實務上幾乎是首選:實作簡單、效果好、自帶機率語意。Microsoft、Yahoo 的廣告平台都重度使用。
UCB(Upper Confidence Bound)#
每次選擇「信賴上界」最大的 arm:
UCB_i = Q_i + c × √(ln(t) / N_i)Q_i:arm i 的平均收益t:總嘗試次數N_i:arm i 被選中的次數c:探索係數
直觀理解(樂觀面對不確定性):
- 嘗試次數越少 ➡️ 不確定性越大 ➡️ UCB 加項越大
- 平均收益高的會被選 ➡️ 利用
- 沒怎麼試過的也會被選(因為加項大)➡️ 探索
- 兩者自動平衡,不需手動調 ε
Contextual Bandit#
動機#
基礎 Bandit 不看「現在面對的是誰」。但推薦顯然要看用戶特徵與場景:年輕女性與中年男性適合的內容差很多。
LinUCB#
假設收益與上下文線性相關:
r = x^T θ + ε
選擇策略:
arm* = argmax(x_a^T θ̂_a + α × √(x_a^T A_a^(-1) x_a))x_a:arm a 在當前上下文下的特徵向量θ̂_a:arm a 的參數估計A_a:協方差矩陣(衡量不確定性)
LinUCB 是 Yahoo 新聞首頁推薦的經典方案(2010 WWW 論文)。線性假設雖簡單,但工程上易於部署、線上學習穩定。
LinUCB 更新規則
class LinUCB:
def __init__(self, d, alpha=0.1):
self.d = d
self.alpha = alpha
self.A = {} # A = I + Σ x x^T
self.b = {} # b = Σ r × x
def get_action(self, x, arms):
best_arm, best_ucb = None, -float('inf')
for a in arms:
if a not in self.A:
self.A[a] = np.eye(self.d)
self.b[a] = np.zeros(self.d)
A_inv = np.linalg.inv(self.A[a])
theta = A_inv @ self.b[a]
ucb = x @ theta + self.alpha * np.sqrt(x @ A_inv @ x)
if ucb > best_ucb:
best_arm, best_ucb = a, ucb
return best_arm
def update(self, arm, x, reward):
self.A[arm] += np.outer(x, x)
self.b[arm] += reward * xBandit + 協同過濾#
COFIBA#
把 CF 的「相似用戶分享資訊」與 Bandit 的「動態探索」結合:
- 用戶聚類:把相似用戶聚成群組
- 群組共享資訊:群內用戶共享探索結果
- 動態更新:根據回饋調整聚類
用戶相似度 → 動態聚類 → 群組級 LinUCB → 個人化推薦COFIBA 緩解了單純 Bandit 對新用戶要從零開始學的問題:新用戶可借助同群其他用戶的探索經驗。
工程實踐#
場景選擇#
| 場景 | 推薦演算法 | 理由 |
|---|---|---|
| 新用戶 / 新物品 | Thompson Sampling | 快速學習、自動平衡 |
| 首頁多策略選擇 | LinUCB | 考慮用戶上下文 |
| A/B 測試替代 | Bandit | 自動收斂到最優策略 |
Thompson Sampling vs UCB#
| 維度 | Thompson Sampling | UCB |
|---|---|---|
| 理論保證 | 較弱 | 較強 |
| 實務效果 | 優秀 | 良好 |
| 實作複雜度 | 簡單 | 簡單 |
| 計算開銷 | 採樣 | 矩陣運算 |
| 適用場景 | 通用 | 需嚴格理論保證時 |
延遲回饋處理#
推薦系統的回饋常有延遲(用戶看到推薦到實際購買可能差好幾天):
策略 1:使用短期代理指標(如點擊)
策略 2:設置回饋等待窗口
策略 3:使用延遲感知的 Bandit 演算法批次更新#
線上不會每個請求都更新模型,而是批次更新:
- 收集一段時間的回饋
- 定期批次更新參數
- 平衡即時性與計算開銷
Bandit vs A/B 測試#
何時用 Bandit#
| 對比 | A/B 測試 | Bandit |
|---|---|---|
| 流量分配 | 固定(如 50/50) | 動態(依表現調整) |
| 結論時點 | 實驗結束才下結論 | 邊跑邊收斂 |
| 對劣質策略 | 全程承擔劣質曝光成本 | 自動降低劣質策略曝光 |
| 結果解讀 | 統計顯著、易於決策 | 不易做嚴格因果推論 |
| 適用場景 | 需要嚴謹結論的功能上線決策 | 多素材 / 多策略動態擇優 |
Bandit 不能完全取代 A/B:A/B 適合「決定要不要上線一個功能」(重大決策、需要乾淨的對照組),Bandit 適合「在多個已上線方案中持續找最好」(運營優化、素材擇優)。
範例:用 Bandit 做策略選擇#
# 每個策略當作一個 arm
strategies = ['strategy_A', 'strategy_B', 'strategy_C']
bandit = ThompsonSampling(len(strategies))
def get_strategy(user):
arm_idx = bandit.select_arm()
return strategies[arm_idx]
def record_feedback(strategy, success):
arm_idx = strategies.index(strategy)
bandit.update(arm_idx, 1 if success else 0)總結#
| 要點 | 說明 |
|---|---|
| 核心問題 | 探索與利用的平衡 |
| 時間區間 | 剩餘時間越長,越該探索 |
| 後悔(Regret) | 探索代價的量化,最佳策略對數成長 |
| Gittins 指數 | 未知選項天生有價值,探索紅利消退慢 |
| ε-Greedy | 簡單但盲目探索 |
| Thompson Sampling | 貝葉斯採樣、實務首選 |
| UCB | 樂觀面對不確定性、自動平衡 E&E |
| LinUCB | 加入上下文,個人化的 Bandit |
| COFIBA | Bandit + CF,緩解新用戶冷啟動 |
| 應用場景 | 冷啟動、多策略 / 多素材擇優、廣告 |
| 與 A/B 的差異 | A/B 重決策,Bandit 重持續優化 |
下一章:排序模型