{"slug": "naive-bayes-from-scratch-a-spam-filter-built-from-word-counts", "title": "Naive Bayes From Scratch: A Spam Filter Built From Word Counts", "summary": "A developer built a Naive Bayes spam filter from scratch using only word counts and Bayes' rule, without gradient descent or iterations. The filter visualizes which words push a message toward spam or ham, and an interactive demo is available online.", "body_md": "Naive Bayes ran real spam filters for years, and it's the rare ML model whose \"training\" is just *counting*. No gradient descent, no iterations — count words, apply Bayes' rule, multiply. I built one from scratch and visualised exactly which words push a message toward spam.\n\n📨 **Interactive demo (type a message):** [https://dev48v.infy.uk/ml/day6-naive-bayes.html](https://dev48v.infy.uk/ml/day6-naive-bayes.html)\n\nThis is Day 6 of MachineLearningFromZero — algorithms from scratch, no scikit-learn.\n\nNaive Bayes treats a message as a *set* of words. \"free cash now\" and \"now cash free\" look identical to it. That throws away grammar, but for spam detection the words present matter far more than their order — and it makes the math tiny.\n\nFor every word, how often does it appear in spam vs ham?\n\n``` js\nfor (const { text, label } of trainingData)\n  for (const w of tokenize(text))\n    counts[label][w] = (counts[label][w] || 0) + 1;\n```\n\n`free`\n\nand `click`\n\nflood spam; `meeting`\n\nand `tomorrow`\n\nlive in ham. One pass over the data, done.\n\nYou measured `P(words | spam)`\n\n, but you want `P(spam | words)`\n\n. Bayes flips it:\n\n```\nP(spam | words) ∝ P(spam) × P(words | spam)\n```\n\n`P(spam)`\n\nis the **prior** (how common spam is); the likelihood multiplies in the word evidence.\n\nThe trick that makes it fast: assume each word is independent given the class, so the likelihood is just a product:\n\n```\nP(words | spam) = P(w1|spam) × P(w2|spam) × ...\n```\n\nReal words aren't independent (\"credit\" and \"card\" co-occur), so it's a naive lie — but the classification still lands right astonishingly often.\n\nTwo practical fixes. Add 1 to every count (Laplace smoothing) so an unseen word doesn't zero out the whole product. And **add logarithms** instead of multiplying tiny probabilities, which would underflow to 0:\n\n``` js\nscore[label] = Math.log(prior[label]);\nfor (const w of words)\n  score[label] += Math.log((counts[label][w] + 1) / (totalWords[label] + V));\nreturn score.spam > score.ham ? \"spam\" : \"ham\";\n```\n\nSoftmax the two scores and you get a probability, like the bars in the demo.\n\nCount words → Bayes → multiply (in logs) → pick the winner. It's one of the simplest classifiers there is, needs almost no data to start working, and remains a great baseline for any text-classification task. [Try the live spam filter](https://dev48v.infy.uk/ml/day6-naive-bayes.html) — red words push spam, blue push ham.", "url": "https://wpnews.pro/news/naive-bayes-from-scratch-a-spam-filter-built-from-word-counts", "canonical_source": "https://dev.to/dev48v/naive-bayes-from-scratch-a-spam-filter-built-from-word-counts-1llc", "published_at": "2026-06-17 15:40:54+00:00", "updated_at": "2026-06-17 15:51:19.678533+00:00", "lang": "en", "topics": ["machine-learning", "natural-language-processing", "ai-products"], "entities": [], "alternates": {"html": "https://wpnews.pro/news/naive-bayes-from-scratch-a-spam-filter-built-from-word-counts", "markdown": "https://wpnews.pro/news/naive-bayes-from-scratch-a-spam-filter-built-from-word-counts.md", "text": "https://wpnews.pro/news/naive-bayes-from-scratch-a-spam-filter-built-from-word-counts.txt", "jsonld": "https://wpnews.pro/news/naive-bayes-from-scratch-a-spam-filter-built-from-word-counts.jsonld"}}