Skip to content
AI360Xpert
Beta

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.

Semi-gradient TD(0) updates value weights by bootstrapping next-state values while stopping gradients through the target.
Semi-gradient TD(0) updates value weights by bootstrapping next-state values while stopping gradients through the target.

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 GtG_t, computes the squared error loss 12[Gt−v^(St,w)]2\frac{1}{2}[G_t - \hat{v}(S_t, \mathbf{w})]^2, and updates weights along the exact negative gradient −∇wL-\nabla_{\mathbf{w}} \mathcal{L}.

However, Monte Carlo updates cannot run online: they require waiting until an episode concludes to calculate GtG_t, cannot function in non-terminating continuing tasks, and suffer from massive sample variance. To learn incrementally after every single transition (St,Rt+1,St+1)(S_t, R_{t+1}, S_{t+1}), Temporal Difference learning replaces the distant future return GtG_t with a bootstrapped target:

Ut=Rt+1+γv^(St+1,wt)U_t = R_{t+1} + \gamma \hat{v}(S_{t+1}, \mathbf{w}_t)

Here lies the mathematical dilemma: the target itself contains the parameter vector wt\mathbf{w}_t.

If we applied standard calculus to differentiate the squared error 12[Ut(w)−v^(St,w)]2\frac{1}{2} [U_t(\mathbf{w}) - \hat{v}(S_t, \mathbf{w})]^2 with respect to w\mathbf{w}, the gradient would backpropagate through both the current estimate and the target: ∇w12[Rt+1+γv^(St+1,w)−v^(St,w)]2=δt[γ∇wv^(St+1,w)−∇wv^(St,w)]\nabla_{\mathbf{w}} \frac{1}{2} \left[ R_{t+1} + \gamma \hat{v}(S_{t+1}, \mathbf{w}) - \hat{v}(S_t, \mathbf{w}) \right]^2 = \delta_t \left[ \gamma \nabla_{\mathbf{w}} \hat{v}(S_{t+1}, \mathbf{w}) - \nabla_{\mathbf{w}} \hat{v}(S_t, \mathbf{w}) \right]

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 v^(St+1,wt)\hat{v}(S_{t+1}, \mathbf{w}_t) 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:

  1. 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.
  2. 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: Discrepancy=8.3 hours−8.0 hours=+0.3 hours\text{Discrepancy} = 8.3\text{ hours} - 8.0\text{ hours} = +0.3\text{ hours}

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 v^(St+1,w)\hat{v}(S_{t+1}, \mathbf{w}) is itself a flawed, noisy hypothesis generated by the exact same parameter weights w\mathbf{w} 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 v^(s,w)\hat{v}(s, \mathbf{w}) be a parameterized function with weight vector w∈Rd\mathbf{w} \in \mathbb{R}^d.

If the target UtU_t were an independent random variable uncorrelated with wt\mathbf{w}_t, the gradient of the squared prediction error with respect to w\mathbf{w} would be:

∇w12[Ut−v^(St,w)]2=−[Ut−v^(St,w)]∇wv^(St,w)\nabla_{\mathbf{w}} \frac{1}{2} \left[ U_t - \hat{v}(S_t, \mathbf{w}) \right]^2 = - \left[ U_t - \hat{v}(S_t, \mathbf{w}) \right] \nabla_{\mathbf{w}} \hat{v}(S_t, \mathbf{w})

In one-step TD, the target is Ut=Rt+1+γv^(St+1,wt)U_t = R_{t+1} + \gamma \hat{v}(S_{t+1}, \mathbf{w}_t). Although this target explicitly depends on wt\mathbf{w}_t, the semi-gradient method deliberately ignores the derivative of the target:

∂Ut∂w≜0(treated as a detached constant)\frac{\partial U_t}{\partial \mathbf{w}} \triangleq \mathbf{0} \quad (\text{treated as a detached constant})

The parameter update rule is therefore:

wt+1=wt+α[Rt+1+γv^(St+1,wt)−v^(St,wt)]⏟TD Error δt∇wv^(St,wt)\mathbf{w}_{t+1} = \mathbf{w}_t + \alpha \underbrace{\left[ R_{t+1} + \gamma \hat{v}(S_{t+1}, \mathbf{w}_t) - \hat{v}(S_t, \mathbf{w}_t) \right]}_{\text{TD Error } \delta_t} \nabla_{\mathbf{w}} \hat{v}(S_t, \mathbf{w}_t)

Because the update includes only the gradient of the estimated value v^(St,w)\hat{v}(S_t, \mathbf{w}) 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:

v^(s,w)=w⊤x(s)\hat{v}(s, \mathbf{w}) = \mathbf{w}^\top \mathbf{x}(s)

Since ∇wv^(s,w)=x(s)\nabla_{\mathbf{w}} \hat{v}(s, \mathbf{w}) = \mathbf{x}(s), the semi-gradient update simplifies into a clean vector operation:

wt+1=wt+αδtx(St)\mathbf{w}_{t+1} = \mathbf{w}_t + \alpha \delta_t \mathbf{x}(S_t)

where the scalar TD error is:

δt=Rt+1+γwt⊤x(St+1)−wt⊤x(St)\delta_t = R_{t+1} + \gamma \mathbf{w}_t^\top \mathbf{x}(S_{t+1}) - \mathbf{w}_t^\top \mathbf{x}(S_t)

This update requires zero backpropagation, runs in O(d)O(d) 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 Bπ\mathcal{B}_\pi and projects the resulting values back onto the subspace representable by the linear feature matrix X\mathbf{X}.

Under on-policy state visitation distribution μ(s)\mu(s), the expected weight update is:

E[Δwt]=α(b−Awt)\mathbb{E}[\Delta \mathbf{w}_t] = \alpha \left( \mathbf{b} - \mathbf{A} \mathbf{w}_t \right)

where:

  • A≜E[x(St)(x(St)−γx(St+1))⊤]∈Rd×d\mathbf{A} \triangleq \mathbb{E} \left[ \mathbf{x}(S_t) \left( \mathbf{x}(S_t) - \gamma \mathbf{x}(S_{t+1}) \right)^\top \right] \in \mathbb{R}^{d \times d}
  • b≜E[Rt+1x(St)]∈Rd\mathbf{b} \triangleq \mathbb{E} \left[ R_{t+1} \mathbf{x}(S_t) \right] \in \mathbb{R}^d

When the step size α\alpha satisfies standard Robbins-Monro stochastic approximation conditions, linear semi-gradient TD(0) is guaranteed to converge to the unique TD fixed point wTD\mathbf{w}_{\text{TD}}:

AwTD=b  ⟹  wTD=A−1b\mathbf{A} \mathbf{w}_{\text{TD}} = \mathbf{b} \implies \mathbf{w}_{\text{TD}} = \mathbf{A}^{-1} \mathbf{b}

The matrix A\mathbf{A} is guaranteed to be positive definite whenever the feature covariance matrix has full rank and γ<1\gamma < 1.

Furthermore, Tsitsiklis and Van Roy (1997) proved that the Mean Squared Value Error at this fixed point is strictly bounded:

VE‾(wTD)≤11−γmin⁡wVE‾(w)\overline{\text{VE}}(\mathbf{w}_{\text{TD}}) \le \frac{1}{1 - \gamma} \min_{\mathbf{w}} \overline{\text{VE}}(\mathbf{w})

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 StS_t with feature vector: x(St)=[1.0,2.0]⊤\mathbf{x}(S_t) = [1.0, 2.0]^\top.
  • Next state St+1S_{t+1} with feature vector: x(St+1)=[0.5,1.0]⊤\mathbf{x}(S_{t+1}) = [0.5, 1.0]^\top.
  • Current weight vector: wt=[0.5,1.0]⊤\mathbf{w}_t = [0.5, 1.0]^\top.
  • Observed transition reward: Rt+1=2.0R_{t+1} = 2.0.
  • Hyperparameters: discount factor γ=0.9\gamma = 0.9, learning rate α=0.1\alpha = 0.1.

1. Forward Predictions

v^(St,wt)=wt⊤x(St)=(0.5×1.0)+(1.0×2.0)=0.5+2.0=2.500\hat{v}(S_t, \mathbf{w}_t) = \mathbf{w}_t^\top \mathbf{x}(S_t) = (0.5 \times 1.0) + (1.0 \times 2.0) = 0.5 + 2.0 = \mathbf{2.500}

v^(St+1,wt)=wt⊤x(St+1)=(0.5×0.5)+(1.0×1.0)=0.25+1.0=1.250\hat{v}(S_{t+1}, \mathbf{w}_t) = \mathbf{w}_t^\top \mathbf{x}(S_{t+1}) = (0.5 \times 0.5) + (1.0 \times 1.0) = 0.25 + 1.0 = \mathbf{1.250}

2. Bootstrapped Target Calculation

Treating v^(St+1,wt)\hat{v}(S_{t+1}, \mathbf{w}_t) as a detached constant: Ut=Rt+1+γv^(St+1,wt)=2.0+0.9×1.250=2.0+1.125=3.125U_t = R_{t+1} + \gamma \hat{v}(S_{t+1}, \mathbf{w}_t) = 2.0 + 0.9 \times 1.250 = 2.0 + 1.125 = \mathbf{3.125}

3. Temporal Difference Error

δt=Ut−v^(St,wt)=3.125−2.500=+0.625\delta_t = U_t - \hat{v}(S_t, \mathbf{w}_t) = 3.125 - 2.500 = \mathbf{+0.625}

4. Semi-Gradient Weight Update

The gradient with respect to w\mathbf{w} evaluates solely at the current state: ∇v^(St)=x(St)\nabla \hat{v}(S_t) = \mathbf{x}(S_t): Δw=αδtx(St)=0.1×0.625×[1.02.0]=0.0625×[1.02.0]=[0.06250.1250]\Delta \mathbf{w} = \alpha \delta_t \mathbf{x}(S_t) = 0.1 \times 0.625 \times \begin{bmatrix} 1.0 \\ 2.0 \end{bmatrix} = 0.0625 \times \begin{bmatrix} 1.0 \\ 2.0 \end{bmatrix} = \begin{bmatrix} 0.0625 \\ 0.1250 \end{bmatrix}

wt+1=wt+Δw=[0.5+0.06251.0+0.1250]=[0.56251.1250]\mathbf{w}_{t+1} = \mathbf{w}_t + \Delta \mathbf{w} = \begin{bmatrix} 0.5 + 0.0625 \\ 1.0 + 0.1250 \end{bmatrix} = \begin{bmatrix} \mathbf{0.5625} \\ \mathbf{1.1250} \end{bmatrix}

5. Verify Updated Current State Estimate

v^(St,wt+1)=(0.5625×1.0)+(1.1250×2.0)=0.5625+2.250=2.8125\hat{v}(S_t, \mathbf{w}_{t+1}) = (0.5625 \times 1.0) + (1.1250 \times 2.0) = 0.5625 + 2.250 = \mathbf{2.8125}

The prediction moved toward the target 3.1253.125 by exactly Δv^=0.3125\Delta \hat{v} = 0.3125, proportional to the step size and feature norm squared (αδt∥x∥2=0.0625×5=0.3125\alpha \delta_t \|\mathbf{x}\|^2 = 0.0625 \times 5 = 0.3125).

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:

  1. Function Approximation (linear or non-linear neural networks)
  2. Bootstrapping (updating estimates from subsequent estimates, as in TD)
  3. Off-Policy Training (evaluating target policy π\pi while collecting data with behavior policy bb)

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 (w−\mathbf{w}^-), temporarily transforming the semi-gradient update into an almost stationary supervised regression target.

The Quick Version

  • The semi-gradient shortcut: The bootstrapped target Rt+1+γv^(St+1,w)R_{t+1} + \gamma \hat{v}(S_{t+1}, \mathbf{w}) contains parameters w\mathbf{w}, but semi-gradient methods treat it as a detached constant (∇wTarget≡0\nabla_{\mathbf{w}} \text{Target} \equiv \mathbf{0}) during differentiation.
  • Linear update formula: With linear features, the gradient is simply the feature vector (∇v^=x(s)\nabla \hat{v} = \mathbf{x}(s)), simplifying updates to wt+1=wt+αδtx(St)\mathbf{w}_{t+1} = \mathbf{w}_t + \alpha \delta_t \mathbf{x}(S_t) without backpropagation.
  • The TD fixed point: Linear on-policy semi-gradient TD converges to a unique stationary point wTD=A−1b\mathbf{w}_{\text{TD}} = \mathbf{A}^{-1}\mathbf{b} 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.