Download tinydoc/models/recognizer.py from harryfrz/tinydoc-5m: direct link, hf CLI and curl.
- Browser
- Download file 2.82 kB
-
https://huggingface.co/harryfrz/tinydoc-5m/resolve/main/tinydoc/models/recognizer.py
- Command line
-
hf download hf://harryfrz/tinydoc-5m/tinydoc/models/recognizer.py
-
curl -L -o recognizer.py https://huggingface.co/harryfrz/tinydoc-5m/resolve/main/tinydoc/models/recognizer.py
2.82 kB
| """Text-line recognizer: CNN feature extractor -> Transformer encoder -> CTC head. | |
| Input is a grayscale line crop of height 32 and variable width. The CNN collapses height to 1 and | |
| downsamples width by 4, so each output step covers 4 input columns. Position comes from a depthwise | |
| convolution (relative, translation-equivariant) rather than an absolute table, so lines longer than | |
| any seen in training are handled the same way as short ones. | |
| """ | |
| import torch | |
| from torch import nn | |
| from ..charset import NUM_CLASSES | |
| from .common import ConvBNAct, TransformerBlock, padding_bias | |
| LINE_HEIGHT = 32 | |
| WIDTH_STRIDE = 4 | |
| X_STRETCH = 1.25 # widen crops so condensed fonts still give CTC >= 1 step per character (+ blanks) | |
| def _separable(cin, cout): | |
| """Depthwise 3x3 then pointwise 1x1: about a ninth of a full 3x3 convolution's arithmetic.""" | |
| return nn.Sequential(ConvBNAct(cin, cin, groups=cin), ConvBNAct(cin, cout, k=1)) | |
| class Recognizer(nn.Module): | |
| """cnn="full" (v1-v4) or "separable": the same shape of front-end with depthwise-separable convolutions, | |
| measured 2.5x (4 threads) to 3x (1 thread) faster per page on CPU, where the full CNN is ~70% of the time.""" | |
| def __init__(self, num_classes=NUM_CLASSES, d=192, layers=5, heads=6, ff=672, dropout=0.1, cnn="full"): | |
| super().__init__() | |
| conv = ConvBNAct if cnn == "full" else _separable | |
| self.cnn = nn.Sequential( | |
| ConvBNAct(1, 32), nn.MaxPool2d(2, 2), # 16 x W/2 | |
| conv(32, 64), nn.MaxPool2d(2, 2), # 8 x W/4 | |
| conv(64, 128), conv(128, 128), nn.MaxPool2d((2, 1), (2, 1)), # 4 x W/4 | |
| conv(128, 192), conv(192, 192), nn.MaxPool2d((2, 1), (2, 1)), # 2 x W/4 | |
| ConvBNAct(192, 256, k=(2, 3), p=(0, 1)), # 1 x W/4 | |
| ) | |
| self.proj = nn.Linear(256, d) | |
| self.pos = nn.Conv1d(d, d, 7, padding=3, groups=d) | |
| self.blocks = nn.ModuleList(TransformerBlock(d, heads, ff, dropout) for _ in range(layers)) | |
| self.ln = nn.LayerNorm(d) | |
| self.head = nn.Linear(d, num_classes) | |
| def forward(self, images, step_mask=None): | |
| """images: (B, 1, 32, W) in [0, 1], ink dark. step_mask: optional (B, W/4) bool, True = padding. | |
| Returns logits (B, T, C) with T = W / 4. | |
| """ | |
| f = self.cnn(images - 0.5).squeeze(2).transpose(1, 2) # (B, T, 256) | |
| x = self.proj(f) | |
| if step_mask is not None: # keep padding from leaking into real steps through the conv | |
| x = x.masked_fill(step_mask[..., None], 0.0) | |
| x = x + self.pos(x.transpose(1, 2)).transpose(1, 2) | |
| bias = None if step_mask is None else padding_bias(step_mask, x.dtype) | |
| for blk in self.blocks: | |
| x = blk(x, bias) | |
| return self.head(self.ln(x)) | |