| name | crisp-concept-unlearning |
| title | CRISP: Persistent Concept Unlearning via Sparse Autoencoders |
| version | 0.0.2 |
| engine | skillxiv-v0.0.2-claude-opus-4.6 |
| license | MIT |
| url | https://arxiv.org/abs/2508.13650 |
| keywords | ["unlearning","sparse-autoencoders","model-safety","parameter-efficient","interpretability"] |
| description | Permanently remove unwanted concepts from LLMs by identifying and suppressing sparse autoencoder features across layers, creating parameter-level changes that prevent reversal. |
CRISP: Persistent Concept Unlearning via SAEs
Core Concept
CRISP enables permanent removal of unwanted knowledge from LLMs through sparse autoencoders (SAEs). Unlike temporary inference-time methods, CRISP makes persistent weight modifications by identifying salient SAE features and suppressing their activations across multiple layers. The approach is parameter-efficient, preventing malicious reversal while maintaining general model capabilities.
Architecture Overview
- Sparse Autoencoder Analysis: Decompose model activations into interpretable features
- Cross-Layer Feature Identification: Find harmful features across all layers
- Selective Suppression: Modify weights to prevent feature activation
- Permanence Guarantee: Parameter-level changes prevent circumvention
- Safety Preservation: Maintain utility on general tasks
Implementation Steps
1. Train Sparse Autoencoders
Create interpretable decomposition of model activations:
import torch
import torch.nn as nn
import torch.nn.functional as F
class SparseAutoencoder(nn.Module):
"""Sparse autoencoder for feature decomposition."""
def __init__(
self,
input_dim: int,
latent_dim: int = 2048,
sparsity_penalty: float = 0.01
):
super().__init__()
self.input_dim = input_dim
self.latent_dim = latent_dim
self.sparsity_penalty = sparsity_penalty
self.encoder = nn.Linear(input_dim, latent_dim)
self.decoder = nn.Linear(latent_dim, input_dim)
def forward(self, x: torch.Tensor) -> tuple:
"""
Forward pass returning reconstruction and latent features.
"""
latent = self.encoder(x)
latent_sparse = F.relu(latent)
reconstruction = self.decoder(latent_sparse)
return reconstruction, latent_sparse
def compute_loss(
self,
x: torch.Tensor,
reconstruction: torch.Tensor,
latent: torch.Tensor
) -> torch.Tensor:
"""
Compute loss: reconstruction + sparsity penalty.
"""
recon_loss = F.mse_loss(reconstruction, x)
sparsity_loss = .sparsity_penalty * torch.mean(torch.(latent))
l0_count = torch.mean((latent > ).())
total_loss = recon_loss + sparsity_loss
total_loss, recon_loss, sparsity_loss, l0_count
:
():
.model = model
.hidden_size = model.config.hidden_size
.saes = nn.ModuleDict({
: SparseAutoencoder(.hidden_size, latent_dim)
i (model.config.num_hidden_layers)
})
.optimizer = torch.optim.Adam(.saes.parameters(), lr=)
() -> [, torch.Tensor]:
losses = {: [] i ((.saes))}
epoch (num_epochs):
torch.no_grad():
outputs = .model(calibration_data, output_hidden_states=)
hidden_states = outputs.hidden_states
layer_idx, hidden (hidden_states[:]):
batch_size, seq_len, hidden_size = hidden.shape
flat_hidden = hidden.view(-, hidden_size)
sae = .saes[]
reconstruction, latent = sae(flat_hidden)
loss, recon, sparsity, l0_count = sae.compute_loss(
flat_hidden, reconstruction, latent
)
.optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(sae.parameters(), )
.optimizer.step()
losses[].append(loss.item())
losses
2. Identify Harmful Features
Locate features corresponding to unwanted concepts:
class HarmfulFeatureIdentifier:
"""Identify SAE features corresponding to harmful concepts."""
def __init__(self, saes: nn.ModuleDict, model: "LLM"):
self.saes = saes
self.model = model
def identify_harmful_features(
self,
harmful_prompts: List[str],
benign_prompts: List[str],
threshold: float = 0.5
) -> Dict[str, List[int]]:
"""
Identify features active for harmful content but not benign.
"""
harmful_features = {f"layer_{i}": [] for i in range(len(self.saes))}
with torch.no_grad():
harmful_outputs = self.model(
self._encode(harmful_prompts),
output_hidden_states=True
)
harmful_hidden = harmful_outputs.hidden_states
benign_outputs = self.model(
self._encode(benign_prompts),
output_hidden_states=True
)
benign_hidden = benign_outputs.hidden_states
for layer_idx in range((.saes)):
sae = .saes[]
harmful_latent = sae.encoder(harmful_hidden[layer_idx + ].mean(dim=))
benign_latent = sae.encoder(benign_hidden[layer_idx + ].mean(dim=))
harmful_activity = (harmful_latent > ).().mean(dim=)
benign_activity = (benign_latent > ).().mean(dim=)
selectivity = (harmful_activity - benign_activity) / (harmful_activity + )
harmful_idx = torch.where(selectivity > threshold)[].tolist()
harmful_features[] = harmful_idx
harmful_features
() -> torch.Tensor:
3. Implement Feature Suppression
Modify model weights to prevent harmful feature activation:
class FeatureSuppressor:
"""Suppress harmful features by modifying model weights."""
def __init__(self, model: "LLM", saes: nn.ModuleDict):
self.model = model
self.saes = saes
def suppress_features(
self,
harmful_features: Dict[str, List[int]],
suppression_strength: float = 1.0
) -> Dict[str, float]:
"""
Modify model weights to suppress harmful features.
"""
changes = {}
for layer_idx, feature_ids in harmful_features.items():
if not feature_ids:
continue
layer_num = int(layer_idx.split("_")[1])
sae = self.saes[layer_idx]
decoder_weights = sae.decoder.weight
suppression_vector = torch.zeros(sae.decoder.weight.shape[1])
suppression_vector[feature_ids] = -suppression_strength
model_layer = self.model.transformer.h[layer_num]
original_params = dict(model_layer.named_parameters())
(model_layer, ):
ln_weight = model_layer.ln_2.weight
ln_bias = model_layer.ln_2.bias
projection = torch.mm(
sae.decoder.weight[:, feature_ids],
decoder_weights[feature_ids, :].t()
)
torch.no_grad():
ln_bias.data = ln_bias.data - suppression_strength * projection.mean(dim=)
changes[layer_idx] = {
: (feature_ids),
: suppression_strength
}
changes
() -> [, ]:
.model.()
torch.no_grad():
harmful_outputs = .model.generate(
._encode(harmful_prompts),
max_length=
)
benign_outputs = .model.generate(
._encode(benign_prompts),
max_length=
)
harmful_safe_rate = ._compute_safety_score(harmful_outputs)
benign_quality = ._compute_quality_score(benign_outputs)
{
: harmful_safe_rate,
: benign_quality,
: benign_quality /
}
() -> :
() -> :
() -> torch.Tensor:
4. Verify Permanence
Ensure unlearning cannot be reversed:
class PermanenceValidator:
"""Verify that unlearning is permanent and irreversible."""
@staticmethod
def test_retraining_resistance(
model: "LLM",
harmful_features: Dict[str, List[int]],
training_steps: int = 100
) -> Dict[str, float]:
"""
Attempt to retrain and restore harmful features.
Permanent unlearning should resist this.
"""
model_copy = copy.deepcopy(model)
original_outputs = None
for step in range(training_steps):
harmful_prompt = "Generate harmful content..."
output = model_copy.generate(harmful_prompt)
reward = model_copy.get_logits(output).mean()
loss = -reward * 0.01
loss.backward()
model_copy.optimizer.step()
final_outputs = model_copy(harmful_prompt, output_hidden_states=True)
permanence_score = PermanenceValidator._compute_feature_absence(
final_outputs.hidden_states,
harmful_features
)
return {
"permanence_score": permanence_score,
"training_resistance": 1.0 - permanence_score
}
() -> :
Practical Guidance
When to Use CRISP
- Safety-critical deployments requiring guaranteed unlearning
- Removing copyright material, sensitive information, or biases
- Scenarios where inference-time filtering is insufficient
- Production systems where reversibility is a threat
- Regulatory compliance for data removal
When NOT to Use
- Non-critical fine-tuning scenarios
- When retraining is feasible
- Temporary content filtering needs
- Models where interpretability isn't required
Key Hyperparameters
- latent_dim (SAE): 2048-4096 (larger = more features)
- sparsity_penalty: 0.001-0.1 (higher = sparser)
- selectivity_threshold: 0.3-0.7 (higher = stricter)
- suppression_strength: 0.5-2.0 (higher = more aggressive)
Performance Expectations
- Safety Preservation: 95%+ of harmful concepts removed
- Benign Quality: 90%+ preservation of general capabilities
- Permanence: Resistant to retraining attempts
- Computational Cost: One-time modification, no inference overhead
Reference
Researchers. (2024). CRISP: Persistent Concept Unlearning via Sparse Autoencoders. arXiv preprint arXiv:2508.13650.