DQN 用 max Q(s',a') 計算目標值,等于在挑 Q 值最高的動作,但是這些動作中包括了那些因為估計噪聲而被高估的動作,素以就會產(chǎn)生過估計偏差,直接后果是訓(xùn)練不穩(wěn)定、策略次優(yōu)。
這篇文章要解決的就是這個問題,內(nèi)容包括:DQN 為什么會過估計、Double DQN 怎么把動作選擇和評估拆開、Dueling DQN 怎么分離狀態(tài)值和動作優(yōu)勢、優(yōu)先經(jīng)驗回放如何讓采樣更聰明,以及用 PyTorch 從頭實現(xiàn)這些改進。最后還會介紹一個 CleanRL 的專業(yè)實現(xiàn)。
![]()
過估計問題
DQN 的目標值如下:
y = r + γ·max?' Q(s', a'; θ?)
問題就在于,同一個網(wǎng)絡(luò)既負責選動作(a* = argmax Q),又負責評估這個動作的價值。Q 值本身是帶噪聲的估計所以有時候噪聲會讓差動作的 Q 值偏高,取 max 操作天然偏向選那些被高估的動作。
數(shù)學上有個直觀的解釋:
E[max(X?, X?, ..., X?)] ≥ max(E[X?], E[X?], ..., E[X?])
最大值的期望總是大于等于期望的最大值,這是凸函數(shù)的 Jensen 不等式。
過估計會導(dǎo)致收斂變慢,智能體把時間浪費在探索那些被高估的動作上。其次是策略質(zhì)量打折扣,高噪聲的動作可能比真正好的動作更受青睞。更糟的是過估計會不斷累積,導(dǎo)致訓(xùn)練發(fā)散。泛化能力也會受損——在狀態(tài)空間的噪聲區(qū)域,智能體會表現(xiàn)得過于自信。
Double DQN:把選擇和評估拆開
標準 DQN 一個網(wǎng)絡(luò)干兩件事:
a* = argmax?' Q(s', a'; θ?) # 選最佳動作
y = r + γ · Q(s', a*; θ?) # 評估這個動作(同一個網(wǎng)絡(luò))
Double DQN 用兩個網(wǎng)絡(luò),各管一件:
a* = argmax?' Q(s', a'; θ) # 用當前網(wǎng)絡(luò)選
y = r + γ · Q(s', a*; θ?) # 用目標網(wǎng)絡(luò)評估
當前網(wǎng)絡(luò)(θ)選動作,目標網(wǎng)絡(luò)(θ?)評估。兩個網(wǎng)絡(luò)的誤差不相關(guān)這樣最大化偏差就被打破了。
為什么有效呢?
假設(shè)當前網(wǎng)絡(luò)把動作 a 的價值估高了,目標網(wǎng)絡(luò)(參數(shù)不同)大概率不會犯同樣的錯。誤差相互獨立,傾向于抵消而非累加。
最通俗的解釋就是DQN 像是自己給菜打分、自己挑菜吃,這樣爛菜可能就混進來了,而Double DQN 讓朋友打分、你來挑,兩邊的誤差對沖掉了。
Standard DQN: E[Q(s, argmax? Q(s,a))] ≥ max? E[Q(s,a)] (有偏)
Double DQN: E[Q?(s, argmax? Q?(s,a))] ≈ max? E[Q(s,a)] (無偏)
從 DQN 到 Double DQN,只需要改一行:
# DQN 目標
next_q_values = target_network(next_states).max(1)[0]
target = rewards + gamma * next_q_values * (1 - dones)
# Double DQN 目標
next_actions = current_network(next_states).argmax(1) # <- 用當前網(wǎng)絡(luò)選
next_q_values = target_network(next_states).gather(1, next_actions.unsqueeze(1)) # <- 用目標網(wǎng)絡(luò)評估
target = rewards + gamma * next_q_values.squeeze() * (1 - dones)
就這一行改動極小,效果卻很明顯。
實現(xiàn):Double DQN
擴展 DQN Agent
class DoubleDQNAgent(DQNAgent):
"""
Double DQN: 通過解耦動作選擇和評估來減少過估計偏差。
"""
def __init__(self, *args, **kwargs):
"""
初始化 Double DQN agent。
從 DQN 繼承所有內(nèi)容,只改變目標計算。
"""
super().__init__(*args, **kwargs)
def update(self) -> Dict[str, float]:
"""
執(zhí)行 Double DQN 更新。
Returns:
metrics: 訓(xùn)練指標
"""
if len(self.replay_buffer) < self.batch_size:
return {}
# 采樣批次
states, actions, rewards, next_states, dones = self.replay_buffer.sample(
self.batch_size
)
states = states.to(self.device)
actions = actions.to(self.device)
rewards = rewards.to(self.device)
next_states = next_states.to(self.device)
dones = dones.to(self.device)
# 當前 Q 值 Q(s,a;θ)
current_q_values = self.q_network(states).gather(1, actions.unsqueeze(1))
# Double DQN 目標計算
with torch.no_grad():
# 使用當前網(wǎng)絡(luò)選擇動作
next_actions = self.q_network(next_states).argmax(1)
# 使用目標網(wǎng)絡(luò)評估動作
next_q_values = self.target_network(next_states).gather(
1, next_actions.unsqueeze(1)
).squeeze()
# 計算目標
target_q_values = rewards + (1 - dones) * self.gamma * next_q_values
# 計算損失
loss = F.mse_loss(current_q_values.squeeze(), target_q_values)
# 梯度下降
self.optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(self.q_network.parameters(), max_norm=10.0)
self.optimizer.step()
self.training_step += 1
return {
'loss': loss.item(),
'q_mean': current_q_values.mean().item(),
'q_std': current_q_values.std().item(),
'target_q_mean': target_q_values.mean().item()
}
訓(xùn)練函數(shù):
def train_double_dqn(
env_name: str,
n_episodes: int = 1000,
max_steps: int = 500,
train_freq: int = 1,
eval_frequency: int = 50,
eval_episodes: int = 10,
verbose: bool = True,
**kwargs
) -> Tuple:
"""
訓(xùn)練 Double DQN agent(使用 DoubleDQNAgent 而不是 DQNAgent)。
"""
# 與 train_dqn 相同但使用 DoubleDQNAgent
env = gym.make(env_name)
eval_env = gym.make(env_name)
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.n
# 使用 DoubleDQNAgent
agent = DoubleDQNAgent(
state_dim=state_dim,
action_dim=action_dim,
**kwargs
)
# 訓(xùn)練循環(huán)(與 DQN 相同)
stats = {
'episode_rewards': [],
'episode_lengths': [],
'losses': [],
'q_values': [],
'target_q_values': [],
'eval_rewards': [],
'eval_episodes': [],
'epsilons': []
}
print(f"Training Double DQN on {env_name}")
print(f"State dim: {state_dim}, Action dim: {action_dim}")
print("=" * 70)
for episode in range(n_episodes):
state, _ = env.reset()
episode_reward = 0
episode_length = 0
episode_metrics = []
for step in range(max_steps):
action = agent.select_action(state, training=True)
next_state, reward, terminated, truncated, _ = env.step(action)
done = terminated or truncated
agent.store_transition(state, action, reward, next_state, done)
if step % train_freq == 0:
metrics = agent.update()
if metrics:
episode_metrics.append(metrics)
episode_reward += reward
episode_length += 1
state = next_state
if done:
break
# 更新目標網(wǎng)絡(luò)
if (episode + 1) % kwargs.get('target_update_freq', 10) == 0:
agent.update_target_network()
agent.decay_epsilon()
# 存儲統(tǒng)計信息
stats['episode_rewards'].append(episode_reward)
stats['episode_lengths'].append(episode_length)
stats['epsilons'].append(agent.epsilon)
if episode_metrics:
stats['losses'].append(np.mean([m['loss'] for m in episode_metrics]))
stats['q_values'].append(np.mean([m['q_mean'] for m in episode_metrics]))
stats['target_q_values'].append(np.mean([m['target_q_mean'] for m in episode_metrics]))
# 評估
if (episode + 1) % eval_frequency == 0:
eval_reward = evaluate_dqn(eval_env, agent, eval_episodes)
stats['eval_rewards'].append(eval_reward)
stats['eval_episodes'].append(episode + 1)
if verbose:
avg_reward = np.mean(stats['episode_rewards'][-50:])
avg_loss = np.mean(stats['losses'][-50:]) if stats['losses'] else 0
avg_q = np.mean(stats['q_values'][-50:]) if stats['q_values'] else 0
print(f"Episode {episode + 1:4d} | "
f"Reward: {avg_reward:7.2f} | "
f"Eval: {eval_reward:7.2f} | "
f"Loss: {avg_loss:7.4f} | "
f"Q: {avg_q:6.2f} | "
f"ε: {agent.epsilon:.3f}")
env.close()
eval_env.close()
print("=" * 70)
print("Training complete!")
return agent, stats
LunarLander-v3
# 訓(xùn)練 Double DQN
if __name__ == "__main__":
device = 'cuda' if torch.cuda.is_available() else 'cpu'
agent_ddqn, stats_ddqn = train_double_dqn(
env_name='LunarLander-v3',
n_episodes=4000,
max_steps=1000,
learning_rate=5e-4,
gamma=0.99,
epsilon_start=1.0,
epsilon_end=0.01,
epsilon_decay=0.9995,
buffer_capacity=100000,
batch_size=128,
target_update_freq=20,
train_freq=4,
eval_frequency=100,
eval_episodes=10,
hidden_dims=[256, 256],
device=device,
verbose=True
)
# 保存模型
agent_ddqn.save('doubledqn_lunar_lander.pth')
輸出:
Training Double DQN on LunarLander-v3
State dim: 8, Action dim: 4
======================================================================
Episode 100 | Reward: -155.24 | Eval: -885.72 | Loss: 52.9057 | Q: 0.20 | ε: 0.951
Episode 200 | Reward: -148.85 | Eval: -85.94 | Loss: 37.2449 | Q: 2.14 | ε: 0.905
Episode 300 | Reward: -111.61 | Eval: -172.48 | Loss: 37.4279 | Q: 3.52 | ε: 0.861
Episode 400 | Reward: -99.21 | Eval: -198.43 | Loss: 41.5296 | Q: 8.15 | ε: 0.819
Episode 500 | Reward: -80.75 | Eval: -103.26 | Loss: 56.2701 | Q: 11.70 | ε: 0.779
...
Episode 3200 | Reward: 102.04 | Eval: 159.71 | Loss: 16.5263 | Q: 27.94 | ε: 0.202
Episode 3300 | Reward: 140.37 | Eval: 191.79 | Loss: 22.5564 | Q: 29.81 | ε: 0.192
Episode 3400 | Reward: 114.08 | Eval: 269.40 | Loss: 23.2846 | Q: 32.40 | ε: 0.183
Episode 3500 | Reward: 166.33 | Eval: 244.32 | Loss: 21.8558 | Q: 32.51 | ε: 0.174
Episode 3600 | Reward: 150.80 | Eval: 265.42 | Loss: 21.6430 | Q: 33.18 | ε: 0.165
Episode 3700 | Reward: 148.59 | Eval: 239.56 | Loss: 23.8328 | Q: 34.65 | ε: 0.157
Episode 3800 | Reward: 162.82 | Eval: 233.36 | Loss: 28.3445 | Q: 37.46 | ε: 0.149
Episode 3900 | Reward: 177.70 | Eval: 259.99 | Loss: 36.2971 | Q: 40.22 | ε: 0.142
Episode 4000 | Reward: 156.60 | Eval: 251.17 | Loss: 46.7266 | Q: 42.15 | ε: 0.135
======================================================================
Training complete!
Dueling DQN:分離值和優(yōu)勢
很多狀態(tài)下,選哪個動作其實差別不大。CartPole 里桿子剛好平衡時,向左向右都行;開車走直線方向盤微調(diào)的結(jié)果差不多;LunarLander 離地面還遠的時候,引擎怎么噴影響也有限。
標準 DQN 對每個動作單獨學 Q(s,a),把網(wǎng)絡(luò)容量浪費在冗余信息上。Dueling DQN 的思路是把 Q 拆成兩部分:V(s) 表示"這個狀態(tài)本身值多少",A(s,a) 表示"這個動作比平均水平好多少"。
架構(gòu)如下
標準 DQN:
Input -> Hidden Layers -> Q(s,a?), Q(s,a?), ..., Q(s,a?)
Dueling DQN:
|-> Value Stream -> V(s)
Input -> Shared Layers |
|-> Advantage Stream -> A(s,a?), A(s,a?), ..., A(s,a?)
Q(s,a) = V(s) + (A(s,a) - mean(A(s,·)))
為什么要減去均值?不減的話,任何常數(shù)加到 V 再從 A 減掉,得到的 Q 完全一樣,網(wǎng)絡(luò)學不出唯一解。
數(shù)學表達如下:
Q(s,a) = V(s) + A(s,a) - (1/|A|)·Σ?' A(s,a')
也可以用 max 代替 mean:
Q(s,a) = V(s) + A(s,a) - max?' A(s,a')
實踐中 max 版本有時效果更好。
舉個例子:V(s) = 10,好動作的 A 是 +5,差動作的 A 是 -3,平均優(yōu)勢 = (+5-3)/2 = +1。那么 Q(s, 好動作) = 10 + 5 - 1 = 14,Q(s, 差動作) = 10 - 3 - 1 = 6。
實現(xiàn)
class DuelingQNetwork(nn.Module):
"""
Dueling DQN 架構(gòu),分離值和優(yōu)勢。
理論: Q(s,a) = V(s) + A(s,a) - mean(A(s,·))
"""
def __init__(
self,
state_dim: int,
action_dim: int,
hidden_dims: List[int] = [128, 128]
):
"""
初始化 Dueling Q 網(wǎng)絡(luò)。
Args:
state_dim: 狀態(tài)空間維度
action_dim: 動作數(shù)量
hidden_dims: 共享層大小
"""
super(DuelingQNetwork, self).__init__()
self.state_dim = state_dim
self.action_dim = action_dim
# 共享特征提取器
shared_layers = []
input_dim = state_dim
for hidden_dim in hidden_dims:
shared_layers.append(nn.Linear(input_dim, hidden_dim))
shared_layers.append(nn.ReLU())
input_dim = hidden_dim
self.shared_network = nn.Sequential(*shared_layers)
# 值流: V(s) = 狀態(tài)的標量值
self.value_stream = nn.Sequential(
nn.Linear(hidden_dims[-1], 128),
nn.ReLU(),
nn.Linear(128, 1)
)
# 優(yōu)勢流: A(s,a) = 每個動作的優(yōu)勢
self.advantage_stream = nn.Sequential(
nn.Linear(hidden_dims[-1], 128),
nn.ReLU(),
nn.Linear(128, action_dim)
)
# 初始化權(quán)重
self.apply(self._init_weights)
def _init_weights(self, module):
"""初始化網(wǎng)絡(luò)權(quán)重。"""
if isinstance(module, nn.Linear):
nn.init.kaiming_normal_(module.weight, nonlinearity='relu')
nn.init.constant_(module.bias, 0.0)
def forward(self, state: torch.Tensor) -> torch.Tensor:
"""
通過 dueling 架構(gòu)的前向傳播。
Args:
state: 狀態(tài)批次, 形狀 (batch_size, state_dim)
Returns:
q_values: 所有動作的 Q(s,a), 形狀 (batch_size, action_dim)
"""
# 共享特征
features = self.shared_network(state)
# 值: V(s) -> 形狀 (batch_size, 1)
value = self.value_stream(features)
# 優(yōu)勢: A(s,a) -> 形狀 (batch_size, action_dim)
advantages = self.advantage_stream(features)
# 組合: Q(s,a) = V(s) + A(s,a) - mean(A(s,·))
q_values = value + advantages - advantages.mean(dim=1, keepdim=True)
return q_values
def get_action(self, state: np.ndarray, epsilon: float = 0.0) -> int:
"""
使用 ε-greedy 策略選擇動作。
"""
if random.random() < epsilon:
return random.randint(0, self.action_dim - 1)
else:
with torch.no_grad():
state_tensor = torch.FloatTensor(state).unsqueeze(0).to(
next(self.parameters()).device
)
q_values = self.forward(state_tensor)
return q_values.argmax(dim=1).item()
Dueling 架構(gòu)的好處:在動作影響不大的狀態(tài)下學得更好,梯度流動更通暢所以收斂更快,值估計也更穩(wěn)健。
還可以把兩種改進疊在一起,做成Double Dueling DQN
class DoubleDuelingDQNAgent(DoubleDQNAgent):
"""
結(jié)合 Double DQN 和 Dueling DQN 的智能體。
"""
def __init__(
self,
state_dim: int,
action_dim: int,
hidden_dims: List[int] = [128, 128],
**kwargs
):
"""
初始化 Double Dueling DQN 智能體。
使用 DuelingQNetwork 而不是標準 QNetwork。
"""
# 暫不調(diào)用 super().__init__()
# 我們需要以不同方式設(shè)置網(wǎng)絡(luò)
self.state_dim = state_dim
self.action_dim = action_dim
self.gamma = kwargs.get('gamma', 0.99)
self.batch_size = kwargs.get('batch_size', 64)
self.target_update_freq = kwargs.get('target_update_freq', 10)
self.device = torch.device(kwargs.get('device', 'cpu'))
# 探索
self.epsilon = kwargs.get('epsilon_start', 1.0)
self.epsilon_end = kwargs.get('epsilon_end', 0.01)
self.epsilon_decay = kwargs.get('epsilon_decay', 0.995)
# 使用 Dueling 架構(gòu)
self.q_network = DuelingQNetwork(
state_dim, action_dim, hidden_dims
).to(self.device)
self.target_network = DuelingQNetwork(
state_dim, action_dim, hidden_dims
).to(self.device)
self.target_network.load_state_dict(self.q_network.state_dict())
self.target_network.eval()
# 優(yōu)化器
learning_rate = kwargs.get('learning_rate', 1e-3)
self.optimizer = torch.optim.Adam(self.q_network.parameters(), lr=learning_rate)
# 回放緩沖區(qū)
buffer_capacity = kwargs.get('buffer_capacity', 100000)
self.replay_buffer = ReplayBuffer(buffer_capacity)
# 統(tǒng)計
self.episode_count = 0
self.training_step = 0
# update() 方法繼承自 DoubleDQNAgent
優(yōu)先經(jīng)驗回放
不是所有經(jīng)驗都同等有價值。TD 誤差大的轉(zhuǎn)換說明預(yù)測偏離現(xiàn)實,能學到東西;TD 誤差小的轉(zhuǎn)換說明已經(jīng)學得差不多了再采到也沒多大用。
均勻采樣把所有轉(zhuǎn)換一視同仁,浪費了學習機會。優(yōu)先經(jīng)驗回放的思路是:讓重要的轉(zhuǎn)換被采到的概率更高。
優(yōu)先級怎么算
p? = |δ?| + ε
其中:
δ? = r + γ·max Q(s',a') - Q(s,a) (TD 誤差)
ε = 小常數(shù),保證所有轉(zhuǎn)換都有被采到的可能
采樣概率:
P(i) = p?^α / Σ? p?^α
α 控制優(yōu)先化程度:
α = 0 -> 退化成均勻采樣
α = 1 -> 完全按優(yōu)先級比例采樣
優(yōu)先采樣改了數(shù)據(jù)分布,會引入偏差。所以解決辦法是用重要性采樣比率來加權(quán)更新:
w? = (N · P(i))^(-β)
β 控制校正力度:
β = 0 -> 不校正
β = 1 -> 完全校正
通常 β 從 0.4 開始,隨訓(xùn)練逐漸增大到 1.0。
實現(xiàn)
class PrioritizedReplayBuffer:
"""
優(yōu)先經(jīng)驗回放緩沖區(qū)。
理論: 按 TD 誤差比例采樣轉(zhuǎn)換。
我們可以從中學到更多的轉(zhuǎn)換會被更頻繁地采樣。
"""
def __init__(self, capacity: int, alpha: float = 0.6, beta: float = 0.4):
"""
Args:
capacity: 緩沖區(qū)最大容量
alpha: 優(yōu)先化指數(shù)(0=均勻, 1=比例)
beta: 重要性采樣指數(shù)(退火到 1.0)
"""
self.capacity = capacity
self.alpha = alpha
self.beta = beta
self.beta_increment = 0.001 # 隨時間退火 beta
self.buffer = []
self.priorities = np.zeros(capacity, dtype=np.float32)
self.position = 0
def push(self, state, action, reward, next_state, done):
"""
以最大優(yōu)先級添加轉(zhuǎn)換。
理論: 新轉(zhuǎn)換獲得最大優(yōu)先級(會很快被采樣)。
它們的實際優(yōu)先級在首次 TD 誤差計算后更新。
"""
max_priority = self.priorities.max() if self.buffer else 1.0
if len(self.buffer) < self.capacity:
self.buffer.append((state, action, reward, next_state, done))
else:
self.buffer[self.position] = (state, action, reward, next_state, done)
self.priorities[self.position] = max_priority
self.position = (self.position + 1) % self.capacity
def sample(self, batch_size: int):
"""
按優(yōu)先級比例采樣批次。
Returns:
batch: 采樣的轉(zhuǎn)換
indices: 采樣轉(zhuǎn)換的索引(用于優(yōu)先級更新)
weights: 重要性采樣權(quán)重
"""
if len(self.buffer) == self.capacity:
priorities = self.priorities
else:
priorities = self.priorities[:len(self.buffer)]
# 計算采樣概率
probs = priorities ** self.alpha
probs /= probs.sum()
# 采樣索引
indices = np.random.choice(len(self.buffer), batch_size, p=probs, replace=False)
# 獲取轉(zhuǎn)換
batch = [self.buffer[idx] for idx in indices]
# 計算重要性采樣權(quán)重
total = len(self.buffer)
weights = (total * probs[indices]) ** (-self.beta)
weights /= weights.max() # 歸一化以保持穩(wěn)定性
# 退火 beta
self.beta = min(1.0, self.beta + self.beta_increment)
# 轉(zhuǎn)換為 tensor
states, actions, rewards, next_states, dones = zip(*batch)
states = torch.FloatTensor(np.array(states))
actions = torch.LongTensor(actions)
rewards = torch.FloatTensor(rewards)
next_states = torch.FloatTensor(np.array(next_states))
dones = torch.FloatTensor(dones)
weights = torch.FloatTensor(weights)
return (states, actions, rewards, next_states, dones), indices, weights
def update_priorities(self, indices, td_errors):
"""
根據(jù) TD 誤差更新優(yōu)先級。
Args:
indices: 采樣轉(zhuǎn)換的索引
td_errors: 那些轉(zhuǎn)換的 TD 誤差
"""
for idx, td_error in zip(indices, td_errors):
self.priorities[idx] = abs(td_error) + 1e-6
def __len__(self):
return len(self.buffer)
生產(chǎn)環(huán)境會用 sum-tree 數(shù)據(jù)結(jié)構(gòu),采樣復(fù)雜度是 O(log N) 而不是這里的 O(N)。這個簡化版本以可讀性為優(yōu)先。
DQN 變體對比
幾個變體各自解決什么問題呢?
DQN 是基線,用單一網(wǎng)絡(luò)選動作、評估動作。它引入了目標網(wǎng)絡(luò)來穩(wěn)定"移動目標"問題,但容易過估計 Q 值,噪聲讓智能體去追逐根本不存在的"幽靈獎勵"。
Double DQN 把選和評拆開。在線網(wǎng)絡(luò)選動作,目標網(wǎng)絡(luò)評估價值。實測下來能有效壓低不切實際的 Q 值,學習曲線明顯更平滑。
Dueling DQN 換了網(wǎng)絡(luò)架構(gòu),單獨學 V(s) 和 A(s,a)。它的核心認知是:很多狀態(tài)下具體動作的影響不大。在 LunarLander 這種存在大量"冗余動作"的環(huán)境里,樣本效率提升明顯——不用為每次引擎脈沖都重新學狀態(tài)值。
Double Dueling DQN 把兩邊的好處結(jié)合起來,既減少估計噪聲,又提高表示效率。實測中這個組合最穩(wěn)健,達到峰值性能的速度和可靠性都優(yōu)于單一改進。
實踐建議
變體選擇對比
![]()
Double DQN 跑得比 DQN 還差?可能是訓(xùn)練不夠長(Double DQN 起步偶爾慢一點),或者目標網(wǎng)絡(luò)更新太頻繁,或者學習率偏高。這時可以將訓(xùn)練時間翻倍,target_update_freq 調(diào)大,學習率砍 2-5 倍。
Dueling 架構(gòu)沒帶來改善?可能是環(huán)境本身不適合(所有狀態(tài)都很關(guān)鍵),或者網(wǎng)絡(luò)太小,或者值流/優(yōu)勢流太淺。需要對網(wǎng)絡(luò)加寬加深,確認環(huán)境里確實有"中性"狀態(tài)。
PER 導(dǎo)致不穩(wěn)定?可能是 β 退火太快、α 設(shè)太高、重要性采樣權(quán)重沒歸一化。可以減慢 β 增量、α 降到 0.4-0.6、確認權(quán)重做了歸一化。
首選 Double DQN 起步,代碼改動極小,收益明確,沒有額外復(fù)雜度。
什么時候加 Dueling:狀態(tài)值比動作優(yōu)勢更重要的環(huán)境,大量狀態(tài)下動作值差不多,需要更快收斂。
什么時候加 PER:樣本效率至關(guān)重要,有算力預(yù)算(PER 比均勻采樣慢),獎勵稀疏(幫助關(guān)注少見的成功經(jīng)驗)。
最后Rainbow 把六項改進疊在一起:Double DQN、Dueling DQN、優(yōu)先經(jīng)驗回放、多步學習(n-step returns)、分布式 RL(C51)、噪聲網(wǎng)絡(luò)(參數(shù)空間探索)。
多步學習把 1-step TD 換成 n-step 回報:
# 1-step TD:
y = r? + γ·max Q(s???, a)
# n-step:
y = r? + γ·r??? + γ2·r??? + ... + γ?·max Q(s???, a)
好處是信用分配更清晰,學習更快。
小結(jié)
這篇文章從 DQN 的過估計問題講起,沿著 Double DQN、Dueling 架構(gòu)、優(yōu)先經(jīng)驗回放等等介紹下來,每種改進對應(yīng)一個具體的失敗模式:max 算子的偏差、低效的狀態(tài)-動作表示、浪費的均勻采樣。
從頭實現(xiàn)這些方法,能搞清楚它們?yōu)槭裁从行В缓芏?高級" RL 算法不過是簡單想法的組合,理解這些想法本身才是真正可擴展的東西。
https://avoid.overfit.cn/post/4c5835f419d840b0acb0a1eb72f92b6f
作者: Jugal Gajjar
特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺“網(wǎng)易號”用戶上傳并發(fā)布,本平臺僅提供信息存儲服務(wù)。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.