Streaming Entropy with Numerical Stability
OpenAIMachine Learning Engineer · Technical ScreenLast updated
Implement an online, numerically stable algorithm to compute Shannon entropy over an unbounded stream of categorical tokens with O(1) updates.
Streaming Entropy with Numerical Stability
In machine learning pipelines, monitoring the Shannon entropy of token distributions (e.g., model predictions, vocabulary usage, or feature categories) is essential for detecting distribution shift and model collapse in real time.
Design and implement a class StreamingEntropy that tracks the Shannon entropy of an unbounded stream of categorical tokens in base-2 (bits).
Mathematical Definition
Given a stream with total token count N and distinct token counts c_i, the empirical probability of token i is p_i = c_i / N. The Shannon entropy H in bits is defined as:
H = - sum(p_i * log2(p_i)) for all unique tokens i where p_i > 0.
By convention, if N == 0, H = 0.0.
Requirements
add(token: str) -> None: Ingests a new token into the stream. Must run inO(1)amortized time.get_entropy() -> float: Returns the current Shannon entropy in bits inO(1)time.- Numerical Stability: Standard naive accumulation using
log2(N) - (1/N) * sum(c_i * log2(c_i))suffers from catastrophic cancellation whenHapproaches0(e.g., when one token dominates the distribution) and precision degradation whenNis large. Your solution must maintain numerical accuracy even for streams with millions of tokens and skewed distributions.
Interface
class StreamingEntropy:
def __init__(self):
pass
def add(self, token: str) -> None:
pass
def get_entropy(self) -> float:
pass
Examples
Example 1:
stream = StreamingEntropy()
stream.add("A")
stream.get_entropy() # Returns 0.0 (only one unique token)
stream.add("B")
stream.get_entropy() # Returns 1.0 (two tokens, each with p = 0.5)
stream.add("A")
stream.add("B")
stream.get_entropy() # Returns 1.0 (counts: A=2, B=2)
Example 2:
stream = StreamingEntropy()
for _ in range(3):
stream.add("A")
stream.add("B")
# Counts: A=3, B=1, N=4
# H = -(3/4 * log2(3/4) + 1/4 * log2(1/4))
# H = -(0.75 * (-0.415037) + 0.25 * (-2.0)) = 0.811278...
stream.get_entropy() # Returns approximately 0.8112781244591328
Constraints
- The total number of tokens
Ncan exceed10^7. - The number of unique tokens
Kcan be up to10^5. - Memory consumption must be
O(K), proportional to the number of unique tokens seen.
Hints
Hint 1
Express the Shannon entropy as H = log2(N) - (1/N) * S, where S = sum(c_i * log2(c_i)), and examine how S and N change incrementally upon each token insertion.
Hint 2
When updating (c + 1) * log2(c + 1) - c * log2(c), expand the term as log2(c + 1) + c * log2(1 + 1/c) to avoid precision loss for large counts.
Hint 3
To eliminate catastrophic cancellation when H is near zero, use math.log1p (converted to base 2) for terms involving log(1 + 1/x).
Follow-up questions
- How would you modify this class to support a sliding window of the last W tokens using an O(1) eviction operation?
- How would you adapt the numerical stability techniques if counts decay exponentially over time (e.g., c_i(t) = gamma * c_i(t-1))?
- If the vocabulary size K is too large to fit in memory, how would you approximate streaming entropy using count-min sketches or reservoir sampling?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.