Instructions to use toranb/theo-bert-base with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use toranb/theo-bert-base with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("fill-mask", model="toranb/theo-bert-base", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModelForMaskedLM model = AutoModelForMaskedLM.from_pretrained("toranb/theo-bert-base", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Initial release: TheoBERT Base — biblical-domain masked language model
Browse files273M-parameter custom bidirectional encoder pretrained from scratch on
biblical and theological text, packaged for Hugging Face Hub.
Model
- Custom architecture: 12 layers, hidden 768, 8 heads, RoPE on Q/K,
RMSNorm on Q/K, ReLU² MLP, gated value embeddings on even layers,
learned residual interpolation between block output and initial
embedding; MLM head: Linear → GELU → RMSNorm → Linear
- Weights released as fp16 safetensors (546 MB)
- config.json declares torch_dtype: float32 so default loads upcast on
read; CPU inference is numerically safe by default. Pass
dtype=torch.float16 for GPU fp16 inference.
- Loads via AutoModel / AutoModelForMaskedLM with trust_remote_code=True
- Bundles the google-bert/bert-base-uncased fast tokenizer so
AutoTokenizer.from_pretrained(repo) and the Hub fill-mask widget work
out of the box
Training
- Stage 1: 24 epochs MLM pretraining, 256-token context, 270K biblical
and theological sequences (final train loss 1.0679, accuracy 76.42%)
- Stage 2: 25 epochs whole-word-masking continued pretraining at 18%
mask rate (final train loss 0.8958, accuracy 79.66%)
- Custom dual optimizer: AdamW for embeddings, MLM head, residual
lambdas, and value gates; Muon for linear layer weight matrices
Evaluation
- 94.7% pass rate (517/546) on domain-specific MLM benchmark covering
9 theological categories; difficulty-weighted 94.6%
- fp16 round-trip verified lossless against the fp32 source checkpoint
- Canonical-knowledge cases overlap with biblical text in the training
corpus; that category should be read as in-distribution recall rather
than held-out generalization
- Full methodology, schema, and per-category breakdown in EVAL.md
Repository contents
- modeling_theo_bert_base.py / configuration_theo_bert_base.py — HF
PreTrainedModel and PretrainedConfig implementations
- muon.py — Muon optimizer (retained for self-contained fine-tuning)
- scripts/mlm_eval_safetensors.py — reproduces the 546-case benchmark
from a HF repo directory (--repo-dir, defaults to current directory)
- model.safetensors, config.json, checkpoint_metadata.json, eval.json
- tokenizer.json, tokenizer_config.json (bundled bert-base-uncased)
- MODEL_CARD.md, README.md, EVAL.md, LICENSE (Apache 2.0)
- .gitignore +3 -0
- EVAL.md +250 -0
- LICENSE +199 -0
- MODEL_CARD.md +155 -0
- README.md +181 -0
- __init__.py +11 -0
- checkpoint_metadata.json +30 -0
- config.json +30 -0
- configuration_theo_bert_base.py +47 -0
- eval.json +0 -0
- model.safetensors +3 -0
- modeling_theo_bert_base.py +317 -0
- muon.py +52 -0
- pyproject.toml +14 -0
- requirements.txt +3 -0
- scripts/mlm_eval_safetensors.py +1138 -0
- tokenizer.json +0 -0
- tokenizer_config.json +15 -0
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
.venv
|
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Evaluation Suite: TheoBERT Base MLM Benchmark
|
| 2 |
+
|
| 3 |
+
A 546-case domain-specific evaluation suite measuring masked language modeling performance on biblical and theological text.
|
| 4 |
+
|
| 5 |
+
## Overview
|
| 6 |
+
|
| 7 |
+
The evaluation tests whether TheoBERT Base has internalized domain-specific semantics — not just surface-level co-occurrence. It goes beyond a standard perplexity measurement by testing the model against carefully constructed test cases that probe theological precision, canonical recall, and doctrinal discrimination.
|
| 8 |
+
|
| 9 |
+
**Result:** 94.7% pass rate (517 / 546), difficulty-weighted score 94.6%.
|
| 10 |
+
|
| 11 |
+
## Test Types
|
| 12 |
+
|
| 13 |
+
The suite uses three evaluation strategies, each targeting a different aspect of model competence:
|
| 14 |
+
|
| 15 |
+
### 1. Doctrinal Association (221 cases, `target_in_top_k`)
|
| 16 |
+
|
| 17 |
+
> *Does the model know what belongs in the blank?*
|
| 18 |
+
|
| 19 |
+
A sentence from scripture or doctrinal writing with one `[MASK]` token. The correct answer(s) must appear in the model's top-k predictions.
|
| 20 |
+
|
| 21 |
+
**Pass condition:** At least one correct or acceptable-alternative token appears in the top-k (k=5) predictions.
|
| 22 |
+
|
| 23 |
+
**Example:**
|
| 24 |
+
```
|
| 25 |
+
Input: "For as often as you eat this bread and drink the cup,
|
| 26 |
+
you proclaim the Lord's [MASK] until he comes."
|
| 27 |
+
Target: "death"
|
| 28 |
+
Pass: "death" is in top-5 predicted tokens → ✅
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
These cases span 9 theological categories (bibliology, christology, ecclesiology, eschatology, hamartiology, pneumatology, soteriology, theology proper, spiritual warfare, kingdom theology, Romans road).
|
| 32 |
+
|
| 33 |
+
### 2. Canonical Knowledge (138 cases, `target_in_top_k`)
|
| 34 |
+
|
| 35 |
+
> *Does the model know the exact wording of specific verses?*
|
| 36 |
+
|
| 37 |
+
Direct recall of well-known biblical passages. No theological reasoning required — this tests pure memorization of canonical text.
|
| 38 |
+
|
| 39 |
+
> **Train/eval overlap note.** The training corpus includes bible text, so canonical-knowledge cases are not held out from the training distribution. The 88.4% pass rate on this category should be interpreted as in-distribution recall, not as evidence of generalization to unseen text.
|
| 40 |
+
|
| 41 |
+
**Pass condition:** Same as doctrinal association (correct token in top-5).
|
| 42 |
+
|
| 43 |
+
**Example:**
|
| 44 |
+
```
|
| 45 |
+
Input: "Love bears all things, [MASK] all things, hopes all things,
|
| 46 |
+
endures all things."
|
| 47 |
+
Target: "believes"
|
| 48 |
+
Pass: "believes" is in top-5 → ✅
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
This is the hardest category for the model (88.4% pass rate), with most failures clustering around Old Testament proper nouns.
|
| 52 |
+
|
| 53 |
+
### 3. Contrastive Theology (187 cases, `correct_beats_foil`)
|
| 54 |
+
|
| 55 |
+
> *Can the model tell the difference between theologically correct and incorrect completions?*
|
| 56 |
+
|
| 57 |
+
A sentence with one `[MASK]` token, a theologically correct target, and a deliberately wrong *foil* — a word that is semantically plausible in general English but theologically incorrect in context. The model must assign higher probability to the target than the foil.
|
| 58 |
+
|
| 59 |
+
**Pass condition:** P(target) > P(foil). The margin is additionally classified as high (>0.10), medium (>0.02), or low (≤0.02) confidence.
|
| 60 |
+
|
| 61 |
+
**Example:**
|
| 62 |
+
```
|
| 63 |
+
Input: "Paul teaches that believers are justified freely by God's [MASK],
|
| 64 |
+
not by human merit or works of the law."
|
| 65 |
+
Target: "grace"
|
| 66 |
+
Foil: "law"
|
| 67 |
+
Pass: P("grace") > P("law") → ✅
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
The foil "law" is lexically primed by the later phrase "works of the law," making this a hard case (the model must override surface-level co-occurrence with doctrinal understanding). Failure examples like "power", "will", "plan" — generic divine attributes that miss the specific grace/law antithesis — are tracked as `critical_failure` signals.
|
| 71 |
+
|
| 72 |
+
## Categories
|
| 73 |
+
|
| 74 |
+
| Category | Cases | Pass Rate | Description |
|
| 75 |
+
|---|---|---|---|
|
| 76 |
+
| Pneumatology | 42 | 100% | Holy Spirit: person, work, gifts, indwelling |
|
| 77 |
+
| Soteriology | 109 | 98.2% | Salvation: justification, sanctification, atonement |
|
| 78 |
+
| Ecclesiology | 40 | 97.5% | Church: body of Christ, ordinances, unity |
|
| 79 |
+
| Hamartiology | 34 | 97.1% | Sin: nature, origin, consequences |
|
| 80 |
+
| Christology | 84 | 96.4% | Christ: person, natures, incarnation, atonement |
|
| 81 |
+
| Eschatology | 36 | 94.4% | Last things: return of Christ, judgment, resurrection |
|
| 82 |
+
| Theology proper | 46 | 91.3% | God: attributes, trinity, sovereignty |
|
| 83 |
+
| Canonical knowledge | 138 | 88.4% | Verse-level recall from specific biblical passages |
|
| 84 |
+
| Bibliology | 6 | — | Scripture: inspiration, authority, sufficiency |
|
| 85 |
+
| Romans road | 8 | — | Evangelistic verses from Romans |
|
| 86 |
+
| Spiritual warfare | 2 | — | Cosmic conflict, armor of God |
|
| 87 |
+
| Kingdom theology | 1 | — | Already/not-yet kingdom framework |
|
| 88 |
+
|
| 89 |
+
Categories with very few cases (bibliology, Romans road, spiritual warfare, kingdom theology) are present in the eval but reported in aggregate; their individual pass rates should be interpreted cautiously.
|
| 90 |
+
|
| 91 |
+
## Difficulty Levels
|
| 92 |
+
|
| 93 |
+
Each test case is assigned a difficulty rating:
|
| 94 |
+
|
| 95 |
+
| Difficulty | Cases | Pass Rate | Criteria |
|
| 96 |
+
|---|---|---|---|
|
| 97 |
+
| Easy | 99 | 94.9% | High-frequency verse, single obvious completion |
|
| 98 |
+
| Medium | 275 | 94.9% | Requires domain context but has strong lexical cues |
|
| 99 |
+
| Hard | 172 | 94.2% | Multiple plausible completions, adversarial foil priming, rare vocabulary, or multi-token targets |
|
| 100 |
+
|
| 101 |
+
### What Makes a Case "Hard"
|
| 102 |
+
|
| 103 |
+
- **Foil priming** — The foil word appears elsewhere in the same sentence, creating lexical pressure toward the wrong answer (e.g., "law" in a sentence that ends with "works of the law")
|
| 104 |
+
- **Multi-piece targets** — Words that tokenize into 2+ wordpieces require sequence-level probability scoring across mask positions
|
| 105 |
+
- **Rare proper nouns** — Old Testament names, places, and transliterated terms with low training frequency
|
| 106 |
+
- **Abstract theological distinctions** — Cases where the model must discriminate between near-synonyms with different doctrinal implications
|
| 107 |
+
|
| 108 |
+
## Multi-Token and Multi-Piece Handling
|
| 109 |
+
|
| 110 |
+
When a target word tokenizes into multiple wordpieces (e.g., "Nebuchadnezzar" → 5 subword tokens), the eval automatically:
|
| 111 |
+
|
| 112 |
+
1. Expands `[MASK]` into the required number of mask tokens
|
| 113 |
+
2. Uses beam search over the mask positions to find the target sequence
|
| 114 |
+
3. Matches either exact sequence equality or subsequence containment (for targets that span fewer positions than available masks)
|
| 115 |
+
|
| 116 |
+
This handles cases like:
|
| 117 |
+
- `"sabachthani"` (multi-piece from Aramaic transliteration)
|
| 118 |
+
- `"iniquity"` (3 wordpieces in bert-base-uncased)
|
| 119 |
+
- `"propitiation"` (4 wordpieces)
|
| 120 |
+
|
| 121 |
+
Failure examples are also tracked for multi-piece targets — if the model produces a generic failure sequence (e.g., "power", "will") instead of the theologically correct multi-piece target, it's flagged.
|
| 122 |
+
|
| 123 |
+
## Scoring Protocol
|
| 124 |
+
|
| 125 |
+
### target_in_top_k (357 cases)
|
| 126 |
+
|
| 127 |
+
```
|
| 128 |
+
pass = correct_token_id ∈ top_k(token_ids)
|
| 129 |
+
mrr = 1 / (rank of first correct token), or 0 if not found
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
### correct_beats_foil (187 cases)
|
| 133 |
+
|
| 134 |
+
```
|
| 135 |
+
pass = P(target) > P(foil)
|
| 136 |
+
margin = P(target) - P(foil)
|
| 137 |
+
confidence = high if margin > 0.10
|
| 138 |
+
| medium if margin > 0.02
|
| 139 |
+
| low otherwise
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
### all_top_k_in_target_set (2 cases)
|
| 143 |
+
|
| 144 |
+
```
|
| 145 |
+
pass = (valid_tokens_in_top_k / k) ≥ 0.8
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
Used when multiple targets are equally acceptable and the model should surface the theological cluster rather than a single token.
|
| 149 |
+
|
| 150 |
+
## Error Classification
|
| 151 |
+
|
| 152 |
+
When a test case fails, the eval attempts to classify *why*:
|
| 153 |
+
|
| 154 |
+
| Error type | Meaning |
|
| 155 |
+
|---|---|
|
| 156 |
+
| `near_miss` | Correct token ranked at k+1 or k+2 — nearly passed |
|
| 157 |
+
| `generic_over_theological` | Top predictions are generic/universal words (e.g., "power", "love", "will") rather than theologically specific terms |
|
| 158 |
+
| `wrong_semantic_cluster` | Wrong token is still semantically related but theologically incorrect |
|
| 159 |
+
| `total_miss` | Correct token ranked below position 20 — model has essentially no signal |
|
| 160 |
+
|
| 161 |
+
## Critical Failures
|
| 162 |
+
|
| 163 |
+
A **critical failure** is a test case where, regardless of pass/fail status, one of the explicitly listed `failure_examples` tokens appears in the top-3 predictions. This signals that the model is drifting toward generic religious language rather than precise theological vocabulary.
|
| 164 |
+
|
| 165 |
+
Critical failure rate: extracted from the full results JSON.
|
| 166 |
+
|
| 167 |
+
## Running the Evaluation
|
| 168 |
+
|
| 169 |
+
Install the dependencies first. `uv pip install` is recommended for speed and resolver behavior, but plain `pip install` works too:
|
| 170 |
+
|
| 171 |
+
```bash
|
| 172 |
+
uv pip install -r requirements.txt
|
| 173 |
+
# or: pip install -r requirements.txt
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
```bash
|
| 177 |
+
# Default: load model.safetensors from repo root, eval against eval.json
|
| 178 |
+
python scripts/mlm_eval_safetensors.py
|
| 179 |
+
|
| 180 |
+
# With GPU
|
| 181 |
+
python scripts/mlm_eval_safetensors.py --device cuda
|
| 182 |
+
|
| 183 |
+
# Compare against a previous run (proves fp16 round-trip fidelity)
|
| 184 |
+
python scripts/mlm_eval_safetensors.py --compare eval_results/d12_encoder_mlm_eval.json
|
| 185 |
+
|
| 186 |
+
# Custom paths
|
| 187 |
+
python scripts/mlm_eval_safetensors.py \
|
| 188 |
+
--repo-dir /path/to/repo \
|
| 189 |
+
--eval-path /path/to/eval.json \
|
| 190 |
+
--device cuda
|
| 191 |
+
|
| 192 |
+
# Adjust top-k and sampling
|
| 193 |
+
python scripts/mlm_eval_safetensors.py --k 10 --n-samples 5
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
The script writes results to `eval_results/safetensors_mlm_eval.json` by default.
|
| 197 |
+
|
| 198 |
+
### Verifying fp16 Fidelity
|
| 199 |
+
|
| 200 |
+
The `--compare` flag diffs the safetensors (fp16→fp32) results against a prior evaluation of the original fp32 `.pt` checkpoint. If every test case produces the same pass/fail outcome, the fp16 storage is proven lossless for this model — no quantization artifacts affect semantic predictions.
|
| 201 |
+
|
| 202 |
+
## Test Case Schema
|
| 203 |
+
|
| 204 |
+
Each test case in `eval.json` has this structure:
|
| 205 |
+
|
| 206 |
+
```json
|
| 207 |
+
{
|
| 208 |
+
"id": "DOC_001",
|
| 209 |
+
"type": "doctrinal_association",
|
| 210 |
+
"category": "soteriology",
|
| 211 |
+
"difficulty": "medium",
|
| 212 |
+
"input": "Paul teaches that the message of the [MASK] is foolishness...",
|
| 213 |
+
"targets": ["cross"],
|
| 214 |
+
"foils": [],
|
| 215 |
+
"acceptable_alternatives": [],
|
| 216 |
+
"failure_examples": ["church", "gospel", "law", "bible", "world"],
|
| 217 |
+
"pass_condition": "target_in_top_k",
|
| 218 |
+
"k": 5,
|
| 219 |
+
"reference": "1 Corinthians 1:18",
|
| 220 |
+
"reasoning": "The cross as the central message of the gospel...",
|
| 221 |
+
"surface_confounder": ""
|
| 222 |
+
}
|
| 223 |
+
```
|
| 224 |
+
|
| 225 |
+
| Field | Description |
|
| 226 |
+
|---|---|
|
| 227 |
+
| `id` | Unique identifier within the suite |
|
| 228 |
+
| `type` | One of: `doctrinal_association`, `canonical_knowledge`, `contrastive_theology` |
|
| 229 |
+
| `category` | Theological category (see table above) |
|
| 230 |
+
| `difficulty` | `easy`, `medium`, or `hard` |
|
| 231 |
+
| `input` | The masked sentence. Must contain at least one `[MASK]` |
|
| 232 |
+
| `targets` | Correct completion(s) for the masked position(s) |
|
| 233 |
+
| `foils` | Deliberately incorrect but plausible completions (contrastive only) |
|
| 234 |
+
| `acceptable_alternatives` | Also-correct completions beyond the primary target |
|
| 235 |
+
| `failure_examples` | Tokens that would indicate the model failed to internalize the domain, even if the primary target is predicted |
|
| 236 |
+
| `pass_condition` | Scoring strategy: `target_in_top_k`, `correct_beats_foil`, or `all_top_k_in_target_set` |
|
| 237 |
+
| `k` | Number of top predictions to consider |
|
| 238 |
+
| `reference` | Source verse or doctrinal concept |
|
| 239 |
+
| `reasoning` | Human-readable explanation of what the case tests and why the foil is wrong (if applicable) |
|
| 240 |
+
| `surface_confounder` | Linguistic surface feature that could mislead a shallow model (if any) |
|
| 241 |
+
|
| 242 |
+
## Design Philosophy
|
| 243 |
+
|
| 244 |
+
This eval was designed to probe **domain-specific MLM behavior**, not general linguistic fluency. A general-purpose BERT model may score well on standard MLM benchmarks while producing theologically incoherent completions on biblical text. The three test types target different aspects of that behavior:
|
| 245 |
+
|
| 246 |
+
1. **Doctrinal association** checks whether the model has absorbed domain-specific co-occurrence patterns — the "language" of theology
|
| 247 |
+
2. **Canonical knowledge** checks whether the model has memorized specific verses — the "data" of scripture
|
| 248 |
+
3. **Contrastive theology** checks whether the model prefers doctrinally correct completions over plausible foils
|
| 249 |
+
|
| 250 |
+
The foil-based contrastive cases are the most discriminative: they test whether the model assigns higher probability to a doctrinally correct target than to a surface-level lexical confounder. Results on this suite should be read as evidence about behavior on cases of this shape, not as a general measure of theological understanding. The training corpus and eval suite were authored privately and have not been externally audited, so some train/eval distributional overlap (especially for canonical recall) is expected.
|
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for describing the origin of the Work and
|
| 141 |
+
reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Support. While redistributing the Work or
|
| 166 |
+
Derivative Works thereof, You may choose to offer, and charge a
|
| 167 |
+
fee for, acceptance of support, warranty, indemnity, or other
|
| 168 |
+
liability obligations and/or rights consistent with this License.
|
| 169 |
+
However, in accepting such obligations, You may act only on Your
|
| 170 |
+
own behalf and on Your sole responsibility, not on behalf of any
|
| 171 |
+
other Contributor, and only if You agree to indemnify, defend,
|
| 172 |
+
and hold each Contributor harmless for any liability incurred by,
|
| 173 |
+
or claims asserted against, such Contributor by reason of your
|
| 174 |
+
accepting any such warranty or support.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also include a copyright
|
| 185 |
+
notice and an attribution notice with copyright information.
|
| 186 |
+
|
| 187 |
+
Copyright 2026 Toran Billups
|
| 188 |
+
|
| 189 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 190 |
+
you may not use this file except in compliance with the License.
|
| 191 |
+
You may obtain a copy of the License at
|
| 192 |
+
|
| 193 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 194 |
+
|
| 195 |
+
Unless required by applicable law or agreed to in writing, software
|
| 196 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 197 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 198 |
+
implied. See the License for the specific language governing permissions
|
| 199 |
+
and limitations under the License.
|
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Model Card: TheoBERT Base
|
| 2 |
+
|
| 3 |
+
## Model Details
|
| 4 |
+
|
| 5 |
+
- **Model name:** TheoBERT Base (`theo-bert-base`)
|
| 6 |
+
- **Model type:** Bidirectional encoder with masked language modeling head
|
| 7 |
+
- **Architecture:** 12-layer Transformer with RoPE, ReLU² MLP, gated value embeddings, residual interpolation
|
| 8 |
+
- **Parameters:** 273,051,864 (≈273M)
|
| 9 |
+
- **Vocabulary:** 30,522 tokens (inherited from `google-bert/bert-base-uncased` tokenizer)
|
| 10 |
+
- **Context length:** 256 tokens (rotary cache supports up to 2,560)
|
| 11 |
+
- **License:** Apache 2.0
|
| 12 |
+
- **Contact:** [Toran Billups](https://huggingface.co/toranb)
|
| 13 |
+
|
| 14 |
+
## Uses
|
| 15 |
+
|
| 16 |
+
### Direct Use
|
| 17 |
+
|
| 18 |
+
TheoBERT Base is a masked language model for biblical and theological text. It is designed for:
|
| 19 |
+
|
| 20 |
+
- **Fill-mask inference** — predicting masked tokens in biblical-domain sentences
|
| 21 |
+
- **Token-level scoring** — computing log-probabilities of candidate tokens in context
|
| 22 |
+
- **Encoder hidden states** — extracting contextual embeddings for downstream task heads
|
| 23 |
+
|
| 24 |
+
### Downstream Use
|
| 25 |
+
|
| 26 |
+
This model is suitable as a base for fine-tuning on domain-specific tasks such as:
|
| 27 |
+
|
| 28 |
+
- Named entity recognition (biblical figures, places, events)
|
| 29 |
+
- Text classification (theological topic, genre, author attribution)
|
| 30 |
+
- Sequence labeling (part-of-speech tagging for biblical languages, when paired with appropriate tokenizers)
|
| 31 |
+
- Continued pretraining or domain adaptation
|
| 32 |
+
|
| 33 |
+
### Out-of-Scope Use
|
| 34 |
+
|
| 35 |
+
- **Text generation** — This is an encoder, not a decoder. It cannot autoregressively generate text.
|
| 36 |
+
- **General-domain NLP** — Trained exclusively on biblical and theological text. Performance on news, social media, or scientific text is untested and likely degraded.
|
| 37 |
+
- **Languages other than English** — Training data is English-language biblical material.
|
| 38 |
+
|
| 39 |
+
## Bias, Risks, and Limitations
|
| 40 |
+
|
| 41 |
+
### Domain Specialization
|
| 42 |
+
|
| 43 |
+
The model is trained on a narrow domain (biblical and theological English text). Its representations are shaped by the theological traditions present in the training corpus. Users should be aware that:
|
| 44 |
+
|
| 45 |
+
- The model's predictions reflect the distributional patterns of the training data, not ground-truth theological positions.
|
| 46 |
+
- Different theological traditions use the same terms with different meanings. The model may favor the usage patterns most prevalent in its training data.
|
| 47 |
+
- Tokenization is inherited from `bert-base-uncased`, which was trained on general English. Multi-piece subword splits on theological terms (e.g., "propitiation" → "prop", "##iti", "##ation") may degrade performance on rare vocabulary.
|
| 48 |
+
|
| 49 |
+
### Training Data
|
| 50 |
+
|
| 51 |
+
The training data is private and consists of:
|
| 52 |
+
|
| 53 |
+
- 270,000 sequences from bible text, Christian books, biblical commentaries, and synthetic data
|
| 54 |
+
- Corpus composition is not publicly auditable, which limits external bias analysis
|
| 55 |
+
|
| 56 |
+
### Known Failure Modes
|
| 57 |
+
|
| 58 |
+
- **Old Testament proper-noun recall** — Names like Jeremiah, Jonah, Job, and Nebuchadnezzar are weakly recalled
|
| 59 |
+
- **Multi-piece subword reconstruction** — Long words spanning many wordpieces (e.g., "sabachthani", "iniquity") have higher error rates
|
| 60 |
+
- **Canonical knowledge** — Lowest per-category performance (88.4%) relative to doctrinal categories
|
| 61 |
+
|
| 62 |
+
### Environmental Impact
|
| 63 |
+
|
| 64 |
+
Training was conducted over 49 epochs total (24 + 25) on a single GPU. Carbon footprint was not directly measured but is modest relative to large-scale LLM training. The released fp16 weights (546 MB) reduce download bandwidth and inference memory.
|
| 65 |
+
|
| 66 |
+
## Training Details
|
| 67 |
+
|
| 68 |
+
### Training Data
|
| 69 |
+
|
| 70 |
+
Private corpus of 270,000 sequences drawn from:
|
| 71 |
+
|
| 72 |
+
- Bible text (multiple English translations)
|
| 73 |
+
- Christian theological books and commentaries
|
| 74 |
+
- Synthetic data generated for domain coverage
|
| 75 |
+
|
| 76 |
+
No public dataset identifiers are available.
|
| 77 |
+
|
| 78 |
+
### Training Procedure
|
| 79 |
+
|
| 80 |
+
**Stage 1 — MLM Pretraining (24 epochs)**
|
| 81 |
+
|
| 82 |
+
- Objective: Masked language modeling
|
| 83 |
+
- Sequence length: 256 tokens
|
| 84 |
+
- Single-piece token masking
|
| 85 |
+
- Final train loss: 1.0679
|
| 86 |
+
- Final train accuracy: 76.42%
|
| 87 |
+
|
| 88 |
+
**Stage 2 — Continued Pretraining with Whole-Word Masking (25 epochs)**
|
| 89 |
+
|
| 90 |
+
- Objective: Whole-word masked language modeling at 18% mask rate
|
| 91 |
+
- Initialized from Stage 1 checkpoint
|
| 92 |
+
- Final train loss: 0.8958
|
| 93 |
+
- Final train accuracy: 79.66%
|
| 94 |
+
|
| 95 |
+
### Optimizer
|
| 96 |
+
|
| 97 |
+
Custom dual-optimizer setup combining AdamW (for embeddings, MLM head, residual lambdas, value gate parameters) and Muon (momentum-orthogonalized Newton-Schulz optimizer) for linear layer weight matrices.
|
| 98 |
+
|
| 99 |
+
### Hyperparameters
|
| 100 |
+
|
| 101 |
+
- AdamW betas: (0.8, 0.95)
|
| 102 |
+
- Muon momentum: 0.95
|
| 103 |
+
- Embedding learning rate: 0.3
|
| 104 |
+
- Matrix learning rate: 0.02
|
| 105 |
+
- Weight decay: 0.0
|
| 106 |
+
- Precision: fp32 training, fp16 release
|
| 107 |
+
|
| 108 |
+
## Evaluation
|
| 109 |
+
|
| 110 |
+
Evaluated on a 546-case domain-specific MLM benchmark. See [`EVAL.md`](EVAL.md) for full methodology, test case examples, and per-category breakdown.
|
| 111 |
+
|
| 112 |
+
| Metric | Value |
|
| 113 |
+
|---|---|
|
| 114 |
+
| Overall pass rate | 94.7% (517 / 546) |
|
| 115 |
+
| Difficulty-weighted | 94.6% |
|
| 116 |
+
| Easy | 94.9% |
|
| 117 |
+
| Medium | 94.9% |
|
| 118 |
+
| Hard | 94.2% |
|
| 119 |
+
|
| 120 |
+
## Technical Specifications
|
| 121 |
+
|
| 122 |
+
| Property | Value |
|
| 123 |
+
|---|---|
|
| 124 |
+
| Framework | PyTorch 2.4+ / Transformers 5.2.0 |
|
| 125 |
+
| Format | safetensors (fp16) |
|
| 126 |
+
| Model size (disk) | 546 MB |
|
| 127 |
+
| Model size (fp32 memory) | ~1.0 GB |
|
| 128 |
+
| Tokenizer | `google-bert/bert-base-uncased` (bundled, see Attribution) |
|
| 129 |
+
| Custom code required | Yes (`trust_remote_code=True`) |
|
| 130 |
+
|
| 131 |
+
### Architecture Detail
|
| 132 |
+
|
| 133 |
+
- 12 transformer blocks, hidden size 768, 8 attention heads (head dim 96)
|
| 134 |
+
- RoPE (Rotary Position Embedding) applied to Q and K projections
|
| 135 |
+
- RMS normalization on Q and K (no learnable gain)
|
| 136 |
+
- ReLU-squared MLP activation (`relu(x)²`)
|
| 137 |
+
- Gated value embeddings on even-indexed layers (0, 2, 4, 6, 8, 10)
|
| 138 |
+
- Learned residual interpolation: `λ_resid · block_output + λ_x0 · initial_embedding`
|
| 139 |
+
- MLM head: `Linear → GELU → RMSNorm → Linear`
|
| 140 |
+
|
| 141 |
+
## Attribution
|
| 142 |
+
|
| 143 |
+
This repository redistributes the fast-tokenizer files (`tokenizer.json`, `tokenizer_config.json`) from [`google-bert/bert-base-uncased`](https://huggingface.co/google-bert/bert-base-uncased) unmodified. Those files are released by Google under the Apache License 2.0 and are bundled here so `AutoTokenizer.from_pretrained` and the Hugging Face Hub `fill-mask` widget work without an extra download step. TheoBERT Base is itself released under Apache 2.0; see `LICENSE`.
|
| 144 |
+
|
| 145 |
+
## Citation
|
| 146 |
+
|
| 147 |
+
```bibtex
|
| 148 |
+
@misc{theobertbase2026,
|
| 149 |
+
author = {Toran Billups},
|
| 150 |
+
title = {TheoBERT Base: A Domain-Specialized Masked Language Model for Biblical and Theological Text},
|
| 151 |
+
year = 2026,
|
| 152 |
+
publisher = {Hugging Face},
|
| 153 |
+
howpublished = {https://huggingface.co/toranb/theo-bert-base}
|
| 154 |
+
}
|
| 155 |
+
```
|
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
language:
|
| 3 |
+
- en
|
| 4 |
+
license: apache-2.0
|
| 5 |
+
library_name: transformers
|
| 6 |
+
pipeline_tag: fill-mask
|
| 7 |
+
tags:
|
| 8 |
+
- masked-language-modeling
|
| 9 |
+
- bible
|
| 10 |
+
- theology
|
| 11 |
+
- christianity
|
| 12 |
+
- trust-remote-code
|
| 13 |
+
model-index:
|
| 14 |
+
- name: theo-bert-base
|
| 15 |
+
results:
|
| 16 |
+
- task:
|
| 17 |
+
type: fill-mask
|
| 18 |
+
name: Masked Language Modeling
|
| 19 |
+
metrics:
|
| 20 |
+
- type: accuracy
|
| 21 |
+
value: 0.947
|
| 22 |
+
name: Pass rate on 546-case theological MLM eval
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
# TheoBERT Base
|
| 26 |
+
|
| 27 |
+
`theo-bert-base` is a domain-specialized **masked language model** for biblical and theological text. It is a custom bidirectional encoder pretrained from scratch on bible and closely related doctrinal material, exported in a Hugging Face–compatible format.
|
| 28 |
+
|
| 29 |
+
This repository ships the MLM-shaped artifact: an encoder body paired with a working MLM head. It is the right checkpoint if you want fill-mask, token-level scoring, or a strong base for further domain-specific fine-tuning where token-level prediction matters.
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
## What This Model Is For
|
| 33 |
+
|
| 34 |
+
Recommended use cases:
|
| 35 |
+
|
| 36 |
+
- Masked token prediction and token-level scoring in biblical-domain text
|
| 37 |
+
- Initialization for continued domain adaptation or supervised downstream fine-tuning
|
| 38 |
+
- Encoder hidden states for downstream task heads (classification, NER, etc.)
|
| 39 |
+
|
| 40 |
+
## Training Pipeline
|
| 41 |
+
|
| 42 |
+
This release is the output of a two-stage pretraining pipeline.
|
| 43 |
+
|
| 44 |
+
**Stage 1 — MLM pretraining from scratch (`encoder`)**
|
| 45 |
+
- 24 epochs of masked language modeling at 256-token context
|
| 46 |
+
- 270,000 sequences from bible text, Christian books, biblical commentaries and synthetic data
|
| 47 |
+
- Final train loss `1.0679`, train accuracy `76.42%`
|
| 48 |
+
|
| 49 |
+
**Stage 2 — Whole-word-masking continued pretraining (`mlmcontinued`)** — *this release*
|
| 50 |
+
- 25 additional epochs of continued pretraining on top of Stage 1
|
| 51 |
+
- 18% whole-word-masking rate (whole-word, not single-piece)
|
| 52 |
+
- Final train loss `0.8958`, train accuracy `79.66%`
|
| 53 |
+
|
| 54 |
+
The MLM head was trained jointly with the body throughout both stages and is preserved in this release.
|
| 55 |
+
|
| 56 |
+
## Evaluation
|
| 57 |
+
|
| 58 |
+
Evaluated on a 546-case domain-specific MLM benchmark covering bibliology, christology, ecclesiology, eschatology, hamartiology, pneumatology, soteriology, theology proper, and canonical knowledge. Full methodology and test case schema in [`EVAL.md`](EVAL.md).
|
| 59 |
+
|
| 60 |
+
| Metric | Value |
|
| 61 |
+
|---|---|
|
| 62 |
+
| Overall pass rate | **94.7%** (517 / 546) |
|
| 63 |
+
| Difficulty-weighted | 94.6% |
|
| 64 |
+
| Easy | 94.9% |
|
| 65 |
+
| Medium | 94.9% |
|
| 66 |
+
| Hard | 94.2% |
|
| 67 |
+
|
| 68 |
+
Per-category highlights:
|
| 69 |
+
|
| 70 |
+
| Category | Pass rate |
|
| 71 |
+
|---|---|
|
| 72 |
+
| Pneumatology | 100% |
|
| 73 |
+
| Soteriology | 98.2% |
|
| 74 |
+
| Ecclesiology | 97.5% |
|
| 75 |
+
| Hamartiology | 97.1% |
|
| 76 |
+
| Christology | 96.4% |
|
| 77 |
+
| Eschatology | 94.4% |
|
| 78 |
+
| Theology proper | 91.3% |
|
| 79 |
+
| Canonical knowledge | 88.4% |
|
| 80 |
+
|
| 81 |
+
Residual failures cluster around Old Testament proper-noun recall (Jeremiah, Jonah, Job, Nebuchadnezzar) and multi-piece subword reconstruction (`sabachthani`, `iniquity`, `Nebuchadnezzar`). The benchmark suggests strong domain-specific MLM behavior on this suite; broader generalization beyond the eval distribution has not been independently verified.
|
| 82 |
+
|
| 83 |
+
## Tokenizer
|
| 84 |
+
|
| 85 |
+
`theo-bert-base` uses the **`google-bert/bert-base-uncased` tokenizer**. The fast-tokenizer files (`tokenizer.json`, `tokenizer_config.json`) are bundled in this repo so `AutoTokenizer.from_pretrained("toranb/theo-bert-base")` and the Hub `fill-mask` widget work out of the box.
|
| 86 |
+
|
| 87 |
+
Tokenizer files are redistributed unmodified from [`google-bert/bert-base-uncased`](https://huggingface.co/google-bert/bert-base-uncased), released by Google under the Apache License 2.0.
|
| 88 |
+
|
| 89 |
+
## Architecture
|
| 90 |
+
|
| 91 |
+
- 12 transformer blocks
|
| 92 |
+
- Hidden size 768
|
| 93 |
+
- 8 attention heads (head dim 96)
|
| 94 |
+
- Training sequence length 256 (rotary cache supports up to 2,560 tokens)
|
| 95 |
+
- Vocabulary size 30,522 via `bert-base-uncased`
|
| 96 |
+
- RoPE positional encoding applied to query and key projections
|
| 97 |
+
- RMS normalization on Q and K (no learnable gain)
|
| 98 |
+
- ReLU-squared MLP activation
|
| 99 |
+
- Gated value embeddings on even-indexed layers
|
| 100 |
+
- Learned residual interpolation between each block output and the initial token-embedding state
|
| 101 |
+
- MLM head: `Linear → GELU → RMSNorm → Linear`
|
| 102 |
+
|
| 103 |
+
Parameter count: **273,051,864** (≈273M).
|
| 104 |
+
|
| 105 |
+
## Quick Start — Fill-Mask
|
| 106 |
+
|
| 107 |
+
```python
|
| 108 |
+
from transformers import AutoModelForMaskedLM, AutoTokenizer
|
| 109 |
+
|
| 110 |
+
repo = "toranb/theo-bert-base"
|
| 111 |
+
|
| 112 |
+
tokenizer = AutoTokenizer.from_pretrained(repo)
|
| 113 |
+
model = AutoModelForMaskedLM.from_pretrained(repo, trust_remote_code=True)
|
| 114 |
+
model.eval()
|
| 115 |
+
|
| 116 |
+
inputs = tokenizer(
|
| 117 |
+
"For God so loved the [MASK] that he gave his only Son.",
|
| 118 |
+
return_tensors="pt",
|
| 119 |
+
)
|
| 120 |
+
outputs = model(**inputs)
|
| 121 |
+
mask_index = (inputs["input_ids"] == tokenizer.mask_token_id).nonzero(as_tuple=False)[0, 1]
|
| 122 |
+
top_ids = outputs.logits[0, mask_index].topk(5).indices.tolist()
|
| 123 |
+
print(tokenizer.convert_ids_to_tokens(top_ids))
|
| 124 |
+
# → ['world', 'universe', 'son', 'church', 'earth']
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
## Quick Start — Encoder Hidden States
|
| 128 |
+
|
| 129 |
+
```python
|
| 130 |
+
from transformers import AutoModel, AutoTokenizer
|
| 131 |
+
|
| 132 |
+
repo = "toranb/theo-bert-base"
|
| 133 |
+
|
| 134 |
+
tokenizer = AutoTokenizer.from_pretrained(repo)
|
| 135 |
+
model = AutoModel.from_pretrained(repo, trust_remote_code=True)
|
| 136 |
+
model.eval()
|
| 137 |
+
|
| 138 |
+
batch = tokenizer(
|
| 139 |
+
["faith working through love", "the kingdom of God"],
|
| 140 |
+
padding=True, truncation=True, max_length=256, return_tensors="pt",
|
| 141 |
+
)
|
| 142 |
+
hidden = model(**batch).last_hidden_state # [B, T, 768]
|
| 143 |
+
mask = batch["attention_mask"].unsqueeze(-1).float()
|
| 144 |
+
pooled = (hidden * mask).sum(1) / mask.sum(1).clamp(min=1)
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
## Repository Contents
|
| 149 |
+
|
| 150 |
+
| File | Purpose |
|
| 151 |
+
|---|---|
|
| 152 |
+
| `configuration_theo_bert_base.py` | Hugging Face config class |
|
| 153 |
+
| `modeling_theo_bert_base.py` | `AutoModel` and `AutoModelForMaskedLM` implementations |
|
| 154 |
+
| `muon.py` | Local Muon optimizer (retained for self-contained fine-tuning) |
|
| 155 |
+
| `config.json` | Generated from the source checkpoint configuration |
|
| 156 |
+
| `model.safetensors` | Released fp16 weights |
|
| 157 |
+
| `checkpoint_metadata.json` | Source checkpoint and per-stage training metadata |
|
| 158 |
+
| `LICENSE` | Apache-2.0 |
|
| 159 |
+
|
| 160 |
+
### Scripts
|
| 161 |
+
|
| 162 |
+
| Script | Purpose |
|
| 163 |
+
|---|---|
|
| 164 |
+
| `scripts/mlm_eval_safetensors.py` | Loads `model.safetensors` + `eval.json` and runs the full 546-case MLM evaluation suite |
|
| 165 |
+
|
| 166 |
+
## Limitations
|
| 167 |
+
|
| 168 |
+
- Specialized for biblical and theological language; may underperform on broad general-domain NLP tasks.
|
| 169 |
+
- Tokenizer inherited from `bert-base-uncased`, so wordpiece behavior follows general English conventions rather than a theology-specific tokenizer.
|
| 170 |
+
- Trained at 256-token context. Longer inputs work within the rotary cache (up to 2,560 tokens), but extended-context behavior is not a primary target of this release.
|
| 171 |
+
- Training data is private, so external auditing of corpus composition is limited. The canonical-knowledge eval cases overlap by design with biblical text that appears in the training corpus, so the 88.4% recall on that category should be read as in-distribution recall, not held-out generalization.
|
| 172 |
+
- Encoder MLM — not an autoregressive decoder.
|
| 173 |
+
|
| 174 |
+
## Release Details
|
| 175 |
+
|
| 176 |
+
- Exported from `mlmcontinued/latest.pt` (Stage 2 final epoch, training accuracy 79.66%)
|
| 177 |
+
- Source checkpoint loss `0.8958`
|
| 178 |
+
- Released weights in fp16 for bandwidth efficiency (546 MB)
|
| 179 |
+
- Release format uses `safetensors`
|
| 180 |
+
- Loading requires `trust_remote_code=True` to register the custom architecture
|
| 181 |
+
- `config.json` declares `torch_dtype: float32` so default loads upcast on read. Disk weights stay fp16 (small download); CPU inference is numerically safe by default. For GPU fp16 inference, pass `dtype=torch.float16` to `from_pretrained`.
|
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .configuration_theo_bert_base import TheoBertBaseConfig
|
| 2 |
+
from .modeling_theo_bert_base import (
|
| 3 |
+
TheoBertBaseForMaskedLM,
|
| 4 |
+
TheoBertBaseModel,
|
| 5 |
+
)
|
| 6 |
+
|
| 7 |
+
__all__ = [
|
| 8 |
+
"TheoBertBaseConfig",
|
| 9 |
+
"TheoBertBaseModel",
|
| 10 |
+
"TheoBertBaseForMaskedLM",
|
| 11 |
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"release_stage": "mlmcontinued (stage 2, epoch 25)",
|
| 3 |
+
"reported_loss": 0.8958267427277438,
|
| 4 |
+
"parameter_count": 273051864,
|
| 5 |
+
"fp16_export": true,
|
| 6 |
+
"tokenizer": "google-bert/bert-base-uncased",
|
| 7 |
+
"pretraining_stages": [
|
| 8 |
+
{
|
| 9 |
+
"stage": 1,
|
| 10 |
+
"name": "encoder",
|
| 11 |
+
"objective": "token-level masked language modeling at 20% mask rate 80/10/10 split",
|
| 12 |
+
"epochs": 24,
|
| 13 |
+
"seq_len": 256,
|
| 14 |
+
"final_train_loss": 1.0678829201169648,
|
| 15 |
+
"final_train_accuracy": 76.41802635495705
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"stage": 2,
|
| 19 |
+
"name": "mlmcontinued",
|
| 20 |
+
"objective": "whole-word-masking continued pretraining at 18% mask rate",
|
| 21 |
+
"epochs": 25,
|
| 22 |
+
"seq_len": 256,
|
| 23 |
+
"final_train_loss": 0.8958267427277438,
|
| 24 |
+
"final_train_accuracy": 79.66191907459469
|
| 25 |
+
}
|
| 26 |
+
],
|
| 27 |
+
"mlm_eval_overall_pass_rate": 0.947,
|
| 28 |
+
"mlm_eval_passed_cases": 517,
|
| 29 |
+
"mlm_eval_total_cases": 546
|
| 30 |
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"TheoBertBaseForMaskedLM"
|
| 4 |
+
],
|
| 5 |
+
"model_type": "theo_bert_base",
|
| 6 |
+
"auto_map": {
|
| 7 |
+
"AutoConfig": "configuration_theo_bert_base.TheoBertBaseConfig",
|
| 8 |
+
"AutoModel": "modeling_theo_bert_base.TheoBertBaseModel",
|
| 9 |
+
"AutoModelForMaskedLM": "modeling_theo_bert_base.TheoBertBaseForMaskedLM"
|
| 10 |
+
},
|
| 11 |
+
"tokenizer_name": "google-bert/bert-base-uncased",
|
| 12 |
+
"torch_dtype": "float32",
|
| 13 |
+
"vocab_size": 30522,
|
| 14 |
+
"n_layer": 12,
|
| 15 |
+
"n_head": 8,
|
| 16 |
+
"n_embd": 768,
|
| 17 |
+
"seq_len": 256,
|
| 18 |
+
"rope_base": 10000,
|
| 19 |
+
"rope_cache_factor": 10,
|
| 20 |
+
"hidden_size": 768,
|
| 21 |
+
"num_hidden_layers": 12,
|
| 22 |
+
"num_attention_heads": 8,
|
| 23 |
+
"max_position_embeddings": 256,
|
| 24 |
+
"pad_token_id": 0,
|
| 25 |
+
"unk_token_id": 100,
|
| 26 |
+
"cls_token_id": 101,
|
| 27 |
+
"sep_token_id": 102,
|
| 28 |
+
"mask_token_id": 103,
|
| 29 |
+
"transformers_version": "5.2.0"
|
| 30 |
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from transformers import PretrainedConfig
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class TheoBertBaseConfig(PretrainedConfig):
|
| 5 |
+
model_type = "theo_bert_base"
|
| 6 |
+
|
| 7 |
+
def __init__(
|
| 8 |
+
self,
|
| 9 |
+
vocab_size=30522,
|
| 10 |
+
n_layer=12,
|
| 11 |
+
n_head=8,
|
| 12 |
+
n_embd=768,
|
| 13 |
+
seq_len=256,
|
| 14 |
+
rope_base=10000,
|
| 15 |
+
rope_cache_factor=10,
|
| 16 |
+
pad_token_id=0,
|
| 17 |
+
unk_token_id=100,
|
| 18 |
+
cls_token_id=101,
|
| 19 |
+
sep_token_id=102,
|
| 20 |
+
mask_token_id=103,
|
| 21 |
+
tokenizer_name="google-bert/bert-base-uncased",
|
| 22 |
+
**kwargs,
|
| 23 |
+
):
|
| 24 |
+
self.vocab_size = vocab_size
|
| 25 |
+
self.n_layer = n_layer
|
| 26 |
+
self.n_head = n_head
|
| 27 |
+
self.n_embd = n_embd
|
| 28 |
+
self.seq_len = seq_len
|
| 29 |
+
self.rope_base = rope_base
|
| 30 |
+
self.rope_cache_factor = rope_cache_factor
|
| 31 |
+
self.tokenizer_name = tokenizer_name
|
| 32 |
+
|
| 33 |
+
# Compatibility aliases expected by downstream tooling.
|
| 34 |
+
self.hidden_size = n_embd
|
| 35 |
+
self.num_hidden_layers = n_layer
|
| 36 |
+
self.num_attention_heads = n_head
|
| 37 |
+
self.max_position_embeddings = seq_len
|
| 38 |
+
self.rope_cache_seq_len = seq_len * rope_cache_factor
|
| 39 |
+
|
| 40 |
+
super().__init__(
|
| 41 |
+
pad_token_id=pad_token_id,
|
| 42 |
+
unk_token_id=unk_token_id,
|
| 43 |
+
cls_token_id=cls_token_id,
|
| 44 |
+
sep_token_id=sep_token_id,
|
| 45 |
+
mask_token_id=mask_token_id,
|
| 46 |
+
**kwargs,
|
| 47 |
+
)
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fb9425642090f3523127bd8808f9a84c053c8c25f101775d0d759a460aad0c3a
|
| 3 |
+
size 546114384
|
|
@@ -0,0 +1,317 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
from typing import Optional
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn as nn
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
from transformers import PreTrainedModel
|
| 8 |
+
from transformers.modeling_outputs import BaseModelOutput, MaskedLMOutput
|
| 9 |
+
|
| 10 |
+
from .configuration_theo_bert_base import TheoBertBaseConfig
|
| 11 |
+
from .muon import Muon
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def norm(x: torch.Tensor) -> torch.Tensor:
|
| 15 |
+
return F.rms_norm(x, (x.size(-1),))
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def apply_rotary_emb(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
|
| 19 |
+
d = x.shape[-1] // 2
|
| 20 |
+
x1, x2 = x[..., :d], x[..., d:]
|
| 21 |
+
return torch.cat([x1 * cos + x2 * sin, x1 * (-sin) + x2 * cos], dim=-1).to(x.dtype)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class RMSNorm(nn.Module):
|
| 25 |
+
def __init__(self, dim: int):
|
| 26 |
+
super().__init__()
|
| 27 |
+
self.dim = dim
|
| 28 |
+
|
| 29 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 30 |
+
return F.rms_norm(x, (self.dim,))
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class SelfAttention(nn.Module):
|
| 34 |
+
def __init__(self, config: TheoBertBaseConfig):
|
| 35 |
+
super().__init__()
|
| 36 |
+
self.n_head = config.n_head
|
| 37 |
+
self.head_dim = config.n_embd // config.n_head
|
| 38 |
+
D = config.n_embd
|
| 39 |
+
self.c_q = nn.Linear(D, D, bias=False)
|
| 40 |
+
self.c_k = nn.Linear(D, D, bias=False)
|
| 41 |
+
self.c_v = nn.Linear(D, D, bias=False)
|
| 42 |
+
self.c_proj = nn.Linear(D, D, bias=False)
|
| 43 |
+
|
| 44 |
+
def forward(self, x, cos_sin, ve=None, attention_mask=None):
|
| 45 |
+
B, T, D = x.shape
|
| 46 |
+
H, Dh = self.n_head, self.head_dim
|
| 47 |
+
|
| 48 |
+
q = self.c_q(x).view(B, T, H, Dh)
|
| 49 |
+
k = self.c_k(x).view(B, T, H, Dh)
|
| 50 |
+
|
| 51 |
+
v_proj = self.c_v(x)
|
| 52 |
+
if ve is not None:
|
| 53 |
+
v_proj = v_proj + ve
|
| 54 |
+
v = v_proj.view(B, T, H, Dh)
|
| 55 |
+
|
| 56 |
+
cos, sin = cos_sin
|
| 57 |
+
q = apply_rotary_emb(q, cos, sin)
|
| 58 |
+
k = apply_rotary_emb(k, cos, sin)
|
| 59 |
+
q = norm(q)
|
| 60 |
+
k = norm(k)
|
| 61 |
+
|
| 62 |
+
q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
|
| 63 |
+
|
| 64 |
+
attn_mask = None
|
| 65 |
+
if attention_mask is not None:
|
| 66 |
+
attn_mask = attention_mask[:, None, None, :].bool()
|
| 67 |
+
y = F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask, is_causal=False)
|
| 68 |
+
return self.c_proj(y.transpose(1, 2).contiguous().view(B, T, D))
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
class MLP(nn.Module):
|
| 72 |
+
def __init__(self, config: TheoBertBaseConfig):
|
| 73 |
+
super().__init__()
|
| 74 |
+
D = config.n_embd
|
| 75 |
+
self.c_fc = nn.Linear(D, 4 * D, bias=False)
|
| 76 |
+
self.c_proj = nn.Linear(4 * D, D, bias=False)
|
| 77 |
+
|
| 78 |
+
def forward(self, x):
|
| 79 |
+
return self.c_proj(F.relu(self.c_fc(x)).square())
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class Block(nn.Module):
|
| 83 |
+
def __init__(self, config: TheoBertBaseConfig, layer_idx: int):
|
| 84 |
+
super().__init__()
|
| 85 |
+
self.attn = SelfAttention(config)
|
| 86 |
+
self.mlp = MLP(config)
|
| 87 |
+
self.resid_lambda = nn.Parameter(torch.ones(1))
|
| 88 |
+
self.x0_lambda = nn.Parameter(torch.full((1,), 0.1))
|
| 89 |
+
if layer_idx % 2 == 0:
|
| 90 |
+
self.value_embed = nn.Embedding(config.vocab_size, config.n_embd)
|
| 91 |
+
self.ve_gate = nn.Linear(32, 1, bias=False)
|
| 92 |
+
|
| 93 |
+
def forward(self, x, cos_sin, x0, token_ids, attention_mask=None):
|
| 94 |
+
normed = norm(x)
|
| 95 |
+
ve = None
|
| 96 |
+
if hasattr(self, "value_embed"):
|
| 97 |
+
raw_ve = self.value_embed(token_ids)
|
| 98 |
+
gate = 2 * torch.sigmoid(self.ve_gate(normed[..., :32]))
|
| 99 |
+
ve = gate * raw_ve
|
| 100 |
+
x = x + self.attn(normed, cos_sin, ve=ve, attention_mask=attention_mask)
|
| 101 |
+
x = x + self.mlp(norm(x))
|
| 102 |
+
return self.resid_lambda * x + self.x0_lambda * x0
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
class TheoBertBasePreTrainedModel(PreTrainedModel):
|
| 106 |
+
config_class = TheoBertBaseConfig
|
| 107 |
+
base_model_prefix = "theo_bert_base"
|
| 108 |
+
supports_gradient_checkpointing = True
|
| 109 |
+
|
| 110 |
+
def _init_weights(self, module):
|
| 111 |
+
if isinstance(module, nn.Linear):
|
| 112 |
+
fan_in = module.weight.size(1)
|
| 113 |
+
fan_out = module.weight.size(0)
|
| 114 |
+
std = (1.0 / math.sqrt(fan_in)) * min(1.0, math.sqrt(fan_out / fan_in))
|
| 115 |
+
nn.init.normal_(module.weight, std=std)
|
| 116 |
+
elif isinstance(module, nn.Embedding):
|
| 117 |
+
nn.init.normal_(module.weight, std=1.0)
|
| 118 |
+
|
| 119 |
+
def _set_gradient_checkpointing(self, module, value=False):
|
| 120 |
+
if isinstance(module, TheoBertBaseModel):
|
| 121 |
+
module.use_gradient_checkpointing = value
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class TheoBertBaseModel(TheoBertBasePreTrainedModel):
|
| 125 |
+
def __init__(self, config: TheoBertBaseConfig):
|
| 126 |
+
super().__init__(config)
|
| 127 |
+
self.use_gradient_checkpointing = False
|
| 128 |
+
self.wte = nn.Embedding(config.vocab_size, config.n_embd)
|
| 129 |
+
self.blocks = nn.ModuleList([Block(config, i) for i in range(config.n_layer)])
|
| 130 |
+
# Retained on the base model so the exported checkpoint can be consumed by
|
| 131 |
+
# AutoModel and AutoModelForMaskedLM from the same repository without key drift.
|
| 132 |
+
self.mlm_head = nn.Sequential(
|
| 133 |
+
nn.Linear(config.n_embd, config.n_embd, bias=False),
|
| 134 |
+
nn.GELU(),
|
| 135 |
+
RMSNorm(config.n_embd),
|
| 136 |
+
nn.Linear(config.n_embd, config.vocab_size, bias=False),
|
| 137 |
+
)
|
| 138 |
+
self._refresh_rope_cache()
|
| 139 |
+
self.post_init()
|
| 140 |
+
self._post_init_architecture()
|
| 141 |
+
|
| 142 |
+
def _post_init_architecture(self):
|
| 143 |
+
nn.init.zeros_(self.mlm_head[-1].weight)
|
| 144 |
+
for block in self.blocks:
|
| 145 |
+
nn.init.zeros_(block.attn.c_proj.weight)
|
| 146 |
+
nn.init.zeros_(block.mlp.c_proj.weight)
|
| 147 |
+
nn.init.ones_(block.resid_lambda)
|
| 148 |
+
block.x0_lambda.data.fill_(0.1)
|
| 149 |
+
|
| 150 |
+
def _make_rotary(self, seq_len, head_dim, base=10000, device=None):
|
| 151 |
+
if device is None:
|
| 152 |
+
device = self.wte.weight.device
|
| 153 |
+
inv_freq = 1.0 / (
|
| 154 |
+
base
|
| 155 |
+
** (torch.arange(0, head_dim, 2, dtype=torch.float32, device=device) / head_dim)
|
| 156 |
+
)
|
| 157 |
+
t = torch.arange(seq_len, dtype=torch.float32, device=device)
|
| 158 |
+
freqs = torch.outer(t, inv_freq)
|
| 159 |
+
cos = freqs.cos()[None, :, None, :]
|
| 160 |
+
sin = freqs.sin()[None, :, None, :]
|
| 161 |
+
return cos, sin
|
| 162 |
+
|
| 163 |
+
def _refresh_rope_cache(self):
|
| 164 |
+
head_dim = self.config.n_embd // self.config.n_head
|
| 165 |
+
cache_len = self.config.seq_len * self.config.rope_cache_factor
|
| 166 |
+
cos, sin = self._make_rotary(cache_len, head_dim, base=self.config.rope_base)
|
| 167 |
+
self.register_buffer("cos", cos, persistent=False)
|
| 168 |
+
self.register_buffer("sin", sin, persistent=False)
|
| 169 |
+
|
| 170 |
+
def get_input_embeddings(self):
|
| 171 |
+
return self.wte
|
| 172 |
+
|
| 173 |
+
def set_input_embeddings(self, value):
|
| 174 |
+
self.wte = value
|
| 175 |
+
|
| 176 |
+
def mean_pool(self, hidden, mask=None):
|
| 177 |
+
if mask is None:
|
| 178 |
+
return hidden.mean(dim=1)
|
| 179 |
+
m = mask.unsqueeze(-1).float()
|
| 180 |
+
return (hidden * m).sum(1) / m.sum(1).clamp(min=1)
|
| 181 |
+
|
| 182 |
+
def setup_optimizers(self, embedding_lr=0.3, matrix_lr=0.02):
|
| 183 |
+
model_dim = self.config.n_embd
|
| 184 |
+
mlm_head_lr = 0.004 * math.sqrt(768 / model_dim)
|
| 185 |
+
|
| 186 |
+
embed_params = list(self.wte.parameters())
|
| 187 |
+
ve_params, ve_gate_params, resid_params, x0_params, matrix_params = [], [], [], [], []
|
| 188 |
+
|
| 189 |
+
for block in self.blocks:
|
| 190 |
+
matrix_params += [
|
| 191 |
+
block.attn.c_q.weight,
|
| 192 |
+
block.attn.c_k.weight,
|
| 193 |
+
block.attn.c_v.weight,
|
| 194 |
+
block.attn.c_proj.weight,
|
| 195 |
+
block.mlp.c_fc.weight,
|
| 196 |
+
block.mlp.c_proj.weight,
|
| 197 |
+
]
|
| 198 |
+
resid_params.append(block.resid_lambda)
|
| 199 |
+
x0_params.append(block.x0_lambda)
|
| 200 |
+
if hasattr(block, "value_embed"):
|
| 201 |
+
ve_params += list(block.value_embed.parameters())
|
| 202 |
+
ve_gate_params += list(block.ve_gate.parameters())
|
| 203 |
+
|
| 204 |
+
adamw_groups = [
|
| 205 |
+
{"params": embed_params + ve_params, "lr": embedding_lr},
|
| 206 |
+
{"params": list(self.mlm_head.parameters()), "lr": mlm_head_lr},
|
| 207 |
+
{"params": resid_params, "lr": 0.005},
|
| 208 |
+
{"params": x0_params, "lr": 0.5, "betas": (0.96, 0.95)},
|
| 209 |
+
{"params": ve_gate_params, "lr": 0.004},
|
| 210 |
+
]
|
| 211 |
+
adamw_groups = [g for g in adamw_groups if g["params"]]
|
| 212 |
+
|
| 213 |
+
adamw = torch.optim.AdamW(
|
| 214 |
+
adamw_groups, betas=(0.8, 0.95), eps=1e-10, weight_decay=0.0, fused=True
|
| 215 |
+
)
|
| 216 |
+
muon = Muon(matrix_params, lr=matrix_lr, momentum=0.95)
|
| 217 |
+
|
| 218 |
+
for opt in (adamw, muon):
|
| 219 |
+
for group in opt.param_groups:
|
| 220 |
+
group["initial_lr"] = group["lr"]
|
| 221 |
+
return adamw, muon
|
| 222 |
+
|
| 223 |
+
def forward(
|
| 224 |
+
self,
|
| 225 |
+
input_ids: torch.LongTensor,
|
| 226 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 227 |
+
output_hidden_states: Optional[bool] = None,
|
| 228 |
+
return_dict: Optional[bool] = None,
|
| 229 |
+
**kwargs,
|
| 230 |
+
):
|
| 231 |
+
del kwargs
|
| 232 |
+
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
| 233 |
+
output_hidden_states = (
|
| 234 |
+
output_hidden_states
|
| 235 |
+
if output_hidden_states is not None
|
| 236 |
+
else self.config.output_hidden_states
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
# Non-persistent rotary buffers are skipped by state_dict load; after
|
| 240 |
+
# from_pretrained's meta→to_empty path they hold uninitialized memory.
|
| 241 |
+
# Refresh once per instance, on the actual parameter device.
|
| 242 |
+
if not getattr(self, "_rope_initialized", False):
|
| 243 |
+
self._refresh_rope_cache()
|
| 244 |
+
self._rope_initialized = True
|
| 245 |
+
|
| 246 |
+
B, T = input_ids.shape
|
| 247 |
+
if T > self.cos.size(1):
|
| 248 |
+
raise ValueError(
|
| 249 |
+
f"Input sequence length {T} exceeds rotary cache length {self.cos.size(1)}."
|
| 250 |
+
)
|
| 251 |
+
cos_sin = self.cos[:, :T], self.sin[:, :T]
|
| 252 |
+
x = norm(self.wte(input_ids))
|
| 253 |
+
x0 = x
|
| 254 |
+
|
| 255 |
+
hidden_states = () if output_hidden_states else None
|
| 256 |
+
if output_hidden_states:
|
| 257 |
+
hidden_states = hidden_states + (x,)
|
| 258 |
+
|
| 259 |
+
if self.training and self.use_gradient_checkpointing:
|
| 260 |
+
from torch.utils.checkpoint import checkpoint
|
| 261 |
+
|
| 262 |
+
for block in self.blocks:
|
| 263 |
+
x = checkpoint(block, x, cos_sin, x0, input_ids, attention_mask, use_reentrant=False)
|
| 264 |
+
if output_hidden_states:
|
| 265 |
+
hidden_states = hidden_states + (x,)
|
| 266 |
+
else:
|
| 267 |
+
for block in self.blocks:
|
| 268 |
+
x = block(x, cos_sin, x0, input_ids, attention_mask=attention_mask)
|
| 269 |
+
if output_hidden_states:
|
| 270 |
+
hidden_states = hidden_states + (x,)
|
| 271 |
+
|
| 272 |
+
x = norm(x)
|
| 273 |
+
if not return_dict:
|
| 274 |
+
return (x, hidden_states)
|
| 275 |
+
return BaseModelOutput(last_hidden_state=x, hidden_states=hidden_states)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
class TheoBertBaseForMaskedLM(TheoBertBaseModel):
|
| 279 |
+
_keys_to_ignore_on_load_unexpected = [r"cls\..*"]
|
| 280 |
+
|
| 281 |
+
def get_output_embeddings(self):
|
| 282 |
+
return self.mlm_head[-1]
|
| 283 |
+
|
| 284 |
+
def set_output_embeddings(self, new_embeddings):
|
| 285 |
+
self.mlm_head[-1] = new_embeddings
|
| 286 |
+
|
| 287 |
+
def forward(
|
| 288 |
+
self,
|
| 289 |
+
input_ids: torch.LongTensor,
|
| 290 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 291 |
+
labels: Optional[torch.LongTensor] = None,
|
| 292 |
+
output_hidden_states: Optional[bool] = None,
|
| 293 |
+
return_dict: Optional[bool] = None,
|
| 294 |
+
**kwargs,
|
| 295 |
+
):
|
| 296 |
+
outputs = super().forward(
|
| 297 |
+
input_ids=input_ids,
|
| 298 |
+
attention_mask=attention_mask,
|
| 299 |
+
output_hidden_states=output_hidden_states,
|
| 300 |
+
return_dict=True,
|
| 301 |
+
**kwargs,
|
| 302 |
+
)
|
| 303 |
+
logits = self.mlm_head(outputs.last_hidden_state).float()
|
| 304 |
+
|
| 305 |
+
loss = None
|
| 306 |
+
if labels is not None:
|
| 307 |
+
loss = F.cross_entropy(
|
| 308 |
+
logits.view(-1, self.config.vocab_size),
|
| 309 |
+
labels.view(-1),
|
| 310 |
+
ignore_index=-100,
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
if return_dict is False:
|
| 314 |
+
result = (logits, outputs.hidden_states)
|
| 315 |
+
return ((loss,) + result) if loss is not None else result
|
| 316 |
+
|
| 317 |
+
return MaskedLMOutput(loss=loss, logits=logits, hidden_states=outputs.hidden_states)
|
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Muon optimizer -- MomentUm Orthogonalized by Newton-Schulz.
|
| 3 |
+
https://kellerjordan.github.io/posts/muon/
|
| 4 |
+
|
| 5 |
+
Included locally so this repository is self-contained for anyone who wants to
|
| 6 |
+
fine-tune or continue pretraining from the exported checkpoint.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from torch import Tensor
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@torch.compile
|
| 14 |
+
def zeropower_via_newtonschulz5(G: Tensor, steps: int) -> Tensor:
|
| 15 |
+
assert G.ndim >= 2
|
| 16 |
+
a, b, c = (3.4445, -4.7750, 2.0315)
|
| 17 |
+
X = G.bfloat16()
|
| 18 |
+
if G.size(-2) > G.size(-1):
|
| 19 |
+
X = X.mT
|
| 20 |
+
X = X / (X.norm(dim=(-2, -1), keepdim=True) + 1e-7)
|
| 21 |
+
for _ in range(steps):
|
| 22 |
+
A = X @ X.mT
|
| 23 |
+
B = b * A + c * A @ A
|
| 24 |
+
X = a * X + B @ X
|
| 25 |
+
if G.size(-2) > G.size(-1):
|
| 26 |
+
X = X.mT
|
| 27 |
+
return X
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class Muon(torch.optim.Optimizer):
|
| 31 |
+
def __init__(self, params, lr=0.02, momentum=0.95, nesterov=True, ns_steps=5):
|
| 32 |
+
defaults = dict(lr=lr, momentum=momentum, nesterov=nesterov, ns_steps=ns_steps)
|
| 33 |
+
params = [*params]
|
| 34 |
+
param_groups = []
|
| 35 |
+
for size in {p.numel() for p in params}:
|
| 36 |
+
param_groups.append(dict(params=[p for p in params if p.numel() == size]))
|
| 37 |
+
super().__init__(param_groups, defaults)
|
| 38 |
+
|
| 39 |
+
@torch.no_grad()
|
| 40 |
+
def step(self):
|
| 41 |
+
for group in self.param_groups:
|
| 42 |
+
for p in group["params"]:
|
| 43 |
+
g = p.grad
|
| 44 |
+
assert g is not None
|
| 45 |
+
state = self.state[p]
|
| 46 |
+
if "momentum_buffer" not in state:
|
| 47 |
+
state["momentum_buffer"] = torch.zeros_like(g)
|
| 48 |
+
buf = state["momentum_buffer"]
|
| 49 |
+
buf.lerp_(g, 1 - group["momentum"])
|
| 50 |
+
g = g.lerp_(buf, group["momentum"]) if group["nesterov"] else buf
|
| 51 |
+
g = zeropower_via_newtonschulz5(g, steps=group["ns_steps"])
|
| 52 |
+
p.add_(g, alpha=-group["lr"] * max(1, p.size(-2) / p.size(-1)) ** 0.5)
|
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "theo-bert-base"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Standalone Hugging Face export for a custom biblical-domain masked language model."
|
| 5 |
+
requires-python = ">=3.10"
|
| 6 |
+
dependencies = [
|
| 7 |
+
"torch>=2.4.0",
|
| 8 |
+
"transformers>=5.2.0",
|
| 9 |
+
"safetensors>=0.7.0",
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
[build-system]
|
| 13 |
+
requires = ["hatchling"]
|
| 14 |
+
build-backend = "hatchling.build"
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch>=2.4.0
|
| 2 |
+
transformers>=5.2.0
|
| 3 |
+
safetensors==0.7.0
|
|
@@ -0,0 +1,1138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MLM evaluation for safetensors-format bareminn encoder weights (fp16).
|
| 3 |
+
|
| 4 |
+
Loads a model.safetensors file directly, converts weights to fp32 for
|
| 5 |
+
bit-exact evaluation, and runs the full eval suite to prove the fp16-
|
| 6 |
+
stored weights produce identical results to the original fp32 checkpoint.
|
| 7 |
+
|
| 8 |
+
Usage:
|
| 9 |
+
python scripts/mlm_eval_safetensors.py --repo-dir . --device cpu
|
| 10 |
+
python scripts/mlm_eval_safetensors.py --repo-dir . --device cuda --compare eval_results/d12_encoder_mlm_eval.json
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import os
|
| 14 |
+
import json
|
| 15 |
+
import argparse
|
| 16 |
+
from collections import Counter
|
| 17 |
+
import math
|
| 18 |
+
|
| 19 |
+
import torch
|
| 20 |
+
import torch.nn.functional as F
|
| 21 |
+
from transformers import BertTokenizer
|
| 22 |
+
|
| 23 |
+
from pathlib import Path
|
| 24 |
+
from transformers import AutoModelForMaskedLM
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
MAX_LENGTH = 104
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# ---------------------------------------------------------------------------
|
| 31 |
+
# Device resolution
|
| 32 |
+
# ---------------------------------------------------------------------------
|
| 33 |
+
|
| 34 |
+
def resolve_device(requested: str) -> str:
|
| 35 |
+
if requested == "cuda" and not torch.cuda.is_available():
|
| 36 |
+
raise RuntimeError(
|
| 37 |
+
"Requested --device cuda, but torch.cuda.is_available() is False."
|
| 38 |
+
)
|
| 39 |
+
return requested
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# ---------------------------------------------------------------------------
|
| 43 |
+
# Safetensors loader (replaces load_model for .pt checkpoints)
|
| 44 |
+
# ---------------------------------------------------------------------------
|
| 45 |
+
|
| 46 |
+
def load_model_from_repo(repo_dir: Path, device: str):
|
| 47 |
+
"""Load encoder weights from a Hugging Face repo directory (config.json +
|
| 48 |
+
model.safetensors + tokenizer files).
|
| 49 |
+
|
| 50 |
+
Weights on disk are fp16; we load at fp32 so the forward pass is
|
| 51 |
+
numerically identical to the original fp32 checkpoint — proving the
|
| 52 |
+
fp16→fp32 round-trip is lossless for these values.
|
| 53 |
+
"""
|
| 54 |
+
print(f" Loading from repo: {repo_dir}")
|
| 55 |
+
model = AutoModelForMaskedLM.from_pretrained(
|
| 56 |
+
str(repo_dir),
|
| 57 |
+
dtype=torch.float32,
|
| 58 |
+
trust_remote_code=True,
|
| 59 |
+
)
|
| 60 |
+
model.to(device)
|
| 61 |
+
model.eval()
|
| 62 |
+
return model, model.config
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
# ---------------------------------------------------------------------------
|
| 66 |
+
# Tokenization helpers (unchanged from original)
|
| 67 |
+
# ---------------------------------------------------------------------------
|
| 68 |
+
|
| 69 |
+
def tokenize_with_mask(text: str, tokenizer: BertTokenizer) -> list[int]:
|
| 70 |
+
lowered = "[MASK]".join(part.lower() for part in text.split("[MASK]"))
|
| 71 |
+
enc = tokenizer(lowered, add_special_tokens=False)
|
| 72 |
+
return enc["input_ids"][:MAX_LENGTH]
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def word_to_token_ids(word: str, tokenizer: BertTokenizer) -> list[int]:
|
| 76 |
+
enc = tokenizer.encode(word.lower(), add_special_tokens=True)
|
| 77 |
+
return enc[1:-1]
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def word_to_first_token_id(word: str, tokenizer: BertTokenizer) -> int:
|
| 81 |
+
inner = word_to_token_ids(word, tokenizer)
|
| 82 |
+
return inner[0] if inner else tokenizer.unk_token_id
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def find_mask_positions(token_ids: list[int], mask_id: int) -> list[int]:
|
| 86 |
+
return [i for i, t in enumerate(token_ids) if t == mask_id]
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
# ---------------------------------------------------------------------------
|
| 90 |
+
# Candidate / sequence helpers (unchanged from original)
|
| 91 |
+
# ---------------------------------------------------------------------------
|
| 92 |
+
|
| 93 |
+
def _build_candidates(tc: dict) -> list[str]:
|
| 94 |
+
return (
|
| 95 |
+
tc.get("targets", []) +
|
| 96 |
+
tc.get("foils", []) +
|
| 97 |
+
tc.get("acceptable_alternatives", []) +
|
| 98 |
+
tc.get("failure_examples", [])
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _required_mask_count(tc: dict, tokenizer: BertTokenizer) -> int:
|
| 103 |
+
existing_masks = tc["input"].count("[MASK]")
|
| 104 |
+
targets = tc.get("targets", [])
|
| 105 |
+
required = max((len(word_to_token_ids(w, tokenizer)) for w in targets), default=1)
|
| 106 |
+
return max(existing_masks, required, 1)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _expand_masks(text: str, required_masks: int) -> str:
|
| 110 |
+
existing_masks = text.count("[MASK]")
|
| 111 |
+
if existing_masks != 1 or required_masks <= 1:
|
| 112 |
+
return text
|
| 113 |
+
return text.replace("[MASK]", " ".join(["[MASK]"] * required_masks), 1)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def _sequence_candidates(
|
| 117 |
+
tc: dict,
|
| 118 |
+
tokenizer: BertTokenizer,
|
| 119 |
+
include_failures: bool = False,
|
| 120 |
+
) -> list[tuple[str, tuple[int, ...]]]:
|
| 121 |
+
seen: set[tuple[int, ...]] = set()
|
| 122 |
+
words = tc.get("targets", []) + tc.get("acceptable_alternatives", [])
|
| 123 |
+
if tc["pass_condition"] == "correct_beats_foil":
|
| 124 |
+
words += tc.get("foils", [])
|
| 125 |
+
if include_failures:
|
| 126 |
+
words += tc.get("failure_examples", [])
|
| 127 |
+
|
| 128 |
+
out = []
|
| 129 |
+
for word in words:
|
| 130 |
+
ids = tuple(word_to_token_ids(word, tokenizer))
|
| 131 |
+
if not ids or ids in seen:
|
| 132 |
+
continue
|
| 133 |
+
seen.add(ids)
|
| 134 |
+
out.append((word, ids))
|
| 135 |
+
return out
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def _sequence_probability(
|
| 139 |
+
probs_i: torch.Tensor,
|
| 140 |
+
mask_pos: list[int],
|
| 141 |
+
token_ids: list[int] | tuple[int, ...],
|
| 142 |
+
) -> float:
|
| 143 |
+
if not token_ids or len(token_ids) > len(mask_pos):
|
| 144 |
+
return 0.0
|
| 145 |
+
log_probs = []
|
| 146 |
+
for pos, token_id in zip(mask_pos, token_ids):
|
| 147 |
+
prob = probs_i[pos, token_id].item()
|
| 148 |
+
log_probs.append(math.log(max(prob, 1e-45)))
|
| 149 |
+
return math.exp(sum(log_probs) / len(log_probs))
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def _best_windowed_sequence_probability(
|
| 153 |
+
probs_i: torch.Tensor,
|
| 154 |
+
mask_pos: list[int],
|
| 155 |
+
token_ids: list[int] | tuple[int, ...],
|
| 156 |
+
) -> float:
|
| 157 |
+
if not token_ids or len(token_ids) > len(mask_pos):
|
| 158 |
+
return 0.0
|
| 159 |
+
best = 0.0
|
| 160 |
+
width = len(token_ids)
|
| 161 |
+
for start in range(0, len(mask_pos) - width + 1):
|
| 162 |
+
prob = _sequence_probability(
|
| 163 |
+
probs_i, mask_pos[start : start + width], token_ids
|
| 164 |
+
)
|
| 165 |
+
best = max(best, prob)
|
| 166 |
+
return best
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def _candidate_sequences(
|
| 170 |
+
words: list[str], tokenizer: BertTokenizer
|
| 171 |
+
) -> list[tuple[str, tuple[int, ...]]]:
|
| 172 |
+
out = []
|
| 173 |
+
for word in words:
|
| 174 |
+
ids = tuple(word_to_token_ids(word, tokenizer))
|
| 175 |
+
if ids:
|
| 176 |
+
out.append((word, ids))
|
| 177 |
+
return out
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def _contains_sequence(seq: tuple[int, ...], candidate: tuple[int, ...]) -> bool:
|
| 181 |
+
if not candidate or len(candidate) > len(seq):
|
| 182 |
+
return False
|
| 183 |
+
width = len(candidate)
|
| 184 |
+
return any(
|
| 185 |
+
seq[start : start + width] == candidate
|
| 186 |
+
for start in range(0, len(seq) - width + 1)
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def _match_sequence(
|
| 191 |
+
seq: tuple[int, ...],
|
| 192 |
+
targets: list[tuple[str, tuple[int, ...]]],
|
| 193 |
+
alternatives: list[tuple[str, tuple[int, ...]]],
|
| 194 |
+
) -> tuple[str, str | None]:
|
| 195 |
+
for word, ids in targets:
|
| 196 |
+
if seq == ids:
|
| 197 |
+
return "exact_target", word
|
| 198 |
+
for word, ids in alternatives:
|
| 199 |
+
if seq == ids:
|
| 200 |
+
return "exact_alt", word
|
| 201 |
+
for word, ids in alternatives:
|
| 202 |
+
if len(ids) < len(seq) and _contains_sequence(seq, ids):
|
| 203 |
+
return "contained_alt", word
|
| 204 |
+
return "none", None
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def _matches_any_sequence(
|
| 208 |
+
seq: tuple[int, ...],
|
| 209 |
+
candidates: list[tuple[str, tuple[int, ...]]],
|
| 210 |
+
) -> bool:
|
| 211 |
+
return any(
|
| 212 |
+
seq == ids or (len(ids) < len(seq) and _contains_sequence(seq, ids))
|
| 213 |
+
for _word, ids in candidates
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def _best_candidate_probability(
|
| 218 |
+
probs_i: torch.Tensor,
|
| 219 |
+
mask_pos: list[int],
|
| 220 |
+
candidates: list[tuple[str, tuple[int, ...]]],
|
| 221 |
+
) -> tuple[float, str | None]:
|
| 222 |
+
best_prob = 0.0
|
| 223 |
+
best_word = None
|
| 224 |
+
for word, ids in candidates:
|
| 225 |
+
prob = _best_windowed_sequence_probability(probs_i, mask_pos, ids)
|
| 226 |
+
if prob > best_prob:
|
| 227 |
+
best_prob = prob
|
| 228 |
+
best_word = word
|
| 229 |
+
return best_prob, best_word
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def _token_candidates(mask_probs: torch.Tensor, k: int, n_samples: int) -> list[int]:
|
| 233 |
+
if n_samples <= 1:
|
| 234 |
+
return torch.topk(mask_probs, k).indices.tolist()
|
| 235 |
+
sampled: set[int] = set()
|
| 236 |
+
draw_k = min(k, mask_probs.numel())
|
| 237 |
+
for _ in range(n_samples):
|
| 238 |
+
drawn = torch.multinomial(mask_probs, draw_k, replacement=False)
|
| 239 |
+
sampled.update(drawn.tolist())
|
| 240 |
+
return sorted(sampled, key=lambda i: mask_probs[i].item(), reverse=True)
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def _decode_token_sequence(
|
| 244 |
+
token_ids: list[int] | tuple[int, ...], tokenizer: BertTokenizer
|
| 245 |
+
) -> str:
|
| 246 |
+
return tokenizer.convert_tokens_to_string(
|
| 247 |
+
tokenizer.convert_ids_to_tokens(list(token_ids))
|
| 248 |
+
).strip()
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def _top_k_sequences(
|
| 252 |
+
probs_i: torch.Tensor,
|
| 253 |
+
mask_pos: list[int],
|
| 254 |
+
k: int,
|
| 255 |
+
tokenizer: BertTokenizer,
|
| 256 |
+
n_samples: int,
|
| 257 |
+
beam_width: int | None = None,
|
| 258 |
+
) -> list[tuple[tuple[int, ...], float]]:
|
| 259 |
+
if not mask_pos:
|
| 260 |
+
return []
|
| 261 |
+
beam_width = beam_width or max(20, k * 4)
|
| 262 |
+
candidates_per_mask = [
|
| 263 |
+
_token_candidates(probs_i[pos], beam_width, n_samples) for pos in mask_pos
|
| 264 |
+
]
|
| 265 |
+
beams: list[tuple[tuple[int, ...], float]] = [(tuple(), 0.0)]
|
| 266 |
+
for pos, token_ids in zip(mask_pos, candidates_per_mask):
|
| 267 |
+
next_beams: list[tuple[tuple[int, ...], float]] = []
|
| 268 |
+
for prefix, score in beams:
|
| 269 |
+
for token_id in token_ids:
|
| 270 |
+
prob = probs_i[pos, token_id].item()
|
| 271 |
+
next_beams.append(
|
| 272 |
+
(prefix + (token_id,), score + math.log(max(prob, 1e-45)))
|
| 273 |
+
)
|
| 274 |
+
next_beams.sort(key=lambda item: item[1], reverse=True)
|
| 275 |
+
beams = next_beams[:beam_width]
|
| 276 |
+
return beams[:k]
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def _classify_error(
|
| 280 |
+
tc: dict,
|
| 281 |
+
mask_probs: torch.Tensor,
|
| 282 |
+
top_k_ids: list[int],
|
| 283 |
+
failure_ids: set,
|
| 284 |
+
k: int,
|
| 285 |
+
passed: bool,
|
| 286 |
+
tokenizer: BertTokenizer,
|
| 287 |
+
) -> str | None:
|
| 288 |
+
if passed:
|
| 289 |
+
return None
|
| 290 |
+
targets = tc.get("targets", [])
|
| 291 |
+
if not targets:
|
| 292 |
+
return "total_miss"
|
| 293 |
+
primary_correct_id = word_to_first_token_id(targets[0], tokenizer)
|
| 294 |
+
sorted_ids = torch.argsort(mask_probs, descending=True).tolist()
|
| 295 |
+
correct_rank = sorted_ids.index(primary_correct_id)
|
| 296 |
+
if k <= correct_rank <= k + 1:
|
| 297 |
+
return "near_miss"
|
| 298 |
+
if failure_ids and any(idx in failure_ids for idx in top_k_ids[:3]):
|
| 299 |
+
return "generic_over_theological"
|
| 300 |
+
if correct_rank >= 20:
|
| 301 |
+
return "total_miss"
|
| 302 |
+
return "wrong_semantic_cluster"
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
# ---------------------------------------------------------------------------
|
| 306 |
+
# Batching and inference (unchanged from original)
|
| 307 |
+
# ---------------------------------------------------------------------------
|
| 308 |
+
|
| 309 |
+
def prepare_batch(
|
| 310 |
+
test_cases: list[dict], tokenizer: BertTokenizer, device: str
|
| 311 |
+
):
|
| 312 |
+
mask_id = tokenizer.mask_token_id
|
| 313 |
+
pad_id = tokenizer.pad_token_id
|
| 314 |
+
|
| 315 |
+
tokenized = []
|
| 316 |
+
mask_positions_list = []
|
| 317 |
+
multi_piece_flags = []
|
| 318 |
+
for tc in test_cases:
|
| 319 |
+
required_masks = _required_mask_count(tc, tokenizer)
|
| 320 |
+
original_masks = tc["input"].count("[MASK]")
|
| 321 |
+
expanded = _expand_masks(tc["input"], required_masks)
|
| 322 |
+
ids = tokenize_with_mask(expanded, tokenizer)
|
| 323 |
+
mpos = find_mask_positions(ids, mask_id)
|
| 324 |
+
tokenized.append(ids)
|
| 325 |
+
mask_positions_list.append(mpos)
|
| 326 |
+
multi_piece_flags.append(original_masks == 1 and required_masks > 1)
|
| 327 |
+
|
| 328 |
+
seq_len = max(len(ids) for ids in tokenized)
|
| 329 |
+
padded = [ids + [pad_id] * (seq_len - len(ids)) for ids in tokenized]
|
| 330 |
+
input_tensor = torch.tensor(padded, dtype=torch.long, device=device)
|
| 331 |
+
attention_mask = (input_tensor != pad_id).long()
|
| 332 |
+
return input_tensor, attention_mask, mask_positions_list, multi_piece_flags
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
@torch.no_grad()
|
| 336 |
+
def run_inference(
|
| 337 |
+
model,
|
| 338 |
+
input_tensor: torch.Tensor,
|
| 339 |
+
attention_mask: torch.Tensor,
|
| 340 |
+
temperature: float = 1.0,
|
| 341 |
+
) -> torch.Tensor:
|
| 342 |
+
outputs = model(input_tensor, attention_mask=attention_mask)
|
| 343 |
+
logits = outputs.logits
|
| 344 |
+
if temperature != 1.0:
|
| 345 |
+
logits = logits / temperature
|
| 346 |
+
return F.softmax(logits, dim=-1)
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
def _predicted_tokens(
|
| 350 |
+
mask_probs: torch.Tensor,
|
| 351 |
+
k: int,
|
| 352 |
+
n_samples: int,
|
| 353 |
+
tokenizer: BertTokenizer,
|
| 354 |
+
) -> list[str]:
|
| 355 |
+
ids = _token_candidates(mask_probs, k, n_samples)
|
| 356 |
+
return tokenizer.convert_ids_to_tokens(ids)
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
# ---------------------------------------------------------------------------
|
| 360 |
+
# Scoring functions (unchanged from original)
|
| 361 |
+
# ---------------------------------------------------------------------------
|
| 362 |
+
|
| 363 |
+
def score_target_in_top_k(
|
| 364 |
+
tc, probs_i, mask_pos, k, n_samples, tokenizer, multi_piece_target=False
|
| 365 |
+
):
|
| 366 |
+
if multi_piece_target:
|
| 367 |
+
top_sequences = _top_k_sequences(
|
| 368 |
+
probs_i, mask_pos, k, tokenizer, n_samples
|
| 369 |
+
)
|
| 370 |
+
top_k_ids = [seq for seq, _ in top_sequences]
|
| 371 |
+
top_k_tokens = [
|
| 372 |
+
_decode_token_sequence(seq, tokenizer) for seq, _ in top_sequences
|
| 373 |
+
]
|
| 374 |
+
|
| 375 |
+
targets = tc.get("targets", [])
|
| 376 |
+
alts = tc.get("acceptable_alternatives", [])
|
| 377 |
+
target_ids = _candidate_sequences(targets, tokenizer)
|
| 378 |
+
alt_ids = _candidate_sequences(alts, tokenizer)
|
| 379 |
+
|
| 380 |
+
passed = False
|
| 381 |
+
mrr = 0.0
|
| 382 |
+
match_type = "none"
|
| 383 |
+
matched_candidate = None
|
| 384 |
+
for rank, seq in enumerate(top_k_ids):
|
| 385 |
+
match_type, matched_candidate = _match_sequence(
|
| 386 |
+
seq, target_ids, alt_ids
|
| 387 |
+
)
|
| 388 |
+
if match_type != "none":
|
| 389 |
+
passed = True
|
| 390 |
+
mrr = 1.0 / (rank + 1)
|
| 391 |
+
break
|
| 392 |
+
|
| 393 |
+
failure_ids = _candidate_sequences(
|
| 394 |
+
tc.get("failure_examples", []), tokenizer
|
| 395 |
+
)
|
| 396 |
+
critical_failure = any(
|
| 397 |
+
_matches_any_sequence(seq, failure_ids) for seq in top_k_ids[:3]
|
| 398 |
+
)
|
| 399 |
+
|
| 400 |
+
target_rank = None
|
| 401 |
+
for _word, ids in target_ids:
|
| 402 |
+
for rank, beam in enumerate(top_k_ids):
|
| 403 |
+
if beam == ids:
|
| 404 |
+
target_rank = rank
|
| 405 |
+
break
|
| 406 |
+
if target_rank is not None:
|
| 407 |
+
break
|
| 408 |
+
if passed:
|
| 409 |
+
error_type = None
|
| 410 |
+
elif target_rank is not None and k <= target_rank <= k + 1:
|
| 411 |
+
error_type = "near_miss"
|
| 412 |
+
elif critical_failure:
|
| 413 |
+
error_type = "generic_over_theological"
|
| 414 |
+
else:
|
| 415 |
+
error_type = "total_miss"
|
| 416 |
+
|
| 417 |
+
return dict(
|
| 418 |
+
id=tc["id"],
|
| 419 |
+
type=tc["type"],
|
| 420 |
+
category=tc["category"],
|
| 421 |
+
difficulty=tc["difficulty"],
|
| 422 |
+
pass_=passed,
|
| 423 |
+
mrr=mrr,
|
| 424 |
+
margin=None,
|
| 425 |
+
confidence=None,
|
| 426 |
+
critical_failure=critical_failure,
|
| 427 |
+
masked_sentence=tc["input"],
|
| 428 |
+
correct_token=targets[0] if targets else "",
|
| 429 |
+
reference=tc.get("reference", ""),
|
| 430 |
+
candidates=_build_candidates(tc),
|
| 431 |
+
predicted_top_k=top_k_tokens,
|
| 432 |
+
error_type=error_type,
|
| 433 |
+
match_type=match_type,
|
| 434 |
+
matched_candidate=matched_candidate,
|
| 435 |
+
)
|
| 436 |
+
|
| 437 |
+
first_mask = mask_pos[0] if mask_pos else 0
|
| 438 |
+
mask_probs = probs_i[first_mask]
|
| 439 |
+
top_k_ids = torch.topk(mask_probs, k).indices.tolist()
|
| 440 |
+
top_k_tokens = _predicted_tokens(mask_probs, k, n_samples, tokenizer)
|
| 441 |
+
|
| 442 |
+
targets = tc.get("targets", [])
|
| 443 |
+
alts = tc.get("acceptable_alternatives", [])
|
| 444 |
+
correct_ids = set(
|
| 445 |
+
word_to_first_token_id(w, tokenizer) for w in targets + alts
|
| 446 |
+
)
|
| 447 |
+
passed = any(idx in correct_ids for idx in top_k_ids)
|
| 448 |
+
|
| 449 |
+
mrr = 0.0
|
| 450 |
+
for rank, idx in enumerate(top_k_ids):
|
| 451 |
+
if idx in correct_ids:
|
| 452 |
+
mrr = 1.0 / (rank + 1)
|
| 453 |
+
break
|
| 454 |
+
|
| 455 |
+
failure_ids = set(
|
| 456 |
+
word_to_first_token_id(w, tokenizer)
|
| 457 |
+
for w in tc.get("failure_examples", [])
|
| 458 |
+
)
|
| 459 |
+
critical_failure = any(idx in failure_ids for idx in top_k_ids[:3])
|
| 460 |
+
error_type = _classify_error(
|
| 461 |
+
tc, mask_probs, top_k_ids, failure_ids, k, passed, tokenizer
|
| 462 |
+
)
|
| 463 |
+
|
| 464 |
+
return dict(
|
| 465 |
+
id=tc["id"],
|
| 466 |
+
type=tc["type"],
|
| 467 |
+
category=tc["category"],
|
| 468 |
+
difficulty=tc["difficulty"],
|
| 469 |
+
pass_=passed,
|
| 470 |
+
mrr=mrr,
|
| 471 |
+
margin=None,
|
| 472 |
+
confidence=None,
|
| 473 |
+
critical_failure=critical_failure,
|
| 474 |
+
masked_sentence=tc["input"],
|
| 475 |
+
correct_token=targets[0] if targets else "",
|
| 476 |
+
reference=tc.get("reference", ""),
|
| 477 |
+
candidates=_build_candidates(tc),
|
| 478 |
+
predicted_top_k=top_k_tokens,
|
| 479 |
+
error_type=error_type,
|
| 480 |
+
)
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
def score_all_top_k_in_target_set(
|
| 484 |
+
tc, probs_i, mask_pos, k, n_samples, tokenizer, multi_piece_target=False
|
| 485 |
+
):
|
| 486 |
+
if multi_piece_target:
|
| 487 |
+
top_sequences = _top_k_sequences(
|
| 488 |
+
probs_i, mask_pos, k, tokenizer, n_samples
|
| 489 |
+
)
|
| 490 |
+
top_k_ids = [seq for seq, _ in top_sequences]
|
| 491 |
+
top_k_tokens = [
|
| 492 |
+
_decode_token_sequence(seq, tokenizer) for seq, _ in top_sequences
|
| 493 |
+
]
|
| 494 |
+
|
| 495 |
+
targets = tc.get("targets", [])
|
| 496 |
+
alts = tc.get("acceptable_alternatives", [])
|
| 497 |
+
target_ids = _candidate_sequences(targets, tokenizer)
|
| 498 |
+
alt_ids = _candidate_sequences(alts, tokenizer)
|
| 499 |
+
|
| 500 |
+
matches = [
|
| 501 |
+
_match_sequence(seq, target_ids, alt_ids) for seq in top_k_ids
|
| 502 |
+
]
|
| 503 |
+
valid_count = sum(
|
| 504 |
+
1 for match_type, _word in matches if match_type != "none"
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
precision = valid_count / k
|
| 508 |
+
passed = precision >= 0.8
|
| 509 |
+
|
| 510 |
+
failure_ids = _candidate_sequences(
|
| 511 |
+
tc.get("failure_examples", []), tokenizer
|
| 512 |
+
)
|
| 513 |
+
critical_failure = any(
|
| 514 |
+
_matches_any_sequence(seq, failure_ids) for seq in top_k_ids
|
| 515 |
+
)
|
| 516 |
+
error_type = (
|
| 517 |
+
None
|
| 518 |
+
if passed
|
| 519 |
+
else (
|
| 520 |
+
"generic_over_theological"
|
| 521 |
+
if critical_failure
|
| 522 |
+
else "wrong_semantic_cluster"
|
| 523 |
+
)
|
| 524 |
+
)
|
| 525 |
+
matched_candidates = [
|
| 526 |
+
word
|
| 527 |
+
for match_type, word in matches
|
| 528 |
+
if match_type != "none" and word is not None
|
| 529 |
+
]
|
| 530 |
+
|
| 531 |
+
return dict(
|
| 532 |
+
id=tc["id"],
|
| 533 |
+
type=tc["type"],
|
| 534 |
+
category=tc["category"],
|
| 535 |
+
difficulty=tc["difficulty"],
|
| 536 |
+
pass_=passed,
|
| 537 |
+
mrr=None,
|
| 538 |
+
margin=precision,
|
| 539 |
+
confidence=None,
|
| 540 |
+
critical_failure=critical_failure,
|
| 541 |
+
masked_sentence=tc["input"],
|
| 542 |
+
correct_token=targets[0] if targets else "",
|
| 543 |
+
reference=tc.get("reference", ""),
|
| 544 |
+
candidates=_build_candidates(tc),
|
| 545 |
+
predicted_top_k=top_k_tokens,
|
| 546 |
+
error_type=error_type,
|
| 547 |
+
match_type="valid_set" if passed else "none",
|
| 548 |
+
matched_candidate=", ".join(matched_candidates)
|
| 549 |
+
if matched_candidates
|
| 550 |
+
else None,
|
| 551 |
+
)
|
| 552 |
+
|
| 553 |
+
first_mask = mask_pos[0] if mask_pos else 0
|
| 554 |
+
mask_probs = probs_i[first_mask]
|
| 555 |
+
top_k_ids = torch.topk(mask_probs, k).indices.tolist()
|
| 556 |
+
top_k_tokens = _predicted_tokens(mask_probs, k, n_samples, tokenizer)
|
| 557 |
+
|
| 558 |
+
targets = tc.get("targets", [])
|
| 559 |
+
alts = tc.get("acceptable_alternatives", [])
|
| 560 |
+
valid_ids = set(
|
| 561 |
+
word_to_first_token_id(w, tokenizer) for w in targets + alts
|
| 562 |
+
)
|
| 563 |
+
|
| 564 |
+
valid_count = sum(1 for idx in top_k_ids if idx in valid_ids)
|
| 565 |
+
precision = valid_count / k
|
| 566 |
+
passed = precision >= 0.8
|
| 567 |
+
|
| 568 |
+
failure_ids = set(
|
| 569 |
+
word_to_first_token_id(w, tokenizer)
|
| 570 |
+
for w in tc.get("failure_examples", [])
|
| 571 |
+
)
|
| 572 |
+
critical_failure = any(idx in failure_ids for idx in top_k_ids)
|
| 573 |
+
error_type = _classify_error(
|
| 574 |
+
tc, mask_probs, top_k_ids, failure_ids, k, passed, tokenizer
|
| 575 |
+
)
|
| 576 |
+
|
| 577 |
+
return dict(
|
| 578 |
+
id=tc["id"],
|
| 579 |
+
type=tc["type"],
|
| 580 |
+
category=tc["category"],
|
| 581 |
+
difficulty=tc["difficulty"],
|
| 582 |
+
pass_=passed,
|
| 583 |
+
mrr=None,
|
| 584 |
+
margin=precision,
|
| 585 |
+
confidence=None,
|
| 586 |
+
critical_failure=critical_failure,
|
| 587 |
+
masked_sentence=tc["input"],
|
| 588 |
+
correct_token=targets[0] if targets else "",
|
| 589 |
+
reference=tc.get("reference", ""),
|
| 590 |
+
candidates=_build_candidates(tc),
|
| 591 |
+
predicted_top_k=top_k_tokens,
|
| 592 |
+
error_type=error_type,
|
| 593 |
+
)
|
| 594 |
+
|
| 595 |
+
|
| 596 |
+
def score_correct_beats_foil(
|
| 597 |
+
tc, probs_i, mask_pos, k, n_samples, tokenizer, multi_piece_target=False
|
| 598 |
+
):
|
| 599 |
+
if multi_piece_target:
|
| 600 |
+
targets = tc.get("targets", [])
|
| 601 |
+
alts = tc.get("acceptable_alternatives", [])
|
| 602 |
+
foils = tc.get("foils", [])
|
| 603 |
+
|
| 604 |
+
target_ids = _candidate_sequences(targets, tokenizer)
|
| 605 |
+
alt_ids = _candidate_sequences(alts, tokenizer)
|
| 606 |
+
p_correct, matched_candidate = _best_candidate_probability(
|
| 607 |
+
probs_i, mask_pos, target_ids + alt_ids
|
| 608 |
+
)
|
| 609 |
+
match_type = "none"
|
| 610 |
+
if matched_candidate is not None:
|
| 611 |
+
match_type = (
|
| 612 |
+
"exact_target"
|
| 613 |
+
if matched_candidate in targets
|
| 614 |
+
else "acceptable_alt"
|
| 615 |
+
)
|
| 616 |
+
|
| 617 |
+
foil_ids = word_to_token_ids(foils[0], tokenizer) if foils else []
|
| 618 |
+
p_foil = (
|
| 619 |
+
_best_windowed_sequence_probability(probs_i, mask_pos, foil_ids)
|
| 620 |
+
if foil_ids
|
| 621 |
+
else 0.0
|
| 622 |
+
)
|
| 623 |
+
|
| 624 |
+
passed = p_correct > p_foil
|
| 625 |
+
margin = p_correct - p_foil
|
| 626 |
+
|
| 627 |
+
if margin > 0.1:
|
| 628 |
+
confidence = "high"
|
| 629 |
+
elif margin > 0.02:
|
| 630 |
+
confidence = "medium"
|
| 631 |
+
else:
|
| 632 |
+
confidence = "low"
|
| 633 |
+
|
| 634 |
+
top_sequences = _top_k_sequences(
|
| 635 |
+
probs_i, mask_pos, k, tokenizer, n_samples
|
| 636 |
+
)
|
| 637 |
+
top_k_ids = [seq for seq, _ in top_sequences]
|
| 638 |
+
top_k_tokens = [
|
| 639 |
+
_decode_token_sequence(seq, tokenizer) for seq, _ in top_sequences
|
| 640 |
+
]
|
| 641 |
+
failure_ids: set = set()
|
| 642 |
+
error_type = None if passed else "wrong_semantic_cluster"
|
| 643 |
+
|
| 644 |
+
return dict(
|
| 645 |
+
id=tc["id"],
|
| 646 |
+
type=tc["type"],
|
| 647 |
+
category=tc["category"],
|
| 648 |
+
difficulty=tc["difficulty"],
|
| 649 |
+
pass_=passed,
|
| 650 |
+
mrr=None,
|
| 651 |
+
margin=margin,
|
| 652 |
+
confidence=confidence,
|
| 653 |
+
critical_failure=False,
|
| 654 |
+
masked_sentence=tc["input"],
|
| 655 |
+
correct_token=targets[0] if targets else "",
|
| 656 |
+
reference=tc.get("reference", ""),
|
| 657 |
+
candidates=_build_candidates(tc),
|
| 658 |
+
predicted_top_k=top_k_tokens,
|
| 659 |
+
error_type=error_type,
|
| 660 |
+
match_type=match_type if passed else "none",
|
| 661 |
+
matched_candidate=matched_candidate if passed else None,
|
| 662 |
+
)
|
| 663 |
+
|
| 664 |
+
first_mask = mask_pos[0] if mask_pos else 0
|
| 665 |
+
mask_probs = probs_i[first_mask]
|
| 666 |
+
|
| 667 |
+
targets = tc.get("targets", [])
|
| 668 |
+
alts = tc.get("acceptable_alternatives", [])
|
| 669 |
+
foils = tc.get("foils", [])
|
| 670 |
+
|
| 671 |
+
correct_ids = [
|
| 672 |
+
word_to_first_token_id(w, tokenizer) for w in targets + alts
|
| 673 |
+
]
|
| 674 |
+
p_correct = (
|
| 675 |
+
max(mask_probs[i].item() for i in correct_ids) if correct_ids else 0.0
|
| 676 |
+
)
|
| 677 |
+
|
| 678 |
+
foil_id = word_to_first_token_id(foils[0], tokenizer) if foils else None
|
| 679 |
+
p_foil = mask_probs[foil_id].item() if foil_id is not None else 0.0
|
| 680 |
+
|
| 681 |
+
passed = p_correct > p_foil
|
| 682 |
+
margin = p_correct - p_foil
|
| 683 |
+
|
| 684 |
+
if margin > 0.1:
|
| 685 |
+
confidence = "high"
|
| 686 |
+
elif margin > 0.02:
|
| 687 |
+
confidence = "medium"
|
| 688 |
+
else:
|
| 689 |
+
confidence = "low"
|
| 690 |
+
|
| 691 |
+
top_k_ids = torch.topk(mask_probs, k).indices.tolist()
|
| 692 |
+
top_k_tokens = _predicted_tokens(mask_probs, k, n_samples, tokenizer)
|
| 693 |
+
failure_ids: set = set()
|
| 694 |
+
error_type = _classify_error(
|
| 695 |
+
tc, mask_probs, top_k_ids, failure_ids, k, passed, tokenizer
|
| 696 |
+
)
|
| 697 |
+
|
| 698 |
+
return dict(
|
| 699 |
+
id=tc["id"],
|
| 700 |
+
type=tc["type"],
|
| 701 |
+
category=tc["category"],
|
| 702 |
+
difficulty=tc["difficulty"],
|
| 703 |
+
pass_=passed,
|
| 704 |
+
mrr=None,
|
| 705 |
+
margin=margin,
|
| 706 |
+
confidence=confidence,
|
| 707 |
+
critical_failure=False,
|
| 708 |
+
masked_sentence=tc["input"],
|
| 709 |
+
correct_token=targets[0] if targets else "",
|
| 710 |
+
reference=tc.get("reference", ""),
|
| 711 |
+
candidates=_build_candidates(tc),
|
| 712 |
+
predicted_top_k=top_k_tokens,
|
| 713 |
+
error_type=error_type,
|
| 714 |
+
)
|
| 715 |
+
|
| 716 |
+
|
| 717 |
+
def score_all(
|
| 718 |
+
test_cases,
|
| 719 |
+
probs,
|
| 720 |
+
mask_positions_list,
|
| 721 |
+
multi_piece_flags,
|
| 722 |
+
tokenizer,
|
| 723 |
+
default_k,
|
| 724 |
+
n_samples=1,
|
| 725 |
+
):
|
| 726 |
+
results = []
|
| 727 |
+
for i, tc in enumerate(test_cases):
|
| 728 |
+
mpos = mask_positions_list[i]
|
| 729 |
+
multi_piece_target = multi_piece_flags[i]
|
| 730 |
+
k = tc.get("k") or default_k
|
| 731 |
+
cond = tc["pass_condition"]
|
| 732 |
+
probs_i = probs[i]
|
| 733 |
+
|
| 734 |
+
if cond == "target_in_top_k":
|
| 735 |
+
result = score_target_in_top_k(
|
| 736 |
+
tc,
|
| 737 |
+
probs_i,
|
| 738 |
+
mpos,
|
| 739 |
+
k,
|
| 740 |
+
n_samples,
|
| 741 |
+
tokenizer,
|
| 742 |
+
multi_piece_target=multi_piece_target,
|
| 743 |
+
)
|
| 744 |
+
elif cond == "all_top_k_in_target_set":
|
| 745 |
+
result = score_all_top_k_in_target_set(
|
| 746 |
+
tc,
|
| 747 |
+
probs_i,
|
| 748 |
+
mpos,
|
| 749 |
+
k,
|
| 750 |
+
n_samples,
|
| 751 |
+
tokenizer,
|
| 752 |
+
multi_piece_target=multi_piece_target,
|
| 753 |
+
)
|
| 754 |
+
elif cond == "correct_beats_foil":
|
| 755 |
+
result = score_correct_beats_foil(
|
| 756 |
+
tc,
|
| 757 |
+
probs_i,
|
| 758 |
+
mpos,
|
| 759 |
+
k,
|
| 760 |
+
n_samples,
|
| 761 |
+
tokenizer,
|
| 762 |
+
multi_piece_target=multi_piece_target,
|
| 763 |
+
)
|
| 764 |
+
else:
|
| 765 |
+
raise ValueError(f"Unknown pass_condition: {cond}")
|
| 766 |
+
results.append(result)
|
| 767 |
+
return results
|
| 768 |
+
|
| 769 |
+
|
| 770 |
+
# ---------------------------------------------------------------------------
|
| 771 |
+
# Reporting (unchanged from original)
|
| 772 |
+
# ---------------------------------------------------------------------------
|
| 773 |
+
|
| 774 |
+
def pct(val):
|
| 775 |
+
return f"{val * 100:.1f}%"
|
| 776 |
+
|
| 777 |
+
|
| 778 |
+
def difficulty_weighted_score(results):
|
| 779 |
+
weights = {"easy": 1.0, "medium": 2.0, "hard": 3.0}
|
| 780 |
+
ws = wt = 0.0
|
| 781 |
+
for result in results:
|
| 782 |
+
weight = weights.get(result["difficulty"], 1.0)
|
| 783 |
+
ws += weight if result["pass_"] else 0.0
|
| 784 |
+
wt += weight
|
| 785 |
+
return ws / wt if wt > 0 else 0.0
|
| 786 |
+
|
| 787 |
+
|
| 788 |
+
def print_results(results):
|
| 789 |
+
total = len(results)
|
| 790 |
+
passed = sum(1 for r in results if r["pass_"])
|
| 791 |
+
overall = passed / total
|
| 792 |
+
|
| 793 |
+
print("\n" + "=" * 70)
|
| 794 |
+
print("MLM EVALUATION RESULTS (safetensors fp16 → fp32)")
|
| 795 |
+
print("=" * 70)
|
| 796 |
+
print(f"\nOverall: {passed}/{total} ({pct(overall)})")
|
| 797 |
+
|
| 798 |
+
print("\n--- BY TYPE ---")
|
| 799 |
+
for eval_type in [
|
| 800 |
+
"doctrinal_association",
|
| 801 |
+
"canonical_knowledge",
|
| 802 |
+
"contrastive_theology",
|
| 803 |
+
]:
|
| 804 |
+
matches = [r for r in results if r["type"] == eval_type]
|
| 805 |
+
passed_matches = sum(1 for r in matches if r["pass_"])
|
| 806 |
+
rate = passed_matches / len(matches) if matches else 0.0
|
| 807 |
+
extra = ""
|
| 808 |
+
if eval_type == "doctrinal_association":
|
| 809 |
+
mrrs = [r["mrr"] for r in matches if r["mrr"] is not None]
|
| 810 |
+
extra = (
|
| 811 |
+
f" mean_mrr={sum(mrrs) / len(mrrs):.3f}" if mrrs else ""
|
| 812 |
+
)
|
| 813 |
+
elif eval_type == "contrastive_theology":
|
| 814 |
+
margins = [r["margin"] for r in matches if r["margin"] is not None]
|
| 815 |
+
extra = (
|
| 816 |
+
f" mean_margin={sum(margins) / len(margins):.4f}"
|
| 817 |
+
if margins
|
| 818 |
+
else ""
|
| 819 |
+
)
|
| 820 |
+
print(
|
| 821 |
+
f" {eval_type}: {passed_matches}/{len(matches)} ({pct(rate)}){extra}"
|
| 822 |
+
)
|
| 823 |
+
|
| 824 |
+
print("\n--- BY CATEGORY ---")
|
| 825 |
+
categories = sorted(set(r["category"] for r in results))
|
| 826 |
+
for category in categories:
|
| 827 |
+
matches = [r for r in results if r["category"] == category]
|
| 828 |
+
passed_matches = sum(1 for r in matches if r["pass_"])
|
| 829 |
+
rate = passed_matches / len(matches) if matches else 0.0
|
| 830 |
+
critical = sum(1 for r in matches if r["critical_failure"])
|
| 831 |
+
print(
|
| 832 |
+
f" {category:<22s} {passed_matches}/{len(matches)} ({pct(rate):<6s}) crit_fail={critical}"
|
| 833 |
+
)
|
| 834 |
+
|
| 835 |
+
print("\n--- BY DIFFICULTY ---")
|
| 836 |
+
for difficulty in ["easy", "medium", "hard"]:
|
| 837 |
+
matches = [r for r in results if r["difficulty"] == difficulty]
|
| 838 |
+
passed_matches = sum(1 for r in matches if r["pass_"])
|
| 839 |
+
rate = passed_matches / len(matches) if matches else 0.0
|
| 840 |
+
print(f" {difficulty:<8} {passed_matches}/{len(matches)} ({pct(rate)})")
|
| 841 |
+
|
| 842 |
+
weighted = difficulty_weighted_score(results)
|
| 843 |
+
critical = sum(1 for r in results if r["critical_failure"])
|
| 844 |
+
print(f"\nDifficulty-weighted score: {pct(weighted)}")
|
| 845 |
+
print(
|
| 846 |
+
f"Critical failure rate: {critical}/{total} ({pct(critical / total)})"
|
| 847 |
+
)
|
| 848 |
+
|
| 849 |
+
contrastive = [r for r in results if r["type"] == "contrastive_theology"]
|
| 850 |
+
if contrastive:
|
| 851 |
+
print("\n--- CONTRASTIVE CONFIDENCE ---")
|
| 852 |
+
for confidence in ["high", "medium", "low"]:
|
| 853 |
+
matches = [r for r in contrastive if r["confidence"] == confidence]
|
| 854 |
+
passed_matches = sum(1 for r in matches if r["pass_"])
|
| 855 |
+
print(f" {confidence:<8s} {passed_matches}/{len(matches)}")
|
| 856 |
+
|
| 857 |
+
easy_results = [r for r in results if r["difficulty"] == "easy"]
|
| 858 |
+
easy_rate = (
|
| 859 |
+
sum(1 for r in easy_results if r["pass_"]) / len(easy_results)
|
| 860 |
+
if easy_results
|
| 861 |
+
else 0.0
|
| 862 |
+
)
|
| 863 |
+
contrastive_rate = (
|
| 864 |
+
sum(1 for r in contrastive if r["pass_"]) / len(contrastive)
|
| 865 |
+
if contrastive
|
| 866 |
+
else 0.0
|
| 867 |
+
)
|
| 868 |
+
|
| 869 |
+
if overall >= 0.80 and easy_rate >= 0.95 and contrastive_rate >= 0.75:
|
| 870 |
+
assessment = "EXCELLENT"
|
| 871 |
+
elif overall >= 0.60 and easy_rate >= 0.85 and contrastive_rate >= 0.60:
|
| 872 |
+
assessment = "GOOD ENOUGH for semantic search"
|
| 873 |
+
elif overall >= 0.30:
|
| 874 |
+
assessment = "NEEDS MORE TRAINING"
|
| 875 |
+
else:
|
| 876 |
+
assessment = "FUNDAMENTALLY BROKEN"
|
| 877 |
+
|
| 878 |
+
print("\n" + "=" * 70)
|
| 879 |
+
print(f"ASSESSMENT: {assessment}")
|
| 880 |
+
print("=" * 70 + "\n")
|
| 881 |
+
|
| 882 |
+
|
| 883 |
+
# ---------------------------------------------------------------------------
|
| 884 |
+
# Comparison against a reference eval JSON
|
| 885 |
+
# ---------------------------------------------------------------------------
|
| 886 |
+
|
| 887 |
+
def compare_results(
|
| 888 |
+
results: list[dict],
|
| 889 |
+
reference_path: str,
|
| 890 |
+
) -> dict:
|
| 891 |
+
"""Diff the safetensors eval against a prior (fp32 .pt checkpoint) run.
|
| 892 |
+
|
| 893 |
+
Returns a comparison summary. Prints any test cases whose pass/fail
|
| 894 |
+
status differs between the two runs.
|
| 895 |
+
"""
|
| 896 |
+
with open(reference_path) as f:
|
| 897 |
+
ref = json.load(f)
|
| 898 |
+
ref_results = ref.get("results", ref)
|
| 899 |
+
|
| 900 |
+
# Build lookup by test case id
|
| 901 |
+
ref_by_id: dict[str, dict] = {}
|
| 902 |
+
for r in ref_results:
|
| 903 |
+
rid = str(r.get("id", ""))
|
| 904 |
+
if rid:
|
| 905 |
+
ref_by_id[rid] = r
|
| 906 |
+
|
| 907 |
+
total = len(results)
|
| 908 |
+
matching = 0
|
| 909 |
+
mismatches: list[dict] = []
|
| 910 |
+
only_new = []
|
| 911 |
+
only_old_ids = set(ref_by_id.keys())
|
| 912 |
+
|
| 913 |
+
for r in results:
|
| 914 |
+
rid = str(r.get("id", ""))
|
| 915 |
+
only_old_ids.discard(rid)
|
| 916 |
+
ref_r = ref_by_id.get(rid)
|
| 917 |
+
if ref_r is None:
|
| 918 |
+
only_new.append(r)
|
| 919 |
+
continue
|
| 920 |
+
|
| 921 |
+
pass_match = r["pass_"] == ref_r.get("pass_")
|
| 922 |
+
if pass_match:
|
| 923 |
+
matching += 1
|
| 924 |
+
else:
|
| 925 |
+
mismatches.append(
|
| 926 |
+
{
|
| 927 |
+
"id": rid,
|
| 928 |
+
"type": r["type"],
|
| 929 |
+
"category": r["category"],
|
| 930 |
+
"safetensors_pass": r["pass_"],
|
| 931 |
+
"reference_pass": ref_r.get("pass_"),
|
| 932 |
+
"masked_sentence": r.get("masked_sentence", ""),
|
| 933 |
+
}
|
| 934 |
+
)
|
| 935 |
+
|
| 936 |
+
print("\n" + "=" * 70)
|
| 937 |
+
print("COMPARISON AGAINST REFERENCE")
|
| 938 |
+
print("=" * 70)
|
| 939 |
+
print(f" Reference: {reference_path}")
|
| 940 |
+
print(f" Total test cases: {total}")
|
| 941 |
+
print(f" Matching pass/fail: {matching}/{total} ({pct(matching / total)})")
|
| 942 |
+
print(f" Mismatches: {len(mismatches)}")
|
| 943 |
+
print(f" Only in safetensors: {len(only_new)}")
|
| 944 |
+
print(f" Only in reference: {len(only_old_ids)}")
|
| 945 |
+
|
| 946 |
+
if mismatches:
|
| 947 |
+
print("\n--- PASS/FAIL MISMATCHES ---")
|
| 948 |
+
for m in mismatches:
|
| 949 |
+
print(
|
| 950 |
+
f" id={m['id']} type={m['type']} category={m['category']}"
|
| 951 |
+
)
|
| 952 |
+
print(
|
| 953 |
+
f" safetensors: {m['safetensors_pass']} reference: {m['reference_pass']}"
|
| 954 |
+
)
|
| 955 |
+
print(f" sentence: {m['masked_sentence']}")
|
| 956 |
+
|
| 957 |
+
equivalent = len(mismatches) == 0
|
| 958 |
+
print(
|
| 959 |
+
f"\nVERDICT: {'EQUIVALENT ✓' if equivalent else 'DIFFERENCES FOUND ✗'}"
|
| 960 |
+
)
|
| 961 |
+
if equivalent:
|
| 962 |
+
print(
|
| 963 |
+
"The safetensors (fp16) model produces identical pass/fail results\n"
|
| 964 |
+
"to the reference fp32 checkpoint. The fp16 storage format is "
|
| 965 |
+
"safe for Hugging Face upload."
|
| 966 |
+
)
|
| 967 |
+
print()
|
| 968 |
+
|
| 969 |
+
return {
|
| 970 |
+
"total": total,
|
| 971 |
+
"matching": matching,
|
| 972 |
+
"mismatches": len(mismatches),
|
| 973 |
+
"equivalent": equivalent,
|
| 974 |
+
}
|
| 975 |
+
|
| 976 |
+
|
| 977 |
+
def build_summary(results, model_tag="safetensors", ckpt_info=None):
|
| 978 |
+
by_type = {}
|
| 979 |
+
for eval_type in [
|
| 980 |
+
"doctrinal_association",
|
| 981 |
+
"canonical_knowledge",
|
| 982 |
+
"contrastive_theology",
|
| 983 |
+
]:
|
| 984 |
+
matches = [r for r in results if r["type"] == eval_type]
|
| 985 |
+
passed_matches = sum(1 for r in matches if r["pass_"])
|
| 986 |
+
by_type[eval_type] = {
|
| 987 |
+
"passed": passed_matches,
|
| 988 |
+
"total": len(matches),
|
| 989 |
+
"pass_rate": passed_matches / len(matches) if matches else 0.0,
|
| 990 |
+
}
|
| 991 |
+
|
| 992 |
+
by_category = {}
|
| 993 |
+
for category in sorted(set(r["category"] for r in results)):
|
| 994 |
+
matches = [r for r in results if r["category"] == category]
|
| 995 |
+
passed_matches = sum(1 for r in matches if r["pass_"])
|
| 996 |
+
by_category[category] = {
|
| 997 |
+
"passed": passed_matches,
|
| 998 |
+
"total": len(matches),
|
| 999 |
+
"pass_rate": passed_matches / len(matches) if matches else 0.0,
|
| 1000 |
+
"critical_failures": sum(1 for r in matches if r["critical_failure"]),
|
| 1001 |
+
}
|
| 1002 |
+
|
| 1003 |
+
by_difficulty = {}
|
| 1004 |
+
for difficulty in ["easy", "medium", "hard"]:
|
| 1005 |
+
matches = [r for r in results if r["difficulty"] == difficulty]
|
| 1006 |
+
passed_matches = sum(1 for r in matches if r["pass_"])
|
| 1007 |
+
by_difficulty[difficulty] = {
|
| 1008 |
+
"passed": passed_matches,
|
| 1009 |
+
"total": len(matches),
|
| 1010 |
+
"pass_rate": passed_matches / len(matches) if matches else 0.0,
|
| 1011 |
+
}
|
| 1012 |
+
|
| 1013 |
+
confidence_counts = dict(
|
| 1014 |
+
Counter(r["confidence"] for r in results if r["confidence"] is not None)
|
| 1015 |
+
)
|
| 1016 |
+
|
| 1017 |
+
return {
|
| 1018 |
+
"model_tag": model_tag,
|
| 1019 |
+
"overall_pass_rate": sum(1 for r in results if r["pass_"]) / len(results),
|
| 1020 |
+
"difficulty_weighted_score": difficulty_weighted_score(results),
|
| 1021 |
+
"critical_failure_rate": sum(1 for r in results if r["critical_failure"])
|
| 1022 |
+
/ len(results),
|
| 1023 |
+
"by_type": by_type,
|
| 1024 |
+
"by_category": by_category,
|
| 1025 |
+
"by_difficulty": by_difficulty,
|
| 1026 |
+
"confidence_counts": confidence_counts,
|
| 1027 |
+
"results": results,
|
| 1028 |
+
}
|
| 1029 |
+
|
| 1030 |
+
|
| 1031 |
+
# ---------------------------------------------------------------------------
|
| 1032 |
+
# Main
|
| 1033 |
+
# ---------------------------------------------------------------------------
|
| 1034 |
+
|
| 1035 |
+
def main():
|
| 1036 |
+
p = argparse.ArgumentParser(
|
| 1037 |
+
description="MLM evaluation for safetensors-format encoder weights"
|
| 1038 |
+
)
|
| 1039 |
+
p.add_argument(
|
| 1040 |
+
"--repo-dir",
|
| 1041 |
+
type=str,
|
| 1042 |
+
default=".",
|
| 1043 |
+
help="Path to the HF repo directory (containing config.json, model.safetensors, and tokenizer files). Defaults to the current directory.",
|
| 1044 |
+
)
|
| 1045 |
+
p.add_argument(
|
| 1046 |
+
"--eval-path",
|
| 1047 |
+
type=str,
|
| 1048 |
+
default="eval.json",
|
| 1049 |
+
)
|
| 1050 |
+
p.add_argument("--device", type=str, default="cpu")
|
| 1051 |
+
p.add_argument("--k", type=int, default=5)
|
| 1052 |
+
p.add_argument(
|
| 1053 |
+
"--temperature",
|
| 1054 |
+
type=float,
|
| 1055 |
+
default=1.0,
|
| 1056 |
+
help="Logit temperature before softmax.",
|
| 1057 |
+
)
|
| 1058 |
+
p.add_argument(
|
| 1059 |
+
"--n-samples",
|
| 1060 |
+
type=int,
|
| 1061 |
+
default=1,
|
| 1062 |
+
help="Number of multinomial draws per mask position.",
|
| 1063 |
+
)
|
| 1064 |
+
p.add_argument("--output", type=str, default=None)
|
| 1065 |
+
p.add_argument(
|
| 1066 |
+
"--compare",
|
| 1067 |
+
type=str,
|
| 1068 |
+
default=None,
|
| 1069 |
+
help="Path to a prior eval JSON to diff pass/fail results against.",
|
| 1070 |
+
)
|
| 1071 |
+
args = p.parse_args()
|
| 1072 |
+
|
| 1073 |
+
device = resolve_device(args.device)
|
| 1074 |
+
repo_dir = Path(args.repo_dir).resolve()
|
| 1075 |
+
output = args.output or "eval_results/safetensors_mlm_eval.json"
|
| 1076 |
+
os.makedirs(os.path.dirname(output) or ".", exist_ok=True)
|
| 1077 |
+
|
| 1078 |
+
print("\n" + "=" * 70)
|
| 1079 |
+
print("MLM EVALUATION SUITE — safetensors (fp16 → fp32)")
|
| 1080 |
+
print("=" * 70)
|
| 1081 |
+
|
| 1082 |
+
print("Loading tokenizer (bert-base-uncased)...")
|
| 1083 |
+
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
|
| 1084 |
+
|
| 1085 |
+
print(f"Loading model from: {repo_dir}")
|
| 1086 |
+
model, config = load_model_from_repo(repo_dir, device)
|
| 1087 |
+
print(
|
| 1088 |
+
f" dim={config.n_embd}, layers={config.n_layer}, vocab={config.vocab_size}"
|
| 1089 |
+
)
|
| 1090 |
+
|
| 1091 |
+
# Report weight dtype status
|
| 1092 |
+
param_dtypes = set(p.dtype for p in model.parameters())
|
| 1093 |
+
print(f" Parameter dtypes after load: {param_dtypes}")
|
| 1094 |
+
|
| 1095 |
+
eval_path = os.path.abspath(args.eval_path)
|
| 1096 |
+
print(f"Loading eval dataset: {eval_path}...")
|
| 1097 |
+
with open(args.eval_path) as f:
|
| 1098 |
+
payload = json.load(f)
|
| 1099 |
+
test_cases = payload["test_cases"] if isinstance(payload, dict) else payload
|
| 1100 |
+
print(f"Loaded {len(test_cases)} test cases")
|
| 1101 |
+
|
| 1102 |
+
input_tensor, attention_mask, mask_positions_list, multi_piece_flags = (
|
| 1103 |
+
prepare_batch(test_cases, tokenizer, device)
|
| 1104 |
+
)
|
| 1105 |
+
temp_note = (
|
| 1106 |
+
f" (temperature={args.temperature}, n_samples={args.n_samples})"
|
| 1107 |
+
if args.temperature != 1.0 or args.n_samples > 1
|
| 1108 |
+
else ""
|
| 1109 |
+
)
|
| 1110 |
+
print(f"Running inference{temp_note}...")
|
| 1111 |
+
probs = run_inference(
|
| 1112 |
+
model, input_tensor, attention_mask, temperature=args.temperature
|
| 1113 |
+
)
|
| 1114 |
+
results = score_all(
|
| 1115 |
+
test_cases,
|
| 1116 |
+
probs,
|
| 1117 |
+
mask_positions_list,
|
| 1118 |
+
multi_piece_flags,
|
| 1119 |
+
tokenizer,
|
| 1120 |
+
args.k,
|
| 1121 |
+
n_samples=args.n_samples,
|
| 1122 |
+
)
|
| 1123 |
+
|
| 1124 |
+
print_results(results)
|
| 1125 |
+
|
| 1126 |
+
summary = build_summary(results)
|
| 1127 |
+
|
| 1128 |
+
with open(output, "w") as f:
|
| 1129 |
+
json.dump(summary, f, indent=2)
|
| 1130 |
+
print(f"Saved to {output}")
|
| 1131 |
+
|
| 1132 |
+
# Optionally compare against a reference eval
|
| 1133 |
+
if args.compare:
|
| 1134 |
+
compare_results(results, args.compare)
|
| 1135 |
+
|
| 1136 |
+
|
| 1137 |
+
if __name__ == "__main__":
|
| 1138 |
+
main()
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"backend": "tokenizers",
|
| 3 |
+
"cls_token": "[CLS]",
|
| 4 |
+
"do_lower_case": true,
|
| 5 |
+
"is_local": false,
|
| 6 |
+
"local_files_only": false,
|
| 7 |
+
"mask_token": "[MASK]",
|
| 8 |
+
"model_max_length": 512,
|
| 9 |
+
"pad_token": "[PAD]",
|
| 10 |
+
"sep_token": "[SEP]",
|
| 11 |
+
"strip_accents": null,
|
| 12 |
+
"tokenize_chinese_chars": true,
|
| 13 |
+
"tokenizer_class": "BertTokenizer",
|
| 14 |
+
"unk_token": "[UNK]"
|
| 15 |
+
}
|