Soft Actor-Critic (SAC): The Complete Guide
Summary
Soft Actor-Critic (SAC) is one of the most performant reinforcement learning algorithms for continuous action spaces. Developed by Tuomas Haarnoja and his collaborators at UC Berkeley in 2018, SAC stands out for a fundamental idea: maximizing not only expected reward, but also policy entropy. This dual objective enables the agent to explore its environment efficiently while converging toward robust and generalizable policies.
Unlike DDPG, which exploits deterministically and risks getting stuck in local optima, or PPO, which requires extensive hyperparameter tuning, Soft Actor-Critic offers remarkable stability through several combined technical innovations: double Q-learning, the reparameterization trick, and automatic entropy temperature adjustment. These mechanisms make it the algorithm of choice for many industrial applications, from robotics to quantitative finance.
This complete guide explores the mathematical principle behind Soft Actor-Critic, its deep intuition, and provides a working PyTorch implementation with automatic alpha tuning.
Mathematical Principle of Soft Actor-Critic
The Entropic Objective
In classical reinforcement learning, the agent seeks a policy π* that maximizes the sum of discounted rewards:
J(π) = E[Σ γ^t · r(s_t, a_t)]
Soft Actor-Critic radically modifies this formula by adding an entropy term:
J_SAC(π) = E[Σ γ^t · (r(s_t, a_t) + α · H(π(·|s_t)))]
where H(π(·|s)) = -E[log π(a|s)] is the policy entropy, measuring the degree of uncertainty or “surprise” of the action distribution. The coefficient α (alpha) controls the exploration-exploitation tradeoff: a high value pushes the agent to explore more, while a low value makes it more exploitative.
This entropy term plays a crucial role. By encouraging the policy to remain “uncertain” about its actions, SAC avoids premature convergence to suboptimal strategies. The agent is rewarded not only for acting well, but also for staying curious about its environment.
Modified Bellman Equations
In the entropic framework, the soft value is defined as:
V_soft(s) = E_a[Q_soft(s, a) - α · log π(a|s)]
and the soft Q-function satisfies the modified Bellman equation:
Q_soft(s, a) = r(s, a) + γ · E_s'[V_soft(s')]
This modification means that each state intrinsically carries a value related to the diversity of available actions — not just the immediate reward.
Double Q-Learning: Reducing Overestimation
Like DQN before it, Soft Actor-Critic suffers from the Q-value overestimation problem. To address this, SAC maintains two critic networks Q₁ and Q₂ with their own parameters. When computing the target, the minimum of the two is taken:
Q_target = r + γ · (min(Q₁(s', π(s')), Q₂(s', π')) - α · log π(a'|s'))
This conservative approach, borrowed from Double DQN and TD3, significantly reduces the overestimation bias that destabilizes learning.
The Reparameterization Trick
One of the major difficulties of Soft Actor-Critic is differentiating the expectation over actions with respect to policy parameters. SAC solves this problem through the reparameterization trick:
Instead of sampling directly a ~ π(·|s), the action is expressed as a differentiable function of noise:
a = f_φ(ε, s) where ε ~ N(0, I)
Concretely, the parameterized Gaussian policy produces a mean μ(s) and a standard deviation σ(s), then:
a = μ(s) + σ(s) ⊙ ε
where ⊙ denotes element-wise multiplication. This reformulation allows computing the gradient of the expectation by differentiating directly through the sampling process, which considerably reduces the variance of the gradient estimator compared to the classical REINFORCE estimator.
Automatic Temperature Adjustment
Instead of manually setting α, Soft Actor-Critic automatically adjusts it by solving the following minimization problem:
J(α) = E_{a~π}[-α · log π(a|s) - α · H_target]
where H_target = -dim(A) is the target entropy, typically set to the negative of the action space dimension. This approach allows the algorithm to automatically find the right exploration-exploitation balance throughout training, without human intervention.
The optimal policy in the entropic framework is written:
π*(a|s) ∝ exp(Q_soft(s, a) / α)
This Boltzmann form clearly shows how entropy influences the policy: the larger α is, the more uniform the action distribution (exploration); the smaller α is, the more the policy concentrates on higher-value actions (exploitation).
Intuition: Why Entropy Changes Everything
Imagine two agents facing a complex maze. The first agent is extremely confident: as soon as it finds a working path, it sticks to it rigidly and explores no further. It may perfectly miss a hidden shortcut or a far more efficient strategy. This the flaw of purely exploitative algorithms like DDPG.
The second agent, by contrast, explores in a chaotic manner. It tests everything without ever really focusing on what works. Its performance remains mediocre because it fails to consolidate its discoveries. This is the risk of unstructured exploration.
Soft Actor-Critic strikes the perfect balance between these two extremes. It maximizes reward while remaining as uncertain as possible — exactly like a curious scientist who seeks concrete results but remains open to surprises and unexpected discoveries.
Entropy is not an arbitrary addition: it is the mechanism that enables the agent to maintain sufficient behavioral diversity to discover better strategies throughout training. It is this property that gives SAC its superior robustness in complex environments.
In practice, this entropic approach offers a decisive advantage: learned policies generalize better to situations not encountered during training. A robot trained with SAC will adapt to unforeseen disturbances (different friction, varying loads) better than a robot trained with a deterministic algorithm.
Python Implementation with PyTorch
Here is a complete implementation of Soft Actor-Critic with double Q-learning, Gaussian policy network, and automatic entropy temperature adjustment.
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
import gymnasium as gym
from collections import deque
import random
import math
# ============================================================
# Neural Networks
# ============================================================
class CriticNetwork(nn.Module):
"""Q(s, a) critic network for double Q-learning."""
def __init__(self, state_dim, action_dim, hidden_dim=256):
super().__init__()
self.q1 = nn.Sequential(
nn.Linear(state_dim + action_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, 1)
)
self.q2 = nn.Sequential(
nn.Linear(state_dim + action_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, 1)
)
def forward(self, state, action):
x = torch.cat([state, action], dim=-1)
q1 = self.q1(x)
q2 = self.q2(x)
return q1, q2
def q_min(self, state, action):
q1, q2 = self.forward(state, action)
return torch.min(q1, q2)
class ActorNetwork(nn.Module):
"""Gaussian policy network with reparameterization trick."""
def __init__(self, state_dim, action_dim, hidden_dim=256,
action_scale=1.0, action_bias=0.0):
super().__init__()
self.action_scale = action_scale
self.action_bias = action_bias
self.shared = nn.Sequential(
nn.Linear(state_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU()
)
self.mean_layer = nn.Linear(hidden_dim, action_dim)
self.log_std_layer = nn.Linear(hidden_dim, action_dim)
def forward(self, state):
x = self.shared(state)
mean = self.mean_layer(x)
log_std = self.log_std_layer(x)
log_std = torch.clamp(log_std, min=-20, max=2)
std = log_std.exp()
return mean, std
def sample(self, state, epsilon=1e-6):
"""Sample an action using the reparameterization trick."""
mean, std = self.forward(state)
# Reparameterization trick: a = mu + sigma * epsilon
normal = torch.distributions.Normal(mean, std)
x_t = normal.rsample()
# Tanh transformation to bound the action
y_t = torch.tanh(x_t)
action = y_t * self.action_scale + self.action_bias
# Entropy correction due to tanh
log_prob = normal.log_prob(x_t)
log_prob = log_prob - torch.log(
self.action_scale * (1 - y_t.pow(2)) + epsilon
)
log_prob = log_prob.sum(dim=-1, keepdim=True)
# Tanh of the mean for target computation
mean_action = torch.tanh(mean) * self.action_scale + self.action_bias
return action, log_prob, mean_action
# ============================================================
# SAC Agent
# ============================================================
class SACAgent:
"""Soft Actor-Critic agent with automatic temperature adjustment."""
def __init__(self, state_dim, action_dim, action_low, action_high,
lr=3e-4, gamma=0.99, tau=0.005,
target_entropy_ratio=0.89,
hidden_dim=256):
self.gamma = gamma
self.tau = tau
self.action_dim = action_dim
# Compute target entropy
self.target_entropy = -target_entropy_ratio * action_dim
# Initialize temperature coefficient alpha
self.log_alpha = torch.tensor(
[0.0], dtype=torch.float32, requires_grad=True
)
self.alpha = self.log_alpha.exp().item()
self.alpha_optimizer = optim.Adam([self.log_alpha], lr=lr)
# Main critic networks
self.critic = CriticNetwork(state_dim, action_dim, hidden_dim)
self.critic_optimizer = optim.Adam(
self.critic.parameters(), lr=lr
)
# Target critic networks (copied)
self.critic_target = CriticNetwork(state_dim, action_dim, hidden_dim)
self.critic_target.load_state_dict(self.critic.state_dict())
# Policy network (actor)
action_scale = torch.tensor(
(action_high - action_low) / 2.0, dtype=torch.float32
)
action_bias = torch.tensor(
(action_high + action_low) / 2.0, dtype=torch.float32
)
self.actor = ActorNetwork(
state_dim, action_dim, hidden_dim,
action_scale, action_bias
)
self.actor_optimizer = optim.Adam(
self.actor.parameters(), lr=lr
)
# Experience buffer
self.replay_buffer = deque(maxlen=1000000)
def select_action(self, state, evaluate=False):
"""Select an action according to the current policy."""
state = torch.FloatTensor(state).unsqueeze(0)
with torch.no_grad():
action, _, mean_action = self.actor.sample(state)
if evaluate:
return mean_action.squeeze(0).numpy()
return action.squeeze(0).numpy()
def store_transition(self, state, action, reward, next_state, done):
"""Store a transition in the experience buffer."""
self.replay_buffer.append({
'state': state,
'action': action,
'reward': reward,
'next_state': next_state,
'done': done
})
def sample_batch(self, batch_size):
"""Sample a mini-batch from the replay buffer."""
batch = random.sample(self.replay_buffer, batch_size)
states = torch.FloatTensor([t['state'] for t in batch])
actions = torch.FloatTensor([t['action'] for t in batch])
rewards = torch.FloatTensor([t['reward'] for t in batch]).unsqueeze(1)
next_states = torch.FloatTensor([t['next_state'] for t in batch])
dones = torch.FloatTensor([t['done'] for t in batch]).unsqueeze(1)
return states, actions, rewards, next_states, dones
def update(self, batch_size=256):
"""Update all networks from a mini-batch."""
if len(self.replay_buffer) < batch_size:
return 0
states, actions, rewards, next_states, dones = \
self.sample_batch(batch_size)
# ---- Update Critics (Double Q) ----
with torch.no_grad():
# Sample next actions with the target policy
next_actions, next_log_probs, _ = self.actor.sample(next_states)
# Minimum Q of the two target networks
q1_target, q2_target = self.critic_target(
next_states, next_actions
)
q_target_min = torch.min(q1_target, q2_target)
# Target with entropy term
q_target = rewards + (1 - dones) * self.gamma * (
q_target_min - self.alpha * next_log_probs
)
# Compute loss for both critics
q1_current, q2_current = self.critic(states, actions)
q1_loss = F.mse_loss(q1_current, q_target)
q2_loss = F.mse_loss(q2_current, q_target)
critic_loss = q1_loss + q2_loss
# Optimize critics
self.critic_optimizer.zero_grad()
critic_loss.backward()
self.critic_optimizer.step()
# ---- Update Policy (Actor) ----
new_actions, log_probs, _ = self.actor.sample(states)
# New Q evaluation with conservative minimum
q_min = self.critic.q_min(states, new_actions)
# Policy loss: we maximize Q - alpha * log_pi
actor_loss = (self.alpha * log_probs - q_min).mean()
self.actor_optimizer.zero_grad()
actor_loss.backward()
self.actor_optimizer.step()
# ---- Automatic Alpha Adjustment ----
alpha_loss = -(self.log_alpha.exp() * (
log_probs.detach() + self.target_entropy
)).mean()
self.alpha_optimizer.zero_grad()
alpha_loss.backward()
self.alpha_optimizer.step()
self.alpha = self.log_alpha.exp().item()
# ---- Soft Target Update ----
self._soft_update(self.critic, self.critic_target, self.tau)
return critic_loss.item()
def _soft_update(self, source, target, tau):
"""Soft update of target parameters."""
for target_param, source_param in zip(
target.parameters(), source.parameters()
):
target_param.data.copy_(
tau * source_param.data + (1.0 - tau) * target_param.data
)
# ============================================================
# Training Loop
# ============================================================
def train_sac(env_name="Pendulum-v1", episodes=500, batch_size=256,
start_steps=1000, hidden_dim=256):
"""Train a SAC agent on a Gymnasium environment."""
env = gym.make(env_name)
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.shape[0]
action_low = env.action_space.low
action_high = env.action_space.high
agent = SACAgent(
state_dim=state_dim,
action_dim=action_dim,
action_low=action_low,
action_high=action_high,
lr=3e-4,
gamma=0.99,
tau=0.005,
target_entropy_ratio=0.89,
hidden_dim=hidden_dim
)
rewards_history = []
alpha_history = []
for episode in range(episodes):
state, _ = env.reset()
episode_reward = 0
done = False
while not done:
# Exploration at the beginning of training
if episode * 200 + len(agent.replay_buffer) < start_steps:
action = env.action_space.sample()
else:
action = agent.select_action(state)
next_state, reward, terminated, truncated, _ = env.step(action)
done = terminated or truncated
agent.store_transition(state, action, reward, next_state, done)
agent.update(batch_size)
state = next_state
episode_reward += reward
rewards_history.append(episode_reward)
alpha_history.append(agent.alpha)
if (episode + 1) % 50 == 0:
avg_reward = np.mean(rewards_history[-50:])
print(f"Episode {episode+1}/{episodes} | "
f"Avg reward (last 50): {avg_reward:.2f} | "
f"Alpha: {agent.alpha:.4f}")
env.close()
return agent, rewards_history, alpha_history
if __name__ == "__main__":
agent, rewards, alphas = train_sac(
env_name="Pendulum-v1",
episodes=300,
batch_size=256,
start_steps=1000
)
print(f"Training complete. Final average reward: "
f"{np.mean(rewards[-50:]):.2f}")
Key Hyperparameters
Soft Actor-Critic depends on several crucial hyperparameters:
| Hyperparameter | Typical value | Role |
|---|---|---|
| α (alpha) | Automatically adjusted | Temperature coefficient, controls exploration |
| Learning rate | 3 × 10⁻⁴ | Network update speed |
| γ (gamma) | 0.99 | Discount factor for future rewards |
| τ (tau) | 0.005 | Soft update rate for target networks |
| Buffer size | 1,000,000 | Replay buffer capacity |
| Batch size | 256 | Number of transitions per update |
| H_target | -dim(A) | Target entropy for automatic adjustment |
| Hidden dimensions | 256 | Size of network hidden layers |
Tuning tips:
- For simple environments like Pendulum, reduce
hidden_dimto 128 to speed up training without losing performance. - For complex environments like HalfCheetah or Humanoid, increase
hidden_dimto 512 andstart_stepsto 10,000 for longer initial exploration. - If the agent doesn’t explore enough, reduce
target_entropy_ratioto 0.5 to force higher entropy. - If the agent diverges, reduce the learning rate to 1 × 10⁻⁴ and increase
tauto 0.01 for more gradual updates.
Advantages and Limitations of Soft Actor-Critic
Advantages
- Exceptional sample efficiency: SAC takes full advantage of the replay buffer, often requiring fewer interactions with the environment than PPO to converge.
- Robustness to hyperparameters: automatic alpha adjustment considerably reduces the need for manual tuning compared to DDPG or TD3.
- Intrinsic exploration: the entropy term naturally encourages the discovery of new strategies without artificial exploration mechanisms like ε-greedy.
- Superior generalization: policies trained with SAC adapt better to conditions not encountered during training due to the encouraged behavioral diversity.
- State-of-the-art performance: on standard MuJoCo benchmarks, SAC achieves or exceeds the performance of TD3 and PPO on most environments.
Limitations
- Computational complexity: SAC requires maintaining four networks (two critics + two targets) plus the actor, significantly increasing the memory footprint compared to simpler algorithms.
- Sensitivity to discrete environments: designed for continuous spaces, Soft Actor-Critic requires substantial adaptations to work effectively with discrete action spaces.
- Computation time per step: each update involves computing two Q-networks and backpropagating through the reparameterization trick, making training slower per iteration.
- Initial steps required: the
start_stepsmechanism demands a minimum number of random interactions before learning begins, which can be problematic in environments where failures are costly.
4 Concrete Use Cases
1. Advanced Robotics Control
Soft Actor-Critic excels in controlling robots with multiple joints. DeepMind has used SAC variants to train object manipulation policies on real robotic arms, with sim-to-real transfer. The intrinsic entropy enables the robot to discover creative gripping strategies that deterministic approaches would miss. For example, learning to flip an object in the hand without visual feedback becomes feasible thanks to entropic exploration that discovers non-intuitive finger configurations.
2. Algorithmic Trading and Quantitative Finance
In automated portfolio management, the action space is continuous (weights allocated to each asset) and the environment is highly noisy. SAC’s entropy term acts as a natural regularizer, preventing the agent from concentrating excessively on a small number of market signals. This implicit strategy diversification leads to trading policies more robust to regime changes, reducing the risk of overfitting on temporary patterns.
3. Autonomous Driving and Navigation
For autonomous navigation systems, SAC enables the learning of continuous control policies (acceleration, steering) in partially observable environments. Entropic exploration is particularly valuable here: an autonomous vehicle trained with SAC will maintain a wider range of defensive behaviors in ambiguous situations, rather than converging to a single rigid strategy. Simulations on CARLA environments have shown that SAC policies generalize better to varied weather conditions.
4. Physical Systems Optimization
Tuning parameters in industrial systems — power plants, production lines, HVAC systems — naturally benefits from SAC. The control space is continuous, rewards are often noisy or delayed, and robustness is crucial. Entropic exploration enables the discovery of unconventional operating configurations that reduce energy consumption while maintaining production quality, where deterministic methods would remain stuck in suboptimal configurations.
See Also
- What? Where? When? Mastering Python Programming: Complete Guide for Beginners
- Mastering Bucket Sort in Python: Complete Guide for Beginners

