1. 策略梯度
这是在看策略梯度算法时产生的一个困惑,假如我们有一个策略
θ
\theta
θ,通过这个策略可以跟环境交互,产生一系列的样本
τ
=
τ
1
,
τ
2
.
.
.
τ
N
\tau={\tau_{1},\tau_{2}...\tau_{N}}
τ=τ1,τ2...τN,这个策略得到的分数为
R
θ
‾
=
∑
τ
R
(
τ
)
p
θ
(
τ
)
=
E
τ
∼
p
θ
[
R
(
τ
)
]
\begin{aligned} \overline{R_\theta}&=\sum_{\tau}R(\tau)p_\theta(\tau)\\ &=E_{\tau \sim p_\theta}[R(\tau)] \end{aligned}
Rθ=τ∑R(τ)pθ(τ)=Eτ∼pθ[R(τ)]
强化学习的核心目标是求取价值函数的最大值,因此有
θ
∗
=
a
r
g
m
a
x
R
θ
‾
\theta^{*}=argmax\overline{R_\theta}
θ∗=argmaxRθ
这个是不可以直接求解的,所以通常我们使用梯度下降法进行策略优化
θ
k
+
1
=
θ
k
+
α
∇
R
θ
‾
\theta_{k+1}=\theta_{k}+\alpha \nabla \overline{R_\theta}
θk+1=θk+α∇Rθ
策略一般非常的复杂,所以我们使用一个神经网络来代替这个策略,损失函数为 R θ ‾ \overline{R_\theta} Rθ,我们将损失函数丢入到神经网络中,使用tf或者torch可以自动帮助我们计算 θ \theta θ,将损失函数进行简化之后得到了一个新的梯度
∇ R θ ‾ = 1 N ∑ n = 1 N ∑ t = 1 T R ( τ ) ∇ l o g p θ ( a t n ∣ s t n ) \nabla \overline{R_{\theta}}=\frac{1}{N}\sum_{n=1}^{N}\sum_{t=1}^{T} R(\tau)\nabla{logp_{\theta}(a_{t}^{n}|s_{t}^{n})} ∇Rθ=N1n=1∑Nt=1∑TR(τ)∇logpθ(atn∣stn)
看到这个梯度的时候我是非常懵逼的,主要有两点
- 一般情况下,我们会定义一个损失函数,然后将这个损失函数丢到神经网络中就可以训练了,这里为什么不去定义损失函数,而是去计算这个梯度呢,梯度不是神经网络自动帮我们计算的吗
- 这个梯度即使计算出来之后,难道要我们手动去进行更新吗,如果是简单的线性函数还可以,复杂的神经网络该怎么去更新呢?
其实这个问题非常的简单,只是我自己把这个问题给想复杂了。举一个例子,假如有一个损失函数
L
θ
=
f
θ
2
(
x
)
L_\theta=f_{\theta}^{2}(x)
Lθ=fθ2(x)
我们该如何求解
θ
\theta
θ,一种非常简单的方法就是直接使用
L
θ
L_\theta
Lθ,丢入到神经网络中会自动帮我们计算梯度,并且更新参数,不过我们可以先做优化,化简一下梯度
∇
L
=
∇
f
θ
2
(
x
)
=
2
∇
f
θ
(
x
)
\begin{aligned} \nabla{L}&=\nabla{f_{\theta}^{2}(x)}\\ &=2\nabla{f_{\theta}(x)} \end{aligned}
∇L=∇fθ2(x)=2∇fθ(x)
你会发现原本求 L θ L_\theta Lθ的梯度,现在变成了求 f θ ( x ) f_{\theta}(x) fθ(x)的梯度了,怎么求 L θ L_\theta Lθ的梯度呢?直接把 L θ L_\theta Lθ丢到神经网络中,怎么求 f θ ( x ) f_{\theta}(x) fθ(x)的梯度呢?直接把 f θ ( x ) f_{\theta}(x) fθ(x)丢到神经网络中就行了,这其实就相当于说我们把损失函数给改变了。梯度符号后面是什么函数,那么我们的损失函数就是什么样子的,只要把梯度后的函数放到神经网络中,就可以自动求梯度了,不用纠结时 L L L还是 f f f,谁在梯度后面,谁就是损失函数。 这两种形式其实得到的结果是一样的。
pytoch中的梯度
之后我们看了策略梯度的代码
optimizer.zero_grad()
for i in reversed(range(len(reward_list))): # 从最后一步算起
reward = reward_list[i]
state = torch.tensor([state_list[i]], dtype=torch.float)
action = torch.tensor([action_list[i]]).view(-1, 1)
log_prob = torch.log(self.policy_net(state).gather(1, action))
G = self.gamma * G + reward
loss = - log_prob * G # 每一步的损失函数
loss.backward() # 反向传播计算梯度
self.optimizer.step() # 梯度下降
其中有两个地方非常的奇怪,
- 首先是为什么在pytorch中需要对梯度进行清除,在tensorflow中从未见过如此奇怪的函数。
- 为什么每一个steps都会有一个
loss.backward()
,这是pytoch计算需要,还是策略梯度算法需要?
1. SGD->BGD
google了一圈之后,算是找到了一些思路。首先看一下一般的使用方式
for idx, (batch_x, batch_y) in enumerate(data_loader): # 获取每个批次的样本
output = model(batch_x) # 计算这些样本的预测结果,神经网络的输出
loss = criterion(output, batch_y) #计算损失函数
optimizer.zero_grad() # 梯度清零
loss.backward() # 计算梯度
optimizer.step() # 更新参数
- 调用
optimizer.zero_grad()
会将梯度清零 ∇ = 0 \nabla =0 ∇=0 - 调用
loss.backward()
时pytoch会帮助我们自动计算每个参数的梯度 ∇ = ∂ L ∂ w \nabla =\frac{\partial{L}}{\partial{w}} ∇=∂w∂L,并且会将这个梯度存储下来,如果没有对梯度清零,pytorch会自动将这些梯度累加的。 - 调用
optimizer.step()
会将参数进行更新 w = w − α ∂ L ∂ w w=w-\alpha \frac{\partial{L}}{\partial{w}} w=w−α∂w∂L。
首先看一下正常的SGD是输入一个样本就会进行一次参数更新
θ
k
+
1
=
θ
k
−
a
k
g
(
x
k
)
\theta_{k+1}=\theta_{k} - a_kg(x_k)
θk+1=θk−akg(xk)
当我们不对梯度清零时,梯度会进行累加,参数更新变为
θ
k
+
1
=
θ
k
−
a
k
∑
i
=
1
N
g
(
x
i
)
\theta_{k+1}=\theta_{k} - a_k\sum_{i=1}^{N} g(x_i)
θk+1=θk−aki=1∑Ng(xi)
看一下BGD的参数更新,BGD是输入一批样本之后再去更新参数
θ
k
+
1
=
θ
k
−
a
k
1
N
∑
i
=
1
N
g
(
x
i
)
\theta_{k+1}=\theta_{k} - a_k\frac{1}{N}\sum_{i=1}^{N} g(x_i)
θk+1=θk−akN1i=1∑Ng(xi)
两个式子放在一起对比一下
{ θ k + 1 = θ k − a k ∑ i = 1 N g ( x i ) S G D 梯 度 未 清 零 θ k + 1 = θ k − a k N ∑ i = 1 N g ( x i ) B G D \left\{ \begin{aligned} \theta_{k+1}&=\theta_{k} - a_k\sum_{i=1}^{N} g(x_i) & & SGD梯度未清零\\ \theta_{k+1}&=\theta_{k} - \frac{a_k}{N}\sum_{i=1}^{N} g(x_i) & & BGD\\ \end{aligned} \right. ⎩⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎧θk+1θk+1=θk−aki=1∑Ng(xi)=θk−Naki=1∑Ng(xi)SGD梯度未清零BGD
发现一个非常有趣的现象,不清梯度的SGD和BGD竟然是一样的,如果我们不对梯度清零,就相当于是在使用BGD,只是在更新参数的时候需要缩小
1
N
\frac{1}{N}
N1倍,好像没什么问题。
去百度的时候发现很多人抄了抄去,说什么梯度不清零就相当于样本放大了N倍,参数更新时还需要缩小N倍,真的完全不知道在说什么,当把公式列出来的时候,就很清晰了。但是这么做有什么好处呢,既然是等价的,为什么pytoch不自动去做呢?
举一个有趣的例子,当我们资源很小模型很大的时候,该如何训练?直接使用BGD更新会导致资源不够,使用SGD收敛速度会变慢,这个时候我们就可以使用pytorch进行骚操作了,我们每次输入一个样本
for idx, (x, y) in enumerate(data_loader): # 输出一个样本
output = model(x) # 计算神经网络输出
loss = criterion(output, y) # 得到损失函数
loss = loss / batch_size # loss进行
loss.backward() # 求梯度
if (idx % batch_size) == 0:
optimizer.step() # 累积batch_size样本后的梯度,我们去做更新
optimizer.zero_grad() # 再对梯度清零
我们依然使用神经网络去计算输出,每次都去计算梯度,但是不对梯度清零,这样梯度就会累加到一起,相当于我们使用了BGD。