Text Classification
Transformers
Safetensors
English
bert
scam-detection
phishing-detection
cybersecurity
multilingual
Eval Results (legacy)
text-embeddings-inference
Instructions to use aattyy11/scam-nlp-ml with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use aattyy11/scam-nlp-ml with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="aattyy11/scam-nlp-ml")# Load model directly from transformers import AutoTokenizer, AutoModelForSequenceClassification tokenizer = AutoTokenizer.from_pretrained("aattyy11/scam-nlp-ml") model = AutoModelForSequenceClassification.from_pretrained("aattyy11/scam-nlp-ml") - Notebooks
- Google Colab
- Kaggle
| """ | |
| Fine-tune google/muril-base-cased for binary scam classification with HuggingFace Trainer. | |
| TrainingArguments uses ``evaluation_strategy`` (Transformers 4.40); targets: F1 > 0.88, | |
| precision > 0.90, recall > 0.85. If ``eval_loss`` rises while F1 falls, reduce epochs or increase early stopping. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import logging | |
| import sys | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| import torch.nn.functional as F | |
| import evaluate | |
| from datasets import DatasetDict, load_from_disk | |
| from transformers import ( | |
| AutoTokenizer, | |
| AutoModelForSequenceClassification, | |
| TrainingArguments, | |
| Trainer, | |
| EarlyStoppingCallback, | |
| ) | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") | |
| log = logging.getLogger(__name__) | |
| ROOT = Path(__file__).resolve().parent.parent | |
| TOKEN_DIR = ROOT / "data" / "processed" / "tokenized" | |
| SAVED_TOKENIZER_DIR = TOKEN_DIR / "muril_tokenizer" | |
| MODEL_OUT = ROOT / "models" / "muril_scam_v1" | |
| LOGS_DIR = ROOT / "models" / "logs" | |
| MODEL_NAME = "google/muril-base-cased" | |
| NUM_LABELS = 2 | |
| LABEL2ID = {"safe": 0, "scam": 1} | |
| ID2LABEL = {0: "safe", 1: "scam"} | |
| CUSTOM_TOKENS = ["[URL]", "[PHONE]", "[EMAIL]", "[AMOUNT]", "[CODE]", "[AADHAAR]", "[PAN]"] | |
| class TrainConfig: | |
| num_epochs: int = 5 | |
| train_batch_size: int = 16 | |
| eval_batch_size: int = 32 | |
| learning_rate: float = 2e-5 | |
| warmup_ratio: float = 0.1 | |
| weight_decay: float = 0.01 | |
| max_grad_norm: float = 1.0 | |
| seed: int = 42 | |
| fp16: bool = False | |
| early_stopping_patience: int = 2 | |
| logging_steps: int = 50 | |
| save_total_limit: int = 2 | |
| def __post_init__(self) -> None: | |
| object.__setattr__(self, "fp16", torch.cuda.is_available()) | |
| CFG = TrainConfig() | |
| class WeightedTrainer(Trainer): | |
| def __init__(self, class_weights: list[float], *args, **kwargs): | |
| super().__init__(*args, **kwargs) | |
| self._ce_weight = torch.tensor(class_weights, dtype=torch.float32) | |
| w0, w1 = class_weights[0], class_weights[1] | |
| log.info("WeightedTrainer | CrossEntropy weights: safe=%.3f scam=%.3f", w0, w1) | |
| def compute_loss(self, model, inputs, return_outputs=False, num_items_in_batch=None): | |
| labels = inputs.pop("labels") | |
| outputs = model(**inputs) | |
| logits = outputs.logits | |
| weight = self._ce_weight.to(device=logits.device, dtype=logits.dtype) | |
| loss = F.cross_entropy(logits, labels, weight=weight) | |
| return (loss, outputs) if return_outputs else loss | |
| def build_compute_metrics(): | |
| accuracy_metric = evaluate.load("accuracy") | |
| f1_metric = evaluate.load("f1") | |
| precision_metric = evaluate.load("precision") | |
| recall_metric = evaluate.load("recall") | |
| def compute_metrics(eval_pred): | |
| logits, labels = eval_pred | |
| logits_t = torch.as_tensor(logits) | |
| probs = torch.softmax(logits_t, dim=-1).numpy() | |
| preds = np.argmax(logits, axis=-1) | |
| scam_prob = probs[:, 1] | |
| acc = accuracy_metric.compute(predictions=preds, references=labels) | |
| f1_bin = f1_metric.compute( | |
| predictions=preds, references=labels, average="binary" | |
| ) | |
| prec = precision_metric.compute( | |
| predictions=preds, references=labels, average="binary" | |
| ) | |
| rec = recall_metric.compute( | |
| predictions=preds, references=labels, average="binary" | |
| ) | |
| try: | |
| from sklearn.metrics import roc_auc_score | |
| auc = float(roc_auc_score(labels, scam_prob)) | |
| except Exception: | |
| auc = 0.0 | |
| return { | |
| "accuracy": round(acc["accuracy"], 4), | |
| "f1": round(f1_bin["f1"], 4), | |
| "precision": round(prec["precision"], 4), | |
| "recall": round(rec["recall"], 4), | |
| "auc_roc": round(auc, 4), | |
| } | |
| return compute_metrics | |
| def load_artifacts() -> tuple[DatasetDict, list[float], AutoTokenizer]: | |
| if not TOKEN_DIR.exists(): | |
| raise FileNotFoundError( | |
| f"Tokenized data missing: {TOKEN_DIR}\nRun: python src/preprocess.py", | |
| ) | |
| log.info("Loading dataset from %s", TOKEN_DIR) | |
| dataset = load_from_disk(str(TOKEN_DIR)) | |
| log.info(" Splits: %s", {k: len(v) for k, v in dataset.items()}) | |
| weights_path = TOKEN_DIR / "class_weights.json" | |
| if weights_path.exists(): | |
| with open(weights_path, encoding="utf-8") as f: | |
| class_weights = json.load(f)["weights"] | |
| log.info(" Class weights: %s", class_weights) | |
| else: | |
| log.warning("class_weights.json missing; using [1.0, 1.0]") | |
| class_weights = [1.0, 1.0] | |
| if SAVED_TOKENIZER_DIR.exists(): | |
| log.info("Loading tokenizer from %s", SAVED_TOKENIZER_DIR) | |
| tokenizer = AutoTokenizer.from_pretrained(str(SAVED_TOKENIZER_DIR)) | |
| else: | |
| log.info("Loading tokenizer %s (+ custom tokens)", MODEL_NAME) | |
| tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME) | |
| tokenizer.add_tokens(CUSTOM_TOKENS) | |
| return dataset, class_weights, tokenizer | |
| def build_model(tokenizer: AutoTokenizer) -> AutoModelForSequenceClassification: | |
| log.info("Loading model: %s", MODEL_NAME) | |
| model = AutoModelForSequenceClassification.from_pretrained( | |
| MODEL_NAME, | |
| num_labels=NUM_LABELS, | |
| id2label=ID2LABEL, | |
| label2id=LABEL2ID, | |
| ignore_mismatched_sizes=True, | |
| ) | |
| orig = model.config.vocab_size | |
| model.resize_token_embeddings(len(tokenizer)) | |
| new = model.get_input_embeddings().weight.shape[0] | |
| log.info(" Embeddings: %s -> %s tokens", orig, new) | |
| total = sum(p.numel() for p in model.parameters()) | |
| trainable = sum(p.numel() for p in model.parameters() if p.requires_grad) | |
| log.info(" Params: %.1fM total | %.1fM trainable", total / 1e6, trainable / 1e6) | |
| return model | |
| def build_training_args() -> TrainingArguments: | |
| MODEL_OUT.mkdir(parents=True, exist_ok=True) | |
| LOGS_DIR.mkdir(parents=True, exist_ok=True) | |
| return TrainingArguments( | |
| output_dir=str(MODEL_OUT), | |
| num_train_epochs=CFG.num_epochs, | |
| per_device_train_batch_size=CFG.train_batch_size, | |
| per_device_eval_batch_size=CFG.eval_batch_size, | |
| learning_rate=CFG.learning_rate, | |
| optim="adamw_torch", | |
| warmup_ratio=CFG.warmup_ratio, | |
| weight_decay=CFG.weight_decay, | |
| max_grad_norm=CFG.max_grad_norm, | |
| evaluation_strategy="epoch", | |
| save_strategy="epoch", | |
| load_best_model_at_end=True, | |
| metric_for_best_model="f1", | |
| greater_is_better=True, | |
| save_total_limit=CFG.save_total_limit, | |
| logging_dir=str(LOGS_DIR), | |
| logging_steps=CFG.logging_steps, | |
| report_to="none", | |
| fp16=CFG.fp16, | |
| dataloader_num_workers=0, | |
| seed=CFG.seed, | |
| data_seed=CFG.seed, | |
| ) | |
| def full_evaluation(trainer: WeightedTrainer, dataset: DatasetDict) -> dict: | |
| log.info("Test set evaluation") | |
| test_results = trainer.evaluate(eval_dataset=dataset["test"]) | |
| for k, v in sorted(test_results.items()): | |
| if not k.startswith("eval_"): | |
| continue | |
| log.info(" %s: %s", k, v) | |
| try: | |
| from sklearn.metrics import classification_report, confusion_matrix | |
| preds_output = trainer.predict(dataset["test"]) | |
| preds = np.argmax(preds_output.predictions, axis=-1) | |
| labels = preds_output.label_ids | |
| print("\nClassification report:") | |
| print( | |
| classification_report( | |
| labels, preds, target_names=["safe", "scam"], digits=4 | |
| ) | |
| ) | |
| cm = confusion_matrix(labels, preds) | |
| print("Confusion matrix (rows=actual, cols=predicted):") | |
| print(f" safe scam") | |
| print(f" safe {cm[0, 0]:5d} {cm[0, 1]:5d}") | |
| print(f" scam {cm[1, 0]:5d} {cm[1, 1]:5d}") | |
| denom = cm[1, 0] + cm[1, 1] | |
| fnr = cm[1, 0] / denom if denom else 0.0 | |
| print(f"\nFalse negative rate (scam -> safe): {fnr * 100:.2f}%") | |
| except Exception as e: | |
| log.warning("Classification report skipped: %s", e) | |
| return test_results | |
| def save_artifacts(model, tokenizer: AutoTokenizer, test_results: dict) -> None: | |
| log.info("Saving model -> %s", MODEL_OUT) | |
| trainer_eval_subset = { | |
| k: v | |
| for k, v in test_results.items() | |
| if k.startswith("eval_") and k not in ("eval_runtime", "eval_samples_per_second", "eval_steps_per_second") | |
| } | |
| model.save_pretrained(str(MODEL_OUT)) | |
| tokenizer.save_pretrained(str(MODEL_OUT)) | |
| metadata = { | |
| "model_name": MODEL_NAME, | |
| "num_labels": NUM_LABELS, | |
| "id2label": {str(k): v for k, v in ID2LABEL.items()}, | |
| "label2id": LABEL2ID, | |
| "max_length": 128, | |
| "custom_tokens": CUSTOM_TOKENS, | |
| "train_config": { | |
| "num_epochs": CFG.num_epochs, | |
| "train_batch_size": CFG.train_batch_size, | |
| "eval_batch_size": CFG.eval_batch_size, | |
| "learning_rate": CFG.learning_rate, | |
| "warmup_ratio": CFG.warmup_ratio, | |
| "weight_decay": CFG.weight_decay, | |
| "fp16": CFG.fp16, | |
| "seed": CFG.seed, | |
| }, | |
| "test_metrics": trainer_eval_subset, | |
| } | |
| meta_path = MODEL_OUT / "training_metadata.json" | |
| with open(meta_path, "w", encoding="utf-8") as f: | |
| json.dump(metadata, f, indent=2) | |
| log.info("Metadata -> %s", meta_path) | |
| def smoke_test(model, tokenizer: AutoTokenizer) -> None: | |
| cases = [ | |
| ( | |
| "CBI officer here. You are under digital arrest for money laundering. " | |
| "Do not disconnect.", | |
| "scam", | |
| ), | |
| ( | |
| "Your OTP is ready. Share it with me for KYC verification on your account.", | |
| "scam", | |
| ), | |
| ("Hey, are you coming to college tomorrow? Let me know.", "safe"), | |
| ("Your Amazon order has been shipped and will arrive by Friday.", "safe"), | |
| ] | |
| device = next(model.parameters()).device | |
| model.eval() | |
| print("\nSmoke test") | |
| print(f"{'Text':<58} exp pred scam%") | |
| print("-" * 88) | |
| for text, expected in cases: | |
| inputs = tokenizer( | |
| text, | |
| return_tensors="pt", | |
| truncation=True, | |
| padding="max_length", | |
| max_length=128, | |
| ) | |
| inputs = {k: v.to(device) for k, v in inputs.items()} | |
| with torch.no_grad(): | |
| logits = model(**inputs).logits | |
| probs = torch.softmax(logits, dim=-1)[0] | |
| pred_id = int(torch.argmax(probs).item()) | |
| pred = ID2LABEL[pred_id] | |
| scam_p = probs[1].item() | |
| ok = "ok" if pred == expected else "XX" | |
| snippet = text[:56].replace("\n", " ") | |
| print(f"{snippet:<58} {expected:<5} {pred:<5} {scam_p * 100:5.1f}% {ok}") | |
| def main() -> None: | |
| if hasattr(sys.stdout, "reconfigure"): | |
| try: | |
| sys.stdout.reconfigure(encoding="utf-8", errors="replace") | |
| except (OSError, ValueError): | |
| pass | |
| log.info("MuRIL scam classifier training") | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| log.info("Device: %s", device) | |
| if device == "cuda": | |
| log.info("GPU: %s", torch.cuda.get_device_name(0)) | |
| dataset, class_weights, tokenizer = load_artifacts() | |
| model = build_model(tokenizer) | |
| training_args = build_training_args() | |
| log.info( | |
| "Config: epochs=%s batch=%s/%s lr=%s fp16=%s", | |
| CFG.num_epochs, | |
| CFG.train_batch_size, | |
| CFG.eval_batch_size, | |
| CFG.learning_rate, | |
| CFG.fp16, | |
| ) | |
| trainer = WeightedTrainer( | |
| class_weights=class_weights, | |
| model=model, | |
| args=training_args, | |
| train_dataset=dataset["train"], | |
| eval_dataset=dataset["validation"], | |
| tokenizer=tokenizer, | |
| compute_metrics=build_compute_metrics(), | |
| callbacks=[ | |
| EarlyStoppingCallback(early_stopping_patience=CFG.early_stopping_patience), | |
| ], | |
| ) | |
| log.info("Training") | |
| train_result = trainer.train() | |
| log.info( | |
| "Done: steps=%s train_loss=%.4f time=%.1f min", | |
| train_result.global_step, | |
| train_result.training_loss, | |
| train_result.metrics.get("train_runtime", 0) / 60.0, | |
| ) | |
| test_results = full_evaluation(trainer, dataset) | |
| save_artifacts(trainer.model, tokenizer, test_results) | |
| smoke_test(trainer.model, tokenizer) | |
| log.info("Finished.") | |
| if __name__ == "__main__": | |
| main() | |