Class: Vangrail::Rails::Bayes
- Inherits:
-
Vangrail::Rail
- Object
- Vangrail::Rail
- Vangrail::Rails::Bayes
- Defined in:
- lib/vangrail/rails/bayes.rb
Overview
A naive Bayes classifier over word n-grams, which is the oldest working text classifier there is and the only rail here that says how sure it is.
Every other rail answers yes or no, so it hands the evidence arithmetic
exactly one bit of information however certain it was. This one computes a
log-likelihood ratio directly, which is the quantity that arithmetic
actually wants: a clause scoring twelve bits and a clause scoring three
both "fire", and they are not the same observation. A rail that puts
bits in its result's raw is read that way by Engine#assess, and this is
the first rail to do it.
Scored clause by clause, taking the worst. The same dilution problem the containment rail hit applies here with force: an attack document is ordinary documentation with one injected sentence in it, and a bag of features over the whole page is mostly evidence about the handbook.
The shipped weights are a demonstration, and the honest number says so. Source groups have disjoint train, calibration, threshold, and final-test roles. On six final-test attacks and six benign pages, the selected threshold catches three attacks and flags two benign pages. Six calibration attacks defend no positive likelihood-ratio bits at 95% joint credibility. The junk-mail filters this borrows from were fitted on millions of examples.
So it is off by default, and what it is for is the retraining path: a deployment with its own traffic runs script/train_bayes.rb against its own corpus and gets a rail fitted to the attacks it actually receives, with separate calibration, threshold, and final-test evidence.
WHAT THE SHIPPED WEIGHTS DO ON REAL TEXT, WHICH IS WORSE THAN "A DEMONSTRATION" SUGGESTS. Measured on installed documentation, this rail fires on 74.9% of it -- three documents in four -- at the threshold that ships. It catches 101 of BIPIA's 125 injections spliced into the same pages, and those two numbers are one number: a rail that fires on everything catches everything. Its AUC on that splice is 0.557 against 0.5 for a coin, and script/calibrate_threshold.rb held to a 1% false-alarm rate finds a threshold that catches 0.8%.
The cause is in the weights and anyone can read it. The top features tie at 2.492 and they are "answer", "page", "as", "all", "of", "that", "thi page", "page as"; 21 of the 300 are nothing but stopwords; and "page" is the commonest word in a corpus of manual pages. This is the shortcut learning Li et al. name in InjecGuard (arXiv:2410.22770), where a guard model learns a trigger word straight through to a verdict, arrived at from 60 training clauses.
The external evaluation carried 8.86% for years because that rate was measured before the artifact was regenerated and nothing re-ran it. A trained artifact and a measurement of it are one unit.
Instance Attribute Summary collapse
-
#calibration ⇒ Object
readonly
Returns the value of attribute calibration.
-
#threshold ⇒ Object
readonly
Returns the value of attribute threshold.
-
#weights ⇒ Object
readonly
Returns the value of attribute weights.
Instance Method Summary collapse
-
#bits(text) ⇒ Object
What that score is worth, fitted only on the calibration role and read at the simultaneous 95% likelihood-ratio bound.
- #bits_for(score) ⇒ Object
- #cache_key(text, _context) ⇒ Object
- #decide(text, _context) ⇒ Object
-
#initialize(weights: BayesData::WEIGHTS, threshold: BayesData::THRESHOLD, calibration: BayesData::CALIBRATION, name: 'bayes', sides: %i[input context])) ⇒ Bayes
constructor
A new instance of Bayes.
- #quantifies? ⇒ Boolean
-
#score_for(text) ⇒ Object
The worst clause's raw naive Bayes score.
- #uncertain_score?(score) ⇒ Boolean
Constructor Details
#initialize(weights: BayesData::WEIGHTS, threshold: BayesData::THRESHOLD, calibration: BayesData::CALIBRATION, name: 'bayes', sides: %i[input context])) ⇒ Bayes
Returns a new instance of Bayes.
58 59 60 61 62 63 64 |
# File 'lib/vangrail/rails/bayes.rb', line 58 def initialize(weights: BayesData::WEIGHTS, threshold: BayesData::THRESHOLD, calibration: BayesData::CALIBRATION, name: 'bayes', sides: %i[input context]) super(name: name, sides: sides) @weights = weights @threshold = threshold @calibration = calibration end |
Instance Attribute Details
#calibration ⇒ Object (readonly)
Returns the value of attribute calibration.
66 67 68 |
# File 'lib/vangrail/rails/bayes.rb', line 66 def calibration @calibration end |
#threshold ⇒ Object (readonly)
Returns the value of attribute threshold.
66 67 68 |
# File 'lib/vangrail/rails/bayes.rb', line 66 def threshold @threshold end |
#weights ⇒ Object (readonly)
Returns the value of attribute weights.
66 67 68 |
# File 'lib/vangrail/rails/bayes.rb', line 66 def weights @weights end |
Instance Method Details
#bits(text) ⇒ Object
What that score is worth, fitted only on the calibration role and read at the simultaneous 95% likelihood-ratio bound. This is the number that goes into a posterior; unsupported score bands contribute zero.
111 112 113 |
# File 'lib/vangrail/rails/bayes.rb', line 111 def bits(text) bits_for(score_for(text)) end |
#bits_for(score) ⇒ Object
115 116 117 118 |
# File 'lib/vangrail/rails/bayes.rb', line 115 def bits_for(score) band = calibration.reverse.detect { |floor, _| score > floor } band ? band.last : calibration.first.last end |
#cache_key(text, _context) ⇒ Object
72 73 74 |
# File 'lib/vangrail/rails/bayes.rb', line 72 def cache_key(text, _context) "#{threshold}\n#{text}" end |
#decide(text, _context) ⇒ Object
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/vangrail/rails/bayes.rb', line 76 def decide(text, _context) score = score_for(text) evidence = bits_for(score) payload = { 'bits' => evidence, 'score' => score } # A positive score at or below the threshold is not a decision. The # threshold role did not select it, and the small calibration role cannot # defend positive evidence for it. if uncertain_score?(score) return unchecked('score sits in a band the calibration cannot separate', raw: payload) end return pass(raw: payload) if score <= threshold block(categories: ['bayes'], raw: payload, reason: format('scores %<score>+.1f, worth %<bits>+.1f bits of evidence', score: score, bits: evidence)) end |
#quantifies? ⇒ Boolean
68 69 70 |
# File 'lib/vangrail/rails/bayes.rb', line 68 def quantifies? true end |
#score_for(text) ⇒ Object
The worst clause's raw naive Bayes score. Not a likelihood ratio and not to be added to one: the features are counted as independent and are not, so this number is confidently wrong about its own size. It decides the block, because a threshold only needs an ordering.
101 102 103 104 105 106 |
# File 'lib/vangrail/rails/bayes.rb', line 101 def score_for(text) clauses = NLP.clauses(text) return clause_score(text.to_s) if clauses.empty? clauses.map { |clause| clause_score(clause) }.max end |
#uncertain_score?(score) ⇒ Boolean
93 94 95 |
# File 'lib/vangrail/rails/bayes.rb', line 93 def uncertain_score?(score) score.positive? && score <= threshold end |