Classic Text Processing and TF-IDF
Tokenize, stem, lemmatize, count, weight with TF-IDF, classify sentiment and find topics with LDA or BERTopic.
- Concepts and terms
- Tasks and commands
- Annotated code
- Diagrams
- Defaults and numbers
- Troubleshooting
- Questions and tips
Classic text pipeline#
From Lesson 05.01, Lesson 05.02, Lesson 05.05
Tasks in code#
| Task | Code |
|---|---|
| Tokenize, stem and lemmatize | preprocess(text, |
| Porter step 1a | stem_step_1a(word) |
| Table lemmatizer with fallback | lemmatize(word, |
| Count matrix | bag_of_words(docs, |
| TF-IDF rows on the unit sphere | l2_normalize(tfidf(bow)) |
| Scope negation | apply_negation(tokens) |
| Naive Bayes baseline | train_nb(docs_by_class, |
| LDA by collapsed Gibbs sampling | collapsed_gibbs_lda(docs, |
From 05.01 code/main.py, 05.02 code/main.py, Lesson 05.05, 05.15 code/main.py
NLTK: tokens, stems, lemmas#
nltk.download("punkt_tab")nltk.download("wordnet")nltk.download("averaged_perceptron_tagger_eng")tokens = word_tokenize(text)contractions, Unicodestems = [PorterStemmer().stem(t) for t in tokens] # all five phasestagged = pos_tag(tokens)Penn Treebank tagslemmatizer = WordNetLemmatizer()lemmas = [lemmatizer.lemmatize(t, nltk_pos_to_wordnet(tag)) for t, tag in tagged]V>v J>a R>r else nFrom Lesson 05.01
Sentiment baseline in scikit-learn#
pipe = Pipeline([ ("tfidf", TfidfVectorizer( ngram_range=(1, 2),bigrams, so not_good is a feature min_df=2, sublinear_tf=True,dampens repeated words stop_words=None)),keeps negations ("clf", LogisticRegression(C=1.0, max_iter=1000)),])pipe.fit(X_train, y_train)print(pipe.score(X_test, y_test))75% to 85% on SST-2 with the 3 flagsFrom Lesson 05.05
Vectorizer knobs#
| Command | What it does |
|---|---|
CountVectorizer() | Tokenization, vocabulary and BoW in one call |
TfidfVectorizer() | Adds IDF weighting and L2 normalization |
ngram_range= | Includes bigrams. Usually boosts classification |
min_df= | Drops words in fewer than 2 docs |
max_df= | Drops words in more than 95% of docs |
stop_words= | Builtin stopword list. Not for sentiment |
sublinear_tf= | Uses 1 + log(tf) instead of raw tf |
get_feature_names_out() | Lists the vocabulary in column order |
From Lesson 05.02
Use X when#
- Stem or lemmatize?
- Stem for search indexing. Lemmatize for text the user reads.
- NLTK or spaCy?
- NLTK to teach and swap parts. spaCy for production speed.
- TF-IDF or embeddings?
- TF-IDF for spam, logs, low data and explainable models.
- When a transformer?
- Sarcasm, aspects, sentiment shifts and low-resource languages.
- LDA or BERTopic?
- LDA for long docs with topic mixtures. BERTopic for short text.
- No GPU?
- Use LDA or NMF.
Tips#
- Stay sparse. Dense TF-IDF for 100k documents does not fit in memory.
- Read 5 wrong predictions per class before you trust a metric.
- Regenerate the baseline on your own pipeline, not the paper's number.
From Lesson 05.02, Lesson 05.05, Lesson 05.15
Topic models#
cv = CountVectorizer(max_features=max_features, stop_words="english", min_df=2, max_df=0.9)LDA needs raw countsX = cv.fit_transform(documents)lda = LatentDirichletAllocation(n_components=n_topics, random_state=42, max_iter=50, learning_method="online")doc_topic = lda.fit_transform(X)(n_docs, n_topics), rows sum to 1topic_model = BERTopic( embedding_model="sentence-transformers/all-MiniLM-L6-v2", min_topic_size=15)library default is 10topics, probs = topic_model.fit_transform(documents)info = topic_model.get_topic_info()Topic -1 is the outlier bucketFrom Lesson 05.15
Formulas#
- TF-IDF
- \frac{\mathrm{count}(w \text{ in } d)}{|d|} \cdot \log\frac{N}{\mathrm{df}(w)}
- Smoothed IDF
- \log\frac{n + 1}{d + 1} + 1. A word in every doc keeps IDF 1.
- L2 row
- divide each row by its norm. Long docs stop dominating.
- Naive Bayes
- log prior plus the sum of log P(\text{word} \mid \text{class}). Take the max.
- Laplace
- (\mathrm{count} + \alpha) / (\mathrm{total} + \alpha |V|). No zero probabilities.
- Logistic regression
- sigmoid of Xw + b. Predict 1 at 0.5 or more.
- Macro-F1
- mean of per-class F1, equally weighted.
- c_v coherence
- NPMI of top-word pairs, compared by cosine. Higher is better.
From Lesson 05.02, Lesson 05.05, Lesson 05.15
Defaults#
| Setting | Default | Note |
|---|---|---|
NB alpha | 1.0 | Teaching default. 0.01 is common in practice |
LR l2 | 0.01 | Start here and tune. Sparse features overfit |
LR epochs, lr | 500 | Learning rate 0.05 |
LR C | 1.0 | With max_iter=1000 in scikit-learn |
BERTopic min_topic_size | 10 | Use 50 or 100 over 10,000 docs |
Gibbs alpha, beta | 0.1 | Beta is 0.01. Both must be positive |
Gibbs n_iters | 200 | The demo runs 300 |
| Topic count | max(5, | Clamp to 200 under 40,000 docs |
Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
| Accuracy drops after deploy | Training and inference preprocess differ | Ship one preprocess function in the model |
| Tokens change after an upgrade | Library version drift | Pin versions. Freeze 20 sample sentences |
ponies stems to poni | Step 1a ies rule only | Use full Porter with step 1b |
watched stays watched | Lemma table has no ed rule | Use WordNet or spaCy |
| "not good" reads positive | Stopword list drops negations | stop_words=None plus bigrams |
| High score, bad minority class | Micro-F1 or accuracy on imbalance | Report macro-F1, AUPRC over 95-5 |
| LDA junk topics | Stopwords and frequent terms | Set min_df and max_df |
| Many docs in topic -1 | HDBSCAN outlier bucket | Check min_topic_size, read the -1 docs |