Semi-Gradient TD(0)
Semi-gradient TD(0) updates value predictions by bootstrapping from the next state's estimate while treating that target as a fixed fact rather than backpropagating through it.
Why Does This Exist?
In supervised learning and Gradient Monte Carlo methods, optimization relies on true gradient descent: an agent observes an external, ground-truth label or complete episode return , computes the squared error loss , and updates weights along the exact negative gradient .
However, Monte Carlo updates cannot run online: they require waiting until an episode concludes to calculate , cannot function in non-terminating continuing tasks, and suffer from massive sample variance. To learn incrementally after every single transition , Temporal Difference learning replaces the distant future return with a bootstrapped target:
Here lies the mathematical dilemma: the target itself contains the parameter vector .
If we applied standard calculus to differentiate the squared error with respect to , the gradient would backpropagate through both the current estimate and the target:
Following this "full gradient" produces Residual Gradient algorithms, which converge painfully slowly, minimize the wrong objective (the Bellman error rather than the value error), and require double-sampling transitions.
Semi-gradient TD(0) resolves this dilemma with an intentional mathematical shortcut: it detaches the target. By treating the next-state estimate as a fixed scalar constant during differentiation, semi-gradient TD(0) enables fast, efficient, online bootstrapping that converges reliably to a bounded fixed point under linear representations.
Think of It Like This
The Road Trip ETA Mileage Markers
Imagine you are driving across the country on a 500-mile highway road trip, continuously predicting your total travel time:
- At Mile Marker 100: You look at your dashboard clock and current traffic conditions. Your internal model estimates your total travel time will be 8.0 hours.
- At Mile Marker 110: Ten minutes later, you reach the next marker. You recalculate your expected total time from this new vantage point and realize it is now 8.3 hours (perhaps due to unexpected construction ahead).
To improve your prediction at Mile Marker 100, you compute the prediction error:
You immediately adjust your Mile Marker 100 prediction model upward to eliminate this 0.3-hour gap.
Crucially, when updating your estimate for Mile Marker 100, you treat the 8.3-hour estimate at Mile Marker 110 as an objective, frozen benchmark. You do not attempt to differentiate or second-guess how your future self at Marker 110 formulated that number; you simply treat it as an updated checkpoint and adjust your past prediction toward it.
Where the analogy stops: Human drivers know the physical laws of distance and speed. In reinforcement learning, the target estimate is itself a flawed, noisy hypothesis generated by the exact same parameter weights that are being updated, meaning the target shifts continuously beneath the algorithm's feet as learning progresses.
How It Actually Works
Why Is It Called "Semi-Gradient"?
In general function approximation, let be a parameterized function with weight vector .
If the target were an independent random variable uncorrelated with , the gradient of the squared prediction error with respect to would be:
In one-step TD, the target is . Although this target explicitly depends on , the semi-gradient method deliberately ignores the derivative of the target:
The parameter update rule is therefore:
Because the update includes only the gradient of the estimated value and drops the gradient of the bootstrapped target, it is termed a semi-gradient method. In modern deep learning frameworks (PyTorch / JAX / TensorFlow), this detachment is implemented explicitly:
# PyTorch equivalent of a semi-gradient TD targettd_target = reward + gamma * v_net(next_state).detach()The Linear Semi-Gradient TD(0) Update
When using linear function approximation, state values are parameterized as an inner product:
Since , the semi-gradient update simplifies into a clean vector operation:
where the scalar TD error is:
This update requires zero backpropagation, runs in time, and performs linear feature projection online on every observed transition.
The TD Fixed Point and the Projection Matrix
Because semi-gradient TD(0) does not follow the true gradient of any fixed scalar loss function, it cannot be analyzed as ordinary gradient descent. Instead, it is an iterative projection operator: it repeatedly applies the Bellman operator and projects the resulting values back onto the subspace representable by the linear feature matrix .
Under on-policy state visitation distribution , the expected weight update is:
where:
When the step size satisfies standard Robbins-Monro stochastic approximation conditions, linear semi-gradient TD(0) is guaranteed to converge to the unique TD fixed point :
The matrix is guaranteed to be positive definite whenever the feature covariance matrix has full rank and .
Furthermore, Tsitsiklis and Van Roy (1997) proved that the Mean Squared Value Error at this fixed point is strictly bounded:
Even though semi-gradient TD does not follow the true error gradient, its asymptotic fixed point never diverges on-policy and remains tightly bounded near the best possible linear approximation.
Worked numerical example
Consider a 2-feature linear value approximator:
- Current state with feature vector: .
- Next state with feature vector: .
- Current weight vector: .
- Observed transition reward: .
- Hyperparameters: discount factor , learning rate .
1. Forward Predictions
2. Bootstrapped Target Calculation
Treating as a detached constant:
3. Temporal Difference Error
4. Semi-Gradient Weight Update
The gradient with respect to evaluates solely at the current state: :
5. Verify Updated Current State Estimate
The prediction moved toward the target by exactly , proportional to the step size and feature norm squared ().
Code
import mathfrom typing import List, Tuple
class LinearSemiGradientTD0: """Linear Semi-Gradient TD(0) for state-value prediction v_hat(s, w) = w^T x(s)."""
def __init__(self, num_features: int, initial_weights: List[float]): assert len(initial_weights) == num_features self.num_features = num_features self.w = list(initial_weights)
def predict(self, x: List[float]) -> float: """Compute inner product between weight vector and feature vector.""" assert len(x) == self.num_features return sum(w_i * x_i for w_i, x_i in zip(self.w, x))
def update( self, x_t: List[float], reward: float, x_next: List[float], gamma: float = 0.9, alpha: float = 0.1, ) -> Tuple[float, float, List[float]]: """Perform one-step semi-gradient TD(0) update.
Returns: Tuple of (td_target, td_error, updated_weights). """ # 1. Forward evaluations v_current = self.predict(x_t) # Detached next-state target: treated as fixed scalar constant v_next = self.predict(x_next)
# 2. Compute bootstrapped TD target and error td_target = reward + gamma * v_next td_error = td_target - v_current
# 3. Semi-gradient update: gradient is x_t (gradient of target is ignored) for i in range(self.num_features): self.w[i] += alpha * td_error * x_t[i]
return td_target, td_error, list(self.w)
if __name__ == "__main__": # 2-feature numerical setup matching the worked example x_s = [1.0, 2.0] x_s_prime = [0.5, 1.0] w_initial = [0.5, 1.0] r_step = 2.0 discount = 0.9 step_size = 0.1
td_agent = LinearSemiGradientTD0(num_features=2, initial_weights=w_initial)
# 1. Initial Predictions v_s_init = td_agent.predict(x_s) v_s_prime_init = td_agent.predict(x_s_prime) print("=== Linear Semi-Gradient TD(0) Verification ===") print(f"Initial v_hat(S_t): {v_s_init:.4f}") print(f"Initial v_hat(S_t+1): {v_s_prime_init:.4f}")
assert math.isclose(v_s_init, 2.5), f"Expected 2.5, got {v_s_init}" assert math.isclose( v_s_prime_init, 1.25 ), f"Expected 1.25, got {v_s_prime_init}"
# 2. Semi-Gradient Transition Update target, error, updated_w = td_agent.update( x_t=x_s, reward=r_step, x_next=x_s_prime, gamma=discount, alpha=step_size, )
print(f"\nBootstrapped TD Target: {target:.4f}") print(f"TD Error delta_t: {error:.4f}") print(f"Updated Weight Vector: {updated_w}")
assert math.isclose(target, 3.125), f"Expected target 3.125, got {target}" assert math.isclose(error, 0.625), f"Expected error 0.625, got {error}" assert math.isclose( updated_w[0], 0.5625 ), f"Expected w[0] = 0.5625, got {updated_w[0]}" assert math.isclose( updated_w[1], 1.1250 ), f"Expected w[1] = 1.1250, got {updated_w[1]}"
# 3. Verify Updated Current State Estimate v_s_updated = td_agent.predict(x_s) print(f"Updated v_hat(S_t): {v_s_updated:.4f}") assert math.isclose( v_s_updated, 2.8125 ), f"Expected 2.8125, got {v_s_updated}"
print( "\nAll semi-gradient mathematical update assertions passed successfully." )
# Expected Output:# === Linear Semi-Gradient TD(0) Verification ===# Initial v_hat(S_t): 2.5000# Initial v_hat(S_t+1): 1.2500## Bootstrapped TD Target: 3.1250# TD Error delta_t: 0.6250# Updated Weight Vector: [0.5625, 1.125]# Updated v_hat(S_t): 2.8125## All semi-gradient mathematical update assertions passed successfully.Watch Out For
Treating Semi-Gradient TD as True Gradient Descent & The Deadly Triad
Because the semi-gradient update formula looks superficially identical to standard gradient descent, practitioners often assume it is descending a well-behaved loss function. It is not.
Because the bootstrapped target changes continuously with the parameters, semi-gradient updates do not point along the negative gradient of any true scalar objective. In fact, when combined with the three elements of the Deadly Triad:
- Function Approximation (linear or non-linear neural networks)
- Bootstrapping (updating estimates from subsequent estimates, as in TD)
- Off-Policy Training (evaluating target policy while collecting data with behavior policy )
Semi-gradient TD can diverge toward infinity, causing weight vectors to blow up and generating numerical NaN parameters (demonstrated by Baird's Counterexample).
The Fix:
- Stay On-Policy When Bootstrapping: Linear semi-gradient TD(0) is mathematically guaranteed to converge if transitions are collected strictly on-policy.
- Use True Gradient Off-Policy Algorithms: When off-policy learning is required with function approximation, use true gradient methods like Gradient TD (GTD2 / TDC) or Emphatic TD, which explicitly minimize the Projected Bellman Error (PBE) with dual-timescale updates.
- Stabilize Deep Networks: In deep Q-learning (DQN), freeze the bootstrapped target using a separate periodic target network (), temporarily transforming the semi-gradient update into an almost stationary supervised regression target.
The Quick Version
- The semi-gradient shortcut: The bootstrapped target contains parameters , but semi-gradient methods treat it as a detached constant () during differentiation.
- Linear update formula: With linear features, the gradient is simply the feature vector (), simplifying updates to without backpropagation.
- The TD fixed point: Linear on-policy semi-gradient TD converges to a unique stationary point governed by an iterative Bellman projection operator.
- Deadly Triad awareness: Because it is not true gradient descent, semi-gradient updating can diverge if paired with off-policy data and function approximation; on-policy linear training remains strictly stable.