← All tutorials

Multi-Agent Reinforcement Learning

Two rackets share one DDPG actor and critic, so each trains against an opponent that keeps improving.

On this page

1 The tennis environment 2 Self-play DDPG 3 Non-stationarity from a learning opponent 4 Frozen versus learning opponent 5 The training curve 6 How the reward shapes the curve 7 The trained rackets 8 Self-play DDPG in practice 9 MADDPG 10 DQN, DDPG and self-play compared 11 References

Single-agent RL, as on the DQN page (a banana-collecting agent) and the DDPG page (the Reacher arm), assumes the world's rules stay fixed while the agent learns. In tennis each racket's world contains the other racket, which keeps learning.

The running example is the TennisGym repo, the collaboration and competition project from Udacity's deep RL nanodegree. A gym, in RL usage, is the environment an agent trains in: it receives actions and returns observations and rewards. This one is built with Unity ML-Agents.

In self-play one DDPG actor and critic play both sides of the net. Self-play gives an opponent that gets harder exactly as fast as the learner improves, but it also makes old experience go out of date: a lesson learned against yesterday's opponent may not hold today. Self-play of this kind trained AlphaZero for Go, chess and shogi (Model-Based RL, section 2) and OpenAI Five for Dota 2.

Both rackets act every step, but learning runs in bursts (Figure 1).

12 rackets act, 2 transitions stored2learn from random past steps3slow copies catch up a little064128192256environment step
Figure 1. ① Acting never stops; ② learning and ③ slow target copies run every 64 steps.

1 The tennis environment

a racket sees the ball, never the other racket net ball Racket A Racket B +0.1 per clear over the net -0.01 per drop or out of bounds

Figure 2. Each racket reads 8 numbers and outputs 2 actions.

A rally runs until a racket misses: the ball hits the ground or goes out. Each racket sees 8 numbers, the position and velocity of the ball and of itself, and answers with 2 continuous actions, scoot toward or away from the net and jump. The gym stacks three consecutive snapshots, so the actor gets 24 numbers per step and sees the ball's motion (Figure 3).

t − 2t − 1t3 snapshots × 8 numbers= 24 numbers insharedactorscootjump−1 to 1−1 to 1
Figure 3. Three snapshots of 8 numbers make the actor's 24 inputs.

brain_name keys the rackets in the output of env, states is (2, 24), one row per racket, and each step returns one reward and one done flag per racket; the flag is 1 when that racket's rally ends.

import numpy as np

N_RACKETS, N_ACTIONS = 2, 2
out = env.reset(train_mode=True)
info = out[brain_name]
states = info.vector_observations
shape = (N_RACKETS, N_ACTIONS)
rand = np.random.uniform
actions = rand(-1, 1, shape)
out = env.step(actions)
info = out[brain_name]
rewards = info.rewards
dones = info.local_done
  1. 1Resets the court and reads both rackets' states.
  2. 2Sends one random action per racket and reads rewards and done flags.

The other racket is not in the observation; the ball is the only signal from it. This is partial observability: the agent cannot see what it most needs to predict. The actions are continuous, so the actor-critic method of the DDPG page applies.

In part of one episode racket A clears the net 3 times and drops once, and racket B clears once and drops twice (Figure 4).

Toy numbers, part of one episode0.29Racket A3 clears, 1 drop0.08Racket B1 clear, 2 dropssolve bar 0.5best
Figure 4. Each racket's clears and drops make its score; the better one counts.
Each racket's episode score adds 0.1 for every clear over the net and subtracts 0.01 for every drop.
score = 0.1·clears − 0.01·drops
Every clear of the net pays 0.1, and every drop, a ball let fall or hit out of bounds, costs 0.01. A racket earns 0.1 only for balls it hits over the net itself. The repo's loop keeps serving new rallies until the 11th done step, or 2,500 steps, ends the episode. The critic of Section 2 learns from these same rewards.
The tennis task counts as solved when the better racket's score, averaged over the last 100 episodes, reaches 0.5.
best = max(scoreA, scoreB)
scoreA and scoreB are the two rackets' episode scores from the score equation above. The max keeps the better racket in each episode, 0.29 in Figure 4, because one good player is enough and the other is the sparring partner.
1100Σepisodes best ≥ 0.5
The fraction and the sum average best over the last 100 episodes, so one lucky episode cannot pass the bar. At 0.1 a clear, 0.5 means about five of its own clears per episode for the better racket. Section 5 plots single episodes against this bar.

2 Self-play DDPG

One actor and one critic are shared by both rackets. Both rackets' transitions go into one replay buffer. Each racket treats its opponent as part of the environment, even though the same networks control the opponent. This simple setup solved the task (Figure 12), with no hand-tuned opponent schedule.

Here o is one racket's observation, its 24 numbers, playing the role s plays on the DDPG page; a is the racket's 2 actions. Figure 5 shows both rackets asking the same actor.

Toy numbers, one shared actorracket A sees oAracket B sees oBone actor μscoot 0.2, jump 0.5scoot −0.4, jump 0.1
Figure 5. One actor gives each racket its own answer.
One actor answers both rackets, each from its own side of the net.
ai = μ(oi)
i names racket A or racket B, oi is that racket's 24 numbers and ai its scoot and jump, kept in [−1, 1] by the actor's tanh. There is one μ, the DDPG actor a = μ(s) (DDPG) with an index, so racket A's opponent is the same actor; that is self-play. The update equations below train this actor, and Section 9 splits it per racket.

The recipe is DDPG from Training DDPG: Target Networks, Replay and Noise, with the settings in Figure 9. Both networks use batch norm, which keeps each layer's inputs on a steady scale, and gradient clipping, which caps each update; the critic also uses weight decay, a small pull of every weight toward zero.

The update rests on three ideas from Deep Q-Networks. A replay buffer stores past steps, and each update trains on a random batch of them. The critic learns toward a target, the reward just earned plus a discounted score of the next step (the Bellman equation). Target networks, slow copies of the critic and actor, compute that target so it does not jump with every update. One stored step of racket A shows the update (Figure 6).

Toy numbers, one step of racket A, no noisereward 0.1next stepscored 0.300.1 + 0.99 × 0.30= 0.397δ = 0.147critic on A’s action 0.25target 0.397
Figure 6. The target sits 0.147 above the critic's current score.
Both rackets feed one DDPG update: the critic learns what an observation and action are worth, and the actor shifts toward actions the critic rates higher.
y = r + γ·Q′(o′, μ′(o′))
The target y is a better guess of what racket A's action was worth: the 0.1 it just earned plus 0.99 times what the slow critic expects from the next step (0.30), so y = 0.397 in Figure 6. r is the reward of Section 1. When the racket's rally ends (done = 1), the γ term is dropped and y is just r. γ = 0.99 makes a reward 10 steps away count 0.90 and one 100 steps away 0.37, so long rallies still count, but less. Primes on Q and μ mark the target networks; the prime on o marks the next observation. It is the DQN target with the max replaced by the target actor (the DDPG critic target): μ′ picks the next action and Q′ scores it.
loss = E[(y − Q(o,a))²]
Q(o,a) is the live critic's Q-value for the stored action, the discounted sum of rewards it expects from here. y − Q is δ, the TD error of Deep Q-Networks, 0.147 in Figure 6, and the loss pulls Q toward y. E averages over a batch of 128 from the one buffer both rackets fill.
J = E[Q(o, μ(o))]
J is the critic's average score for the actor's own actions on stored observations, and the actor climbs it (the DDPG actor update; Silver et al., 2014); the code in Section 8 minimizes -q.mean(), the negative of J. With toy numbers, if the critic rates jump 0.6 at 0.27, above the 0.25 of jump 0.5, the actor step raises the jump. The soft-update equation below slows the primes, Section 3 shows each racket's climb moving the other's target, and Section 9 widens the critic to both rackets.
Racket A reads 24 numbers Racket B reads 24 numbers Shared actor (256-256-128) sets 2 actions per racket Racket A sets 2 actions Racket B sets 2 actions Shared replay buffer 1,000,000 transitions every 64 steps 128 updates of 128 each Critic judges, actor follows targets follow slowly the same actor plays the other racket every update also changes the opponent

Figure 7. One training step; both rackets share one actor and one critic.

Figure 8 shows one weight of the slow copies.

Toy weight, repo τ = 0.001one updateafter 1,000 updatestarget 0.50 to 0.5003live 0.80gap 0.30gap 0.11target 0.69live 0.80
Figure 8. Each update moves the target 0.1 percent of the gap.
After each update, the target networks move a small share of the way toward the live ones.
θ′ ← τθ + (1 − τ)θ′
θ are the live network weights, θ′ the target copy, and τ the small share. The arrow means "is replaced by": τ parts live, 1 − τ parts old. A small τ keeps the critic's targets moving slowly; τ = 1 copies the live network outright. At 128 updates every 64 steps, 2 per environment step, 1,000 updates take about 500 environment steps. The copy is an exponential moving average of the live weights over about 1/τ = 1,000 updates, the same averaging rule Adam applies to gradients (Optimizers, section 1.6). These are the primes of the update equations, the target network of the DQN page, and note 5 of the Section 8 code runs this line.

Exploration uses the Ornstein-Uhlenbeck (OU) noise of Training DDPG: Target Networks, Replay and Noise, so a jump lasts a few steps instead of one. Its κ pulls the noise back toward zero, σ sets the size of each kick, and the repo divides each sample by 5.

Setting and repo valueRaising itLowering it
Layers
actor 256, 256, 128, tanh; critic 256, 256, 1
more capacity, slowercheaper, may underfit
Replay buffer
1,000,000
more varied, older opponentsfresher opponents; at 128 it holds one batch
Batch
128
steadier E average, more computecheaper, noisier
Updates
128 per 64 steps
more reuse; overfits stale stepscheaper, slower
Actor learning rate
2e-4
faster; can outrun the criticsteadier, slower; 0 freezes the actor
Critic learning rate
8e-4, 4 times the actor's
tracks the opponent; can oscillatesteadier, slower; 0 freezes the critic
Critic weight decay
1e-5
smaller weights; can underfit0 turns it off
Discount γ
0.99
later clears count moreγ = 0 sees only the next reward
Target update τ
1e-3
faster, less stable; τ = 1 copies outrightsteadier; τ = 0 freezes them
Clip norm
1.5, both networks
larger steps; huge turns it offsteadier, slower
OU noise
κ 0.1, σ 0.3, ÷ 5
wider exploration, jumpier playnarrower; 0 stops exploring
Figure 9. Settings of the TennisGym run, scripts/agent.py.

In the loop, s holds both rackets' observations o, agent is the shared DDPG agent, noise its exploration noise and buf the one replay buffer both rackets fill, with add and sample(n); learn is the update in Section 8.

import numpy as np

BATCH, LEARN_EVERY, N_UPDATES = 128, 64, 128
max_steps, BREAK_AFTER = 2500, 10
count = 0

for t in range(max_steps):
    a = agent.act(s)
    a = a + noise.sample() / 5
    a = np.clip(a, -1, 1)
    out = env.step(a)
    info = out[brain_name]
    s2 = info.vector_observations
    r = info.rewards
    d = info.local_done
    rows = zip(s, a, r, s2, d)
    for row in rows:
        buf.add(*row)
    s = s2
    ready = len(buf) >= BATCH
    due = t % LEARN_EVERY == 0
    if due and ready:
        for _ in range(N_UPDATES):
            b = buf.sample(BATCH)
            learn(b)
    if np.any(d):
        count += 1
    if count > BREAK_AFTER:
        break
  1. 1Acts with exploration noise, clips to [−1, 1], and steps the court; a is (2, 2), one row per racket. The repo clips first and adds the noise after, so its actions can leave [−1, 1].
  2. 2Stores one transition per racket in the shared buffer.
  3. 3Every LEARN_EVERY steps, runs N_UPDATES updates on random batches.
  4. 4Counts the done steps and ends the episode after more than 10 of them, or after 2,500 steps.

The buffer holds the last million transitions and gains only two per step, so its contents change slowly and most samples come from older versions of the opponent. That steadies learning when the world is fixed and slows adaptation when the opponent changes (Section 3).

3 Non-stationarity from a learning opponent

A replay buffer assumes past experience previews the future, as it did for the banana agent and the Reacher arm. In tennis, what happens after each shot depends on the other racket, and that racket's policy changes with every update. This is non-stationarity: the world changes while the agent learns it. A lesson such as "hit the ball deep" stops working once the opponent learns to stand deep.

You improve Rallies lengthen Opponent improves Old lessons expire each side's gains change the other's world

Figure 10. Each racket's improvement changes what the other faces.

Training only on recent transitions would give up the large replay buffer that keeps DDPG stable. Other fixes include predicting the opponent's next move, opponents that get harder step by step, and the centralized critic of Section 9.

4 Frozen versus learning opponent

The tool shows the tennis run's problem: the faster the other racket improves, the sooner a stored rally goes stale.

Opponent frozen, smooth climb
Opponent learning at 0.60, jagged climb
opponent changed, old lessons failed

Figure 11. Toy curves; the slider sets how fast the opponent learns.

5 The training curve

01230.5050100150200episodescore123
Figure 12. Score traced from the repo's plot; ① past 0.5, ② long rallies, ③ peak 2.7.

Apart from a brief bump to 0.3 in episodes 5 to 12, the better racket's score sits near 0 for about 25 episodes while both rackets miss most balls, then climbs once rallies start. After episode 64 single episodes swing between about 0.2 and 2.7: each racket's gains keep changing the other's world, the jagged climb of Figure 11. The loop stops right after episode 200, with the 100-episode average above 0.5.

6 How the reward shapes the curve

Once both rackets return the ball, rallies get long, and the score grows with rally length. For the racket that drops, 5 clears score 0.49 and 6 score 0.59; the other racket reaches 0.5 at 5 clears (Figure 13).

In one rally the racket that dropped the ball earns its clears and pays for one drop; the other racket only earns its clears.
rally score = 0.1 × n − 0.01
n is the number of clears by one racket, about half the hits in the rally, and the single 0.01 is the drop that ends it: the score equation of Section 1 for one rally. Each clear adds 0.1, so the score grows in a straight line with n; the one drop costs only 0.01, a tenth of one clear, and the other racket pays no drop. Longer rallies, not fewer mistakes, drive the score, and failed early attempts cost little.
solve line 0.5 dropped, 6 clears, 0.59 no drop, 5 clears, 0.5 0 5 10 0 0.5 1 clears by one racket rally score

Figure 13. In one rally, 5 clears reach 0.5 without the drop, 6 with it.

7 The trained rackets

Figure 14. The trained rackets rallying at test time, with the final policy.

At test time the replay buffer and the critic are not used, and the repo turns the OU noise off (add_noise=False in Tennis.ipynb). Only the actor runs, in eval mode, with no gradients.

import torch

actor.eval()
with torch.no_grad():
    obs = torch.as_tensor(states)
    obs = obs.float()
    a = actor(obs).numpy()
out = env.step(a)
info = out[brain_name]
  1. 1Runs the trained actor without noise or gradients; a is (2, 2), one row per racket.
  2. 2Sends the actions to the court.

8 Self-play DDPG in practice

The shared agent is a DDPG agent whose buffer receives two transitions per step, with the settings in Figure 9 (TennisGym, scripts/agent.py).

learn is the DDPG update on one batch from the shared buffer, with the repo's gradient clipping on both networks.

import torch
import torch.nn.functional as F
import torch.nn.utils as U

GAMMA, TAU, CLIP = 0.99, 1e-3, 1.5
tgts = actor_target, critic_target
nets = actor, critic

def soft_update(tgt, live):
    dst = list(tgt.parameters())
    src = list(live.parameters())
    with torch.no_grad():
        for t, p in zip(dst, src):
            t.lerp_(p, TAU)

@torch.no_grad()
def td_target(r, s_next, done):
    a2 = actor_target(s_next)
    q2 = critic_target(s_next, a2)
    keep = 1 - done
    return r + GAMMA * q2 * keep

def learn(batch):
    s, a, r, s_next, done = batch
    y = td_target(r, s_next, done)
    q = critic(s, a)
    critic_loss = F.mse_loss(q, y)
    critic_opt.zero_grad()
    critic_loss.backward()
    w = critic.parameters()
    U.clip_grad_norm_(w, CLIP)
    critic_opt.step()
    q = critic(s, actor(s))
    actor_loss = -q.mean()
    actor_opt.zero_grad()
    actor_loss.backward()
    w = actor.parameters()
    U.clip_grad_norm_(w, CLIP)
    actor_opt.step()
    for t, n in zip(tgts, nets):
        soft_update(t, n)
  1. 1Sets the constants, pairs each target network with its live network, and defines soft_update.
  2. 2Builds the target y from the target networks; keep is 0 on a step where the racket's rally ended, its done flag (line 1 of the update equations in Section 2).
  3. 3Critic step, with gradient clipping.
  4. 4Actor step, with gradient clipping.
  5. 5Soft-updates both target networks (the soft-update equation in Section 2).

Two mistakes to avoid.

9 MADDPG

MADDPG (Lowe et al., 2017) answers the problem of Section 3. Each agent gets its own actor, fed only its own observations, and its own centralized critic, which sees every agent's observations and actions during training, so its score for "hit deep" knows where the opponent stood. At test time each actor sees only its own observations, centralized training with decentralized execution. The authors' code is openai/maddpg.

This page: one shared actor and criticRacket ARacket Bone actor, one criticcritic sees one racket at a timeMADDPG: own actor, own criticactor 1own observationsactor 2own observationscritic 1sees everybody,training onlycritic 2sees everybody,training onlyat test time each actor seesonly its own observations
Figure 15. Each MADDPG critic sees every agent's observations and actions.

The centralized critic reads twice the inputs (Figure 16).

Critic inputs, countedown 2424 + 2 = 26TennisGym criticA 24B 2448 + 4 = 52MADDPG criticeach actor still reads 24
Figure 16. The centralized critic reads both rackets; each actor still reads its own.
Each racket keeps its own actor, and its critic scores the play of both rackets together.
ai = μi(oi)
The actor equation of Section 2 with μ split into μi, one actor per racket, still fed only its own 24 numbers.
yi = ri + γ·Q′i(o′A, o′B, μ′A(o′A), μ′B(o′B))
This is Section 2's target, now per racket, where ri is racket i's reward and Q′i its own slow critic with the same γ = 0.99, and that critic reads both rackets' next observations and the next action each racket's own slow actor would take.
loss = E[(yi − Qi(oA, oB, aA, aB))²]
Qi is racket i's live critic, now fed all 52 inputs of Figure 16, and the loss is Section 2's. With every action as an input, the world the critic models stays fixed while the other policy changes, the paper's argument; the critic is dropped at test time.
JA = E[QA(oA, oB, μA(oA), aB)]
Racket A's actor climbs its own critic, as the actor climbs J in Section 2: its own action comes from μA, and racket B's action aB is the one stored in the buffer. Racket B's line swaps the letters. Section 10's third line is the shared actor of Section 2, before this split.

The paper argues that Q-learning struggles here because the environment is non-stationary, and that the variance of policy gradients (how much the actor's learning signal varies from batch to batch) grows with the number of agents. It trains each agent as an ensemble, several policies with one picked per episode, so it does not overfit one opponent, and shows coordination on particle-world tasks such as cooperative navigation and predator-prey.

10 DQN, DDPG and self-play compared

Each rule turns one input into an action (Figure 17).

DQN DDPG Self-play fwd 2.5 back 0.4 left 1.2 right 1.9 argmax picks fwd 0.62 −0.10 0.35 0.05 four torques one μ A 0.2, 0.5 B −0.4, 0.1
Figure 17. Toy numbers; argmax over a list, four torques, one actor for two rackets.
DQN scores a list of actions, DDPG outputs the move, and self-play outputs the move for two players with one shared actor.
a = argmaxa Q(s, a)
DQN's network Q scores every move a in state s, and argmax plays the highest score. That only works while the list is short, like the four moves of the DQN banana agent.
a = μ(s)
DDPG's actor μ outputs the continuous actions directly from the state, and the critic Q(s, a) only scores the action during training. No argmax anywhere. That is the Reacher arm on the DDPG page.
ai = μ(oi)
In self-play the same μ drives both rackets, each from its own observations oi: DDPG's formula with an index, the actor equation of Section 2. The opponent runs the same μ, so every update also rewrites the opponent.

In all three setups the reward comes from the environment. The Aligning Language Models with RL page covers rewards that come from people judging which answer is better.

11 References

  1. Silver, D., et al. (2014). Deterministic Policy Gradient Algorithms. ICML.
  2. Lillicrap, T. P., et al. (2016). Continuous control with deep reinforcement learning. ICLR. arXiv:1509.02971.
  3. Lowe, R., et al. (2017). Multi-Agent Actor-Critic for Mixed Cooperative-Competitive Environments. NeurIPS. arXiv:1706.02275.
  4. OpenAI, Berner, C., et al. (2019). Dota 2 with Large Scale Deep Reinforcement Learning. arXiv:1912.06680.