神经网络的作用
- 将状态和动作当成神经网络的输入, 然后经过神经网络分析后得到动作的 Q 值, 这样我们就没必要在表格中记录 Q 值。而是直接使用神经网络生成 Q 值.
- 也能只输入状态值, 输出所有的动作值, 然后按照 Q learning 的原则, 直接选择拥有最大值的动作当做下一步要做的动作.
神经网络接受外部的信息, 相当于眼睛鼻子耳朵收集信息, 然后通过大脑加工输出每种动作的值,最后通过强化学习的方式选择动作.
更新神经网络
基于第二种神经网络
- 需要 a1, a2 正确的Q值, 这个 Q 值我们就用之前在 Q learning 中的 Q 现实来代替. 还需要一个 Q 估计来实现神经网络的更新.
- 所以神经网络的的参数就是老的 NN 参数 加学习率 alpha 乘以 Q 现实 和 Q 估计 的差距.
通过 NN 预测出Q(s2, a1) 和 Q(s2,a2) 的值, 这就是 Q 估计. 然后我们选取 Q 估计中最大值的动作来换取环境中的奖励 reward. 而 Q 现实中也包含从神经网络分析出来的两个 Q 估计值, 不过这个 Q 估计是针对于下一步在 s’ 的估计. 最后再通过刚刚所说的算法更新神经网络中的参数.
Experience replay 和 Fixed Q-targets
Experience Replay
- 深度神经网络,要求数据满足独立同分布。但 Q Learning 算法得到的样本前后是有关系的。为了打破数据之间的关联性,Experience Replay 方法通过存储-采样的方法将这个关联性打破了。
Fixed Q-targets
- 也是一种打乱相关性的机理, 使用 fixed Q-targets, 就会在 DQN 中使用到两个结构相同但参数不同的神经网络, 预测 Q估计 的神经网络具备最新的参数, 而预测 Q 现实 的神经网络使用的参数则是很久以前的.
实例
建立两个神经网络eval_net用来训练,target_net用来暂存
并保存
RL_brain
import numpy as np
import pandas as pd
from RLearning.DQN.mazeEnv import Maze
import torch
import torch.nn as nn
class DeepQNetwork():
def __init__(self, n_actions, n_features, learning_rate=0.01, reward_decay=0.9,
e_greedy=0.9, replace_target_iter=200, memory_size=2000, batch_size=32,
e_greedy_increment=None):
self.n_actions = n_actions # 输出多少个action的值
self.n_features = n_features #接受多少个observation
self.lr = learning_rate
self.gamma = reward_decay#衰减度
self.epsilon_max = e_greedy#贪婪度
# self.q_table = pd.DataFrame(columns=self.actions, dtype=np.float64)#纵轴是有多少个state横轴标签是可用action
# 初始化qtable是一个空的dataframe
self.replace_target_iter = replace_target_iter # 多少步更换一次
self.memory_size = memory_size # 整个记忆库的数据
self.batch_size = batch_size
self.epsilon_increment = e_greedy_increment # 缩小随机范围
self.epsilon = 0 if e_greedy_increment is not None else self.epsilon_max
self.memory_counter=0
self.learn_step_counter = 0 # 记录学了多少步 epsilon根据counter不断提高
# 全0 memory [s, a, r, s_]记忆库
self.memory = np.zeros((self.memory_size, n_features * 2 + 2)) # 行为200条记忆 列中+2为a,r
# 建立[target_net, evaluate_net]
self.eval_net, self.target_net = self.buildNet(n_features, n_actions), self.buildNet(n_features, n_actions)
self.cost_his = [] # 记录误差
self.optimizer = torch.optim.Adam(self.eval_net.parameters(), lr=self.lr)#优化器
self.loss_func = nn.MSELoss()#损失函数
def store_transition(self, s, a, r, s_):
transition = np.hstack((s, [a, r], s_)) # 把所有的记忆捆在一起
index = self.memory_counter % self.memory_size # 如果memory_counter超过了MEMORY_CAPACITY(记忆上限)覆盖掉老的记忆
self.memory[index, :] = transition # 存到相对性的位置
self.memory_counter += 1
def choose_action(self, x):
x = torch.unsqueeze(torch.FloatTensor(x), 0)
if np.random.uniform() < self.epsilon: # 如果随机数小于0.9
# 根据最优选action
actions_value = self.eval_net.forward(x)
action = torch.max(actions_value, 1)[1]
action = int(action)
else:
# 10%的情况随机选择action
action = np.random.randint(0, self.n_actions)
return action
def buildNet(self, n_features, n_actions):
return torch.nn.Sequential(
torch.nn.Linear(n_features, 10),
torch.nn.ReLU(),
torch.nn.Linear(10, n_actions),
)
def learn(self):
if self.learn_step_counter % self.replace_target_iter == 0: # 到多少步数就更新
self.target_net.load_state_dict(self.eval_net.state_dict()) # 将eval_net参数赋值到target_net
self.learn_step_counter += 1
# 让eval_net每步都要更新
sample_index = np.random.choice(self.memory_size, self.batch_size) # 从记忆库中随机抽取一些记忆
b_memory = self.memory[sample_index, :]
# 将存储的记忆打包
b_s = torch.FloatTensor(b_memory[:, :self.n_features])
b_a = torch.LongTensor(b_memory[:, self.n_features:self.n_features + 1].astype(int))
b_r = torch.FloatTensor(b_memory[:, self.n_features + 1:self.n_features + 2])
b_s_ = torch.FloatTensor(b_memory[:, -self.n_features:])
q_eval = self.eval_net(b_s).gather(1, b_a) # 输入现在的状态通过forward(self, x)生成abctions_value
# 根据所有动作的价值选取当初施加动作上的动作价值的价值
# 如:输出了两个向左走向右走动作上的价值然后根据当初选择动作(如向左),变成q_eval(当初向左走的价值是多少)
q_next = self.target_net(b_s_).detach()
q_target = b_r + self.gamma * q_next.max(1)[0].view(self.batch_size, 1) # shape (batch, 1)
loss = self.loss_func(q_eval, q_target)
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
def save(self, save_path):
torch.save(self.eval_net.state_dict(), save_path) # 保存整个神经网络的参数
if __name__ == '__main__':
# __name__ 获取的是当前文件的name属性,只有在自己的文件下这个name才是main,是以在别的文件调用时这部分不会被执行
# 这里相当于本文件的测试区域,可以直接在这里执行
env = Maze()
dqn = DeepQNetwork(env.n_actions, env.dim_state)
print('\nCollecting experience...')
for i_episode in range(10):
s = env.reset()
ep_r = 0
while True:
env.render()
a = dqn.choose_action(s)
s_, r, done = env.step(a) # gym中的环境通常有4个返回值,最后一个是大抵就保存了一些日志信息
dqn.store_transition(s, a, r, s_)
ep_r += r # 记录总的r,应该大抵可以算作一种衡量路径优劣的指标吧
# if dqn.memory_counter > dqn.memory_size:
if dqn.memory_counter > 200:
dqn.learn()
if done:
print('Ep: ', i_episode, '| Ep_r: ', round(ep_r, 2))
break
s = s_
dqn.save('data/models/dqn_params.pkl')
print('\ntest...')
# 要是直接在这里写load的画也没什么意义,因为现在dqn还是上面的dqn,还保留着这些参数
maze环境
# 构造gym Maze环境
import gym
from gym import spaces
import numpy as np
import time
from gym.envs.classic_control import rendering
class Maze(gym.Env):
metadata = {
'render.modes': ['human', 'rgb_array'],
'video.frames_per_second': 2
}
# 初始化动作空间与状态空间等环境中会用的全局变量可以声明为类(self.)的变量
def __init__(self):
self.action_space = spaces.Discrete(5) # 0, 1, 2,3,4: 不动,上下左右
self.observation_space = spaces.Box(np.array([1, 1]), np.array([4, 4]), dtype=np.int)
self.n_actions = self.action_space.n
self.dim_state = self.observation_space.shape[0] # 转态向量维度
self.state = None
self.target = {(4,2): 20} # 安全/目标状态
self.danger = {(2,2): -20, (3,3): -20} # 危险状态
self.viewer = rendering.Viewer(500, 500, 'maze')
self.gridsize = 100
# 用来处理状态的转换逻辑
# 返回动作的回报、下一时刻的状态、以及是否结束当前episode及调试信息
def step(self, action):
assert self.action_space.contains(action), "%r (%s) invalid" % (action, type(action))
x, y = self.state
if action == 0: # 不动
x = x
y = y
elif action == 1: # 上
x = x
y = y + 1
elif action == 2: # 下
x = x
y = y - 1
elif action == 3: # 左
x = x - 1
y = y
elif action == 4: # 右
x = x + 1
y = y
next_state = np.array([x,y])
self.state = next_state if self.observation_space.contains(next_state) else self.state
self.counts += 1
s = tuple(self.state)
if s in self.danger.keys():
reward = self.danger[s]
done = True
elif s in self.target.keys():
reward = self.target[s]
done = True
else:
reward = -self._dis(list(self.target)[0])
# print(reward)
# reward = -1
# reward = 0
done = False
return self.state, reward, done
# 用于在每轮开始之前重置智能体的状态,把环境恢复到最开始
def reset(self, startstate=None):
'''
:param startstate: (1,1)
:return:
'''
if startstate==None:
self.state = self.observation_space.sample()
else:
self.state = startstate
self.counts = 0
return self.state
# metadata、render()、close()是与图像显示有关的
# render()绘制可视化环境的部分
def render(self, mode='human', draw_dict=False, states=None, states_label=None):
# 绘制网格
for i in range(5):
# 竖线
self.viewer.draw_line(
(50, 50),
(50, 450),
color=(0, 0, 0)
).add_attr(rendering.Transform((100 * i, 0)))
# 横线
self.viewer.draw_line(
(50, 50), (450, 50)).add_attr(rendering.Transform((0, 100 * i)))
# 绘制出口(安全状态)
for state in self.target:
self.drawrectangle2(state, color=(0, 1, 0))
# 绘制危险区域
for state in self.danger:
self.drawrectangle2(state, color=(1, 0, 0))
# 绘制当前state的位置(圆)
center = (
50 + self.state[0] * self.gridsize - 0.5 * self.gridsize,
50 + self.state[1] * self.gridsize - 0.5 * self.gridsize)
self.viewer.draw_circle(
48, 30, filled=True, color=(1, 1, 0)).add_attr(rendering.Transform(center))
if draw_dict:
for s, l in zip(states, states_label):
self._drawDict(s, l, color=(.5, .5, 1))
return self.viewer.render(return_rgb_array=mode == 'human')
def close(self):
if self.viewer:
self.viewer.close()
def drawrectangle(self, point, width, height, **attrs):
'''
画矩形
:param point: 左下角的坐标(480,320)
:param width: 横向长度
:param height: 竖向长度
:param attrs: 其他参数,such as color=(0,0,255), linewidth=5
:return:
'''
points = [point,
(point[0] + width, point[1]),
(point[0] + width, point[1] + height),
(point[0], point[1] + height)]
self.viewer.draw_polygon(points, **attrs)
def drawrectangle2(self, point, **attrs):
'''
画一个正多边形
:param point: 格点坐标(4,3)
:param attrs: 其他参数,such as color=(0,0,1), linewidth=5
:return:
'''
size = 100
center = (50 + point[0] * size - 0.5 * size, 50 + point[1] * size - 0.5 * size)
radius = 100 / np.sqrt(2)
res = 4
points = []
for i in range(res):
ang = 2 * np.pi * (i - 0.5) / res
points.append((np.cos(ang) * radius, np.sin(ang) * radius))
self.viewer.draw_polygon(points, filled=True, **attrs).add_attr(rendering.Transform(center))
def _drawgrid(self, s, **attrs):
point = (s[0] * self.gridsize, s[1] * self.gridsize) # 方格坐标右上角的像素坐标
angles = np.linspace(0, 2 * np.pi, 4 + 1)-0.25*np.pi # 角度值的起始向上为0度方向,顺时针为角度增大方向
points = [(0.5 * self.gridsize * np.sin(t), 0.5 * self.gridsize * np.cos(t)) for t in angles]
self.viewer.draw_polygon(points, **attrs).add_attr(rendering.Transform((point[0], point[1])))
def _drawDict(self, s, l, **attrs):
if l==0:
self._drawgrid(s, **attrs)
else:
point = (s[0] * self.gridsize, s[1] * self.gridsize) # 方格坐标右上角的像素坐标
angles = np.linspace(0, 2*np.pi, 3+1) # 角度值的起始向上为0度方向,顺时针为角度增大方向
if l==1:
pass
elif l==2:
angles = angles+np.pi
elif l==3:
angles = angles+1.5*np.pi
elif l==4:
angles = angles+0.5*np.pi
points = [(0.5*self.gridsize*np.sin(t), 0.5*self.gridsize*np.cos(t)) for t in angles]
self.viewer.draw_polygon(points, **attrs).add_attr(rendering.Transform((point[0], point[1])))
# 因为point是方格右上角的的坐标,也就巧合不用加画布边缘(5,5)的偏移
def _dis(self, state):
return np.sqrt((self.state[0]-state[0])**2+(self.state[1]-state[1])**2)
if __name__ == '__main__':
env = Maze()
env.reset()
states = [(x, y) for x in range(1, 5) for y in range(1, 5)]
labels = [3] * len(states) # 方向测试
env.render(draw_dict=True, states=states, states_label=labels)
time.sleep(50)
env.close()
run_this测试部分
from RL_brain import DeepQNetwork
from mazeEnv import Maze
import torch
import time
def train():
env = Maze()
dqn = DeepQNetwork(env.n_actions, env.dim_state)
print('\nCollecting experience...')
for i_episode in range(200):
s = env.reset()#清空环境
env.render()
ep_r = 0
while True:
a = dqn.choose_action(s)
#选择动作
s_, r, done = env.step(a) # gym中的环境通常有4个返回值,最后一个是大抵就保存了一些日志信息
env.render()
dqn.store_transition(s, a, r, s_)
ep_r += r # 记录总的r,应该大抵可以算作一种衡量路径优劣的指标吧
# if dqn.memory_counter > dqn.memory_size:
if dqn.memory_counter > 200:
dqn.learn()
if done:
print('Ep: ', i_episode, '| Ep_r: ', round(ep_r, 2))
break
s = s_
dqn.save('data/models/dqn_params.pkl')#保存训练结果
env.close()
def test():
env = Maze()
dqn = DeepQNetwork(env.n_actions, env.dim_state) # 这里是新建的一个,它的参数都是初始化的
dqn.eval_net.load_state_dict(torch.load('data/models/dqn_params.pkl'))
print('\nTesting...')
for i_episode in range(10):
s = env.reset()
time.sleep(0.5) # test时做延时,为了更好的观察效果
ep_r = 0
while True:
a = dqn.choose_action(s)
# take action
s_, r, done = env.step(a) # gym中的环境通常有4个返回值,最后一个是大抵就保存了一些日志信息
env.render()
time.sleep(0.5)
ep_r += r # 这个的意思是记录一下总的r,应该大抵可以算作一种衡量路径优劣的指标吧
# 测试过程就没有存储状态和学习的过程了
if done:
print('Epoch{} | ep_r: {}'.format(i_episode, ep_r))
break
s = s_
def test2():
env = Maze()
dqn = DeepQNetwork(env.n_actions, env.dim_state) # 这里是新建的一个,它的参数都是初始化的
dqn.eval_net.load_state_dict(torch.load('data/models/dqn_params.pkl'))
# 测试部分
dqn.epsilon = 1 # 这里就取消了随机选动作的可能,全部由网络生成
states = [(x, y) for x in range(1, 5) for y in range(1, 5)] # 所有的动作空间
t_states = torch.tensor(states).type(torch.float) # 必须转换成张量才能被pytorch的网络识别,cpu下默认得是float的才行
labels = torch.max(dqn.eval_net(t_states), 1)[1]
env.render(draw_dict=True, states=states, states_label=labels) # 出图
time.sleep(50)
env.close()
if __name__ == '__main__':
train()
# test()
test2()
print('over~')