joenathan commited on
Commit
d0ddd41
·
verified ·
1 Parent(s): 95cdfc0

Correct superseded conformal claims

Browse files

The conformal tab still carried pre-clean-split results: it claimed the bound held
at every risk level tested while the chart beside it shows three points above the
line. Now 6 of 9, with the exceedances given as 1.12/0.55/0.83 standard errors.
Also corrected: abstention 72%/3.4% -> 91.7%/12.8%; the monotonicity minimum
(lambda ~ 0.57 "falls to zero" -> 0.029 at lambda ~ 0.461, first rise 0.486);
"the bound holds at whatever value is chosen"; and the figure caption.

The header conflated micro-F1 with top-1 accuracy, which are the same metric here
-- the real difference is two training runs. Verified the served checkpoint at
172/180 = 95.6% against the notebook's 94.1%, and corrected train.py's comments
claiming it IS the notebook's model.

Mirrors llm-wiki commit edfa1a9.

Files changed (2) hide show
  1. app.py +31 -19
  2. train.py +6 -4
app.py CHANGED
@@ -134,8 +134,10 @@ with gr.Blocks(title="Explainable & Auditable Alloy Defect Detection",
134
  "hooks, following the course's own notebook).\n\n"
135
  "*The classifier itself is deliberately ordinary* — a compact 164k-parameter "
136
  "CNN, trained by the same recipe as the notebook: a stratified 70/10/10/10 split "
137
- "where the test images are never used to fit weights or pick a checkpoint "
138
- "(94.1% micro-F1 there; 95.6% top-1 accuracy for this checkpoint). That is the "
 
 
139
  "point: the question is not whether a CNN can score well on NEU-DET (it can), but "
140
  "whether the explanation sitting on top of it means anything. Tabs 2 and 3 test "
141
  "exactly that."
@@ -223,8 +225,14 @@ with gr.Blocks(title="Explainable & Auditable Alloy Defect Detection",
223
  The guarantee is arithmetic, not optimism.
224
 
225
  **It was validated, not just asserted:** empirical error stayed at or below
226
- target at **every** risk level tested (α = 0.1 through 0.9). Every point
227
- on the chart below sits on or under the dashed line.
 
 
 
 
 
 
228
 
229
  ---
230
 
@@ -240,8 +248,9 @@ with gr.Blocks(title="Explainable & Auditable Alloy Defect Detection",
240
  | Empty | Nothing clears the bar | **Abstain** — the model declines |
241
 
242
  The plant picks α. That single dial trades throughput against escalation
243
- volume, and the bound holds at whatever value is chosen. **This, not the
244
- accuracy number and not the heatmap, is what we would put in front of an
 
245
  auditor.**
246
 
247
  #### Three honest limits
@@ -256,26 +265,29 @@ with gr.Blocks(title="Explainable & Auditable Alloy Defect Detection",
256
  - **At lenient α the metric is dominated by abstentions.** We adapted a
257
  method built for pixel-level segmentation, where a prediction set spans
258
  thousands of pixels and is essentially never empty. With six classes, empty
259
- sets are common at strict bars — 72% of images at the most extreme
260
- setting — and an empty set is scored as a full error. The useful
261
- operating range here is the **low-α end**, where empty sets are rare
262
- (3.4%).
 
263
  - **A precondition of the theorem is not strictly met.** Conformal Risk Control
264
  requires the loss to be *monotone non-increasing* — as you flag more, the
265
  risk must never climb back. False-negative rate satisfies that; **false
266
- discovery rate does not.** Ours is U-shaped: it falls to zero around
267
- λ ≈ 0.57, then rises again as the flagged set fills with
268
- false positives. Every threshold we actually selected sits on the *descending*
269
- branch, where the condition effectively holds — which is consistent with
270
- the bound holding at all nine levels. But that is a property of this data, not
271
- a proof. **It is why the chart above is load-bearing rather than decorative:**
272
- with a precondition in question, checking the bound empirically on held-out
273
- data is doing real work.
 
 
274
  """
275
  )
276
  gr.Image(
277
  value="assets/conformal-guarantee.png",
278
- label="Conformal Risk Control: guarantee holds at every tested risk level (precomputed in the notebook)",
279
  interactive=False,
280
  )
281
 
 
134
  "hooks, following the course's own notebook).\n\n"
135
  "*The classifier itself is deliberately ordinary* — a compact 164k-parameter "
136
  "CNN, trained by the same recipe as the notebook: a stratified 70/10/10/10 split "
137
+ "where the test images are never used to fit weights or pick a checkpoint. "
138
+ "The notebook's run scores 94.1% on that test split; the separately trained "
139
+ "checkpoint served here scores 95.6% on the same split — same recipe and "
140
+ "same split, different training run. That is the "
141
  "point: the question is not whether a CNN can score well on NEU-DET (it can), but "
142
  "whether the explanation sitting on top of it means anything. Tabs 2 and 3 test "
143
  "exactly that."
 
225
  The guarantee is arithmetic, not optimism.
226
 
227
  **It was validated, not just asserted:** empirical error stayed at or below
228
+ target at **six of the nine** risk levels tested (α = 0.1 through 0.9).
229
+ The three that came in over — α = 0.10, 0.20 and 0.90 — miss by
230
+ **1.12, 0.55 and 0.83 standard errors** on 180 test images. The bound is on
231
+ *expected* error, so realisations scatter either side of the line; near-misses in
232
+ both directions are what a correctly behaving bound looks like. An earlier version
233
+ of this project reported nine out of nine, which looked stronger and was weaker
234
+ — that model had been checkpoint-selected on the very images it was scored
235
+ against.
236
 
237
  ---
238
 
 
248
  | Empty | Nothing clears the bar | **Abstain** — the model declines |
249
 
250
  The plant picks α. That single dial trades throughput against escalation
251
+ volume. Pick it at the **strict end**: α = 0.10 abstains on 12.8% of images,
252
+ α = 0.90 on 91.7% — within budget and operationally useless. **This, not
253
+ the accuracy number and not the heatmap, is what we would put in front of an
254
  auditor.**
255
 
256
  #### Three honest limits
 
265
  - **At lenient α the metric is dominated by abstentions.** We adapted a
266
  method built for pixel-level segmentation, where a prediction set spans
267
  thousands of pixels and is essentially never empty. With six classes, empty
268
+ sets are common at strict bars — 91.7% of images at the most lenient
269
+ α — and an empty set is scored as a full error. So at that end the
270
+ reported "false discovery rate" is very nearly a synonym for "abstention rate."
271
+ The useful operating range here is the **low-α end**, where empty sets are
272
+ far rarer (12.8%).
273
  - **A precondition of the theorem is not strictly met.** Conformal Risk Control
274
  requires the loss to be *monotone non-increasing* — as you flag more, the
275
  risk must never climb back. False-negative rate satisfies that; **false
276
+ discovery rate does not.** Ours is U-shaped: it falls from 0.949 to a minimum of
277
+ 0.029 around λ ≈ 0.461, then climbs back to 0.755 as the
278
+ flagged set fills with false positives — rising at 80 of 399 grid steps.
279
+ Every threshold we actually selected (0.004–0.219) sits on the *descending*
280
+ branch, where the condition effectively holds; the first rise is not until
281
+ λ = 0.486. Capping the search grid at λ ≤ 0.48
282
+ would satisfy the precondition outright **without changing a single reported
283
+ number**. But that is a property of this data, not a proof. **It is why the chart
284
+ above is load-bearing rather than decorative:** with a precondition in question,
285
+ checking the bound empirically on held-out data is doing real work.
286
  """
287
  )
288
  gr.Image(
289
  value="assets/conformal-guarantee.png",
290
+ label="Conformal Risk Control: empirical FDR against target at nine risk levels; under target at six, over by ~1 standard error at three (precomputed in the notebook)",
291
  interactive=False,
292
  )
293
 
train.py CHANGED
@@ -17,7 +17,7 @@ from torch.utils.data import DataLoader, Dataset
17
 
18
  from model import CLASS_NAMES, IMAGE_SIZE, MEAN, STD, DefectCNN
19
 
20
- SEED = 41 # matches the notebook, so this checkpoint IS the notebook's model
21
  DATA_ROOT = os.path.join("..", "data", "neu-det-src", "IMAGES")
22
  CHECKPOINT_PATH = "model.pt"
23
  EXAMPLES_DIR = "examples"
@@ -74,9 +74,11 @@ def main():
74
  for name, count in Counter(labels_by_path.values()).items():
75
  print(f" {name:16s} {count:4d}")
76
 
77
- # Stratified 70/10/10/10 per class, identical to the notebook's Section 2 so the
78
- # served checkpoint is the same model the notebook reports on. Only train and the
79
- # final test split are needed here; val is used for checkpoint selection.
 
 
80
  rng = np.random.default_rng(SEED)
81
  train_paths, val_paths, eval_paths = [], [], []
82
  for cls in CLASS_NAMES:
 
17
 
18
  from model import CLASS_NAMES, IMAGE_SIZE, MEAN, STD, DefectCNN
19
 
20
+ SEED = 41 # matches the notebook, so this checkpoint sees the notebook's exact splits
21
  DATA_ROOT = os.path.join("..", "data", "neu-det-src", "IMAGES")
22
  CHECKPOINT_PATH = "model.pt"
23
  EXAMPLES_DIR = "examples"
 
74
  for name, count in Counter(labels_by_path.values()).items():
75
  print(f" {name:16s} {count:4d}")
76
 
77
+ # Stratified 70/10/10/10 per class, identical to the notebook's Section 2, so the
78
+ # served checkpoint is trained and scored on exactly the notebook's splits. It is a
79
+ # separate training run, so its weights and test score differ from the notebook's
80
+ # (95.6% here vs 94.1% there). Only train and the final test split are needed here;
81
+ # val is used for checkpoint selection.
82
  rng = np.random.default_rng(SEED)
83
  train_paths, val_paths, eval_paths = [], [], []
84
  for cls in CLASS_NAMES: