| import math |
| import torch |
| import torch.nn as nn |
| try: |
| from transformers import PreTrainedModel |
| from .configuration_socrate import SocrateConfig |
| HAS_TRANSFORMERS = True |
| except ImportError: |
| class PreTrainedModel(nn.Module): |
| def __init__(self, config): |
| super().__init__() |
| self.config = config |
| HAS_TRANSFORMERS = False |
|
|
| class PositionalEncoding(nn.Module): |
| def __init__(self,d_model,max_len): |
| super().__init__() |
| pe = torch.zeros(max_len,d_model) |
|
|
| position = torch.arange(0,max_len,dtype=torch.float32).unsqueeze(1) |
| |
| div_term = torch.exp( |
| torch.arange(0,d_model,2,dtype=torch.float32 ) * (-1) * math.log(10000)/d_model |
| ) |
|
|
| pe[:,::2] = torch.sin(div_term * position) |
| pe[:,1::2] = torch.cos(div_term * position) |
| pe = pe.unsqueeze(0) |
|
|
| self.register_buffer("pe",pe) |
| |
| def forward(self,x): |
| return x + self.pe[:,:x.size(1),:] |
|
|
| class ResidualBlock(nn.Module): |
| def __init__(self,in_,out_,stride_=1): |
| super().__init__() |
| self.conv1 = nn.Sequential( |
| nn.Conv2d(in_,out_,kernel_size=3,stride=stride_,padding=1), |
| nn.BatchNorm2d(out_), |
| nn.ReLU(), |
| ) |
|
|
| self.relu = nn.ReLU() |
| self.conv2 = nn.Sequential( |
| nn.Conv2d(out_,out_,kernel_size=3,stride=1,padding=1), |
| nn.BatchNorm2d(out_), |
| ) |
|
|
| self.id = nn.Identity() |
| if in_ != out_ or stride_ != 1: |
| self.id = nn.Sequential( |
| nn.Conv2d(in_,out_,kernel_size=1,stride=stride_), |
| nn.BatchNorm2d(out_), |
| ) |
| def forward(self,x): |
| identity = self.id(x) |
| x = self.conv1(x) |
| x = self.conv2(x) |
| x = x + identity |
| x = self.relu(x) |
| return x |
|
|
| class SocratePool(nn.Module): |
| """ |
| SocratePool ensures that the feature map's height is always compressed to a fixed dimension, |
| while the width dynamically adapts based on the input sequence. |
| target_height controls the vertical resolution after pooling. |
| """ |
| def __init__(self, target_height=4): |
| super().__init__() |
| self.target_height = target_height |
| self.pool = nn.AdaptiveMaxPool2d((target_height, None)) |
| |
| def forward(self, x): |
| return self.pool(x) |
|
|
| class SOCRATE(PreTrainedModel): |
| if HAS_TRANSFORMERS: |
| config_class = SocrateConfig |
| else: |
| config_class = None |
|
|
| def __init__(self, config, tokenizer=None, sx_config=None): |
| super().__init__(config) |
| self.tokenizer = tokenizer |
| |
| self.sx_config = sx_config |
| |
| |
| if tokenizer is not None: |
| self.vocab_size = tokenizer.get_vocab_size() |
| self.pad_id = tokenizer.token_to_id("<pad>") |
| self.bos_id = tokenizer.token_to_id("<bos>") |
| self.eos_id = tokenizer.token_to_id("<eos>") |
| else: |
| |
| self.vocab_size = config.vocab_size |
| self.pad_id = config.pad_id |
| self.bos_id = config.bos_id |
| self.eos_id = config.eos_id |
| |
| self.d_model = config.d_model |
| |
| |
| _pool_h = getattr(sx_config, "pool_height", None) or getattr(config, "pool_height", 4) |
| |
| self.convolution = nn.Sequential( |
| ResidualBlock(3, 32, 2), |
| ResidualBlock(32, 64), |
| ResidualBlock(64, 128, 2), |
| ResidualBlock(128, 256), |
| ResidualBlock(256, self.d_model, 2), |
| ) |
| self.project = nn.Sequential( |
| nn.Linear(self.d_model * _pool_h, self.d_model), |
| nn.LayerNorm(self.d_model), |
| ) |
| self.pool = SocratePool(target_height=_pool_h) |
| self.pe = PositionalEncoding(self.d_model, config.max_len) |
| self.norm_image = nn.LayerNorm(self.d_model) |
| self.embedding = nn.Embedding(self.vocab_size, self.d_model, padding_idx=self.pad_id) |
|
|
| encoder_layer = nn.TransformerEncoderLayer( |
| d_model=self.d_model, nhead=config.nhead, dim_feedforward=config.dim_feedforward, |
| batch_first=True, norm_first=config.norm_first, activation=config.activation |
| ) |
| self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=config.num_layers) |
|
|
| decoder_layer = nn.TransformerDecoderLayer( |
| d_model=self.d_model, nhead=config.nhead, dim_feedforward=config.dim_feedforward, |
| batch_first=True, norm_first=config.norm_first, activation=config.activation |
| ) |
| self.decoder = nn.TransformerDecoder(decoder_layer, num_layers=config.num_layers) |
|
|
| self.output = nn.Linear(self.d_model, self.vocab_size) |
| self.last_norm = nn.LayerNorm(self.d_model) |
|
|
| def encode(self, image): |
| """Separate encode method (custom requirement)""" |
| image = self.convolution(image) |
| image = self.pool(image) |
| B, C, H, W = image.shape |
| image = image.permute(0, 3, 1, 2).contiguous() |
| image = image.view(B, W, C * H) |
| image = self.project(image) |
| image = self.pe(image) |
| image = self.encoder(image) |
| return image |
|
|
| def decode(self, memory_image, text): |
| """Separate decode method (custom requirement)""" |
| tgt_key_padding_mask = (text == self.pad_id) |
| tgt_mask = (nn.Transformer.generate_square_subsequent_mask(text.size(1), device=text.device) != 0.0) |
| |
| text_emb = self.embedding(text) |
| text_emb = self.pe(text_emb * math.sqrt(self.d_model)) |
| |
| output = self.decoder(text_emb, memory_image, tgt_key_padding_mask=tgt_key_padding_mask, tgt_mask=tgt_mask) |
| output = self.last_norm(output) |
| return self.output(output) |
|
|
| def forward(self, image, text): |
| memory_image = self.encode(image) |
| return self.decode(memory_image, text) |
|
|
| |
| |
| |
| |
| def fit(self, dataloader, optimizer, criterion, scheduler=None, scaler=None, device="cuda", best_loss=float('inf'), epochs=1, save_dir=".", save_name="socrate", save_interval=100): |
| """ |
| Trains the model for the given number of epochs on the given dataloader. |
| Returns the best_loss. |
| """ |
| from .trainer import Trainer |
| trainer = Trainer(self, optimizer, scheduler, criterion, device, scaler, save_dir, save_name, save_interval) |
| for e in range(1, epochs + 1): |
| best_loss = trainer.train_epoch(dataloader, best_loss, epoch_num=e, total_epochs=epochs) |
| return best_loss |
|
|
| def predict(self, image_paths, tokenizer=None, wpb=16, function="generate", doctr_model=None, |
| bos_id=None, eos_id=None, device="cuda", |
| |
| temp=0.5, max_iter=64, penalty=1.15, top_k=5, |
| |
| fast_max_iter=32, |
| |
| beam_width=4, beam_max_iter=64): |
| """ |
| Performs end-to-end inference on images. |
| |
| Args: |
| image_paths (str | List[str]): Path(s) to input images. |
| tokenizer: Optional tokenizer override. |
| wpb (int): Words per batch. Default: 16. |
| function (str): 'generate', 'generate_fast', 'beam_search', or a custom callable. |
| doctr_model: Pre-loaded doctr detection model (avoids reloading). |
| bos_id (int): Override BOS token ID. |
| eos_id (int): Override EOS token ID. |
| device (str): Target device. Default: 'cuda'. |
| temp (float): Temperature for generate(). Default: 0.5. |
| max_iter (int): Max tokens for generate(). Default: 64. |
| penalty (float): Repetition penalty for generate(). Default: 1.15. |
| top_k (int): Top-k candidates per step in generate(). Default: 5. |
| fast_max_iter (int): Max tokens for generate_fast(). Default: 32. |
| beam_width (int): Number of beams for beam_search(). Default: 4. |
| beam_max_iter (int): Max tokens per beam for beam_search(). Default: 64. |
| """ |
| from .inference import predict as inf_predict |
| tk = tokenizer if tokenizer is not None else self.tokenizer |
|
|
| return inf_predict( |
| self, tk, image_paths, wpb, function, doctr_model, bos_id, eos_id, device, |
| temp=temp, max_iter=max_iter, penalty=penalty, top_k=top_k, |
| fast_max_iter=fast_max_iter, |
| beam_width=beam_width, beam_max_iter=beam_max_iter |
| ) |
|
|
| def load_parameters(self, path, strict=False): |
| """ |
| Loads state dict from path with strict fallback. |
| Returns the best_loss if it was saved, otherwise float('inf'). |
| """ |
| import os |
| import torch |
| if not os.path.exists(path): |
| print(f"Warning: The weights file {path} was not found.") |
| return float('inf') |
| |
| try: |
| checkpoint = torch.load(path, map_location=next(self.parameters()).device, weights_only=False) |
| if "model" in checkpoint: |
| state_dict = checkpoint["model"] |
| best_loss = checkpoint.get("best_loss", float('inf')) |
| else: |
| state_dict = checkpoint |
| best_loss = float('inf') |
| |
| self.load_state_dict(state_dict, strict=strict) |
| return best_loss |
| except Exception as e: |
| print(f"Warning: Could not load parameters completely due to: {e}") |
| return float('inf') |
|
|
| def load(self, path, strict=False): |
| """ |
| Alias for load_parameters. |
| """ |
| return self.load_parameters(path, strict=strict) |
|
|
| def make_dataset(self, images, labels=None, transform=None, height=None, max_length=None): |
| """ |
| Creates a Makeset object for this model. |
| height and max_length default to values from sx.Config if provided during init, |
| otherwise fall back to 32 and 64 respectively. |
| """ |
| from .dataset import Makeset |
| sx_cfg = getattr(self, "sx_config", None) |
| _height = height if height is not None else (sx_cfg.height if sx_cfg else 32) |
| _max_length = max_length if max_length is not None else (sx_cfg.max_length if sx_cfg else 64) |
| return Makeset( |
| images=images, |
| labels=labels, |
| transform=transform, |
| tokenizer=self.tokenizer, |
| pad_id=self.pad_id, |
| bos_id=self.bos_id, |
| eos_id=self.eos_id, |
| height=_height, |
| max_length=_max_length |
| ) |
|
|
| def freeze_encoder(self): |
| """ |
| Freezes the encoder weights (CNN + TransformerEncoder). |
| Useful if you want to fine-tune only the decoder. |
| """ |
| for param in self.convolution.parameters(): |
| param.requires_grad = False |
| for param in self.project.parameters(): |
| param.requires_grad = False |
| for param in self.encoder.parameters(): |
| param.requires_grad = False |
| print("Encoder (CNN + TransformerEncoder) has been frozen.") |
|
|
| def unfreeze_encoder(self): |
| """ |
| Unfreezes the encoder weights. |
| """ |
| for param in self.convolution.parameters(): |
| param.requires_grad = True |
| for param in self.project.parameters(): |
| param.requires_grad = True |
| for param in self.encoder.parameters(): |
| param.requires_grad = True |
| print("Encoder has been unfrozen.") |
| |
| |
| |
| |
|
|
| @staticmethod |
| def create_config( |
| d_model=256, |
| nhead=4, |
| num_layers=4, |
| dim_feedforward=1024, |
| activation="gelu", |
| norm_first=True, |
| max_len=512, |
| pool_height=4, |
| ): |
| """ |
| Create a custom architecture config without needing to import SocrateX separately. |
| |
| Example:: |
| |
| model = AutoModel.from_pretrained("ihatebaselines/Socrate", trust_remote_code=True) |
| cfg = model.create_config(d_model=512, nhead=8, num_layers=6, dim_feedforward=2048) |
| tok = model.make_tokenizer() |
| new_model = model.new(config=cfg, tokenizer=tok) |
| """ |
| try: |
| from .configuration_socrate import SocrateConfig |
| except ImportError: |
| try: |
| from configuration_socrate import SocrateConfig |
| except ImportError: |
| from SocrateX.configuration_socrate import SocrateConfig |
| return SocrateConfig( |
| d_model=d_model, |
| nhead=nhead, |
| num_layers=num_layers, |
| dim_feedforward=dim_feedforward, |
| activation=activation, |
| norm_first=norm_first, |
| max_len=max_len, |
| pool_height=pool_height, |
| ) |
|
|
| @staticmethod |
| def make_tokenizer(path=None): |
| """ |
| Initialize a fresh BPE tokenizer from scratch, or load one from a file. |
| |
| Example:: |
| |
| tok = model.make_tokenizer() # fresh tokenizer |
| tok = model.make_tokenizer("ocr_bpe_tokenizer.json") # load from file |
| """ |
| if path is not None: |
| from tokenizers import Tokenizer |
| return Tokenizer.from_file(path) |
| try: |
| from .tokenizer import init_tokenizer |
| except ImportError: |
| from SocrateX.tokenizer import init_tokenizer |
| return init_tokenizer() |
|
|
| @classmethod |
| def new(cls, config, tokenizer, device="cuda"): |
| """ |
| Build a brand-new SOCRATE model from a config + tokenizer. |
| No pretrained weights — starts from scratch. |
| |
| Example:: |
| |
| cfg = model.create_config(d_model=256, nhead=4, num_layers=4, dim_feedforward=1024) |
| tok = model.make_tokenizer() |
| my_model = model.new(config=cfg, tokenizer=tok, device="cpu") |
| print(my_model.summary()) |
| """ |
| import torch |
| hf_config = cls.create_config( |
| d_model=config.d_model if hasattr(config, 'd_model') else 256, |
| nhead=config.nhead if hasattr(config, 'nhead') else 4, |
| num_layers=config.num_layers if hasattr(config, 'num_layers') else 4, |
| dim_feedforward=config.dim_feedforward if hasattr(config, 'dim_feedforward') else 1024, |
| ) |
| hf_config.vocab_size = tokenizer.get_vocab_size() |
| hf_config.pad_id = tokenizer.token_to_id("<pad>") |
| hf_config.bos_id = tokenizer.token_to_id("<bos>") |
| hf_config.eos_id = tokenizer.token_to_id("<eos>") |
| return cls(hf_config, tokenizer=tokenizer, sx_config=config).to(device) |
|
|
| def make_trainer(self, dataloader, optimizer, criterion, device=None): |
| """ |
| Returns a Trainer object wired to this model. |
| |
| Example:: |
| |
| loader = model.make_dataset(images, labels).to_loader(batch_size=16) |
| opt = torch.optim.AdamW(model.parameters(), lr=1e-4) |
| crit = torch.nn.CrossEntropyLoss() |
| trainer = model.make_trainer(loader, opt, crit) |
| for epoch in range(50): |
| loss = trainer.train_epoch() |
| """ |
| try: |
| from .trainer import Trainer |
| except ImportError: |
| from SocrateX.trainer import Trainer |
| _device = device or ("cuda" if __import__("torch").cuda.is_available() else "cpu") |
| return Trainer(self, dataloader, optimizer, criterion, device=_device) |
|
|
| def generate_data(self, source, count=1000, output_dir="silly_train", mode="train"): |
| """ |
| Generate a quick synthetic dataset directly from the model object. |
| |
| Args: |
| source: URL or file path with words to render |
| count: number of images to generate |
| output_dir: folder to save images + labels.csv |
| mode: 'train' or 'test' |
| |
| Example:: |
| |
| model.generate_data( |
| source="https://raw.githubusercontent.com/.../google-10000-english.txt", |
| count=500, |
| output_dir="my_data", |
| mode="train" |
| ) |
| """ |
| try: |
| from .synthetic import generate_silly_training_set, generate_silly_testing_set |
| except ImportError: |
| from SocrateX.synthetic import generate_silly_training_set, generate_silly_testing_set |
| if mode == "train": |
| return generate_silly_training_set(source=source, count=count, output_dir=output_dir) |
| else: |
| return generate_silly_testing_set(source=source, count=count, output_dir=output_dir) |
|
|
| def load_data(self, path): |
| """ |
| Load a dataset from a CSV / JSON / TXT file. |
| Returns (images, labels). |
| |
| Example:: |
| |
| images, labels = model.load_data("label.csv") |
| dataset = model.make_dataset(images, labels) |
| """ |
| try: |
| from .dataset import load_dataset |
| except ImportError: |
| from SocrateX.dataset import load_dataset |
| return load_dataset(path) |
|
|
| |
|
|
| def cat(tokenizer, weights=None, device="cuda"): |
| """ |
| SOCRATE Cat - The original full-sized model. |
| """ |
| if HAS_TRANSFORMERS: |
| config = SocrateConfig( |
| d_model=640, |
| max_len=512, |
| nhead=10, |
| dim_feedforward=2560, |
| activation="gelu", |
| norm_first=True, |
| num_layers=12, |
| vocab_size=tokenizer.get_vocab_size() if tokenizer else 1000, |
| pad_id=tokenizer.token_to_id("<pad>") if tokenizer else 0, |
| bos_id=tokenizer.token_to_id("<bos>") if tokenizer else 1, |
| eos_id=tokenizer.token_to_id("<eos>") if tokenizer else 2, |
| ) |
| else: |
| |
| class DummyConfig: pass |
| config = DummyConfig() |
| config.d_model = 640 |
| config.max_len = 512 |
| config.nhead = 10 |
| config.dim_feedforward = 2560 |
| config.activation = "gelu" |
| config.norm_first = True |
| config.num_layers = 12 |
|
|
| model = SOCRATE(config, tokenizer=tokenizer).to(device) |
| |
| if weights: |
| if weights.startswith("http://") or weights.startswith("https://"): |
| import torch.hub |
| checkpoint = torch.hub.load_state_dict_from_url(weights, map_location=device) |
| else: |
| import os |
| if os.path.exists(weights): |
| checkpoint = torch.load(weights, map_location=device, weights_only=False) |
| else: |
| print(f"Warning: The weights file {weights} was not found locally.") |
| checkpoint = None |
|
|
| if checkpoint is not None: |
| if "model" in checkpoint: |
| model.load_state_dict(checkpoint["model"]) |
| else: |
| model.load_state_dict(checkpoint) |
| |
| return model |
|
|
| cat.pretrained = "best_socrate_1.3.pt" |
|
|
| def rat(tokenizer, weights=None, device="cuda"): |
| """ |
| SOCRATE Rat - The medium-sized variant. |
| """ |
| if HAS_TRANSFORMERS: |
| config = SocrateConfig( |
| d_model=512, |
| max_len=512, |
| nhead=8, |
| dim_feedforward=2048, |
| activation="gelu", |
| norm_first=True, |
| num_layers=8, |
| vocab_size=tokenizer.get_vocab_size() if tokenizer else 1000, |
| pad_id=tokenizer.token_to_id("<pad>") if tokenizer else 0, |
| bos_id=tokenizer.token_to_id("<bos>") if tokenizer else 1, |
| eos_id=tokenizer.token_to_id("<eos>") if tokenizer else 2, |
| ) |
| else: |
| class DummyConfig: pass |
| config = DummyConfig() |
| config.d_model = 512 |
| config.max_len = 512 |
| config.nhead = 8 |
| config.dim_feedforward = 2048 |
| config.activation = "gelu" |
| config.norm_first = True |
| config.num_layers = 8 |
|
|
| model = SOCRATE(config, tokenizer=tokenizer).to(device) |
| |
| if weights: |
| if weights.startswith("http://") or weights.startswith("https://"): |
| import torch.hub |
| checkpoint = torch.hub.load_state_dict_from_url(weights, map_location=device) |
| else: |
| import os |
| if os.path.exists(weights): |
| checkpoint = torch.load(weights, map_location=device, weights_only=False) |
| else: |
| print(f"Warning: The weights file {weights} was not found locally.") |
| checkpoint = None |
|
|
| if checkpoint is not None: |
| if "model" in checkpoint: |
| model.load_state_dict(checkpoint["model"]) |
| else: |
| model.load_state_dict(checkpoint) |
| |
| return model |
|
|
| rat.pretrained = None |
|
|
| def mice(tokenizer, weights=None, device="cuda"): |
| """ |
| SOCRATE Mice - The tiny variant for performance and edge devices. |
| """ |
| if HAS_TRANSFORMERS: |
| config = SocrateConfig( |
| d_model=256, |
| max_len=512, |
| nhead=4, |
| dim_feedforward=1024, |
| activation="gelu", |
| norm_first=True, |
| num_layers=4, |
| vocab_size=tokenizer.get_vocab_size() if tokenizer else 1000, |
| pad_id=tokenizer.token_to_id("<pad>") if tokenizer else 0, |
| bos_id=tokenizer.token_to_id("<bos>") if tokenizer else 1, |
| eos_id=tokenizer.token_to_id("<eos>") if tokenizer else 2, |
| ) |
| else: |
| class DummyConfig: pass |
| config = DummyConfig() |
| config.d_model = 256 |
| config.max_len = 512 |
| config.nhead = 4 |
| config.dim_feedforward = 1024 |
| config.activation = "gelu" |
| config.norm_first = True |
| config.num_layers = 4 |
|
|
| model = SOCRATE(config, tokenizer=tokenizer).to(device) |
| |
| if weights: |
| if weights.startswith("http://") or weights.startswith("https://"): |
| import torch.hub |
| checkpoint = torch.hub.load_state_dict_from_url(weights, map_location=device) |
| else: |
| import os |
| if os.path.exists(weights): |
| checkpoint = torch.load(weights, map_location=device, weights_only=False) |
| else: |
| print(f"Warning: The weights file {weights} was not found locally.") |
| checkpoint = None |
|
|
| if checkpoint is not None: |
| if "model" in checkpoint: |
| model.load_state_dict(checkpoint["model"]) |
| else: |
| model.load_state_dict(checkpoint) |
| |
| return model |
|
|
| mice.pretrained = None |
|
|