mirror of
https://github.com/elder-plinius/OBLITERATUS.git
synced 2026-07-19 10:27:38 +02:00
390 lines
16 KiB
Python
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)
|