阐述、总结【动手学强化学习】章节内容的学习情况,复现并理解代码。
文章目录
一、算法背景
1.1 算法目标
给定“黑盒”模型,求解最优policy
1.2 存在问题
动态规划、时序差分-TD、值函数近似算法中策略的表示都是基于价值(value-based)的,即算法目标是学习值函数,然后根据值函数导出一个策略,学习过程中并不存在一个显式的策略。
1.3 解决方法
采用基于策略(policy-based)算法,直接显式地学习一个目标策略。
可以理解为,之前policy的表示是“表格式(tabular)”的,即存在一个“
π
−
t
a
b
l
e
:
π
(
a
∣
s
)
{\pi}-table:{\pi}(a|s)
π−table:π(a∣s) ”,现在通过“函数拟合”去学习一个关于policy的函数,即
π
θ
(
a
∣
s
)
{\pi}_θ(a|s)
πθ(a∣s),参数为θ。
二、REINFORCE算法
- 🌟算法类型
环境依赖:❌model-based ✅model-free
价值估计:✅non-incremental ❌incremental(采用蒙特卡洛方法估计)
价值表征:✅tabular representation ❌function representation(采用蒙特卡洛方法估计)
学习方式:✅on-policy ❌off-policy
策略表征:❌value-based ✅policy-based(通过函数拟合去计算 π θ ( a ∣ s ) {\pi}_θ(a|s) πθ(a∣s) 概率)
2.1 必要说明
softmax()函数
softmax函数是一种常用的激活函数,主要用于将一个向量转换为概率分布,使得向量中的每个元素都被映射到 [0, 1] 区间内,并且所有元素的和为 1。
对于一个向量
z
=
[
z
1
,
z
2
,
…
,
z
n
]
\mathbf{z}=[z_1,z_2,\ldots,z_n]
z=[z1,z2,…,zn] ,softmax函数的定义为:
s
o
f
t
m
a
x
(
z
)
i
=
e
z
i
∑
j
=
1
n
e
z
j
\mathrm{softmax}(\mathbf{z})_i=\frac{e^{z_i}}{\sum_{j=1}^ne^{z_j}}
softmax(z)i=∑j=1nezjezi
- 特点
①归一化:softmax 函数将向量的每个元素转换为一个介于 0 和 1 之间的值,并且所有元素的和为 1。这使得输出可以被解释为概率分布。
** 在这里就体现为在state状态下与环境交互时,action的使用概率,即 π θ ( a ∣ s ) {\pi}_θ(a|s) πθ(a∣s)
②强调最大值:softmax 函数会放大较大的值并抑制较小的值。这意味着在多分类问题中,模型更倾向于选择具有最高输出值的类别。
** 在这里就体现为最优policy中action的选择。
③平滑性:softmax 函数是平滑的,这意味着它在所有点上都是可导的,这在训练神经网络时非常有用,因为可以通过梯度下降等优化算法来更新模型参数。
交叉熵
当采用softmax函数作为神经网络的输出时,一般采用交叉熵来衡量两个概率的区别,即真值与神经网络输出概率的区别,因此交叉熵可以作为神经网络的损失函数。参考【动手学深度学习】part9-softmax回归
- 交叉熵定义为:
假设模型的输出为 z,真实标签为 y,则整个过程可以表示为:
p = s o f t m a x ( z ) L = H ( y , p ) = − ∑ i = 1 n y i log ( p i ) \begin{aligned}\mathbf{p}&=\mathrm{softmax}(\mathbf{z})\\\\L=H(\mathbf{y},\mathbf{p})&=-\sum_{i=1}^ny_i\log(p_i)\end{aligned} pL=H(y,p)=softmax(z)=−i=1∑nyilog(pi)
策略更新思想
将policy_net的目标函数定义为最大化state value,即:
J
(
θ
)
=
E
[
∑
t
=
0
∞
γ
t
R
t
+
1
]
=
E
s
0
[
V
π
θ
(
s
0
)
]
J(\theta)=\mathbb{E}\left[\sum_{t=0}^\infty\gamma^tR_{t+1}\right]=\mathbb{E}_{s_0}[V^{\pi_\theta}(s_0)]
J(θ)=E[t=0∑∞γtRt+1]=Es0[Vπθ(s0)]
V
π
θ
(
s
0
)
=
∑
a
π
(
a
∣
s
,
θ
)
q
π
(
s
,
a
)
V^{\pi_\theta}(s_0)=\sum_a\pi(a|s,\theta)q_\pi(s,a)
Vπθ(s0)=a∑π(a∣s,θ)qπ(s,a)
为了求解最优policy使得state value最优,借鉴“梯度下降”(参考:【算法学习】优化算法-小批量随机梯度下降mini-batch SGD)的思想,求解
J
(
θ
)
J(\theta)
J(θ)的梯度,即:
∇
θ
J
=
∑
s
d
(
s
)
∑
a
∇
θ
π
(
a
∣
s
,
θ
)
q
π
(
s
,
a
)
=
∑
s
d
(
s
)
∑
a
π
(
a
∣
s
,
θ
)
∇
θ
ln
π
(
a
∣
s
,
θ
)
q
π
(
s
,
a
)
=
E
S
∼
d
[
∑
a
π
(
a
∣
S
,
θ
)
∇
θ
ln
π
(
a
∣
S
,
θ
)
q
π
(
S
,
a
)
]
=
E
S
∼
d
,
A
∼
π
[
∇
θ
ln
π
(
A
∣
S
,
θ
)
q
π
(
S
,
A
)
]
≐
E
[
∇
θ
ln
π
(
A
∣
S
,
θ
)
q
π
(
S
,
A
)
]
\begin{aligned} \nabla_{\theta}J& =\sum_sd(s)\sum_a\nabla_\theta\pi(a|s,\theta)q_\pi(s,a) \\ &=\sum_{s}d(s)\sum_{a}\pi(a|s,\theta)\nabla_{\theta}\ln\pi(a|s,\theta)q_{\pi}(s,a) \\ &=\mathbb{E}_{S\sim d}\left[\sum_{a}\pi(a|S,\theta)\nabla_{\theta}\ln\pi(a|S,\theta)q_{\pi}(S,a)\right] \\ &=\mathbb{E}_{S\sim d,A\sim\pi}\big[\nabla_\theta\ln\pi(A|S,\theta)q_\pi(S,A)\big] \\ &\doteq\mathbb{E}\big[\nabla_\theta\ln\pi(A|S,\theta)q_\pi(S,A)\big] \end{aligned}
∇θJ=s∑d(s)a∑∇θπ(a∣s,θ)qπ(s,a)=s∑d(s)a∑π(a∣s,θ)∇θlnπ(a∣s,θ)qπ(s,a)=ES∼d[a∑π(a∣S,θ)∇θlnπ(a∣S,θ)qπ(S,a)]=ES∼d,A∼π[∇θlnπ(A∣S,θ)qπ(S,A)]≐E[∇θlnπ(A∣S,θ)qπ(S,A)]
因此,policy_net的参数更新可以设置为:
θ
t
+
1
=
θ
t
+
α
∇
θ
J
(
θ
)
=
θ
t
+
α
E
[
∇
θ
ln
π
(
A
∣
S
,
θ
t
)
q
π
(
S
,
A
)
]
\begin{aligned} \theta_{t+1}& =\theta_t+\alpha\nabla_\theta J(\theta) \\ &=\theta_t+\alpha\mathbb{E}\Big[\nabla_\theta\ln\pi(A|S,\theta_t)q_\pi(S,A)\Big] \end{aligned}
θt+1=θt+α∇θJ(θ)=θt+αE[∇θlnπ(A∣S,θt)qπ(S,A)]
采用“随机近似(stochastic approximation)”去估计梯度,即:
θ
t
+
1
=
θ
t
+
α
∇
θ
ln
π
(
a
t
∣
s
t
,
θ
t
)
q
π
(
s
t
,
a
t
)
\theta_{t+1}=\theta_t+\alpha\nabla_\theta\ln\pi(a_t|s_t,\theta_t)q_\pi(s_t,a_t)
θt+1=θt+α∇θlnπ(at∣st,θt)qπ(st,at)
2.2 伪代码
算法流程简述
①初始化policy_net:构建policy_net网络模型。
②采样episode:设定周期数num_episodes,循环迭代采样episode,根据
π
θ
(
a
∣
s
)
{\pi}_θ(a|s)
πθ(a∣s) 采样(s,a,r,s’,done)直至terminal state,即done为true,得到一条episode,即(s,a,r,s’)链。
③价值更新:依据蒙特卡洛方法,基于episode采用first-visit更新对应q(s,a)值。
④策略更新:设定损失函数为交叉熵,基于“梯度下降”方法更新policy_net网络参数。
⑤终止判断:根据已产生episode个数是否达到num_episodes,判断算法是否终止,并输出policy。
2.3 算法代码
import gym
import torch
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt
from tqdm import tqdm
import rl_utils
class PolicyNet(torch.nn.Module):
def __init__(self, state_dim, hidden_dim, action_dim):
super(PolicyNet, self).__init__()
self.fc1 = torch.nn.Linear(state_dim, hidden_dim)
self.fc2 = torch.nn.Linear(hidden_dim, action_dim)
def forward(self, x):
x = F.relu(self.fc1(x))
return F.softmax(self.fc2(x), dim=1)
class REINFORCE:
def __init__(self, state_dim, hidden_dim, action_dim, learning_rate, gamma,
device):
self.policy_net = PolicyNet(state_dim, hidden_dim,
action_dim).to(device)
self.optimizer = torch.optim.Adam(self.policy_net.parameters(),
lr=learning_rate) # 使用Adam优化器
self.gamma = gamma # 折扣因子
self.device = device
def take_action(self, state): # 🌟根据动作概率分布随机采样,不再是根据Q值去选取action了
state = torch.tensor([state], dtype=torch.float).to(self.device)
probs = self.policy_net(state)
action_dist = torch.distributions.Categorical(probs)
action = action_dist.sample()
return action.item()
def update(self, transition_dict):
reward_list = transition_dict['rewards']
state_list = transition_dict['states']
action_list = transition_dict['actions']
G = 0
self.optimizer.zero_grad()
for i in reversed(range(len(reward_list))): # 从最后一步算起
reward = reward_list[i]
state = torch.tensor([state_list[i]],
dtype=torch.float).to(self.device)
action = torch.tensor([action_list[i]]).view(-1, 1).to(self.device)
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() # 梯度下降,更新policy_net参数
learning_rate = 1e-3
num_episodes = 1000
hidden_dim = 128
gamma = 0.98
device = torch.device("cuda") if torch.cuda.is_available() else torch.device(
"cpu")
env_name = "CartPole-v0"
env = gym.make(env_name)
env.seed(0)
torch.manual_seed(0)
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.n
agent = REINFORCE(state_dim, hidden_dim, action_dim, learning_rate, gamma,
device)
return_list = []
for i in range(10):
with tqdm(total=int(num_episodes / 10), desc='Iteration %d' % i) as pbar:
for i_episode in range(int(num_episodes / 10)):
episode_return = 0
transition_dict = {
'states': [],
'actions': [],
'next_states': [],
'rewards': [],
'dones': []
}
state = env.reset()
done = False
while not done:
action = agent.take_action(state)
next_state, reward, done, _ = env.step(action)
transition_dict['states'].append(state)
transition_dict['actions'].append(action)
transition_dict['next_states'].append(next_state)
transition_dict['rewards'].append(reward)
transition_dict['dones'].append(done)
state = next_state
episode_return += reward
return_list.append(episode_return)
agent.update(transition_dict)
if (i_episode + 1) % 10 == 0:
pbar.set_postfix({
'episode':
'%d' % (num_episodes / 10 * i + i_episode + 1),
'return':
'%.3f' % np.mean(return_list[-10:])
})
pbar.update(1)
episodes_list = list(range(len(return_list)))
plt.plot(episodes_list, return_list)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('REINFORCE on {}'.format(env_name))
plt.show()
mv_return = rl_utils.moving_average(return_list, 9)
plt.plot(episodes_list, mv_return)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('REINFORCE on {}'.format(env_name))
plt.show()
2.4 运行结果
Iteration 0: 100%|██████████| 100/100 [00:09<00:00, 10.01it/s, episode=100, return=28.200]
Iteration 1: 100%|██████████| 100/100 [00:09<00:00, 10.21it/s, episode=200, return=78.300]
Iteration 2: 100%|██████████| 100/100 [00:14<00:00, 6.77it/s, episode=300, return=146.000]
Iteration 3: 100%|██████████| 100/100 [00:18<00:00, 5.30it/s, episode=400, return=160.100]
Iteration 4: 100%|██████████| 100/100 [00:24<00:00, 4.02it/s, episode=500, return=199.400]
Iteration 5: 100%|██████████| 100/100 [00:25<00:00, 3.91it/s, episode=600, return=200.000]
Iteration 6: 100%|██████████| 100/100 [00:25<00:00, 3.96it/s, episode=700, return=183.700]
Iteration 7: 100%|██████████| 100/100 [00:26<00:00, 3.79it/s, episode=800, return=187.000]
Iteration 8: 100%|██████████| 100/100 [00:23<00:00, 4.22it/s, episode=900, return=193.200]
Iteration 9: 100%|██████████| 100/100 [00:25<00:00, 3.88it/s, episode=1000, return=180.700]
- 结果分析
对比DQN算法,得到的episode_return波动更大,但能获取到最优的性能,得益于蒙特卡洛方法,如:
Iteration 5: 100%|██████████| 100/100 [00:25<00:00, 3.91it/s, episode=600, return=200.000]
(carpole的最佳奖励分数为200)
2.5 算法流程说明
初始化参数
learning_rate = 1e-3
num_episodes = 1000
hidden_dim = 128
gamma = 0.98
device = torch.device("cuda") if torch.cuda.is_available() else torch.device(
"cpu")
初始化“车杆”环境
env_name = "CartPole-v0"
env = gym.make(env_name)
env.seed(0)
torch.manual_seed(0)
初始化policy_net网络模型
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.n
agent = REINFORCE(state_dim, hidden_dim, action_dim, learning_rate, gamma,
device)
...
class PolicyNet(torch.nn.Module):
def __init__(self, state_dim, hidden_dim, action_dim):
super(PolicyNet, self).__init__()
self.fc1 = torch.nn.Linear(state_dim, hidden_dim)
self.fc2 = torch.nn.Linear(hidden_dim, action_dim)
def forward(self, x):
x = F.relu(self.fc1(x))
return F.softmax(self.fc2(x), dim=1)
设置为4-128-2的全连接网络,输入为state=4维,输出为action的选取概率=2维,网络模型可表示为:
y
=
s
o
f
t
m
a
x
(
f
c
2
(
r
e
l
u
(
f
c
1
(
x
)
)
)
)
,
x
=
s
t
a
t
e
s
y=softmax\left(fc_2\left(relu\bigl(fc_1(x)\bigr)\right)\right),x=states
y=softmax(fc2(relu(fc1(x)))),x=states
episode采样
for i_episode in range(int(num_episodes / 10)):
episode_return = 0
transition_dict = {
'states': [],
'actions': [],
'next_states': [],
'rewards': [],
'dones': []
}
state = env.reset()
done = False
while not done: #采样单个episode直至terminal state,即done=true
action = agent.take_action(state)
next_state, reward, done, _ = env.step(action)
transition_dict['states'].append(state)
transition_dict['actions'].append(action)
transition_dict['next_states'].append(next_state)
transition_dict['rewards'].append(reward)
transition_dict['dones'].append(done)
state = next_state
episode_return += reward
return_list.append(episode_return)
...
def take_action(self, state): # 🌟根据动作概率分布随机采样,不再是根据Q值去选取action了
state = torch.tensor([state], dtype=torch.float).to(self.device)
probs = self.policy_net(state)
action_dist = torch.distributions.Categorical(probs)
action = action_dist.sample()
return action.item()
①获取初始state:state = env.reset()
②根据policy_net的输出采取action:agent.take_action(state)
③与环境交互,得到(s,a,r,s’,done):env.step(action)
④将样本添加至episode:transition_dict
⑤统计episode即时奖励累加值:episode_return += reward
价值更新
agent.update(transition_dict) #训练
...
def update(self, transition_dict):
reward_list = transition_dict['rewards']
state_list = transition_dict['states']
action_list = transition_dict['actions']
G = 0
self.optimizer.zero_grad()
for i in reversed(range(len(reward_list))): # 从最后一步算起
reward = reward_list[i]
state = torch.tensor([state_list[i]],
dtype=torch.float).to(self.device)
action = torch.tensor([action_list[i]]).view(-1, 1).to(self.device)
log_prob = torch.log(self.policy_net(state).gather(1, action))
G = self.gamma * G + reward
采样完一个episode后,倒序累加discounted reward:G,以🌟fist-visit方式来估计多个q(s,a),这里的G就用来估计q(s,a),以实现:
reversed(range(len(reward_list)))+G = self.gamma * G + reward
q
(
s
t
,
a
t
)
=
∑
k
=
t
+
1
T
γ
k
−
t
−
1
r
k
q({s_{t},a_{t}})=\sum_{k=t+1}^{T}\gamma^{k-t-1}r_{k}
q(st,at)=k=t+1∑Tγk−t−1rk
策略更新
agent.update(transition_dict) #训练
...
self.optimizer.zero_grad()
...
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() # 梯度下降,更新policy_net参数
计算交叉熵损失函数:loss = -log_prob * G,对应上交叉熵的定义:
L
=
H
(
y
,
p
)
=
−
∑
i
=
1
n
y
i
log
(
p
i
)
L=H(\mathbf{y},\mathbf{p})=-\sum_{i=1}^ny_i\log(p_i)
L=H(y,p)=−i=1∑nyilog(pi)
其中,G对应“真值”,log_prob 对应网络模型输出概率的对数。这里的G等同于episode在初始的env.state后采取action获得的累计折扣奖励,等同于就是在估计q(s,a)。
随后采用Adam优化器进行:梯度清零+反向传播+参数更新
三、疑问
3.1 为什么要采用softmax作为policy_net的输出?
要保证 π θ ( a ∣ s ) {\pi}_θ(a|s) πθ(a∣s) 中每个action的概率为正数,并且概率在(0,1)之间取值。
3.2 为什么REINFORCE算法比DQN算法波动更大?
借助蒙特卡洛方法采样轨迹来估计动作价值,这种做法的一大优点是可以得到无偏的梯度。但是,正是因为使用了蒙特卡洛方法, REINFORCE 算法的梯度估计的方差很大,可能会造成一定程度上的不稳定
3.3 策略更新的步骤具体在算法哪一块?
算法示例中没有显式地去计算policy_net的梯度,而是在Adam optimizer中进行的梯度计算,具体代码为反向传播的这一步:loss.backward() # 反向传播计算梯度
在策略更新中,采用梯度下降的方式去更新policy_net的参数,推导出:
θ
t
+
1
=
θ
t
+
α
∇
θ
ln
π
(
a
t
∣
s
t
,
θ
t
)
q
π
(
s
t
,
a
t
)
\theta_{t+1}=\theta_t+\alpha\nabla_\theta\ln\pi(a_t|s_t,\theta_t)q_\pi(s_t,a_t)
θt+1=θt+α∇θlnπ(at∣st,θt)qπ(st,at)
在设置损失函数时,由于神经网络采用了softmax函数作为输出,因此设定交叉熵为损失函数。那么就引出一个问题:在多分类问题中,交叉熵中的真值
y
i
y_i
yi 表示的是真实分类的标签,而在REINFORCE算法中构建的policy_net网络输出为action的概率,没有对应的真值,即在特定的state下应该采取的action。
于是根据“梯度下降”中policy_net参数的更新方式,选取
q
π
(
s
t
,
a
t
)
q_\pi(s_t,a_t)
qπ(st,at)为指导,即在计算交叉熵时,将
y
i
y_i
yi设定为
q
π
(
s
t
,
a
t
)
q_\pi(s_t,a_t)
qπ(st,at)。
四、总结
- REINFORCE算法采用policy_net网络去显示地表征policy,采用蒙特卡洛的方法去估计q(s,a),智能体根据当前策略直接和环境交互,通过采样得到的轨迹数据直接计算出策略参数的梯度,进而更新当前策略,使其向最大化策略期望回报的目标靠近。