deva-ocr-75m-research / code /shared_character_recognizer.py
voidash's picture
Release 74.84M Nepali Hindi English research OCR with source and native Paddle weights
5f70891 verified
Raw History Blame Contribute Delete
5.77 kB
"""Research recognizer sharing one English encoder across two explicit branches.
The character branch applies saved low-rank corrections through functional_call
and supplies precomputed encoder outputs to a decoder-only VED container. No
second learned encoder is registered or saved. Functional parameter overrides
are serial CPU research operations, not a concurrent/native serving interface.
"""
import copy
import hashlib
import json
from pathlib import Path
import torch
from torch import nn
from safetensors.torch import load_file
from transformers import DeiTModel, GenerationConfig, VisionEncoderDecoderConfig, VisionEncoderDecoderModel
def sha(path):
value = hashlib.sha256()
with Path(path).open('rb') as stream:
for block in iter(lambda: stream.read(1024 * 1024), b''): value.update(block)
return value.hexdigest()
def eager(config):
config._attn_implementation = 'eager'
config.encoder._attn_implementation = 'eager'
config.decoder._attn_implementation = 'eager'
return config
def load_english(folder):
config = eager(VisionEncoderDecoderConfig.from_pretrained(folder, local_files_only=True))
model, report = VisionEncoderDecoderModel.from_pretrained(folder, config=config,
encoder=DeiTModel(config.encoder, add_pooling_layer=False), local_files_only=True,
dtype=torch.float32, attn_implementation='eager', output_loading_info=True)
assert not any(report.get(k) for k in ['missing_keys', 'unexpected_keys', 'mismatched_keys', 'error_msgs'])
assert sum(p.numel() for p in model.parameters()) == 61448832
return model.eval().requires_grad_(False)
class PrecomputedOnlyEncoder(nn.Module):
main_input_name = 'pixel_values'
def __init__(self, config):
super().__init__(); self.config = copy.deepcopy(config)
def get_output_embeddings(self):
return None
def forward(self, pixel_values=None, **kwargs):
raise RuntimeError('Character decoding requires the shared encoder outputs')
class Correction(nn.Module):
def __init__(self, spec):
super().__init__(); self.spec = spec
if spec['kind'] == 'factor':
self.left = nn.Parameter(torch.empty(spec['matrix_shape'][0], spec['rank']))
self.right = nn.Parameter(torch.empty(spec['rank'], spec['matrix_shape'][1]))
else:
assert spec['kind'] == 'replacement'
self.value = nn.Parameter(torch.empty(spec['shape']))
def apply(self, base):
if self.spec['kind'] == 'replacement': return self.value
return base + (self.left @ self.right).reshape(self.spec['shape'])
class SharedCharacterRecognizer(nn.Module):
def __init__(self, original, character_decoder, character_projection, character_config,
english_generation, character_generation, specifications):
super().__init__()
self.original = original
cfg = eager(copy.deepcopy(character_config))
self.character = VisionEncoderDecoderModel(config=cfg,
encoder=PrecomputedOnlyEncoder(cfg.encoder), decoder=character_decoder)
if character_projection is not None: self.character.enc_to_dec_proj = character_projection
self.original.generation_config = copy.deepcopy(english_generation)
self.character.generation_config = copy.deepcopy(character_generation)
self.specifications = specifications
self.corrections = nn.ModuleDict({spec['key']: Correction(spec) for spec in specifications})
def generate_pixels(self, pixels, branch, generation):
if self.training: raise ValueError('Explicit eval mode is required')
if branch == 'english': return self.original.generate(pixel_values=pixels, **generation)
if branch != 'character': raise ValueError('Require an explicit English or character branch')
base = self.original.encoder.state_dict()
overrides = {spec['name']: self.corrections[spec['key']].apply(base[spec['name']])
for spec in self.specifications}
output = torch.func.functional_call(self.original.encoder, overrides, (),
dict(pixel_values=pixels, return_dict=True), strict=False)
return self.character.generate(encoder_outputs=output, **generation)
@classmethod
def from_bundle(cls, folder):
folder = Path(folder); manifest = json.loads((folder / 'bundle.json').read_text())
for name, expected in manifest['files_sha256'].items(): assert sha(folder / name) == expected
configs = json.loads((folder / 'model-configs.json').read_text())
english = eager(VisionEncoderDecoderConfig.from_dict(configs['english']))
character = eager(VisionEncoderDecoderConfig.from_dict(configs['character']))
original = VisionEncoderDecoderModel(config=english,
encoder=DeiTModel(english.encoder, add_pooling_layer=False))
private = VisionEncoderDecoderModel(config=character,
encoder=PrecomputedOnlyEncoder(character.encoder))
model = cls(original, private.decoder, getattr(private, 'enc_to_dec_proj', None), character,
GenerationConfig.from_pretrained(folder, config_file_name='english-generation.json', local_files_only=True),
GenerationConfig.from_pretrained(folder, config_file_name='character-generation.json', local_files_only=True),
manifest['corrections'])
result = model.load_state_dict(load_file(str(folder / 'weights.safetensors')), strict=True)
assert not result.missing_keys and not result.unexpected_keys
assert sum(p.numel() for p in model.parameters()) == manifest['parameters'] == 74842752
assert len(model.character.encoder.state_dict()) == 0
return model.eval().requires_grad_(False)