Natural Language Processing Basics
What Is NLP?
Natural Language Processing is the branch of AI/data science focused on enabling computers to read, interpret, and generate human language. It powers a huge range of everyday tools.
This lesson focuses on the foundational technique behind classical (pre-deep-learning) NLP: converting text into numbers a standard classifier can use, then reusing exactly the same LogisticRegression workflow from Lesson 25.
Text Preprocessing
Before any numbers get involved, raw text usually goes through a few cleanup steps to reduce noise and make similar words match each other.
Here's what preprocessing does to a sample sentence, step by step:
↓ lowercase + tokenize
↓ remove stopwords ("i", "this")
CountVectorizer and TfidfVectorizer (Section 4) both accept a lowercase=True parameter (the default) and a stop_words='english' parameter that handles lowercasing, tokenization, and stopword removal for you as part of fitting the vectorizer. Dedicated NLP libraries like NLTK or spaCy offer more advanced preprocessing (like stemming/lemmatizing "running" → "run"), but scikit-learn's built-in options are enough for the bag-of-words approach this lesson covers.The Bag-of-Words Concept
Bag-of-words is the simplest way to turn text into numbers: represent each document as a count of how many times each word appears, completely ignoring word order and grammar — as if the words were dumped into a bag and only their counts mattered.
| Document | good | movie | bad | great |
|---|---|---|---|---|
| "good movie" | 1 | 1 | 0 | 0 |
| "bad movie" | 0 | 1 | 1 | 0 |
| "good movie, great movie" | 1 | 2 | 0 | 1 |
Each row becomes a numeric feature vector, exactly like the rows in every dataset from earlier lessons — the "features" are simply "how many times did this specific word appear." That vector is what actually gets passed into LogisticRegression.fit() or any other scikit-learn model.
CountVectorizer and TfidfVectorizer
scikit-learn implements bag-of-words directly with CountVectorizer from sklearn.feature_extraction.text.
from sklearn.feature_extraction.text import CountVectorizer docs = [ "good movie", "bad movie", "good movie great movie", ] vec = CountVectorizer() X = vec.fit_transform(docs) # returns a sparse matrix print("Vocabulary:", vec.get_feature_names_out()) print("Dense matrix:\n", X.toarray())
fit_transform() returns a sparse matrix, a memory-efficient format that only stores the non-zero entries — essential once vocabularies grow into the thousands of words and most documents only use a tiny fraction of them. .toarray() converts it to a normal dense NumPy array for printing/inspection, but for real datasets that conversion can use huge amounts of memory — models like LogisticRegression accept the sparse matrix directly, so there's rarely a need to call .toarray() outside of teaching examples.TfidfVectorizer improves on plain counts with TF-IDF (Term Frequency-Inverse Document Frequency) weighting — words that appear in almost every document (even after stopword removal) get down-weighted, while words that are frequent in one document but rare across the whole collection get boosted, since they're more distinctive.
from sklearn.feature_extraction.text import TfidfVectorizer tfidf = TfidfVectorizer(stop_words='english', lowercase=True) X_tfidf = tfidf.fit_transform(docs) print("Vocabulary:", tfidf.get_feature_names_out()) print("TF-IDF matrix:\n", X_tfidf.toarray().round(3))
Worked Example — Sentiment Classification
This ties everything together: TF-IDF vectorizing a small set of illustrative sentences, then feeding the result straight into LogisticRegression from Lesson 25 — the exact same classifier, just fed text-derived features instead of numeric measurements.
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split # Illustrative, hand-labeled sentiment examples — 1 = positive, 0 = negative texts = [ "I absolutely love this movie", "This was a fantastic experience", "Best purchase I have made all year", "Amazing quality and great service", "Pretty good overall, happy with it", "I hate this so much", "Terrible, worst purchase ever", "This made me really angry", "Awful, would not recommend", "Disappointing and frustrating experience", ] labels = [1, 1, 1, 1, 1, 0, 0, 0, 0, 0] # Vectorize FIRST, then split — the vectorizer's vocabulary is learned from all text vec = TfidfVectorizer(stop_words='english', lowercase=True) X = vec.fit_transform(texts) X_train, X_test, y_train, y_test = train_test_split( X, labels, test_size=0.3, random_state=42 ) clf = LogisticRegression(max_iter=1000) clf.fit(X_train, y_train) print(f"Test accuracy: {clf.score(X_test, y_test):.2f}") # Classify brand-new text — must use .transform(), never .fit_transform() again new_review = vec.transform(["I really enjoyed this, would buy again"]) prediction = clf.predict(new_review)[0] probability = clf.predict_proba(new_review)[0] print("Predicted sentiment:", "Positive" if prediction == 1 else "Negative") print(f"P(negative)={probability[0]:.3f}, P(positive)={probability[1]:.3f}")
train_test_split, .predict_proba(), classification_report, and cross_val_score from earlier lessons all work identically on it. NLP's special ingredient is entirely in how the features get created, not in how the model afterward is trained or evaluated.Extend Section 5's texts/labels sentiment example for all three tasks.
Add 4 new sentences to
texts (2 positive, 2 negative) with matching entries in labels, then re-run the full pipeline from Section 5. Does the vocabulary size (len(vec.get_feature_names_out())) grow as expected?
Repeat the Section 5 pipeline, but swap
TfidfVectorizer for CountVectorizer (same stop_words='english' setting). Does the test accuracy on your expanded dataset change?
vec.get_feature_names_out() for your fitted vectorizer and manually check: did stopword removal actually drop words like "this," "was," and "and" from the vocabulary?
💡 Show hints if you're stuck
- Task 1: Keep the same 1/0 label convention — 1 for positive, 0 for negative — and make sure
len(texts) == len(labels)still holds. - Task 2:
vec = CountVectorizer(stop_words='english'); X = vec.fit_transform(texts)— everything else in the pipeline stays identical. - Task 3:
print(sorted(vec.get_feature_names_out()))— common English stopwords should be absent from the printed list.