Skip to content

About

A three-class hate speech classifier — five algorithms, one hard-voting ensemble, and an honest account of what it misses.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Hate Classify

A three-class hate speech classifier — hate speech, offensive, neither — with a case study that says plainly what it does and what it misses.

This is a ground-up rebuild of Hate-Classify: A Service Framework for Hate Speech Identification on Social Media, a Django/MySQL project. The machine learning is ported faithfully — same corpus, same preprocessing rule, same five algorithms, same three-model hard vote. Everything around it is new.

npm install
npm run dev          # http://localhost:3000

The trained model is committed, so it runs immediately. npm run train re-fits it from the corpus in about 1.5 seconds.


How it works

The analyser classifying two sentences

Every submission runs through the same six stages, server-side, in well under a millisecond:

raw text
   │
   ├─ 1. preprocess    lowercase · erase @mentions · non-alphanumerics → space
   │                   "you people don't belong here" → "you people don t belong here"
   │
   ├─ 2. tokenise      \b\w\w+\b — words of two or more characters
   │
   ├─ 3. vectorise     count each token's column in a 7,729-term vocabulary
   │                   unknown terms have no column, so they carry no weight at all
   │
   ├─ 4. score         five fitted models read the same sparse vector
   │                   Naive Bayes · Linear SVM · Logistic Regression · Decision Tree · SGD
   │
   ├─ 5. vote          three of them — NB + SVM + Logistic — cast a hard majority vote
   │                   the decision tree is scored but benched, as in the original
   │
   └─ 6. attribute     Logistic Regression coefficients × term counts, ranked
                       → the verdict, the spread, and the terms it rests on

The clip above shows the interesting failure first. Three voters split 2–1 for hate speech, while the averaged posterior across those same three voters puts offensive ahead at 45.0% against 39.5%. A hard vote reports the majority and discards how close the call was — so the analyser shows both rather than picking the flattering one. It also shows the evidence: here, people and you are carrying the decision, which is exactly the shallowness the limits section describes.


What it does

Route
/ The case study — the corpus, the live reader, four decisions, the model bench, the full classification reports and confusion matrix, the limits
/analyze Classify any sentence: all five models, the ensemble vote, and the terms each decision rests on

There are no accounts and nothing is logged. The original's user registration and service-provider console were dropped: the classifier is the point, and every number they displayed comes from the trained model, so it lives in the case study instead.


The numbers

Measured on a stratified 20% holdout (3,965 rows), seed 42.

Model Accuracy Macro-F1 Hate recall
SGD Classifier 90.39% 0.734 0.317
Logistic Regression 90.06% 0.730 0.322
Linear SVM 89.03% 0.717 0.330
Decision Tree 88.05% 0.686 0.248
Naive Bayes 88.17% 0.651 0.152
Ensemble (hard vote) 89.84% 0.724 0.313

The gap between columns two and four is the whole finding. 77.6% of the corpus is a single class, so accuracy largely measures the corpus. The ensemble is ~90% accurate and finds roughly one hate post in three. Refitting Logistic Regression with balanced class weights moves hate recall 0.322 → 0.530 and costs 5.4 points of accuracy; both arms, swept across every decision threshold, are in the case study's fourth panel.

Verify any claim on the site:

npm run probe                                   # the sentences the demo uses
npm run probe -- "your own sentence here"

Tests

npm test

70 assertions over the ported pipeline, run on Node's built-in test runner. They exist because a port that claims fidelity to scikit-learn has to be checkable, so they pin the things that would otherwise drift silently:

  • the preprocessing regex, character for character, including the URL-shredding flaw
  • the CSV reader against quoted commas, escaped quotes, embedded newlines and a BOM
  • the token pattern and the alphabetically sorted vocabulary — the shipped weight matrices are indexed by column position, so a vocabulary that sorts differently pairs every weight with the wrong term and still produces plausible-looking output
  • precision/recall/F1 against a hand-checked confusion matrix, and sklearn's zero_division=0
  • the hard vote's tie-break — numpy's argmax, lowest class id, which on this label map means a three-way split resolves to hate speech
  • every figure published in the table above, asserted against the shipped model card

That last one is the point of the suite: retraining in a way that moves the numbers fails npm test instead of quietly turning this README into fiction.


What was ported, and what was changed

Kept, because changing it would make the reported metrics meaningless:

  • The preprocessing rule, character for character — re.sub("(@[A-Za-z0-9]+)|([^0-9A-Za-z \t])", " ", tweet.lower())
  • CountVectorizer's default token pattern \b\w\w+\b and alphabetically sorted vocabulary
  • The label map (0 hate, 1 offensive, 2 neither) and the 80/20 split
  • Multinomial Naive Bayes (α=1.0), LinearSVC, Logistic Regression, a gini decision tree, hinge-loss SGD
  • The ensemble's exact membership: Naive Bayes + SVM + Logistic Regression, hard vote. The decision tree is trained and scored but never votes — as in the original

Changed, with reasons:

Original Here
Training Refit all five models from a 20k-row CSV on every prediction request Fitted once offline; weights ship as JSON. ~40s → 0.2ms
Reproducibility train_test_split unseeded — every run reported different accuracy Seeded, stratified
Vocabulary Fit on the full corpus before splitting Fit on the training split only; the original leaked test terms into training
Vocabulary size Unbounded min_df=2, capped at 20k — 19,473 terms seen, 7,729 kept
Accounts Registration and login, passwords in plaintext CharField(max_length=10), raw user id in request.session Removed — nothing here needs to know who you are
Console Gated on if admin == "Admin" and password == "Admin" inline in the view; users table, prediction log, .xls export via xlwt Removed. The model reports and confusion matrix it displayed are in the case study, ungated
Class ratio Deleted and rebuilt a DB table per page view; divided by zero on a fresh install Removed with the log it counted

The one thing deliberately not fixed: the preprocessing rule strips punctuation before tokenising, so URLs are not removed but shredded — t.co links leave co behind as a feature in 1,871 training documents. It is preserved so the metrics stay honest, and surfaced in the case study as a finding.


Layout

data/tweets.csv              19,826 labelled tweets
scripts/train.ts             offline training → src/model/
scripts/probe.ts             classify from the terminal
tests/                       the suite described above
src/lib/ml/                  preprocessing, vectoriser, the five models, metrics
src/lib/classifier.ts        runtime inference
src/model/bundle.gen.ts      generated — the fitted weights the server imports
src/model/card.json          generated — every number the UI renders

The model is embedded as a generated TypeScript module rather than an imported .json: a 1 MB JSON import makes tsc infer a 7,729-element tuple type, and it removes any question of file tracing at deploy time.

Training and tests run on Node's native TypeScript support (Node 22.18+), so there is no build step and no ts-node.


Limits

It reads bag-of-words unigrams. It has no access to sarcasm, negation, or who is being addressed — "you people" scores as hate on the strength of "you" and "people". It inherits its annotators' judgements wholesale, including this corpus's documented tendency to mark African-American English as offensive more often than the same sentence in standard English. That bias is in the labels, not the architecture, so more training does not fix it.

A classifier is a suggestion. A person decides.

About

A three-class hate speech classifier — five algorithms, one hard-voting ensemble, and an honest account of what it misses.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages