在前几章的算法中,求解最优策略都是试图估计最优价值函数,这些算法称为最优价值算法(optimal value algorithm)。本章开始介绍试图用含参函数近似最优策略,并通过迭代更新参数值,这类算法称为策略梯度算法(optimal gradient algorithm)。
用函数近似方法估计最优策略 的基本思想是用含参函数 来近似最优策略,由于任意策略都需要满足对于任意的状态 ,均有 ,为此引入动作偏好函数(action preference function) ,其 softmax 的值为 ,即:
动作偏好函数可以具有线性组合、人工神经网络等多种形式,其参数 通常使用基于梯度的迭代算法更新,所以动作偏好函数往往需要对参数 可导,另外还需要知道期望回报对参数 的梯度,这样就能沿着梯度方向更新 而使得期望回报增大;而策略梯度定理(policy gradient theorem)给出了期望回报和策略梯度之间的关系,是策略梯度方法的基础。
在回合制任务中,策略梯度定理给出了策略 的期望回报 对策略参数 的梯度为:
证明:对策略 的 Bellman 期望方程分别求梯度,有:
将 代入到 中,并对 求期望,有:
这样就得到了从 到 的递推式,再注意到最终关注的梯度值 ,有:
由式 可直接得到一个策略梯度算法——简单的策略梯度算法(Vanilla Policy Gradient, VPG),其每一步的更新式为:
这样迭代完一个回合轨迹就实现了 。R. Willims 在文章《Simple statistical gradient-following algorithms for connectionist reinforcement learning》中给出该算法,并称为 “REward Increment = Nonnegative Factor Offset Reinforcement Characteristic Eligibility”(REINFORCE),表示增量 是由三个部分的积组成的。当采用自动微分的软件包来学习参数时,可定义单步损失为 ,然后让软件包自动处理,具体算法如下:
回合更新的方法没有用到自益,不会引入偏差,但往往有非常大的方差。为了降低方差,引入基线函数 对简单的策略梯度算法进行改进——带基线的简单的策略梯度算法(REINFORCE with baselines),基线函数可以是任意随机函数或确定函数,他可以与状态 有关,但不能和动作 有关,满足这些条件后,基线函数自然满足:
证明:
基线函数可以任意选择,例如:
但在实际选择基线时,应当参照以下两点:
一个能有效降低方差的基线是状态价值函数的估计,其对应的算法如下:
下面分析一下什么样的基线函数能最大程度地减小方差。考虑 的方差,并对 求偏导有:
假设 ,即两者相互独立,并令上述偏导为 0 ,则有:
这意味着,最佳的基线函数应当接近回报 以 为权重加权平均的结果,但是实际应用中,无法事先知道这个值,所以无法使用这样的基线函数。值得一提的是,当策略参数和价值参数同时需要学习的时候,算法的收敛性需要通过双时间轴 Robbins-Monro 算法(two timescale Robbins-Monro algorithm)来分析。
在简单的策略梯度算法的基础上引入重要性采样,即可得到对应的异策算法。记行为策略为 ,有:
所以采用重要性采样的离线算法,只需要修改更新式 中期望回报的梯度表达式即可,得到的具体算法如下:
重要性采样虽然使得可利用其他策略的样本来更新策略参数,但可能会带来较大的偏差,算法稳定性比同策算法差。
以上算法都是通过更新策略参数 以试图增大形如 的目标(单个条目则为 ),其中 可取 、 等值。从监督学习的角度来看,如果已经有一个表达式未知的策略 ,当要用策略 来近似它时,可以考虑用最大似然的方法来估计策略参数 。具体而言,未知策略 的许多样本对于策略 的对数似然值正比于 ,这时使用这些样本进行有监督学习,则是更新 以增大 (单个条目则为 ),可以看出这里是目标 中取 时得到的,在形式上具有想相似性。事实上,策略梯度算法在学习过程中巧妙地利用观测到的奖励信号决定每步对数似然值 对策略奖励的贡献,为其加权为 ,使得表现好的行为策略更新幅度大,更加倾向于出现;表现很差的行为策略更新幅度很小,更加倾向于不出现;最终使得整个策略 变得越来越好。
使用 Gym 库里的车杆平衡问题(CartPole-v0)作为案例分析。该问题的环境为一个小车(cart)上连着一根杆(pole),目的是控制小车左右移动,使得杆保持直立;环境的观测值、动作值、奖励值、起始状态、回合结束标志在源代码中有描述,此处不再赘述。
在使用书中的同策策略梯度算法求解最优策略的代码时,经多次测试发现该代码的收敛性较差,训练智能体过程的回合奖励值变化大多呈下降趋势,在尝试调节学习率后,最好的情况也是呈现波动趋势,且测试结果的平均奖励值不超过 50 。经调试,造成该结果的可能原因是书中代码直接将 作为输出结果进行训练,而实际上网络输出层的激活函数是 softmax
,输出的应该是 。
具体而言就是,假设以 作为输出结果来训练,由于训练网络的损失函数为交叉熵函数,网络各个输出结果的值域为 ,为了使损失函数降低,训练会使网络的输出结果趋向于 0 或 1 ,即与环境交互得到新样本的 值趋向于 0 或 1 ,这样损失函数才会越来越小;但实际上在该环境中得到的新样本的 值基本上都是大于 1 的,那么以上的训练过程就只会导致 越来越小,且尽可能的接近 1 ,最终导致算法无法收敛或甚至发散。修改前的代码如下:
xxxxxxxxxx
21y = np.eye(self.action_n)[df["action"]] * df["psi"].values[:, np.newaxis]
2self.policy_net.fit(x, y, verbose=0)
解决办法是在训练时,将 作为样本权重, 作为输出结果;即 大的样本,其权重也大,更新幅度越大,这样才符合算法的思想。另外,在每个回合后直接使用整个回合的样本作为 batch
进行训练,其效果有时候会非常好,对应函数为 train_on_batch
,可能是因为这样的更新方式是直接符合更新式 的,而不需要通过更新式 来间接更新参数 。修改后的代码如下:
xxxxxxxxxx
31sample_weight = df["psi"].values[:, np.newaxis]
2y = np.eye(self.action_n)[df["action"]]
3self.policy_net.train_on_batch(x, y, sample_weight=sample_weight)
因为在异策代码中使用的损失函数不同,所以上述不收敛的问题在异策代码中并不存在,但同样也可以使用被修改后的代码进行训练,其思想是一致的。根据书中源代码,将同策异策智能体整合,并对 做标准化处理保证网络训练的稳定性,以及将行为策略指定为随机策略,最终修改后的智能体类代码如下:
xxxxxxxxxx
541class VPG():
2 def __init__(self, env, policy_kwargs, baseline_kwargs=None, gamma=0.99, offpolicy=False):
3 self.action_n = env.action_space.n
4 self.gamma= gamma
5 self.trajectory = []
6
7 if not offpolicy:
8 self.random_behavior = False
9 policy_loss = keras.losses.categorical_crossentropy
10 else:
11 self.random_behavior = True
12 policy_loss = lambda y_true, y_pred: -tf.reduce_sum(y_true * y_pred, axis=-1)
13
14 self.policy_net = self.build_network(output_size=self.action_n, \
15 output_activation=tf.nn.softmax, loss=policy_loss, **policy_kwargs)
16 if baseline_kwargs:
17 self.baseline_net = self.build_network(output_size=1, **baseline_kwargs)
18
19 def build_network(self, hidden_sizes, output_size, activation=tf.nn.relu, \
20 output_activation=None, loss=keras.losses.mse, learning_rate=0.01):
21 model = keras.Sequential()
22 for hidden_size in hidden_sizes:
23 model.add(keras.layers.Dense(units=hidden_size, activation=activation))
24 model.add(keras.layers.Dense(units=output_size, activation=output_activation))
25 model.compile(loss=loss, optimizer=keras.optimizers.Adam(lr=learning_rate))
26 return model
27
28 def choose_action(self, state):
29 probs = self.policy_net.predict(state[np.newaxis])[0]
30 return np.random.choice(self.action_n, p=probs)
31
32 def learn(self, state, action, reward, done):
33 self.trajectory.append((state, action, reward))
34
35 if done:
36 df = pd.DataFrame(self.trajectory, columns=["state", "action", "reward"])
37 df["discount"] = self.gamma ** df.index.to_series()
38 df["psi"] = (df["discount"] * df["reward"])[::-1].cumsum()
39
40 x = np.stack(df["state"])
41 if hasattr(self, "baseline_net"):
42 df["return"] = df["psi"] / df["discount"]
43 y = df["return"].values[:, np.newaxis]
44 self.baseline_net.train_on_batch(x, y)
45 df["baseline"] = self.baseline_net.predict(x)
46 df["psi"] -= (df["discount"] * df["baseline"])
47 if self.random_behavior:
48 df["psi"] *= self.action_n
49
50 df["psi"] = (df["psi"] - df["psi"].mean()) / df["psi"].std()
51 sample_weight = df["psi"].values[:, np.newaxis]
52 y = np.eye(self.action_n)[df["action"]]
53 self.policy_net.train_on_batch(x, y, sample_weight=sample_weight)
54 self.trajectory = []
(由于学习率参数和探索随机性的影响,并不能保证每次运行的指定回合数的训练都能收敛,或者往好的趋势发展;可以尝试多次运行、调节学习率或增加训练回合数,也可以修改代码,在训练后保存模型,然后在下次训练前读取模型并调节学习率继续训练。)