learn-feature-engineering-with-phoebe / Practitioner session 9 of 10
Learn Feature Engineering with Phoebe · Practitioner track · Session 9 of 10

Embeddings as features

Eight sessions of hand-crafted craft, and one column has been sitting untouched the whole time: support_note, free text, unusable by anything you have built so far. Tonight you encode it and bolt the result onto the same tabular matrix feeding the same logistic regression from b1. This is a bridge session, not a conversion: embeddings do not replace the craft, they extend it to the columns the craft cannot reach - and they arrive with their own cost, their own interpretability bill, and their own leakage rails.

🔴 Practitioner track DS · analysts · ML engineers Python · live workbench on key pages 45 min
0-3 · Welcome 3-18 · When hand-crafting stops 18-42 · Build-along: support_note becomes a feature 42-45 · Q&A
Part 1 · covers Google MLCC "Embeddings" + glossary, Rules of ML #17

Start with what you observed 8 min live

Before a single word about embeddings, the counterweight, and it is Google's own: Rule 17 - "Start with directly observed and reported features as opposed to learned features." That rule is why this session is number nine and not number two. Everything in b2 to b8 is directly observed: a spend figure, a channel, a weekday, a count. Learned features are the ones a model invented for you, and they buy real capability at the price of everything you liked about a hand-crafted column.

So the honest question is not "should we use embeddings?" It is narrower and much easier to answer: is there a column here that hand-crafting genuinely cannot reach? On Lumen there is exactly one. support_note is free text a customer typed. There is no scaler for it, no sensible one-hot, no decomposition. It is either encoded or it is dropped, and for eight sessions we have been dropping it.

LiveWhy one-hot runs out on text5 min▶

Google's crash course gives two reasons, and both matter more for text than for anything you encoded in b3:

  • Width. With M one-hot entries feeding N first-layer nodes, "the model has to train MxN weights for that layer." A vocabulary is not nine channels; it is tens of thousands of entries, and the weight count follows it up.
  • Meaninglessness. One-hot encodings "lack meaningful relationships." Every category sits the same distance from every other, so "hot dogs and shawarmas" are exactly as unrelated as "hot dogs and salads" - which is the opposite of what you want a model to know about food, or about a customer note.

An embedding fixes both by being small and by placing similar things near each other. Google's glossary is precise about the size: an embedding vector is "A relatively low-dimensional vector of floating-point values (typically, between 5 and 500)." Low-dimensional relative to the vocabulary, not relative to your table - 384 columns is still 384 columns, and Part 3 prices that.

And then the line that makes this a feature-engineering session rather than an NLP session, from OpenAI's embeddings guide: "An embedding can be used as a general free-text feature encoder within a machine learning model." That is the whole idea. The embedding is not the model. It is a transformer that turns a column you could not use into columns you can, and everything downstream stays exactly as it was.

TEXT COLUMN support_note "gift wrap?" 1 column, free text ENCODER all-MiniLM-L6-v2 EMBEDDING VECTOR [0.12, -0.44, 0.07, ... 0.31] shape [n, 384] HAND-CRAFTED (b2-b8) scaled numerics (b2) one-hot channel (b3) tenure, weekday (b4) ONE MATRIX 384 embedding cols + crafted columns feeding the SAME logistic regression you built in b1 Two lanes, one matrix. The embedding extends the craft; it does not replace it.
🔍 Click to zoom - the text lane joins the tabular lane, and the model never notices
Where this session sits in the track Representation learning proper - training the encoder as part of your model - belongs to learn-deep-learning-with-phoebe. Here the encoder is a fixed, pretrained transform you call once, exactly like a scaler you fitted somewhere else. That framing is what keeps this a feature-engineering lesson.
Part 2 · covers OpenAI embeddings guide + Sentence Transformers

support_note becomes 384 columns 9 min live

The code is shorter than the discussion. Encode the text, stack the result beside the matrix you already had, fit the same estimator. Note where the split sits - first, as always, because an encoder call is still a transform and Part 4 is about what happens when you forget that.

LiveThe build-along, end to end6 min▶
from sentence_transformers import SentenceTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
import numpy as np

X_train, X_test, y_train, y_test = train_test_split(      # split FIRST
    df, y, test_size=0.4, stratify=y, random_state=42)

encoder = SentenceTransformer("all-MiniLM-L6-v2")         # pin this string

E_train = encoder.encode(X_train["support_note"].fillna("").tolist())
E_test  = encoder.encode(X_test["support_note"].fillna("").tolist())
print(E_train.shape)                                      # (n, 384)

Z_train = np.hstack([prep.fit_transform(X_train), E_train])   # b2-b8 lane
Z_test  = np.hstack([prep.transform(X_test),      E_test])    # transform only

clf = LogisticRegression(max_iter=1000).fit(Z_train, y_train)

Swap the first two lines for an API call and nothing else changes: you get back a list of float vectors, you np.hstack them, the estimator is untouched. That interchangeability is the practical form of OpenAI's "general free-text feature encoder" line - the encoder is a component, not a commitment.

LiveThree practical notes before you ship this5 min▶
  • Dimensionality against rows. Rule 21: "The number of feature weights you can learn in a linear model is roughly proportional to the amount of data you have." 384 new columns against Lumen's 40,000 rows is comfortable. 3,072 columns against 4,000 rows is a different conversation, and the answer there is a smaller encoder or a shortened output, not more regularization bolted on afterwards.
  • Cache the embeddings. Text does not change; a support note written in November is the same string every time you retrain. Encode once, store the vectors keyed by a hash of the text, and you pay the cost exactly one time instead of once per experiment. This is the single biggest difference between a cheap embedding pipeline and an expensive one.
  • Pin the model version. A new encoder is a different feature space. The columns keep their names and their shape, the numbers mean something else entirely, and a model trained on the old vectors scored against the new ones will degrade quietly. Treat the encoder identifier as part of the feature definition - which is field 4 of the spec you write in b10.
Self-studyWhat the workbench cannot show you2 min read▶

Plainly: this page has no live workbench, because the in-browser workbench hosts Lumen's tabular features only and cannot run a text encoder. The ladder demo and the encode demo return in b10 for the capstone. Tonight the code above is the demonstration, and it runs on your laptop rather than in the slide.

Part 3 · covers OpenAI dimensions/pricing + Sentence Transformers hosting

What it costs, in dollars and in explanation 8 min live

Two bills arrive with an embedding feature. The first is money and latency, and it is easy to compare. The second is interpretability, and it is the one that shows up months later in a review meeting.

What each encoder adds to your matrix, one text column in bar length = columns added per row text-embedding-3-large 3072 9,615 pages per dollar, roughly 6.5x the cost text-embedding-3-small 1536 62,500 pages all-MiniLM-L6-v2, local 384 free, but you host it - 10,000+ pretrained models 3-large shortened to 256 256 still beats an unshortened ada-002 at 1536 Rule 21: 384 columns against Lumen's 40,000 rows is comfortable; 3,072 against 4,000 rows is not Cache the embeddings: encode once, keyed by a hash of the text, and pay the cost exactly one time.
🔍 Click to zoom - the columns you add are a claim on the rows you have
OptionDimsCostWhen it is the right call
text-embedding-3-small153662,500 pages per dollarThe default hosted choice. Cheap enough that caching is a convenience rather than a necessity.
text-embedding-3-large30729,615 pages per dollarRoughly 6.5x the cost per page and twice the columns. Justify it with a measured lift, not a hunch.
3-large + dimensions256same per page, fewer columnsThe parameter exists to "trade-off performance and cost". A 3-large embedding "shortened to a size of 256" still outperforms an unshortened ada-002 at 1536.
all-MiniLM-L6-v2 (local)384free, but you host itNo per-call bill, no data leaving your network, 10,000+ pretrained models on Hugging Face to choose from. You own the serving.
LiveThe interpretability bill, stated plainly5 min▶

Here is what actually happened to your feature set. You traded one explainable column for hundreds of anonymous ones. Before: "support_note mentions a gift." After: "dimension 217 is 0.043." No stakeholder will ever ask about dimension 217, and no one can answer if they do.

What you traded: one named column for hundreds of anonymous ones BEFORE · 1 explainable column support_note mentions a gift a stakeholder can ask about it, and you can answer a named column tells you where to look AFTER · 384 anonymous columns dimension 217 is 0.043 nobody will ask about dimension 217 and nobody can answer if they do THE COMPROMISE · KEEP BOTH, THE FLAGS COST NOTHING note length contains a question mark a keyword list fires Permutation importance still works on the block as a whole: keep the text feature, or drop it. When the embedding adds no measured lift over the flags, you just saved an encoder in production.
🔍 Click to zoom - the second bill arrives months later, in a review meeting

Three places this bill comes due:

  • The feature review. The leader track (a5, a6) teaches executives to ask "what does this feature mean and who owns it?" For 384 dimensions the honest answer is "the encoder means it," which is a defensible answer only if you say it out loud in advance.
  • Regulated settings. Anywhere a decision has to be explained to the person it affects, an anonymous 384-column block is a governance problem before it is a modelling one. Hand-crafted features are not merely nostalgic there; they are the compliance artifact.
  • Debugging. When performance drifts, a named column tells you where to look. A dimension does not. Permutation importance (b8) still works on the block as a whole, which is usually the granularity you can actually act on: keep the text feature, or drop it.
A practical compromise Keep both. Derive two or three hand-crafted flags from the same text - note length, whether it contains a question mark, whether a keyword list fires - and carry them alongside the embedding block. They cost nothing, they are explainable, and when the embedding adds no measured lift over them you have just saved yourself an encoder in production.
Part 4 · covers sklearn "Common pitfalls", applied to text

The leakage rails for embeddings 9 min live

Every rule from b5 still applies, and text gives them new hiding places. sklearn's definition has not changed: information "that would not be available at prediction time." A text column obeys it exactly like a numeric one - the difference is that text feels like context rather than data, so people forget to ask when it was written.

Rail 1 - the text must exist at prediction time. A support_note written after the order is target leakage, the b5 family-1 shape precisely: a column that only exists because the outcome already happened. Ask the same audit question you ask of every other column - "was this value knowable before the thing I am predicting?" - and be strict, because a well-written note is exactly the kind of feature that dominates importance and feels magically good.

Rail 2 - fit any dimensionality reduction on training data only. PCA or SVD over the full embedding matrix, all rows, before the split is family-2 preprocessing leakage. It is invisible in the output, it raises no error, and the test rows quietly voted on their own components. Same fix as always: inside the Pipeline, fit_transform on train, transform on test.

Rail 3 - a fine-tuned encoder can be a leak. If the encoder was fine-tuned on your labels, the vectors carry target information into every row that produced them, including the rows you later call held-out. This is the target-encoding failure from b5 wearing a much larger coat: naive encoding of a random postal code scored train 0.931 and held-out 0.493 for exactly this reason. A pretrained, frozen, off-the-shelf encoder has never seen your target and is safe by construction.

And the splits still matter. Text does not exempt you from family 3 or family 4. If notes accumulate over time, split by time. If the same customer writes several, split by customer - otherwise the model memorizes a writing style and you will not find out until a genuinely new customer arrives.

Real world

The resolution summary. A support team embedded a "resolution summary" field into a churn model and got a spectacular offline score - the best the team had ever seen, across every fold. The summary was written when the case closed. It described the outcome in fluent prose, and the encoder faithfully turned that prose into 768 excellent predictors of a thing that had already happened. No cross-validation can catch this: the column is contaminated in every fold, so every fold agrees. The only thing that catches it is field 3 of a feature spec, which is what b10 makes you write.

Homework

Try it yourself - this week ◐ 30-40 min total

Source material

Official sources covered

Certificates and graded exercises stay with the official providers (linked each session). This page covers:

✓Google MLCC "Embeddings" + glossary - the one-hot problem and the vector definitionPart 1 · MxN weights, "lack meaningful relationships", 5 to 500 dimensions
✓OpenAI embeddings guide - the free-text feature encoder framing, dims and pricingParts 1 and 3 · 3-small 1536, 3-large 3072, the dimensions parameter
✓Sentence Transformers - local encoding, all-MiniLM-L6-v2Part 2 build-along · 384 dims, 10,000+ pretrained models, you host it
◐Google "Rules of Machine Learning" - rules 17 and 21Part 1 counterweight and the dimensionality note in Part 2
◐sklearn "Common pitfalls" - leakage applied to text and to reduction stepsPart 4 · the full leakage treatment is b5; the spec field is b10
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · Why does this course teach embeddings in session 9 rather than session 2?

Google's Rule 17 is the counterweight to the whole session. Directly observed features are cheaper, explainable, and debuggable. Reach for a learned feature when a column genuinely cannot be hand-crafted - on Lumen that is exactly one column, support_note.

2 · What do embeddings buy you that one-hot encoding of text cannot?

Google's two objections to one-hot on text: with M entries and N first-layer nodes "the model has to train MxN weights for that layer", and every category sits equidistant from every other, so "hot dogs and shawarmas" are no closer than "hot dogs and salads". An embedding is low-dimensional - "typically, between 5 and 500" - and similar text lands nearby.

3 · Your encoder was fine-tuned on your own labels before being used to embed the training text. What is the problem?

A correct split does not save you here, exactly as it did not in b5's Demo 2: naive target encoding of a random postal code scored train 0.931 and held-out 0.493. A pretrained, frozen encoder has never seen your target and is safe by construction.

Practitioner session 9 cheat sheet · pin this

Rule 17 first"Start with directly observed and reported features as opposed to learned features." Embeddings extend the craft; they do not replace it.
The bridge quote"An embedding can be used as a general free-text feature encoder within a machine learning model" (OpenAI). Same model, new columns.
Why not one-hotM entries x N nodes means "the model has to train MxN weights for that layer", and one-hot "lack meaningful relationships".
What a vector is"A relatively low-dimensional vector of floating-point values (typically, between 5 and 500)" (Google glossary).
The buildencode(text) then np.hstack onto the tabular matrix, fit the same logistic regression. all-MiniLM-L6-v2 gives shape [n, 384].
Cost, in numbers3-small 1536 dims, 62,500 pages/$ · 3-large 3072, 9,615 pages/$ · 3-large shortened to 256 still beats ada-002 at 1536 · MiniLM 384, free but self-hosted.
Interpretability billOne explainable column traded for hundreds of anonymous ones. Keep two or three hand-crafted flags from the same text as a baseline.
Three leakage railsText must exist at prediction time · fit reduction on train only · never fine-tune the encoder on your labels. Pin the encoder version.