参数化值函数近似
Q-learning 算法中,我们存储每个状态下所有动作的 Q Q Q 值的表格。表格动作价值 Q ( s , a ) Q(s, a) Q ( s , a ) 表示在状态 s s s 下选择动作 a a a 然后继续遵循某一策略预期得到的期望回报。然而,这种用表格存储动作价值的做法只在环境的状态和动作都是离散的,并且空间都比较小的情况下适用
当处理大规模 MDP 或者连续状态空间时,采用的方式往往是参数化值函数估计的方式:
V θ ( s ) ≈ V π ( s ) , Q θ ( s , a ) ≈ Q π ( s , a ) V_\theta(s) \approx V^\pi(s), \quad Q_\theta(s,a) \approx Q^\pi(s,a)
V θ ( s ) ≈ V π ( s ) , Q θ ( s , a ) ≈ Q π ( s , a )
其中这时候就采用了从函数的级别去近似,而不是对维护一张表的级别近似,这里提出两个核心的问题来加深理解
为什么使用参数化模型
这里先要明确强化学习中收集的数据有两个特点:时变性与非稳定性
我们考虑时变性:策略在迭代过程中,由于策略的改变,占用度量也会发生改变。使用梯度下降法的参数化模型,可以有利于模型适应新的数据(梯度下降法的灾难性遗忘的问题反而变成了它的优势了 )。
同时参数化还有一个好处在于可以将可见的状态泛化到没见过的状态上 ,而参数化模型比树模型的泛化能力更强更适合
对于 off-policy 算法,参数化值函数是对于什么策略的估计
我们知道 off-policy 算法有两种策略,行为策略和目标策略,我们使用参数化值函数近似是为了在决策过程中对状态的估计。行为策略的目的是为了探索新的数据(explore),而目标策略才是真正需要去近似的东西。因此选择目标策略的估计而非行为策略
参数化模型的学习方式:
根据我们的学习目标来确定损失函数,学习目标:找到参数向量 θ \theta θ 最小化值函数近似值与真实值之间的均方误差,,优化方式为:
J ( θ ) = E π [ 1 2 ( V π ( s ) − V θ ( s ) ) 2 ] ⇒ − ∂ J ( θ ) ∂ θ = E π [ ( V π ( s ) − V θ ( s ) ) ∂ V θ ( s ) ∂ θ ] θ ← θ − α ∂ J ( θ ) ∂ θ = θ + α ( V π ( s ) − V θ ( s ) ) ∂ V θ ( s ) ∂ θ J(\theta) = \mathbb{E}_\pi \left[ \frac{1}{2} \left( V^{\pi}(s) - V_{\theta}(s) \right)^2 \right] \\
\Rightarrow -\frac{\partial J(\theta)}{\partial \theta} = \mathbb{E}_{\pi} \left[ \left( V^{\pi}(s) - V_{\theta}(s) \right) \frac{\partial V_{\theta}(s)}{\partial \theta} \right] \\
\theta \leftarrow \theta - \alpha \frac{\partial J(\theta)}{\partial \theta}
= \theta + \alpha \left( V^{\pi}(s) - V_{\theta}(s) \right) \frac{\partial V_{\theta}(s)}{\partial \theta}
J ( θ ) = E π [ 2 1 ( V π ( s ) − V θ ( s ) ) 2 ] ⇒ − ∂ θ ∂ J ( θ ) = E π [ ( V π ( s ) − V θ ( s ) ) ∂ θ ∂ V θ ( s ) ] θ ← θ − α ∂ θ ∂ J ( θ ) = θ + α ( V π ( s ) − V θ ( s ) ) ∂ θ ∂ V θ ( s )
此事还需要估计真实的 V π ( s ) V^\pi(s) V π ( s ) ,这时候又可以使用前面的两种估计方法:蒙特卡洛方法和时序差分方法:
θ ← { θ + α ( 1 n ∑ i = 1 n g i − V θ ( s ) ) ∂ V θ ( s ) ∂ θ θ + α ( r t + γ V θ ( s t + 1 ) − V θ ( s ) ) ∂ V θ ( s ) ∂ θ \theta \leftarrow
\left\{ \begin{array}{c}
\theta + \alpha \left( \frac{1}{n}\sum_{i=1}^n g_i - V_{\theta}(s) \right) \frac{\partial V_{\theta}(s)}{\partial \theta} \\
\theta + \alpha \left( r_t + \gamma V_\theta(s_{t+1}) - V_{\theta}(s) \right) \frac{\partial V_{\theta}(s)}{\partial \theta} \\
\end{array} \right.
θ ← { θ + α ( n 1 ∑ i = 1 n g i − V θ ( s ) ) ∂ θ ∂ V θ ( s ) θ + α ( r t + γ V θ ( s t + 1 ) − V θ ( s ) ) ∂ θ ∂ V θ ( s )
应用:Deep Q-Network(DQN)
Q-learning
由于 DQN 的环境是会实时改变的,在更新网络参数的同时目标也在不断地改变,这非常容易造成神经网络训练的不稳定性。为了解决这一问题,DQN 便使用了目标网络( target network)的思想: 既然在训练过程中 Q 网络的不断更新会导致目标不断发生改变,不如暂时先将 TD 误差目标中 的 Q 网络固定住。为了实现这一思想,我们需要利用两套 Q 网络
同时我们考虑神经网络输入什么输出什么:
一种方法是:神经网络的输入是状态 s s s 和动作 a a a ,然后输出一个标量,表示在状态 s s s 下 采取动作 a a a 能获得的价值
若动作是离散(有限)的,还可以只将状态 s s s 输入到神经网络中,使其同时输出每一 个动作的 Q Q Q 值。通常 DQN(以及 Q-learning)只能处理动作离散的情况 ,因为在函数 Q 的更新过 程中有 m a x a max_a m a x a 这一操作
优化过程如下,第 i i i 轮迭代中的损失函数:
L i ( θ i ) = E ( s , a , r , s ′ ) ∼ U ( D ) [ ( r + γ max a ′ Q ( s ′ , a ′ ; θ i − ) ) ⏟ 目标Q值 − Q ( s , a ; θ i ) ⏟ 预测Q值 ] 2 L_i(\theta_i) = \mathbb{E}_{(s,a,r,s') \sim U(D)} \left[ \underbrace{\left(r + \gamma \max_{a'} Q(s', a'; \theta_i^-)\right)}_{\text{目标Q值}} - \underbrace{Q(s, a; \theta_i)}_{\text{预测Q值}} \right]^2
L i ( θ i ) = E ( s , a , r , s ′ ) ∼ U ( D ) ⎣ ⎢ ⎢ ⎢ ⎡ 目标 Q 值 ( r + γ a ′ max Q ( s ′ , a ′ ; θ i − ) ) − 预测 Q 值 Q ( s , a ; θ i ) ⎦ ⎥ ⎥ ⎥ ⎤ 2
θ i \theta_i θ i 是第 i i i 轮迭代中将要更新的网络参数,通过标准的反向传播算法进行更新
θ i − \theta_i^- θ i − 是目标网络参数,仅在 θ i \theta_i θ i 每更新 C C C 步后进行更新
( s , a , r , s ′ ) ∼ U ( D ) (s,a,r,s') \sim U(D) ( s , a , r , s ′ ) ∼ U ( D ) :样本从经验池 D D D 中均匀抽样,这样做可以避免在近期经验上过拟合
有几点概念需要强调:
延迟更新:
目标网络参数 θ i − \theta_i^- θ i − 每 C C C 步才更新一次 ,其作用包括:
避免目标值剧烈波动:若目标网络与当前网络参数同步更新(θ i − = θ i \theta_i^- = \theta_i θ i − = θ i ),目标 Q Q Q 值会随每次参数调整剧烈变化,导致梯度方向频繁震荡。通过固定θ i − \theta_i^- θ i − 的短期参数,目标值在C C C 步内保持稳定,为优化提供平稳目标
缓解自举偏差:同步更新会导致目标网络与当前网络高度相关,可能放大Q值的高估误差(如“过乐观估计”)。延迟更新通过参数滞后性打破这种耦合,减少误差累积
经验回放:
在一般的有监督学习中,假设训练数据是独立同分布的,每一个训练数 据会被使用多次在原来的 Q-learning 算法中,每一个数据只会用来更新一次Q 值。同样地 DQN 算法采用了经验回放(experience replay)方法,具体做法为维护一个回放缓冲区,将每次从环境中采样得到的四元组数据(状态、动作、奖励、 下一状态)存储到回放缓冲区中,训练 Q 网络的时候再从回放缓冲区中随机采样若干数据来进行训练
使样本满足独立假设。在MDP 中交互采样得到的数据本身不满足独立假设,因为这一时刻的状态和上一时刻的状态有关。非独立同分布的数据对训练神经网络有很大的影响,会使神经网络拟合到最近训练的数据上。采用经验回放可以一定程度上打破样本之间的相关性
提高样本效率。每一个样本可以被使用多次
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 import randomimport gymnasium as gymimport numpy as npimport collectionsfrom tqdm import tqdmimport torchimport torch.nn.functional as Fimport matplotlib.pyplot as pltclass ReplayBuffer (object ): ''' 经验回放池 ''' def __init__ (self, capacity ): self.buffer = collections.deque(maxlen=capacity) def add (self, state, action, reward, next_state, done ): self.buffer.append((state, action, reward, next_state, done)) def sample (self, batch_size ): transitions = random.sample(self.buffer, batch_size) state, action, reward, next_state, done = zip (*transitions) return np.array(state), action, reward, np.array(next_state), done def size (self ): return len (self.buffer) class Qnet (torch.nn.Module): def __init__ (self, state_dim, hidden_dim, action_dim ): super (Qnet, self).__init__() self.fc1 = torch.nn.Linear(state_dim, hidden_dim) self.fc2 = torch.nn.Linear(hidden_dim, action_dim) def forward (self, x ): x = F.relu(self.fc1(x)) return self.fc2(x) class DQN (object ): ''' DQN算法 ''' def __init__ (self, state_dim, hidden_dim, action_dim, learning_rate, gamma, epsilon, target_update, device ): self.action_dim = action_dim self.q_net = Qnet(state_dim, hidden_dim, self.action_dim).to(device) self.target_q_net = Qnet(state_dim, hidden_dim, self.action_dim).to(device) self.optimizer = torch.optim.Adam(self.q_net.parameters(), lr=learning_rate) self.gamma = gamma self.epsilon = epsilon self.target_update = target_update self.count = 0 self.device = device def take_action (self, state ): if np.random.random() < self.epsilon: action = np.random.randint(self.action_dim) else : state = torch.tensor([state], dtype=torch.float ).to(self.device) action = self.q_net(state).argmax().item() return action def update (self, transition_dict ): states = torch.tensor(transition_dict['states' ], dtype=torch.float ).to(self.device) actions = torch.tensor(transition_dict['actions' ]).view(-1 , 1 ).to(self.device) rewards = torch.tensor(transition_dict['rewards' ], dtype=torch.float ).view(-1 , 1 ).to(self.device) next_states = torch.tensor(transition_dict['next_states' ], dtype=torch.float ).to(self.device) dones = torch.tensor(transition_dict['dones' ], dtype=torch.float ).view(-1 , 1 ).to(self.device) q_values = self.q_net(states).gather(1 , actions) max_next_q_values = self.target_q_net(next_states).max (1 )[0 ].view(-1 , 1 ) q_targets = rewards + self.gamma * max_next_q_values * (1 - dones) dqn_loss = torch.mean(F.mse_loss(q_values, q_targets)) self.optimizer.zero_grad() dqn_loss.backward() self.optimizer.step() if self.count % self.target_update == 0 : self.target_q_net.load_state_dict(self.q_net.state_dict()) self.count += 1 lr = 2e-3 num_episodes = 500 hidden_dim = 128 gamma = 0.98 epsilon = 0.01 target_update = 10 buffer_size = 10000 minimal_size = 500 batch_size = 64 device = torch.device("cuda" ) if torch.cuda.is_available() else torch.device("cpu" ) env_name = 'CartPole-v1' env = gym.make(env_name) random.seed(0 ) np.random.seed(0 ) torch.manual_seed(0 ) replay_buffer = ReplayBuffer(buffer_size) state_dim = env.observation_space.shape[0 ] action_dim = env.action_space.n agent = DQN(state_dim, hidden_dim, action_dim, lr, gamma, epsilon, target_update, device) return_list = []for i in range (10 ): with tqdm(total=int (num_episodes / 10 ), desc='Iteration %d' % i) as pbar: for i_episode in range (int (num_episodes / 10 )): episode_return = 0 state = env.reset()[0 ] done = False while not done: action = agent.take_action(state) next_state, reward, done, _, _ = env.step(action) replay_buffer.add(state, action, reward, next_state, done) state = next_state episode_return += reward if replay_buffer.size() > minimal_size: b_s, b_a, b_r, b_ns, b_d = replay_buffer.sample(batch_size) transition_dict = { 'states' : b_s, 'actions' : b_a, 'next_states' : b_ns, 'rewards' : b_r, 'dones' : b_d } agent.update(transition_dict) return_list.append(episode_return) if (i_episode + 1 ) % 10 == 0 : pbar.set_postfix({ 'episode' : '%d' % (num_episodes / 10 * i + i_episode + 1 ), 'return' : '%.3f' % np.mean(return_list[-10 :]) }) pbar.update(1 ) episodes_list = list (range (len (return_list))) plt.plot(episodes_list, return_list) plt.xlabel('Episodes' ) plt.ylabel('Returns' ) plt.title('DQN on {}' .format (env_name)) plt.show()
DQN的改进算法
Double DQN
普通的 DQN 算法通常会导致对 Q 值的过高估计(overestimation)。原因在与 Q 网络的自举(bootstrapping):
传统 DQN 优化的 TD 误差目标为:
r + γ max a ′ ∈ A Q w − ( s ′ , a ′ ) r+\gamma \max_{a'\in A}Q_{w^-}(s',a')
r + γ a ′ ∈ A max Q w − ( s ′ , a ′ )
其中,max a ′ Q w − ( s ′ , a ′ ) \max_{a'}Q_{w-}(s',a') max a ′ Q w − ( s ′ , a ′ ) 由参数为 w − w^- w − 的目标网络计算得出,其中 max \max max 操作可以视为由两部分构成:
选取状态 s ′ s' s ′ 下的最优动作 a ∗ = arg max a ′ Q a^*=\arg \max_{a'}Q a ∗ = arg max a ′ Q
再计算该动作对应的价值 Q w − ( s ′ , a ′ ) Q_{w^-}(s', a') Q w − ( s ′ , a ′ )
当这两部分共同采用同一套 Q 网络的时候,每次得到的都是神经网络中估算所有动作状态的最大值 ,考虑神经网络会不断累计正误差,DQN 的过高估计问题会非常严重,本质上的原因在于:
探索不足:探索不足导致产生的样本无法反应出概率分布: π : p ( s ′ , r ∣ s , a ) \pi:p\left( s',r {\mid} s,a \right) π : p ( s ′ , r ∣ s , a )
值函数存在方差: Q ( s , a ) Q(s,a) Q ( s , a ) 的更新存在方差,不可能一步就更新到目标值,它实际的形式应该是围绕目标值上下波动,但是波动的幅度越来越小
贪心思想将高估的值进行了传递 :训练过程中,实际的 Q ( s ′ , a ′ ) Q\left( s',a' \right) Q ( s ′ , a ′ ) 其实是一个估计值,它和最优的 Q ∗ ( s ′ , a ′ ) Q^{*}\left( s',a' \right) Q ∗ ( s ′ , a ′ ) 存在误差,存在一些动作对应的值被高估,而贪心思想又在更新 Q ( s , a ) Q(s,a) Q ( s , a ) 时使用了下一个状态 s ′ s' s ′ 中最大的 max a ′ Q ( s ′ , a ′ ) \max_{a'}Q\left( s',a' \right) max a ′ Q ( s ′ , a ′ ) ,恰好会使得那些被高估的动作的再更新时不断反向传播到之前的状态
为了解决这一问题,Double DQN 算法提出利用两套独立训练的神经网络来估算 max a ‘’ Q ∗ ( s ′ , a ′ ) \max\limits_{ a‘’}Q^*\left(s', a' \right) a ‘ ’ max Q ∗ ( s ′ , a ′ ) ,具体做法是利用 一套神经网络 Q ω Q_{\omega} Q ω 的输出来选取价值最大的动作,但在使用该动作的价值时,用另一套神经网 络 Q a r Q_{ar} Q a r 来计算该动作的价值 。这样即使其中一套神经网络的某个动作存在比较严重的过高估计问题,由于另一套神经网络的存在,这个动作最终使用的 Q Q Q 值也不会存在很大的过高估计
优化目标:
我们设选取动作网络参数为 w w w (训练网络),预测 Q 值网络参数为 w − w^- w − (目标网络),则通过之前的分析,我们的优化目标为:
r + γ Q w − ( s ′ , arg max a ′ Q w ( s ′ , a ′ ) ) r+\gamma Q_{w^-}(s', \arg\max_{a'}Q_{w}(s', a'))
r + γ Q w − ( s ′ , arg a ′ max Q w ( s ′ , a ′ ) )
Dueling DQN
Dueling DQN是 DQN的另一种改进算法,它在传统 DQN 的基础上只进行了微小的改动,但能大幅提升 DQN的表现
它从另外一个方面考虑 DQN 的问题:每次更新 Q 网络的时候,我们只是对网络中的最大值进行反向传播,而不管其他的 Q 值,这导致不同动作的 Q 值可能相差过大:
因此在 Dueling DQN 中,引入优势函数的概念:
将动作价值函数 Q Q Q 减去状态价值函数 V V V 的结果定义为优势函数 A A A ,即 A ( s , a ) = Q ( s , a ) − V ( s ) A(s,a)=Q(s,a)-V(s) A ( s , a ) = Q ( s , a ) − V ( s ) ,则在同一状态下,所有动作的优势值之和为 0(因为所有动作价值的期望就是这个状态的状态价值)
因此 Q 网络被建模为:
Q η , α , β ( s , a ) = V η , α ( s ) + A η , β ( s , a ) Q_{\eta, \alpha, \beta}(s,a)=V_{\eta, \alpha}(s)+A_{\eta, \beta}(s,a)
Q η , α , β ( s , a ) = V η , α ( s ) + A η , β ( s , a )
Dueling DQN 的问题:
对于公式 Q η , α , β ( s , a ) = V η , α ( s ) + A η , β ( s , a ) Q_{\eta,\alpha,\beta}\left( s,a\right) = V_{\eta,\alpha}\left( s \right) + A_{\eta,\beta}\left( s,a\right) Q η , α , β ( s , a ) = V η , α ( s ) + A η , β ( s , a ) ,它存在对于 V V V 值和 A A A 值建模不唯一性的问题。例如,对于同样的 Q Q Q 值,如果将 V V V 值加上任意大小的常数 C C C ,再将所有 A A A 值减去 C C C ,则得到的 Q Q Q 值依然不变,这就导致了训练的不稳定性 。为了解决这一问题,Dueling DQN 强制最优动作的优势函数的实际输出为 0 , 即:
Q_{\eta,\alpha,\beta}\left(s,a \right) = V_{\eta,\alpha}\left( {s} \right) + A_{\eta,\beta}\left( {s}, {a} \right) {-} {\max}\limits_{ {d}}A_{\eta,\beta}\left( {s}, a' \right)
此时 V ( s ) = max a Q ( s , a ) V(s) = \max_aQ(s,a) V ( s ) = max a Q ( s , a ) ,可以确保 V V V 值建模的唯一性。在实现过程中,我们还可以用平均操作代替最大化操作,即:
Q η , α , β ( s , a ) = V η , α ( s ) + A η , β ( s , a ) − 1 ∣ A ∣ ∑ d A η , β ( s , a ′ ) Q_{\eta,\alpha,\beta}\left(s,a\right) = V_{\eta,\alpha}\left( s\right) + A_{\eta,\beta}\left(s, a \right) - \frac{1}{|A|}\sum_dA_{\eta,\beta}\left(s,a' \right)
Q η , α , β ( s , a ) = V η , α ( s ) + A η , β ( s , a ) − ∣ A ∣ 1 d ∑ A η , β ( s , a ′ )
此时 V ( s ) = 1 ∣ A ∣ ∑ d Q ( s , a ′ ) V(s) = \frac{1}{|A|}\sum_d Q( s,a') V ( s ) = ∣ A ∣ 1 ∑ d Q ( s , a ′ ) ,虽然它不再满足贝尔曼最优方程,但实际应用时更加稳定