Downloads · 30 days
0
jngb-labs/sms-spam-classical
sms-spam-classical is a text classification model from jngb-labs. Use it when you need a label for a piece of text. It is set up for sklearn. The card lists the license as mit.
A spam classifier that fits in one megabyte.
Downloads · 30 days
0
Access
Public
Updated May 20, 2026
Repo size
2.1 MB
Likes
0
Public
Click a slice to open those files.
.joblib1.1 MB · 98%
From the Hugging Face model README
A spam classifier that fits in one megabyte.
The thing about SMS spam, empirically, is that you can catch most of it by counting words. Not in a hand-wavy way. In a literal, "compute the term-frequency-inverse-document-frequency matrix and feed it to a linear classifier" way. The trick was understood in the late 1990s. It works.
This is the linear classifier. It costs about thirty seconds of CPU time to train. It runs in milliseconds. It weighs less than a single photo on your phone.
You hand it an SMS. It tells you ham or spam, and gives you a probability in case you would like to set your own threshold.
import sys, joblib
from huggingface_hub import snapshot_download
repo = snapshot_download("jngb-labs/sms-spam-classical")
sys.path.insert(0, f"{repo}/src")
import features # required for unpickling
bundle = joblib.load(f"{repo}/model/classifier.joblib")
clf = bundle["pipeline"]
clf.predict(["FREE entry to win £1000! Text WIN to 87121"])
# array([1]) # 1 = spam
clf.predict_proba(["FREE entry to win £1000! Text WIN to 87121"])
# array([[0.000..., 0.999...]]) # [P(ham), P(spam)]
| Metric | Mean | Std |
|---|---|---|
| Accuracy | 0.9888 | 0.0055 |
| Spam F1 | 0.9536 | 0.0231 |
| Spam precision | 0.9772 | 0.0215 |
| Spam recall | 0.9315 | 0.0298 |
These are 5-fold stratified cross-validation numbers on the full deduplicated dataset (5,159 messages). Translated: about 99 of every 100 messages get classified correctly. The 1 percent that doesn't is usually a message that a human would also hesitate on.
On the same dataset, a fine-tuned DistilBERT (250 megabytes, GPU-trained) lands in the same neighbourhood. The interesting thing is not which model wins, but how close they end up.
The pipeline is, in order:
A FeatureUnion over three feature blocks. Word TF-IDF, unigrams and bigrams, sublinear-scaled. Character TF-IDF, three to five character windows with char_wb (so n-grams respect word boundaries). Hand-crafted surface features: length, digit ratio, uppercase ratio, punctuation density, presence of URLs, presence of phone numbers, presence of currency symbols, exclamation count. Spam, visually, looks different from ham, and a few engineered features capture that.
A LinearSVC with balanced class weights (the dataset is roughly 88 percent ham), wrapped in a CalibratedClassifierCV so you get probabilities instead of bare decision scores.
No deep stop-word list. max_df=0.95 plus IDF weighting handles common-word suppression more carefully than sklearn's default English stop list, which would happily discard "call". In the context of SMS spam, "call" does a lot of work.
When you have:
For binary classification of short English text where the signal is mostly lexical, this kind of pipeline is competitive with anything you can throw a transformer at. It also runs on a laptop, in a container without a GPU runtime, in any inference environment that can import joblib.
When the task requires semantics that words alone do not capture: paraphrase detection, intent classification with subtle context, sarcasm, multilingual transfer, or anything where the signal is "what the message means" rather than "what specific words appear in it." This model has no notion of meaning. It counts.
It is also, specifically, trained on the SMS Spam Collection v.1, which dates to 2011. Modern smishing campaigns (URL shorteners, brand impersonation, two-factor scams) are underrepresented in that corpus. The model will happily call a 2026-vintage "URGENT: your account has been suspended, click here" message ham, because nothing quite like it appears in the training data. If you are deploying against contemporary attacks, retrain on contemporary data.
Deduplicated SMS Spam Collection v.1, distributed as jngb-labs/sms-spam. 5,159 messages, 87.6 percent ham, 12.4 percent spam, originally assembled by Almeida and Gómez Hidalgo. Full data card at the dataset link.
jngb-labs/sms-spam-distilbert, DistilBERT fine-tuned on the same dataset.jngb-labs/sms-spam-classifier, a Hugging Face Space that runs this classifier on any SMS you type.git clone https://huggingface.co/jngb-labs/sms-spam-classical
cd sms-spam-classical
pip install -r requirements.txt
python scripts/train.py \
--data /path/to/jngb-labs/sms-spam/data.csv \
--out model/classifier.joblib \
--report model/cv_report.json
Seed is 42. Training takes about 30 seconds on a single CPU core. The output should match the CV report in this repo to the third decimal place.
The training data:
@inproceedings{Almeida2011SMSSpam,
author = {Tiago A. Almeida and Jos\'{e} Mar\'{i}a G\'{o}mez Hidalgo and Akebo Yamakami},
title = {Contributions to the study of {SMS} spam filtering: new collection and results},
booktitle = {Proceedings of the 2011 ACM Symposium on Document Engineering},
year = {2011},
}
Model artifact and training code under the MIT license. The training data has its own license; see the dataset card.