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
Files changed (1) hide show
  1. 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 = "models/safety_guardrail"
6
- CRISIS_LABEL = 0
7
- HYPOTHESIS = "This person is expressing suicidal ideation or intent to self-harm."
8
 
9
 
10
  class SafetyGuardrail:
11
- def __init__(self, checkpoint=CHECKPOINT):
12
  self.tokenizer = AutoTokenizer.from_pretrained(checkpoint)
13
- self.model = AutoModelForSequenceClassification.from_pretrained(checkpoint).eval()
14
- self.ig = IntegratedGradients(self._forward)
 
 
 
 
 
 
15
 
16
- def _forward(self, input_ids, attention_mask=None, token_type_ids=None):
17
- emb = self.model.deberta.embeddings.word_embeddings(input_ids)
18
- return self.model(inputs_embeds=emb, attention_mask=attention_mask).logits
 
 
 
 
 
 
 
 
19
 
20
  def check(self, text: str, threshold: float = 0.5):
21
- """Returns (is_crisis: bool, confidence: float, token_attributions: list)."""
 
 
 
 
 
 
 
 
22
  enc = self.tokenizer(
23
- text, HYPOTHESIS, return_tensors="pt", truncation=True, max_length=256
 
 
 
 
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
- input_ids = enc["input_ids"]
34
- baseline = torch.zeros_like(input_ids)
 
 
 
 
 
 
 
 
 
 
35
  attrs, _ = self.ig.attribute(
36
- input_ids,
37
- baselines=baseline,
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
- token_attrs = attrs.sum(-1).abs().squeeze().tolist()
 
 
47
  tokens = self.tokenizer.convert_ids_to_tokens(input_ids[0])
48
- paired = sorted(zip(tokens, token_attrs), key=lambda x: x[1], reverse=True)[:5]
49
- return True, crisis_prob, paired
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.")