- 强化学习与其他机器学习方法最大的不同,就在于前者的训练信号是用来评估给定动作的好坏的,而不是通过给出正确动作范例来进行直接的指导。
一个 k k k臂赌博机问题
- k k k个动作中的每一个在被选择时都有一个期望或者平均收益,称为这个动作的价值。将在时刻 t t t时选择的动作记作 A t A_t At,并将对应收益记作 R t R_t Rt。任一动作 a a a对应的价值,记作 q ∗ ( a ) q_*(a) q∗(a),是给定动作 a a a时收益期望:
q ∗ ( a ) = E [ R t ∣ A t = a ] q_*(a) = E[R_t|A_t=a] q∗(a)=E[Rt∣At=a]
我们将对动作 a a a在时刻t时的价值的估计记作 Q t ( a ) Q_t(a) Qt(a),我们希望它接近 q ∗ ( a ) q_*(a) q∗(a)。
- 当你从这些动作中选择时,我们称为开发当前你所知道的关于动作的价值的知识。选择非贪心的动作,称为试探,因为这可以让你改善对非贪心动作的价值的估计
- 到底选择“试探”还是“开发”一种复杂的方式依赖与我们得到的函数估计、不确定性和剩余时刻的精确数值。
- 开发和试探的平衡是强化学习中的一个问题。
动作-价值方法
- 使用价值的估计来进行动作选择,这一类方法被统称为“动作-价值方法”。
- 一种自然的方式就是通过计算实际收益的平均值来估计动作的价值:
Q t ( a ) = t 时刻前通过执行动作 a 得到的收益总和 t 时刻前执行动作 a 的次数 = ∑ i = 1 t − 1 R i ⋅ I A i = a ∑ i = 1 t − 1 I A i = a Q_t(a) = \frac{t时刻前通过执行动作a得到的收益总和}{t时刻前执行动作a的次数} = \frac{\sum_{i = 1}^{t-1}R_i \cdot I_{A_i = a}}{\sum_{i = 1}^{t-1}I_{A_i = a}} Qt(a)=t时刻前执行动作a的次数t时刻前通过执行动作a得到的收益总和=∑i=1t−1IAi=a∑i=1t−1Ri⋅IAi=a
其中, I p r e d i c a t e I_{predicate} Ipredicate表示随机变量,当 p r e d i c a t e predicate predicate为真时其值为1,反之为0。当分母为0时,我们将 Q t ( a ) Q_t(a) Qt(a)定义为某个默认值,比如 Q t ( a ) = 0 Q_t(a) = 0 Qt(a)=0。当分母趋向于无穷大时,根据大数定律, Q t ( a ) Q_t(a) Qt(a)会收敛到 q ∗ ( a ) q_{*}(a) q∗(a)
- 我们将这种估计动作价值的方法称为采样平均方法,因为每一次估计都是相关收益样本的平均。
- 最简单的动作选择规则是选择具有最高估计值的动作,如果有多个贪心动作,那就任意选择一个,比如随机挑选,我们将这种贪心动作的选择方法记作:
A t = arg min a Q t ( a ) A_t = \mathop{\arg\min}\limits_{a}Q_t(a) At=aargminQt(a)
其中, arg min a \mathop{\arg\min}\limits_{a} aargmin是使得 Q t ( a ) Q_t(a) Qt(a)值最大的动作 a a a
- 贪心策略的一个简单代替策略是大部分时间都表现得很贪心,但偶尔(比如以一个很小的概率 ϵ \epsilon ϵ)以独立于动作-价值的估计方式从所有动作中等概率随机地做出选择。我们将使用这种近乎贪心的选择规则方法称为 ϵ \epsilon ϵ-贪心方法。
- ϵ \epsilon ϵ-贪心方法的优点是,如果时刻可以无限长,则每一个动作都会被无限次采样,从而确保所有的 Q t ( a ) Q_t(a) Qt(a)收敛到 q ∗ a q_{*}a q∗a。这意味着选择最优动作的概率会收敛到大于 1 − ϵ 1-\epsilon 1−ϵ。
10臂测试平台
- 从长远来看,贪心方法表现明显很糟糕,因为它经常陷入执行次优动作的怪圈。 ϵ \epsilon ϵ-贪心方法最终表现更好,因为他们持续地试探并且提升找到最优动作的机会。
- ϵ = 0.1 \epsilon = 0.1 ϵ=0.1相较于 ϵ = 0.01 \epsilon = 0.01 ϵ=0.01方法试探得更多,通常更早发现最优的动作,但是在每时刻选择这个最优动作的概率却永远不会超过91%。
- ϵ \epsilon ϵ-贪心方法相对于贪心方法的优点依赖于任务。比如,假设收益的方差更大,不是1而是10,由于收益的噪声更多,所以为了找到最优的动作需要更多次的试探, ϵ \epsilon ϵ-贪心方法会更有优势。
- 如果收益方差为0,那么贪心方法会在尝试一次之后就知道每一个动作的真实价值。在这种情况下,贪心方法实际上可能表现最好。因为它很开就会找到最佳的动作,然后再也不会进行试探。
增量式实现
- 至今我们讨论的动作-价值方法都把动作价值作为观测到的收益的样本均值来估计。
- 令 R i R_i Ri表示这一动作被选择 i i i次后获得的收益, Q n Q_{n} Qn表示被选择 n − 1 n-1 n−1次后它的估计的动作价值,可以简写为:
Q n = R 1 + R 2 + ⋯ + R n − 1 n − 1 Q_n = \frac{R_1 + R_2 + \cdots + R_{n-1}}{n-1} Qn=n−1R1+R2+⋯+Rn−1
- 为了计算每个新的收益,很容易设计增量式公式以小而恒定的计算来更新平均值。给定 Q n Q_n Qn和第 n n n次的收益 R n R_{n} Rn,所有 n n n个收益的新均值:
Q n + 1 = 1 n ∑ i = 1 n R i = 1 n ( R n + ∑ i = 1 n − 1 R i ) = 1 n ( R n + ( n − 1 ) 1 n − 1 ∑ i = 1 n − 1 R i ) = 1 n ( R n + ( n − 1 ) Q n ) = 1 n ( R n + n Q n − Q n ) = Q n + 1 n [ R n − Q n ] \begin{align} Q_{n+1} &= \frac{1}{n}\sum^{n}_{i = 1}R_i\\ &=\frac{1}{n}(R_n+\sum_{i=1}^{n-1}R_i)\\ &=\frac{1}{n}(R_n+(n-1)\frac{1}{n-1}\sum_{i=1}^{n-1}R_i)\\ &=\frac{1}{n}(R_n+(n-1)Q_n)\\ &=\frac{1}{n}(R_n+nQ_n-Q_n)\\ &=Q_n+\frac{1}{n}[R_{n}-Q_{n}]\\ \end{align} Qn+1=n1i=1∑nRi=n1(Rn+i=1∑n−1Ri)=n1(Rn+(n−1)n−11i=1∑n−1Ri)=n1(Rn+(n−1)Qn)=n1(Rn+nQn−Qn)=Qn+n1[Rn−Qn]
- 更新公式的一般形式为:
新估计值 ← 旧估计值 + 步长 × [ 目标 − 旧估计值 ] 新估计值 \leftarrow 旧估计值 + 步长 \times [目标-旧估计值] 新估计值←旧估计值+步长×[目标−旧估计值]
表达式 [ 目标 − 旧估计值 ] [目标-旧估计值] [目标−旧估计值]是估计值的误差。误差会随着向“目标”靠近的每一步而减小。虽然“目标中可能充满噪声”,但我们还是假定“目标”会告诉我们可行的前进方向。
- 增量式方法中的“步长”会随着时间而变化。处理动作 a a a对应的第 n n n个收益的方法用的步长是 1 n \frac{1}{n} n1。
跟踪一个非平稳问题
- 取平均方法对平稳的赌博机问题是合适的,即收益的概率分布不随着时间变化。但如果赌博机的收益概率是随着时间变化的该方法就不合适。
- 给近期的收益赋予比过去很久的收益更高的权值就是一种合理的处理方式,最流行的方法之一是使用固定步长。比如,用于更新 n − 1 n-1 n−1个过去的收益均值Q_{n}的增量更新规则可以改为:
Q n + 1 = Q n + α [ R n − Q n ] Q_{n+1} = Q_{n} + \alpha[R_n - Q_n] Qn+1=Qn+α[Rn−Qn]
- 随机逼近理论中的一个著名结果给出了保证收敛概率为1所需的条件:
∑ n = 1 ∞ α n ( a ) = ∞ 且 ∑ n = 1 ∞ a n 2 ( a ) < ∞ \sum_{n = 1}^{\infty}\alpha_n(a) = \infty 且 \sum_{n=1}^{\infty}a_n^{2}(a)<\infty n=1∑∞αn(a)=∞且n=1∑∞an2(a)<∞
第一个条件是要求保证有足够大的步长,最终客服任何初始条件或随机波动。第二个条件保证最终步长变小,以保证收敛。
练习2.5
testbed.py
import numpy as np
from numpy.random import normal as GaussianDistribution
class K_armed_testbed():
def __init__(self, k_actions):
self.k = k_actions
self.action_values = np.full(self.k, fill_value=0.0)
def random_walk_action_values(self):
increment = GaussianDistribution(loc=0, scale=0.01, size=self.k)
self.action_values += increment
def sample_action(self, action_i):
return GaussianDistribution(loc=self.action_values[action_i], scale=1, size=1)[0]
def get_optimal_action(self):
return np.argmax(self.action_values)
def get_optimal_action_value(self):
return self.action_values[self.get_optimal_action()]
def is_optimal_action(self, action_i):
return float(self.get_optimal_action_value() == self.action_values[action_i])
def __str__(self):
return "\t".join(["A%d: %.2f" % (action_i, self.action_values[action_i]) for action_i in range(self.k)])
estimators.py
import numpy as np
class Estimator(object):
def __init__(self, action_value_initial_estimates):
self.action_value_estimates = action_value_initial_estimates
self.k_actions = len(action_value_initial_estimates)
self.action_selected_count = np.full(self.k_actions, fill_value=0, dtype="int64")
def select_action(self):
raise NotImplementedError("Need to implement a method to select actions")
def update_estimates(self):
raise NotImplementedError("Need to implement a method to update action value estimates")
def select_greedy_action(self):
return np.argmax(self.action_value_estimates)
def select_action_randomly(self):
return np.random.choice(self.k_actions)
class SampleAverageEstimator(Estimator):
def __init__(self, action_value_initial_estimates, epsilon):
super(SampleAverageEstimator, self).__init__(action_value_initial_estimates)
self.epsilon = epsilon
def update_estimates(self, action_selected, r):
self.action_selected_count[action_selected] += 1
qn = self.action_value_estimates[action_selected]
n = self.action_selected_count[action_selected]
self.action_value_estimates[action_selected] = qn + (1.0 / n) * (r - qn)
def select_action(self):
probability = np.random.rand()
if probability >= self.epsilon:
return self.select_greedy_action()
return self.select_action_randomly()
class WeightedEstimator(SampleAverageEstimator):
def __init__(self, action_value_initial_estimates, epsilon=0, alpha=0.5):
super(WeightedEstimator, self).__init__(action_value_initial_estimates, epsilon)
self.alpha = alpha
def update_estimates(self, action_selected, r):
qn = self.action_value_estimates[action_selected]
self.action_value_estimates[action_selected] = qn + self.alpha * (r - qn)
Exercise 2.5.py
import numpy as np
import matplotlib.pyplot as plt
from tqdm import tqdm
from estimators import SampleAverageEstimator, WeightedEstimator
from testbed import K_armed_testbed
np.random.seed(250)
def plot_performance(estimator_names, rewards, action_optimality):
for i, estimator_name in enumerate(estimator_names):
average_run_rewards = np.average(rewards[i], axis=0)
plt.plot(average_run_rewards, label=estimator_name)
plt.legend()
plt.xlabel("Steps")
plt.ylabel("Average reward")
plt.show()
for i, estimator_name in enumerate(estimator_names):
average_run_optimality = np.average(action_optimality[i], axis=0)
plt.plot(average_run_optimality, label=estimator_name)
plt.legend()
plt.xlabel("Steps")
plt.ylabel("% Optimal action")
plt.show()
if __name__ == "__main__":
K = 10
N_STEPS = 10000
N_RUNS = 2000
N_ESTIMATORS = 2
rewards = np.full((N_ESTIMATORS, N_RUNS, N_STEPS), fill_value=0.)
optimal_selections = np.full((N_ESTIMATORS, N_RUNS, N_STEPS), fill_value=0.)
for run_i in tqdm(range(N_RUNS)):
testbed = K_armed_testbed(k_actions=K)
action_value_estimates = np.full(K, fill_value=0.0)
sample_average_estimator = SampleAverageEstimator(action_value_estimates.copy(), epsilon=0.1)
weighted_estimator = WeightedEstimator(action_value_estimates.copy(), epsilon=0.1, alpha=0.1)
estimators = [sample_average_estimator, weighted_estimator]
for step_i in range(N_STEPS):
for estimator_i, estimator in enumerate(estimators):
action_selected = estimator.select_action()
is_optimal = testbed.is_optimal_action(action_selected)
reward = testbed.sample_action(action_selected)
estimator.update_estimates(action_selected, reward)
rewards[estimator_i][run_i][step_i] = reward
optimal_selections[estimator_i][run_i][step_i] = is_optimal
testbed.random_walk_action_values()
plot_performance(["Ɛ=0.1", "Ɛ=0.1 α=0.1"], np.array(rewards), np.array(optimal_selections))
乐观初始值
- 目前为止我们讨论的所有方法都在一定程度上依赖于初始动作值 Q 1 ( a ) Q_1(a) Q1(a)的选择。
- 初始动作的价值提供了一种简单的试探方式。比如一个10臂测试平台,我们将初始值全部设为+5,因为 q ∗ ( a ) q_{*}(a) q∗(a)是按照均值为0方差为1的正态分布选择的。因此无论哪一种动作被选择,收益都比最开始的估计值要小;因此学习器会对得到的收益感到“失望”,从而转向另一个动作。
- 我们把这种鼓励试探的技术叫作乐观初始价值,在平稳问题中非常有效,但它远非鼓励试探的普遍有用方法。例如它不太适合非平稳问题,因为它试探的驱动力天生是暂时的。
基于置信度上界的动作选择
- ϵ \epsilon ϵ-贪心方法会尝试选择非贪心的动作,但是这是一种盲目的选择,因为它不大会去选择接近贪心或者不确定性特别大的动作。
- 在非贪心动作中,最好是根据它们的潜力来选择可能事实上是最优的动作,在就要考虑到它们的估计有多接近最大值,以及这些估计的不确定性。
- 一个有效的方法是按照以下公式选择动作:
A t ≐ arg min a [ Q t ( a ) + c l n t N t ( a ) ] A_t \doteq \mathop{\arg\min}\limits_{a}\left[Q_t(a) + c\sqrt{\frac{ln\ t}{N_t(a)}}\right] At≐aargmin[Qt(a)+cNt(a)ln t ]
其中 l n t ln\ t ln t表示 t t t的自然对数, N t ( a ) N_t(a) Nt(a)表示在时刻 t t t之前动作 a a a被选择的次数。 c c c是一个大于0的数,它控制试探的程度。如果 N t ( a ) = 0 N_t(a) = 0 Nt(a)=0,则 a a a就被任务是满足最大化条件的动作。
- 这种基于置信度上界的动作选择的思想是,平方根项是对 a a a动作值估计的不确定性或方差的度量。
- UCB(置信度上界)算法比较难处理非平稳问题,另一方面难处理打的状态空间。
梯度赌博机算法
- 在本节中,我们针对每个动作 a a a考虑学习一个数值化的偏好函数 H t ( a ) H_t(a) Ht(a)。
- 偏好函数越大,动作就越频繁地被选择,但偏好函数的概率并不是从“收益”的意义上提出的。只有一个动作对另一个动作的相对偏好才是重要的。
- 如果我们给每一个动作的偏好函数都加上1000,那么对于按照softmax分布确定的动作概率没有任何影响:
P r { A t = a } ≐ e H t ( a ) ∑ b = 1 k e H t ( a ) ≐ π t ( a ) Pr\{A_t = a\}\doteq\frac{e^{H_t(a)}}{\sum_{b=1}^{k}e^{H_t(a)}}\doteq\pi_t(a) Pr{At=a}≐∑b=1keHt(a)eHt(a)≐πt(a)
其中, π t ( a ) \pi_t(a) πt(a)是一个新的且重要的定义,用来表示动作 a a a在时刻 t t t时被选择的概率。所有偏好函数的初始值都是一样的,所以每个动作被选择的概率是相同的。
- 基于梯度上升思想,提出一种自然学习算法。在每个步骤中,在选择动作 A t A_t At并获得 R t R_t Rt之后,偏好函数将会按如下方式更新:
H t + 1 ( A t ) ≐ H t ( A t ) + α ( R t − R ‾ t ) ( 1 − π t ( A t ) ) , 以及 H t + 1 ( a ) ≐ H t ( a ) − α ( R t − R ‾ t ) π t ( a ) , 对所有 a ≠ A t H_{t+1}(A_t) \doteq H_t(A_t)+\alpha(R_t-\overline{R}_{t})(1-\pi_t(A_t)),\ \ 以及\\ H_{t+1}(a) \doteq H_t(a) - \alpha(R_t - \overline{R}_t)\pi_t(a) ,\ \ 对所有a\neq A_t Ht+1(At)≐Ht(At)+α(Rt−Rt)(1−πt(At)), 以及Ht+1(a)≐Ht(a)−α(Rt−Rt)πt(a), 对所有a=At
其中, α \alpha α是一个大于0的数,表示步长。 R ‾ t \overline{R}_t Rt项作为比较收益的一个基准项。如果收益高于它,那么在未来选择动作 A t A_t At的概率就会增加,反之概率就会降低。未选择的动作被选择的概率上升。
- 对非关联任务,当任务是平稳的时候,学习器会试图寻找一个最佳的动作;当任务是非平稳的时候,最佳动作会随着时间的变化而改变,此时它会试着去追踪最佳动作。
- 在一般的强化学习任务中,往往有不止一种情境,它们的目标是学习一种策略:一个从特定情境到最优动作的映射。
本章小节
- ϵ \epsilon ϵ-贪心方法在一小段时间内进行随机的动作选择。
- 而UCB方法虽然采用确定的动作选择,却可以通过在每个时刻对那些具有较少样本的动作进行优先选择来实现试探。
- 梯度赌博机算法则不估计动作价值,而上利用偏好函数,使用softmax分布来以一种分级的、概率式的方式选择更优的动作。
- 贝叶斯方法假定已知动作价值的初始分布,然后在每步之后更新分布(假定真实的动作价值是平稳的)