极客兔兔

TensorFlow 2.0 (九) - 强化学习 70行代码实战 Policy Gradient

源代码/数据集已上传到 Github - tensorflow-tutorial-samples

Geektutu Policy Gradient Success

这篇文章是 TensorFlow 2.0 Tutorial 入门教程的第九篇文章。

实战策略梯度算法(Policy Gradient),代码70行

CartPole 简介

在之前的文章中,我们使用过纯监督学习的算法,强化学习算法中的Q学习(Q-Learning)和深度Q网络(Deep Q-learning Network, DQN),这一篇文章,我们选择策略梯度算法(Policy Gradient),来玩一玩 CartPole。

先回顾一下CartPole-v0的几个重要概念。

概念 解释 示例
State 状态,[车位置, 车速度, 杆角度, 杆速度] 0.02, 0.95, -0.07, -1.53
Action 动作(0向左/1向右) 1
Reward 奖励(每走一步得1分) 1.0

我们在 TensorFlow 2.0 (八) - 强化学习 DQN 玩转 gym Mountain Car这篇文章中,介绍了基于价值(value-based)的强化学习算法 DQN,在 DQN 中,神经网络的输入是状态,输出是每一个动作的价值。每一次从所有可行的动作中选择Q值最大的执行。我们使用了一个公式来不断地计算期望的Q值,训练神经网络。

那有没有可能,直接输出动作呢?这就是我们今天要介绍的基于策略(policy-based)的策略梯度算法(Policy Gradient)。

本文不涉及数学推导,仅介绍如何高效实现。如对该算法该兴趣,推荐 Medium 上有3.8k点赞的一篇文章An introduction to Policy Gradients

搭建神经网络

Policy Gradient 网络的输入也是状态(State),那输出呢?每个动作的概率。例如 [0.7, 0.3] ,这意味着有70%的几率会选择动作0,30%的几率选择动作1。相对于 Policy Gradient,DQN 的动作更确定,因为 DQN 每次总是选择Q值最大的动作,而Policy Gradient 按照概率选择,会产生更多的不确定性。

废话不多说,神经网络先搭起来吧~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# policy_gradient.py
# https://geektutu.com
import matplotlib.pyplot as plt
import gym
import numpy as np
from tensorflow.keras import models, layers, optimizers

env = gym.make('CartPole-v0')

STATE_DIM, ACTION_DIM = 4, 2
model = models.Sequential([
layers.Dense(100, input_dim=STATE_DIM, activation='relu'),
layers.Dropout(0.1),
layers.Dense(ACTION_DIM, activation="softmax")
])
model.compile(loss='mean_squared_error',
optimizer=optimizers.Adam(0.001))

我们的神经网络很简单,输入层为4,输出层为2,隐藏层为100。不过这次代码多了一个Dropout,Dropout(0.1) 的含义是,随机忘记10%的权重。学习初期,一开始的数据质量不高,随着学习的进行,质量才逐步高了起来,一开始容易陷入局部最优过拟合,使用 Dropout 可以有效避免。

如何选择动作呢?前文已经介绍,按照概率。

1
2
3
4
5
6
# policy_gradient.py
# https://geektutu.com
def choose_action(s):
"""预测动作"""
prob = model.predict(np.array([s]))[0]
return np.random.choice(len(prob), p=prob)

优化策略

接下来是最大的问题,如何优化策略呢?

Policy Gradient Optimize

1) 衰减的累加期望

我们先想象一下,假如你在玩坦克大战,你的每一步都会对后面的局势产生巨大的影响。比如,敌方攻打你的老巢,你是选择先消灭敌方呢,还是选择坐视不理?很可能一步就决定了结局。因此,需要从整个回合的角度看待这个问题。先引入一个概念 带衰减reward的累加期望

discount_reward[i] = reward[i] + gamma * discount_reward[i+1]

某一步的累加期望等于下一步的累加期望乘衰减系数gamma,加上reward

手工算一算。

1
2
3
4
最后一步:1
倒数第二步:1 + 0.95 * 1 = 1.95
倒数第三步:1 + 0.95 * 1.95 = 2.8525
倒数第四步:1 + 0.95 * 2.8525 = 3.709875

假设某个回合只得了10分,那么这个回合的每一步的累加期望都不会高。假设得到了满分200分,那么回合中的大部分步骤的累加期望很会很高,越是前面的步骤,累加期望越高。

代码实现就很简单了,唯一的不同是最后加了中心化和标准化的处理。这样处理的目的是希望得到相同尺度的数据,避免因为数值相差过大而导致网络无法收敛。

1
2
3
4
5
6
7
8
9
10
# policy_gradient.py
# https://geektutu.com
def discount_rewards(rewards, gamma=0.95):
"""计算衰减reward的累加期望,并中心化和标准化处理"""
prior = 0
out = np.zeros_like(rewards)
for i in reversed(range(len(rewards))):
prior = prior * gamma + rewards[i]
out[i] = prior
return out / np.std(out - np.mean(out))

2) 给loss加权重

一个动作的累加期望很高,自然希望该动作出现的概率变大,这就是学习的目的。一般,我们通过构造**标签(y_true/label),来训练神经网络。就如在TensorFlow 2.0 (六) - 监督学习玩转 OpenAI gym game这篇文章中做的一样。当然,我们还可以通过改变损失函数(loss function)**达到目的。对于累加期望大的动作,可以放大loss的值,而对于累加期望小的动作,那么就减小loss的值。这样呢?神经网络就能快速朝着累加期望大的方向优化了。最简单的方法,给loss加一个权重。

所以我们的最终的损失函数就变成了:

loss = discount_reward * loss

这里的discount_reward可以理解为策略梯度算法(Policy Gradient)中的梯度(Gradient)。如果对梯度不熟悉,可以看第一篇文章TensorFlow入门(一) - mnist手写数字识别(网络搭建)

TensorFlow 1.x的版本中,搭建一个自定义loss的网络很复杂,而使用TensorFlow 2.0,借助Keras,我们可以写出非常简洁的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
# policy_gradient.py
# https://geektutu.com
def train(records):
s_batch = np.array([record[0] for record in records])
# action 独热编码处理,方便求动作概率,即 prob_batch
a_batch = np.array([[1 if record[1] == i else 0 for i in range(ACTION_DIM)]
for record in records])
# 假设predict的概率是 [0.3, 0.7],选择的动作是 [0, 1]
# 则动作[0, 1]的概率等于 [0, 0.7] = [0.3, 0.7] * [0, 1]
prob_batch = model.predict(s_batch) * a_batch
r_batch = discount_rewards([record[2] for record in records])

model.fit(s_batch, prob_batch, sample_weight=r_batch, verbose=0)

设置参数sample_weight,即可给loss设权重。

训练过程与结果

接下来,把 OpenAI gym 的代码融入进来吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# policy_gradient.py
# https://geektutu.com
episodes = 2000 # 至多2000次
score_list = [] # 记录所有分数
for i in range(episodes):
s = env.reset()
score = 0
replay_records = []
while True:
a = choose_action(s)
next_s, r, done, _ = env.step(a)
replay_records.append((s, a, r))

score += r
s = next_s
if done:
train(replay_records)
score_list.append(score)
print('episode:', i, 'score:', score, 'max:', max(score_list))
break
# 最后10次的平均分大于 195 时,停止并保存模型
if np.mean(score_list[-10:]) > 195:
model.save('CartPole-v0-pg.h5')
break
env.close()

运行一下试一试吧。

1
2
3
4
5
6
7
8
9
10
$ python policy_gradient.py
episode: 0 score: 13.0 max: 13.0
episode: 1 score: 35.0 max: 35.0
episode: 2 score: 18.0 max: 35.0
...
episode: 793 score: 200.0 max: 200.0
episode: 794 score: 200.0 max: 200.0
episode: 795 score: 164.0 max: 200.0
episode: 796 score: 200.0 max: 200.0
model saved

画一张图,感受下学习的过程,这一次稍微多了3行多项式拟合的代码,能够更好地展现整个分数变化的趋势。

1
2
3
4
5
6
7
# policy_gradient.py
# https://geektutu.com
plt.plot(score_list)
x = np.array(range(len(score_list)))
smooth_func = np.poly1d(np.polyfit(x, score_list, 3))
plt.plot(x, smooth_func(x), label='Mean', linestyle='--')
plt.show()

Geektutu Policy Gradient Plot

测试

按照惯例,测试下效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# test_policy_gradient.py
# https://geektutu.com
import time
import numpy as np
import gym
from tensorflow.keras import models

saved_model = models.load_model('CartPole-v0-pg.h5')
env = gym.make("CartPole-v0")

for i in range(5):
s = env.reset()
score = 0
while True:
time.sleep(0.01)
env.render()
prob = saved_model.predict(np.array([s]))[0]
a = np.random.choice(len(prob), p=prob)
s, r, done, _ = env.step(a)
score += r
if done:
print('using policy gradient, score: ', score) # 打印分数
break
env.close()
1
2
3
4
5
6
python test_policy_gradient.py
using policy gradient, score: 200.0
using policy gradient, score: 200.0
using policy gradient, score: 200.0
using policy gradient, score: 200.0
using policy gradient, score: 200.0

Geektutu Policy Gradient Success

如何优化

教程中,每个回合不管多少条训练数据,直接训练,而没有固定大小的batch,不利于训练。有时间可以尝试,设置一个大小为2000的队列,存储历史的训练数据,每次固定取32/64条训练集,对比下两者的效果。

代码已上传至 Github - CartPole-v0-policy-gradient

附 推荐


专题:

本文发表于 2019-07-06,最后修改于 2023-11-15。

本站永久域名「 geektutu.com 」,也可搜索「 极客兔兔 」找到我。


上一篇 « 博客折腾记(五) - 友链这件事,没那么简单 下一篇 » TensorFlow 2 / 2.0 中文文档

赞赏支持

请我吃胡萝卜 =^_^=

i ali

支付宝

i wechat

微信

推荐阅读

Big Image