| """ |
| Adaptive decoding controller implementing proposal Phase Four (closed loop). |
| |
| Workflow (proposal): |
| Measure state → Compute MEA distance → Derive Reward → Compute TD Error |
| → Adjust Decoding Parameters |
| |
| Equations: |
| (xxii) s_t = {λ(model), λ(human), N̄, B̄, Q̄} [state representation] |
| (xxxiii) r_{t+1} = D(s_t, x) - D(s_{t+1}, x) [reward signal] |
| (xxxiv) V_ψ(s_t, x) ≈ E[ Σ_ℓ γ^ℓ r_{t+1+ℓ} | s_t, x ] [TD value function] |
| (xxxv) δ_t(x) = r_{t+1} + γ·V_ψ(s_{t+1}, x) - V_ψ(s_t, x) [TD error] |
| (xxxvi) V_ψ(s_t, x) ← V_ψ(s_t, x) + α·δ_t(x) [value update] |
| (xxxvii) θ_{t+1} = clip(θ_t + η·δ_t(x)·c, θ_min, θ_max) [decoding update] |
| |
| θ_t = {τ_t, p_t, k_t}: temperature, top-p (nucleus), top-k. |
| c is the fixed heuristic control direction; it is chosen once per episode |
| from the dominant component of the MEA gap vector Δ(s_t, x) (Eq xxxi): |
| a novelty deficit points c toward more exploration (+τ, +p, +k), a bias |
| excess or quality deficit points it toward more conservative decoding |
| (-τ, -p, -k), and a decay mismatch follows the sign of λ(model)-λ(human). |
| |
| Bootstrap of the first update: before any regeneration there is no observed |
| transition, so no δ_t exists yet. With V_ψ ≡ 0 initially, the expected |
| reward of moving from s_0 to the target end-state (distance 0) is |
| r̂ = D(s_0) - 0, hence δ_0 := D(s_0) ≥ 0 and the first action moves along c |
| with magnitude η·D(s_0). Every subsequent update uses the observed δ_t of |
| Equation (xxxv) exactly. |
| """ |
|
|
| from dataclasses import dataclass, field |
| from typing import Dict, List, Optional, Tuple |
| import math |
|
|
|
|
| @dataclass |
| class FrameworkState: |
| """State representation s_t from Equation (xxii).""" |
| lambda_model: Optional[float] |
| lambda_human: float |
| aggregate_novelty: Optional[float] |
| aggregate_bias: Optional[float] |
| aggregate_quality: Optional[float] |
|
|
| def as_dict(self) -> Dict[str, Optional[float]]: |
| return { |
| "lambda_model": self.lambda_model, |
| "lambda_human": self.lambda_human, |
| "aggregate_novelty": self.aggregate_novelty, |
| "aggregate_bias": self.aggregate_bias, |
| "aggregate_quality": self.aggregate_quality, |
| } |
|
|
| def key(self, resolution: float = 0.05) -> str: |
| """Discretised key for the tabular value function V_ψ.""" |
| def bucket(v: Optional[float]) -> str: |
| if v is None: |
| return "na" |
| return str(round(float(v) / resolution) * resolution) |
|
|
| return "|".join([ |
| bucket(self.lambda_model), |
| bucket(self.aggregate_novelty), |
| bucket(self.aggregate_bias), |
| bucket(self.aggregate_quality), |
| ]) |
|
|
|
|
| @dataclass |
| class DecodingParameters: |
| """Decoding parameter vector θ_t = {τ_t, p_t, k_t}.""" |
| temperature: float |
| top_p: float |
| top_k: int |
|
|
| def as_tuple(self) -> Tuple[float, float, int]: |
| return (self.temperature, self.top_p, self.top_k) |
|
|
|
|
| @dataclass |
| class ControlStep: |
| """Record of one controller transition (for reports/audit).""" |
| theta_before: DecodingParameters |
| theta_after: DecodingParameters |
| distance_before: float |
| distance_after: Optional[float] |
| reward: Optional[float] |
| td_error: float |
| value_before: float |
| value_after: float |
| direction: Dict[str, float] |
| dominant_gap: str |
| rationale: str |
|
|
|
|
| class AdaptiveController: |
| """ |
| TD-driven adaptive decoding controller (Equations xxii, xxxiii-xxxvii). |
| """ |
|
|
| |
| TEMP_MIN, TEMP_MAX = 0.20, 1.20 |
| TOP_P_MIN, TOP_P_MAX = 0.70, 0.98 |
| TOP_K_MIN, TOP_K_MAX = 10, 200 |
|
|
| |
| |
| |
| CONTROL_DIRECTIONS = { |
| "novelty_deficit": {"temperature": +1.0, "top_p": +0.3, "top_k": +0.5}, |
| "bias_excess": {"temperature": -1.0, "top_p": -0.3, "top_k": -0.5}, |
| "quality_deficit": {"temperature": -1.0, "top_p": -0.2, "top_k": -0.3}, |
| |
| |
| "decay_mismatch_fast": {"temperature": +1.0, "top_p": +0.3, "top_k": +0.5}, |
| "decay_mismatch_slow": {"temperature": -0.5, "top_p": -0.15, "top_k": -0.25}, |
| "none": {"temperature": 0.0, "top_p": 0.0, "top_k": 0.0}, |
| } |
|
|
| def __init__( |
| self, |
| targets: Optional[Dict[str, float]] = None, |
| eta: float = 0.15, |
| gamma: float = 0.95, |
| alpha: float = 0.10, |
| human_lambda: float = 0.15, |
| state_resolution: float = 0.05, |
| ): |
| """ |
| Args: |
| targets: Optional static fallback targets for the legacy API |
| eta: Adaptation rate η in Equation (xxxvii) |
| gamma: Discount factor γ in Equations (xxxiv)-(xxxv) |
| alpha: Value learning rate α in Equation (xxxvi) |
| human_lambda: Human decay baseline λ(human) |
| state_resolution: Discretisation step for the tabular V_ψ |
| """ |
| self.targets = targets or {} |
| self.eta = float(eta) |
| self.gamma = float(gamma) |
| self.alpha = float(alpha) |
| self.human_lambda = float(human_lambda) |
| self.state_resolution = float(state_resolution) |
|
|
| |
| self.value_table: Dict[str, float] = {} |
| self.history: List[ControlStep] = [] |
|
|
| |
| self.q_table: Dict[str, Dict[str, float]] = {} |
| self.experience_buffer: List[Dict] = [] |
|
|
| |
| |
| |
|
|
| def build_state( |
| self, |
| metrics: Dict[str, Optional[float]], |
| lambda_model: Optional[float] = None, |
| ) -> FrameworkState: |
| """Assemble s_t = {λ(model), λ(human), N̄, B̄, Q̄} from metrics.""" |
| novelty = metrics.get("Novelty", metrics.get("novelty")) |
| bias = metrics.get("Bias Proxy", metrics.get("bias_proxy")) |
| quality = ( |
| metrics.get("Quality (Q)") |
| or metrics.get("quality_q") |
| or metrics.get("BERTScore F1") |
| or metrics.get("bertscore") |
| or metrics.get("Fallback Quality") |
| or metrics.get("fallback_quality") |
| ) |
| lam = lambda_model if lambda_model is not None else metrics.get( |
| "decay_rate", metrics.get("Decay Rate (λ)") |
| ) |
| return FrameworkState( |
| lambda_model=_maybe_float(lam), |
| lambda_human=self.human_lambda, |
| aggregate_novelty=_maybe_float(novelty), |
| aggregate_bias=_maybe_float(bias), |
| aggregate_quality=_maybe_float(quality), |
| ) |
|
|
| |
| |
| |
|
|
| def value(self, state: FrameworkState) -> float: |
| """Current estimate V_ψ(s, x).""" |
| return self.value_table.get(state.key(self.state_resolution), 0.0) |
|
|
| def compute_reward(self, distance_before: float, distance_after: float) -> float: |
| """Reward signal from Equation (xxxiii): r_{t+1} = D(s_t) - D(s_{t+1}).""" |
| return float(distance_before) - float(distance_after) |
|
|
| def compute_td_error( |
| self, |
| reward: float, |
| state: FrameworkState, |
| next_state: FrameworkState, |
| ) -> float: |
| """TD error from Equation (xxxv): δ_t = r + γ·V(s_{t+1}) - V(s_t).""" |
| return reward + self.gamma * self.value(next_state) - self.value(state) |
|
|
| def update_value(self, state: FrameworkState, td_error: float) -> float: |
| """Value update from Equation (xxxvi): V(s_t) ← V(s_t) + α·δ_t.""" |
| key = state.key(self.state_resolution) |
| self.value_table[key] = self.value_table.get(key, 0.0) + self.alpha * td_error |
| return self.value_table[key] |
|
|
| |
| |
| |
|
|
| def select_control_direction( |
| self, |
| gap_vector: Dict[str, float], |
| state: FrameworkState, |
| ) -> Tuple[str, Dict[str, float]]: |
| """ |
| Pick the fixed heuristic direction c from the dominant component of |
| the MEA gap vector Δ(s_t, x) (Eq xxxi). |
| """ |
| if not gap_vector: |
| return "none", dict(self.CONTROL_DIRECTIONS["none"]) |
|
|
| dominant, value = max(gap_vector.items(), key=lambda kv: kv[1]) |
| if value <= 1e-12: |
| return "none", dict(self.CONTROL_DIRECTIONS["none"]) |
|
|
| if dominant == "decay_mismatch": |
| lam = state.lambda_model if state.lambda_model is not None else state.lambda_human |
| key = "decay_mismatch_fast" if lam >= state.lambda_human else "decay_mismatch_slow" |
| return dominant, dict(self.CONTROL_DIRECTIONS[key]) |
|
|
| return dominant, dict(self.CONTROL_DIRECTIONS.get(dominant, self.CONTROL_DIRECTIONS["none"])) |
|
|
| |
| |
| |
|
|
| def apply_parameter_update( |
| self, |
| theta: DecodingParameters, |
| td_error: float, |
| direction: Dict[str, float], |
| ) -> DecodingParameters: |
| """Apply Equation (xxxvii) with per-parameter clipping.""" |
| step = self.eta * td_error |
| new_temp = theta.temperature + step * direction.get("temperature", 0.0) |
| new_top_p = theta.top_p + step * direction.get("top_p", 0.0) |
| top_k_range = self.TOP_K_MAX - self.TOP_K_MIN |
| new_top_k = theta.top_k + step * direction.get("top_k", 0.0) * top_k_range |
|
|
| return DecodingParameters( |
| temperature=_clip(new_temp, self.TEMP_MIN, self.TEMP_MAX), |
| top_p=_clip(new_top_p, self.TOP_P_MIN, self.TOP_P_MAX), |
| top_k=int(round(_clip(new_top_k, self.TOP_K_MIN, self.TOP_K_MAX))), |
| ) |
|
|
| |
| |
| |
|
|
| def initial_step( |
| self, |
| state: FrameworkState, |
| distance_result: Dict, |
| theta: DecodingParameters, |
| ) -> ControlStep: |
| """ |
| First controller action of an episode (bootstrap). |
| |
| No transition has been observed yet, so with V_ψ ≡ 0 the expected |
| reward of reaching the target end-state (D = 0) is D(s_0), giving |
| δ_0 := D(s_0) + γ·V(s_0) - V(s_0-implicit-start) = D(s_0) when the |
| table is empty. The action direction is the fixed heuristic c for |
| the dominant gap. |
| """ |
| distance = float(distance_result.get("total_distance", 0.0)) |
| gap_vector = distance_result.get("gap_vector", {}) or {} |
| dominant, direction = self.select_control_direction(gap_vector, state) |
|
|
| td_error = distance + self.gamma * self.value(state) - self.value(state) |
| theta_next = self.apply_parameter_update(theta, td_error, direction) |
|
|
| step = ControlStep( |
| theta_before=theta, |
| theta_after=theta_next, |
| distance_before=distance, |
| distance_after=None, |
| reward=None, |
| td_error=td_error, |
| value_before=self.value(state), |
| value_after=self.value(state), |
| direction=direction, |
| dominant_gap=dominant, |
| rationale=( |
| f"Bootstrap step: dominant gap '{dominant}' " |
| f"(D(s_0)={distance:.4f}); θ moved along fixed direction c " |
| f"with magnitude η·δ_0 = {self.eta:.2f}·{td_error:.4f}." |
| ), |
| ) |
| self.history.append(step) |
| return step |
|
|
| def transition_step( |
| self, |
| state: FrameworkState, |
| next_state: FrameworkState, |
| distance_before: Dict, |
| distance_after: Dict, |
| theta: DecodingParameters, |
| ) -> ControlStep: |
| """ |
| Full observed transition: reward (Eq xxxiii), TD error (Eq xxxv), |
| value update (Eq xxxvi), and next parameter proposal (Eq xxxvii). |
| """ |
| d_before = float(distance_before.get("total_distance", 0.0)) |
| d_after = float(distance_after.get("total_distance", 0.0)) |
|
|
| reward = self.compute_reward(d_before, d_after) |
| td_error = self.compute_td_error(reward, state, next_state) |
| value_before = self.value(state) |
| value_after = self.update_value(state, td_error) |
|
|
| gap_vector = distance_after.get("gap_vector", {}) or {} |
| dominant, direction = self.select_control_direction(gap_vector, next_state) |
| theta_next = self.apply_parameter_update(theta, td_error, direction) |
|
|
| step = ControlStep( |
| theta_before=theta, |
| theta_after=theta_next, |
| distance_before=d_before, |
| distance_after=d_after, |
| reward=reward, |
| td_error=td_error, |
| value_before=value_before, |
| value_after=value_after, |
| direction=direction, |
| dominant_gap=dominant, |
| rationale=( |
| f"Observed transition: r={reward:+.4f} (Eq xxxiii), " |
| f"δ={td_error:+.4f} (Eq xxxv), V(s) {value_before:.4f}→{value_after:.4f} " |
| f"(Eq xxxvi); next dominant gap '{dominant}'." |
| ), |
| ) |
| self.history.append(step) |
| return step |
|
|
| @staticmethod |
| def describe_theta(theta: DecodingParameters) -> str: |
| return ( |
| f"temperature={theta.temperature:.2f}, top_p={theta.top_p:.2f}, " |
| f"top_k={theta.top_k}" |
| ) |
|
|
| |
| |
| |
| |
|
|
| def recommend_parameters( |
| self, |
| metrics: Dict[str, Optional[float]], |
| current_temp: float, |
| current_top_p: float |
| ) -> str: |
| """Legacy heuristic recommendation (pre-TD fallback).""" |
| novelty = metrics.get("Novelty", metrics.get("novelty")) |
| self_bleu = metrics.get("Self-BLEU", metrics.get("self_bleu")) |
| quality = ( |
| metrics.get("BERTScore F1") |
| or metrics.get("bertscore") |
| or metrics.get("Fallback Quality") |
| or metrics.get("fallback_quality") |
| ) |
| bias = metrics.get("Bias Proxy", metrics.get("bias_proxy")) |
| ppl = metrics.get("Perplexity", metrics.get("perplexity")) |
|
|
| new_temp = float(current_temp) |
| new_top_p = float(current_top_p) |
| reasons = [] |
|
|
| if novelty is not None and novelty < 0.35: |
| new_temp += 0.10 |
| new_top_p += 0.03 |
| reasons.append("novelty is below target (increase exploration)") |
|
|
| if self_bleu is not None and self_bleu > 45.0: |
| new_temp += 0.05 |
| new_top_p += 0.03 |
| reasons.append("Self-BLEU is high (increase diversity)") |
|
|
| if quality is not None and quality < 0.75: |
| new_temp -= 0.05 |
| reasons.append("quality is below target (decrease randomness)") |
|
|
| if bias is not None and bias > 0.25: |
| new_temp -= 0.05 |
| reasons.append("bias/risk proxy is above target (decrease randomness)") |
|
|
| if ppl is not None and math.isfinite(ppl) and ppl > 80.0: |
| new_temp -= 0.05 |
| new_top_p -= 0.02 |
| reasons.append("perplexity is high (decrease uncertainty)") |
|
|
| new_temp = _clip(new_temp, self.TEMP_MIN, self.TEMP_MAX) |
| new_top_p = _clip(new_top_p, self.TOP_P_MIN, self.TOP_P_MAX) |
|
|
| if not reasons: |
| reasons.append("metrics are close to target state") |
|
|
| return ( |
| f"Suggested: temperature={new_temp:.2f}, top_p={new_top_p:.2f}. " |
| f"Reason: {', '.join(reasons)}" |
| ) |
|
|
|
|
| def _clip(value: float, lo: float, hi: float) -> float: |
| return min(hi, max(lo, float(value))) |
|
|
|
|
| def _maybe_float(value) -> Optional[float]: |
| if value is None: |
| return None |
| try: |
| f = float(value) |
| except (TypeError, ValueError): |
| return None |
| if math.isnan(f) or math.isinf(f): |
| return None |
| return f |
|
|