The headlines are about generative AI. Chatbots write code, paint pictures, and summarise legal documents. It is tempting to believe that large language models have swallowed the field and that classical machine learning is a relic. The reality is the opposite. Most production predictions — fraud detection, demand forecasting, recommendation ranking, spam filtering — still run on classical models: logistic regression, gradient-boosted trees, and small neural nets. They run on cheap CPUs, return answers in milliseconds, and cost pennies per million predictions. An LLM call costs a thousand times more and takes a hundred times longer. The future is not LLM-only; it is LLM-augmented, with classical ML doing the heavy lifting of structured prediction.
flowchart TD A["Production AI landscape"] --> B["Classical ML"] A --> C["Deep Learning"] A --> D["Foundation Models / LLMs"] B --> B1["Fraud, demand, ranking"] C --> C1["Vision, speech"] D --> D1["Text generation, reasoning"] style B fill:#166534,color:#fff style D fill:#2563eb,color:#fff
Classical machine learning excels at structured data with clear inputs and outputs. You have a spreadsheet of customer transactions and you want to flag fraud. You have a history of product sales and you want to predict next month's demand. You have sensor readings and you want to detect a failing machine. These tasks need speed, reliability, and interpretability. A gradient-boosted tree can train in minutes, run inference in microseconds, and expose which features mattered for each decision. Regulators like that. Finance teams like that. Site-reliability engineers like that. Classical ML is boring technology in the best sense: it works, it is cheap, and it does not surprise you at 3 a.m.
def predict_price(square_feet):
# A simple linear model: $100 per sq ft + $50k base
return 100 * square_feet + 50000
print(predict_price(1000))
print(predict_price(2500))
Large language models and generative AI shine where the input is messy, variable, and unstructured. A customer emails support with a vague complaint. A developer needs a summary of a fifty-page legal contract. A scientist wants to brainstorm hypotheses from a hundred research papers. These are natural-language tasks with no fixed schema, and that is exactly what transformers were built for. They understand context, nuance, and syntax in ways that classical models cannot. The mistake is not using LLMs for these tasks; the mistake is using them for tasks that do not need that power, like classifying a transaction as fraud when a logistic regression already scores 0.97 accuracy at one-thousandth the cost.
The economics are stark. A classical fraud model might cost ten dollars per month in compute and handle a million predictions a day. An LLM doing the same job might cost thousands of dollars per month and add hundreds of milliseconds of latency to every checkout. Worse, an LLM can hallucinate: it might invent a reason to decline a legitimate transaction because it is trying to be helpful and explanatory. Classical models are deterministic; the same inputs always yield the same score. In production, determinism is not a limitation — it is a feature. You cannot debug a model that gives different answers on Tuesday than it did on Monday.
flowchart LR A["Classical ML"] --> B["Cost: low"] A --> C["Latency: milliseconds"] A --> D["Deterministic: yes"] E["Foundation Model"] --> F["Cost: high"] E --> G["Latency: hundreds of ms"] E --> H["Deterministic: no"] style A fill:#166534,color:#fff style E fill:#b45309,color:#fff
def extract_features(text):
features = {
"length": len(text),
"exclamation_count": text.count("!"),
"question_count": text.count("?"),
}
return features
print(extract_features("Hello world!"))
print(extract_features("Really?"))
The most productive architectures are hybrids. A large language model reads a support ticket and extracts structured fields: product category, sentiment, urgency level. Then a classical classifier routes the ticket to the right team based on those fields. The LLM handles the unstructured text; the classical model handles the structured decision. This split plays to each tool's strength. The LLM is invoked once per ticket, not once per routing decision, so the cost stays low. The classifier is fast and auditable, so the business logic remains transparent. Separating extraction from decision also makes the system easier to test: you can unit-test the classifier with fixed inputs without calling the LLM at all.
Foundation models are not a replacement for the ML stack; they are a new layer on top of it. Embeddings — dense vectors that capture meaning — bridge the two worlds. You turn text into an embedding with a foundation model, then feed that vector into a classical model. The classical model learns a decision boundary in embedding space, which is far cheaper than fine-tuning the foundation model itself. This pattern, called embedding plus classifier, powers modern search, recommendation, and anomaly detection. It is the practical way to leverage generative AI without surrendering the cost and speed advantages of classical machine learning.
flowchart LR A["Unstructured input<br/>(email, ticket)"] --> B["LLM extracts<br/>structured features"] B --> C["Classical model<br/>makes decision"] C --> D["Fast, cheap,<br/>auditable output"] style B fill:#2563eb,color:#fff style C fill:#166534,color:#fff
What does this print? A straightforward linear prediction.
def predict_price(square_feet):
return 150 * square_feet + 20000
print(predict_price(1000))
150 times 1000 equals 150000.
150000 plus 20000 equals 170000.
Write extract_features(text) that returns a dict with three keys: 'length' (total characters), 'digit_count' (how many characters are digits), and 'upper_count' (how many characters are uppercase letters).
def extract_features(text):
# return {'length': ..., 'digit_count': ..., 'upper_count': ...}
pass
Use len(text) for the total length.
Use generator expressions with c.isdigit() and c.isupper().
def extract_features(text):
return {
"length": len(text),
"digit_count": sum(c.isdigit() for c in text),
"upper_count": sum(c.isupper() for c in text),
}
This spam-score function is meant to return a float between 0 and 1, but it returns a string. Fix it so it always returns a float.
def spam_score(text):
words = text.split()
spam_words = text.lower().count("free") + text.lower().count("win")
return str(spam_words / len(words)) if words else "0.0"
Remove the str(...) wrapper.
Return 0.0 as a number, not a string.
def spam_score(text):
words = text.split()
spam_words = text.lower().count("free") + text.lower().count("win")
return spam_words / len(words) if words else 0.0
Which task is BEST handled by a classical ML model rather than a foundation model?
Fraud detection on structured transaction data is fast, cheap, and highly accurate with classical models. The other tasks involve open-ended text generation better suited to LLMs.
Write route_ticket(ticket_text, urgency_keywords) that returns 'urgent' if any keyword (lowercase) appears in ticket_text (lowercase), otherwise returns 'standard'. This simulates the classical routing part of a hybrid system.
def route_ticket(ticket_text, urgency_keywords):
# return 'urgent' or 'standard'
pass
Convert ticket_text to lowercase before checking.
Return as soon as you find a match.
def route_ticket(ticket_text, urgency_keywords):
text_lower = ticket_text.lower()
for kw in urgency_keywords:
if kw in text_lower:
return "urgent"
return "standard"
Recap
- Classical ML runs most production predictions because it is fast, cheap, and deterministic.
- LLMs excel at unstructured text, reasoning, and creativity, but they are expensive and non-deterministic.
- The future is hybrid: LLMs extract structure from text, and classical models make fast, auditable decisions.
- Embeddings bridge the two worlds, letting you leverage foundation models without fine-tuning them.
- Do not replace a working classical model with an API call unless the LLM solves a problem the old system cannot.
Next you will learn the three major kinds of learning — supervised, unsupervised, and reinforcement — and when to use each.