""" Custom TextCNN model made Hugging Face Hub compatible. Architecture inferred from checkpoint weights: - Embedding: (30522 vocab, 140 dim) - 3 parallel Conv1d branches: kernel sizes 3, 4, 5 — each with 100 output channels - Final FC layer: (300 -> 1) [300 = 100*3 concatenated conv outputs] """ import torch import torch.nn as nn import torch.nn.functional as F from huggingface_hub import PyTorchModelHubMixin class TextCNNConfig: def __init__( self, vocab_size: int = 30522, embed_dim: int = 140, num_filters: int = 100, kernel_sizes: list = [3, 4, 5], num_classes: int = 1, **kwargs, ): self.vocab_size = vocab_size self.embed_dim = embed_dim self.num_filters = num_filters self.kernel_sizes = kernel_sizes self.num_classes = num_classes class TextCNN(nn.Module, PyTorchModelHubMixin): """ TextCNN for MCQ scoring, pushable to / loadable from the Hugging Face Hub. Usage: # Save + push model = TextCNN(config) model.load_state_dict(torch.load("original_state_dict.pt")) model.push_to_hub("your-username/cnn-mcq-model") # Load back model = TextCNN.from_pretrained("your-username/cnn-mcq-model") """ def __init__(self, config: dict = None): super().__init__() if config is None: config = {} cfg = TextCNNConfig(**config) self.config = config if config else cfg.__dict__ self.embedding = nn.Embedding(cfg.vocab_size, cfg.embed_dim) self.convs = nn.ModuleList([ nn.Conv1d(in_channels=cfg.embed_dim, out_channels=cfg.num_filters, kernel_size=k) for k in cfg.kernel_sizes ]) self.fc = nn.Linear(cfg.num_filters * len(cfg.kernel_sizes), cfg.num_classes) def forward(self, input_ids): # input_ids: (batch, seq_len) x = self.embedding(input_ids) # (batch, seq_len, embed_dim) x = x.permute(0, 2, 1) # (batch, embed_dim, seq_len) conv_outs = [F.relu(conv(x)) for conv in self.convs] pooled = [F.max_pool1d(c, c.size(2)).squeeze(2) for c in conv_outs] cat = torch.cat(pooled, dim=1) # (batch, num_filters * len(kernel_sizes)) out = self.fc(cat) # (batch, num_classes) return out