Fill-Mask
Transformers
Safetensors
Ancient Greek (to 1453)
char_bert
ancient-greek
classical-philology
character-level
masked-diffusion
text-restoration
epigraphy
papyrology
custom_code
Instructions to use anonymous-stoicheia/Stoicheia-restoration-test4 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use anonymous-stoicheia/Stoicheia-restoration-test4 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("fill-mask", model="anonymous-stoicheia/Stoicheia-restoration-test4", trust_remote_code=True)# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("anonymous-stoicheia/Stoicheia-restoration-test4", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Publish safetensors weights, config and model card
Browse files- README.md +44 -11
- config.json +21 -0
- configuration_char_bert.py +40 -0
- model.safetensors +3 -0
- modeling_char_bert.py +210 -0
- processing_char_bert.py +346 -0
- training_metadata.json +54 -0
README.md
CHANGED
|
@@ -1,17 +1,50 @@
|
|
| 1 |
---
|
| 2 |
license: apache-2.0
|
| 3 |
-
language:
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
---
|
| 6 |
-
# Stoicheia-restoration-test4
|
| 7 |
|
| 8 |
-
|
| 9 |
-
masked-diffusion Transformer for Ancient Greek whose input factors into five
|
| 10 |
-
independently maskable planes (letters, word/sentence boundaries, diacritics,
|
| 11 |
-
capitalization, punctuation).
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- grc
|
| 5 |
+
library_name: transformers
|
| 6 |
+
tags:
|
| 7 |
+
- ancient-greek
|
| 8 |
+
- classical-philology
|
| 9 |
+
- character-level
|
| 10 |
+
- masked-diffusion
|
| 11 |
+
- text-restoration
|
| 12 |
+
- epigraphy
|
| 13 |
+
- papyrology
|
| 14 |
+
pipeline_tag: fill-mask
|
| 15 |
---
|
|
|
|
| 16 |
|
| 17 |
+
# Stoicheia -- documentary restoration (digit 4 held out)
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
+
**Stoicheia** is a 405M-parameter character-level masked-diffusion encoder for Ancient Greek
|
| 20 |
+
(`d_model` 1024, depth 32, banded attention: three of every four blocks attend within a
|
| 21 |
+
256-character window, the fourth globally). Its input is factored into five aligned planes --
|
| 22 |
+
letters, word/sentence boundaries, diacritics, capitalization, punctuation -- each of which can
|
| 23 |
+
be masked independently to an explicit *unknown* state at inference. That is what lets one model
|
| 24 |
+
read an edited text, *scriptio continua*, and a lacuna of unknown length without changing
|
| 25 |
+
anything but its input.
|
| 26 |
|
| 27 |
+
Anonymous release accompanying a paper under review.
|
| 28 |
+
|
| 29 |
+
Fine-tuned from `Stoicheia-doc_clean` to restore damaged inscriptions and papyri, on every
|
| 30 |
+
document **except** those whose PHI/TM identifier ends in **4**.
|
| 31 |
+
|
| 32 |
+
Ten such checkpoints are released, one per digit, because a fixed split makes a model useless
|
| 33 |
+
for exactly the documents an editor cares about: whatever inscription or papyrus you are working
|
| 34 |
+
on, one of the ten has provably never read it. Pick that one and the reading it proposes cannot
|
| 35 |
+
be a memory of the edition you are trying to check.
|
| 36 |
+
|
| 37 |
+
## Usage
|
| 38 |
+
|
| 39 |
+
```python
|
| 40 |
+
import torch
|
| 41 |
+
from transformers import AutoModel
|
| 42 |
+
model = AutoModel.from_pretrained("anonymous-stoicheia/Stoicheia-restoration-test4", trust_remote_code=True).eval()
|
| 43 |
+
|
| 44 |
+
from processing_char_bert import CharBertProcessor
|
| 45 |
+
proc = CharBertProcessor()
|
| 46 |
+
# "[7]" is a lacuna of known width; "[7±2]" if the width itself is uncertain
|
| 47 |
+
text = "αγαθηιτυχηιεδοξεντ[7]βουληικαιτωιδημωι"
|
| 48 |
+
best, width, cands = proc.restore_elastic(model, text, mask_dia_boundary=True)
|
| 49 |
+
print(best)
|
| 50 |
+
```
|
config.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"n_alpha": 24,
|
| 3 |
+
"mask_id": 24,
|
| 4 |
+
"blank_id": 25,
|
| 5 |
+
"pad_id": 26,
|
| 6 |
+
"n_char_ids": 27,
|
| 7 |
+
"n_boundary": 4,
|
| 8 |
+
"n_dia": 49,
|
| 9 |
+
"n_punct": 7,
|
| 10 |
+
"n_heads": 16,
|
| 11 |
+
"char_window": 256,
|
| 12 |
+
"attn_impl": "sdpa",
|
| 13 |
+
"qk_norm": true,
|
| 14 |
+
"d_model": 1024,
|
| 15 |
+
"depth": 32,
|
| 16 |
+
"model_type": "char_bert",
|
| 17 |
+
"auto_map": {
|
| 18 |
+
"AutoConfig": "configuration_char_bert.CharBertConfig",
|
| 19 |
+
"AutoModel": "modeling_char_bert.CharBertModel"
|
| 20 |
+
}
|
| 21 |
+
}
|
configuration_char_bert.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HF-Hub-compatible config for Stoicheia (CharBertEncoder)."""
|
| 2 |
+
from transformers import PretrainedConfig
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class CharBertConfig(PretrainedConfig):
|
| 6 |
+
model_type = "char_bert"
|
| 7 |
+
|
| 8 |
+
def __init__(
|
| 9 |
+
self,
|
| 10 |
+
n_alpha: int = 24,
|
| 11 |
+
mask_id: int = 24,
|
| 12 |
+
blank_id: int = 25,
|
| 13 |
+
pad_id: int = 26,
|
| 14 |
+
n_char_ids: int = 27,
|
| 15 |
+
n_boundary: int = 4,
|
| 16 |
+
n_dia: int = 49,
|
| 17 |
+
n_punct: int = 7,
|
| 18 |
+
d_model: int = 1024,
|
| 19 |
+
n_heads: int = 16,
|
| 20 |
+
depth: int = 32,
|
| 21 |
+
char_window: int = 256,
|
| 22 |
+
attn_impl: str = "sdpa",
|
| 23 |
+
qk_norm: bool = True,
|
| 24 |
+
**kwargs,
|
| 25 |
+
):
|
| 26 |
+
self.n_alpha = n_alpha
|
| 27 |
+
self.mask_id = mask_id
|
| 28 |
+
self.blank_id = blank_id
|
| 29 |
+
self.pad_id = pad_id
|
| 30 |
+
self.n_char_ids = n_char_ids
|
| 31 |
+
self.n_boundary = n_boundary
|
| 32 |
+
self.n_dia = n_dia
|
| 33 |
+
self.n_punct = n_punct
|
| 34 |
+
self.d_model = d_model
|
| 35 |
+
self.n_heads = n_heads
|
| 36 |
+
self.depth = depth
|
| 37 |
+
self.char_window = char_window
|
| 38 |
+
self.attn_impl = attn_impl
|
| 39 |
+
self.qk_norm = qk_norm
|
| 40 |
+
super().__init__(**kwargs)
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c34b21c26b17524a74fea11b0732c6e53ada466bbf407d72adb839e3b4eb59e7
|
| 3 |
+
size 1620144776
|
modeling_char_bert.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HF-Hub-compatible model for Stoicheia (CharBertEncoder).
|
| 2 |
+
|
| 3 |
+
Self-contained: vendors the transformer primitives (RMSNorm/RoPE/Attention/GeGLU/Block)
|
| 4 |
+
so this file has no dependency on the original research repo. Uses the SDPA attention
|
| 5 |
+
path only (portable to CPU and any CUDA GPU) -- the original training code also supports
|
| 6 |
+
a compiled FlexAttention block-sparse path for long packed sequences on GPU, which is not
|
| 7 |
+
needed for standalone inference on single passages and is omitted here for portability.
|
| 8 |
+
"""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
from dataclasses import dataclass
|
| 12 |
+
from typing import Optional
|
| 13 |
+
|
| 14 |
+
import torch
|
| 15 |
+
import torch.nn as nn
|
| 16 |
+
import torch.nn.functional as F
|
| 17 |
+
from transformers import PreTrainedModel
|
| 18 |
+
from transformers.modeling_outputs import ModelOutput
|
| 19 |
+
|
| 20 |
+
from .configuration_char_bert import CharBertConfig
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class RMSNorm(nn.Module):
|
| 24 |
+
def __init__(self, d, eps=1e-6):
|
| 25 |
+
super().__init__()
|
| 26 |
+
self.w = nn.Parameter(torch.ones(d))
|
| 27 |
+
self.eps = eps
|
| 28 |
+
|
| 29 |
+
def forward(self, x):
|
| 30 |
+
x = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
|
| 31 |
+
return x * self.w
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class RoPE(nn.Module):
|
| 35 |
+
def __init__(self, dim, base=10000.0):
|
| 36 |
+
super().__init__()
|
| 37 |
+
self.dim = dim
|
| 38 |
+
self.base = base
|
| 39 |
+
|
| 40 |
+
def cos_sin(self, pos):
|
| 41 |
+
# Recomputed on every call rather than cached in a registered buffer: a
|
| 42 |
+
# persistent=False buffer is never covered by the checkpoint's state dict,
|
| 43 |
+
# so it depends entirely on __init__-time materialization -- which some
|
| 44 |
+
# transformers versions' meta-device/low_cpu_mem_usage loading path can
|
| 45 |
+
# skip, silently leaving this tensor uninitialized. Recomputing here is
|
| 46 |
+
# immune to that regardless of how the model was constructed/loaded.
|
| 47 |
+
inv = 1.0 / (self.base ** (torch.arange(0, self.dim, 2, device=pos.device).float() / self.dim))
|
| 48 |
+
f = torch.outer(pos.float(), inv)
|
| 49 |
+
emb = torch.cat([f, f], -1)
|
| 50 |
+
return emb.cos(), emb.sin()
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _rotate_half(x):
|
| 54 |
+
d = x.shape[-1] // 2
|
| 55 |
+
return torch.cat([-x[..., d:], x[..., :d]], -1)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def apply_rope(q, k, cos, sin):
|
| 59 |
+
cos = cos[None, None]
|
| 60 |
+
sin = sin[None, None]
|
| 61 |
+
return q * cos + _rotate_half(q) * sin, k * cos + _rotate_half(k) * sin
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class Attention(nn.Module):
|
| 65 |
+
def __init__(self, d, n_heads, rope: RoPE, qk_norm=False):
|
| 66 |
+
super().__init__()
|
| 67 |
+
self.h = n_heads
|
| 68 |
+
self.dh = d // n_heads
|
| 69 |
+
self.qkv = nn.Linear(d, 3 * d, bias=False)
|
| 70 |
+
self.o = nn.Linear(d, d, bias=False)
|
| 71 |
+
self.rope = rope
|
| 72 |
+
self.qk_norm = qk_norm
|
| 73 |
+
if qk_norm:
|
| 74 |
+
self.q_norm = RMSNorm(self.dh)
|
| 75 |
+
self.k_norm = RMSNorm(self.dh)
|
| 76 |
+
|
| 77 |
+
def forward(self, x, pos, attn_mask):
|
| 78 |
+
B, T, D = x.shape
|
| 79 |
+
qkv = self.qkv(x).view(B, T, 3, self.h, self.dh).permute(2, 0, 3, 1, 4)
|
| 80 |
+
q, k, v = qkv[0], qkv[1], qkv[2]
|
| 81 |
+
if self.qk_norm:
|
| 82 |
+
q, k = self.q_norm(q), self.k_norm(k)
|
| 83 |
+
cos, sin = self.rope.cos_sin(pos)
|
| 84 |
+
cos, sin = cos.to(x.dtype), sin.to(x.dtype)
|
| 85 |
+
q, k = apply_rope(q, k, cos, sin)
|
| 86 |
+
out = F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask)
|
| 87 |
+
out = out.transpose(1, 2).reshape(B, T, D)
|
| 88 |
+
return self.o(out)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class GeGLU(nn.Module):
|
| 92 |
+
def __init__(self, d, mult=8 / 3):
|
| 93 |
+
super().__init__()
|
| 94 |
+
hidden = int(d * mult)
|
| 95 |
+
hidden = (hidden + 63) // 64 * 64
|
| 96 |
+
self.wi = nn.Linear(d, 2 * hidden, bias=False)
|
| 97 |
+
self.wo = nn.Linear(hidden, d, bias=False)
|
| 98 |
+
|
| 99 |
+
def forward(self, x):
|
| 100 |
+
a, b = self.wi(x).chunk(2, -1)
|
| 101 |
+
return self.wo(F.gelu(a) * b)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
class Block(nn.Module):
|
| 105 |
+
def __init__(self, d, n_heads, rope, window=0, qk_norm=False):
|
| 106 |
+
super().__init__()
|
| 107 |
+
self.n1 = RMSNorm(d)
|
| 108 |
+
self.attn = Attention(d, n_heads, rope, qk_norm=qk_norm)
|
| 109 |
+
self.n2 = RMSNorm(d)
|
| 110 |
+
self.mlp = GeGLU(d)
|
| 111 |
+
self.window = window # 0 = global; >0 = local sliding window (characters)
|
| 112 |
+
|
| 113 |
+
def forward(self, x, pos, base_mask):
|
| 114 |
+
x = x + self.attn(self.n1(x), pos, base_mask)
|
| 115 |
+
x = x + self.mlp(self.n2(x))
|
| 116 |
+
return x
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def build_attn_mask(seg_id, window, device, dtype):
|
| 120 |
+
"""Additive mask (B,1,T,T): same-segment AND (window==0 or |i-j|<window)."""
|
| 121 |
+
B, T = seg_id.shape
|
| 122 |
+
same = seg_id[:, None, :] == seg_id[:, :, None]
|
| 123 |
+
if window and window > 0:
|
| 124 |
+
idx = torch.arange(T, device=device)
|
| 125 |
+
near = (idx[None, :] - idx[:, None]).abs() < window
|
| 126 |
+
same = same & near[None]
|
| 127 |
+
mask = torch.zeros(B, 1, T, T, dtype=dtype, device=device)
|
| 128 |
+
mask.masked_fill_(~same[:, None], float("-inf"))
|
| 129 |
+
return mask
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
@dataclass
|
| 133 |
+
class CharBertOutput(ModelOutput):
|
| 134 |
+
char: torch.FloatTensor = None
|
| 135 |
+
boundary: torch.FloatTensor = None
|
| 136 |
+
dia: torch.FloatTensor = None
|
| 137 |
+
cap: torch.FloatTensor = None
|
| 138 |
+
punct: torch.FloatTensor = None
|
| 139 |
+
hidden_states: Optional[tuple] = None
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
class CharBertModel(PreTrainedModel):
|
| 143 |
+
config_class = CharBertConfig
|
| 144 |
+
|
| 145 |
+
def __init__(self, config: CharBertConfig):
|
| 146 |
+
super().__init__(config)
|
| 147 |
+
self.e_char = nn.Embedding(config.n_char_ids, config.d_model)
|
| 148 |
+
self.e_bnd = nn.Embedding(config.n_boundary, config.d_model)
|
| 149 |
+
self.e_dia = nn.Embedding(config.n_dia, config.d_model)
|
| 150 |
+
self.e_punct = nn.Embedding(config.n_punct, config.d_model)
|
| 151 |
+
rope = RoPE(config.d_model // config.n_heads)
|
| 152 |
+
blocks = []
|
| 153 |
+
for i in range(config.depth):
|
| 154 |
+
win = 0 if i % 4 == 3 else config.char_window # 3 local : 1 global
|
| 155 |
+
blocks.append(Block(config.d_model, config.n_heads, rope, window=win, qk_norm=config.qk_norm))
|
| 156 |
+
self.blocks = nn.ModuleList(blocks)
|
| 157 |
+
self.norm_out = RMSNorm(config.d_model)
|
| 158 |
+
self.head_char = nn.Linear(config.d_model, config.n_char_ids, bias=False)
|
| 159 |
+
self.head_bnd = nn.Linear(config.d_model, 3, bias=False)
|
| 160 |
+
self.head_dia = nn.Linear(config.d_model, 48, bias=False)
|
| 161 |
+
self.head_cap = nn.Linear(config.d_model, 2, bias=False)
|
| 162 |
+
self.head_punct = nn.Linear(config.d_model, 6, bias=False)
|
| 163 |
+
self.post_init()
|
| 164 |
+
|
| 165 |
+
def _init_weights(self, module):
|
| 166 |
+
if isinstance(module, nn.Linear):
|
| 167 |
+
nn.init.normal_(module.weight, std=0.02)
|
| 168 |
+
elif isinstance(module, nn.Embedding):
|
| 169 |
+
nn.init.normal_(module.weight, std=0.02)
|
| 170 |
+
|
| 171 |
+
def forward(
|
| 172 |
+
self,
|
| 173 |
+
input_ids: torch.LongTensor,
|
| 174 |
+
boundary: torch.LongTensor,
|
| 175 |
+
dia: torch.LongTensor,
|
| 176 |
+
punct: torch.LongTensor,
|
| 177 |
+
seg_id: Optional[torch.LongTensor] = None,
|
| 178 |
+
output_hidden_states: bool = False,
|
| 179 |
+
return_dict: bool = True,
|
| 180 |
+
**kwargs,
|
| 181 |
+
):
|
| 182 |
+
cfg = self.config
|
| 183 |
+
B, T = input_ids.shape
|
| 184 |
+
pos = torch.arange(T, device=input_ids.device)
|
| 185 |
+
seg = seg_id if seg_id is not None else torch.zeros(B, T, dtype=torch.long, device=input_ids.device)
|
| 186 |
+
|
| 187 |
+
x = self.e_char(input_ids) + self.e_bnd(boundary) + self.e_dia(dia) + self.e_punct(punct)
|
| 188 |
+
attn_mask = build_attn_mask(seg, cfg.char_window, input_ids.device, x.dtype)
|
| 189 |
+
glob_mask = build_attn_mask(seg, 0, input_ids.device, x.dtype)
|
| 190 |
+
|
| 191 |
+
hidden_states = [] if output_hidden_states else None
|
| 192 |
+
for blk in self.blocks:
|
| 193 |
+
m = glob_mask if blk.window == 0 else attn_mask
|
| 194 |
+
x = blk(x, pos, m)
|
| 195 |
+
if output_hidden_states:
|
| 196 |
+
hidden_states.append(x)
|
| 197 |
+
|
| 198 |
+
x = self.norm_out(x)
|
| 199 |
+
out = dict(
|
| 200 |
+
char=self.head_char(x),
|
| 201 |
+
boundary=self.head_bnd(x),
|
| 202 |
+
dia=self.head_dia(x),
|
| 203 |
+
cap=self.head_cap(x),
|
| 204 |
+
punct=self.head_punct(x),
|
| 205 |
+
)
|
| 206 |
+
if output_hidden_states:
|
| 207 |
+
out["hidden_states"] = tuple(hidden_states) + (x,)
|
| 208 |
+
if not return_dict:
|
| 209 |
+
return tuple(v for v in out.values() if v is not None)
|
| 210 |
+
return CharBertOutput(**out)
|
processing_char_bert.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HF-Hub-compatible processor for Stoicheia: text <-> the model's four input planes.
|
| 2 |
+
|
| 3 |
+
Wraps the reference normalization/denormalization logic (character classification,
|
| 4 |
+
diacritic packing, word/sentence-boundary detection) into a single callable that
|
| 5 |
+
produces model-ready tensors, plus decode helpers for the three masking use cases
|
| 6 |
+
described in the model card (restoration, accent recovery, re-segmentation).
|
| 7 |
+
|
| 8 |
+
This is intentionally NOT a `PreTrainedTokenizer` subclass: the underlying encoding is
|
| 9 |
+
a row-per-letter, four-parallel-plane structure (not a single token-id stream), which
|
| 10 |
+
doesn't fit that base class's assumptions. It follows the same `register_for_auto_class`
|
| 11 |
+
mechanism transformers uses for tokenizers/feature extractors, so
|
| 12 |
+
`AutoProcessor.from_pretrained(repo_id, trust_remote_code=True)` works the same way.
|
| 13 |
+
"""
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import json
|
| 17 |
+
import re
|
| 18 |
+
import unicodedata
|
| 19 |
+
from dataclasses import dataclass, field
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
+
import numpy as np
|
| 23 |
+
import torch
|
| 24 |
+
|
| 25 |
+
MASK, BLANK, PAD = 24, 25, 26
|
| 26 |
+
UNK_BND, UNK_DIA, UNK_PUNCT = 3, 48, 6
|
| 27 |
+
|
| 28 |
+
ALPHABET = "αβγδεζηθικλμνξοπρστυφχψω"
|
| 29 |
+
LETTER_IDS = {c: i for i, c in enumerate(ALPHABET)}
|
| 30 |
+
ID2LETTER = np.array(list(ALPHABET))
|
| 31 |
+
|
| 32 |
+
_EXTRA_BASE = {
|
| 33 |
+
"ς": "σ", "ϲ": "σ", "Ϲ": "σ", "ϐ": "β", "ϑ": "θ", "ϕ": "φ", "ϰ": "κ", "ϱ": "ρ", "ϖ": "π",
|
| 34 |
+
}
|
| 35 |
+
_MARK_MAP = {
|
| 36 |
+
0x0301: "acute", 0x0341: "acute", 0x0300: "grave", 0x0340: "grave",
|
| 37 |
+
0x0342: "circ", 0x0302: "circ", 0x0313: "smooth", 0x0343: "smooth",
|
| 38 |
+
0x0314: "rough", 0x0345: "iota", 0x0308: "diaer",
|
| 39 |
+
}
|
| 40 |
+
_ACC = {"acute": 1, "grave": 2, "circ": 3}
|
| 41 |
+
_BR = {"smooth": 1, "rough": 2}
|
| 42 |
+
_MARK_CHARS = {"acute": "́", "grave": "̀", "circ": "͂",
|
| 43 |
+
"smooth": "̓", "rough": "̔", "iota": "ͅ", "diaer": "̈"}
|
| 44 |
+
# punct plane classes (matches data/normalize.py's canonical 6-way scheme):
|
| 45 |
+
# 0 none, 1 comma, 2 high-dot(·), 3 colon, 4 period, 5 question/exclamation
|
| 46 |
+
_PUNCT_CHARS = {1: ",", 2: "·", 3: ":", 4: ".", 5: ";"}
|
| 47 |
+
|
| 48 |
+
# "λόγ[5±3]καὶ" -- a lacuna of uncertain width: best guess 5 letters, plausible
|
| 49 |
+
# range 5-3..5+3. See CharBertProcessor.restore_elastic.
|
| 50 |
+
_ELASTIC_RE = re.compile(r"\[(\d+)±(\d+)\]")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _pack_dia(acc, br, iota, diaer):
|
| 54 |
+
return ((acc * 3 + br) * 2 + iota) * 2 + diaer
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _unpack_dia(d):
|
| 58 |
+
diaer = d % 2; d //= 2
|
| 59 |
+
iota = d % 2; d //= 2
|
| 60 |
+
br = d % 3; acc = d // 3
|
| 61 |
+
return acc, br, iota, diaer
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@dataclass
|
| 65 |
+
class _Encoded:
|
| 66 |
+
chars: list # int, may include MASK
|
| 67 |
+
boundary: list
|
| 68 |
+
dia: list
|
| 69 |
+
punct: list
|
| 70 |
+
cap: list # original capitalization, for round-tripping non-masked positions
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class CharBertProcessor:
|
| 74 |
+
"""`processor(text)` -> dict of batched tensors ready for `CharBertModel(**batch)`."""
|
| 75 |
+
|
| 76 |
+
def __init__(self):
|
| 77 |
+
pass
|
| 78 |
+
|
| 79 |
+
@classmethod
|
| 80 |
+
def from_pretrained(cls, *_args, **_kwargs):
|
| 81 |
+
return cls()
|
| 82 |
+
|
| 83 |
+
def save_pretrained(self, save_directory, **_kwargs):
|
| 84 |
+
Path(save_directory).mkdir(parents=True, exist_ok=True)
|
| 85 |
+
(Path(save_directory) / "processor_config.json").write_text(json.dumps({"processor_class": "CharBertProcessor"}))
|
| 86 |
+
|
| 87 |
+
# ---------------------------------------------------------------- encode
|
| 88 |
+
|
| 89 |
+
def _classify(self, text: str) -> _Encoded:
|
| 90 |
+
"""Turn raw NFC/NFD polytonic text into per-letter plane lists, damage ('-' runs)
|
| 91 |
+
preserved as MASK/UNK positions, gold values kept for every other position."""
|
| 92 |
+
nfd = unicodedata.normalize("NFD", text)
|
| 93 |
+
chars, boundary, dia, punct, cap = [], [], [], [], []
|
| 94 |
+
acc = br = iota = diaer = 0
|
| 95 |
+
pending_bnd = 0
|
| 96 |
+
i = 0
|
| 97 |
+
while i < len(nfd):
|
| 98 |
+
ch = nfd[i]
|
| 99 |
+
if ch == "-":
|
| 100 |
+
run = 0
|
| 101 |
+
while i < len(nfd) and nfd[i] == "-":
|
| 102 |
+
run += 1
|
| 103 |
+
i += 1
|
| 104 |
+
for _ in range(run):
|
| 105 |
+
chars.append(MASK); boundary.append(UNK_BND)
|
| 106 |
+
dia.append(UNK_DIA); punct.append(UNK_PUNCT); cap.append(0)
|
| 107 |
+
continue
|
| 108 |
+
low = ch.lower()
|
| 109 |
+
base = low if low in LETTER_IDS else _EXTRA_BASE.get(low)
|
| 110 |
+
if base is not None:
|
| 111 |
+
if chars and pending_bnd:
|
| 112 |
+
boundary[-1] = pending_bnd
|
| 113 |
+
pending_bnd = 0
|
| 114 |
+
chars.append(LETTER_IDS[base])
|
| 115 |
+
cap.append(1 if ch != low else 0)
|
| 116 |
+
boundary.append(0)
|
| 117 |
+
dia.append(0) # filled in by trailing combining marks below
|
| 118 |
+
punct.append(0)
|
| 119 |
+
acc = br = iota = diaer = 0
|
| 120 |
+
elif unicodedata.combining(ch) or ord(ch) in _MARK_MAP:
|
| 121 |
+
kind = _MARK_MAP.get(ord(ch))
|
| 122 |
+
if kind in _ACC:
|
| 123 |
+
acc = _ACC[kind]
|
| 124 |
+
elif kind in _BR:
|
| 125 |
+
br = _BR[kind]
|
| 126 |
+
elif kind == "iota":
|
| 127 |
+
iota = 1
|
| 128 |
+
elif kind == "diaer":
|
| 129 |
+
diaer = 1
|
| 130 |
+
if dia:
|
| 131 |
+
dia[-1] = _pack_dia(acc, br, iota, diaer)
|
| 132 |
+
elif ch.isspace():
|
| 133 |
+
pending_bnd = max(pending_bnd, 1)
|
| 134 |
+
elif ch in ".;!?":
|
| 135 |
+
pending_bnd = max(pending_bnd, 2)
|
| 136 |
+
if punct:
|
| 137 |
+
punct[-1] = 4 if ch == "." else 5
|
| 138 |
+
elif ch in ",:··":
|
| 139 |
+
if punct:
|
| 140 |
+
punct[-1] = {",": 1, "·": 2, "·": 2, ":": 3}.get(ch, 0)
|
| 141 |
+
i += 1
|
| 142 |
+
if boundary:
|
| 143 |
+
boundary[-1] = max(boundary[-1], 2)
|
| 144 |
+
return _Encoded(chars, boundary, dia, punct, cap)
|
| 145 |
+
|
| 146 |
+
def __call__(self, text: str, mask_planes: list[str] | None = None, has_boundaries: bool = True):
|
| 147 |
+
"""Encode `text` into model-ready tensors.
|
| 148 |
+
|
| 149 |
+
mask_planes: any subset of {"chars", "boundary", "dia", "punct"} to force to
|
| 150 |
+
UNKNOWN at every position (in addition to any '-' runs, which are always
|
| 151 |
+
treated as a damaged/masked span regardless of mask_planes).
|
| 152 |
+
has_boundaries: set False for scriptio continua input (no real spaces) so the
|
| 153 |
+
boundary plane starts fully UNKNOWN rather than "no boundaries found".
|
| 154 |
+
"""
|
| 155 |
+
mask_planes = set(mask_planes or [])
|
| 156 |
+
enc = self._classify(text)
|
| 157 |
+
n = len(enc.chars)
|
| 158 |
+
chars = np.array(enc.chars, dtype=np.int64)
|
| 159 |
+
boundary = np.array(enc.boundary, dtype=np.int64)
|
| 160 |
+
dia = np.array(enc.dia, dtype=np.int64)
|
| 161 |
+
punct = np.array(enc.punct, dtype=np.int64)
|
| 162 |
+
|
| 163 |
+
if "chars" in mask_planes:
|
| 164 |
+
chars[:] = MASK
|
| 165 |
+
if "boundary" in mask_planes or not has_boundaries:
|
| 166 |
+
boundary[:] = UNK_BND
|
| 167 |
+
if "dia" in mask_planes:
|
| 168 |
+
dia[:] = UNK_DIA
|
| 169 |
+
if "punct" in mask_planes:
|
| 170 |
+
punct[:] = UNK_PUNCT
|
| 171 |
+
|
| 172 |
+
batch = dict(
|
| 173 |
+
input_ids=torch.from_numpy(chars)[None],
|
| 174 |
+
boundary=torch.from_numpy(boundary)[None],
|
| 175 |
+
dia=torch.from_numpy(dia)[None],
|
| 176 |
+
punct=torch.from_numpy(punct)[None],
|
| 177 |
+
seg_id=torch.zeros(1, n, dtype=torch.long),
|
| 178 |
+
)
|
| 179 |
+
batch["_cap"] = enc.cap # kept out-of-band; not a model input (cap is output-only)
|
| 180 |
+
return batch
|
| 181 |
+
|
| 182 |
+
# ---------------------------------------------------------------- decode
|
| 183 |
+
|
| 184 |
+
@staticmethod
|
| 185 |
+
def _restore_polytonic(chars, dia, cap, boundary, punct=None) -> str:
|
| 186 |
+
words, cur = [], []
|
| 187 |
+
for i in range(len(chars)):
|
| 188 |
+
ch = ID2LETTER[chars[i]] if chars[i] < 24 else "?"
|
| 189 |
+
a, b, io, dd = _unpack_dia(int(dia[i]))
|
| 190 |
+
if cap[i]:
|
| 191 |
+
ch = ch.upper()
|
| 192 |
+
s = ch
|
| 193 |
+
if b:
|
| 194 |
+
s += _MARK_CHARS[{1: "smooth", 2: "rough"}[b]]
|
| 195 |
+
if dd:
|
| 196 |
+
s += _MARK_CHARS["diaer"]
|
| 197 |
+
if a:
|
| 198 |
+
s += _MARK_CHARS[{1: "acute", 2: "grave", 3: "circ"}[a]]
|
| 199 |
+
if io:
|
| 200 |
+
s += _MARK_CHARS["iota"]
|
| 201 |
+
cur.append(s)
|
| 202 |
+
p = int(punct[i]) if punct is not None else 0
|
| 203 |
+
if boundary[i] >= 1:
|
| 204 |
+
w = "".join(cur)
|
| 205 |
+
if w and w[-1] == "σ":
|
| 206 |
+
w = w[:-1] + "ς"
|
| 207 |
+
w = unicodedata.normalize("NFC", w)
|
| 208 |
+
if p in _PUNCT_CHARS:
|
| 209 |
+
w += _PUNCT_CHARS[p]
|
| 210 |
+
elif boundary[i] == 2:
|
| 211 |
+
w += "."
|
| 212 |
+
words.append(w)
|
| 213 |
+
cur = []
|
| 214 |
+
if cur:
|
| 215 |
+
w = unicodedata.normalize("NFC", "".join(cur))
|
| 216 |
+
p = int(punct[-1]) if punct is not None else 0
|
| 217 |
+
if p in _PUNCT_CHARS:
|
| 218 |
+
w += _PUNCT_CHARS[p]
|
| 219 |
+
words.append(w)
|
| 220 |
+
return " ".join(words)
|
| 221 |
+
|
| 222 |
+
def decode_restoration(self, model_out, batch) -> str:
|
| 223 |
+
"""Fill masked positions with the model's argmax predictions; keep every
|
| 224 |
+
other position exactly as given. Each plane is filled independently
|
| 225 |
+
wherever IT is unknown -- chars/cap only inside a '-' gap (chars==MASK),
|
| 226 |
+
but boundary/dia/punct wherever THAT plane is UNK, which may be the whole
|
| 227 |
+
sequence if mask_planes was also used for joint gap+accent+boundary
|
| 228 |
+
restoration (not just the '-' gap itself)."""
|
| 229 |
+
pred_char = model_out.char.argmax(-1)[0].tolist()
|
| 230 |
+
pred_bnd = model_out.boundary.argmax(-1)[0].tolist()
|
| 231 |
+
pred_dia = model_out.dia.argmax(-1)[0].tolist()
|
| 232 |
+
pred_cap = model_out.cap.argmax(-1)[0].tolist()
|
| 233 |
+
pred_punct = model_out.punct.argmax(-1)[0].tolist()
|
| 234 |
+
chars = batch["input_ids"][0].tolist()
|
| 235 |
+
boundary = batch["boundary"][0].tolist()
|
| 236 |
+
dia = batch["dia"][0].tolist()
|
| 237 |
+
punct = batch["punct"][0].tolist()
|
| 238 |
+
cap = batch["_cap"]
|
| 239 |
+
for i in range(len(chars)):
|
| 240 |
+
if chars[i] == MASK:
|
| 241 |
+
chars[i] = pred_char[i] if pred_char[i] < 24 else 0
|
| 242 |
+
cap[i] = pred_cap[i]
|
| 243 |
+
if boundary[i] == UNK_BND:
|
| 244 |
+
boundary[i] = pred_bnd[i]
|
| 245 |
+
if dia[i] == UNK_DIA:
|
| 246 |
+
dia[i] = pred_dia[i]
|
| 247 |
+
if punct[i] == UNK_PUNCT:
|
| 248 |
+
punct[i] = pred_punct[i]
|
| 249 |
+
return self._restore_polytonic(chars, dia, cap, boundary, punct)
|
| 250 |
+
|
| 251 |
+
def decode_diacritics(self, model_out, batch) -> str:
|
| 252 |
+
"""Replace the diacritic plane with the model's predictions; letters/boundaries/
|
| 253 |
+
capitalization/punctuation are taken from the input as given."""
|
| 254 |
+
pred_dia = model_out.dia.argmax(-1)[0].tolist()
|
| 255 |
+
chars = batch["input_ids"][0].tolist()
|
| 256 |
+
boundary = batch["boundary"][0].tolist()
|
| 257 |
+
punct = batch["punct"][0].tolist()
|
| 258 |
+
cap = batch["_cap"]
|
| 259 |
+
return self._restore_polytonic(chars, pred_dia, cap, boundary, punct)
|
| 260 |
+
|
| 261 |
+
def decode_boundaries(self, model_out, batch) -> str:
|
| 262 |
+
"""Replace the boundary plane with the model's predictions (0/1/2); letters/
|
| 263 |
+
diacritics/capitalization/punctuation are taken from the input as given.
|
| 264 |
+
Only useful when the input truly has no accents either (a spaced-out or
|
| 265 |
+
scriptio-continua text that already carries accents gives the boundary head
|
| 266 |
+
a strong shortcut -- each word carries exactly one accent -- so this isn't a
|
| 267 |
+
meaningful standalone test of the boundary head specifically; see
|
| 268 |
+
decode_restoration/restore_elastic for the realistic joint case)."""
|
| 269 |
+
pred_bnd = model_out.boundary.argmax(-1)[0].tolist()
|
| 270 |
+
chars = batch["input_ids"][0].tolist()
|
| 271 |
+
dia = batch["dia"][0].tolist()
|
| 272 |
+
punct = batch["punct"][0].tolist()
|
| 273 |
+
cap = batch["_cap"]
|
| 274 |
+
return self._restore_polytonic(chars, dia, cap, pred_bnd, punct)
|
| 275 |
+
|
| 276 |
+
def restore_elastic(self, model, text: str, min_width: int = 1,
|
| 277 |
+
mask_dia_boundary: bool = False):
|
| 278 |
+
"""Restore a lacuna of *uncertain* width -- the realistic editorial case,
|
| 279 |
+
since editors estimate a lacuna's length, they rarely know it exactly.
|
| 280 |
+
|
| 281 |
+
`text` must contain exactly one `[N±M]` marker (best-guess width N,
|
| 282 |
+
plausible range N-M..N+M), e.g. `"λόγ[5±3]καὶ ὁ λόγος ἦν πρὸς τὸν θεόν"`.
|
| 283 |
+
For every candidate width in that range, this fills the gap, then scores
|
| 284 |
+
each candidate by the mean log-probability of the model's own letter
|
| 285 |
+
predictions inside the gap specifically (that's what distinguishes widths).
|
| 286 |
+
|
| 287 |
+
`mask_dia_boundary` controls what happens OUTSIDE the gap:
|
| 288 |
+
- False (default): real accents/word-boundaries already present in
|
| 289 |
+
`text` are kept as given -- only the gap itself is filled. Use this
|
| 290 |
+
for text where the surrounding context is already known/accented (the
|
| 291 |
+
common editorial case: a lacuna in an otherwise-legible inscription).
|
| 292 |
+
- True: accents and word-boundaries are masked and reconstructed
|
| 293 |
+
everywhere, not just inside the gap -- for fully bare scriptio
|
| 294 |
+
continua surrounding the lacuna too (no spaces, no accents at all).
|
| 295 |
+
|
| 296 |
+
Returns `(best_text, best_width, candidates)`, where `candidates` is every
|
| 297 |
+
`(width, filled_text, mean_logp)` tried, sorted best-first. Needs the model
|
| 298 |
+
(not just its output), since it runs one forward pass per candidate width.
|
| 299 |
+
"""
|
| 300 |
+
m = _ELASTIC_RE.search(text)
|
| 301 |
+
if not m:
|
| 302 |
+
raise ValueError("text must contain one '[N±M]' marker, e.g. 'λόγ[5±3]καὶ'")
|
| 303 |
+
n, spread = int(m.group(1)), int(m.group(2))
|
| 304 |
+
prefix, suffix = text[:m.start()], text[m.end():]
|
| 305 |
+
gap_start = len(self._classify(prefix).chars)
|
| 306 |
+
|
| 307 |
+
candidates = []
|
| 308 |
+
for L in range(max(min_width, n - spread), n + spread + 1):
|
| 309 |
+
probe = prefix + ("-" * L) + suffix
|
| 310 |
+
if mask_dia_boundary:
|
| 311 |
+
batch = self(probe, mask_planes=["dia", "boundary"], has_boundaries=False)
|
| 312 |
+
else:
|
| 313 |
+
batch = self(probe)
|
| 314 |
+
with torch.no_grad():
|
| 315 |
+
out = model(**{k: v for k, v in batch.items() if not k.startswith("_")})
|
| 316 |
+
|
| 317 |
+
logp = torch.log_softmax(out.char, dim=-1)[0]
|
| 318 |
+
pred_char = out.char.argmax(-1)[0].tolist()
|
| 319 |
+
gap_logp = sum(logp[gap_start + i, pred_char[gap_start + i]].item()
|
| 320 |
+
for i in range(L)) / L
|
| 321 |
+
|
| 322 |
+
pred_bnd = out.boundary.argmax(-1)[0].tolist()
|
| 323 |
+
pred_dia = out.dia.argmax(-1)[0].tolist()
|
| 324 |
+
pred_cap = out.cap.argmax(-1)[0].tolist()
|
| 325 |
+
pred_punct = out.punct.argmax(-1)[0].tolist()
|
| 326 |
+
chars = batch["input_ids"][0].tolist()
|
| 327 |
+
boundary = batch["boundary"][0].tolist()
|
| 328 |
+
dia = batch["dia"][0].tolist()
|
| 329 |
+
punct = batch["punct"][0].tolist()
|
| 330 |
+
cap = batch["_cap"]
|
| 331 |
+
for i in range(len(chars)):
|
| 332 |
+
if chars[i] == MASK:
|
| 333 |
+
chars[i] = pred_char[i] if pred_char[i] < 24 else 0
|
| 334 |
+
cap[i] = pred_cap[i]
|
| 335 |
+
if boundary[i] == UNK_BND:
|
| 336 |
+
boundary[i] = pred_bnd[i]
|
| 337 |
+
if dia[i] == UNK_DIA:
|
| 338 |
+
dia[i] = pred_dia[i]
|
| 339 |
+
if punct[i] == UNK_PUNCT:
|
| 340 |
+
punct[i] = pred_punct[i]
|
| 341 |
+
filled = self._restore_polytonic(chars, dia, cap, boundary, punct)
|
| 342 |
+
candidates.append((L, filled, gap_logp))
|
| 343 |
+
|
| 344 |
+
candidates.sort(key=lambda c: -c[2])
|
| 345 |
+
best_L, best_text, _ = candidates[0]
|
| 346 |
+
return best_text, best_L, candidates
|
training_metadata.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "whole_v4_t4v5",
|
| 3 |
+
"test_digit": "4",
|
| 4 |
+
"val_digit": "5",
|
| 5 |
+
"seed": 0,
|
| 6 |
+
"attn": "flex",
|
| 7 |
+
"compile": true,
|
| 8 |
+
"seq_len": 8192,
|
| 9 |
+
"micro_batch": 4,
|
| 10 |
+
"grad_accum": 2,
|
| 11 |
+
"lr": 0.0001,
|
| 12 |
+
"wd": 0.1,
|
| 13 |
+
"warmup": 200,
|
| 14 |
+
"total_steps": 6000,
|
| 15 |
+
"decay_len": 1500,
|
| 16 |
+
"eval_every": 1000000000,
|
| 17 |
+
"eval_n": 256,
|
| 18 |
+
"dev_cer_n_per_L": 60,
|
| 19 |
+
"dev_cer_L_max": 20,
|
| 20 |
+
"stall_window": 8,
|
| 21 |
+
"stall_eps": 0.002,
|
| 22 |
+
"anneal_patience": 3,
|
| 23 |
+
"log_every": 20,
|
| 24 |
+
"lam": 0.1,
|
| 25 |
+
"meta_condition": true,
|
| 26 |
+
"min_len": 20,
|
| 27 |
+
"torso": "best.pt",
|
| 28 |
+
"mix": {
|
| 29 |
+
"iphi": 1.0,
|
| 30 |
+
"iphi_syn": 0.5,
|
| 31 |
+
"papyri": 1.0,
|
| 32 |
+
"iphi_seg": 0.5,
|
| 33 |
+
"pap_seg": 0.3,
|
| 34 |
+
"literary": 0.3,
|
| 35 |
+
"literary_n": 60000
|
| 36 |
+
},
|
| 37 |
+
"noise": {
|
| 38 |
+
"w_span": 0.45,
|
| 39 |
+
"w_word": 0.15,
|
| 40 |
+
"w_elastic": 0.05,
|
| 41 |
+
"w_iid": 0.15,
|
| 42 |
+
"w_halfword": 0.15,
|
| 43 |
+
"w_substitute": 0.05,
|
| 44 |
+
"span_mean": 4.0,
|
| 45 |
+
"span_max": 12,
|
| 46 |
+
"p_region_none": 0.3,
|
| 47 |
+
"p_century_none": 0.3
|
| 48 |
+
},
|
| 49 |
+
"dev_exclude": "",
|
| 50 |
+
"out_dir": "whole_v4_t4v5",
|
| 51 |
+
"save_every": 0,
|
| 52 |
+
"_source_checkpoint": "final.pt",
|
| 53 |
+
"_source_step": 6000
|
| 54 |
+
}
|