import torch import torch.nn as nn import gradio as gr import os import pickle class LayerNorm(nn.Module): def __init__(self, emb_dim): super().__init__() self.eps = 1e-5 self.scale = nn.Parameter(torch.ones(emb_dim)) self.shift = nn.Parameter(torch.zeros(emb_dim)) def forward(self, x): mean = x.mean(dim=-1, keepdim=True) var = x.var(dim=-1, keepdim=True) norm_x = (x - mean) / torch.sqrt(var + self.eps) return self.scale * norm_x + self.shift class GELU(nn.Module): def __init__(self): super().__init__() def forward(self, x): return 0.5 * x * (1 + torch.tanh(torch.sqrt(torch.tensor(2.0 / torch.pi)) * (x + 0.044715 * torch.pow(x, 3)))) class MultiHeadAttention(nn.Module): def __init__(self, d_in, d_out, context_length, dropout, num_head, qkv_bias=False): super().__init__() assert (d_out % num_head == 0) self.d_out = d_out self.num_head = num_head self.head_dim = d_out // num_head self.W_query = torch.nn.Linear(d_in, d_out, bias=qkv_bias) self.W_key = torch.nn.Linear(d_in, d_out, bias=qkv_bias) self.W_value = torch.nn.Linear(d_in, d_out, bias=qkv_bias) self.out_proj = torch.nn.Linear(d_out, d_out) self.dropout = torch.nn.Dropout(dropout) self.register_buffer("mask", torch.triu(torch.ones(context_length, context_length), diagonal=1)) def forward(self, x): b, num_tokens, d_in = x.shape keys = self.W_key(x) queries = self.W_query(x) values = self.W_value(x) keys = keys.view(b, num_tokens, self.num_head, self.head_dim) values = values.view(b, num_tokens, self.num_head, self.head_dim) queries = queries.view(b, num_tokens, self.num_head, self.head_dim) keys = keys.transpose(1, 2) values = values.transpose(1, 2) queries = queries.transpose(1, 2) attn_score = queries @ keys.transpose(2, 3) mask_bool = self.mask.to(torch.bool)[:num_tokens, :num_tokens] attn_score.masked_fill_(mask_bool, -torch.inf) attn_weight = torch.softmax(attn_score / keys.shape[-1] ** 0.5, dim=-1) attn_weight = self.dropout(attn_weight) context_vector = (attn_weight @ values).transpose(1, 2) context_vector = context_vector.contiguous().view(b, num_tokens, self.d_out) context_vector = self.out_proj(context_vector) return context_vector class FeedForward(nn.Module): def __init__(self, cfg): super().__init__() self.layers = nn.Sequential( nn.Linear(cfg["emb_dim"], 4 * cfg["emb_dim"]), GELU(), nn.Linear(4 * cfg["emb_dim"], cfg["emb_dim"]) ) def forward(self, x): return self.layers(x) class TransformerBlock(nn.Module): def __init__(self, cfg): super().__init__() self.att = MultiHeadAttention( d_in=cfg["emb_dim"], d_out=cfg["emb_dim"], context_length=cfg["context_length"], num_head=cfg["n_heads"], dropout=cfg.get("drop_rate", 0.0), qkv_bias=cfg.get("qkv_bias", False) ) self.ff = FeedForward(cfg) self.norm1 = LayerNorm(cfg["emb_dim"]) self.norm2 = LayerNorm(cfg["emb_dim"]) self.drop_shortcut = nn.Dropout(cfg.get("drop_rate", 0.0)) def forward(self, x): shortcut = x x = self.norm1(x) x = self.att(x) x = self.drop_shortcut(x) x = x + shortcut shortcut = x x = self.norm2(x) x = self.ff(x) x = self.drop_shortcut(x) x = x + shortcut return x class GPTModel(nn.Module): def __init__(self, cfg): super().__init__() self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"]) self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"]) self.drop_emb = nn.Dropout(cfg.get("drop_rate", 0.0)) self.trf_blocks = nn.Sequential( *[TransformerBlock(cfg) for _ in range(cfg["n_layers"]) ] ) self.final_norm = LayerNorm(cfg["emb_dim"]) self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False) def forward(self, in_idx): batch_size, seq_len = in_idx.shape tok_embeds = self.tok_emb(in_idx) pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device)) x = tok_embeds + pos_embeds x = self.drop_emb(x) x = self.trf_blocks(x) x = self.final_norm(x) logits = self.out_head(x) return logits model_path = "review_classifier_model.pth" if not os.path.exists(model_path): raise FileNotFoundError(f"{model_path} not found. Please check the path.") try: loaded_full = None safe_ctx = getattr(torch.serialization, "safe_globals", None) add_safe = getattr(torch.serialization, "add_safe_globals", None) if safe_ctx is not None: try: with torch.serialization.safe_globals([GPTModel]): loaded_full = torch.load(model_path, map_location=torch.device("cpu"), weights_only=False) except Exception: loaded_full = None elif add_safe is not None: try: # older helper: register globally then load torch.serialization.add_safe_globals([GPTModel]) loaded_full = torch.load(model_path, map_location=torch.device("cpu"), weights_only=False) except Exception: loaded_full = None else: # If neither helper exists, try loading with weights_only=False (may execute code during unpickle). try: loaded_full = torch.load(model_path, map_location=torch.device("cpu"), weights_only=False) except Exception: loaded_full = None if loaded_full is not None and hasattr(loaded_full, "state_dict") and not isinstance(loaded_full, dict): model = loaded_full print(f"Loaded full model object from {model_path}") else: state = None try: state = torch.load(model_path, map_location=torch.device("cpu"), weights_only=True) except Exception: try: if safe_ctx is not None: with torch.serialization.safe_globals([GPTModel]): tmp = torch.load(model_path, map_location=torch.device("cpu"), weights_only=False) elif add_safe is not None: torch.serialization.add_safe_globals([GPTModel]) tmp = torch.load(model_path, map_location=torch.device("cpu"), weights_only=False) else: tmp = torch.load(model_path, map_location=torch.device("cpu"), weights_only=False) if hasattr(tmp, "state_dict"): state = tmp.state_dict() else: state = tmp except Exception as e: raise RuntimeError(f"Unable to load checkpoint as full model or weights-only. Last error: {e}") if isinstance(state, dict): print("Attempting to load checkpoint state into a GPTModel instance...") BASE_CONFIG = { "vocab_size": 50257, "context_length": 1024, "drop_rate": 0.0, "qkv_bias": True, "emb_dim": 768, "n_layers": 12, "n_heads": 12, } model = GPTModel(BASE_CONFIG) if "model_state_dict" in state: state_dict = state["model_state_dict"] elif "state_dict" in state: state_dict = state["state_dict"] else: state_dict = state model.load_state_dict(state_dict, strict=False) print("Loaded state_dict into GPTModel instance (non-strict).") else: raise RuntimeError("Unrecognized checkpoint format and unable to construct model from checkpoint.") except Exception as e: raise RuntimeError(f"Failed to load model checkpoint: {e}") device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model.to(device) model.eval() print(f"Model loaded and moved to {device}") tokenizer_path = "tokenizer.pkl" if os.path.exists(tokenizer_path): with open(tokenizer_path, "rb") as f: tokenizer = pickle.load(f) print(f"Tokenizer loaded from {tokenizer_path}") else: raise FileNotFoundError(f"{tokenizer_path} not found. Please check the path.") MAX_SEQUENCE_LENGTH = 120 def classify_review(text, model, tokenizer_obj, device, max_length=MAX_SEQUENCE_LENGTH, pad_token_id=50256): model.eval() input_ids = tokenizer_obj.encode(text) input_ids = input_ids[:max_length] + [pad_token_id] * (max_length - len(input_ids)) input_tensor = torch.tensor(input_ids, device=device).unsqueeze(0) with torch.no_grad(): logits = model(input_tensor)[:, -1, :] predicted_label = torch.argmax(logits, dim=-1).item() return "spam" if predicted_label == 1 else "not spam" def chatbot_classify(message, history): result = classify_review( message, model, tokenizer, device, max_length=MAX_SEQUENCE_LENGTH ) return result print("Launching Gradio interface...") iface = gr.ChatInterface( chatbot_classify, title="📬 Spam Detection System", description="Enter an SMS message below...", theme="compact", css=""" /* Customize chat bubble colors */ .chatbot-message { background-color: #e0f7fa; /* Light cyan */ color: #006064; /* Dark teal text */ font-weight: 600; border-radius: 12px; padding: 12px; } .user-message { background-color: #c8e6c9; /* Light green */ color: #1b5e20; /* Dark green text */ font-weight: 600; border-radius: 12px; padding: 12px; } .chat-ending-message { font-style: italic; color: #555; } """, ) ICON_CDN = "https://img.icons8.com/color/48/mail-envelope.png" custom_head_html = f""" """ iface.launch( share=True, favicon_path=ICON_CDN )