Class: Vangrail::Rails::Perplexity
- Inherits:
-
Vangrail::Rail
- Object
- Vangrail::Rail
- Vangrail::Rails::Perplexity
- Defined in:
- lib/vangrail/rails/perplexity.rb
Overview
Reads how surprised a model is by the text, and blocks the span that is not language.
The optimised attacks in the literature do not produce sentences. A gradient-search suffix reads like a hash with punctuation in it, and it works on the model while meaning nothing to a reader. No pattern catches it, because there is no pattern: the string is different every time it is searched for. What it has instead is a signature that is hard to remove while keeping the attack, which is that a language model finds it wildly improbable.
So the check is the model's own log probabilities, which is the published detector for exactly this family. It needs an endpoint that will echo a prompt and score it, and many will not; when that is the case this rail reports uncertain and the deployment knows the family is uncovered rather than believing it is handled.
Windowed rather than averaged over the whole text, because an attack is a short span inside an ordinary question. Twenty improbable tokens after a hundred readable ones barely move the mean, and the window is what keeps the signal from being diluted by the sentence the attacker wrapped it in.
The threshold is not a constant of nature. Log probabilities depend on the model, its tokenizer, and its quantisation, so the default here is a starting point and script/perplexity_probe.rb is what turns it into a measured number for the endpoint in use. A deployment that has not run it is running an uncalibrated detector, and this rail is off by default for that reason as much as for the round trip.
Constant Summary collapse
- THRESHOLD =
Mean negative log likelihood per token, in nats. Ordinary prose under a small instruct model sits around 2 to 4; a gradient-search suffix sits far above it. Calibrate before trusting.
7.0- WINDOW =
Tokens per window. Short enough that a twenty-token suffix fills one, long enough that a rare proper noun does not.
16- FLOOR =
Below this many scored tokens there is no window worth taking, and a three-word question is not evidence of anything.
8
Instance Attribute Summary collapse
-
#completion ⇒ Object
readonly
Returns the value of attribute completion.
-
#threshold ⇒ Object
readonly
Returns the value of attribute threshold.
-
#window ⇒ Object
readonly
Returns the value of attribute window.
Instance Method Summary collapse
- #cache_key(text, _context) ⇒ Object
- #call(text, _context) ⇒ Object
-
#initialize(completion:, threshold: THRESHOLD, window: WINDOW, floor: FLOOR, name: 'perplexity', sides: %i[input context])) ⇒ Perplexity
constructor
A new instance of Perplexity.
- #offline? ⇒ Boolean
-
#worst_window(text) ⇒ Object
The highest mean negative log likelihood of any window, or nil when the text is too short to have one.
Constructor Details
#initialize(completion:, threshold: THRESHOLD, window: WINDOW, floor: FLOOR, name: 'perplexity', sides: %i[input context])) ⇒ Perplexity
Returns a new instance of Perplexity.
52 53 54 55 56 57 58 59 |
# File 'lib/vangrail/rails/perplexity.rb', line 52 def initialize(completion:, threshold: THRESHOLD, window: WINDOW, floor: FLOOR, name: 'perplexity', sides: %i[input context]) super(name: name, sides: sides) @completion = completion @threshold = threshold @window = window @floor = floor end |
Instance Attribute Details
#completion ⇒ Object (readonly)
Returns the value of attribute completion.
50 51 52 |
# File 'lib/vangrail/rails/perplexity.rb', line 50 def completion @completion end |
#threshold ⇒ Object (readonly)
Returns the value of attribute threshold.
50 51 52 |
# File 'lib/vangrail/rails/perplexity.rb', line 50 def threshold @threshold end |
#window ⇒ Object (readonly)
Returns the value of attribute window.
50 51 52 |
# File 'lib/vangrail/rails/perplexity.rb', line 50 def window @window end |
Instance Method Details
#cache_key(text, _context) ⇒ Object
65 66 67 |
# File 'lib/vangrail/rails/perplexity.rb', line 65 def cache_key(text, _context) "#{completion.model}\n#{threshold}\n#{window}\n#{text}" end |
#call(text, _context) ⇒ Object
69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/vangrail/rails/perplexity.rb', line 69 def call(text, _context) body = text.to_s return pass if body.strip.empty? score = worst_window(body) return pass if score.nil? || score < threshold block(categories: ['high_perplexity'], reason: format('a span of %<window>d tokens scores %<score>.1f nats against a threshold of ' \ '%<threshold>.1f: this is not language', window: window, score: score, threshold: threshold)) rescue Error => e unchecked("perplexity check did not run: #{e.}") end |
#offline? ⇒ Boolean
61 62 63 |
# File 'lib/vangrail/rails/perplexity.rb', line 61 def offline? false end |
#worst_window(text) ⇒ Object
The highest mean negative log likelihood of any window, or nil when the text is too short to have one. Raises what the transport raises, so a probe script sees the error and a rail sees a Result.
87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/vangrail/rails/perplexity.rb', line 87 def worst_window(text) logprobs = completion.token_logprobs(text) return nil if logprobs.size < @floor size = [window, logprobs.size].min means = (0..(logprobs.size - size)).map do |start| slice = logprobs[start, size] -slice.sum / slice.size end means.max end |