深度强化学习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通道 \rightarrow 16通道
  • Conv 16通道 \rightarrow 32通道
  • Conv 32通道 \rightarrow 32通道
  • Linear 512节点 \rightarrow 2节点

为何第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原图
  • 中间灰色图是差值部分,蓝色是少去的部分,棕色是多出的部分
  • 下面两图是原始图覆盖差值图,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(),大致过程是这样的:

  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后的训练结果。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270