Views
No views yet
Code is also mirrored on GitHub: https://github.com/Nieuwlaar/resnet50-scroll1-checkpoint-repair
model.safetensors for scrollprize/resnet50_7.9um_scroll1_frags.
The released checkpoint contains a well-trained 3D ResNet-50 backbone but a decoder head whose weights were never trained, so the full model outputs ~0.5 everywhere and finds no ink. I kept the released backbone, retrained the head on the public Grand Prize ink labels, and lightly fine-tuned. On two held-out Scroll 1 segments the checkpoint goes from chance level to strong ink detection:| held-out segment | stock checkpoint | repaired checkpoint |
|---|---|---|
20230827161847 (never used for training or model selection) | 0.5022 | 0.9369 |
20231210121321 (used only to pick the best epoch) | 0.5122 | 0.9431 |


20230827161847: scan layer, Grand Prize ink labels, stock checkpoint output (constant ~0.5), repaired checkpoint output.numpy and safetensors:1pip install numpy safetensors huggingface_hub
2huggingface-cli download scrollprize/resnet50_7.9um_scroll1_frags model.safetensors --local-dir stock
3python verify_checkpoint.py stock/model.safetensorsfile: stock/model.safetensors
size: 342,867,728 bytes
sha256: fa00728db74d714769960dd77e9911a0e0938b3f272933c153632b5bed543c34
decoder conv weights vs PyTorch default init U(-b, +b), b = 1/sqrt(fan_in)
tensor absmax/b mean|w|/(b/2) std/(b/sqrt(3))
decoder.convs.0.0.weight 1.0000 1.0004 1.0002
decoder.convs.1.0.weight 1.0000 1.0001 1.0000
decoder.convs.2.0.weight 1.0000 0.9999 0.9999
decoder.logit.weight 0.9835 0.9990 0.9898
backbone.conv1.weight 48.2277 (trained contrast)
BatchNorm num_batches_tracked (training batches seen):
decoder.convs.0.1.num_batches_tracked 0
decoder.convs.1.1.num_batches_tracked 0
decoder.convs.2.1.num_batches_tracked 0
backbone (53 layers) 2,234,791
VERDICT: decoder head is at PyTorch default init (UNTRAINED).
Full-model ink predictions from this file will be chance-level.model.safetensors reports the decoder as trained (decoder conv ratios 6.6–146x their init bound, decoder BatchNorm counters 32,536) and exits 0.f3975de34d4734dd507360304f828808b7182139), three separate checks all say the same thing:num_batches_tracked = 0 with pristine default statistics (mean 0, variance 1); every backbone batch-norm reads 2,234,791. A supporting oddity: config.json declares n_classes=1139, and the 1139-way classification head from pretraining is still present in the file as backbone.fc, trained (absmax ~14x its init bound) but unused by the ink pipeline. That fits a checkpoint exported from a pretraining setup, with the segmentation decoder that replaced that head left at default init.config.json loads the full model. A user who runs it gets silent chance-level output that looks like "no ink found".ink_9um checkpoints, and the three Scroll 1 TimeSformer releases. 19 files, anonymous downloads, CPU only:resnet50_3um_01122024/model.safetensors is broken in exactly the same way: a different, independently trained backbone (batch-norm counters 2,231,011 vs 2,234,791; different weights) with its own never-trained decoder: all decoder convs on the untrained-init bound, decoder batch-norm counters 0. The unmodified verify_checkpoint.py detects it and exits 1.ink_9um checkpoints (head-group tensors 3.7–5.5x off their init bounds, the ink task head itself 6.1x or more, and the head weights move smoothly between released training steps, which is what an actively trained head looks like) and the TimeSformer-family files (ink_detection_pipeline is byte-identical to timesformer_GP_scroll1).audit/CORPUS_AUDIT.md with machine-readable results in audit/results.csv; audit/audit_checkpoint.py generalizes the verifier to the other architectures.backbone.* + decoder.*), so it is a drop-in replacement wherever the original is loaded: download the original repository for the model code and config, then point the weights at this file.1import sys
2from huggingface_hub import snapshot_download, hf_hub_download
3
4code_dir = snapshot_download("scrollprize/resnet50_7.9um_scroll1_frags") # model code + config
5weights = hf_hub_download("Nieuwlaar/resnet50_7.9um_scroll1_frags-repaired",
6 "model.safetensors")
7
8sys.path.insert(0, "scripts") # scripts/ from this repository
9from common import load_resnet_model
10model, cfg = load_resnet_model(code_dir, ckpt_path=weights) # torch nn.Module + config dictscripts/common.py selects them, global z 24–41 of the standard 65-layer segment stacks), uint8 intensities normalized as clip(x, 0, 200) / 255, sigmoid over the output logits, output at 1/4 resolution. scripts/eval_panel.py implements full-segment tiled inference (window 256, stride 128, Gaussian blending) end to end:1python scripts/prep_data.py --seg-dir data/20230827161847 --seg-id 20230827161847
2python scripts/eval_panel.py --model resnet --model-dir <code_dir> \
3 --ckpt model.safetensors --seg-dir data/20230827161847 \
4 --out-prefix eval/20230827161847_after*_mask.png come from the public data server (https://dl.ash2txt.org/full-scrolls/Scroll1/PHercParis4.volpkg/paths/); the *_inklabels files come from the ScrollPrize villa repository at deprecated/ink-detection/all_labels (until 2026-08-17 at ink-detection/all_labels). 20231022170901's label is a .tiff there, which prep_data.py handles.deprecated/ink-detection/all_labels; until 2026-08-17 at ink-detection/all_labels). Two segments were held out and never trained on: 20231210121321 and 20230827161847.20231022170901, 20231106155351, 20231005123336, 20230820203112, 20230826170124, 20230702185753, 20230522215721, 20230531193658, 20230903193206, 20230902141231, 20231007101615, 20230929220926, 20231016151000, 20231012184423, 20231031143850| phase | epoch | val tile AUC |
|---|---|---|
| decoder only | 1 → 5 | 0.9165 → 0.9256 |
| full fine-tune | 1 | 0.9272 |
| full fine-tune | 3 (released) | 0.9314 |
| full fine-tune | 4 | 0.9306 |
training/metrics.json; exact configuration is the defaults of scripts/train_decoder.py:1# one-time setup: the original repo provides model code, config, and the stock weights
2huggingface-cli download scrollprize/resnet50_7.9um_scroll1_frags --local-dir models/resnet50_7.9um_scroll1_frags
3
4python scripts/train_decoder.py --data-root data --out out/job1 --always-full-ftmodel.safetensors: repaired weights, drop-in replacement (same key layout as the original checkpoint); sha256 6133ef74905d32a40d0e053d9378bf76207c29614cb77210218f1df898b8d3cfverify_checkpoint.py: the 60-second CPU diagnosis scriptscripts/: train_decoder.py, prep_data.py, eval_panel.py, common.py, the exact training and evaluation code, segment lists, and seedeval/: before/after panels and per-segment AUC JSONs for both held-out segmentstraining/: metrics.json (full training history), best_info.json (selected checkpoint, seed, segment lists)audit/: corpus audit of the public scrollprize checkpoints, with CORPUS_AUDIT.md (findings), results.csv (per-file statistics), audit_checkpoint.py (the verifier generalized to the other architectures)ink_9um release targets ~9 µm isotropic inputs. This repaired model does a different job: it restores a working detector for the ~7.9 µm surface-volume stacks that most existing Scroll 1 segments ship in. It is not the only detector for those stacks (the 2023 Grand Prize TimeSformer also runs natively on them; it is the control in defect Evidence 3 above), but it is the only released 3D-ResNet one, useful as an independent-architecture second opinion and as an initialization for distillation or fine-tuning work.20231210121321 was used to choose the best epoch, and the stock model card lists it among the backbone's pretraining fragments. Both are reasons 20230827161847 is the cleaner held-out number.scrollprize/resnet50_7.9um_scroll1_frags release; see that repository for its terms. Scroll data and ink labels come from the Vesuvius Challenge data server (https://dl.ash2txt.org/LICENSE.txt).