Files
OBLITERATUS/obliteratus/analysis/alignment_imprint.py
T
2026-03-04 12:38:18 -08:00

390 lines
16 KiB
Python

"""DPO/RLHF Alignment Imprint Detector.
Different alignment training methods leave distinct geometric "fingerprints"
in model activations. This module detects and characterizes these imprints
by comparing the structure of the refusal subspace against known signatures:
**DPO (Direct Preference Optimization)**:
- Refusal tends to be *sparse* and *concentrated* in a few layers
- The refusal direction has high cosine similarity with the preference
gradient direction (since DPO directly optimizes logprob ratios)
- Imprint signature: High Gini coefficient of per-layer refusal strength,
low effective rank of the refusal subspace
**RLHF (PPO-based)**:
- Refusal is more *distributed* across layers due to policy gradient updates
- The reward model introduces smoothing that spreads the signal
- Imprint signature: Lower Gini coefficient, higher effective rank,
smoother cross-layer alignment profile
**Constitutional AI (CAI)**:
- Multi-round self-critique creates *layered* refusal with recursive structure
- Refusal directions at different layers tend to be more mutually orthogonal
- Imprint signature: Low mean pairwise cosine between layer directions,
high cone dimensionality
**SFT-only (Supervised Fine-Tuning)**:
- Simplest imprint — refusal lives mostly in the final few layers
- Often highly concentrated with low dimensionality
- Imprint signature: Strong tail-layer bias, low spread
Contributions:
- Systematic taxonomy of alignment training fingerprints in
the refusal subspace geometry
- Quantitative Alignment Imprint Score (AIS) that maps geometric
features to a probability distribution over training methods
- Cross-layer spectral analysis to detect recursive CAI structures
References:
- Rafailov et al. (2023): DPO — Direct Preference Optimization
- Ouyang et al. (2022): InstructGPT / RLHF
- Bai et al. (2022): Constitutional AI
- Lee et al. (2025): Geometric signatures of RLHF
"""
from __future__ import annotations
import math
from dataclasses import dataclass, field
import torch
@dataclass
class AlignmentImprint:
"""Detected alignment training imprint."""
# Probability estimates for each method
dpo_probability: float
rlhf_probability: float
cai_probability: float
sft_probability: float
# The most likely alignment method
predicted_method: str
# Geometric features used for classification
gini_coefficient: float # Concentration of refusal strength across layers
effective_rank: float # Dimensionality of refusal subspace
cross_layer_smoothness: float # How smoothly refusal varies across layers
tail_layer_bias: float # Fraction of refusal in final 25% of layers
mean_pairwise_orthogonality: float # Mean (1 - |cos|) between layer directions
spectral_decay_rate: float # How fast singular values decay
# Per-layer feature vector
per_layer_strength: dict[int, float] = field(default_factory=dict)
# Confidence in the prediction
confidence: float = 0.0
@dataclass
class BaseInstructDelta:
"""Comparison between base model and instruct model activations.
This captures what alignment training actually changed — the "delta"
between the base model's representations and the aligned model's.
"""
layer_idx: int
cosine_with_refusal: float # How aligned is the delta with the refusal direction
delta_magnitude: float # How much the layer changed
delta_direction: torch.Tensor # Unit vector of the change
refusal_component: float # Magnitude of delta along refusal direction
orthogonal_component: float # Magnitude of delta orthogonal to refusal
class AlignmentImprintDetector:
"""Detect alignment training method from refusal geometry.
Analyzes the geometric structure of refusal directions across layers
to infer which alignment training procedure was used. Different methods
leave distinct geometric signatures ("imprints") that can be detected
from the refusal subspace alone.
"""
# Feature weights for method classification (derived from literature)
# Format: {method: {feature: (ideal_value, weight)}}
METHOD_SIGNATURES = {
"dpo": {
"gini_coefficient": (0.7, 2.0), # DPO: concentrated
"effective_rank": (1.5, 1.5), # DPO: low-rank
"cross_layer_smoothness": (0.3, 1.0), # DPO: not smooth
"tail_layer_bias": (0.5, 1.0), # DPO: moderate tail bias
"mean_pairwise_orthogonality": (0.2, 1.0), # DPO: aligned
"spectral_decay_rate": (2.0, 1.5), # DPO: fast decay
},
"rlhf": {
"gini_coefficient": (0.3, 2.0), # RLHF: distributed
"effective_rank": (3.0, 1.5), # RLHF: higher rank
"cross_layer_smoothness": (0.7, 1.0), # RLHF: smooth
"tail_layer_bias": (0.3, 1.0), # RLHF: not tail-biased
"mean_pairwise_orthogonality": (0.4, 1.0), # RLHF: moderate
"spectral_decay_rate": (0.8, 1.5), # RLHF: slow decay
},
"cai": {
"gini_coefficient": (0.4, 1.5), # CAI: moderate
"effective_rank": (4.0, 2.0), # CAI: high rank (recursive)
"cross_layer_smoothness": (0.5, 1.0), # CAI: moderate
"tail_layer_bias": (0.35, 0.5), # CAI: not strongly biased
"mean_pairwise_orthogonality": (0.6, 2.0), # CAI: orthogonal layers
"spectral_decay_rate": (0.5, 1.5), # CAI: very slow decay
},
"sft": {
"gini_coefficient": (0.8, 2.0), # SFT: very concentrated
"effective_rank": (1.2, 1.5), # SFT: nearly rank-1
"cross_layer_smoothness": (0.2, 1.0), # SFT: not smooth
"tail_layer_bias": (0.7, 2.0), # SFT: strong tail bias
"mean_pairwise_orthogonality": (0.15, 1.0), # SFT: very aligned
"spectral_decay_rate": (3.0, 1.5), # SFT: very fast decay
},
}
def detect_imprint(
self,
refusal_directions: dict[int, torch.Tensor],
refusal_strengths: dict[int, float] | None = None,
) -> AlignmentImprint:
"""Detect alignment method from refusal direction geometry.
Args:
refusal_directions: {layer_idx: direction_vector} per layer.
refusal_strengths: {layer_idx: strength} if available.
If None, uses direction norms.
Returns:
AlignmentImprint with method prediction and feature analysis.
"""
if not refusal_directions:
return AlignmentImprint(
dpo_probability=0.25, rlhf_probability=0.25,
cai_probability=0.25, sft_probability=0.25,
predicted_method="unknown",
gini_coefficient=0.0, effective_rank=0.0,
cross_layer_smoothness=0.0, tail_layer_bias=0.0,
mean_pairwise_orthogonality=0.0, spectral_decay_rate=0.0,
confidence=0.0,
)
# Compute per-layer strengths
if refusal_strengths is None:
strengths = {k: v.norm().item() for k, v in refusal_directions.items()}
else:
strengths = dict(refusal_strengths)
# Extract geometric features
features = self._extract_features(refusal_directions, strengths)
# Classify using feature matching
scores = self._classify(features)
# Normalize to probabilities via softmax
max_score = max(scores.values())
exp_scores = {k: math.exp(v - max_score) for k, v in scores.items()}
total = sum(exp_scores.values())
probs = {k: v / total for k, v in exp_scores.items()}
predicted = max(probs, key=probs.get)
confidence = probs[predicted]
return AlignmentImprint(
dpo_probability=probs["dpo"],
rlhf_probability=probs["rlhf"],
cai_probability=probs["cai"],
sft_probability=probs["sft"],
predicted_method=predicted,
gini_coefficient=features["gini_coefficient"],
effective_rank=features["effective_rank"],
cross_layer_smoothness=features["cross_layer_smoothness"],
tail_layer_bias=features["tail_layer_bias"],
mean_pairwise_orthogonality=features["mean_pairwise_orthogonality"],
spectral_decay_rate=features["spectral_decay_rate"],
per_layer_strength=strengths,
confidence=confidence,
)
def compare_base_instruct(
self,
base_activations: dict[int, torch.Tensor],
instruct_activations: dict[int, torch.Tensor],
refusal_directions: dict[int, torch.Tensor],
) -> list[BaseInstructDelta]:
"""Compare base vs. instruct activations to measure alignment delta.
Args:
base_activations: {layer_idx: mean_activation} from base model.
instruct_activations: {layer_idx: mean_activation} from instruct model.
refusal_directions: {layer_idx: refusal_direction} for decomposition.
Returns:
List of per-layer BaseInstructDelta results.
"""
results = []
common_layers = set(base_activations.keys()) & set(instruct_activations.keys())
for layer_idx in sorted(common_layers):
base_act = base_activations[layer_idx].float().squeeze()
inst_act = instruct_activations[layer_idx].float().squeeze()
delta = inst_act - base_act
delta_mag = delta.norm().item()
if delta_mag < 1e-10:
results.append(BaseInstructDelta(
layer_idx=layer_idx,
cosine_with_refusal=0.0,
delta_magnitude=0.0,
delta_direction=torch.zeros_like(delta),
refusal_component=0.0,
orthogonal_component=0.0,
))
continue
delta_dir = delta / delta.norm()
# Decompose delta into refusal and orthogonal components
if layer_idx in refusal_directions:
ref_dir = refusal_directions[layer_idx].float().squeeze()
ref_dir = ref_dir / ref_dir.norm().clamp(min=1e-10)
cos = (delta_dir @ ref_dir).item()
refusal_comp = abs(cos) * delta_mag
orth_comp = math.sqrt(max(0, delta_mag**2 - refusal_comp**2))
else:
cos = 0.0
refusal_comp = 0.0
orth_comp = delta_mag
results.append(BaseInstructDelta(
layer_idx=layer_idx,
cosine_with_refusal=cos,
delta_magnitude=delta_mag,
delta_direction=delta_dir,
refusal_component=refusal_comp,
orthogonal_component=orth_comp,
))
return results
def _extract_features(
self,
directions: dict[int, torch.Tensor],
strengths: dict[int, float],
) -> dict[str, float]:
"""Extract geometric features from refusal directions."""
layers = sorted(directions.keys())
n_layers = len(layers)
# 1. Gini coefficient of layer strengths
vals = sorted(strengths.values())
n = len(vals)
if n > 0 and sum(vals) > 0:
cumulative = sum((2 * (i + 1) - n - 1) * v for i, v in enumerate(vals))
gini = cumulative / (n * sum(vals))
else:
gini = 0.0
gini = max(0.0, min(1.0, gini))
# 2. Effective rank of direction matrix
if n_layers >= 2:
D = torch.stack([directions[li].float().squeeze() for li in layers])
s = torch.linalg.svdvals(D)
s = s[s > 1e-10]
if len(s) > 0:
p = s / s.sum()
entropy = -(p * p.log()).sum()
eff_rank = torch.exp(entropy).item()
# Spectral decay rate
if len(s) >= 2:
decay = (s[0] / s[-1]).item()
spectral_decay = math.log(max(1.0, decay))
else:
spectral_decay = 0.0
else:
eff_rank = 0.0
spectral_decay = 0.0
else:
eff_rank = 1.0
spectral_decay = 0.0
# 3. Cross-layer smoothness (mean cosine between adjacent layers)
adj_cosines = []
for i in range(len(layers) - 1):
d_a = directions[layers[i]].float().squeeze()
d_b = directions[layers[i + 1]].float().squeeze()
cos = (d_a @ d_b).abs().item() / max(
d_a.norm().item() * d_b.norm().item(), 1e-10
)
adj_cosines.append(cos)
smoothness = sum(adj_cosines) / len(adj_cosines) if adj_cosines else 0.0
# 4. Tail layer bias
if n_layers >= 4:
tail_start = layers[int(0.75 * n_layers)]
total_strength = sum(strengths.values())
tail_strength = sum(
v for k, v in strengths.items() if k >= tail_start
)
tail_bias = tail_strength / max(total_strength, 1e-10)
else:
tail_bias = 0.5
# 5. Mean pairwise orthogonality
pair_orths = []
for i in range(len(layers)):
for j in range(i + 1, len(layers)):
d_a = directions[layers[i]].float().squeeze()
d_b = directions[layers[j]].float().squeeze()
cos = (d_a @ d_b).abs().item() / max(
d_a.norm().item() * d_b.norm().item(), 1e-10
)
pair_orths.append(1.0 - cos)
mean_orth = sum(pair_orths) / len(pair_orths) if pair_orths else 0.0
return {
"gini_coefficient": gini,
"effective_rank": eff_rank,
"cross_layer_smoothness": smoothness,
"tail_layer_bias": tail_bias,
"mean_pairwise_orthogonality": mean_orth,
"spectral_decay_rate": spectral_decay,
}
def _classify(self, features: dict[str, float]) -> dict[str, float]:
"""Compute method scores using Gaussian-kernel feature matching."""
scores = {}
for method, signature in self.METHOD_SIGNATURES.items():
score = 0.0
for feat_name, (ideal, weight) in signature.items():
actual = features.get(feat_name, 0.0)
# Gaussian kernel: exp(-0.5 * ((actual - ideal) / sigma)^2)
sigma = max(0.3 * abs(ideal), 0.1)
dist = (actual - ideal) / sigma
feat_score = math.exp(-0.5 * dist * dist)
score += weight * feat_score
scores[method] = score
return scores
@staticmethod
def format_imprint(imprint: AlignmentImprint) -> str:
"""Format alignment imprint as a report."""
lines = []
lines.append("Alignment Imprint Detection")
lines.append("=" * 40)
lines.append("")
lines.append(f"Predicted method: {imprint.predicted_method.upper()}")
lines.append(f"Confidence: {imprint.confidence:.1%}")
lines.append("")
lines.append("Method probabilities:")
lines.append(f" DPO: {imprint.dpo_probability:.1%}")
lines.append(f" RLHF: {imprint.rlhf_probability:.1%}")
lines.append(f" CAI: {imprint.cai_probability:.1%}")
lines.append(f" SFT: {imprint.sft_probability:.1%}")
lines.append("")
lines.append("Geometric features:")
lines.append(f" Gini coefficient: {imprint.gini_coefficient:.3f}")
lines.append(f" Effective rank: {imprint.effective_rank:.2f}")
lines.append(f" Cross-layer smooth: {imprint.cross_layer_smoothness:.3f}")
lines.append(f" Tail layer bias: {imprint.tail_layer_bias:.3f}")
lines.append(f" Pairwise orthogon: {imprint.mean_pairwise_orthogonality:.3f}")
lines.append(f" Spectral decay: {imprint.spectral_decay_rate:.2f}")
return "\n".join(lines)