Mukul Rayana commited on
Commit ·
6535fa7
1
Parent(s): 0e9c4c7
fix: guardrail_ig.py — IG in embedding space, token_type_ids, correct dtype
Browse files- src/models/guardrail_ig.py +101 -22
src/models/guardrail_ig.py
CHANGED
|
@@ -1,27 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import torch
|
| 2 |
from captum.attr import IntegratedGradients
|
| 3 |
from transformers import AutoTokenizer, AutoModelForSequenceClassification
|
| 4 |
|
| 5 |
-
CHECKPOINT
|
| 6 |
-
CRISIS_LABEL = 0
|
| 7 |
-
HYPOTHESIS
|
| 8 |
|
| 9 |
|
| 10 |
class SafetyGuardrail:
|
| 11 |
-
def __init__(self, checkpoint=CHECKPOINT):
|
| 12 |
self.tokenizer = AutoTokenizer.from_pretrained(checkpoint)
|
| 13 |
-
|
| 14 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
def
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
def check(self, text: str, threshold: float = 0.5):
|
| 21 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
enc = self.tokenizer(
|
| 23 |
-
text, HYPOTHESIS,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
)
|
|
|
|
| 25 |
with torch.no_grad():
|
| 26 |
logits = self.model(**enc).logits
|
| 27 |
probs = torch.softmax(logits, dim=-1)
|
|
@@ -30,20 +66,63 @@ class SafetyGuardrail:
|
|
| 30 |
if crisis_prob < threshold:
|
| 31 |
return False, crisis_prob, []
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
attrs, _ = self.ig.attribute(
|
| 36 |
-
|
| 37 |
-
baselines=
|
| 38 |
target=CRISIS_LABEL,
|
| 39 |
-
additional_forward_args=(
|
| 40 |
-
enc.get("attention_mask"),
|
| 41 |
-
enc.get("token_type_ids"),
|
| 42 |
-
),
|
| 43 |
return_convergence_delta=True,
|
| 44 |
n_steps=50,
|
| 45 |
)
|
| 46 |
-
|
|
|
|
|
|
|
| 47 |
tokens = self.tokenizer.convert_ids_to_tokens(input_ids[0])
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
src/models/guardrail_ig.py
|
| 3 |
+
EmpathRAG — DeBERTa NLI Safety Guardrail with Integrated Gradients
|
| 4 |
+
|
| 5 |
+
Runs on CPU. Model loaded as float32 (upcast from bf16 checkpoint).
|
| 6 |
+
IG operates in embedding space — interpolation is continuous and meaningful.
|
| 7 |
+
token_type_ids passed through — required by this tokenizer version.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
import torch
|
| 11 |
from captum.attr import IntegratedGradients
|
| 12 |
from transformers import AutoTokenizer, AutoModelForSequenceClassification
|
| 13 |
|
| 14 |
+
CHECKPOINT = "models/safety_guardrail"
|
| 15 |
+
CRISIS_LABEL = 0 # label 0 = entailment = crisis
|
| 16 |
+
HYPOTHESIS = "This person is expressing suicidal ideation or intent to self-harm."
|
| 17 |
|
| 18 |
|
| 19 |
class SafetyGuardrail:
|
| 20 |
+
def __init__(self, checkpoint: str = CHECKPOINT):
|
| 21 |
self.tokenizer = AutoTokenizer.from_pretrained(checkpoint)
|
| 22 |
+
# Load as float32 on CPU — bf16 checkpoint upcasts cleanly, CPU inference is stable
|
| 23 |
+
self.model = (
|
| 24 |
+
AutoModelForSequenceClassification
|
| 25 |
+
.from_pretrained(checkpoint, torch_dtype=torch.float32)
|
| 26 |
+
.eval()
|
| 27 |
+
)
|
| 28 |
+
# IG forward operates on embedding vectors, not integer token IDs
|
| 29 |
+
self.ig = IntegratedGradients(self._forward_embeddings)
|
| 30 |
|
| 31 |
+
def _forward_embeddings(self, inputs_embeds, attention_mask=None, token_type_ids=None):
|
| 32 |
+
"""
|
| 33 |
+
Forward pass accepting embedding tensors instead of token IDs.
|
| 34 |
+
Required by IntegratedGradients — interpolation must happen in
|
| 35 |
+
continuous embedding space, not discrete integer token ID space.
|
| 36 |
+
"""
|
| 37 |
+
return self.model(
|
| 38 |
+
inputs_embeds=inputs_embeds,
|
| 39 |
+
attention_mask=attention_mask,
|
| 40 |
+
token_type_ids=token_type_ids,
|
| 41 |
+
).logits
|
| 42 |
|
| 43 |
def check(self, text: str, threshold: float = 0.5):
|
| 44 |
+
"""
|
| 45 |
+
Run guardrail on a single text string.
|
| 46 |
+
|
| 47 |
+
Returns:
|
| 48 |
+
is_crisis (bool) — True if crisis probability >= threshold
|
| 49 |
+
confidence (float) — crisis class probability [0, 1]
|
| 50 |
+
top_tokens (list) — [(token_str, attribution_score), ...] top-5
|
| 51 |
+
empty list if is_crisis is False
|
| 52 |
+
"""
|
| 53 |
enc = self.tokenizer(
|
| 54 |
+
text, HYPOTHESIS,
|
| 55 |
+
return_tensors="pt",
|
| 56 |
+
truncation=True,
|
| 57 |
+
max_length=256,
|
| 58 |
+
padding="max_length",
|
| 59 |
)
|
| 60 |
+
|
| 61 |
with torch.no_grad():
|
| 62 |
logits = self.model(**enc).logits
|
| 63 |
probs = torch.softmax(logits, dim=-1)
|
|
|
|
| 66 |
if crisis_prob < threshold:
|
| 67 |
return False, crisis_prob, []
|
| 68 |
|
| 69 |
+
# ── Integrated Gradients ───────────────────────────────────────────────
|
| 70 |
+
# Get the embedding layer
|
| 71 |
+
embed_layer = self.model.deberta.embeddings.word_embeddings
|
| 72 |
+
|
| 73 |
+
input_ids = enc["input_ids"] # [1, seq_len]
|
| 74 |
+
attention_mask = enc["attention_mask"] # [1, seq_len]
|
| 75 |
+
token_type_ids = enc.get("token_type_ids") # [1, seq_len] or None
|
| 76 |
+
|
| 77 |
+
# Input embeddings and all-zeros baseline in embedding space
|
| 78 |
+
input_embeds = embed_layer(input_ids) # [1, seq_len, hidden]
|
| 79 |
+
baseline_embeds = torch.zeros_like(input_embeds) # [1, seq_len, hidden]
|
| 80 |
+
|
| 81 |
attrs, _ = self.ig.attribute(
|
| 82 |
+
input_embeds,
|
| 83 |
+
baselines=baseline_embeds,
|
| 84 |
target=CRISIS_LABEL,
|
| 85 |
+
additional_forward_args=(attention_mask, token_type_ids),
|
|
|
|
|
|
|
|
|
|
| 86 |
return_convergence_delta=True,
|
| 87 |
n_steps=50,
|
| 88 |
)
|
| 89 |
+
# attrs: [1, seq_len, hidden_dim] → sum over hidden → [seq_len]
|
| 90 |
+
token_scores = attrs.sum(dim=-1).abs().squeeze(0) # [seq_len]
|
| 91 |
+
|
| 92 |
tokens = self.tokenizer.convert_ids_to_tokens(input_ids[0])
|
| 93 |
+
|
| 94 |
+
# Filter out padding tokens ([PAD] = token id 0)
|
| 95 |
+
pairs = [
|
| 96 |
+
(tok, score.item())
|
| 97 |
+
for tok, score, mask in zip(tokens, token_scores, attention_mask[0])
|
| 98 |
+
if mask.item() == 1 and tok not in ("[CLS]", "[SEP]", "▁", "<s>", "</s>")
|
| 99 |
+
]
|
| 100 |
+
top5 = sorted(pairs, key=lambda x: x[1], reverse=True)[:5]
|
| 101 |
+
|
| 102 |
+
return True, crisis_prob, top5
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
# ── Standalone test ────────────────────────────────────────────────────────────
|
| 106 |
+
if __name__ == "__main__":
|
| 107 |
+
print("Loading SafetyGuardrail from", CHECKPOINT)
|
| 108 |
+
g = SafetyGuardrail()
|
| 109 |
+
print("Model loaded.\n")
|
| 110 |
+
|
| 111 |
+
tests = [
|
| 112 |
+
("I want to kill myself and I have a plan.", True),
|
| 113 |
+
("I think everyone would be better off without me.", True),
|
| 114 |
+
("I'm really stressed about my thesis deadline.", False),
|
| 115 |
+
("this exam is literally killing me lol", False),
|
| 116 |
+
]
|
| 117 |
+
|
| 118 |
+
all_pass = True
|
| 119 |
+
for text, expect_crisis in tests:
|
| 120 |
+
is_crisis, conf, tokens = g.check(text)
|
| 121 |
+
status = "✅" if is_crisis == expect_crisis else "❌"
|
| 122 |
+
print(f"{status} crisis={is_crisis} ({conf:.3f}) | {text}")
|
| 123 |
+
if is_crisis and tokens:
|
| 124 |
+
print(f" top tokens: {tokens}")
|
| 125 |
+
if is_crisis != expect_crisis:
|
| 126 |
+
all_pass = False
|
| 127 |
+
|
| 128 |
+
print("\n✅ guardrail_ig.py verified." if all_pass else "\n❌ at least one case wrong.")
|