{"id":"l-ml-5-3","courseId":"ml-crash-course","lang":"en","kind":"lesson","summary":"Explain what a large language model and an embedding are, compare texts with cosine similarity, and decide when an LLM, embeddings or a classic scikit-learn model is the right tool.","html":"<p>Large language models have changed what software can do with text, and every data team now gets asked “can’t we just use an LLM for this?”. Sometimes the answer is yes. Often it’s “for part of it”. To answer well, you need an accurate picture of what LLMs and embeddings are, and that picture is built from ideas you’ve used all course.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">What an LLM is</h2>\n<p>A <strong>large language model</strong> is a neural network, almost always a <strong>transformer</strong>, trained on an enormous amount of text with one objective: given the text so far, predict the next <strong>token</strong> (a word or a piece of a word). That’s <strong>pretraining</strong>. Done at sufficient scale, next-token prediction forces the network to absorb grammar, facts, styles and a surprising amount of reasoning-like behaviour, because all of it helps predict what comes next.</p>\n<p>A pretrained model then gets further training to follow instructions and to give helpful, safe answers, usually with human-written examples and human or AI preference feedback. When you send it a prompt, it computes a probability for every possible next token, picks one (with some randomness, controlled by a <strong>temperature</strong> setting), appends it, and repeats.</p>\n<p>Three consequences matter for a data scientist:</p>\n<ul>\n<li><strong>The output is generated text, not a measured probability.</strong> If you ask “how likely is this customer to churn?” and it says “70%”, that number hasn’t been calibrated against outcomes the way <code dir=\"ltr\">predict_proba</code> was in section 3.</li>\n<li><strong>It can be confidently wrong.</strong> Fluency isn’t accuracy. Anything you build on an LLM needs a labelled test set and a baseline, exactly like a churn model.</li>\n<li><strong>It costs per call</strong> in money and latency, and it may send data outside your systems. A logistic regression scores a million customers in under a second on a laptop.</li>\n</ul>\n<h2 id=\"sec-2\" tabindex=\"-1\">Embeddings: text as coordinates</h2>\n<p>The more quietly useful idea is the <strong>embedding</strong>. An embedding model reads a piece of text and returns a fixed-length vector, often several hundred to a few thousand numbers, arranged so that texts with similar meaning get nearby vectors. Cartwheel’s support tickets become rows of numbers, and everything you know about rows of numbers applies.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 640 280\" role=\"img\" aria-labelledby=\"l-ml-5-3-t1\"><defs><marker id=\"arrow-l-ml-5-3-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-ml-5-3-t1\">A two-dimensional sketch of embedding space. Delivery complaints cluster in one corner, billing complaints in another, and an address-change question sits apart. A new ticket about a missing parcel lands next to the delivery cluster.</title>\n  <rect class=\"d-box\" x=\"20\" y=\"20\" width=\"600\" height=\"240\" rx=\"12\"/>\n  <circle class=\"d-dot\" cx=\"120\" cy=\"80\" r=\"7\"/><circle class=\"d-dot\" cx=\"150\" cy=\"105\" r=\"7\"/>\n  <text class=\"d-label\" x=\"165\" y=\"78\">parcel never arrived</text>\n  <text class=\"d-label\" x=\"165\" y=\"112\">package did not show up</text>\n  <circle class=\"d-dot\" cx=\"460\" cy=\"190\" r=\"7\"/><circle class=\"d-dot\" cx=\"490\" cy=\"215\" r=\"7\"/>\n  <text class=\"d-label\" x=\"300\" y=\"182\">charged twice</text>\n  <text class=\"d-label\" x=\"310\" y=\"235\">refund duplicate payment</text>\n  <circle class=\"d-dot\" cx=\"420\" cy=\"70\" r=\"7\"/>\n  <text class=\"d-label\" x=\"435\" y=\"75\">change delivery address</text>\n  <rect class=\"d-box-warn\" x=\"92\" y=\"138\" width=\"14\" height=\"14\"/>\n  <text class=\"d-label-strong\" x=\"114\" y=\"152\">new: \"where is my order?\"</text>\n  <path class=\"d-arrow\" d=\"M105 138 L128 112\" marker-end=\"url(#arrow-l-ml-5-3-1)\"/>\n</svg><figcaption>Texts with similar meaning sit close together in embedding space</figcaption></figure>\n<p>Closeness is measured with <strong>cosine similarity</strong>: the cosine of the angle between two vectors, 1 for the same direction, near 0 for unrelated. Compare that with the word-count features scikit-learn can build on its own:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"python\" data-run=\"python\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Python</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"python\" data-index=\"0\" aria-label=\"Run this example\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> numpy </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> np</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.feature_extraction.text </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> TfidfVectorizer</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.metrics.pairwise </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cosine_similarity</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">tickets </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"My parcel never arrived\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"The package did not show up\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">           \"I was charged twice for one order\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Refund the duplicate payment please\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">           \"How do I change my delivery address\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">tfidf </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> TfidfVectorizer().fit_transform(tickets)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"TF-IDF similarity of ticket 0 to the others:\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, cosine_similarity(tfidf)[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].round(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># Illustrative 4-number \"embeddings\" (real ones come from an embedding model and are far longer)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">emb </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> np.array([[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.90</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.00</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.20</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">], [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.85</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.15</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.05</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">                [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.05</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.90</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.00</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">], [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.85</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.20</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.05</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">                [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.30</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.00</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.90</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]])</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"embedding similarity of ticket 0 to the others:\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, cosine_similarity(emb)[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].round(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">))</span></span></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p>TF-IDF counts shared words. “Parcel never arrived” and “package did not show up” share none, so it scores them 0, yet it links ticket 0 to the address question because both contain “my”. Embeddings score the two delivery complaints as near-identical. The vectors here are hand-made to show the idea; a real embedding model produces this behaviour from its training, for texts it has never seen.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Treating an LLM answer as a measurement</span></p>\n<p>Asking a chat model to score 500 customers “from 0 to 100 for churn risk” gives you numbers that look like model output but have never been checked against outcomes. Before any LLM-produced label or score drives a decision, compare it with ground truth on a held-out sample and with a simple baseline. The workflow from section 1 applies unchanged.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Where each tool fits at Cartwheel</h2>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"Scrollable table\" tabindex=\"0\"><table><thead>\n<tr>\n<th>Task</th>\n<th>Good fit</th>\n<th>Why</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Predict churn from customer facts</td>\n<td>scikit-learn on the table</td>\n<td>Labelled tabular data, needs calibrated, cheap, testable scores</td>\n</tr>\n<tr>\n<td>Route support tickets to teams</td>\n<td>Embeddings + logistic regression, or an LLM</td>\n<td>Text input; embeddings make it a classic classifier</td>\n</tr>\n<tr>\n<td>Find tickets similar to a new one</td>\n<td>Embeddings + nearest neighbours</td>\n<td>Similarity search is what embeddings are for</td>\n</tr>\n<tr>\n<td>Summarise a week of complaints for a manager</td>\n<td>An LLM</td>\n<td>Free-form text output; a person reviews it</td>\n</tr>\n<tr>\n<td>Add “complaint topic” as a churn feature</td>\n<td>Embeddings → k-means, or an LLM label</td>\n<td>Turns text into a column the churn model can test</td>\n</tr>\n</tbody>\n</table></div><p>The last row is where the two worlds meet. Text becomes a feature; cross-validation decides whether it helps. Nothing about the discipline changes because the feature came from a large model.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Start from the data type</span></p>\n<p>Tables of facts: scikit-learn. Free text you need to search, group or classify: embeddings first, because they’re cheap and slot into tools you know. Text you need to generate, or tasks with no labelled data at all: an LLM, evaluated against a sample you labelled yourself.</p>\n</aside>\n<p>Whichever model you ship, it will make decisions about people. Next lesson asks whether those decisions are equally good for everyone, and what to watch after launch.</p>\n","toc":[{"id":"sec-1","text":"What an LLM is","level":2},{"id":"sec-2","text":"Embeddings: text as coordinates","level":2},{"id":"sec-3","text":"Where each tool fits at Cartwheel","level":2}],"takeaways":["A large language model is a transformer network pretrained to predict the next token on huge amounts of text, then tuned to follow instructions.","An LLM’s output is sampled text, not a calibrated probability, so it needs the same evaluation discipline as any model: a labelled test set and a baseline.","An embedding model maps a text to a fixed-length vector so that texts with similar meaning land close together; cosine similarity measures how close.","Embeddings turn unstructured text into features that classic tools (k-NN, logistic regression, k-means) can use.","For tabular predictions such as churn, scikit-learn models stay cheaper, faster and easier to validate; LLMs and embeddings earn their place on text."],"further":[{"title":"Feature extraction from text","url":"https://scikit-learn.org/stable/modules/feature_extraction.html#text-feature-extraction"},{"title":"cosine_similarity","url":"https://scikit-learn.org/stable/modules/generated/sklearn.metrics.pairwise.cosine_similarity.html"}],"quiz":[{"q":"<p>What is a large language model fundamentally trained to do during pretraining?</p>","options":[{"text":"Look up answers in a database of verified facts.","why":"LLMs don’t consult a fact database by default; they generate text from patterns learned in their weights."},{"text":"Classify documents into a fixed set of topics.","why":"Classification can be done with an LLM, but it isn’t the pretraining objective."},{"text":"Translate between languages only.","why":"Translation is one emergent ability, not the training objective."},{"text":"Predict the next token of text, over and over, on a huge corpus.","why":"Correct. Instruction following and chat behaviour come from further tuning on top of that."}],"answer":3},{"q":"<p>TF-IDF gives ‘My parcel never arrived’ and ‘The package did not show up’ a cosine similarity of 0. An embedding model gives them about 0.9. Why the difference?</p>","options":[{"text":"TF-IDF only matches shared words, and these two share none; embeddings place texts by meaning.","why":"Correct. Synonyms and paraphrases are exactly where embeddings beat word counts."},{"text":"TF-IDF is broken for short texts.","why":"It works as designed; it just has no notion that ‘parcel’ and ‘package’ mean the same thing."},{"text":"The embedding model memorised these two sentences.","why":"Embedding models generalise from training on huge text collections; they don’t need to have seen these exact sentences."}],"answer":0},{"q":"<p>Cartwheel wants to predict churn from the eight numeric columns. A colleague suggests sending each customer’s row to an LLM and asking ‘will they churn?’. What’s the strongest objection?</p>","options":[{"text":"LLMs can’t read numbers.","why":"They can read numbers in text; the problems are calibration, cost and validation, not reading."},{"text":"It would be less accurate than any baseline by definition.","why":"Nothing guarantees that; you’d have to measure. The point is you’d pay more for something no better validated."},{"text":"The task already has labelled tabular data, where a scikit-learn model is cheaper, faster, testable and gives real probabilities.","why":"Correct. Use the tool that fits the data; an LLM adds cost and uncertainty with no clear gain here."},{"text":"LLMs are only allowed for chat applications.","why":"There’s no such rule; LLMs are used for many tasks. The question is fit, not permission."}],"answer":2}],"exercise":{"kind":"choice","multiple":true,"options":["review-search","refund-forecast","ticket-topic-feature","llm-churn-score","review-summary","coupon-ab"],"correct":["review-search","ticket-topic-feature","review-summary"],"title":"Which tasks call for embeddings or an LLM?","prompt":"<p>Cartwheel’s data team has six requests. Select every one where <strong>embeddings or an LLM</strong> is a better first tool than a classic scikit-learn model on the existing tables.</p>\n","hints":["Ask what the input is: a table of facts, or free text?","Ask what the output is: a calibrated number, or text or similarity?","Three requests involve free text; the other three are tabular or are experiments."],"explanation":"<p>Searching reviews by meaning, turning ticket text into a topic feature, and summarising reviews are all text problems: embeddings handle the first two cheaply, and an LLM is the natural tool for a written summary a person will read. Forecasting refunds and scoring churn are tabular predictions where scikit-learn gives cheap, testable, calibrated outputs. The coupon question is an experiment to analyse, not a prediction task at all.</p>\n","checkLabels":{},"optionText":{"review-search":{"text":"Let staff type “arrived broken” and find product reviews that describe damage in any wording.","why":"Good fit: embeddings + nearest-neighbour search match meaning, not exact words."},"refund-forecast":{"text":"Forecast next month’s refund amount from order and return tables.","why":"Tabular regression with numeric history; a scikit-learn model (with a time-based split) fits."},"ticket-topic-feature":{"text":"Turn free-text support tickets into a ‘complaint topic’ column to test as a churn feature.","why":"Good fit: embed the tickets, cluster or classify them, then let cross-validation judge the new column."},"llm-churn-score":{"text":"Ask a chat model to rate each customer’s churn risk from their eight numeric columns.","why":"The labelled table already supports a cheaper, calibrated, testable model; the LLM adds cost and uncertainty."},"review-summary":{"text":"Write a one-paragraph weekly summary of new product reviews for the merchandising team.","why":"Good fit: generating readable text from text is what LLMs do; a person reviews the result."},"coupon-ab":{"text":"Decide whether the SPRING15 coupon increased order value, using a randomised test.","why":"That’s experiment analysis (compare groups, check significance), not a modelling problem for an LLM."}}},"words":748,"readMinutes":4}