【李宏毅】强化学习笔记(一)
什么是强化学习
强化学习(Reinforcement Learning)就是一个Agent从环境中观察来得到输入,然后根据输入来做相应的动作,再根据该动作的有害无害来决定奖励或惩罚。
监督学习 VS. 强化学习
AlphaGo
-
监督学习:就是从看到某个围棋布局就落到相应的位置,看到另外一个就落到那里。但当某个布局人都不知道落在那里的时候,这就有不适用了。说白了就是老师叫你看到这样的盘式下在这个位置,看到另外一个下在哪里。也许机器可以学出一个好的Agent,但学不到一个很厉害的Agent(随机应变)。
-
强化学习:机器找一个人与他对弈,若赢了就给予奖励(Reward),输了就惩罚。有时候可能要下上万盘围棋才变得厉害,所以一般是两个Agent相互对弈。可认为是监督学习+强化学习。
Chat-bot
-
监督学习:就告诉机器一句话,然后机器就对应说啥话,比如告诉机器“Hello”,你就要说“Hi”。
在这里插入图片描述 -
强化学习:让机器不断的跟人讲话,讲着讲着人就生气了,这是机器就知道自己某句话将的不对,但没有人告诉他哪句话讲的不好,然后机器就要想办法去挖掘自己哪里讲的不好。
我们就让两个机器互相讲话
通过这种方法,我们就生成了许多的对话,但是机器一开始并不知道哪些对话是没问题的。所以我们就要先预定义一些规则来评估机器的对话是否是好的对话
Outline
1. Policy-based Approach:Learning an Actor/Policy
1) Actor= π \pi π(Observation)
该方法类似于Machine Learning 中的找到一个函数,Actor就是一个函数,Observation是Actor函数的input,而Action是函数Actor的Output。
2) The three steps for DL
我们知道深度学习的三个步骤是定义函数,然后评价函数的好坏,最后是选出最好的函数
如果我们的actor是neural network,那我们就是在做DRL。
Step1: Neural network as Actor
找到这个Actor有三个步骤,如果你的Actor是Neural Network,那么你就在做deep reinforcement learning。
- Input of NN: the observation of machine represented as a vector or a matrix,例如在玩下图的游戏中,图像可作为神经网络的输入
- Output of NN: each action corresponds to a neuron in output layer
- NN与表格型学习的方法相比在于,表格型方法不能表示无限多个状态,而我们的NN是给我一个任意的状态(如游戏的画面)就可以输出动作的分布,所以NN是generalization的。
\\[30pt]
Step2: Goodness of Actor
我们怎么评价一个NN的好坏呢,在监督学习中,我们输入一个样本到NN中,将输出标签和输入的样本计算一个loss,若二者很像则loss越小,所以这个NN就越好。因此我们需要找到一组参数(即NN的权值向量)使得loss最小。
那么在强化学习中我们怎么定义一个NN好坏呢?使用总的
R
θ
R_{\theta}
Rθ的期望值来评估。
-
给定一个actor π θ ( s ) \pi_{\theta}(s) πθ(s),其中 θ \theta θ为该actor的参数。
-
采用 π θ ( s ) \pi_{\theta}(s) πθ(s)玩一次游戏得到一幕数据,即 s 1 , a 1 , r 1 , s 2 , a 2 , r 2 , . . . , s T , a T , r T s_1,a_1,r_1,s_2,a_2,r_2,...,s_T,a_T,r_T s1,a1,r1,s2,a2,r2,...,sT,aT,rT。我们将所有的reward加起来得到总的回报 R θ = ∑ t = 1 T r t R_{\theta}=\sum_{t=1}^T{r_t} Rθ=∑t=1Trt, 但是即使是每次的Actor都是相同的,每次的 R θ \ R_{\theta} Rθ也不一样。有两个原因:1)首先actor(策略)本身就是随机的,当输入相同的状态时可能会采取不同的动作。2)环境的随机性,即采取相同的动作,由于状态不一样所以结果就会不一样。由此可见 R θ R_{\theta} Rθ也是随机的,因此我们用它的期望值来评估,我们希望该期望值越大越好。
-
那么我们怎样计算 R θ R_{\theta} Rθ的期望值 R ˉ θ \bar{R}_\theta Rˉθ呢?
假如我们采用该actor生成了一幕数据为 τ = s 1 , a 1 , r 1 , s 2 , a 2 , r 2 , . . . , s T , a T , r T \tau={s_1,a_1,r_1,s_2,a_2,r_2,...,s_T,a_T,r_T} τ=s1,a1,r1,s2,a2,r2,...,sT,aT,rT,计算回报为 R ( τ ) = ∑ n = 1 N r n R(\tau)=\sum_{n=1}^N r_n R(τ)=∑n=1Nrn。当玩很多很多幕游戏后,有可能玩的这些多幕的游戏没有列举完所有的幕游戏,但某些幕游戏容易出现,而有些幕游戏则不容易出现,比如我们的这个actor比较智障,它看到子弹会移过去自杀,所以这个actor玩的多次幕游戏自杀的幕游戏占多数。因此当用actor玩游戏时得到的每个 τ \tau τ都有一个出现的几率 P ( τ ∣ θ ) P(\tau|\theta) P(τ∣θ),这个概率取决于参数 θ \theta θ。
这时候我们就可以定义 R ˉ θ = ∑ τ R ( τ ) P ( τ ∣ θ ) \bar R_\theta=\sum_\tau R(\tau) P(\tau|\theta) Rˉθ=∑τR(τ)P(τ∣θ),然而我们不可能统计所有可能的幕游戏,所以可以玩N次游戏来近似他,即 R ˉ θ ≈ 1 N ∑ n = 1 N R ( τ n ) \bar{R}_\theta \approx \frac{1}{N} \sum_{n=1}^N R(\tau^n) Rˉθ≈N1∑n=1NR(τn)。
\\[30pt]
Step2: Pick best Actor: 最大化期望值 R ˉ θ \bar{R}_{\theta} Rˉθ,采用Gadient Ascent
-
找到一个使 R ˉ θ \bar{R}_{\theta} Rˉθ达到最大的 θ ∗ \theta^* θ∗,那么应该怎么做呢?首先随机初始化一个 θ 0 \theta^0 θ0,然后使用初始的actor的情况下计算 R ˉ θ 0 对 θ 0 \bar R_{\theta^0} 对\theta^0 Rˉθ0对θ0的微分 ∇ R ˉ θ 0 \nabla \bar R_{\theta^0} ∇Rˉθ0,最后用该微分得到更新的参数 θ 1 \theta^1 θ1,重复以上过程。假如我们的参数 θ \theta θ可表示为 θ = { w 1 , w 2 , . . . , b 1 , . . . } \theta=\{w_1,w_2,...,b_1,...\} θ={w1,w2,...,b1,...},则 R ˉ θ 对 θ \bar R_{\theta} 对\theta Rˉθ对θ的微分 ∇ R ˉ θ \nabla \bar R_{\theta} ∇Rˉθ为:
∇ R ˉ θ = [ ∂ R ˉ θ ∂ w 1 ∂ R ˉ θ ∂ w 2 ⋮ ∂ R ˉ θ ∂ b 1 ⋮ ] \nabla \bar R_{\theta}= \begin{bmatrix} \frac{\partial \bar R_\theta}{\partial w_1} \\ \frac{\partial \bar R_\theta}{\partial w_2} \\ \vdots \\ \frac{\partial \bar R_\theta}{\partial b_1} \\ \vdots \end{bmatrix} ∇Rˉθ=⎣⎢⎢⎢⎢⎢⎢⎢⎡∂w1∂Rˉθ∂w2∂Rˉθ⋮∂b1∂Rˉθ⋮⎦⎥⎥⎥⎥⎥⎥⎥⎤
-
要对 R ˉ θ \bar{R}_{\theta} Rˉθ求导数,只需对 P ( τ ∣ θ ) P(\tau|\theta) P(τ∣θ)求导,而 R ( τ ) R(\tau) R(τ)是依赖于环境的,因此与 θ \theta θ无关。
-
下面说明怎么计算 ∇ l o g P ( τ ∣ θ ) \nabla log P(\tau|\theta) ∇logP(τ∣θ),首先计算 P ( τ ∣ θ ) P(\tau|\theta) P(τ∣θ),计算过程如下。其中 p ( s 1 ) 和 p ( r t , s t + 1 ∣ s t , a t ) p(s_1)和p(r_t,s_{t+1}|s_t,a_t) p(s1)和p(rt,st+1∣st,at)和我们的actor是没有关系的,因为它是取决于环境,因此只有 p ( a t ∣ s t , θ ) p(a_t|s_t,\theta) p(at∣st,θ)(表示在 s t s_t st处采取动作 a t a_t at的概率)与策略 π θ \pi_\theta πθ有关。
对
P
(
τ
∣
θ
)
P(\tau|\theta)
P(τ∣θ)取对数得到:
l
o
g
P
(
τ
∣
θ
)
=
l
o
g
p
(
s
1
)
+
∑
t
=
1
T
[
l
o
g
p
(
a
t
∣
s
t
,
θ
)
+
l
o
g
p
(
r
t
,
s
t
+
1
∣
s
t
,
a
t
)
]
logP(\tau|\theta)=logp(s_1)+\sum_{t=1}^T \left[logp(a_t|s_t,\theta)+logp(r_t,s_{t+1}|s_t,a_t) \right]
logP(τ∣θ)=logp(s1)+t=1∑T[logp(at∣st,θ)+logp(rt,st+1∣st,at)]
再对
l
o
g
P
(
τ
∣
θ
)
logP(\tau|\theta)
logP(τ∣θ)对
θ
\theta
θ求微分得到:
∇
l
o
g
P
(
τ
∣
θ
)
=
∑
t
=
1
T
∇
l
o
g
p
(
a
t
∣
s
t
,
θ
)
\nabla logP(\tau|\theta)=\sum_{t=1}^T \nabla logp(a_t|s_t,\theta)
∇logP(τ∣θ)=t=1∑T∇logp(at∣st,θ)
因此我们可以得到
∇
R
ˉ
θ
\nabla \bar R_\theta
∇Rˉθ(下图中红色矩形所示)。注意我们这里是使用每幕的总收益
R
(
τ
n
)
R(\tau^n)
R(τn),不能用单步的收益
r
t
n
r_t^n
rtn,这会导致只学习单步的动作。
这里需要说明的是,我们为什么要除以
p
(
a
t
n
∣
s
t
n
,
θ
)
p(a_t^n|s_t^n,\theta)
p(atn∣stn,θ)这一项呢?我们现在采样了多幕数据,假如状态s只出现在幕
τ
13
,
τ
15
,
τ
17
,
τ
33
\tau^{13},\tau^{15},\tau^{17},\tau^{33}
τ13,τ15,τ17,τ33中,在
τ
13
\tau^{13}
τ13中,状态s处采取动作a得到总的收益为
R
(
τ
13
)
=
2
R(\tau^{13})=2
R(τ13)=2。然而在
τ
15
,
τ
17
,
τ
33
\tau^{15},\tau^{17},\tau^{33}
τ15,τ17,τ33中,状态s处采取动作b得到的总收益都为1。当做更新的时候,会更偏好于出现次数更多的那些动作,因为我们是对所有幕的所有时间步求和,但这些动作显然并不是那么好(比如在这个例子中)。因此这里除以出现几率比较高的动作,这个操作类似于标准化。
2. Tip 1: Add a baseline
这里有个问题,假如我们的
R
(
τ
n
)
R(\tau^n)
R(τn)总是正的,在理想状态下这是没有问题的,假如在状态s下采取a,b,c三个动作,计算得出a和c的
R
(
τ
n
)
R(\tau^n)
R(τn)比较大,b的
τ
n
\tau^n
τn比较小,那在做完更新之后,选择a和c的概率会增加的多一点,选择b的概率会增加的少一点,虽然概率都会增加,但总体来看b是减少了。但是在实作的时候我们是通过采样幕数据来做更新的,假如现在只采样到动作b和c,而a没有被采样到。因此采样到的动作的几率会增加,未被采样到的动作相对就减小了。
因此我们在
R
(
τ
n
)
R(\tau^n)
R(τn)的基础上减去一个值
b
b
b使得它有正有负,如果我的
R
(
τ
n
)
R(\tau^n)
R(τn)非常好,它好过b,则我们就增加它的概率。若小于b,则减小它的概率。这样就可以避免某个动作没被采样到,使得它的几率变小了。
对于b值的取法,我们可以设置
b
=
E
[
R
(
τ
)
]
b=E[R(\tau)]
b=E[R(τ)],即取多幕数据的平均回报。
3. Tip 2: Assign suitable credit
- 在原来的
∇
R
ˉ
θ
\nabla \bar R_\theta
∇Rˉθ中我们使用的是每幕的总收益
R
(
τ
n
)
R(\tau^n)
R(τn),但这里做一个替换操作,使用每一个时间步的回报,即
R
(
τ
t
n
)
=
∑
t
′
=
t
T
n
r
t
′
n
R(\tau_t^n)=\sum_{t^\prime=t}^{T_n} r_{t^\prime}^n
R(τtn)=∑t′=tTnrt′n。
- 再进一步,我们使用折扣回报
∑
t
′
=
t
T
n
γ
t
′
−
t
r
t
′
n
\sum_{t^\prime=t}^{T_n} \gamma^{t^\prime-t}r_{t^\prime}^n
∑t′=tTnγt′−trt′n,其中
γ
<
1
\gamma<1
γ<1是折扣因子。使用
A
θ
(
s
t
,
a
t
)
A_\theta(s_t,a_t)
Aθ(st,at)来表示
R
(
τ
n
)
−
b
R(\tau^n)-b
R(τn)−b,它取决于参数
θ
\theta
θ。
4. Policy Gradient
- 给定随机初始的actor参数 θ \theta θ,然后拿这个初始的 θ \theta θ去玩N次游戏后,我们就搜集到N个幕游戏,即 τ 1 , τ 2 , . . . , τ N \tau^1, \tau^2,...,\tau^N τ1,τ2,...,τN。
- 根据搜集到的N次数据来更新 θ \theta θ,然后根据新的 θ \theta θ再玩N次游戏,由于是不同的 θ \theta θ因此搜集到的数据不同了。如下图这样循环往复地更新 θ \theta θ,使得 R ˉ ( θ ) \bar{R}(\theta) Rˉ(θ)达到最大。
-
关于上图中右边蓝色框中的 ∇ R ˉ θ \nabla \bar R_\theta ∇Rˉθ,你可能对它的求解不太清楚,这里我们知道 R ( τ n ) R(\tau^n) R(τn)如何计算,但 ∇ l o g p θ ( a t n ∣ s t n ) \nabla logp_\theta(a_t^n|s_t^n) ∇logpθ(atn∣stn)如何得到有些困惑。现在假定我们需要处理如下图的一个分类问题,输入一个状态画面,输出三个动作 y i , i = 1 , 2 , 3 y_i,i=1,2,3 yi,i=1,2,3,这里我们是提前知道该状态画面的标签 y ^ i , i = 1 , 2 , 3 \hat y_i,i=1,2,3 y^i,i=1,2,3,我们的目标为最小化 − ∑ i = 1 3 y ^ i l o g y i -\sum_{i=1}^3 \hat y_i log y_i −∑i=13y^ilogyi,对于下面的这个例子来说,实际上是最大化 l o g y 1 = l o g P ( " l e f t " ∣ s ) logy_1=log P("left"|s) logy1=logP("left"∣s),基于此更新参数 θ \theta θ,即 θ ← θ + η ∇ l o g P ( " l e f t " ∣ s ) \theta \leftarrow \theta+\eta \nabla log P("left"|s) θ←θ+η∇logP("left"∣s)
-
回到最初的时候,我们随机初始actor的参数 θ \theta θ,然后拿 θ \theta θ去玩N次游戏后,我们就搜集到N个幕游戏,即 τ 1 , τ 2 , . . . , τ n , . . . , τ N \tau^1, \tau^2,...,\tau^n,...,\tau^N τ1,τ2,...,τn,...,τN。先不考虑 R ( τ n ) R(\tau^n) R(τn)(下图中用蓝色矩形覆盖),举两个例子:1)在 τ 1 \tau^1 τ1中,假如 a 1 1 = l e f t a_1^1=left a11=left(玩游戏得到的真实标签),我们把 s 1 1 s_1^1 s11输入到NN中得到三个动作的输出值(NN预测的分类结果),然后根据二者更新NN使得NN越接近动作"left"越好。2)同理,我们希望NN越接近"fire"越好。
-
这里我们看到强化学习中的NN跟以前的NN做的事情是一样的,但不一样的地方是更新公式里面多了一个 R ( τ n ) R(\tau^n) R(τn),即在每个时间步前面都乘上了这个时间步对应幕的总收益。那这样做的目的是什么呢?假如现在 R ( τ 1 ) = 2 , R ( τ 2 ) = 1 R(\tau^1)=2,R(\tau^2)=1 R(τ1)=2,R(τ2)=1, s 1 1 s_1^1 s11输入到NN得到输出结果,与真实标签做训练,相当于这样的事情做了两次训练。而对于第二幕数据来说,每个样本做一次训练,因为 R ( τ 2 ) = 1 R(\tau^2)=1 R(τ2)=1。
具体算法流程,这种算法叫做 REINFORCE 算法 (Williams, 1992),这也是最早期使用策略梯度思想的算法,其中
π
θ
(
a
t
∣
s
t
)
\pi_\theta(a_t|s_t)
πθ(at∣st)与以上的
p
θ
(
a
t
∣
s
t
)
p_\theta(a_t|s_t)
pθ(at∣st)等同。
\\[30pt]
5. PG解决Cartpole-v0
本节以tensorflow给出的强化学习算法示例代码为例子,看看PG应该如何实现。
1)CarPole-v0环境
通过以下代码将CarPole-v0环境渲染出来:
import gym
env = gym.make('CartPole-v0')
env.reset()
for _ in range(1000):
env.render()
env.step(env.action_space.sample()) # take a random action
env.close()
显示效果如下:
黑色方块为小车,杆子的一端连接到小车上,由于重力影响杆子会倾斜,当杆子倾斜一定的角度后会倒下。该游戏的任务是控制小车左右移动使得杆子立起来,立起来的时间越长收益越多,即收益与杆子立起来的时间成正比,一旦杆子倒下,游戏结束。
- CarPole-v0的状态由四个值构成:
序号 | 环境信息 | 最小值 | 最大值 |
---|---|---|---|
1 | 小车的位置 | − 1.4 -1.4 −1.4 | 2.4 2.4 2.4 |
2 | 小车的速度 | − ∞ -\infty −∞ | + ∞ +\infty +∞ |
3 | 杆子的倾斜角度 | − 41. 8 ∘ -41.8^\circ −41.8∘ | 41. 8 ∘ 41.8^\circ 41.8∘ |
4 | 杆子顶端的移动速度 | − ∞ -\infty −∞ | + ∞ +\infty +∞ |
- 动作为0和1,分别表示向左和向右移动一步。
2)PG更新框架
for i_episode in range(num_episodes):
observation = env.reset()
for step in range(200):
# 注意:这里没有用贪婪算法,而是根据pi随机动作,以保证一定的探索性。
action = RL.choose_action(observation)
observation_, reward, done, info = env.step(action)
total_reward += reward
# 保存数据
RL.store_transition(observation, action, reward)
# PG用的是MC,如果到了最终状态
if done:
vt = RL.learn()
break
# 开始新一步
observation = observation_
下面重点介绍更新框架中的选择动作函数RL.choose_action(),存储数据函数RL.store_transition(),学习函数RL.learn()。
- RL.choose_action()
第二行:输入状态s,输出动作值
第三行:将输出值使用softmax函数作用,结果为所有动作的概率分布
第四行:根据概率分布随机选择动作
def choose_action(self, s):
_logits = self.model(np.array([s], np.float32))
_probs = tf.nn.softmax(_logits).numpy()
return tl.rein.choice_action_by_probs(_probs.ravel()) #根据策略PI选择动作。
- RL.store_transition()
将当前状态s,采取的动作a,以及得到收益r分别存储在三个列表中。
def store_transition(self, s, a, r):
self.ep_obs.append(np.array([s], np.float32))
self.ep_as.append(a)
self.ep_rs.append(r)
- RL.learn()
def learn(self):
discounted_ep_rs_norm = self._discount_and_norm_rewards() #1
with tf.GradientTape() as tape:
_logits = self.model(np.vstack(self.ep_obs))
neg_log_prob = tf.nn.sparse_softmax_cross_entropy_with_logits \
(logits=_logits, labels=np.array(self.ep_as)) #2
loss = tf.reduce_mean(neg_log_prob * discounted_ep_rs_norm) #3
grad = tape.gradient(loss, self.model.trainable_weights) #4
self.optimizer.apply_gradients(zip(grad, self.model.trainable_weights))
self.ep_obs, self.ep_as, self.ep_rs = [], [], [] # empty episode data
return discounted_ep_rs_norm
#1:计算每个时间步的累积折扣回报和标准化
- for循环的作用是计算每个时间步的累积折扣回报,计算公式为: G t = R t + 1 + γ R t + 2 + . . . = ∑ k = 0 ∞ γ k R t + k + 1 G_t=R_{t+1}+\gamma R_{t+2}+...=\sum_{k=0}^\infty \gamma^k R_{t+k+1} Gt=Rt+1+γRt+2+...=∑k=0∞γkRt+k+1,这样做的目的请参考本文第3节(Tip 2 : Assign suitable credit)。
- 对所有时间步的累积折扣回报进行标准化,即减均值再除以标准差,这样使得G值有正有负,这样做的目的请参考本文第2节(Tip 1: Add a baseline)。
def _discount_and_norm_rewards(self):
discounted_ep_rs = np.zeros_like(self.ep_rs)
running_add = 0
# 从ep_rs的最后往前,逐个计算G
for t in reversed(range(0, len(self.ep_rs))):
running_add = running_add * self.gamma + self.ep_rs[t]
discounted_ep_rs[t] = running_add
# 我们希望G值有正有负,这样比较容易学习。
discounted_ep_rs -= np.mean(discounted_ep_rs)
discounted_ep_rs /= np.std(discounted_ep_rs)
return discounted_ep_rs
#2:计算NN后输出(动作的概率分布)和真实标签之间的cross entropy。
- _logits为所有状态输入NN后的输出,注意这里的输出没有通过softmax函数作用。但tf.nn.sparse_softmax_cross_entropy_with_logits()函数会将_logits用softmax函数作用。
- labels为真实的分类标签,但tf.nn.sparse_softmax_cross_entropy_with_logits()函数会自动将类别转化成one-hot码。
_logits = self.model(np.vstack(self.ep_obs))
neg_log_prob = tf.nn.sparse_softmax_cross_entropy_with_logits \
(logits=_logits, labels=np.array(self.ep_as)) #2
1 N ∑ n = 1 N ∑ t = 1 T n A ( s t , a t ) ∇ l o g p ( a t n ∣ s t n , θ ) A ( s t , a t ) = ∑ t ′ = t T n γ t ′ − t r t ′ \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_n} A(s_t, a_t)\nabla log p(a_t^n|s_t^n, \theta)\\ A(s_t, a_t) = \sum_{t^\prime=t}^{T_n} \gamma^{t^\prime - t} r_{t^\prime} N1n=1∑Nt=1∑TnA(st,at)∇logp(atn∣stn,θ)A(st,at)=t′=t∑Tnγt′−trt′
- 举个例子,假如_logits = [[-0.00724776, 0.12655543], [-0.06601118, 0.12850013]],即它是两个状态输入NN的输出结果。labels = [0, 1],它是这两个状态下采取的动作,第一个状态采取动作0,第二个状态采取动作1,这是actor与环境互动后所采样得到的真实数据。
三个步骤:
1)将_logits使用softmax函数作用,结果为:[[0.46659902, 0.53340098], [0.45152491, 0.54847509] ]。
2)将labels转化成one-hot码,结果为:[[1, 0], [0, 1]]。
3)计算cross entropy(具体过程请单击此处),结果为:[-1 * log(0.46659902) - 0 * log(0.53340098) , -0 * log(0.45152491) - 1 * log(0.54847509)] = [-1 * log(0.46659902), -1 * log(0.54847509)] = [0.76228502, 0.60061342]。这一步正好与PG中求解 log p ( a t n ∣ s t n , θ ) \log p(a_t^n|s_t^n, \theta) logp(atn∣stn,θ)相对应。
tf.nn.sparse_softmax_cross_entropy_with_logits()的作用就是在完成以上三个步骤。
#3 - #4: 计算梯度
loss = tf.reduce_mean(neg_log_prob * discounted_ep_rs_norm) #3
grad = tape.gradient(loss, self.model.trainable_weights) #4