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
| """ | |
| MuRIL preprocessing: Unicode normalize, mask URLs/phones/PII, ASCII-only lowercasing, | |
| tokenize with google/muril-base-cased, stratified 70/15/15 split → HuggingFace DatasetDict. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import logging | |
| import re | |
| import unicodedata | |
| from pathlib import Path | |
| import numpy as np | |
| import pandas as pd | |
| from datasets import Dataset, DatasetDict | |
| from sklearn.model_selection import train_test_split | |
| from sklearn.utils.class_weight import compute_class_weight | |
| from transformers import AutoTokenizer, PreTrainedTokenizerBase | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") | |
| log = logging.getLogger(__name__) | |
| ROOT = Path(__file__).resolve().parent.parent | |
| INPUT_CSV = ROOT / "data" / "processed" / "combined.csv" | |
| OUTPUT_DIR = ROOT / "data" / "processed" / "tokenized" | |
| TOKENIZER_SAVE_DIR = OUTPUT_DIR / "muril_tokenizer" | |
| MODEL_NAME = "google/muril-base-cased" | |
| MAX_LENGTH = 128 | |
| TRAIN_RATIO = 0.70 | |
| VAL_RATIO = 0.15 | |
| RANDOM_SEED = 42 | |
| CUSTOM_TOKENS = ["[URL]", "[PHONE]", "[EMAIL]", "[AMOUNT]", "[CODE]", "[AADHAAR]", "[PAN]"] | |
| # ASCII lowercasing would mangle placeholders ([URL] -> [url]); restore for tokenizer add_tokens. | |
| _PLACEHOLDER_LOWER_TO_CANON = { | |
| "[url]": "[URL]", | |
| "[phone]": "[PHONE]", | |
| "[email]": "[EMAIL]", | |
| "[amount]": "[AMOUNT]", | |
| "[code]": "[CODE]", | |
| "[aadhaar]": "[AADHAAR]", | |
| "[pan]": "[PAN]", | |
| } | |
| INDIC_RANGE_RE = re.compile(r"[\u0900-\u0D7F]+") | |
| _URL_RE = re.compile(r"https?://\S+|www\.\S+", re.IGNORECASE) | |
| _PHONE_RE = re.compile( | |
| r"(\+91[\s\-]?)?[6-9]\d{9}" | |
| r"|\b\d{10}\b" | |
| r"|\b\d{5}[\s\-]\d{5}\b" | |
| r"|\+\d{1,3}[\s\-]\d{6,14}", | |
| ) | |
| _EMAIL_RE = re.compile(r"\S+@\S+\.\S+") | |
| _AMOUNT_RE = re.compile(r"₹\s?\d[\d,]*(\.\d+)?|\brs\.?\s?\d[\d,]*", re.IGNORECASE) | |
| _OTP_RE = re.compile(r"\b\d{4,8}\b") | |
| _AADHAAR_RE = re.compile(r"\b\d{4}\s\d{4}\s\d{4}\b") | |
| _PAN_RE = re.compile(r"\b[A-Z]{5}[0-9]{4}[A-Z]\b") | |
| _WHITESPACE = re.compile(r"\s{2,}") | |
| _FLAG_PATTERNS = { | |
| "urgency": re.compile( | |
| r"\b(urgent|immediately|right now|abhi|turant|jaldi|do not disconnect" | |
| r"|tatkaal|asap|within \d+ (minutes?|hours?|seconds?))\b", | |
| re.IGNORECASE, | |
| ), | |
| "authority_impersonation": re.compile( | |
| r"\b(cbi|ed|ncb|nia|police|officer|inspector|narcotics|cyber cell" | |
| r"|income tax|enforcement|ib|raw|customs|interpol|court|judge" | |
| r"|sarkar|government|adhikari)\b", | |
| re.IGNORECASE, | |
| ), | |
| "threat": re.compile( | |
| r"\b(arrest|warrant|case register|jail|prison|legal action|sue|fir" | |
| r"|criminal|giriaftari|case darj|pakad|band|block|freeze)\b", | |
| re.IGNORECASE, | |
| ), | |
| "payment_demand": re.compile( | |
| r"\b(pay|transfer|send money|deposit|upi|neft|rtgs|wire|bhej" | |
| r"|paisa|rupee|amount|fee|fine|penalty|bail|zamaanat)\b", | |
| re.IGNORECASE, | |
| ), | |
| "secrecy": re.compile( | |
| r"\b(do not tell|kisi ko mat batao|secret|confidential|family ko mat" | |
| r"|don.t inform|keep quiet|chup raho|private)\b", | |
| re.IGNORECASE, | |
| ), | |
| } | |
| def normalize_unicode(text: str) -> str: | |
| return unicodedata.normalize("NFC", text) | |
| def lowercase_ascii_only(text: str) -> str: | |
| return "".join(ch.lower() if ord(ch) < 128 else ch for ch in text) | |
| def replace_sensitive_tokens(text: str) -> str: | |
| text = _AADHAAR_RE.sub("[AADHAAR]", text) | |
| text = _PAN_RE.sub("[PAN]", text) | |
| text = _URL_RE.sub("[URL]", text) | |
| text = _EMAIL_RE.sub("[EMAIL]", text) | |
| text = _PHONE_RE.sub("[PHONE]", text) | |
| text = _AMOUNT_RE.sub("[AMOUNT]", text) | |
| text = _OTP_RE.sub("[CODE]", text) | |
| return text | |
| def clean_whitespace(text: str) -> str: | |
| return _WHITESPACE.sub(" ", text).strip() | |
| def restore_placeholder_tokens(text: str) -> str: | |
| for lower, canon in _PLACEHOLDER_LOWER_TO_CANON.items(): | |
| text = text.replace(lower, canon) | |
| return text | |
| def full_normalize(text: str) -> str: | |
| if not isinstance(text, str) or not text.strip(): | |
| return "" | |
| text = normalize_unicode(text) | |
| text = replace_sensitive_tokens(text) | |
| text = lowercase_ascii_only(text) | |
| text = restore_placeholder_tokens(text) | |
| text = clean_whitespace(text) | |
| return text | |
| def extract_flags(text: str) -> list[str]: | |
| return [flag for flag, pattern in _FLAG_PATTERNS.items() if pattern.search(text)] | |
| def has_indic_script(text: str) -> bool: | |
| return bool(INDIC_RANGE_RE.search(text)) | |
| def load_and_preprocess(path: Path = INPUT_CSV) -> pd.DataFrame: | |
| log.info("Loading combined dataset from %s …", path) | |
| if not path.exists(): | |
| raise FileNotFoundError( | |
| f"Missing {path}. Run: python src/data_loader.py", | |
| ) | |
| df = pd.read_csv(path) | |
| required = {"text", "label", "category"} | |
| if not required.issubset(df.columns): | |
| raise ValueError(f"CSV missing columns: {required - set(df.columns)}") | |
| log.info(" Raw rows: %d", len(df)) | |
| log.info("Extracting scam flags …") | |
| df = df.copy() | |
| df["flags"] = df["text"].apply(extract_flags) | |
| df["flag_count"] = df["flags"].apply(len) | |
| df["has_indic"] = df["text"].apply(has_indic_script) | |
| log.info("Normalizing text (Unicode NFC, [URL]/[PHONE]/…, ASCII lower only) …") | |
| df["text"] = df["text"].apply(full_normalize) | |
| empty_mask = df["text"].str.len() < 5 | |
| if empty_mask.sum() > 0: | |
| log.warning(" Dropping %d rows with text < 5 chars after normalization", int(empty_mask.sum())) | |
| df = df[~empty_mask].reset_index(drop=True) | |
| df["label"] = df["label"].astype(int) | |
| log.info(" After normalization: %d rows", len(df)) | |
| log.info(" Indic script present: %d rows (%.1f%%)", df["has_indic"].sum(), df["has_indic"].mean() * 100) | |
| scam_mean = df.loc[df["label"] == 1, "flag_count"].mean() | |
| log.info(" Avg flags per scam message: %.2f", scam_mean if np.isfinite(scam_mean) else 0.0) | |
| return df | |
| def _train_val_test_split(df: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: | |
| test_size = 1.0 - TRAIN_RATIO - VAL_RATIO | |
| val_relative = VAL_RATIO / (TRAIN_RATIO + VAL_RATIO) | |
| def _stratify(series: pd.Series | None): | |
| return series if series is not None and series.nunique() > 1 else None | |
| try: | |
| train_val, test = train_test_split( | |
| df, | |
| test_size=test_size, | |
| stratify=_stratify(df["label"]), | |
| random_state=RANDOM_SEED, | |
| ) | |
| train, val = train_test_split( | |
| train_val, | |
| test_size=val_relative, | |
| stratify=_stratify(train_val["label"]), | |
| random_state=RANDOM_SEED, | |
| ) | |
| except ValueError as e: | |
| log.warning("Stratified split failed (%s); falling back to random split.", e) | |
| train_val, test = train_test_split(df, test_size=test_size, random_state=RANDOM_SEED) | |
| train, val = train_test_split(train_val, test_size=val_relative, random_state=RANDOM_SEED) | |
| log.info("Split → train: %d | val: %d | test: %d", len(train), len(val), len(test)) | |
| for name, split in [("train", train), ("val", val), ("test", test)]: | |
| scam_pct = split["label"].mean() * 100 | |
| log.info(" %s: scam=%.1f%%", name, scam_pct) | |
| return train.reset_index(drop=True), val.reset_index(drop=True), test.reset_index(drop=True) | |
| def prepare_tokenizer() -> PreTrainedTokenizerBase: | |
| log.info("Loading tokenizer: %s", MODEL_NAME) | |
| tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME) | |
| log.info(" Vocab size: %d", tokenizer.vocab_size) | |
| log.info(" Max model length: %s", tokenizer.model_max_length) | |
| added = tokenizer.add_tokens(CUSTOM_TOKENS) | |
| log.info(" Added %d custom tokens: %s", added, CUSTOM_TOKENS) | |
| return tokenizer | |
| def tokenize_dataset( | |
| tokenizer: PreTrainedTokenizerBase, | |
| df_dict: dict[str, pd.DataFrame], | |
| ) -> DatasetDict: | |
| split_name_map = {"train": "train", "val": "validation", "test": "test"} | |
| def tokenize_batch(batch: dict) -> dict: | |
| encoded = tokenizer( | |
| batch["text"], | |
| truncation=True, | |
| padding="max_length", | |
| max_length=MAX_LENGTH, | |
| return_token_type_ids=True, | |
| ) | |
| if "token_type_ids" not in encoded: | |
| encoded["token_type_ids"] = [[0] * MAX_LENGTH for _ in batch["text"]] | |
| return { | |
| "input_ids": encoded["input_ids"], | |
| "attention_mask": encoded["attention_mask"], | |
| "token_type_ids": encoded["token_type_ids"], | |
| "labels": batch["label"], | |
| } | |
| out: dict[str, Dataset] = {} | |
| for key, frame in df_dict.items(): | |
| hf_name = split_name_map[key] | |
| ds = Dataset.from_pandas(frame[["text", "label"]], preserve_index=False) | |
| ds = ds.map( | |
| tokenize_batch, | |
| batched=True, | |
| batch_size=256, | |
| desc=f"Tokenizing {hf_name}", | |
| remove_columns=["text", "label"], | |
| ) | |
| out[hf_name] = ds | |
| return DatasetDict(out) | |
| def compute_class_weights(train_df: pd.DataFrame) -> list[float]: | |
| classes = np.array([0, 1]) | |
| weights = compute_class_weight( | |
| class_weight="balanced", | |
| classes=classes, | |
| y=train_df["label"].values, | |
| ) | |
| log.info("Class weights → safe: %.3f | scam: %.3f", weights[0], weights[1]) | |
| return weights.tolist() | |
| def inspect_tokenization(text: str, tokenizer: PreTrainedTokenizerBase) -> None: | |
| normalized = full_normalize(text) | |
| flags = extract_flags(text) | |
| encoding = tokenizer( | |
| normalized, | |
| return_tensors="pt", | |
| max_length=MAX_LENGTH, | |
| truncation=True, | |
| padding=False, | |
| return_token_type_ids=True, | |
| ) | |
| tokens = tokenizer.convert_ids_to_tokens(encoding["input_ids"][0]) | |
| print(f"\nOriginal : {text}") | |
| print(f"Normalized: {normalized}") | |
| print(f"Flags : {flags}") | |
| print(f"Tokens ({len(tokens)}): {tokens}") | |
| print(f"Token IDs : {encoding['input_ids'][0].tolist()}") | |
| def run_pipeline() -> tuple[DatasetDict, list[float], PreTrainedTokenizerBase]: | |
| df = load_and_preprocess() | |
| train_df, val_df, test_df = _train_val_test_split(df) | |
| class_weights = compute_class_weights(train_df) | |
| tokenizer = prepare_tokenizer() | |
| dataset = tokenize_dataset( | |
| tokenizer, | |
| {"train": train_df, "val": val_df, "test": test_df}, | |
| ) | |
| OUTPUT_DIR.mkdir(parents=True, exist_ok=True) | |
| dataset.save_to_disk(str(OUTPUT_DIR)) | |
| log.info("Tokenized dataset saved → %s", OUTPUT_DIR) | |
| weights_path = OUTPUT_DIR / "class_weights.json" | |
| with open(weights_path, "w", encoding="utf-8") as f: | |
| json.dump({"weights": class_weights, "labels": ["safe", "scam"]}, f, indent=2) | |
| log.info("Class weights saved → %s", weights_path) | |
| TOKENIZER_SAVE_DIR.mkdir(parents=True, exist_ok=True) | |
| tokenizer.save_pretrained(str(TOKENIZER_SAVE_DIR)) | |
| log.info("Tokenizer saved → %s", TOKENIZER_SAVE_DIR) | |
| return dataset, class_weights, tokenizer | |
| if __name__ == "__main__": | |
| import sys | |
| if hasattr(sys.stdout, "reconfigure"): | |
| try: | |
| sys.stdout.reconfigure(encoding="utf-8", errors="replace") | |
| sys.stderr.reconfigure(encoding="utf-8", errors="replace") | |
| except (OSError, ValueError): | |
| pass | |
| dataset, class_weights, tok = run_pipeline() | |
| print("\n-- Sample token inspection (Hindi / mixed scam) --") | |
| inspect_tokenization( | |
| "CBI officer bol raha hoon. Aapka Aadhaar money laundering case mein hai. " | |
| "Turant ₹50000 UPI se bhejo https://evil.example/pay warna giraftari.", | |
| tok, | |
| ) | |
| print("\n-- Sample token inspection (safe message) --") | |
| inspect_tokenization( | |
| "Kal meeting 3 baje hai. Conference room B mein milte hain.", | |
| tok, | |
| ) | |
| print("\n-- Dataset summary --") | |
| for split_name, ds in dataset.items(): | |
| print(f" {split_name:12s}: {len(ds):5d} examples | columns: {ds.column_names}") | |
| ratio = class_weights[1] / class_weights[0] if class_weights[0] else 0.0 | |
| print(f"\nClass weights: safe={class_weights[0]:.3f}, scam={class_weights[1]:.3f}") | |
| if ratio > 2.0 or (1.0 / ratio if ratio else 0) > 2.0: | |
| print( | |
| "Note: imbalance >2:1 — use WeightedRandomSampler or class_weight in loss during training.", | |
| ) | |
| print("\nNext: python src/train.py (after you add training script)") | |