Uncertainty Quantification › Code Implementation
Language AI HandbookAll chapters Uncertainty Quantification › Code ImplementationOne section · Sections in this chapter6 / 11 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Let's build a practical uncertainty quantification system. We'll implement token-level confidence extraction, temperature scaling calibration, and sampling-based uncertainty estimation.
Setup and Data
We start with an implementation that works with any model through the Hugging Face Transformers API. For this example, we'll use a small model and simulate a factual QA scenario to illustrate the key ideas.
import warnings
import numpy as np
warnings.filterwarnings("ignore")
# Reproducibility
np.random.seed(42)Simulating Model Confidence Scores
In a real deployment, you'd extract logits from a language model. Here we simulate the behavior of a well-documented phenomenon: overconfident LLMs that assign high probabilities even when wrong.
def simulate_lm_confidences(n_samples=500, overconfidence_factor=2.0):
"""
Simulate an overconfident language model.
True probabilities are drawn from a beta distribution (realistic spread).
Observed confidences are a monotone transform that stretches toward 1.
"""
# True underlying probabilities
true_probs = np.random.beta(3, 2, n_samples) # skewed toward higher values
# Overconfident model: compress toward 1 using a power < 1
# overconfidence_factor > 1 means logit is scaled up
logit_true = np.log(true_probs / (1 - true_probs + 1e-9))
logit_observed = logit_true * overconfidence_factor
observed_confidences = 1 / (1 + np.exp(-logit_observed))
# Generate binary outcomes: correct (1) or incorrect (0)
correct = (np.random.uniform(0, 1, n_samples) < true_probs).astype(int)
return observed_confidences, correct, true_probs
# Generate data
confidences, correct, true_probs = simulate_lm_confidences(n_samples=1000)Mean model confidence: 0.644 Actual accuracy: 0.616 Overconfidence gap: 0.028 Samples: 1000
The gap between mean confidence and actual accuracy quantifies the overconfidence. In this simulation, the model claims an average confidence well above its actual accuracy rate, reproducing what has been observed in empirical studies of large language models.
Calibration Curve and ECE
Let's compute and visualize the calibration curve.
def compute_ece(confidences, correct, n_bins=10):
"""Compute Expected Calibration Error (ECE)."""
bin_boundaries = np.linspace(0, 1, n_bins + 1)
ece = 0.0
n = len(confidences)
for i in range(n_bins):
lower, upper = bin_boundaries[i], bin_boundaries[i + 1]
mask = (confidences >= lower) & (confidences < upper)
if mask.sum() == 0:
continue
bin_conf = confidences[mask].mean()
bin_acc = correct[mask].mean()
bin_weight = mask.sum() / n
ece += bin_weight * abs(bin_conf - bin_acc)
return ece
ece_before = compute_ece(confidences, correct)ECE before calibration: 0.1026
Temperature Scaling
Now we implement temperature scaling to correct the overconfidence. Temperature scaling finds the single scalar that minimizes the negative log-likelihood on the calibration set.
from scipy.optimize import minimize_scalar
def apply_temperature(logits, temperature):
"""Apply temperature scaling to logits and return probabilities."""
scaled = logits / temperature
return 1 / (1 + np.exp(-scaled)) # sigmoid for binary
def calibrate_temperature(confidences, correct):
"""
Find optimal temperature by minimizing negative log-likelihood
on a calibration set (here we use the full dataset for illustration).
"""
# Convert confidences back to approximate logits
eps = 1e-7
logits = np.log(confidences / (1 - confidences + eps) + eps)
def nll(temperature):
if temperature <= 0:
return 1e9
probs = apply_temperature(logits, temperature)
probs = np.clip(probs, eps, 1 - eps)
return -np.mean(
correct * np.log(probs) + (1 - correct) * np.log(1 - probs)
)
result = minimize_scalar(nll, bounds=(0.1, 10.0), method="bounded")
return result.x
logits = np.log(confidences / (1 - confidences + 1e-7) + 1e-7)
optimal_temp = calibrate_temperature(confidences, correct)
calibrated_confidences = apply_temperature(logits, optimal_temp)
ece_after = compute_ece(calibrated_confidences, correct)Optimal temperature: 2.107 ECE before calibration: 0.1026 ECE after calibration: 0.0531 ECE reduction: 48.3%
The optimal temperature is greater than 1.0, confirming the model was overconfident (temperature above 1 softens probabilities). The ECE drops substantially after calibration.
Sampling-Based Uncertainty
Now let's implement sampling-based uncertainty estimation. We simulate multiple samples from a model on the same question and measure semantic consistency.
from scipy.stats import entropy
def simulate_model_samples(question_difficulty, n_samples=10, noise_level=0.3):
"""
Simulate model answers for a question of given difficulty.
difficulty: 0 (easy) to 1 (hard)
Returns: list of 0/1 answers (0=wrong, 1=correct)
"""
# Move from a certain correct answer toward a maximally uncertain 50/50 split.
true_prob = 1.0 - 0.5 * question_difficulty
# Add sampling noise
sample_probs = np.clip(
true_prob + np.random.normal(0, noise_level, n_samples), 0.05, 0.95
)
answers = (np.random.uniform(0, 1, n_samples) < sample_probs).astype(int)
return answers
def sampling_uncertainty(answers):
"""
Compute uncertainty metrics from binary (0/1) answer samples.
Returns confidence (agreement fraction) and entropy.
"""
p_correct = np.mean(answers)
agreement = max(p_correct, 1 - p_correct)
# Binary entropy
h = (
entropy([p_correct, 1 - p_correct], base=2)
if 0 < p_correct < 1
else 0.0
)
return agreement, h
# Test across different difficulty levels
difficulties = np.linspace(0, 1, 100)
n_samples = 20
agreements = []
entropies = []
for d in difficulties:
answers = simulate_model_samples(d, n_samples=n_samples)
agreement, h = sampling_uncertainty(answers)
agreements.append(agreement)
entropies.append(h)Easy question (diff=0.1): agreement=0.80, entropy=0.722 Medium question (diff=0.5): agreement=0.80, entropy=0.722 Hard question (diff=0.9): agreement=0.60, entropy=0.971
Self-Consistency Score
Self-consistency computes confidence from majority voting across chain-of-thought samples. The confidence score is simply the fraction of samples that agree with the majority answer.
from collections import Counter
def self_consistency_confidence(answers):
"""
Given a list of sampled answers (any hashable type),
return the self-consistency confidence (majority fraction).
"""
counts = Counter(answers)
majority_count = counts.most_common(1)[0][1]
return majority_count / len(answers)
def run_self_consistency_experiment(n_questions=200):
"""
Simulate self-consistency over questions with varying difficulty.
Returns a list of dicts with difficulty, sc_confidence, and correctness.
"""
results = []
difficulties = np.random.uniform(0, 1, n_questions)
for diff in difficulties:
answers = simulate_model_samples(diff, n_samples=10)
sc_conf = self_consistency_confidence(answers)
majority_answer = Counter(answers).most_common(1)[0][0]
results.append(
{
"difficulty": diff,
"sc_confidence": sc_conf,
"correct": int(majority_answer == 1),
}
)
return results
sc_results = run_self_consistency_experiment(n_questions=500)
sc_confidences = np.array([r["sc_confidence"] for r in sc_results])
sc_correct = np.array([r["correct"] for r in sc_results])
sc_ece = compute_ece(sc_confidences, sc_correct)Self-consistency results: Mean SC confidence: 0.714 Overall accuracy: 0.854 ECE: 0.1396
Semantic Entropy Simulation
Let's implement a simplified version of semantic entropy for open-ended responses. In a real system, the clustering step uses NLI entailment; here we simulate clusters directly.
def compute_semantic_entropy(response_clusters):
"""
Compute semantic entropy from cluster assignment counts.
response_clusters: list of cluster IDs (one per sampled response)
Returns: semantic entropy in nats
"""
counts = Counter(response_clusters)
total = len(response_clusters)
probs = np.array([count / total for count in counts.values()])
return entropy(probs) # natural log entropy
def simulate_semantic_responses(question_difficulty, n_samples=10):
"""
Simulate semantic cluster assignments for an open-ended question.
For easy questions: most responses fall in the same cluster.
For hard questions: responses are spread across many clusters.
"""
# Number of distinct clusters increases with difficulty
n_clusters_expected = 1 + question_difficulty * 5
cluster_probs = np.ones(int(np.ceil(n_clusters_expected)))
# Weight toward first cluster (most common answer) inversely with difficulty
cluster_probs[0] = (1.0 - question_difficulty) * 10 + 1
cluster_probs /= cluster_probs.sum()
clusters = np.random.choice(
len(cluster_probs), size=n_samples, p=cluster_probs
)
return clusters
# Compute semantic entropy across difficulty levels
sem_entropies = []
for d in difficulties:
clusters = simulate_semantic_responses(d, n_samples=20)
se = compute_semantic_entropy(clusters)
sem_entropies.append(se)Semantic entropy by difficulty: Easy (diff=0.1): 0.325 nats Medium (diff=0.5): 0.613 nats Hard (diff=0.9): 1.330 nats
Higher semantic entropy at higher difficulty confirms that more diverse (uncertain) answers correspond to harder questions. The overall upward relationship between difficulty and semantic entropy validates semantic entropy as an uncertainty signal that tracks model uncertainty rather than surface variation.
Key Parameters
The key parameters for the uncertainty quantification methods implemented above are:
- n_bins (ECE): Number of confidence bins for calibration error computation. More bins give finer-grained measurement but require more data for reliable estimates. 10-15 bins is standard.
- overconfidence_factor (temperature scaling): The logit scaling factor that produces overconfident outputs. Values greater than 1.0 stretch probabilities toward certainty, simulating the systematic bias seen in RLHF-trained models.
- temperature (calibration): The divisor applied to logits before the softmax. Learned values above 1.0 soften an overconfident model; values below 1.0 sharpen an underconfident one.
- n_samples (sampling-based methods): Number of model runs per query. More samples produce a more accurate uncertainty estimate but multiply inference cost proportionally.
- n_questions (self-consistency): Number of questions in the evaluation set. Larger evaluation sets produce more reliable ECE estimates.