theapemachine commited on
Commit
0c9628d
·
verified ·
1 Parent(s): 36be9a7

Add cortex/hallucination_gate.py

Browse files
Files changed (1) hide show
  1. cortex/hallucination_gate.py +151 -0
cortex/hallucination_gate.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HallucinationGate: Per-token confidence estimation and gated suppression.
3
+
4
+ Inspired by InternalInspector (Chen et al. 2024) and "The Map of Misbelief" (2025).
5
+
6
+ Architecture:
7
+ - A lightweight probe network that reads the residual stream at each layer
8
+ - Outputs a per-token confidence score in [0, 1]
9
+ - When confidence is low, the gate suppresses the residual update
10
+ (pulling toward the safe "prior" — i.e., the identity/previous representation)
11
+ - Multi-layer aggregation: confidence from different layers is ensembled
12
+
13
+ Failure mode addressed:
14
+ - Hallucination: model generates confident-sounding but factually wrong text
15
+ - By detecting low-confidence internal states BEFORE they reach the output,
16
+ we can suppress risky updates to the residual stream
17
+ - The model effectively "hedges" — when unsure, it preserves its prior state
18
+ rather than committing to a potentially wrong update
19
+
20
+ Injection point: POST_FFN (after each transformer block)
21
+ - Rationale: The full block output contains the richest signal about confidence.
22
+ We gate the entire block's contribution to the residual stream.
23
+
24
+ Key insight from InternalInspector: Internal states (especially attention outputs and
25
+ FFN outputs) contain significantly more information about correctness than the final
26
+ output distribution. I(Θ; K(X)|X) - I(Y; K(X)|X) ≥ Δ, where Δ is large.
27
+ """
28
+
29
+ import torch
30
+ import torch.nn as nn
31
+ import torch.nn.functional as F
32
+ from typing import Optional, Union, List
33
+ from cortex.core import CortexModule, InjectionPoint
34
+
35
+
36
+ class ConfidenceProbe(nn.Module):
37
+ """
38
+ Lightweight MLP that estimates per-token confidence from hidden states.
39
+
40
+ Uses a bottleneck architecture to keep parameter count low.
41
+ Trained with contrastive loss on correct/incorrect generations.
42
+ """
43
+
44
+ def __init__(self, hidden_dim: int, bottleneck_dim: int = 64):
45
+ super().__init__()
46
+ self.net = nn.Sequential(
47
+ nn.Linear(hidden_dim, bottleneck_dim),
48
+ nn.GELU(),
49
+ nn.Linear(bottleneck_dim, bottleneck_dim),
50
+ nn.GELU(),
51
+ nn.Linear(bottleneck_dim, 1),
52
+ )
53
+ # Initialize to output ~0.5 (neutral confidence) — sigmoid(0) = 0.5
54
+ nn.init.zeros_(self.net[-1].weight)
55
+ nn.init.zeros_(self.net[-1].bias)
56
+
57
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
58
+ """
59
+ Args:
60
+ hidden_states: [batch, seq_len, hidden_dim]
61
+ Returns:
62
+ confidence: [batch, seq_len, 1] in (0, 1)
63
+ """
64
+ return torch.sigmoid(self.net(hidden_states))
65
+
66
+
67
+ class HallucinationGate(CortexModule):
68
+ """
69
+ Gates the residual stream update based on internal confidence.
70
+
71
+ When the model is confident (confidence → 1), the layer's output passes through
72
+ normally. When uncertain (confidence → 0), the gate suppresses the layer's update,
73
+ effectively making it a near-identity operation.
74
+
75
+ This creates an implicit "I don't know" signal that propagates through the model,
76
+ causing it to rely more on earlier, safer representations.
77
+
78
+ Args:
79
+ hidden_dim: Model hidden dimension
80
+ bottleneck_dim: Confidence probe bottleneck size
81
+ suppression_strength: How aggressively to suppress uncertain updates (0-1)
82
+ confidence_threshold: Below this confidence, suppression kicks in hard
83
+ """
84
+
85
+ def __init__(
86
+ self,
87
+ hidden_dim: int,
88
+ bottleneck_dim: int = 64,
89
+ suppression_strength: float = 0.8,
90
+ confidence_threshold: float = 0.3,
91
+ target_layers: Union[List[int], str] = "deep",
92
+ ):
93
+ super().__init__(InjectionPoint.POST_FFN, target_layers)
94
+
95
+ self.hidden_dim = hidden_dim
96
+ self.suppression_strength = suppression_strength
97
+ self.confidence_threshold = confidence_threshold
98
+
99
+ # Confidence probe
100
+ self.probe = ConfidenceProbe(hidden_dim, bottleneck_dim)
101
+
102
+ # Learnable gate bias per dimension — allows the model to learn which
103
+ # dimensions are safe to suppress vs which should always pass through
104
+ self.dim_gate = nn.Parameter(torch.zeros(1, 1, hidden_dim))
105
+
106
+ # Running confidence for monitoring/logging
107
+ self.register_buffer("_last_confidence", torch.tensor(0.5), persistent=False)
108
+ self.register_buffer("_running_input", None, persistent=False)
109
+
110
+ def forward(
111
+ self,
112
+ hidden_states: torch.Tensor,
113
+ layer_idx: int,
114
+ **kwargs
115
+ ) -> torch.Tensor:
116
+ """
117
+ Gate the hidden states based on confidence.
118
+
119
+ The gating formula is:
120
+ output = hidden_states * (1 - suppression * (1 - confidence))
121
+
122
+ When confidence = 1: output = hidden_states (pass through)
123
+ When confidence = 0: output = hidden_states * (1 - suppression)
124
+
125
+ This is applied as a soft gate on the RESIDUAL UPDATE, not the full hidden state.
126
+ """
127
+ # Estimate confidence
128
+ confidence = self.probe(hidden_states) # [B, T, 1]
129
+ self._last_confidence = confidence.mean().detach()
130
+
131
+ # Compute per-dimension gate
132
+ dim_bias = torch.sigmoid(self.dim_gate) # [1, 1, D], in (0,1)
133
+
134
+ # Effective gate: combines token-level confidence with dimension-level bias
135
+ # High dim_bias = dimension always passes through
136
+ # Low dim_bias = dimension is gated by confidence
137
+ gate = 1.0 - self.suppression_strength * (1.0 - confidence) * (1.0 - dim_bias)
138
+
139
+ # Apply gate
140
+ gated_output = hidden_states * gate
141
+
142
+ return gated_output
143
+
144
+ def get_confidence(self) -> float:
145
+ """Return the last computed average confidence score."""
146
+ return self._last_confidence.item()
147
+
148
+ def extra_repr(self):
149
+ return (f"hidden_dim={self.hidden_dim}, "
150
+ f"suppression_strength={self.suppression_strength}, "
151
+ f"{super().extra_repr()}")