本文目的是用最简单的代码,展示DQN玩游戏的效果,不涉及深度学习原理讲解。
毕竟,入门如此艰难,唯一的动力不过是看个效果,装个biu……
安装OpenAI的游戏库gym
pip install gym
看一下运行效果
import gym
env = gym.make('CartPole-v1')
print('State shape:', env.observation_space.shape)
print('Number of actions:', env.action_space.n)
for _ in range(20):
observation = env.reset() # 初始状态
for t in range(500):
env.render() # 显示图像
action = env.action_space.sample() # 随机选择一个动作
observation, reward, done, info = env.step(action) # 状态,回报,是否结束,信息
print(observation, reward, done, info)
if done:
print("Episode finished after {} timesteps".format(t + 1))
break
env.close()
这是个手推车平衡游戏,我们可以采取两个动作(action),向左推或向右推,保持杆子不倒的时间越长,分数越高。游戏的每个时刻都有一个状态(observation),这个游戏中状态是由4个数值描述的,我们其实不必了解这四个值的含义,规律反正是交给网络学习的,这是深度学习很爽的地方,写出AlphaGo的程序员不需要懂围棋规则。每采取一个动作,除了得到下一时刻的状态,最重要的是得到一个回报(reward),在这个游戏中,无论什么动作,回报都是+1,意思是只要活着,就给你加分,直到杆子倒下,游戏结束。
代码
import random
import gym
import torch
from torch import nn, optim
class QNet(nn.Sequential):
def __init__(self):
super(QNet, self).__init__(
nn.Linear(4, 256),
nn.ReLU(),
nn.Linear(256, 128),
nn.ReLU(),
nn.Linear(128, 2)
)
class Game:
def __init__(self, exp_pool_size, explore):
self.env = gym.make('CartPole-v1')
self.exp_pool = []
self.exp_pool_size = exp_pool_size
self.q_net = QNet()
self.explore = explore
self.loss_fn = nn.MSELoss()
self.opt = optim.Adam(self.q_net.parameters())
def __call__(self):
is_render = False
avg = 0
while True:
# 数据采样
state = self.env.reset()
R = 0
while True:
if is_render:
self.env.render()
if len(self.exp_pool) >= self.exp_pool_size:
self.exp_pool.pop(0)
self.explore += 1e-7
if torch.rand(1) > self.explore:
action = self.env.action_space.sample()
else:
_state = torch.tensor(state, dtype=torch.float32)
Qs = self.q_net(_state[None, ...])
action = torch.argmax(Qs, 1)[0].item()
else:
action = self.env.action_space.sample()
next_state, reward, done, _ = self.env.step(action)
R += reward
self.exp_pool.append([state, reward, action, next_state, done])
state = next_state
if done:
avg = 0.95 * avg + 0.05 * R
print(avg, R)
if avg > 400:
is_render = True
break
# 训练
if len(self.exp_pool) >= self.exp_pool_size:
exps = random.choices(self.exp_pool, k=100)
_state = torch.tensor([exp[0] for exp in exps]).float()
_reward = torch.tensor([[exp[1]] for exp in exps])
_action = torch.tensor([[exp[2]] for exp in exps])
_next_state = torch.tensor([exp[3] for exp in exps]).float()
_done = torch.tensor([[int(exp[4])] for exp in exps])
# 预测值
_Qs = self.q_net(_state)
_Q = torch.gather(_Qs, 1, _action)
# 目标值
_next_Qs = self.q_net(_next_state)
_max_Q = torch.max(_next_Qs, dim=1, keepdim=True)[0]
_target_Q = _reward + (1 - _done) * 0.9 * _max_Q
loss = self.loss_fn(_Q, _target_Q.detach())
self.opt.zero_grad()
loss.backward()
self.opt.step()
if __name__ == '__main__':
g = Game(10000, 0.9)
g()
代码很简练,慢慢读都能懂,我解释一下几个重点。
- 整个流程是先采样,将样本存入经验池self.exp_pool,当样本足够时,从经验池中随机选取100条样本进行训练,而后边更新经验池边训练。
- QNet就是我们学习的网络,状态到动作的映射,根据输入状态建议采取的动作。
- self.explore探索值,很容易可以看明白它是一个概率,控制动作是随机选取还是由网络推荐。探索可以让我们发现新鲜样本,但随着训练进行,我们见过的样本越来越多,应该逐渐减少探索,也就是降低随机动作的概率。
- R是每一局游戏的得分,因为这个值在训练中变化非常大,所以加了个avg滑动平均的操作,这样可以更清晰地看出训练效果。由于游戏限制,每局游戏最多到500分就会结束,所以我们设置当avg>400就开始显示图像。
- 其实强化学习DQN真正的精髓是在训练中目标值的确定,所以这块我们跳过~[666]
没错,就这点代码,训练2分钟,DQN就能学会玩这个平衡游戏啦!
再附带一个小车爬坡的小游戏
游戏名:MountainCar-v0。小车想到达最高峰,但其引擎强度不足以单程通过,所以要在两个山坡间反复横跳,积蓄力量,一鸣惊人~
这个游戏的区别是reward始终为-1,意思是只有到达终点才有奖励,其他打酱油行为都要扣分,拿到高分的办法就是尽快到达终点。状态有2个值:水平位置和速度。动作有3个值:左、右、不动。所以要把QNet的输入和输出维度改一下。
训练难点在于:游戏只持续200个动作,在随机选择动作的情况下,小车很难靠运气到达终点,也就很少有成功的经验,不容易学习。
next_state, reward, done, _ = self.env.step(action)
position, velocity = next_state
reward = (position + 0.5) ** 2
R += reward
于是我根据状态把reward给改了,position + 0.5的原因是,起始最低点的水平坐标值为-0.5,改完之后的reward含义就是:离最低点越远奖励越高,而且奖励还是成平方增长。
经过这通骚操作,就可以训练了,也只需要训练几分钟。avg的阈值我给的30,同学们看着给。