1. 资料
- Udemy Deep Q Agents 课程:https://www.udemy.com/course/deep-q-learning-from-paper-to-code/?couponCode=LEARNWITHUSNOW
- OPEN MLSYS 强化学习介绍:https://openmlsys.github.io/chapter_reinforcement_learning/index.html
- 知乎白话强化学习:https://www.zhihu.com/column/c_1215667894253830144
- Double Q Learning 论文:https://cdn.aaai.org/ojs/10295/10295-13-13823-1-2-20201228.pdf
- Dueling Deep Q Learning 论文:https://arxiv.org/pdf/1511.06581
2. Q Learning 的思路
强化学习的目标是:获得一个智能体 Agent ,当把环境信息告知 Agent 以后,Agent 可以做出”好”的决策。
有一些关键词:环境信息、“好”决策
- 为了建模环境信息和交互影响,可以简单粗暴利用马尔科夫链来建模整个交互场景。
- 每个状态 state 都可以被独立描述(不依赖历史和未来状态)
- 特定 state 下做出特定的行为 action 后,会概率转移到某其他 state
- 基于马尔科夫链可以极大简化建模的难度。虽然也有其限制
- 很多现实问题的环境并非是独立的,要强行独立则需要给 state 加入历史信息,可能会导致状态空间非常大
- 而为了评估”好”决策,则需要有价值评分机制
基于马尔科夫链和价值度量,我们可以建立一个 Q 值函数,用以度量各个 state 下各个 action 的 期望价值
\(Q(s, a) = \mathbb{E} \left[ \sum_{t=0}^{\infty} \gamma^t r_{t+1} \mid s_t = s, a_t = a \right]\)
- s 表示状态
- a 表示动作
- \(r_{t+1}\) 是下一时刻的奖励
- \(\gamma\) 是折扣因子,用来平衡当前奖励和未来奖励的重要性,0-1 之间,越小越看重当前奖励
不过我们最后更关心的 Q 值是在最优策略选择下的 \(Q^* \),如果获得了这么一个 \(Q^* \),那对于任意 state 我们只需要选择使 \(Q^* (s,a)\) 最大的那个 action 即可。即
\(Q^*(s, a) = \max_{\pi} \mathbb{E} \left[ \sum_{t=0}^{\infty} \gamma^t r_{t+1} \mid s_t = s, a_t = a, \pi \right]\)
- 其中 \(\pi\) 表示 Agent 的策略,对应于在各个 state 下会选择的 action。\(a=\pi(s)\)
由于我们并不知道 \(Q^* \) 函数的真实函数形式,我们需要去寻找它。参考机器学习思路:
- 先建立一个带参数的 Q 函数 \(Q(s,a,\theta)\) ,其中 \(\theta\) 是待定的参数(组)。
- 通过类似随机梯度下降的方法去更新参数 \(\theta\),使 \(Q(s,a,\theta)\) 逼近 \(Q^*(s, a) \)
- 目标值:\(r_t + \gamma \max_{a’} Q(s_{t+1}, a’)\)
- 向目标值靠近的更新方法:\(Q(s_t, a_t) \leftarrow Q(s_t, a_t) + \alpha \left( r_t + \gamma \max_{a’} Q(s_{t+1}, a’) – Q(s_t, a_t) \right)\)
- 不断用微小步伐向样本目标靠近(随机梯度下降)
3. Table Q Learning
3.1 思路说明
Q 本身是一个 状态,动作 -> 价值 (s,a -> v) 的函数,如果 s 和 a 的可能性都足够少,那完全可以建立一个 (s,a -> v) 的表去直接记录映射,如
States \ Actions | a1 | a2 | a3 |
s1 | v11 | v12 | v13 |
s2 | v21 | v22 | v23 |
s3 | v31 | v32 | v33 |
其参数组 \(\theta\) 最简单粗暴的设定方式,就是和表大小一致,每个 v 对应一个 \(\theta\)
即 \(Q(s,a,\theta) = \theta(s,a)\)
对应的参数更新方法 \(\theta(s_t, a_t) = \theta(s_t, a_t) + \alpha \left( r_t + \gamma \max_{a’} \theta(s_{t+1}, a’) – \theta(s_t, a_t) \right)\)
- 需要不停和环境交互获得 \(s_t, a_t, r_t, s_{t+1}\) 的经验信息
- 在尝试并获得经验过程中,尝试时的 action 选择策略也很重要,常用有
- ε-greedy 策略:这是最常见的选择策略之一。在 ε-greedy 策略中,智能体以 ε 的概率选择随机动作(探索),以 1− ε 的概率选择当前最优的动作(利用)。ε 通常从较高的值(如 1.0)开始,逐渐降低到一个较低的值(如 0.01),以便在训练初期有更多探索,后期逐步收敛到利用。
- Softmax 策略:将 Q 值转换成概率分布来选择动作,Q 值越高被选中概率越大。
- Thompson Sampling(汤普森采样):估计各个动作的回报的概率分布,并基于分布进行采样。
- … 其他策略
3.2 实践
用 OpenAI Gym 的经典强化学习环境 冰湖 来尝试 Table Q Learning。
- “Frozen Lake”(冰湖) 介绍:https://www.gymlibrary.dev/environments/toy_text/frozen_lake/
简而言之:控制小人在 4*4 的冰湖上走动。目标是走到右下角拿奖励。冰湖上有洞,掉洞就死。可以选择上下左右走,但选择并不一定对应于上下左右,可能由于滑动导致行为和结果不一致(有干扰)。
我们在让 Agent 学习的过程中,采用 ε-greedy 策略进行探索。
详细实现过程的 Jupyter Notebook 见:https://github.com/Raytto/my_ml_study/blob/main/deep_q_study/s1_regular_q_agents.ipynb
训练结果:
基本能稳定在 65% 左右的胜率,考虑到冰湖场景下由于滑动天然就非必胜,基本上 70% 左右的成功率就是最好策略了。
4. 基本的 Deep Q Learning
4.1 思路说明
当 Q 函数的参数都是离散型且很有限变量时,用上面的 table 方式表示 Q 函数就非常直观方便。
但是如果 state 和 action 的可能性都非常多,取两个值域的笛卡尔积可能构成非常庞大的集合,甚至如果 state 或 action 是连续变量时,构成的集合是无限大的。这种情况下则无法用 table 表示了。
这种情况下最方便的做法是采用深度神经网络(DNN)。
- 根据通用逼近定理,当 DNN 神经元足够多且层数足够时,可以拟合任意连续函数
- 当 DNN 结合 ReLU 等非线性函数后,可以拟合任意分段连续的函数
即用 DNN 来拟合我们的 Q 函数,如果 DNN 的各连接参数为 \(\theta\),则可简单表示为 \(Q(s,a,\theta) = N(s,a,\theta)\)
不过由于 DNN 的最后一层可以是多个输出神经元。如果 action 是离散的,则可以让最后一层的神经元和 action 的各个可能性一一对应。
- 这样仅需要一次 state 的输入即可得到此 state 下各个 action 的 Q 值。由于决策时确实需要知道所有 action 的 Q 值才行。所以这样建模后使用会比较方便
这样一来,则可表示为 \(Q(s,a,\theta) = N(s,\theta)[a]\)
- 取 action 对应的的那个输出结果
学习时采用同样思路的更新方法:\(N(s_t,\theta)[a] \leftarrow N(s_t, \theta)[a_t] + \alpha \left( r_t + \gamma \max_{a’} N(s_{t+1})[a’] – N(s_t)[a_t] \right)\)
- 不过不能像之前 Table Q Learning 那样,直接对 \(\theta\) 进行更新了。需要用反向传播,同步更新 DNN 所有参数。
- 事实上,table Q Learning 可以视作单层单分支的神经网络,反向传播的梯度都全作用到一个 \(\theta\) 参数上。
4.2 实践
这次用 OpenAI Gym 的经典强化学习环境 小车和杆 来尝试 CartPole-v1。
很经典的控制问题:一个小车上面有一根杆,小车只能在一条线上左右移动。目标是尽可能使上面的杆保持平衡不倒,平衡时间越长分越高(500为上限,会强制结束一次测试)
我们在让 Agent 学习的过程中,继续采用 ε-greedy 策略进行探索。不过 Q 函数替换为简单的 DNN 进行拟合。
详细实现过程的 Jupyter Notebook 见:https://github.com/Raytto/my_ml_study/blob/main/deep_q_study/s2_deep_q_agents.ipynb
训练结果:
- 可见在初期训练中,Agent 实力确实在上涨。但到一个阶段(也仅仅40分,上限500分)后就反而越来越差。
可能的原因
- 模型是用一个个样本进行训练的,可能导致新的单一样本对模型产生极大的参数干扰
4. 优化 Deep Q Learning 的表现
4.1 尝试添加 DropOut
参考 GPT 实现中使用过的 dropout 方法,训练中随机抛弃一部分神经元。以减少过拟合的可能性,提升模型的稳定性。
详细实现过程的 Jupyter Notebook 见:https://github.com/Raytto/my_ml_study/blob/main/deep_q_study/s3_deep_q_agents_dropout.ipynb
- 仅修改了 DNN 部分,对每一层神经元都添加了 dropout
训练结果:
可见有 dropout 确实比没 dropout 的基础版好了不少
- 最高达到120分
- 没有在某个时间后一直趴在低分区
但依旧不够好
- 没达到500的最高分左右,代表还是不能驾驭 CartPole 场景
- 训练曲线不稳定,不收敛
4.2 尝试添加记忆回放
为了进一步增强模型的稳定性。减少经验对参数的严重干扰。一种常用的方式是基于批量的经验进行学习。
- 对批次中各个样本求参数梯度后求平均。更高概率抵消掉噪音性质的梯度,留下对学习有价值的梯度。
但为了做到这一点,需要把历史经验保存下来,并每次学习的时候随机取一部分历史经验。
详细实现过程的 Jupyter Notebook 见:https://github.com/Raytto/my_ml_study/blob/main/deep_q_study/s4_deep_q_agents%20mem.ipynb
- Agent 中添加 memory 以记录经验
- 添加 sample_memory 以每次随机选取一批经验用于学习
- 修改 learn 方法,每次学习的时候基于一批次的经验(给每个数据添加一个 batch 维度,从单数据变成张量)
训练结果:
可见模型收敛性上确实好了不少,效果明显。但分数依旧不算很高(仅90左右)。
首先能想到的是 DNN 神经元不够,没能拟合场景需要的 Q*
4.3 尝试扩大模型规模
先尝试把模型每一层的神经元数量从128提升到256
详细实现过程的 Jupyter Notebook 见:https://github.com/Raytto/my_ml_study/blob/main/deep_q_study/s5_deep_q_agents%20256.ipynb
训练结果:
模型扩大到256后,结果确实好了一些,提升很有限。
继续尝试扩大,每一层加到512个神经元,并且加一层 LayerNorm 尝试稳定训练过程。
详细实现过程的 Jupyter Notebook 见:https://github.com/Raytto/my_ml_study/blob/main/deep_q_study/s6_deep_q_agents%20512_norm.ipynb
训练结果:
确实表现又有进一步,但并不太满意:
- 首先小车平衡单节的杆的问题本身也不算太复杂,却需要依赖如此多神经元,学习和实际推理效率不高。
- 学习曲线上依旧不是很稳定,甚至随时间还略有变差的趋势
4.4 尝试 Clipped Double Q-Learning
根据 Clip Double Q Learning 的论文:https://proceedings.mlr.press/v80/fujimoto18a/fujimoto18a.pdf
目前我们的 Q Learning 存在一个比较大的问题:
- 学习过程中,由于取 max Q 的操作,天然容易高估 Q 值
- 每次经验对 Q 的采样可能偏高也可能偏低,由于取 max ,则学习过程中偏低的会被忽略,偏高的会被关注,进而导致整体上 Q 值的高估。
Clipped Double Q 可以一定程度缓解高估的问题。
思路如下:
- 同时采用两个用于估计 Q 的 DNN 神经网络
- 在更新 Q 的过程中,选择下一步 max Q 时,对每个 a 而言,选用 min(Q1(s,a),Q2(s,a))。这样除非两个 Q 都高估,否则不会受高估影响
- 在策略选择时,由于没有更新那么严格,可以和学习时一致取 min,也可以取平均
详细实现过程的 Jupyter Notebook 见:https://github.com/Raytto/my_ml_study/blob/main/deep_q_study/s7_clip_double_q.ipynb
训练结果:
分数提升明显,已经可以达到500(最高值)了。
但是依旧有不收敛的问题。
Clip Double Q 论文中也有提到,采用 Clip Double Q 的方法能减少高估,但也并非完全消去高估的影响
4.5 尝试传统 Double Q Learning
论文见:https://cdn.aaai.org/ojs/10295/10295-13-13823-1-2-20201228.pdf
传统 Double Q Learning 也是为了缓解高估 Q 值的问题。
核心实现方式是:用一个网络来选取最优的行为,但用另一个网络来获取此行为的 Q 值进而更新
具体实现:https://github.com/Raytto/my_ml_study/blob/main/deep_q_study/s8_double_q.ipynb
训练结果:
可见 Double Q 表现已经挺不错的了。
4.6 尝试 Duel Q Learning
Duel Q Learning 论文:https://arxiv.org/pdf/1511.06581
核心思路:拆分 Q 值为当前状态的价值和动作价值的和
- 很多强化学习场景下,确实就是很多 state 比其他 state 更有优势,即忽略 action 的情况下 state 也一般是有独立价值的。拆分开可以让 state 的价值独立训练,更稳定不受干扰。
\(Q(s, a) = V(s) + \left(A(s, a) – \frac{1}{|A|} \sum_{a’} A(s, a’)\right)\)
- \(\frac{1}{|A|}\)是为了求s下所有action的价值平均。之所以减去平均值是为让 V(s) 可以更纯粹地表达状态的价值。且避免数据过分波动让 Q 值更稳定。有利于学习。
具体实现:https://github.com/Raytto/my_ml_study/blob/main/deep_q_study/s9_duel_q_agents.ipynb
训练情况:
虽然也不完美收敛,但看起来还是可以接受。
5. 渲染训练过程和测试
可以用 gym 自带的渲染方法查看训练过程和训练后的 Agent 玩游戏表现。
主要需要在创建环境时添加 render_mode=”human”
env = gym.make("CartPole-v1",)
即可实时看到运行过程。
不过要渲染的话,程序不能跑在 Jupyter Notebook 中,需要用普通的 py 文件运行。
代码详见:https://github.com/Raytto/my_ml_study/blob/main/deep_q_study/s10_render.py
- 除了添加渲染逻辑以外,还加入了模型参数的保存和加载
- 运行前需要设置模式变量,如:mode = “render_test_model”
- if mode == “train_new” :从新开始训练一个模型
- if mode == “train_continue” : 接着上次的训练继续训练模型
- if mode == “render_test_model” : 用训练好的模型尝试推理看表现
除了小车杆场景以外,个人还尝试了下 gym 中的 Acrobot-v1 场景
- 双节的连杆,控制第一根连杆,尝试将第二节杆的端点尽快甩到目标高度以上
- 花的时间越久分数越低
用 Dueling Q Learning 也可以达到 -70 分左右(基本还算不错的表现)
代码详见:https://github.com/Raytto/my_ml_study/blob/main/deep_q_study/s11_render%20acrobot.py
6. 总结
Q Learning 整体具有一些优势
- 方便理解和实践
- 可以利用离线数据来学习
- 结合了深度学习的优势:可以直接从图像像素中提取信息
但也有一些劣势:
- 对超参比较敏感,没选好模型可能就表现很差
- 如学习率、batch size、神经网络大小、epsilon 递减方式 等
- 不稳定,容易发散
- 即使用 Dueling Q Learning 学 CartPole,随学习次数增加,分数依旧会有一些波动
- 延迟奖励情境下不方便学习
- 部分场景下,奖励可能很久才出现,而 Q Learning 的折扣因子可能导致远期奖励被极大削弱