本文章通过keras实现DQN算法来解决倒立摆的平衡问题
一.环境
cartpole是一个经典的环境,可以验证许多的算法。这次我用的是cartpole-v0,一个离散动作空间的倒立摆环境,该环境有两个动作,即左和右,并且包含环境的四个状态观测值。
然后就瞅瞅这个环境:
黑色的载体可以左右移动,来保持平衡杆直立
然后看一下gym官方的描述:
这里面尤其要注意的一点就是关于“Reward”的描述,每一步都给“1”分的回报,而且最关键的是,即使这一步导致一轮交互结束了,也给予“1”分的回报。这样显然是不可取的,因此要在交互过程结束后,给这一步的Reward赋予“坏”的回报,这里我设置了-10。
reward = reward if not done else -10
二.DQN代码
代码部分我直接用了我上一篇文章解决“MountainCar”的主体代码部分,该文章链接在末尾会附上。
%%time
from tensorflow.keras import Sequential,layers
from tensorflow.keras.models import load_model
from tensorflow.keras.optimizers import Adam
from collections import deque
import numpy as np
import random
import gym
class Agent(object):
def __init__(self,):
'''
初始化类的变量:
(1)steps记录与环境交互的次数,用来控制target网络的参数更新以及模型的训练
(2)var和e共同参与探索率的衰减
(3)model和target网络,model是决策的主体,target网络作为一个“监督”
(4)replay_memory是经验池
'''
self.steps = 0
self.var = 1e-1
self.e = 1e-5
self.Model = self.model_()
self.Target = self.model_()
self.replay_memory = deque(maxlen=1000)
def model_(self):
'''
定义决策网络,这里采用了两层“relu”作为激活函数的全连接层
最后一层不采取激活函数,也可使用linear,但用处不大
最后一层的神经元数量为输出的动作类型的数量,这个模型有两个动作
因此我们最后一层有两个神经元
编译使用了均方差作为loss函数
使用Adam优化器,并设置学习率为0.01
'''
model = Sequential(name="DQN")
model.add(layers.Dense(64,"relu"))
model.add(layers.Dense(64,"relu"))
model.add(layers.Dense(2,"linear"))
model.compile(optimizer=Adam(lr=0.01),loss = "mse")
return model
def add(self,obs,action,reward,n_s,done):
'''
向经验池中添加交互序列
'''
self.replay_memory.append((obs,action,reward,n_s,done))
def sample(self,obs):
'''
动作选择函数,根据决策网络选择动作
也有一定的概率随机选择动作,即探索
随着训练的进行,探索率会逐渐降低
因为是两个动作,所以随机取值设定为0或1
'''
self.var -= self.e
if np.random.uniform() <= self.var:
return np.random.randint(2)
return np.argmax(self.Model.predict(obs))
def data(self):
'''
从经验池中选择数据,batch_size = 64
这里可以根据需要自行调整
每一条数据都包括五部分,即为
(观测值,动作,回报,下一个观测值,结束状态)
每次调用这个函数都返回一个batch的数据用来训练
'''
batch = random.sample(self.replay_memory,64)
Obs,Action,Reward,N_s,Done = [],[],[],[],[]
for (obs,action,reward,n_s,done) in batch:
Obs.append(obs)
Action.append(action)
Reward.append(reward)
N_s.append(n_s)
Done.append(done)
return np.array(Obs).astype("float32"),np.array(Action).astype("int64"),np.array(Reward).astype("float32"),\
np.array(N_s).astype("float32"),np.array(Done).astype("float32")
def learn(self):
'''
训练的主体
(1)与环境交互100次同步一哈target网络与决策网络的参数
即复制决策网络到target网络
(2)为了更高效的训练,在数据足够的前提下,每隔五步训练一次
(3)通过obs得到Q,next_obs得到Target_Q,通过Q_learning的方法更新Q的
数据如果done为True,说明环境终止,不需要考虑下一个动作。否则依据
Q(s,a) <-- Q(s,a) + α{r + γ*max*Q(s', : ) - Q(s,a)}来更新Q
最后将更新好的Q作为“标签”来进行训练
'''
if self.steps % 100 == 0:
self.Target.set_weights(self.Model.get_weights())
if self.steps % 5 == 0 and len(self.replay_memory) >= 200:
#取出一个batch的数据
Obs,Action,Reward,N_s,Done = self.data()
Q = self.Model.predict(Obs.reshape(64,1,4))
Q_ = self.Target.predict(N_s.reshape(64,1,4))
'''
因为编译模型里设置了学习率,在这里不重复设置
'''
for i in range(64):
if Done[i]:
Q[i][0][Action[i]] = Reward[i]
Q[i][0][Action[i]] = (Reward[i] + 0.9*np.amax(Q_[i][0]))
#训练决策网络
self.Model.fit(Obs.reshape(64,1,4),Q,verbose=0)
env = gym.make("CartPole-v0")
agent = Agent()
Scores = []
for times in range(1000):
s = env.reset()
Score = 0
while True:
agent.steps += 1
a = agent.sample(s.reshape(1,1,4))
next_s, reward, done, _ = env.step(a)
'''
对于模型done之后的reward设置是必要的
否则模型不会收敛,因为这个环境获得回报
是根据稳定的时间来算的,即使done为True
也会积累正值的回报,因此将done==True之
后的reward设置为负值,以让模型收敛
'''
reward = reward if not done else -10
agent.add(s,a,reward,next_s,done)
agent.learn()
Score += reward
s = next_s
if done:
Scores.append(Score)
print('episode:',times,'score:',Score,'max:',np.max(Scores))
break
#提前终止训练
if np.mean(Scores[-6:])>= 180:
agent.Model.save("cartpole_2.h5")
break
agent.Model.save("cartpole_2.h5")
因为网络结构相对较小,以及采取不是逐步训练的方式,算法执行的效率还是很快的。最快的一次大概是10秒以内,就收敛到可取的范围。
训练的过程(57次训练并结束)
接着用训练好的模型进行六轮测试,结果都是满分(200),还不错
经过多次尝试,发现模型稳定在满分状态。
关于DQN解决的另一个“小车”环境:
https://blog.csdn.net/weixin_43968987/article/details/113057733
然后老习惯,附上Eason的果:
笑,何妨与你又重温,仍然我说我庆幸,你永远胜过别人 ——— 无条件