File size: 2,820 Bytes
4dce182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e24bf15
 
 
 
 
4dce182
e24bf15
 
 
 
4dce182
e24bf15
4dce182
 
e24bf15
 
 
4dce182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
"""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))