Evaluation lab · public reproduction
Sentiment, measured
Two sentiment models on 3,000 public review sentences: where each one fails, and what a threshold costs.
| Model | F1 and bootstrap interval | Plot |
|---|---|---|
| A · Naive Bayes | 0.795 0.757 to 0.828 | |
| B · Logistic | 0.803 0.764 to 0.835 | |
| B minus A | +0.008 −0.021 to +0.038 |
This is a public reproduction on open data, built to show how I evaluate a classifier. It is not the Companionship Chatbot’s model, data or reported result.
The 30-second version
- Test sentences
- 596, see the scorecard
- Best F1
- 0.803, see the scorecard model B
- F1 gap, B minus A
- +0.008, see the scorecard
- Takeaway
- Model B edges model A by 0.8 F1 points at the default threshold, but the bootstrap interval for the gap (−0.021 to +0.038) crosses zero: on 596 sentences that is not a reliable win.
Scorecard
Held-out test set. Every number is computed in your browser from the published predictions.
Reference numbers at the default threshold of 0.50, computed by the Python script. With JavaScript on, this becomes an interactive scorecard.
| Metric | A · Naive Bayes | B · Logistic |
|---|---|---|
| Precision | 0.795 | 0.805 |
| Recall | 0.795 | 0.802 |
| F1 | 0.795 | 0.803 |
| Accuracy | 0.795 | 0.804 |
| Source | A | B | Sentences |
|---|---|---|---|
| Amazon | 0.797 | 0.843 | 197 |
| Yelp | 0.814 | 0.819 | 199 |
| IMDb | 0.775 | 0.750 | 200 |
Start here: drag the threshold
Call a sentence positive when the model’s P(positive) is at least this value. Raise it and fewer sentences pass: precision tends to rise, recall can only fall.
| Metric | A · Naive Bayes | B · Logistic |
|---|---|---|
| Precision | ||
| Recall | ||
| F1 | ||
| Accuracy |
| Predicted positive | Predicted negative | |
|---|---|---|
| Actually positive | True pos. | False neg. |
| Actually negative | False pos. | True neg. |
| Predicted positive | Predicted negative | |
|---|---|---|
| Actually positive | True pos. | False neg. |
| Actually negative | False pos. | True neg. |
| Source | A · Naive Bayes | B · Logistic | Sentences |
|---|---|---|---|
| Amazon | |||
| Yelp | |||
| IMDb |
Exploring the slider on the test set is fine; choosing the threshold here would not be. A deployed threshold should come from validation data or from what a false positive costs.
Where they fail
Picked by rule, not by hand: the most confident shared errors, then negation cases, then errors only one model makes.
Double negatives beat both models: “wasn’t terrible” and “not a bad watch” read as negative, because “terrible” and “bad” are among the strongest negative words in training and the not_ feature has too few examples to flip them. Model A counts every token, so nine exclamation marks in “no longer working!!!!!!!!!” are nine votes for positive; model B only notes that “!” is present and gets it right. Model A also learned that “Hitchcock” signals a positive review.
-
Both models wrong, both confident · IMDb
The soundtrack wasn't terrible, either.
-
Both models wrong, both confident · IMDb
Predictable, but not a bad watch.
-
Negation: A wrong, B right · Amazon
I purchased this and within 2 days it was no longer working!!!!!!!!!
-
Negation: A wrong, B right · Amazon
Not a good bargain.
-
Negation: B still wrong · Yelp
If you're not familiar, check it out.
-
Negation: B still wrong · Yelp
The goat taco didn't skimp on the meat and wow what FLAVOR!
-
Only B wrong · Yelp
Ryan's Bar is definitely one Edinburgh establishment I won't be revisiting.
-
Only A wrong · IMDb
There are the usual Hitchcock logic flaws.
Method
- Data. 3,000 labelled sentences, 1,000 each from Amazon, Yelp and IMDb, half positive. 21 exact repeats (ignoring case and spacing) were dropped first, leaving 2,979, so no sentence can appear in both training and test.
- Split. Seeded (331) and stratified by source and label: 2,084 train, 299 validation, 596 test.
- Features. Lowercase regex tokens; contractions split so “don’t” keeps its “n’t”. Model A counts unigrams. Model B uses unigram and bigram presence plus a
not_copy of each word between a negation and the next punctuation mark. - Training. A: multinomial Naive Bayes, Laplace smoothing (alpha 1), not tuned. B: L2 logistic regression, full-batch gradient descent with Nesterov momentum, 1,000 steps; the L2 strength (3 × 10−5) was picked from 7 values by validation log loss. The test set played no part in fitting or tuning.
- Uncertainty. 1,000 bootstrap resamples of the test set; intervals are the 2.5th to 97.5th percentiles. F1 for A: 0.757 to 0.828; B: 0.764 to 0.835; B minus A: −0.021 to +0.038 (B ahead in 70% of resamples).
Limits
- Small sample. Each source contributes about 200 test sentences, so a slice accuracy moves by half a point with every sentence. Read slice gaps as hints, not findings.
- One split, one seed. The bootstrap covers test-set sampling only. A different split or seed would also move the numbers.
- Easier than real use. The dataset’s authors kept sentences with a clear polarity and no neutral ones. Chat messages, sarcasm and mixed feelings are harder, and these models have never seen them.
- Domain shift is not tested. Training and test sentences come from the same three sites. Training on two sources and testing on the third would be the next experiment.
- Examples are quoted, not written. The failure examples are verbatim test sentences from the public dataset, reviews posted on Amazon, Yelp and IMDb; any names in them come from that text.
- Validation reads higher than test. At the L2 strength it picked, model B scored F1 0.852 on the 299 validation sentences and 0.803 on the test set. That setting was chosen on the validation set, so its score there is optimistic; the test number is the one to quote.
- Scores are not calibrated. Neither model’s P(positive) was checked against observed frequencies. Naive Bayes pushes more of its scores to the extremes, so the same slider move changes fewer of its calls.
Reproduce it
One script, Python 3 standard library only. It downloads the zip (or reuses a cached copy outside the repository), checks its SHA-256, trains both models and writes the predictions this page reads.
python3 scripts/eval/build_sentiment_eval.py
The same seed gives a byte-identical file. Only test sentences and their predictions are published, as sentiment-eval.json.
Data: Kotzias, D., Denil, M., de Freitas, N., and Smyth, P. “From Group to Individual Labels Using Deep Features.” KDD 2015. Sentiment Labelled Sentences, UCI Machine Learning Repository, doi:10.24432/C57604, licensed CC BY 4.0. Changes: exact repeats removed, punctuation encoding repaired.
Related reading: How I evaluate agents , the same habits applied to AI agents.