Views
No views yet
distilbert-base-uncased model that classifies Indian bank/UPI/payment SMS messages
into expense categories. Built with LoRA (Low-Rank Adaptation) for better generalization
on small datasets.| ID | Category | Description | Example SMS |
|---|---|---|---|
| 0 | Bills | Utility bills, subscriptions, EMI | "Electricity bill Rs.1340 paid" |
| 1 | Expense | Generic bank debits, UPI transfers | "A/c debited by Rs.1530. Bal Rs.3303." |
| 2 | Food | Food delivery, restaurant orders | "Food order Rs.345 confirmed. Delivery 20 mins." |
| 3 | Income | Credits, salary, refunds, cashback | "Rs.45000 credited. Salary for March." |
| 4 | Recharge | Mobile/DTH recharge | "Rs.239 recharged. Validity 28 days." |
| 5 | Transport | Rides, flights, trains, toll | "Ride completed. Rs.234 charged." |
1from transformers import pipeline
2
3clf = pipeline("text-classification",
4 model="udayugale/expense-tracker-distilbert-v3")
5
6# Single SMS
7result = clf("Food order Rs.345 confirmed. Delivery in 20 mins. Enjoy your meal!")
8print(result)
9# [{'label': 'Food', 'score': 0.97}]
10
11# Batch prediction
12sms_list = [
13 "A/c XX5274 debited by Rs. 1530. Total Bal Rs. 3303 CR.",
14 "Rs698 recharged! Enjoy Unlimited Calls. Valid 28 days.",
15 "Your ride has ended. Total fare Rs.234. Thanks for riding.",
16 "Rs.45000 credited to your account. Salary for March.",
17]
18results = clf(sms_list)
19for sms, r in zip(sms_list, results):
20 print(f"{r['label']:>10} ({r['score']:.2f}) {sms[:55]}")1import re
2from transformers import pipeline
3
4clf = pipeline("text-classification",
5 model="udayugale/expense-tracker-distilbert-v3")
6
7def clean_sms(text):
8 """Same cleaning used during training — must match exactly."""
9 t = text.lower()
10 t = re.sub(r'https?://\S+', 'URL', t)
11 t = re.sub(r'inr|rs\.?|₹', 'rs ', t)
12 t = re.sub(r'a/c\s*(?:xx|\*+)?\d+', 'ACNO', t, flags=re.IGNORECASE)
13 t = re.sub(r'\b\d{8,}\b', 'REFNO', t)
14 t = re.sub(r'[^\x00-\x7F]+', ' ', t)
15 return re.sub(r'\s+', ' ', t).strip()
16
17APP_PATTERNS = [
18 ("Swiggy", r"\bswiggy\b"), ("Zomato", r"\bzomato\b"),
19 ("Blinkit", r"\bblinkit\b"), ("Zepto", r"\bzepto\b"),
20 ("Uber", r"\buber\b"), ("Ola", r"\bola\s+ride\b|olacab"),
21 ("Rapido", r"\brapido\b"), ("IRCTC", r"\birctc\b"),
22 ("FASTag", r"\bfastag\b"), ("Amazon", r"\bamazon\b"),
23 ("Flipkart", r"\bflipkart\b"), ("Netflix", r"\bnetflix\b"),
24 ("Jio", r"\bjio\b"), ("Airtel", r"\bairtel\b"),
25 ("PhonePe", r"\bphonepe\b"), ("Paytm", r"\bpaytm\b"),
26 ("HDFC", r"\bhdfc\b"), ("SBI", r"\bsbi\b"),
27 ("ICICI", r"\bicici\b"), ("Kotak", r"\bkotak\b"),
28 # Add more as new apps emerge
29]
30
31def analyze_sms(raw_text, sender=None):
32 """
33 Full analysis: category + amount + transaction type + app.
34 Returns None for app if unknown — never forces a wrong answer.
35 """
36 # Layer 1: ML classification
37 ml = clf(clean_sms(raw_text))[0]
38
39 # Layer 2: Amount extraction
40 amounts = [
41 float(a.replace(",", ""))
42 for a in re.findall(
43 r"(?:rs\.?\s*|inr\s*|₹\s*)(\d[\d,]*(?:\.\d{1,2})?)",
44 raw_text, re.IGNORECASE
45 )
46 ]
47 txn_type = (
48 "credit" if re.search(r"\bcredited\b|\breceived\b|\bsalary\b", raw_text, re.I)
49 else "debit" if re.search(r"\bdebited\b|\bsent\b|\bpaid\b|\bcharged\b", raw_text, re.I)
50 else "recharge" if re.search(r"\brecharged\b", raw_text, re.I)
51 else "unknown"
52 )
53 bal = re.search(
54 r"(?:total bal|avl bal|balance)[:\s]*(?:rs\.?\s*|₹)?([\d,]+(?:\.\d{1,2})?)",
55 raw_text, re.I
56 )
57
58 # Layer 3: App detection (None if unknown — not forced)
59 app = None
60 for name, pat in APP_PATTERNS:
61 if re.search(pat, raw_text, re.IGNORECASE):
62 app = name
63 break
64
65 return {
66 "category": ml["label"],
67 "confidence": round(ml["score"], 4),
68 "amount": amounts[0] if amounts else None,
69 "type": txn_type,
70 "balance": float(bal.group(1).replace(",", "")) if bal else None,
71 "app": app, # None = unknown app, model still classified correctly
72 }
73
74# Example
75import json
76result = analyze_sms(
77 "A/c XX5274 credited by Rs. 350.00 via UPI from RAHUL VILAS",
78 sender="AD-CENTBK-T"
79)
80print(json.dumps(result, indent=2))
81# {
82# "category": "Income",
83# "confidence": 0.9734,
84# "amount": 350.0,
85# "type": "credit",
86# "balance": null,
87# "app": null
88# }| Setting | Value |
|---|---|
| Base model | distilbert-base-uncased |
| Method | LoRA (Low-Rank Adaptation) — NOT QLoRA |
| LoRA rank (r) | 16 |
| LoRA alpha | 32 |
| LoRA dropout | 0.1 |
| LoRA target layers | q_lin, k_lin, v_lin, out_lin |
| Trainable parameters | ~1.2M (1.8% of total 66M) |
| Frozen parameters | ~64.8M (base DistilBERT) |
| Setting | Value |
|---|---|
| Epochs | 12 |
| Learning rate | 3e-4 (higher than standard — correct for LoRA) |
| Batch size | 32 |
| LR scheduler | Cosine decay |
| Warmup ratio | 0.06 |
| Weight decay | 0.01 |
| Loss function | Weighted CrossEntropy (minority classes weighted higher) |
| Max sequence length | 128 tokens |
| Optimizer | AdamW |
bitsandbytes 4-bit quantization which is incompatible with
DistilBERT's encoder architecture (dtype conflicts between uint8 base and fp32
classification head). LoRA gives the same accuracy improvement — the gain
comes from training fewer parameters, not from quantization.| Source | Type | Rows Used |
|---|---|---|
merged_final_dataset.csv (real Indian SMS) | Real SMS from 93 users | ~4,200 |
engreemali/bank-transactions-sms-datasetss (Kaggle) | Real Indian SMS 100K | ~1,200 |
kumarperiya/pan-indian-consumer-transaction-dataset (Kaggle) | Structured → synthesized SMS | ~600 |
realistic_synthetic_sms.csv (ChatGPT generated) | Synthetic SMS | ~3,200 |
| Pattern templates (programmatic) | Language pattern augmentation | ~1,400 |
merged_final_dataset.csvengreemali datasetkumarperiya datasetTraining: "Swiggy order Rs.450" → Food
Training: "Zomato order Rs.340" → Food
→ Model learns: Swiggy/Zomato = Food
→ At inference: "NewApp order Rs.280" → FAILS (never saw NewApp)Training: "food order rs 450 confirmed. delivery in 30 mins" → Food
Training: "order placed rs 340. out for delivery" → Food
→ Model learns: delivery + order + confirmed = Food
→ At inference: "NewApp food order Rs.280" → WORKS ✅Income category may overlap with Expense for peer-to-peer transfersOthers category (promotional SMS, OTPs, personal chats) is intentionally excludednull for unknown/new apps — this is by design"A/c XX5274 debited by Rs. 1530 via UPI")"HDFC Bank: Rs 450 debited from a/c")"Your food order Rs.345 confirmed")"Rs.239 recharged. Enjoy 2GB daily")"Trip ended. Fare Rs.234 charged")@misc{expense-tracker-distilbert-v3,
title = {Expense Tracker — Indian SMS Classifier (DistilBERT + LoRA)},
author = {your_name},
year = {2025},
url = {https://huggingface.co/your_username/expense-tracker-distilbert-v3}
}