深度强化学习DQN详解CartPole(2)

2020-06-29  本文已影响0人  何哀何欢

二、 卷积网络和训练

接上回 处理环境图片
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))

还是比较直白的:

为何第2层最后转为512节点,用到了卷积形状计算公式:

conv = \frac{(X - kernel + 2*padding)}{stride}+1

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层变化,乘上第三层通道数,就是最终全连接层的大小:conv _{height} \times conv _{width} \times channel。代码写作:

        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).indicesview(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的变化。

以下是关键训练循环代码,逻辑是一样的。只是有一处需要注意,在循环的时候,会将(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(),大致过程是这样的:

  1. 从memory列表里选取n个 (state, action, next_state, reward)
  2. 用net获取state的Y[0, 1](net输出为2个值),再用action选出结果y
  3. 用net获取next_state获取Y'[0,1],取最大值 y'。如果state没有对应的next_state,则y' = 0
  4. 用公式算出期望y:\hat y = \gamma y' + reward (常量 \gamma = 0.9
  5. 用smooth_l1_loss计算误差
  6. 用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的,就相当于
y \leftarrow y + \hat y \times lr,所以 Q Learning 公式中的
Q_{s,a} \leftarrow ( 1- \alpha) \times Q_{s,a} + \alpha \times \hat y
前面的部分就省掉了。

示例使用的gamma \gamma为0.99,效果并不好,几乎不会学习。我改为0.7后,训练120次达到57步,总的来说,就小车环境而言,示例中的卷积网络,效果比128节点的全连接层网络差太多。128节点的全连接层网络,训练几十次就可以达到满分200步。


这是训练中持续时长统计,橙色为平均值,最高也就是50多,感觉示例代码的效果并不是很好。OpenAI官方的要求是,连续跑100次平均持续时长为195。这是\gamma改为0.7后的训练结果。

上一篇下一篇

猜你喜欢

热点阅读