import torch
import torch.nn.functional as F
from typing import List, Optional
class EarlyStopReasoner:
def __init__(
self,
model,
tokenizer,
entropy_threshold: float = 0.3,
min_steps: int = 10,
max_steps: int = 1000,
window_size: int = 5,
):
self.model = model
self.tokenizer = tokenizer
self.entropy_threshold = entropy_threshold
self.min_steps = min_steps
self.max_steps = max_steps
self.window_size = window_size
def _estimate_answer_entropy(
self,
logits: torch.Tensor,
generated_ids: List[int]
) -> float:
"""估计当前推理状态下最终答案的条件熵"""
probs = F.softmax(logits[0, -1, :], dim=-1)
top_k = 50
top_probs, top_indices = torch.topk(probs, top_k)
entropy = -torch.sum(top_probs * torch.log(top_probs + 1e-10))
max_entropy = torch.log(torch.tensor(top_k, dtype=torch.float))
normalized_entropy = entropy / max_entropy
return normalized_entropy.item()
@torch.no_grad()
def generate(self, prompt: str) -> tuple[str, int, float]:
inputs = self.tokenizer(prompt, return_tensors="pt")
input_ids = inputs["input_ids"]
attention_mask = inputs.get("attention_mask")
device = next(self.model.parameters()).device
input_ids = input_ids.to(device)
if attention_mask is not None:
attention_mask = attention_mask.to(device)
past_key_values = None
generated = input_ids[0].tolist()
entropy_history = []
early_stopped = False
for step in range(self.max_steps):
with torch.no_grad():
outputs = self.model(
input_ids=input_ids,
attention_mask=attention_mask,
past_key_values=past_key_values,
use_cache=True,
)
logits = outputs.logits
past_key_values = outputs.past_key_values
if step >= self.min_steps:
entropy = self._estimate_answer_entropy(logits, generated)
entropy_history.append(entropy)
if len(entropy_history) >= self.window_size:
avg_entropy = sum(
entropy_history[-self.window_size:]
) / self.window_size
if avg_entropy < self.entropy_threshold:
early_stopped = True
break
next_token = logits[0, -1].argmax(dim=-1).unsqueeze(0)
input_ids = next_token.unsqueeze(0)
generated.append(next_token.item())
if next_token.item() == self.tokenizer.eos_token_id:
break
decoded = self.tokenizer.decode(generated)
total_steps = len(entropy_history) if early_stopped else step + 1
return decoded, total_steps, entropy_history[-1] if entropy_history else 1.0