文章目錄
- 前言
- 創建多臂老虎機環境
- 多臂老虎機算法基本框架(基類)
- 1. ε-貪心算法 (Epsilon-Greedy)
- 2. 隨時間衰減的ε-貪婪算法 (Decaying ε-Greedy)
- 3. 上置信界算法 (Upper Confidence Bound, UCB)
- 4. 湯普森采樣算法 (Thompson Sampling)
- 總結
前言
歡迎來到“從代碼學習深度強化學習”系列!在本篇文章中,我們將深入探討一個強化學習中的經典問題——多臂老虎機(Multi-Armed Bandit, MAB)。
多臂老虎機問題,顧名思義,源于一個賭徒在賭場面對一排老虎機(即“多臂老虎機”)的場景。每個老虎機(“臂”)都有其內在的、未知的獲獎概率。賭徒的目標是在有限的回合內,通過選擇拉動不同的老虎機,來最大化自己的總收益。
這看似簡單的場景,卻完美地詮釋了強化學習中的一個核心困境:探索(Exploration)與利用(Exploitation)的權衡。
- 利用(Exploitation):選擇當前已知收益最高的老虎機。這能保證我們在短期內獲得不錯的收益,但可能會錯過一個潛在收益更高但尚未被充分嘗試的選項。
- 探索(Exploration):嘗試那些我們不確定其收益的老虎機。這可能會在短期內犧牲一些收益,但卻有機會發現全局最優的選擇,從而獲得更高的長期總回報。
為了量化算法的性能,我們引入一個重要概念——累積懊悔(Cumulative Regret)。懊悔指的是在某一步選擇的動作所帶來的期望收益與“上帝視角”下最優動作的期望收益之差。一個優秀的算法,其目標就是最小化在整個過程中的累積懊悔。
在本篇博客中,我們將通過 Python 代碼,從零開始實現一個多臂老虎機環境,并逐步實現和對比以下四種經典的求解策略:
- ε-貪心算法 (Epsilon-Greedy)
- 隨時間衰減的ε-貪心算法 (Decaying Epsilon-Greedy)
- 上置信界算法 (Upper Confidence Bound, UCB)
- 湯普森采樣算法 (Thompson Sampling)
關于 PyTorch: 盡管標題提及 PyTorch,但對于 MAB 這種基礎問題,使用 NumPy 能更清晰地展示算法的核心邏輯,而無需引入深度學習框架的復雜性。本文中的實現將基于 NumPy,但其核心思想(如價值估計、策略選擇)是構建更復雜的深度強化學習算法(如DQN)的基石,在那些場景中 PyTorch 將發揮關鍵作用。
讓我們開始吧!
完整代碼:下載鏈接
創建多臂老虎機環境
多臂老虎機問題可以表示為一個元組 ? A , R ? \langle\mathcal{A},\mathcal{R}\rangle ?A,R? ,其中:
- A \mathcal{A} A為動作集合,其中一個動作表示拉動一個拉桿。若多臂老虎機一共有 K K K根拉桿,那動作空間就是集合 { a 1 , … , a K } \{a_1,\ldots,a_K\} {a1?,…,aK?},我們用 a t ∈ A a_t\in\mathcal{A} at?∈A表示任意一個動作;
- R \mathcal{R} R為獎勵概率分布,拉動每一根拉桿的動作 a a a都對應一個獎勵概率分布 R ( r ∣ a ) \mathcal{R}(r|a) R(r∣a),不同拉桿的獎勵分布通常是不同的。
假設每個時間步只能拉動一個拉桿,多臂老虎機的目標為最大化一段時間步 T T T內累積的獎勵: max ? ∑ t = 1 T r t , r t ~ R ( ? ∣ a t ) \max\sum_{t=1}^Tr_t, r_t\sim\mathcal{R}\left(\cdot|a_t\right) max∑t=1T?rt?,rt?~R(?∣at?)。其中 a t a_t at?表示在第 t t t個時間步拉動某一拉桿的動作, r t r_t rt?表示動作 a t a_t at?獲得的獎勵。
首先,我們需要一個模擬環境。我們創建一個 BernoulliBandit
類來模擬一個擁有 K
個臂的老虎機。每個臂都服從伯努利分布,即每次拉動它,會以一個固定的概率 p
獲得獎勵 1
(獲獎),以 1-p
的概率獲得獎勵 0
(未獲獎)。在我們的環境中,這 K
個臂的獲獎概率 p
是在初始化時隨機生成的,并且對我們的算法(智能體)是未知的。
# 導入需要使用的庫
import numpy as np # numpy是支持數組和矩陣運算的科學計算庫
import matplotlib.pyplot as plt # matplotlib是繪圖庫class BernoulliBandit:"""伯努利多臂老虎機類該類實現了一個多臂老虎機問題的環境,每個拉桿都服從伯努利分布"""def __init__(self, K):"""初始化伯努利多臂老虎機參數:K (int): 拉桿個數,標量屬性:probs (numpy.ndarray): 每個拉桿的獲獎概率數組,維度為 (K,)best_idx (int): 獲獎概率最大的拉桿索引,標量best_prob (float): 最大的獲獎概率值,標量K (int): 拉桿總數,標量"""# 隨機生成K個0~1之間的數,作為拉動每根拉桿的獲獎概率# probs: (K,) - K個拉桿的獲獎概率數組self.probs = np.random.uniform(size=K)# 找到獲獎概率最大的拉桿索引# best_idx: 標量 - 最優拉桿的索引號self.best_idx = np.argmax(self.probs)# 獲取最大的獲獎概率# best_prob: 標量 - 最大獲獎概率值self.best_prob = self.probs[self.best_idx]# 保存拉桿總數# K: 標量 - 拉桿個數self.K = Kdef step(self, k):"""執行一次拉桿動作當玩家選擇了k號拉桿后,根據該拉桿的獲獎概率返回獎勵結果參數:k (int): 選擇的拉桿編號,標量,取值范圍為 [0, K-1]返回:int: 獎勵結果,標量1 表示獲獎0 表示未獲獎"""# 根據k號拉桿的獲獎概率進行伯努利采樣# np.random.rand(): 標量 - 生成[0,1)之間的隨機數# self.probs[k]: 標量 - k號拉桿的獲獎概率if np.random.rand() < self.probs[k]:return 1 # 獲獎else:return 0 # 未獲獎# 設定隨機種子,使實驗具有可重復性
np.random.seed(1)# 設置拉桿數量
# K: 標量 - 多臂老虎機的拉桿個數
K = 10# 創建一個10臂伯努利老虎機實例
# bandit_10_arm: BernoulliBandit對象 - 包含10個拉桿的老虎機
bandit_10_arm = BernoulliBandit(K)# 輸出老虎機的基本信息
print("隨機生成了一個%d臂伯努利老虎機" % K)
print("獲獎概率最大的拉桿為%d號,其獲獎概率為%.4f" % (bandit_10_arm.best_idx, bandit_10_arm.best_prob))
運行以上代碼,我們創建了一個10臂老虎機,并打印出了最優拉桿的信息。在我們的實驗中,1號拉桿是收益最高的,其獲獎概率為 0.7203。這個信息算法本身是不知道的,但我們可以用它來計算懊悔。
隨機生成了一個10臂伯努利老虎機
獲獎概率最大的拉桿為1號,其獲獎概率為0.7203
多臂老虎機算法基本框架(基類)
為了方便實現和比較不同的算法,我們先定義一個 Solver
基類。這個基類包含了所有算法都需要共享的功能,例如記錄每個臂被拉動的次數、記錄歷史動作以及計算和更新累積懊悔。具體的決策邏輯(run_one_step
)將由各個子類來實現, 需要求解的是選取某根拉桿的策略。
累積懊悔
對于每一個動作,我們定義其期望獎勵為 Q ( a ) = E r ~ R ( ? ∣ a ) [ r ] {Q}(a)=\mathbb{E}_{r\sim\mathcal{R}(\cdot|a)}\begin{bmatrix}r\end{bmatrix} Q(a)=Er~R(?∣a)?[r?]。于是,至少存在一根拉桿,它的期望獎勵不小于拉動其他任意一根拉桿,我們將該最優期望獎勵表示為 Q ? = max ? a ∈ A Q ( a ) Q^*=\max_{a\in\mathcal{A}}Q(a) Q?=maxa∈A?Q(a)。為了更加直觀、方便地觀察拉動一根拉桿的期望獎勵離最優拉桿期望獎勵的差距,我們引入懊悔(regret)概念。懊悔定義為拉動當前拉桿的動作與最優拉桿的期望獎勵差,即 R ( a ) = Q ? ? Q ( a ) R(a)=Q^*-Q(a) R(a)=Q??Q(a)。累積懊悔(cumulative regret)即操作 T T