Log-Probability of a Softmax Policy
Problem Statement
Policy-gradient methods need log pi(a|s). Given action preferences (logits) logits and a chosen action index a, return the log-probability of that action under the softmax policy:
logπ(a)=ha−log∑behb
Implement log_prob(logits, a) using the log-sum-exp trick for stability.
Example:
log_prob([0.0, 0.0], 0)
-0.6931
- Identify the maximum logit m to ensure numerical stability in the exponential calculations: m=max(0.0,0.0)=0.0.
- Compute the shifted exponentials for each logit hb by subtracting m: e0.0−0.0=1.0 for both actions.
- Sum these shifted values to get the unnormalized partition function: ∑=1.0+1.0=2.0.
- Calculate the log-sum-exp (LSE) value by adding the max logit back to the log of the sum: LSE=0.0+ln(2.0)≈0.6931.
- Subtract the LSE from the logit of the chosen action a=0 to obtain the log-probability: logπ(0)=0.0−0.6931=−0.6931.
- The final output is -0.6931
Constraints:
0 <= a < len(logits).- Return a float.
1. Background Knowledge
In reinforcement learning, a policy π(a∣s) maps states to a probability distribution over actions. When actions are discrete, this is typically parameterized by a softmax over a vector of unnormalized scores called logits. If hb is the logit for action b, the probability of action a is:
π(a)=∑behbehaPolicy-gradient algorithms (e.g., REINFORCE, A2C) require the log-probability logπ(a) rather than the probability itself. This is because gradients of the log-likelihood are numerically more stable and algebraically simpler. Taking the log of the softmax gives:
logπ(a)=ha−logb∑ehbThe term log∑behb is the log-sum-exp (LSE) of the logits. Directly computing ∑behb can overflow for large logits or underflow for very negative ones. The log-sum-exp trick stabilizes this by shifting all logits by a constant m (typically m=maxbhb):
LSE(h)=m+logb∑ehb−mSince m is the maximum, every hb−m≤0, so each exponential is in (0,1] and the sum cannot overflow. The shift m is added back to preserve the correct value.
2. Algorithm Approach
The core pattern is the log-sum-exp trick:
- Find the maximum logit m=maxbhb.
- Subtract m from every logit to get shifted values hb−m.
- Exponentiate the shifted values and sum them.
- Take the natural log of that sum.
- Add m back to get the stable LSE.
- Subtract the LSE from the chosen action's logit ha to get logπ(a).
This is a standard numerical-stability technique used throughout machine learning (e.g., in cross-entropy loss, attention mechanisms, and softmax layers).
3. Step-by-Step Strategy
- Extract the max: Compute m=max(logits). This is a single pass over the array.
- Shift and exponentiate: For each logit hb, compute ehb−m. Because hb−m≤0, these values are bounded above by 1.
- Sum the exponentials: Accumulate ∑behb−m.
- Log and unshift: Compute m+log(sum) to obtain the stable LSE.
- Subtract for log-probability: Return logits[a]−LSE.
In code, this looks roughly like:
import numpy as np
def log_prob(logits, a):
m = np.max(logits)
shifted = logits - m
lse = m + np.log(np.sum(np.exp(shifted)))
return logits[a] - lse
The key insight is that you never compute ∑ehb directly; you always work with the shifted version.
4. Common Pitfalls
- Skipping the shift: Computing log(∑ehb) directly will overflow if any hb>∼709 (the log of the max float64) or underflow if all hb are very negative.
- Using the wrong max: The shift must be the maximum of the logits, not the mean or zero. Using a smaller shift can still cause overflow.
- Index confusion: Make sure you subtract the LSE from logits[a] (the chosen action's logit), not from the max or some other element.
- Integer logits: If logits is an integer array, logits - m stays integer and np.exp may behave unexpectedly. Cast to float first.
- Empty or single-element edge cases: With one action, the LSE equals that logit and logπ(a)=0, which is correct (π=1). Verify your implementation handles this gracefully.
5. Time & Space Complexity
- Time: O(n) where n is the number of actions (length of logits). You scan once for the max, once for the shifted exponentials, and once for the sum. These are all linear passes.
- Space: O(n) if you materialize the shifted array, or O(1) extra space if you compute the sum in a single loop without storing intermediate values. In practice, vectorized NumPy implementations use O(n) temporary storage.