Live Coding Interview Prep

Course Content

Live Coding Interview Prep

7 sections · 50 lessons

Build a supervised classifier using embedding features.


What you need to know

A pre-trained embedding model has already learned what text means. A linear classifier on frozen embeddings only has to learn where to draw boundaries between your classes in that space — a much easier job, which is why a few hundred examples are often enough.

Logistic regression learns one weight vector per class; the score for a class is a dot product with the text's vector, and softmax turns scores into probabilities. It is fast, hard to overfit with regularisation, and gives calibrated-enough probabilities for a confidence threshold.

Four practices matter more than the choice of model:

  • Stratified split — train and test keep the same class proportions, so a rare class is not missing from the test set.
  • Balanced class weights — errors on rare classes cost more, so the model does not ignore them.
  • Macro metrics — the average of per-class F1, so each class counts equally. Accuracy on a 95/5 split is 95% for a model that never predicts the rare class.
  • Deduplicate before splitting — the same message in train and test inflates the score.
Python
from collections.abc import Callableimport numpy as npfrom sklearn.linear_model import LogisticRegressionfrom sklearn.metrics import accuracy_score, f1_scorefrom sklearn.model_selection import train_test_splitdef build_features(texts: list[str], embed_fn: Callable, batch_size: int = 64) -> np.ndarray:    vectors = []    for i in range(0, len(texts), batch_size):        vectors.extend(embed_fn(texts[i:i + batch_size]))    X = np.asarray(vectors, dtype=np.float32)    return X / (np.linalg.norm(X, axis=1, keepdims=True) + 1e-10)def train_classifier(texts: list[str], labels: list[str], embed_fn: Callable, seed: int = 42):    """Train on 80%, report macro F1 and accuracy on the stratified 20% hold-out."""    texts, labels = map(list, zip(*dict(zip(texts, labels)).items()))   # dedupe texts    X, y = build_features(texts, embed_fn), np.asarray(labels)    X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=seed, stratify=y)    clf = LogisticRegression(max_iter=2000, class_weight="balanced").fit(X_tr, y_tr)    pred = clf.predict(X_te)    return clf, {"accuracy": round(accuracy_score(y_te, pred), 3),                 "macro_f1": round(f1_score(y_te, pred, average="macro"), 3)}def predict(clf, texts: list[str], embed_fn: Callable, min_confidence: float = 0.6):    probs = clf.predict_proba(build_features(texts, embed_fn))    return [(str(clf.classes_[i]) if p[i] >= min_confidence else "needs_review", round(float(p[i]), 3))            for p, i in zip(probs, probs.argmax(axis=1))]

The tricky parts:

  • dict(zip(texts, labels)) keeps one label per unique text (the last one seen), removing exact duplicates before the split so none leak across it.
  • clf.classes_ is the label order behind predict_proba's columns. Indexing it with the argmax gives the label name; assuming the order you passed in is a classic bug.
  • max_iter=2000 because the default can stop before convergence on high-dimensional embeddings and print a warning.
  • The same build_features is used for training and prediction, so normalisation is identical in both.

Complexity: embedding N texts is N model passes (batched). Training is roughly O(N·d·k) per optimiser iteration — seconds for tens of thousands of rows. Prediction is one embedding plus O(d·k). Memory is O(N·d) for the features.

A real-life example

Synthetic 16-dimensional "embeddings" for three ticket types stand in for a real embedding model, so the run is reproducible:

Python
rng = np.random.default_rng(0)topics = np.eye(16)[:3]names = ["billing", "delivery", "account"]data = {f"ticket {i}": (names[i % 3], topics[i % 3] + rng.normal(0, 0.25, 16)) for i in range(150)}fake_embed = lambda ts: [data[t][1] if t in data else EXTRA[t] for t in ts]EXTRA = {"charged and not delivered": (topics[0] + topics[1]) / 2 + rng.normal(0, 0.05, 16)}clf, report = train_classifier(list(data), [lab for lab, _ in data.values()], fake_embed)print(report)                                         # {'accuracy': 0.967, 'macro_f1': 0.967}print(predict(clf, ["ticket 4", "charged and not delivered"], fake_embed, min_confidence=0.7))# [('delivery', 0.813), ('needs_review', 0.642)]
stepdetail
data150 tickets, 50 per class, each a class direction plus noise
split120 train, 30 test, 10 of each class in the test set (stratified)
test resultaccuracy 0.967, macro F1 0.967 — 29 of the 30 test tickets right
"ticket 4"4 % 3 = 1 → a delivery ticket, predicted with probability 0.813
mixed tickethalfway between billing and delivery: top probability 0.642 (billing) is below 0.7 → needs review

Real embeddings are messier than this, and real scores are lower; the mechanics — split, balanced training, macro F1, a confidence gate — are the same.

E-commerce companies route millions of support tickets a month with exactly this: embeddings plus a linear classifier, retrained weekly, with low-confidence tickets sent to a human queue.

Follow-up questions to expect

  • "Why not fine-tune the embedding model instead?" — Fine-tuning can add a few points but costs GPU time and a deployment. Try the linear model first; fine-tune only if the gap matters.
  • "A class has only 3 examples — what breaks?" — stratify needs at least 2 per class to split, and the model barely learns it. Collect more examples or merge the class.
  • "How do you keep it accurate over time?" — Log low-confidence predictions and human corrections, add them to the training set, and retrain on a schedule, watching macro F1 on a fixed hold-out.