深度强化学习DQN详解CartPole(2)
二、 卷积网络和训练
接上回 处理环境图片。
python几处值得关注的用法(连接)
示例用卷积网络来训练动作输出:
def conv2d_size_out(size, kernel_size = 5, stride = 2):
return (size - (kernel_size - 1) - 1) // stride + 1
class DQN(nn.Module):
def __init__(self, h, w, outputs):
super(DQN, self).__init__()
self.conv1 = nn.Conv2d(3, 16, kernel_size=5, stride=2)
self.bn1 = nn.BatchNorm2d(16)
self.conv2 = nn.Conv2d(16, 32, kernel_size=5, stride=2)
self.bn2 = nn.BatchNorm2d(32)
self.conv3 = nn.Conv2d(32, 32, kernel_size=5, stride=2)
self.bn3 = nn.BatchNorm2d(32)
convw = conv2d_size_out(conv2d_size_out(conv2d_size_out(w)))
convh = conv2d_size_out(conv2d_size_out(conv2d_size_out(h)))
linear_input_size = convw * convh * 32
self.head = nn.Linear(linear_input_size, outputs)
# Called with either one element to determine next action, or a batch
# during optimization. Returns tensor([[left0exp,right0exp]...]).
def forward(self, x):
x = F.relu(self.bn1(self.conv1(x)))
x = F.relu(self.bn2(self.conv2(x)))
x = F.relu(self.bn3(self.conv3(x)))
return self.head(x.view(x.size(0), -1))
还是比较直白的:
- Conv 3通道 16通道
- Conv 16通道 32通道
- Conv 32通道 32通道
- Linear 512节点 2节点
为何第2层最后转为512节点,用到了卷积形状计算公式:
conv 为某维度上卷积后的尺寸,X为卷积前的尺寸。
(W - kernel_size + 2 * padding ) // stride + 1
示例中的Conv层没有padding,所以公式变为:
(size - kernel_size) // stride + 1
但不知为何示例代码将 - kernel_size 写为 - (kernel_size - 1) - 1。因为两者完全相等:
def conv2d_size_out(size, kernel_size = 5, stride = 2):
return (size - (kernel_size - 1) - 1) // stride + 1
这只是某个维度的一次卷积变化,所以一张图,完整的尺寸应该是2个维度的乘积,再经过3层变化,乘上第三层通道数,就是最终全连接层的大小:。代码写作:
convw = conv2d_size_out(conv2d_size_out(conv2d_size_out(w)))
convh = conv2d_size_out(conv2d_size_out(conv2d_size_out(h)))
linear_input_size = convw * convh * 32
这个网络的输出为动作值,动作值为0或1,但0/1代表的是枚举类型,并不是值类型,也就是说,动作0并不意味着没有,动作1也不意味着1和0之间的某种数值度量关系,0和1纯粹是枚举,所以输出数为2个,而不是1个。应为将图像缩放到40 x 90,所以网络的参数就是(40, 90,2)。试一下这个网络:
net = DQN(40, 90, 2).to(device) scr = get_screen() net(scr)
tensor([[-1.0281, 0.0997]], device='cuda:0', grad_fn=<AddmmBackward>)
OK,返回两个值。
行动决策采用 epsilon greedy policy,就是有一定的比例,选择随机行为(否则按照网络预测的最佳行为行事)。这个比例从0.9逐渐降到0.05,按EXP曲线递减:
EPS_START = 0.9 # 概率从0.9开始
EPS_END = 0.05 # 下降到 0.05
EPS_DECAY = 200 # 越小下降越快
steps_done = 0 # 执行了多少步
100时
200时
随机行为是强化学习的灵魂,没有随机行动,就没有探索,没有探索就没有持续的成长。select_action() 的作用就是 选择网络输出的2个值中的最大值()或 随机数
def select_action(state):
global steps_done
sample = random.random() #[0, 1)
#epsilon greedy policy。EPS_END 加上额外部分,steps_done 越小,额外部分越接近0.9
eps_threshold = EPS_END + (EPS_START - EPS_END) * math.exp(-1. * steps_done / EPS_DECAY)
steps_done += 1
if sample > eps_threshold:
with torch.no_grad():
#选择使用网络来做决定。max返回 0:最大值和 1:索引
return policy_net(state).max(1)[1].view(1, 1)
else:
#选择一个随机数 0 或 1
return torch.tensor([[random.randrange(n_actions)]], device=device, dtype=torch.long)
通常网络做枚举输出,是需要用到CrossEntropy的。(关于CrossEntropy的文章),示例代码在使用网络时,简单判断了一下,谁大就取谁的索引,所以就相当于做了一个CrossEntropy。
pytorch 的 tensor.max() 返回所有维度的最大值及其索引,但如果指定了维度,就会返回namedtuple,包含各维度最大值及索引 (values=..., indices=...) 。
max(1)[1] 只取了索引值,也可以用 max(1).indices。view(1,1) 把数值做成[[1]] 的二维数组形式。为何返回一个二维 [[1]] ? 这是因为后面要把所有的state用torch.cat() 合成batch(cat()说明连接)。
return policy_net(state).max(1)[1].view(1, 1)
# return 0 if value[0] > value[1] else 1
示例中,训练是用两次屏幕截图的差别来训练网络:
for t in count():
# 1. 获取屏幕 1
last_screen = get_screen()
# 2. 选择行为、步进
action = select_action(state)
_, reward, done, _ = env.step(action)
# 3. 获取屏幕 2
current_screen = get_screen()
# 4. 计算差别 2-1
state = current_screen - last_screen
# 5. 优化网络
optimize_model()
当前状态及两次状态的差,如下所示,
- 上边两个分别是step0和step1原图
- 中间灰色图是差值部分,蓝色是少去的部分,棕色是多出的部分
- 下面两图是原始图覆盖差值图,step0将完全复原为step1,step1则多出部分颜色加强
可以看出,差值是step0到step1的变化。
以下是关键训练循环代码,逻辑是一样的。只是有一处需要注意,在循环的时候,会将(state, action, next_state, reward)这四个值,保存起来,循环存放在一个叫memory的列表里,凑够批次后,才会用数据训练网络,否则optimize_model()直接返回。
num_episodes = 50
TARGET_UPDATE = 10
for i_episode in range(num_episodes):
env.reset()
last_screen = get_screen()
current_screen = get_screen()
state = current_screen - last_screen
# [0, 无限) 直到 done
for t in count():
action = select_action(state)
_, reward, done, _ = env.step(action.item())
reward = torch.tensor([reward], device=device)
last_screen = current_screen
current_screen = get_screen()
next_state = None if done else current_screen - last_screen
// 保存 state, action, next_state, reward 到列表 memory
state = next_state
optimize_model()
if done:
break
关于optimize_model(),大致过程是这样的:
- 从memory列表里选取n个 (state, action, next_state, reward)
- 用net获取state的(net输出为2个值),再用action选出结果
- 用net获取next_state获取,取最大值 。如果state没有对应的next_state,则
- 用公式算出期望y: (常量 )
- 用smooth_l1_loss计算误差
- 用RMSprop 反向传导优化网络
期望y的计算方法很简单,就是把next_state的net结果,直接乘一个0.9然后加上奖励。如果有 next_state,就是1,如果next_state为None,奖励是0。因此,没有明天的state,期望y最小。
这里的关键是如何求期望y,用了Q learning:Q Learning解释
也就是遗忘率为1的Q learning求值函数。为何遗忘率是1呢?我的想法是,在NN optimize的时候,本身就是有一个learning rate的,就相当于
,所以 Q Learning 公式中的
前面的部分就省掉了。
示例使用的gamma 为0.99,效果并不好,几乎不会学习。我改为0.7后,训练120次达到57步,总的来说,就小车环境而言,示例中的卷积网络,效果比128节点的全连接层网络差太多。128节点的全连接层网络,训练几十次就可以达到满分200步。
这是训练中持续时长统计,橙色为平均值,最高也就是50多,感觉示例代码的效果并不是很好。OpenAI官方的要求是,连续跑100次平均持续时长为195。这是改为0.7后的训练结果。