TLDR
- A score is not a decision. A model estimates a quantity under a dataset, objective and sampling scheme. Policy, evidence, human authority and downstream consequences determine the action.
- Start with the learning contract: target, observation unit, prediction time, eligible evidence, error costs and abstention route. Algorithm choice comes later.
- Validation must resemble use. Random splits can leak time, entities and future information into the past. Report uncertainty, subgroup behaviour, calibration and decision impact, not one headline metric.
- Complexity must earn its place. Linear models, trees and nearest neighbours remain valuable because they expose baselines, failure modes and operating trade-offs.
- The operating model includes monitoring and recovery. Data drift, score drift, policy changes and delayed outcomes need separate owners, thresholds and routes.
Reader and route
This edition is for engineers, data scientists, architects, product leaders and control practitioners who need to connect machine-learning mechanics to accountable decisions. Parts I and II establish learning, mathematics, model families and optimisation. Part III covers data preparation and evaluation. Part IV treats deep, specialised, unsupervised and ranking systems. Part V turns the material into design reviews and a governed lending-memo lab.
Evidence boundary
Merehaven Bank is wholly fictional. Every customer, dataset, metric, incident, committee and architecture attributed to it is synthetic. Library interfaces and code fragments are learning specimens; pin the selected runtime and verify current official documentation before use. The edition explains general methods and does not reproduce or imitate any named writer’s voice.
The delayed-outcome problem
Suppose a model estimates the probability that a small business will miss a repayment. The score arrives today; a reliable outcome may arrive months later. The credit team sees a neat decimal and a ranked list. That apparent precision hides at least six choices: who entered the training population, what counted as default, which evidence existed at prediction time, how missing data was encoded, what errors cost, and where a human may override or abstain.
The model can be statistically competent and operationally wrong. It can rank well but be badly calibrated. It can be calibrated overall while failing a small segment. It can pass an offline test and drift after policy changes. The useful object is therefore the whole decision system, not the fitted estimator.
The learning contract
Before training, write a compact contract with six fields: decision, unit, horizon, target, eligible evidence and error costs. Add a seventh field for abstention. This contract turns modelling into a falsifiable engineering task and provides the spine for evaluation, governance and monitoring.
Part I: Define the learning problem
A model can only be evaluated against a declared problem. The opening chapters turn observations, labels, probability and optimisation into an explicit learning contract.
Start with the learning contract
Why do we call it “learning” at all?
Machine learning fits parameters from examples so that a declared mapping can be tested on observations that were not used for fitting. The word learning names that parameter-estimation process; it does not imply understanding, intention or authority.
A useful specification names the observation unit, target, prediction time, eligible evidence, loss, validation population and abstention route. Without those fields, an apparently strong model may be answering a different question from the one the operating decision requires.
Notice what is missing from that definition. There is no mention of neurons. No mention of GPUs. No mention of large language models, attention, or any of the vocabulary that tends to dominate the conversation. Machine learning is older, broader, and stranger than any single technique. A decision tree trained on twelve features of a small business loan is machine learning. A logistic regression on credit bureau data is machine learning. A 400-billion-parameter transformer reading a management accounts PDF is also machine learning. The family is held together by one idea: the program’s behaviour comes from data rather than from rules that a person wrote down.
Think of it as the difference between two ways of building a fraud detection system.
Way one, rules-based. A senior fraud analyst sits with a business analyst for six weeks. They write things like: “If the transaction is over £500 and the merchant country is not the card country and the card has not been used abroad in the last ninety days, flag for review.” They end up with about two hundred rules. The rules run in a decision engine. When fraud patterns change, the analyst writes new rules. Everything the system does, a human somewhere chose.
Way two, learned. An engineer gathers five years of historical transactions, with a label on each one indicating whether it was later confirmed as fraudulent. She feeds the lot into a model. The model figures out, on its own, which combinations of features predict fraud. When she looks at what the model has learned, she sometimes sees things that surprise even the senior fraud analyst. Nobody wrote those rules. The model extracted them from the data.
Both ways can catch fraud. The second way scales in a particular direction: as you feed it more data, it can keep improving, because the model is not bottlenecked by the analyst’s time. That’s why, in practice, every large bank today runs both approaches in parallel, with rules doing the things that regulators insist on seeing explicitly and learned models doing the things that are too subtle or too fluid for rules to capture.
Read this as two parallel pipelines. The top one depends on a human to write rules, so its ceiling is the human’s attention. The bottom one depends on data and an algorithm, so its ceiling is the data’s quality and the algorithm’s capacity.
Formally, what we mean by “the machine learned” is this: there exists an algorithm A and a dataset D such that A(D) produces a function f, and f, when fed new inputs drawn from the same kind of world as D, produces useful outputs. That is literally all that “learning” means in this context. There is no consciousness, no understanding, no intent. Just a function built from examples.
Where this goes wrong. The most common failure mode of machine learning in banking is not model accuracy. It is model applicability. A team spends six months building a beautiful model that predicts default on SME term loans from 2015 to 2019, then deploys it in 2024 after two years of post-pandemic shocks have reshuffled the distribution of customers, and the model quietly stops working because the world it was trained on is no longer the world it operates in. This failure mode has a name: distribution shift. We will return to it repeatedly. For now, remember that a learned function is only as good as the match between the data it learned from and the data it will see.
If asked: “What’s the difference between machine learning and traditional software?”
What makes a learning problem “supervised”?
The word “supervised” is a bit misleading, and that’s worth ten minutes of clarity.
It comes from the following picture. Imagine a graduate student in machine learning in the late 1980s, sat in a basement at Carnegie Mellon, trying to build a program that can tell handwritten digits apart. She has a drawer full of index cards, each with a digit scrawled on it, and on the back of each card she has written what the digit actually is. She shows the front of a card to her program. The program guesses. She flips the card, checks the back, and if the program got it wrong, she tells it so, and her training procedure nudges the program’s internal state in a direction that would have produced the right answer. The backs of the cards are the “supervision.” A teacher is telling the student whether each answer is right or wrong.
That’s supervised learning. The formal setup is this. You have a dataset that looks like pairs:
{(x1, y1), (x2, y2), …, (xN, yN)}
Each xi is a feature vector, which is just a list of numbers describing one example. Each yi is a label, the correct answer for that example. The goal is to produce a function f such that, for a new feature vector x that was not in the training set, f(x) is a good guess for what the label should be.
Let’s make this concrete for credit risk. Imagine you are building a probability-of-default model for small business term loans In the synthetic Merehaven case. Each “example” is a loan that was originated some time in the last ten years. For each loan you have a feature vector that might look like this:
| Feature name | Symbol | Example value |
|---|---|---|
| Loan amount (£k) | x(1) | 250 |
| Term (months) | x(2) | 60 |
| Debt service coverage ratio | x(3) | 1.45 |
| Years trading | x(4) | 7 |
| Sector code (one-hot bucket) | x(5) | 3 |
| LTV on secured collateral | x(6) | 0.62 |
| Director’s Experian score | x(7) | 820 |
So xi is the vector (250, 60, 1.45, 7, 3, 0.62, 820) for one loan i. Notice the convention: the subscript i indexes the loan, and the superscript (j) in parentheses indexes the feature. The loan in position 1 has a height, weight, sector and score, and those slots always mean the same thing for every loan. The first slot is always loan amount. The seventh slot is always the director’s score. If you scramble the slots, the model cannot learn anything, because the meaning of each position would be inconsistent across examples.
And the label yi? In this case it is a simple binary: yi = 1 if the loan went into default in the first 24 months after origination, and yi = 0 otherwise. So our full dataset is a big table where each row is a historical loan, the first seven columns are features, and the last column is the outcome.
The goal of supervised learning is to find a function f such that given a new x (features of a loan application we have not yet approved), f(x) predicts the y (whether it is likely to default). Notice how everything is anchored on the fact that in the training data, we knew both the features and the outcome. The model never trains without knowing the answer on the training examples. That is the supervision.
Read this as two phases. During training you need both features and labels. During prediction you only need features, because the whole point is that the label is what you are trying to guess.
A subtle but critical point about labels. In the credit risk example, the label is “did this loan default within 24 months?” That sounds simple until you realise what it implies. To label a loan from January 2022, you must wait until at least January 2024 to know its true outcome. This means the freshest training data you can use for a 24-month default model is always at least two years stale. This is not a bug you can engineer away. It is a structural feature of the problem and it has huge consequences for how often you can retrain, how you monitor drift, and how you handle regime changes like the 2022 interest rate shock. Every time you look at a supervised model in finance, ask: “How was the label generated, and what is the lag?”
Formal statement. A supervised learning algorithm takes a dataset D = {(xi, yi)}i = 1N and produces a function f : ℝD → 𝒴, where 𝒴 is the output space. When 𝒴 is a finite set of categories, the problem is called classification. When 𝒴 is a real number, the problem is called regression. When 𝒴 is something stranger like a sequence or a tree, the problem is called structured prediction. Credit default (yes/no) is classification. Loss given default (a percentage) is regression. Generating a credit memo paragraph is structured prediction. Same framework, different output shapes.
Where this goes wrong. A very common failure in banking supervised learning is label leakage. Suppose you are predicting default and one of your features is “has a workout case open.” That feature is enormously predictive, but only because workout cases are opened after default has already been detected. Your model will look spectacular in backtest and catastrophic in production. Finding leakage is tedious, adversarial work, and it is one of the reasons senior ML engineers in regulated industries spend more time on data diligence than on model architecture.
If asked: “In a supervised classification problem, how do you spot label leakage?”
What if you have no labels at all?
“We’ve got six million active business customers. We want to design differentiated service propositions. Marketing wants five to eight segments. What do we have to work with?”
“We’ve got transaction history, product holdings, channel usage, industry code, turnover bands, tenure, digital logins.”
“What’s the label?”
“There isn’t one.”
“What do you mean there isn’t one?”
“There’s no pre-existing category. Marketing wants us to invent the segments.”
That’s an unsupervised learning problem. Unsupervised learning is what you do when your dataset is just {xi}i = 1N, with no yi to lean on. You are not trying to predict a known answer. You are trying to discover structure.
Unsupervised learning has three main flavours that you should be able to name on demand:
Clustering. Group the examples into piles such that examples within a pile are similar to each other and examples across piles are different. Customer segmentation is the canonical example.
Dimensionality reduction. Take a feature vector with hundreds or thousands of dimensions and compress it to a feature vector with just a handful, while preserving as much of the interesting structure as possible. If you have ever looked at a PCA plot of your customers and seen them spread out on a two-dimensional scatter you could actually read, that’s dimensionality reduction at work.
Outlier detection. Given the “typical” pattern of the data, output a score for each example indicating how weird it is. Anti-money-laundering (AML) transaction monitoring leans heavily on outlier detection. The system doesn’t have a labelled list of money launderers, because criminals don’t helpfully tag their transactions. Instead, the system builds a model of what “normal” looks like for each customer segment and flags transactions that deviate.
This diagram is the decision tree I actually draw on a whiteboard whenever someone says “we don’t have labels.” You ask what they are trying to achieve, and the answer usually lands cleanly in one of three branches.
The Merehaven Bank AML example, concretely. The bank processes tens of millions of transactions daily. Only a tiny fraction are suspicious. Labelling all transactions would be impossible. Instead, an outlier detection model builds, for each customer, a notion of their normal transaction profile: usual counterparties, usual amounts, usual times of day, usual seasonality. When a transaction comes in that is far from that profile, the model emits an anomaly score. Analysts then investigate the top-scoring anomalies. Over time, the analysts’ verdicts (was this actually suspicious?) can be fed back to create a supervised layer on top of the unsupervised one. This stacking of unsupervised and supervised is how most mature AML systems work in practice.
Where this goes wrong. Unsupervised learning has a much softer notion of “right answer” than supervised learning does. If a clustering algorithm gives you six customer segments, there is no ground truth to compare them against. You cannot simply compute an accuracy. This means evaluating unsupervised models is a judgement call, involving stability tests, silhouette scores, downstream business usefulness, and stakeholder buy-in. New data scientists sometimes present a clustering result with the confidence of a classifier’s accuracy number, and senior reviewers rightly push back. Know the difference.
If asked: “Why would you use unsupervised learning over supervised learning if you had a choice?”
Can half a label be better than none?
Here is a historical thread that most engineers don’t know.
In the late 1990s, a group of researchers at CMU and elsewhere became curious about a strange question. Imagine you are training a spam classifier and you have a hundred labelled emails but a million unlabelled ones. Intuitively, the unlabelled emails should be useless, because they don’t tell you anything about the label. But a series of papers in the early 2000s showed that this intuition was wrong. If you used the unlabelled data to learn something about the shape of the input distribution, and then used the labelled data to attach that shape to the correct class, you could sometimes outperform a model that had only the hundred labelled examples. The technique has various names, and the umbrella term is semi-supervised learning.
Let’s anchor this in a banking analogy.
Imagine you are training a model to predict whether a small business will take up a new FX hedging product. You only have labels on the 3,000 SMEs your analysts have actively pitched the product to. But you have 400,000 SMEs in your data warehouse with full transaction and product usage histories. A pure supervised model would only learn from the 3,000. A semi-supervised model finds ways to use the full 400,000 to shape its understanding of what “SME” means in your data, and then uses the 3,000 labels to calibrate a decision boundary within that shape.
Why does this work? Intuitively, because the unlabelled data tells you where the customers are in feature space. It reveals clusters, density, and structure. The labels tell you which side of a line you want to be on. With only the labels, you are drawing a line through a void. With the unlabelled data, you can at least see where the crowd stands before deciding where to cut.
The power of this idea, once you see it, is hard to overstate. It is the intuition behind many modern techniques including self-training, consistency regularisation, and in a looser form, the whole pre-training / fine-tuning paradigm that underpins large language models. The Merehaven Credit Workbench sitting on your analysts’s screen is, at its foundation, a semi-supervised descendant: a huge language model pre-trained on unlabelled text, then fine-tuned on a much smaller labelled dataset of credit memos, chat transcripts, and policy documents.
Where this goes wrong. Semi-supervised learning makes an assumption called the smoothness assumption: examples that are close in feature space should have similar labels. When that assumption fails, semi-supervised methods can make your model worse than using labels alone. A classic case in credit risk is where two clusters of SMEs look structurally similar on surface features (size, sector, turnover) but have wildly different default rates because of a hidden feature like management quality or customer concentration. A semi-supervised model will happily smooth the labels across the cluster and smuggle in errors the pure supervised model would never have made.
If asked: “When is semi-supervised learning a bad idea?”
How does a machine learn to play the long game?
Step into a completely different kind of problem. Forget predicting a static label. Imagine instead that you are teaching a computer to manage a portfolio of FX positions overnight. At every moment it can buy, sell, or hold. Every action it takes moves the portfolio into a new state. At the end of the week, it has made or lost money. You don’t want to tell it “this exact action was correct” because there is no single correct action. You want to tell it “at the end of the week, you were up three percent, well done” or “at the end of the week, you lost two percent, learn from that.”
This is reinforcement learning, and it has a vocabulary worth memorising.
- The environment is the world the agent acts in. For FX it’s the market. For a robot it’s the room. For a game it’s the game.
- The state is the feature vector describing the environment at a given moment.
- An action is something the agent can do in that state.
- A reward is a numerical signal the environment hands back, usually small at each step and occasionally large at key moments.
- A policy is a function that takes a state as input and outputs an action, or a distribution over actions.
- The goal of reinforcement learning is to learn a policy that maximises the expected cumulative reward over time.
Read this as a loop. The agent looks at the current state, picks an action according to its policy, the environment responds with a new state and a reward, and the agent updates its policy to do better next time. The interesting difficulty is that the reward today might be because of an action ten steps ago, not the most recent one. This is called the credit assignment problem and it’s the central technical challenge of the field.
Banking applications of reinforcement learning are real but narrower than hype suggests. The genuine wins are in sequential decision problems where you control something and get measurable feedback. Market-making engines on a trading desk. Order execution algorithms that decide how to break up a large parent order. Portfolio allocation strategies that rebalance over time. Dynamic pricing of short-term savings products. In all of these, the key ingredient is a reliable simulator or a safe testbed, because letting a raw RL agent learn on live capital is a career-ending decision. Most production RL systems in banks are trained in simulation for millions of episodes, then deployed with tight guardrails and constant monitoring.
Where RL is not a fit: anything where you don’t have a clear reward, anywhere the environment is hostile to experimentation, anywhere an individual wrong action has consequences you cannot undo. You don’t use RL to decide whether to approve a mortgage, because you can’t “experiment” on people’s home purchases. You might use RL inside the Merehaven Credit Workbench to decide which three pieces of information to show the analyst first, because that’s a cheap and reversible decision.
If asked: “Why is reinforcement learning harder than supervised learning?”
How does supervised learning actually work, step by step?
We have done definitions. Now let’s trace a complete supervised learning project end to end, with numbers, and make sure every link in the chain is concrete.
We’re building a simplified probability-of-default model for SME term loans In the synthetic Merehaven case. Let’s walk it in six steps.
Step 1: Define the problem. What exactly are we predicting? The candidate definitions include “will the loan default within the first twelve months after origination?” or “within 24 months?” or “will the loan enter forbearance at any point?” Each choice gives you a different label, a different time horizon, and a different downstream use case. For this walkthrough we pick 24-month default, because that matches Merehaven Bank’ internal rating horizon for commercial SME lending.
Step 2: Gather the data. You go to the data warehouse. You pull every SME term loan originated between January 2012 and December 2021. You get, say, 180,000 loans. For each one you collect: the features at origination (what we knew about the loan and borrower at the point of approval) and the label (did the loan default in the first 24 months?). You cut off 2022 and later because the 24-month window is not yet complete for those loans, so you cannot label them honestly.
Step 3: Clean and engineer features. This step takes at least half the total project time and is never glamorous. You deal with missing values (some loans have no director’s score because the director was international). You handle categorical features like sector code by converting them to one-hot encodings. You drop features that leak (workout flags, anything touched by collections). You normalise numerical features so they are on comparable scales. You handle outliers. You discover that a non-trivial percentage of your historical DSCR values are wrong because of a change in how Merehaven Bank calculated the ratio in 2017. You fix them. You document everything.
Step 4: Split the data. You do not train and evaluate on the same data, ever. You split your 180,000 loans into training (say, 70 percent), validation (15 percent) and test (15 percent). importantly, because this is time-series data, you split by origination date, not randomly. The training set is 2012 to 2018 loans. The validation set is 2019 loans. The test set is 2020 and 2021 loans. This mimics how the model will actually be used in production: trained on the past, evaluated on more recent data.
Step 5: Choose an algorithm and train. You pick a model family. For this walkthrough we will use SVM, which we study in the next section. You feed the training set to the SVM training procedure. The procedure finds the parameters w* and b* that separate defaulters from non-defaulters as well as possible, under the algorithm’s specific definition of “well.”
Step 6: Evaluate and iterate. You score the validation set with your trained model. You look at accuracy, precision, recall, AUC, and business-relevant measures like expected loss at a chosen approval cutoff. If the performance is acceptable, you re-run everything on the test set (once and only once, to get an unbiased estimate). If not, you go back and adjust: more features, a different algorithm, different hyperparameters. You iterate on the training and validation sets. You touch the test set at the very end.
This is a loop inside a loop. The inner loop is training, the middle loop is feature and hyperparameter iteration, and the test set sits outside both loops as the final judgement. If you ever show someone a model that was evaluated on data the iteration loop touched, you are lying with statistics and senior reviewers will catch you.
The same principle applies across banking. A credit memo PDF becomes a feature vector by extracting numerical fields (turnover, EBITDA, headcount) and one-hot encoding categorical fields (sector, region, entity type). A customer’s twelve-month transaction history becomes a feature vector by computing summary statistics (count, sum, mean, max, standard deviation) across several categories (direct debits, card spend, transfers in, transfers out). A management accounts spreadsheet becomes a feature vector by mapping line items to a canonical chart of accounts and taking the most recent values and ratios. The art is in choosing which summaries preserve the predictive signal and which destroy it.
Where this goes wrong. The single most common production failure in bank ML systems is training-serving skew in feature engineering. The training pipeline, written by a data scientist, computes DSCR one way. The serving pipeline, written three months later by an engineer who was not in the original room, computes DSCR a slightly different way. Both are “DSCR” in name. Neither team notices. In production the model performs worse than in backtest by a measurable margin. The fix is to have exactly one piece of code that computes features, and to call it from both training and serving. This pattern is sometimes called a feature store, and it is now the backbone of most serious bank ML platforms.
If asked: “What’s the biggest production risk in a supervised learning system you haven’t seen mentioned yet?”
Why does a line separate good borrowers from bad ones?
Now for the geometry.
Let’s say you have done the credit risk pipeline above, and you have 180,000 loans, each represented as a feature vector in seven dimensions. For simplicity of visualisation, pretend there are only two features: debt service coverage ratio (DSCR) on the x-axis, and loan-to-value (LTV) on the y-axis. Plot every historical loan as a point. Colour the defaulters red and the non-defaulters blue.
In 1963, Vladimir Vapnik and Alexey Chervonenkis, working in Moscow at the Institute of Control Sciences, were thinking about exactly this kind of picture. They asked a deceptively simple question. If the red points and blue points are separable by a straight line, which line should you draw?
There are infinitely many lines that separate them. Any of them would classify the training data perfectly. Vapnik’s insight, which took decades to mature into the Support Vector Machine in the 1990s, was that not all separating lines are equal. The best line is the one that is as far as possible from the nearest point of either class. The “as far as possible” matters, and we will see why in a moment.
First the analogy. Imagine you are driving a car down a winding country lane in Yorkshire. On one side is a stone wall. On the other is a ditch. You have a choice of where to aim the car. You could drive two inches from the wall and fourteen feet from the ditch. You could drive fourteen feet from the wall and two inches from the ditch. Or you could drive right down the middle, eight feet from each. Which do you choose? Obviously the middle. Why? Because you might flinch. The car might wobble. A fox might run out. The middle gives you the most margin for error. That is the core intuition of the Support Vector Machine.
Now the math, slowly.
The equation of a line in two dimensions can be written as w(1)x(1) + w(2)x(2) − b = 0, where w(1) and w(2) are real numbers and b is another real number. The two w values control the line’s slope, and b controls where it sits. Any point (x(1), x(2)) that satisfies the equation is exactly on the line. Points with w(1)x(1) + w(2)x(2) − b > 0 are on one side. Points with w(1)x(1) + w(2)x(2) − b < 0 are on the other side.
In more than two dimensions the same equation works, we just have more terms:
w ⋅ x − b = 0
where w ⋅ x (read “w dot x”) is shorthand for w(1)x(1) + w(2)x(2) + … + w(D)x(D), summing the products of corresponding entries. In two dimensions this equation describes a line. In three dimensions it describes a plane. In D dimensions it describes a hyperplane, which is a (D − 1)-dimensional flat surface. For our seven-feature credit model, the decision boundary is a six-dimensional hyperplane sitting inside seven-dimensional space. You cannot picture this. Nobody can. But the algebra works exactly the same as the two-dimensional case, and the two-dimensional case is something you can draw.
The SVM then predicts the label of a new point x like this:
y = sign(w ⋅ x − b)
where sign returns +1 if the input is positive and −1 if the input is negative. So the prediction depends only on which side of the hyperplane the point falls on. One side is the “will not default” side. The other side is the “will default” side. The output is binary.
Let’s compute this by hand for one loan. Suppose our two features are DSCR and LTV, the trained parameters are w = (2.0, −3.0) and b = 0.5, and we have a new loan with DSCR = 1.4 and LTV = 0.6. Then:
w ⋅ x − b = (2.0)(1.4) + (−3.0)(0.6) − 0.5 = 2.8 − 1.8 − 0.5 = 0.5
The result is positive, so sign(0.5) = +1. Under our labelling convention this means “predicted non-defaulter.” If we flip the features to a riskier loan with DSCR = 0.9 and LTV = 0.85:
w ⋅ x − b = (2.0)(0.9) + (−3.0)(0.85) − 0.5 = 1.8 − 2.55 − 0.5 = −1.25
The result is negative, so sign(−1.25) = −1, predicted defaulter. Notice what the signs of the weights are telling us: the positive weight on DSCR means higher DSCR makes the score more positive (good), and the negative weight on LTV means higher LTV makes the score more negative (bad). The model has learned the intuitions a credit analyst would have.
That is the prediction step. We still have not explained how w and b get chosen. That’s the training step, and it’s where the margin comes in.
If asked: “What does it mean for two classes to be ‘linearly separable’ and why does it matter?”
What makes the best line the widest line?
Here’s the deeper question. There are infinitely many hyperplanes that separate the red and blue points in a linearly separable problem. Any of them achieves perfect training accuracy. Why should we prefer the one with the largest margin?
The answer is the most important single insight in classical machine learning, and it is worth sitting with.
Claim: A separating hyperplane that is far from the nearest training point is more likely to correctly classify new, unseen points than a separating hyperplane that is close to the nearest training point.
Argument: New points do not appear out of nowhere. They are drawn from the same underlying distribution as the training points. So if the new points come from the same world as the training points, they will tend to land near the training points, not far from them. A hyperplane that is far from all training points leaves a buffer around each point. New points that land slightly away from their training cousins will still be on the right side. A hyperplane that is tight against the training points has no buffer. The first new point that lands slightly differently from its training cousin will flip sides.
That is why we want the largest margin.
Formally, SVM defines the margin as the distance between two parallel hyperplanes that just touch the nearest points of each class. You can picture it as the width of the corridor between the two classes, centred on the decision boundary.
Read this as a corridor. The defaulters sit on one side. The non-defaulters sit on the other side. The decision boundary runs down the middle. The margin is the half-width of the corridor. SVM chooses the w and b that make the corridor as wide as possible while still correctly separating the training points.
The mathematical trick to make this work is elegant. You choose to scale w and b such that the parallel hyperplanes are at w ⋅ x − b = +1 and w ⋅ x − b = −1. With that convention, geometry gives you the width of the corridor as , where ∥w∥ is the Euclidean norm of w, defined as . This is just the ordinary length of the vector w in standard geometric terms: square each component, add them up, take the square root.
So to maximise the corridor width , you minimise ∥w∥. And you have to do it while keeping every training point correctly classified. The constraint for a correctly-classified positive example (yi = +1) is that it lies on or beyond the +1 hyperplane: w ⋅ xi − b ≥ +1. For a correctly-classified negative example (yi = −1), the constraint is that it lies on or beyond the −1 hyperplane: w ⋅ xi − b ≤ −1. Both can be written in a single compact form:
yi(w ⋅ xi − b) ≥ 1 for all i = 1, …, N
Check the algebra on a positive example: yi = +1 gives (+1)(w ⋅ xi − b) ≥ 1, which simplifies to w ⋅ xi − b ≥ 1. Check on a negative example: yi = −1 gives (−1)(w ⋅ xi − b) ≥ 1, which rearranges to w ⋅ xi − b ≤ −1. Both constraints captured in one line. That is the compact SVM optimisation problem:
minimise ∥w∥ subject to yi(w ⋅ xi − b) ≥ 1 for i = 1, …, N
Machines are very good at solving problems of this form. There is a whole field, convex optimisation, dedicated to them, and the specific structure of this problem (quadratic objective, linear constraints) means that industrial solvers can handle it reliably at scale. The solution, a specific pair w* and b*, is your trained SVM. Plug it into f(x) = sign(w* ⋅ x − b*) and you have a classifier.
Reading the optimisation in plain English. “Find the vector w that is as short as possible, subject to the constraint that all positive training examples score at least +1 and all negative training examples score at most -1.” Short w means wide corridor. Correct classification of all training points means the constraints are met. The solver finds the sweet spot where both conditions hold and the corridor is as wide as the data allows.
When real data is not perfectly separable, which is always the case in credit risk because a small fraction of very healthy-looking borrowers default for reasons that have nothing to do with the features you observe, the pure SVM has no solution. The fix is the soft-margin SVM, which introduces a penalty hyperparameter C that allows some points to sit inside the corridor or on the wrong side of the boundary, at a cost. Setting C is a trade-off between corridor width and training accuracy, and it is one of the things you tune during the validation loop.
Where this goes wrong. In high-dimensional problems with few training points, SVM (and every other classifier) suffers from a phenomenon called the curse of dimensionality. As the number of features grows, the volume of feature space grows exponentially, and any fixed number of training points becomes very sparse relative to the space they sit in. The “margin” intuition that worked beautifully in two dimensions becomes much weaker in two thousand. A concrete symptom: a spam classifier trained on a 20,000-word vocabulary with only 10,000 emails is already in a regime where fitting a good hyperplane is statistically fraught. The mitigations are feature selection, regularisation, and gathering much more data. In banking this means that throwing every column of the data warehouse at the model is a bad default, even though it feels like giving the model “more information.”
If asked: “Why does SVM try to maximise the margin rather than just find any separating hyperplane?”
Why does any of this work on borrowers we’ve never seen?
This is the deepest question in the chapter and it deserves a slow answer.
Here is the puzzle, stated plainly. We took 180,000 historical SME loans, we trained a model on them, and we now apply that model to a brand new SME loan that walks in the door tomorrow. The model has never seen this specific borrower. There is no principled reason, from pure logic alone, to believe that what worked on past loans will work on this one. So why does it?
The answer has two parts, and missing either one is what gets data scientists in trouble.
Part one: the independence and identical distribution assumption, usually written i.i.d. in textbooks. Supervised learning rests on the belief that the training data and the future test data are drawn independently from the same underlying probability distribution. “Same underlying distribution” means there is some fixed statistical process producing SME loans and their default outcomes, and every loan we see, past or future, is a fresh sample from that process. “Independently” means one loan’s outcome doesn’t directly depend on another’s. If the assumption holds, then patterns learned on past samples will hold on future samples, because they are samples from the same population.
Part two: the patterns the case shows must be well-tested. Even if the distribution is stable, our model might have memorised quirks of the training data rather than general patterns. A model that memorises quirks is said to be overfitting. The margin-maximisation principle of SVM is one specific defence against overfitting: by preferring the widest separating corridor, we are preferring simpler decision boundaries that don’t contort themselves around individual training points. Simpler models that fit the training data almost as well as complicated ones usually generalise better, which is the modern formalisation of a principle that goes back to William of Ockham in the fourteenth century: prefer simpler explanations. In machine learning this is called the bias-variance tradeoff, and we will meet it repeatedly.
Read this as the two pillars holding up generalisation. Kick either one and the building falls. This diagram is the single most important idea in the chapter; internalise it.
A concrete walkthrough of why the i.i.d. assumption matters in banking. Suppose you train a default model on 2015-2019 SME loans. You deploy it in January 2022. By mid-2022 the Bank of England has raised rates multiple times, energy prices have spiked, and the SMEs that were comfortably meeting their debt service on three percent loans are struggling on six. The borrowers in the test-time distribution (2022) are no longer behaving like the borrowers in the training distribution (2015-2019). The i.i.d. assumption has been violated by a regime change in the macro environment. Your model’s performance will quietly degrade. The fix is not “better features” or “more data from 2015-2019.” The fix is to retrain on more recent data, accept that the model will always lag reality, and monitor for drift so you know when retraining is overdue.
This is the single most important operational discipline in production ML. Models that were excellent at launch are mediocre a year later and dangerous two years later, not because the code has rotted but because the world has moved. The industry term for this is concept drift (when the relationship between features and labels changes) or covariate shift (when the distribution of features changes), and mature organisations have dashboards that track both on their live models.
A concrete walkthrough of why simplicity matters. Suppose you train a very complicated model, perhaps a deep neural network with millions of parameters, on those same 180,000 loans. With enough capacity, the model can memorise every training example perfectly and achieve 100 percent training accuracy. When you evaluate on the validation set, accuracy is much worse. What happened? The model learned the training set, not the underlying pattern. It treated the specific noise in the 2016 loans as signal. It fit a twisted, elaborate boundary that hugged every single training point. On new points that are slightly different, the twisted boundary misclassifies. This is overfitting. The SVM’s margin principle explicitly fights overfitting by pushing for the smoothest, flattest separating boundary consistent with the training data.
The take-home. Supervised learning works because of two big bets: a statistical bet that the world is stable enough that yesterday’s patterns apply tomorrow, and a philosophical bet that among patterns consistent with the training data, the simpler ones are more likely to be real. Neither bet is guaranteed to pay off. Both usually do, and the places where they don’t are where real engineering judgement matters.
If asked: “How would you detect that a production credit model has started to drift?”
Read the mathematics as operations
What are scalars, vectors, and matrices really?
Let’s start with the containers. Every single number that ever enters a machine learning system sits inside one of three containers, or something built on top of them.
A scalar is just a single number. Written in italic lowercase: x, a, b, α, θ. A customer’s age (42) is a scalar. A loan amount (£250,000) is a scalar. A probability of default (0.037) is a scalar. A hyperparameter like the SVM penalty C (1.0) is a scalar. If it’s one number, it’s a scalar.
A vector is an ordered list of scalars. Written in bold lowercase: x, w, b. The order matters: the first entry means the same thing across every vector in your dataset, and you can’t shuffle them without breaking the meaning. You can think of a vector in two equivalent ways: as an arrow in space pointing in some direction, or as a point in space. For machine learning the “point” intuition is more useful more often, but you’ll see the “arrow” intuition used whenever we talk about gradients or directions of change.
The individual entries of a vector are called attributes or features or components depending on the author’s taste. We write the j-th attribute of vector x as x(j). The parenthesised superscript is just a position index. It is not a power. So x(2) is the second attribute of x, whereas x2 would mean x squared. When we need to talk about both the example index and the feature index, we stack them: xi(j) is the j-th feature of the i-th example. The i is which customer. The (j) is which of their attributes.
Here is one concrete vector from the Merehaven Bank data warehouse, representing a single SME customer:
xi = [250, 60, 1.45, 7, 3, 0.62, 820]
This is a seven-dimensional vector. The first component (xi(1)) is the loan amount in thousands. The second (xi(2)) is the term in months. The third (xi(3)) is the debt service coverage ratio. And so on, exactly as we described in Chapter 1. The subscript i identifies which customer this is among the tens of thousands in the portfolio. The (j) positions are fixed: wherever you see a vector in this dataset, position 3 always means DSCR.
A vector is not just a tuple. It has algebraic structure. You can add two vectors to get another vector. You can multiply a vector by a scalar to get another vector. You can take the dot product of two vectors to get a scalar. These operations have geometric meanings (the sum of two arrows is their tip-to-tail composition, scalar multiplication stretches or shrinks an arrow, dot product measures how much one arrow projects onto another). We’ll use all of them.
A matrix is a rectangular grid of numbers: rows and columns. Written in bold uppercase: X, W, A. You can think of a matrix as a collection of vectors stacked together, which is how datasets are usually stored. If you have N customers and each one has D features, the whole dataset is a matrix X with N rows and D columns:
Row i is customer i’s feature vector. Column j is the j-th feature across all customers. This is literally how pandas and SQL represent your data: rows are records, columns are fields. The matrix notation is just giving it a symbolic name so we can manipulate the whole dataset in one go.
Concrete walkthrough In the synthetic Merehaven case scale. The SME lending portfolio might have 180,000 active customers, each with 240 features after all the engineering is done. Then X is a 180, 000 × 240 matrix. That’s over 43 million numbers. Stored in 32-bit floats, it’s about 170 MB of data. You can load it into memory on a laptop. You couldn’t do this easily in 1995. The fact that you can today is why modern machine learning works.
Sets round out the basic containers. A set is an unordered collection of unique elements, written with calligraphic capitals like 𝒮 or 𝒟. Curly braces list the elements: {1, 3, 5, 8}. The training dataset in Chapter 1 was a set of pairs: 𝒟 = {(x1, y1), (x2, y2), …, (xN, yN)}. The key difference between a set and a vector is that sets have no order and no duplicates. {3, 1, 1, 5} is the same set as {1, 3, 5}. Vectors, by contrast, care about order and happily repeat values: [3, 1, 1, 5] is different from [1, 3, 5].
You’ll also see set operations occasionally. Intersection, written 𝒮1 ∩ 𝒮2, gives you the elements that appear in both sets. Union, written 𝒮1 ∪ 𝒮2, gives you the elements in either set. Cardinality, written |𝒮|, gives you the number of elements. Membership, written x ∈ 𝒮, asks whether x is in 𝒮. The special set ℝ denotes all real numbers, and the worked design uses it constantly: writing x ∈ ℝ means “x is some real number, could be anything.”
Read this as a hierarchy for ordered numerical data (scalar → vector → matrix) plus sets as a separate unordered container. Almost every data structure you meet in ML is one of these four or a generalisation of them (tensors, which are just matrices in more than two dimensions, are the obvious one).
Where this goes wrong. The most common beginner
mistake in code is confusing row-major and column-major layouts, or
forgetting whether a vector is a row or a column. In pure notation we
are usually careful, but in NumPy a vector created as
np.array([1,2,3]) is one-dimensional with no row/column
distinction, while np.array(1,2,3) is a 2D row vector and
np.array([[1],[2],[3]]) is a 2D column vector. Mixing them
causes silent broadcasting bugs that change the semantics of your
computation without raising an error. Every experienced engineer has
been bitten. The defensive habit is to print the shape of every
intermediate tensor during development, and to assert shapes explicitly
in production code.
If asked: “What’s the difference between a vector and a set in machine learning notation?”
How do you sum thousands of things without losing the structure?
In machine learning you add things up a lot. Loss over the training set. Weighted sum over the features. Probabilities over outcomes. Gradients over examples. If you had to write these out longhand every time, the notation would collapse under its own weight. So mathematicians invented two shortcuts.
Capital sigma notation, ∑, is the summation shortcut. The letter is a capital Greek sigma, chosen because S suggests “sum.” Below the sigma you put the index variable and its starting value. Above you put the ending value. To the right you put the expression to sum. So:
Read this out loud: “the sum from i equals 1 to N of xi.” It means: let i take each integer value from 1 to N, compute xi each time, and add them all up. The index i is a placeholder that exists only inside the sum. It’s called a dummy variable, and you can rename it freely: means exactly the same thing.
Here’s a banking example that you compute every day somewhere in your bank. The total balance across all current accounts held by a single customer:
where Ai is the number of accounts customer i holds and balancei, a is the balance on customer i’s a-th account. In English: “sum the balances of all of customer i’s accounts to get their total.” One line of notation, one unambiguous meaning.
Sigma generalises to summing over the attributes of a vector:
You will see this exact pattern constantly, because this is the dot product of two vectors, and the dot product is the core ingredient of almost every linear model. When you read w ⋅ x or w⊤x, you are reading this sigma in disguise.
Capital pi notation, ∏, is the same idea for products. The Greek letter is P for “product.” So:
When do you multiply a long list of numbers in ML? Mainly when you compute likelihoods. If you have N independent observations and you want the probability of seeing all of them together, you multiply the individual probabilities. In parameter estimation (coming up later in this section) this is the workhorse.
Concrete walkthrough: a tiny weighted sum. Suppose we have a weight vector w = [0.3, −1.2, 0.05, 2.0] and a feature vector x = [1, 0.5, 200, 0.8]. The weighted sum is:
= 0.3 − 0.6 + 10 + 1.6 = 11.3
That single number 11.3 is the “score” produced by this linear model for this particular customer. In practice the weights come from training and the features come from the customer’s record, but the arithmetic is exactly this: multiply pairwise, add everything up.
A nasty thing that happens with products. If you multiply many small probabilities together, the result gets tiny very fast. Multiply a thousand probabilities of size 0.01 and you get 10−2000, which is smaller than any 64-bit float can represent. The computer returns zero and your algorithm breaks. The standard fix is the log trick: take the logarithm of the whole thing. Logarithms turn products into sums, because log (ab) = log (a) + log (b). So instead of computing ∏ipi directly, you compute ∑ilog pi, which is numerically well-behaved, and exponentiate at the end if you need to. Every mature ML library does this automatically under the hood, and you will see log-likelihoods instead of likelihoods written everywhere. Now you know why.
If asked: “Why do ML libraries work with log-probabilities instead of probabilities?”
What do you do to vectors?
You now have containers. You need verbs.
Vector addition is element-wise. Two vectors of the same length add component by component:
x + z = [x(1) + z(1), x(2) + z(2), …, x(m) + z(m)]
The result is another vector of the same length. Banking example: if x is the portfolio exposure across sectors this month and z is the change from last month, then x + z is where you are and where you are headed, sector by sector. You can subtract in exactly the same way.
Scalar multiplication stretches a vector. If c is a number and x is a vector, then:
cx = [cx(1), cx(2), …, cx(m)]
Multiply every component by c. If c > 1, the vector gets longer. If 0 < c < 1, it gets shorter. If c is negative, it flips direction. Banking example: you have a small sample of 1,000 SME customers with total annual fee income of £X. To estimate the fee income on a population of 10,000 similar customers, you multiply the sample total by 10. You have just scalar-multiplied a vector of fee amounts.
Dot product is the star of the show. Two vectors of the same length combine into a single scalar:
This is exactly the weighted sum you already know. The dot product has a geometric meaning as well as an algebraic one: it equals ∥w∥∥x∥cos θ where θ is the angle between the two vectors. Two vectors pointing in the same direction have a large positive dot product. Two vectors pointing in opposite directions have a large negative dot product. Two perpendicular vectors have a dot product of zero. This matters enormously: the SVM from Chapter 1, the attention mechanism in transformers, the similarity score between two customer embeddings, and the cosine similarity used in recommendation engines are all dot products wearing different hats.
The dot product is also sometimes written w⊤x, using the transpose notation ⊤. We’ll come back to the transpose in a moment. The two notations, w ⋅ x and w⊤x, mean the same thing when both vectors are the same length.
Matrix-vector multiplication generalises the dot product. If W is a matrix with m rows and n columns, and x is a vector with n components, then Wx is a new vector with m components:
The i-th entry of the result is the dot product of the i-th row of W with x. In other words, a matrix-vector multiplication is many dot products at once. A matrix with five rows, multiplied by a vector, gives you a five-dimensional result, which is five separate dot products bundled into one operation.
This pattern appears everywhere. The output of a single layer of a neural network is exactly Wx + b, followed by a nonlinearity. The transformation of a feature vector into a set of basis scores in PCA is a matrix-vector multiply. The scoring of a single credit application against a model with multiple output heads (probability of default, expected loss, probability of prepayment) is a matrix-vector multiply.
The transpose x⊤ flips a vector from column shape to row shape, or a matrix from m × n to n × m. Concretely, if x is written as a column [x(1); x(2); x(3)], then x⊤ is the row [x(1), x(2), x(3)]. This matters for matrix multiplication because you can only multiply a matrix by a vector when the inner dimensions match. You may need to transpose one of the operands to make that happen.
Concrete walkthrough: scoring three loans with one model. Suppose your credit model has weights w = [2.0, −3.0] and bias b = 0.5, where the features are DSCR and LTV as before. You have three new loans in your batch:
Row 1 is loan 1, row 2 is loan 2, row 3 is loan 3. The scores are:
Subtract b = 0.5 from each: [0.5, −1.25, 2.5]. Apply the sign function: loan 1 is positive (non-default), loan 2 is negative (default), loan 3 is positive (non-default). Three predictions, one matrix multiply. This is batch scoring, and it’s how every production ML system actually processes inference: not one example at a time but many at once, because GPUs and modern CPUs are brutally efficient at matrix arithmetic.
If asked: “Why is almost all of modern ML built on matrix multiplications?”
What’s a function, and why should you care about its minimum?
Take a breath. You’ve done the containers and the operations. Now we’re going to talk about functions, because the entire training process of every ML model is: define a function that measures how bad your model is, then find the values of parameters that make that function as small as possible.
A function is a rule that takes an input and produces an output. If the function is called f, we write y = f(x), read as “y equals f of x.” The set of allowed inputs is called the domain and the set of possible outputs is called the codomain. You already know functions from school: f(x) = x2 takes a real number and squares it. f(x) = 2x + 3 takes a real number and doubles it then adds three.
In machine learning, the functions we care about are usually loss functions or cost functions. Their input is a set of model parameters (weights, biases, thresholds). Their output is a single number measuring how poorly the model with those parameters performs on the training data. The smaller the output, the better the parameters. Our job is to find the parameters that produce the smallest possible output.
A very concrete loss function. Suppose you have a simple one-feature linear regression model predicting a customer’s lifetime value from their current month’s fee income: ŷ = wx + b. For one customer with actual lifetime value y, the squared error is (ŷ − y)2 = (wx + b − y)2. For a whole training set of N customers, the total squared error is:
L is a function of two parameters, w and b. Given any specific pair, L returns a single number: the average squared error on the training set. Our goal is to find the specific w* and b* that minimise L.
Local versus global minimum. A function can have many low points. A local minimum at x = c means that f(x) ≥ f(c) for every x in some small neighbourhood around c: it’s the lowest point you can see if you only look a short distance to either side. A global minimum is the absolute lowest point anywhere in the domain. Every global minimum is also a local minimum, but not vice versa.
This distinction is critical in practice. The SVM from Chapter 1 has a convex loss function, which is a special kind of function whose only local minimum is also its global minimum. This means you can’t get stuck in a mediocre solution while optimising it. Linear regression is also convex. The loss functions of deep neural networks, on the other hand, are non-convex. They have countless local minima. Training a deep network well means not just finding a low point but finding a good one, and much of the art of modern deep learning is about techniques for escaping bad local minima and settling into basins that generalise well.
Read this as a fork. Convex losses are well-behaved and any sensible downhill algorithm converges to the best answer. Non-convex losses require more sophisticated training strategies and there is a whole research literature on them.
Banking example of the local/global distinction. In credit model development, you might define a loss that combines predictive accuracy with fairness constraints and capital efficiency. The combined loss is often non-convex. Two different training runs from different initial weights can end up at two different “solutions” with similar overall loss but different behaviours on slices of the portfolio. Model risk management In the synthetic Merehaven case and every serious bank insists on training stability checks precisely because of this: run the same training procedure several times and verify you get materially similar models, not wildly different ones.
One more bit of notation before we move on. The argmax operator, written arg maxx ∈ Af(x), returns the element of A that makes f as big as possible. The argmin does the same for the minimum. Contrast with maxx ∈ Af(x), which returns the maximum value itself. So maxxf(x) = 10 says “the biggest value f achieves is 10,” while arg maxxf(x) = 3 says “the input that produces that maximum is x = 3.” These are separate things and ML papers use both. In classification, you will see ŷ = arg maxcP(y = c|x) meaning “predict the class whose posterior probability is highest.” The argmax picks the winning class, not its probability.
The assignment operator ← is the last small piece of notation. It
means “set the thing on the left to the value of the thing on the
right.” So w ← w − 0.01∇L
means “update w by subtracting
0.01 times the gradient.” It’s the
pseudocode equivalent of w = w - 0.01 * grad_L in actual
code. The arrow makes clear this is an action, not an equation.
If asked: “Why do we frame training as minimising a loss function?”
How do you find the direction that goes down fastest?
Finding the minimum of a loss function is the core training problem. For simple convex losses you can often solve it analytically with a closed-form formula (we’ll see this in Chapter 3 for linear regression). For most interesting models you can’t, and you have to find the minimum numerically, by taking many small downhill steps. To take a downhill step you need to know which direction is downhill. That’s what derivatives and gradients are for.
The derivative of a function f of a single variable, written f′(x) or , is another function that tells you how fast f is changing at each point. If f′(x) > 0 at some x, the function is going up as you move right. If f′(x) < 0, it’s going down. If f′(x) = 0, the function is locally flat: you are at a peak, a valley, or a plateau.
The everyday intuition: imagine you are standing on a hill and you want to know which way is down. You take a tiny step in some direction and see how much your elevation changed. That’s the derivative of elevation with respect to position in that direction. Take the step in the direction where your elevation drops fastest and you are following the negative derivative. Do this repeatedly and you will eventually reach a low point. That is, in one sentence, how every neural network in the world is trained.
Derivatives of simple functions are memorised facts. Three you should know by muscle memory:
- If f(x) = x2, then f′(x) = 2x.
- If f(x) = cx for any constant c, then f′(x) = c.
- If f(x) = c for any constant, then f′(x) = 0.
The first one makes sense: at x = 3, the function x2 is changing at rate 6, so a small step right of size 0.01 changes the function by approximately 0.06. The second one is obvious: a straight line with slope c is always changing at rate c. The third one is obvious too: a constant function isn’t changing at all.
The chain rule is how you handle derivatives of functions built from other functions. If F(x) = f(g(x)), meaning you first apply g to x and then apply f to the result, then:
F′(x) = f′(g(x)) ⋅ g′(x)
In words: the rate of change of the whole thing is the rate of change of the outer function (evaluated at where the inner function landed) times the rate of change of the inner function. If F(x) = (5x + 1)2, let g(x) = 5x + 1 and f(u) = u2. Then g′(x) = 5 and f′(u) = 2u, so f′(g(x)) = 2(5x + 1). Multiply: F′(x) = 2(5x + 1) ⋅ 5 = 50x + 10.
The chain rule matters because modern neural networks are functions composed of many functions composed of many functions, and the way we compute their gradients is by chaining together the derivatives of each layer. The backpropagation algorithm, which we’ll meet properly in Chapter 6, is nothing but the chain rule applied systematically to a deeply nested function.
The gradient generalises the derivative to functions of more than one input. If your function takes a vector x = (x(1), x(2), …, x(D)) and returns a single number, the gradient is a vector whose entries are the partial derivatives, each one computed by pretending all the other inputs are fixed constants:
The upside-down triangle ∇ is called “nabla” and it’s pronounced “del.” Read ∇f as “the gradient of f” or “del f.” The curly ∂ is the partial derivative sign, distinguishing it from the plain d used for one-variable derivatives.
Concrete walkthrough. Suppose f(x(1), x(2)) = ax(1) + bx(2) + c for constants a, b, c. The partial derivative with respect to x(1) is obtained by treating x(2) and c as constants and differentiating as usual: , because ax(1) differentiates to a and the other terms (now constants) differentiate to zero. Similarly, . So the gradient is ∇f = [a, b].
The geometric meaning of the gradient is critical. At any point in input space, the gradient vector points in the direction of steepest ascent of the function, and its length tells you how steep the ascent is. The negative gradient, −∇f, points in the direction of steepest descent. If you stand at any point and take a small step in the direction of −∇f, your function value drops as much as any step of that size could possibly make it drop. This is the single most important geometric fact in machine learning and it’s worth staring at until it sinks in.
Read this as the iteration at the heart of essentially every training algorithm in ML. Compute where you are, compute which way is down, step that way by a small amount, repeat. The α is the learning rate, a hyperparameter that controls how big each step is. Too small and training is slow. Too big and you overshoot minima and bounce around. Finding a good learning rate is an art that has generated a whole family of techniques (Adam, RMSProp, learning rate schedules, one-cycle policies) all of which are variations on this basic loop.
Banking example: why the gradient matters for mortgage pricing. Suppose Merehaven Bank wants to set the interest rate on a new fixed-rate mortgage product to maximise risk-adjusted return. The return is a function of the rate offered, the take-up probability at that rate, the default probability, and the funding cost. Each of these depends on the rate in a different way. You write the total expected return as a function R(r) of the rate r, then compute and look for the rate where it is zero. That zero-derivative point is a local maximum or minimum of return. Combined with a second-derivative check (or just computing R on a grid), you find the rate that maximises return. No magic. Just calculus applied to a business problem.
Where this goes wrong. In high dimensions, the gradient-descent loop has famous failure modes. The loss surface can have saddle points where the gradient is zero but you are not at a minimum. It can have plateaus where the gradient is tiny and progress stalls. It can have pathological curvature where you oscillate along some directions and crawl along others. Modern optimisers like Adam use per-parameter adaptive learning rates and momentum to mitigate all of these. For the linear and convex models in the next chapter, plain gradient descent is fine. For neural networks in Chapter 6 and beyond, you will care about optimiser choice.
If asked: “Explain the relationship between a derivative, a gradient, and backpropagation in one minute.”
What is randomness, and how do we talk about it with math?
We pivot from the deterministic world of algebra and calculus into the uncertain world of probability. You need this pivot because machine learning is fundamentally about uncertainty. No credit model predicts default with certainty. No fraud detector catches every fraud. Everything we do is a statement about likelihoods and distributions, and probability is the language for those.
A random variable is a variable whose value is the numerical outcome of some random process. It’s written with a capital italic letter, usually X or Y. Examples of random variables that matter In the synthetic Merehaven case:
- X = the number of direct debits on a random customer’s account next month
- Y = the amount of a random ATM withdrawal in pounds
- Z = the indicator of whether a loan defaults in the next 24 months (1 or 0)
- T = the time between a customer’s first and second product purchases, in days
Random variables come in two flavours. A discrete random variable can only take a countable set of values: 0, 1, 2, 3, and so on, or {red, yellow, blue}. The number of direct debits and the default indicator are discrete. A continuous random variable can take any value in some interval of real numbers. The ATM withdrawal amount and the time between purchases are continuous (at least, we model them that way, even though in practice amounts are in pennies).
Probability mass function (PMF). For a discrete random variable, the distribution is given by a probability mass function, which is just a list of probabilities: one for each possible value. We write Pr(X = xi) for the probability that X takes the specific value xi. All these probabilities are non-negative, and they sum to 1 because the variable has to take some value. A fair six-sided die has a PMF with six entries of each, summing to 1.
Probability density function (PDF). For a continuous random variable, the probability that X takes any specific exact value is zero, because there are infinitely many possible values. Instead, we describe the distribution by a density function fX(x) that says how concentrated the probability is near each value. The probability that X falls in an interval [a, b] is the area under the density curve between a and b. The total area under the curve is 1. Densities can be larger than 1 at individual points, as long as the total area is 1: a very concentrated distribution has a very tall narrow peak.
Read this as two parallel worlds. Discrete and continuous random variables have analogous machinery: PMFs instead of PDFs, sums instead of integrals, but the core ideas are the same. Most banking applications mix both: loan amounts are continuous, default indicators are discrete, and a full model has to handle both.
The expectation. For any random variable, the most important single summary is its expected value or expectation, written 𝔼[X]. For a discrete variable, it’s the weighted average of possible values, weighted by their probabilities:
𝔼[X] = ∑ixi ⋅ Pr(X = xi)
For a continuous variable, the sum becomes an integral:
𝔼[X] = ∫ℝx ⋅ fX(x) dx
The integral is just the continuous version of the sum. You can read ∫ as “sum over all tiny slices” of the quantity to its right. The expected value is also called the mean or average, and is often denoted by the Greek letter μ (mu).
Concrete walkthrough: expected value of a default outcome. Let Z be the default indicator for a particular loan, with Pr(Z = 1) = 0.03 (a 3% default probability) and Pr(Z = 0) = 0.97. Then:
𝔼[Z] = (1)(0.03) + (0)(0.97) = 0.03
The expected value of a 0/1 indicator is exactly the probability that it is 1. This simple identity is the reason probability of default estimates can be directly plugged into expected loss calculations. Useful fact.
Concrete walkthrough: expected loss. Now imagine the loan has exposure at default of £500,000 and loss given default of 40%. The loss (if default occurs) is £500,000 × 0.4 = £200,000. The loss random variable L takes value £200,000 with probability 0.03 and £0 with probability 0.97. So:
𝔼[L] = (200, 000)(0.03) + (0)(0.97) = 6, 000
The expected loss on this loan is £6,000. If you aggregate this across all loans in the portfolio, you get the portfolio expected loss, which drives provisioning under IFRS 9 and capital under IRB. The expectation operator is doing real work here.
The variance and standard deviation. The expectation tells you the centre of a distribution, but not how spread out it is. For that you want the variance σ2, defined as the expected squared deviation from the mean:
σ2 = 𝔼[(X − μ)2]
And the standard deviation σ is the square root of the variance. It has the same units as the original variable, which makes it easier to interpret. A portfolio with expected loss £6 million per quarter and standard deviation £500k is tightly concentrated. The same expected loss with standard deviation £5 million is terrifyingly spread out.
For a discrete random variable with a finite set of outcomes, you can compute the variance directly:
σ2 = ∑iPr(X = xi)(xi − μ)2
The variance is always non-negative. A variance of zero means the variable is deterministic: it always takes the same value. A large variance means the variable bounces around a lot. In banking, variance of return is the classical measure of risk in portfolio theory, and the Sharpe ratio, the VaR, and the RWA calculations all depend on variance-like quantities somewhere in their derivation.
If asked: “Why are expectation and variance such central concepts in probability-based machine learning?”
How can you trust an estimate from a tiny sample?
Here’s a problem that keeps data scientists honest. You almost never know the true probability distribution of the thing you care about. You have a sample, maybe a few thousand or a few hundred thousand data points, drawn from that distribution. You want to make statements about the distribution itself, not just the sample. How do you do that responsibly?
This is the territory of statistical estimation. The core setup: there is some unknown quantity about the population (call it θ, maybe it’s the true mean, or the true default rate, or a model parameter). You compute some quantity from your sample (call it θ̂, the little hat indicates “estimate”) and you use it as a stand-in for θ. The question is: under what conditions is θ̂ a good stand-in?
An estimator is called unbiased if, on average across all possible samples you could draw, the estimator equals the true value:
𝔼[θ̂(SX)] = θ
In words: “the expected value of the estimator, where the expectation is taken over the randomness in drawing samples, equals the thing it’s estimating.” Unbiasedness is a modest but important property. It doesn’t say your estimate from this particular sample will be correct. It says that if you drew many samples and averaged the estimates, you’d converge on the truth.
The most important unbiased estimator. If you have a sample SX = {x1, x2, …, xN} from a distribution with unknown mean μ, the sample mean is:
This is literally just the arithmetic average of your sample. It’s an unbiased estimator of the true mean. If you compute it on a sample of 1,000 customers and then on a sample of 1,000,000, both are unbiased. The difference is that the estimate from the larger sample is much more likely to be close to the true value in any single draw, because its variance is lower. “Unbiased in expectation” doesn’t mean “accurate in any single sample.”
Concrete walkthrough: estimating a default rate. Suppose you sample 10,000 SME loans from the bank’s historical portfolio and find that 372 of them defaulted within 24 months. Your sample mean is 372/10, 000 = 0.0372, or 3.72%. This is your unbiased estimate of the true portfolio-wide 24-month default rate. But you want to know how reliable it is. A quick confidence interval calculation (using the fact that a binomial proportion has approximate standard error ) gives a standard error of about 0.19%, so a 95% confidence interval is roughly [3.34%, 4.10%]. You can report “the default rate is 3.72%, with 95% confidence it’s between 3.34% and 4.10%.” You cannot report “the default rate is 3.72%” without qualification, and you cannot make decisions that assume the estimate is exact.
Where this goes wrong. Unbiased estimators can still be misleading when the sample is not representative. If you sample 10,000 loans but your sampling procedure over-represents one sector, your sample mean of default rate is an unbiased estimate of the sector-weighted default rate, not the true portfolio-wide rate. Sampling design matters as much as estimator choice. In credit risk specifically, historical data is often biased by the fact that declined applications weren’t funded and so you have no outcome for them, a phenomenon called reject inference. Handling it properly is a whole subfield, and model validation teams at UK banks spend significant time scrutinising how their modelling teams have approached it.
If asked: “What’s the difference between saying an estimator is unbiased and saying it’s accurate?”
How do you update a belief in the light of new evidence?
The rule says:
Read this carefully. The bar | means “given that.” So Pr(X = x ∣ Y = y) is “the probability that X takes value x, given that we have observed that Y takes value y.” This is called a conditional probability. The left-hand side is what we want to know. The right-hand side is how we compute it from other things we can more easily get at.
The rule has a poetic structure. The numerator multiplies two things: the probability of observing the evidence if our hypothesis is true, and the prior probability of the hypothesis. The denominator is just the total probability of the evidence regardless of hypothesis, which serves as a normalising constant. In words:
The posterior is the updated belief about the hypothesis after seeing the evidence. The prior is what you believed before. The likelihood is how compatible the evidence is with the hypothesis. The evidence is the total probability of the observation under all possible hypotheses, and it’s often the hardest part to compute.
A banking example that sticks. Suppose 0.5% of all wire transfers over £100k In the synthetic Merehaven case are actually fraudulent. So the prior is Pr(fraud) = 0.005. Suppose a particular anomaly detection model flags a transaction as suspicious. The model has the following performance:
- If a transaction is genuinely fraudulent, the model flags it 95% of the time. So Pr(flag ∣ fraud) = 0.95. This is the likelihood.
- If a transaction is genuine, the model still flags it 2% of the time (false positives). So Pr(flag ∣ genuine) = 0.02.
A new transaction comes in, and the model flags it. What’s the probability it’s actually fraudulent?
Let’s compute carefully. We want Pr(fraud ∣ flag). Bayes:
The denominator Pr(flag) is the total probability of getting a flag, combining both sources:
Pr(flag) = Pr(flag ∣ fraud) ⋅ Pr(fraud) + Pr(flag ∣ genuine) ⋅ Pr(genuine)
= (0.95)(0.005) + (0.02)(0.995) = 0.00475 + 0.01990 = 0.02465
Now plug in:
So if the model flags a transaction, there’s only about a 19% chance it’s actually fraudulent. That’s counter-intuitive. The model is “95% accurate at detecting fraud,” isn’t it? Yes, but fraud is rare, and even a small 2% false positive rate applied to the vast majority of genuine transactions produces many more false alarms than true alarms. This is why every serious fraud and AML operation needs human review queues sized to handle the false positive load, and why model operators obsess over the trade-off between recall and precision.
This is also why any headline like “AI detects cancer with 95% accuracy” should be read with suspicion until you know the base rate. The famous mathematical trap, called base rate neglect, gets people every time.
Read this as a pipeline for updating belief. You start with a prior, you observe evidence, you weight by likelihood, and you end with a posterior. The posterior is the new quantity you should base decisions on. The diagram is the calculation we just did, arrow by arrow. Memorise the shape.
If asked: “A diagnostic model is 99% accurate. It says someone is a fraudster. How worried should the bank be?”
How do you find the best parameters for a model of the world?
We’ve seen that a model is a function with parameters, and training is finding good values for those parameters. Bayes’ rule gives us a principled way to do the finding.
Suppose you assume that the data you’ve observed was generated by some distribution fθ whose shape you know (say, a Gaussian) but whose parameters θ you don’t know. You want to estimate θ from the data. One approach is maximum a posteriori (MAP) estimation, which says: pick the value of θ that has the highest posterior probability given the data.
Applying Bayes, the posterior over parameters is:
The denominator doesn’t depend on θ, so for finding the maximum you can ignore it. This leaves:
θ̂MAP = arg maxθ Pr(X = x ∣ θ) ⋅ Pr(θ)
For a dataset of multiple independent observations, the likelihood of the whole dataset is the product of individual likelihoods:
This is beautiful but numerically nasty. Products of many small numbers underflow. Remember the log trick. Take logs of everything:
Now we’re maximising a sum, not a product, and we’re safe from underflow. If you drop the prior term log Pr(θ), you get maximum likelihood estimation (MLE), which is the same thing with a “flat” prior that treats all parameter values as equally plausible before seeing data. MLE is the workhorse of classical statistical fitting. MAP is its Bayesian cousin, regularised by a prior.
Concrete walkthrough: fitting a Gaussian. Suppose we want to model the distribution of debt service coverage ratios across Merehaven Bank SME customers and we assume it’s approximately Gaussian. The Gaussian density is:
with parameters θ = (μ, σ). Given a sample of N observed DSCRs, the maximum likelihood estimate of μ is the sample mean (which we met above), and the MLE of σ2 is the sample variance. You get clean closed-form answers because the Gaussian has nice mathematical structure. For most other distributions, you don’t get closed-form answers, and you have to optimise the log-likelihood numerically. Numerically means gradient descent, which we spent the middle of this section building up.
So the full story of parameter estimation is: write down the log-likelihood of your data under your model, take its gradient with respect to the parameters, and step downhill (or uphill, depending on whether you’re minimising the negative log-likelihood or maximising the log-likelihood; same thing). This is exactly how logistic regression, probabilistic graphical models, and even many deep learning models with probabilistic output layers are trained. You’ve now seen the whole recipe.
Where this goes wrong. MLE overfits. With enough parameters relative to data, you can drive the likelihood arbitrarily high by memorising the training set. MAP with a sensible prior acts as a regulariser, pulling the estimate toward plausible values and preventing the worst of the overfitting. When you see terms like L2 regularisation in deep learning, they can often be re-derived as MAP estimation with a Gaussian prior over weights. Modern practice blurs the line between pure optimisation and Bayesian estimation, and good engineers keep both perspectives in mind.
If asked: “What’s the difference between maximum likelihood estimation and maximum a posteriori estimation?”
Parameters versus hyperparameters, and the other vocabulary distinctions
The last block of this section is a cluster of terminological distinctions that every practitioner uses reflexively and that beginners constantly mix up. You should be able to recite these in your sleep.
Parameters versus hyperparameters. A parameter is a value that the learning algorithm modifies during training, on the basis of the data. The weights w and bias b of a linear model are parameters. The weights of every neuron in a neural network are parameters. Parameters are what “learning” means: they change during training.
A hyperparameter is a value that the data scientist sets before training begins and that does not change during training. The SVM penalty C is a hyperparameter. The learning rate α in gradient descent is a hyperparameter. The number of layers in a neural network, the number of neighbours in k-NN, the depth of a decision tree, the regularisation coefficient in ridge regression, all hyperparameters.
Read this as the two kinds of knobs on a model. The hyperparameter knobs are set by you. The parameter knobs are set by the training algorithm. Both affect model behaviour. Neither can be skipped.
Classification versus regression. We met these briefly in Chapter 1. Classification assigns a label from a finite set of classes: spam or not spam, default or no default, high risk or low risk. Regression predicts a real-valued number: loss given default as a percentage, house valuation in pounds, expected time to next purchase in days. Same training data shape, different output shape, different loss functions, different evaluation metrics.
Model-based versus instance-based learning. This is a subtle but important distinction. A model-based algorithm uses the training data to compute a model with a relatively small set of parameters, and then throws the training data away. The model is self-contained and can predict on new inputs by a quick computation using just its parameters. Linear regression, logistic regression, SVM, and neural networks are all model-based.
An instance-based algorithm doesn’t compute a compact model. Instead it stores the entire training set and, at prediction time, compares the new input against the stored examples to make its decision. k-Nearest Neighbours (k-NN) is the canonical example. To predict the class of a new input, you find the k closest training examples in feature space and return the majority vote of their labels. There’s no “model” to speak of. The training data is the model.
Shallow versus deep learning. A shallow learning algorithm learns parameters directly from input features. Linear regression, SVM, logistic regression, and classical tree ensembles are shallow: the parameters they learn relate directly to the raw features you fed them. Deep learning, by contrast, stacks multiple learned layers, where each layer’s parameters operate not on raw features but on the outputs of the previous layer. In a deep neural network with six layers, only the first layer’s weights act directly on the input features; every subsequent layer’s weights act on transformed representations that the earlier layers constructed.
The distinction matters because deep learning’s extra layers enable the model to learn useful representations automatically, which is powerful and dangerous. Powerful because you don’t have to hand-engineer features as painstakingly. Dangerous because the learned representations are hard to interpret, which matters enormously in regulated banking contexts where you must be able to explain every decision. We’ll come back to deep learning in Chapter 6, and to interpretability of deep models in later chapters.
If asked: “Explain the difference between model-based and instance-based learning with a banking example.”
Part II: Fit models before adding machinery
Simple models establish the baseline and expose the objective. Optimisation then becomes an observable route from initial parameters to a tested operating point.
Choose transparent baselines before complexity
How do you draw the best straight line through a cloud of points?
In 1805, a French mathematician called Adrien-Marie Legendre, working on a problem in astronomy, published a technique for fitting a line to a collection of noisy observations. He called it the “method of least squares,” and the paper was a modest six-page appendix to a larger astronomy monograph. Legendre argued, with typical French clarity, that if you wanted to choose the line that best fit the data, you should choose the one that minimised the sum of squared differences between the line and the data points. He did not bother to prove it was optimal. He simply said it was “of all the principles which can be proposed, the most general, the most exact, and the easiest to apply.”
Three years later, Carl Friedrich Gauss published a more rigorous derivation and claimed, annoyingly, that he had been using the method since 1795 but had not bothered to write it down. Whether Gauss was first or second, the least-squares method has been the workhorse of every field that fits a line through points for more than two centuries, and it is exactly what modern linear regression computes.
Here’s what we’re trying to do. Imagine you are on the retail banking strategy team and you want a quick model that predicts a customer’s annual fee income from a handful of their observable attributes: years tenure, number of products held, total deposits, and average monthly card spend. You have 50,000 historical customers with all those attributes plus their actual annual fee income. You want a function that takes the four features and returns a fee estimate for a new customer.
The linear regression assumption is that fee income is, approximately, a weighted sum of the features plus a constant:
fw, b(x) = w ⋅ x + b = w(1)x(1) + w(2)x(2) + w(3)x(3) + w(4)x(4) + b
The vector w holds four weights, one per feature. The scalar b is the bias, a baseline fee income when all features are at zero. Our job is to find the specific w and b that best match the historical data.
The “best” needs a precise definition. In linear regression, “best” means: the values of w and b that make the model’s predictions as close to the true fee incomes as possible, on average, where “close” is measured by the squared difference. For one historical customer i the squared error is (fw, b(xi) − yi)2. For the whole training set the average squared error is:
This is the mean squared error or MSE, and it is the most famous loss function in classical statistics. L is a function of the parameters. Plug in any specific w and b, get a number. The number tells you how bad those parameters are. Find the w* and b* that make L as small as possible and you have your linear regression.
Contrast with the SVM from Chapter 1. In SVM, the hyperplane sits as far as possible from all training points, because it’s a decision boundary separating two classes and the distance is your safety margin. In linear regression, the hyperplane sits as close as possible to all training points, because it’s a prediction surface that has to pass through the middle of the data cloud, not skirt around it. Same algebraic object (a hyperplane), opposite geometric role. The picture below shows the difference.
Read this as “same shape, opposite job.” A hyperplane is just a flat surface, and what makes it SVM or linear regression is what you tell it to do.
Why squared error specifically, and not absolute error or cubic error? This is the question every thoughtful beginner asks, and it has three answers.
First, mathematical convenience. The squared function is smooth and differentiable everywhere, while absolute value has a kink at zero that makes the derivative undefined. Smooth functions are easier to optimise, and in 1805 Legendre needed an optimisation technique that could be done by hand. Squared error gave him one.
Second, closed-form solutions. If you write out the gradient of the MSE with respect to w and b, set it equal to zero, and solve, you get a direct algebraic formula for w* and b*. No iterative optimisation, no learning rate, no stopping criterion. Just a matrix inversion. The closed-form solution is:
w* = (X⊤X)−1X⊤y
assuming we’ve folded b into w by adding a column of ones to X. Don’t panic about the notation if it’s new. What matters is that this is a formula you can evaluate in one pass over your data. No iterative training. No gradient descent. Plug in the data, get the parameters. For decades this was the definition of a well-behaved model: it had a closed form.
Third, the statistical interpretation. If you assume the true relationship between features and target is linear plus Gaussian noise, then the least-squares estimate is the maximum likelihood estimate of the parameters. We derived maximum likelihood in Chapter 2, and now we see it popping up again as a justification for the squared loss. The same mathematical principle keeps returning, wearing different hats.
A concrete walkthrough with tiny numbers. Forget the four-feature fee model for a moment. Consider the simplest possible linear regression: one feature, three training points. Our feature is “years tenure” and our target is “annual fee income in pounds.” Training data:
| Customer | Years tenure | Annual fee |
|---|---|---|
| 1 | 2 | 120 |
| 2 | 5 | 210 |
| 3 | 8 | 330 |
Assume a linear model ŷ = wx + b with scalar parameters. The closed-form least-squares solution for a single feature is:
Compute the means: x̄ = (2 + 5 + 8)/3 = 5, ȳ = (120 + 210 + 330)/3 = 220. Compute the deviations: (xi − x̄) = (−3, 0, 3), (yi − ȳ) = (−100, −10, 110). Products: (−3)(−100) + (0)(−10) + (3)(110) = 300 + 0 + 330 = 630. Sum of squared deviations: 9 + 0 + 9 = 18. So w* = 630/18 = 35, and b* = 220 − 35 ⋅ 5 = 45.
The fitted model is ŷ = 35x + 45. For a new customer with 4 years tenure, predicted fee is 35 ⋅ 4 + 45 = 185. That’s it. One feature, three points, pencil and paper arithmetic, real working model. Scale this up to 240 features and 180,000 points and the same principle holds: matrix algebra does the work. No gradient descent required.
Formal statement of the training problem. Given N labelled examples {(xi, yi)}i = 1N with yi ∈ ℝ, find parameters (w*, b*) that minimise . When this minimisation is done, the prediction for any new input x is ŷ = w*⊤x + b*. Reading the loss left to right: for each training example, compute the model’s prediction, subtract the true value, square the difference, add them all up, divide by the count. Any choice of parameters that makes this number smaller is a better choice.
Where this goes wrong. Linear regression assumes two things that are frequently violated. First, that the true relationship is actually linear in the features, which means that doubling a feature exactly doubles its contribution. Fee income does not grow linearly with tenure forever. The tenth year of a relationship does not add twice as much fee income as the fifth year; there are diminishing returns. If you blindly fit a linear model to a curved relationship, you end up with a line that is systematically wrong everywhere. The fix is feature engineering: add a tenure2 term, or use a log transform, or switch to a model that handles non-linearity natively.
Second, linear regression assumes that all training points are equally important and equally reliable. A single extreme outlier can drag the line far away from the rest of the data, because the squared error for that one point dominates the sum. If your training set contains a wealth management customer whose annual fee is £30,000 while everyone else is under £1,000, the linear regression will skew heavily toward accommodating that one customer, and the predictions on ordinary customers will suffer. The fixes are well-tested regression (which uses a less punishing loss function, like Huber loss or absolute error) or outlier removal before fitting.
There is also a third failure mode you should know about by name: multicollinearity. If two of your features are nearly linear combinations of each other (for example, “total deposits” and “total current account balance” in a dataset where most customers hold only current accounts), the matrix X⊤X becomes nearly singular and the inversion becomes numerically unstable. The closed-form solution gives you wild weights that cancel each other out and react catastrophically to small changes in the training data. The fixes are feature selection (drop one of the correlated features), regularisation (ridge regression, which we’ll meet in Chapter 5), or using pseudo-inverse variants.
If asked: “Why do the worked design uses squared error instead of absolute error in linear regression?”
How do you turn a straight line into a probability?
Now we face a slightly different problem. Linear regression predicts a real number. What if you want to predict a binary outcome, like whether a customer will default on a credit card payment in the next ninety days? You could just fit a linear regression with yi ∈ {0, 1}, and some people do, but it has an obvious defect: the model can predict values less than zero or greater than one, which are meaningless as probabilities. The fix, invented in the 1940s by the Belgian mathematician Pierre-François Verhulst and refined into its modern form by Sir David Cox in 1958, is to take the linear score and squash it into the range (0, 1) using the logistic function:
The σ is a lowercase Greek sigma, and the function is called the sigmoid because its graph is S-shaped. Some key values: σ(0) = 0.5, σ(2) ≈ 0.88, σ(−2) ≈ 0.12, σ(5) ≈ 0.993, σ(−5) ≈ 0.007. As z goes to positive infinity, σ(z) approaches 1. As z goes to negative infinity, σ(z) approaches 0. For any real input z, the output is a valid probability.
The logistic regression model is then:
You can see the familiar w⊤x + b term from linear regression, wrapped inside the sigmoid. That’s all logistic regression is: a linear score fed through a squashing function. The output is interpreted as Pr(y = 1 ∣ x): the probability that the true label is 1 given the observed features. For a credit card default model, it’s the probability of default. For a fraud model, it’s the probability of fraud. For a cross-sell model, it’s the probability of take-up.
Why not just use linear regression with binary labels? Three reasons. First, the predictions can fall outside [0, 1], which you then have to clip, and clipped predictions are not differentiable at the clipping boundary, so you can’t train them with gradient methods. Second, the variance of the errors depends on the predicted probability, violating one of the assumptions of linear regression (homoscedasticity). Third, the squared error loss is the wrong loss for probabilities: it treats a prediction of 0.99 when the truth is 1 as about the same as a prediction of 0.51 when the truth is 1, whereas any sensible probability scoring rule would reward the confident correct prediction far more. Logistic regression fixes all three issues by choice of model (sigmoid) and choice of loss (log-likelihood, below).
How do we train it? Not by squared error. Instead, the worked design uses maximum likelihood estimation, which you met in Chapter 2. Here’s how it unfolds.
For a single training example (xi, yi), the model says the probability of observing the label yi is fw, b(xi) if yi = 1, and 1 − fw, b(xi) if yi = 0. These two cases can be combined into a single compact expression:
Pr(yi ∣ xi, w, b) = fw, b(xi)yi ⋅ (1 − fw, b(xi))1 − yi
Check the math. When yi = 1, the exponent on the first term is 1 and the exponent on the second is 0, so the whole thing simplifies to fw, b(xi). When yi = 0, the exponent on the first is 0 and the second is 1, so it simplifies to 1 − fw, b(xi). One formula, two cases, no branching. The trick of using yi and (1 − yi) as exponents to pick between two expressions is a common idiom and you’ll see it constantly.
For the whole training set, assuming examples are independent, the likelihood is the product of individual likelihoods:
We want to find w and b that maximise this. But we know from Chapter 2 that products of many small numbers are numerically nasty. Take the logarithm to turn products into sums, and we get the log-likelihood:
Maximising the log-likelihood is equivalent to maximising the likelihood, because logarithm is a strictly increasing function, so the location of the maximum is the same. Flip the sign and you get the binary cross-entropy loss, which is what you’ll see in almost every code example:
The minus sign in front and the division by N are conventions so that the number is positive, normalised per example, and gets smaller as the model improves. When you see “cross-entropy loss” in a deep learning framework, this is exactly what it means.
importantly, logistic regression has no closed-form solution. Unlike linear regression, there is no matrix formula that gives you w* and b* directly. The log-likelihood is a concave function of the parameters (its negative is convex), so it has a unique global maximum, but finding that maximum requires iterative optimisation: gradient descent, Newton’s method, or quasi-Newton methods like L-BFGS. This is the first place in our journey where we meet an algorithm whose training is not an algebraic formula but an iterative procedure. Chapter 4 covers the mechanics of gradient descent in detail; for now just know that training a logistic regression in practice means “start with random weights, compute the gradient of the BCE loss, step downhill, repeat until convergence.”
Read this as the full loop. At inference time, you run the top path: linear score, sigmoid, probability, threshold. At training time, you also run the bottom path: compute the loss, take its gradient, update the parameters, iterate.
Concrete walkthrough: credit card default In the synthetic Merehaven case. Suppose you are building a simple credit card default predictor with three features: utilisation ratio (current balance divided by credit limit), months since last late payment (high means clean history), and number of credit products held. Say after training you get weights w = (3.5, −0.2, 0.4) and bias b = −2.1. These weights have interpretable meanings: higher utilisation pushes toward default (positive weight on utilisation), more months since last late pushes away from default (negative weight), and more credit products modestly pushes toward default (mildly positive weight).
For a customer with utilisation 0.8, last late payment 24 months ago, holding 3 credit products:
z = 3.5 ⋅ 0.8 + (−0.2) ⋅ 24 + 0.4 ⋅ 3 + (−2.1) = 2.8 − 4.8 + 1.2 − 2.1 = −2.9
The model says there’s a 5.2% chance this customer defaults in the window of interest. Low risk. If the utilisation were instead 0.95 and the last late payment were 3 months ago:
z = 3.5 ⋅ 0.95 + (−0.2) ⋅ 3 + 0.4 ⋅ 3 + (−2.1) = 3.325 − 0.6 + 1.2 − 2.1 = 1.825
86.1% chance of default. High risk. The same model, the same weights, giving materially different probabilities for different customers. This is why logistic regression is the absolute workhorse of retail credit risk in banking: every scorecard at every high-street bank, from Merehaven Bank to Barclays to HSBC, is a logistic regression at its heart, often with additional discretisation or binning on top for interpretability. The weights are reviewed by credit experts and challenge teams; the scores are mapped to rating grades; and the whole apparatus has been stable for thirty-plus years because it works.
Where this goes wrong. Three common failure modes.
First, probability calibration drift. A logistic regression trained on one population may give probabilities that are systematically biased on a different population. If your training data had 5% defaults and your production population has 3% defaults, the raw probabilities will be too high. Calibration techniques like Platt scaling or isotonic regression can correct this, but the drift has to be monitored. In credit risk this matters because probability of default is used directly for regulatory capital calculation and any miscalibration flows through to capital numbers.
Second, nonlinear interactions. Logistic regression is linear in its features. If the true relationship between features and default involves interactions (e.g., high utilisation is only a problem when tenure is short), a plain logistic regression will miss them unless you explicitly engineer interaction terms. This is why logistic regression in regulated credit often sits alongside or is replaced by gradient boosted trees, which find interactions automatically.
Third, separation. If your features are so predictive that they perfectly separate defaulters from non-defaulters in the training set, the maximum likelihood estimate of w is infinite. This is mathematically true and computationally disastrous. The training procedure fails to converge, and the weights you get are extreme and meaningless. The fix is regularisation, which we come back to in Chapter 5, and which in essence adds a penalty term to the loss that keeps weights from growing too large.
If asked: “Why is logistic regression called ‘regression’ if it’s actually a classification algorithm?”
How do you build a model that a credit officer can read?
Decision trees are the most interpretable machine learning algorithm in common use, and interpretability is the reason they refuse to die even in the age of massive neural networks. A decision tree is just a sequence of yes/no questions about the features, arranged in a branching structure, with a prediction at each leaf. You can print one on a sheet of A4 paper, hand it to a credit officer, and they can trace through it exactly as they would trace through a policy document.
Here’s what one looks like for a simplified SME default model:
Read this top to bottom. Every new loan enters at the top. The first question asks whether DSCR is below 1.1. If yes, we check whether LTV is also above 0.75. If both answers put us in the riskiest category, we predict a 42% probability of default. If not, we land in one of four leaves, each with a specific predicted probability. The whole model is summarised by this picture and the four numbers at its leaves. Any credit officer can read it. Any compliance reviewer can challenge it. Any model risk team can reproduce its output by hand.
This interpretability is a profound advantage. When a customer disputes a credit decision under applicable data-protection and consent rules article 22, a logistic regression answer would be something like “your score was -1.8 because the model weights your utilisation at 3.5 and your utilisation was high.” A decision tree answer is “your application had DSCR 0.95 and LTV 0.82, which placed it in the branch where we predict default with probability 0.42, above the approval threshold.” The second answer is comprehensible to a human whose job title is not “statistician.”
How does the algorithm figure out which questions to ask, and in what order? This is where the cleverness lies. The basic recipe, known as ID3 (Iterative Dichotomiser 3, invented by Ross Quinlan in 1986 at the University of Sydney), is deceptively simple and deeply elegant. You start with all your training data in one bucket. You try every possible question you could ask at the root, score each one by how much “mess” it eliminates, and pick the best. You then recursively repeat this procedure on each of the two resulting buckets. You stop when the bucket is pure enough, or when it’s too small to split further, or when you hit a pre-set depth limit.
The bit we need to define is “mess.” In ID3, mess is measured by entropy, a concept borrowed from information theory. For a set S of labelled examples, define p as the proportion of examples in S that are class 1 (say, defaulters). The entropy of S is:
H(S) = −plog p − (1 − p)log (1 − p)
What does this number mean? When p = 0 or p = 1, everything in S is one class, no mess, and H(S) = 0 (the conventions are that 0log 0 = 0). When p = 0.5, half the set is each class, maximum mess, and H(S) = 1 if you use log2 or ln 2 if you use natural log. Between these extremes, the entropy rises and falls smoothly. It’s a measure of “how uncertain you’d be if you had to guess the label of a random example from S.” When uncertainty is highest (50/50), entropy is highest. When uncertainty is zero (everyone is one class), entropy is zero.
When you split S on a feature and threshold, you get two subsets S− and S+. The combined entropy of the split, weighted by how many examples ended up on each side, is:
Read this as “the entropy of the left bucket weighted by its size, plus the entropy of the right bucket weighted by its size, divided by the total size.” A good split is one where both S− and S+ are more homogeneous than S was. In that case the weighted combined entropy is lower than H(S), and the difference is called the information gain. The algorithm searches over every feature j and every threshold t, computes the information gain, and picks the pair with the highest gain.
Concrete walkthrough with numbers. Suppose you have 12 historical loans, 4 defaulters and 8 non-defaulters. The entropy of the whole set is .
You try splitting on “DSCR < 1.1”. Suppose this gives you $S_- = $ 7 loans (5 defaulters, 2 non-defaulters) and $S_+ = $ 5 loans (no defaulters, 5 non-defaulters). Wait, that doesn’t quite work with our totals; let me recount. Try: $S_- = $ 7 loans (4 defaulters, 3 non-defaulters) and $S_+ = $ 5 loans (0 defaulters, 5 non-defaulters). Total checks: 4 + 0 = 4 defaulters, 3 + 5 = 8 non-defaulters. OK.
. That’s high. The left bucket is still messy.
. The right bucket is pure. All five are non-defaulters. Clean.
Weighted combined entropy: .
Information gain: 0.918 − 0.575 ≈ 0.343.
That’s a meaningful reduction in entropy. The split on DSCR has cleanly carved off a chunk of non-defaulters. The algorithm tries every other feature and threshold, computes the information gain for each, and picks the highest. Then it recurses into S− and continues splitting. S+ is pure, so it becomes a leaf with prediction “non-default” and no further splitting is needed.
Formal statement of the algorithm. ID3 builds a decision tree by recursively selecting the feature and threshold that maximise information gain on the current subset of training data, stopping when the subset is pure, too small, or when the gain is below a threshold. At a leaf, the prediction is the majority class (for classification) or the mean target (for regression). The resulting model is a tree structure where each internal node asks a question about one feature and each leaf emits a prediction.
- All examples in the leaf are the same class, so the leaf is perfectly pure.
- No remaining feature can split the examples (the features don’t distinguish them).
- The best available split reduces entropy by less than a pre-set threshold ϵ.
- The tree has reached a maximum depth d set in advance.
Both ϵ and d are hyperparameters. They have to be chosen before training. Too lenient (small ϵ, large d) and the tree grows enormous, memorising noise in the training data and overfitting spectacularly. Too strict and the tree is too shallow to capture real patterns. The right values come from validation-set tuning, which we cover in Chapter 5.
The extension to C4.5. ID3 is the academic version. In practice, the most widely used single-tree algorithm is C4.5, also from Quinlan (1993), which adds three important refinements:
- Handles continuous and discrete features. ID3 really wants discrete features; C4.5 handles continuous features by finding the best threshold as part of the split selection.
- Handles missing values. Real banking data has missing values everywhere (a customer’s previous address may not be recorded; an SME’s last year of management accounts may be overdue). C4.5 has explicit machinery for propagating examples with missing values down both branches of a split.
- Pruning. After the tree is fully grown, C4.5 goes back through and prunes branches that don’t earn their keep, replacing them with leaves. Pruning is a regularisation technique that reduces overfitting without needing to stop growth early. It’s sometimes more effective than early stopping.
Where this goes wrong. Decision trees have several famous failure modes.
First, they are greedy. ID3 and C4.5 make each split decision locally, without looking ahead to future splits. The best first split is not always the first split of the globally best tree. As a result, decision trees often find suboptimal solutions. The cure is ensembles: random forests and gradient boosted trees, which combine many trees to cancel out the errors of individual greedy choices. We’ll meet these in Chapter 7.
Second, they are unstable. A small change in the training data can cause the entire tree structure to change, because a change in which feature is chosen at the root cascades through everything below. This is why individual decision trees are rarely used in production credit risk today; ensembles smooth out the instability.
Third, they struggle with smooth relationships. If the true relationship between a feature and the target is genuinely smooth (like a linear trend), a decision tree has to approximate it with a staircase of splits, which is clunky and needs many splits to look smooth. Linear and logistic regression handle smooth relationships natively and elegantly.
And fourth, they overfit if allowed to grow without limit. A deep tree can memorise individual training points with unique paths, achieving zero training error while being useless on new data. Pruning and depth limits are the standard defences.
If asked: “Why do decision trees tend to overfit, and what do we do about it?”
How does SVM deal with data that isn’t a clean straight line?
In Chapter 1 we met SVM as the classifier that draws the widest possible corridor between two classes. That was the happy case: the classes are linearly separable and you just find the widest separating hyperplane. Real data is not that tidy. Two failure modes are nearly universal.
Failure mode one: noise. Even when the classes are mostly separable, there are often a few points in the wrong place. A healthy-looking SME that defaulted because of an unexpected fraud at its largest customer. A fraudulent transaction that looks utterly ordinary because the criminal was patient. A single mislabelled record in the training data. These noise points mean no straight line can separate the classes perfectly, and the hard-margin SVM from Chapter 1 has no solution: its constraints cannot all be satisfied simultaneously.
Failure mode two: inherent non-linearity. Even with clean labels, sometimes the geometry of the problem is genuinely not linear. Picture a situation where the “default” customers are clustered in a ring around the “non-default” customers in feature space. No straight line can carve out the ring. You need a curved boundary, and a hyperplane is by definition flat.
SVM has elegant answers to both. Let’s take them one at a time.
Noise, and the soft-margin SVM. The original (hard-margin) SVM requires every training point to sit on the correct side of the margin. For noisy data, this constraint is too strict. The soft-margin variant, invented by Corinna Cortes and Vladimir Vapnik in 1995, relaxes the constraint by allowing training points to violate the margin, at a cost.
The trick is the hinge loss:
Lhinge(yi, w, b) = max (0, 1 − yi(w⊤xi − b))
Read this carefully. If the point xi is on the correct side of the margin (that is, yi(w⊤xi − b) ≥ 1), the expression inside the max is negative or zero, so the max gives zero: no penalty. If the point is on the wrong side of the margin (either inside the corridor or entirely misclassified), the expression inside the max is positive, and the loss grows linearly with how far wrong the point is.
The combined objective for the soft-margin SVM is:
Two terms. The first, C∥w∥2, is the same old margin-maximisation term: keep w small so the corridor is wide. The second is the average hinge loss: pay a penalty for any point that violates the margin. The hyperparameter C controls the trade-off: large C means “heavily penalise margin violations, which gives a narrower margin that fits the training data tightly”; small C means “be tolerant of violations in exchange for a wider margin.”
Setting C is one of the most important choices when using SVM. Too large and the model overfits by twisting the margin to accommodate noisy points. Too small and the model underfits by ignoring real structure. The right value comes from validation-set tuning. In banking use cases where training sets are modest and noise is real, a moderate C is usually appropriate.
Non-linearity, and the kernel trick. This is one of the most beautiful ideas in classical machine learning, and it deserves a proper story.
In the late 1990s, Vapnik and peers at AT&T Bell Labs were trying to extend SVM to problems where the classes were not linearly separable in the original feature space. The obvious idea was to transform the features into a higher-dimensional space where they would be separable. Take a two-dimensional problem where the classes form concentric rings, for example, and map each point (q, p) to a new point in three dimensions. In this new space, the ring-shaped boundary becomes a plane, and SVM can find it directly.
The problem is that this explicit mapping can be expensive. If you want to project into a 100-dimensional space, you compute 100 new features per point. If you want a 1,000-dimensional space, 1,000 features. For some useful mappings, the target space is formally infinite-dimensional and you can’t compute the features explicitly at all.
Here’s the insight. The SVM optimisation, when written in its “dual form” using Lagrange multipliers, depends on the training data only through the dot products xi⊤xk between pairs of examples. Not the individual features. Just the dot products. If you had a way to compute the dot product in the transformed space without explicitly computing the transformed features, you could run the SVM optimisation as if you were working in the high-dimensional space, while never actually going there.
That’s the kernel trick. A kernel function k(xi, xk) is a function that takes two original feature vectors and returns the value that the dot product of their transformed versions would have, without actually computing the transformations. Some famous kernels:
Linear kernel: k(x, x′) = x⊤x′. The identity: no transformation. SVM with a linear kernel is just the plain linear SVM.
Polynomial kernel: k(x, x′) = (x⊤x′ + c)d for some degree d. Corresponds to projecting into a space where the features include all products of up to d original features. A degree-2 polynomial kernel implicitly captures all pairwise interactions.
Radial basis function (RBF) kernel: . This corresponds to projecting into an infinite-dimensional space, which sounds wild but is mathematically clean. The RBF kernel is the most popular SVM kernel in practice because it handles most non-linearities well with just one hyperparameter (σ, the bandwidth) to tune.
Read this as two paths to the same destination. The top path explicitly transforms the features, which is expensive or impossible. The bottom path uses a kernel function to compute the final dot product directly, which is cheap. Both lead to the same SVM solution. The kernel trick is how you get the benefits of the high-dimensional space without paying the cost.
Concrete walkthrough of the quadratic kernel. Take a 2D input x1 = (q1, p1) and x2 = (q2, p2). Define the transformation into 3D. The dot product in the transformed space is:
ϕ(x1)⊤ϕ(x2) = q12q22 + 2q1p1q2p2 + p12p22
Now compute the kernel k(x1, x2) = (x1⊤x2)2 = (q1q2 + p1p2)2. Expand:
(q1q2 + p1p2)2 = q12q22 + 2q1q2p1p2 + p12p22
Identical. The kernel gives you the result of working in 3D by doing arithmetic in 2D. If you had instead projected into 100 dimensions, you’d still just compute (q1q2 + p1p2)2; the 100-dimensional explicit projection would take 100 multiplications per point, but the kernel takes 3 (two multiplies plus a square). The saving scales massively as the projected space grows.
Banking applications of non-linear SVM are narrow but real. In regulated credit risk, SVM with non-linear kernels is rarely used because the decision boundary is hard to explain to regulators and the model does not give calibrated probabilities out of the box. In fraud detection and AML, where interpretability requirements are less stringent and subtle non-linear patterns matter, RBF-kernel SVM was a staple technique through the late 2000s before being largely displaced by gradient boosting and later deep learning. You will still find it in legacy systems and as a baseline in model evaluations.
If asked: “Explain the kernel trick in one paragraph without jargon.”
What if the simplest possible algorithm is also one of the best?
k-Nearest Neighbours is the anti-algorithm. It trains nothing. It builds no model. It has no weights to fit, no parameters to optimise, no loss function to minimise. All it does is store the entire training set and, when asked to predict on a new input, find the k training examples closest to the new input and return the majority vote (for classification) or the average (for regression). That’s it. The whole algorithm fits in three sentences.
Despite its simplicity, or because of it, k-NN is remarkably useful, and it is the first technique every competent data scientist reaches for when they need a sanity check baseline. If your fancy new model can’t beat k-NN on a given problem, either the problem is extremely hard or your fancy model is broken. Either way, k-NN gives you an honest floor.
The formal algorithm. At prediction time:
- For the new input x, compute the distance from x to every training example.
- Identify the k training examples with the smallest distances.
- For classification, return the majority class among those k. For regression, return the average target value.
At training time: do nothing. Just remember the data. This is what “instance-based learning” means, and k-NN is the canonical example. The training set is the model.
Choosing the distance metric. Everything depends on how you measure closeness. The most common choice is Euclidean distance:
In words: subtract corresponding entries of the two vectors, square the differences, sum them, take the square root. In two dimensions this is the familiar straight-line distance from school geometry. In higher dimensions it generalises directly.
Cosine similarity is another common choice, and especially popular when the vectors represent directions rather than positions. Defined as:
This is the dot product of the two vectors divided by the product of their lengths. If two vectors point in the same direction, cosine similarity is 1. If they are perpendicular, it’s 0. If they point in opposite directions, it’s −1. To use it as a distance (where smaller means closer), you negate it or subtract from 1. Cosine similarity is the standard metric for comparing text embeddings, document embeddings, and customer behavioural vectors where the magnitude is less informative than the direction.
Other distance metrics include:
- Manhattan (L1) distance: ∑j|xi(j) − xk(j)|. Summed absolute differences, like city-block distance in a grid layout.
- Chebyshev distance: maxj|xi(j) − xk(j)|. The largest single-dimension gap.
- Mahalanobis distance: Euclidean distance but scaled by the inverse covariance matrix of the features. It accounts for feature correlations and different scales.
- Hamming distance: for binary or categorical vectors, the number of positions where they differ.
Choosing the right distance is a problem-specific decision. For numerical features on comparable scales, Euclidean is the default. For text, cosine. For high-dimensional sparse data, cosine or L1. For features on vastly different scales (e.g., age in years versus income in pounds), you should standardise first or use Mahalanobis.
Read this as the full flowchart of k-NN inference. No training step, no parameters to store; the algorithm is pure lookup and aggregation.
Choosing k. The single hyperparameter k controls the bias-variance trade-off. Small k (like 1) means each prediction depends on a single nearest neighbour, so the model is very flexible but sensitive to noise: one bad training example can flip a prediction. Large k (like 50) averages over many neighbours, smoothing out noise but also smoothing out real structure. The sweet spot depends on the problem and is typically found by validation.
A useful rule of thumb: start with where N is the size of your training set. For N = 10, 000, try k = 100. Adjust from there based on validation performance. An even better practice is to use odd values of k for binary classification to avoid tied votes.
Concrete walkthrough: customer similarity for cross-sell. In the synthetic Merehaven case, suppose the commercial banking marketing team wants to identify which SMEs are most likely to take up a new foreign exchange hedging product. They have 500 SMEs that already hold it (the “positive” examples) and 50,000 SMEs that don’t (the potential market). For each SME, they have a 40-dimensional feature vector capturing industry, size, payment patterns, product usage, and geography. They use k-NN with k = 10 and cosine similarity.
For a candidate SME, they compute cosine similarity to all 50,500 training SMEs, pick the 10 closest, count how many are FX product holders, and emit that count (or its fraction) as a score. A candidate whose 10 nearest neighbours include 6 existing FX users gets a score of 0.6. A candidate whose 10 nearest include no existing users gets 0.0. The team ranks all candidates by this score and the top 2,000 get contacted by their analysts.
Formal statement. Given a training set {(xi, yi)}i = 1N and a distance function d, the k-NN prediction for new input x is:
ŷ(x) = aggregate({yi : xi ∈ NNk(x)})
where NNk(x) denotes the set of k training points closest to x under d, and “aggregate” is majority vote for classification or mean for regression. There are no parameters w or b; the hyperparameters are k and the choice of d.
Where this goes wrong. k-NN has several serious limitations.
First, prediction is slow and memory-heavy. Every prediction requires computing the distance to every training point. For a training set of a million examples this is expensive, and for billions it is infeasible with naive implementations. The fix is approximate nearest neighbour (ANN) data structures like ball trees, KD-trees, HNSW graphs, or IVF indexes. Modern vector databases like Faiss, Pinecone, Weaviate, and pgvector are essentially industrialised k-NN engines. They let you query over billions of vectors in milliseconds. The retrieval layer in any RAG (retrieval-augmented generation) system is exactly this: a big ANN index that pulls the most relevant documents by embedding similarity. When you hear “vector search,” think “k-NN at scale.”
Second, the curse of dimensionality. In high-dimensional feature spaces, the notion of “nearest neighbour” becomes statistically weak. Almost all pairs of points have similar distances to a given query, so the “10 nearest” are not meaningfully closer than the “100 nearest.” This is a deep property of high-dimensional geometry and it’s why naive k-NN performs poorly on, say, raw pixel representations of images. The cure is learned embeddings: instead of running k-NN on raw features, you train a model (often a neural network) to map raw inputs to a lower-dimensional space where meaningful similarity structure exists, and then run k-NN in that space. This is exactly how modern semantic search works.
Third, sensitivity to feature scaling. Because k-NN is based on distances, features with larger numeric ranges dominate features with smaller ranges. If one feature is “income in pounds” (range 0 to 500,000) and another is “years tenure” (range 0 to 30), income completely dominates any Euclidean distance calculation. The fix is to standardise all features to zero mean and unit variance before running k-NN. This is the first thing you do, always. Forgetting it is a rite of passage for every new data scientist.
And fourth, no natural interpretability beyond the neighbours themselves. You can’t write down “the k-NN model” as a set of weights or rules. The best explanation you can give for a prediction is “this new customer is similar to these ten past customers, and here’s why they were each labelled this way.” In many contexts this is actually the most useful form of explanation, but in regulated contexts where model risk teams expect parametric explainability, it can be awkward.
If asked: “Why is k-NN still useful in a world of deep learning?”
Optimise by observing the loss
What is every learning algorithm secretly doing?
There is a beautiful unification waiting for you in this section, and once you see it, you cannot unsee it.
Take any of the five algorithms from Chapter 3. Strip away the historical baggage, the names, the cute analogies. What’s left? In every case, it’s three things: a way of measuring how wrong the model is on a single example, a way of summing that wrongness across the training set, and a way of nudging the parameters to make the sum smaller. That’s the entire shape of supervised machine learning. If you can write down those three things for any model, you can train it.
Here’s the table.
| Algorithm | Loss function | Optimisation criterion | Optimisation routine |
|---|---|---|---|
| Linear regression | (yi − f(xi))2 | mean squared error | closed-form or gradient descent |
| Logistic regression | −yilog pi − (1 − yi)log (1 − pi) | mean cross-entropy | gradient descent |
| SVM (soft margin) | max (0, 1 − yi(w⊤xi − b)) | mean hinge loss + C∥w∥2 | quadratic programming or gradient descent |
| Decision tree (ID3) | implicit (entropy reduction) | implicit (greedy info gain) | recursive greedy splitting |
| k-NN | implicit (no training) | implicit (no training) | nearest neighbour lookup at inference |
The first three rows have explicit losses, criteria, and optimisers. The last two are the historical accidents we mentioned: invented by intuition, with criteria reverse-engineered later. Every modern algorithm follows the explicit-three-part pattern. Every neural network you will meet in Chapter 6 fits the same template.
Why does this matter for an enterprise architect at a bank? Because when you sit in a model design review and someone proposes a new approach, you can ask three precise questions: What’s the loss function? What’s the cost function? What’s the optimiser? If the answers are clear, the proposal is grounded. If any of them is vague, push back. “We’re going to use a deep learning approach” is not an answer; it’s a category. “We’re going to minimise binary cross-entropy across a dataset of past defaults using Adam with learning rate scheduling, trained via a managed training service Training on the selected cloud platform or a managed training service Training Jobs on the selected cloud platform” is an answer. The three-part decomposition is your filter for distinguishing real proposals from buzzwords.
It also matters for governance. When the PRA’s SS1/23 supervisory statement on model risk management asks how a model is fitted, the right answer is structured: loss function, cost function, optimisation method, hyperparameters. A model risk management team that has internalised this structure will produce documentation that satisfies regulators. A team that has not will write essays full of hand-waving and end up rewriting them.
If asked: “What are the three building blocks of every supervised learning algorithm?”
Why do we ever need an iterative optimiser at all?
Here’s a fair question. In Chapter 3 we saw that linear regression has a closed-form solution. You don’t need to iterate. You compute one matrix inversion and you have the answer. So why do machine learning engineers spend so much time talking about gradient descent?
Three reasons.
First, most useful models do not have closed-form solutions. Logistic regression, almost every flavour of neural network, regularised models with non-smooth penalties, models with custom losses or constraints, all of these require iterative optimisation. Closed-form is the lucky exception, not the rule.
Second, even when a closed form exists, it can be impractical at scale. The closed-form linear regression solution requires computing (X⊤X)−1, which is a D × D matrix inversion. For D = 100 this is fine. For D = 10, 000 it’s painful. For D = 1, 000, 000 it is impossible on a single machine. Iterative methods like gradient descent have memory and compute costs that scale much more gently with model size, which is why they are the only realistic option for modern large models.
Third, the closed form does not generalise. As soon as you add a custom regularisation term that has no clean derivative, or you use a non-standard loss, or you have constraints on the parameters, the closed form vanishes. Iterative optimisation, by contrast, is general-purpose: tell me how to compute the loss and its gradient at any point, and I can find the minimum.
So the practical situation is: closed forms are nice when you can use them, but you can’t usually use them, and even when you can, you should still understand iterative optimisation because the next thing you do will probably need it.
The algorithm is humble in a way that more clever methods often are not. It does not try to plan a clever route. It does not look ahead. It just feels the immediate slope and steps downhill. And it works, more or less, for almost every loss function in machine learning, including ones with millions or billions of dimensions where no human or analytical method could plan a route through the space.
How does gradient descent actually work, line by line?
Gradient descent alternates observation and correction. It computes predictions, measures residual error, differentiates the aggregate loss and applies a bounded parameter update. The implementation below makes the data contract and update direction explicit.
def linear_gradient_step(x, y, weight, bias, rate):
prediction = x @ weight + bias
residual = prediction - y
grad_weight = (2 / len(x)) * (x.T @ residual)
grad_bias = (2 / len(x)) * residual.sum()
return weight - rate * grad_weight, bias - rate * grad_biasThe code is intentionally small. A release-tested trainer must also define batching, stopping, numerical checks, random seeds, checkpointing and the evidence recorded for each run.
The setup. We have 200 data points. Each is a (spending on radio advertising in millions, units sold) pair. We want to fit a single-feature linear regression to predict sales from spending. The model is ŷ = wx + b, with two scalar parameters w and b to learn.
The loss is mean squared error:
To minimise L via gradient descent, we need its partial derivatives with respect to w and b. Apply the chain rule:
Reading these in plain English: for each training point, compute the residual (true minus predicted), multiply by negative two and (for the w derivative) by the feature value, sum across all points, average. The result is a number telling you how the loss would change if you nudged w (or b) up or down.
The gradient descent update rule is:
We subtract because the partial derivative points uphill and we want to go downhill. The learning rate α controls how big each step is.
Walk through it line by line.
dl_dw = 0.0 and dl_db = 0.0 initialise
accumulators for the gradient with respect to each parameter. We will
sum contributions from every training example into these.
N = len(spendings) gets the number of training examples,
which we’ll use to average the gradient later.
The for loop iterates over every training example. For
each example, it computes the residual
(sales[i] - (w*spendings[i] + b)) and uses it to update the
two accumulators. The expressions -2*spendings[i]*residual
and -2*residual are exactly the per-example terms in the
partial derivative formulas above. After the loop, dl_dw
and dl_db contain the sum (not yet the mean) of the
gradient contributions.
w = w - (1/float(N))*dl_dw*alpha updates w
by subtracting the average gradient times the learning rate. The
(1/float(N)) converts the sum to a mean. The same thing
happens for b.
return w, b hands back the updated parameters so the
caller can use them in the next epoch.
The training loop wraps this in a counter:
def train(spendings, sales, w, b, alpha, epochs):
for e in range(epochs):
w, b = update_w_and_b(spendings, sales, w, b, alpha)
# log the progress
if e % 400 == 0:
print("epoch:", e, "loss: ", avg_loss(spendings, sales, w, b))
return w, bFor each epoch, we update the parameters once and occasionally log
the loss so we can watch the model improve. The avg_loss
helper just computes the mean squared error directly:
def avg_loss(spendings, sales, w, b):
N = len(spendings)
total_error = 0.0
for i in range(N):
total_error += (sales[i] - (w*spendings[i] + b))**2
return total_error / float(N)And the prediction function, once training is done, is one line:
def predict(x, w, b):
return w*x + bThat’s the entire gradient descent training procedure for linear regression in pure Python. About thirty lines. No external libraries.
epoch: 0 loss: 92.32
epoch: 400 loss: 33.79
epoch: 800 loss: 27.99
epoch: 1200 loss: 24.33
epoch: 1600 loss: 22.03
...
epoch: 2800 loss: 19.08
The loss starts at 92, drops fast in the first few hundred epochs as the algorithm finds the broad shape of the solution, then settles into slow refinement as it homes in on the exact bottom. After 15,000 epochs the parameters are essentially converged, and predicting on a new spending of 23.0 gives 13.97 units. This is the same answer the closed-form solution would give. We just got there by stumbling downhill instead of by matrix inversion.
Read this as one full training loop. Most modern training, no matter how complex the model, is some elaboration of this picture. Even GPT-class language model training is, at its core, this loop applied at extreme scale with fancy variants of step E and F.
Why does this work? Because at every point on the loss surface, the negative gradient is the direction of steepest descent, so each step makes the loss smaller (provided α is small enough). Repeated steps drive you toward a local minimum. For convex losses like linear regression and logistic regression, the only local minimum is the global minimum, so you are guaranteed to find the optimal parameters. For non-convex losses like neural networks, you find some local minimum, which is usually good enough in practice.
Where this goes wrong, in three places. First, the learning rate. As we saw in the failure specimen at the top of this section, if α is too large, each step overshoots and the loss bounces or diverges. If α is too small, training crawls and may never finish. Picking the right learning rate is a problem-specific judgement and there’s a small industry of techniques (learning rate finders, schedulers, warm-up periods) to help.
Second, the scale of the features. If one feature ranges from 0 to 1 and another ranges from 0 to 1,000,000, the gradient with respect to the second one will be vastly larger than with respect to the first. A single learning rate cannot serve both. The fix is to standardise features to comparable scales before training, which we covered in Chapter 3 in the k-NN section and which applies even more strongly here.
Third, initialisation. We start with w = 0, b = 0 in the linear case and it works fine because the loss surface is convex. For neural networks with many parameters, all-zeros initialisation is catastrophic because it makes every neuron in a layer compute the same thing, and they never differentiate. The standard fix is small random initialisation (e.g., Xavier or He initialisation), tailored to the architecture. Bad initialisation in deep networks can mean the difference between converging to a useful model and producing garbage. We’ll come back to it in Chapter 6.
If asked: “Walk me through one iteration of gradient descent on a logistic regression model.”
Why is full-batch gradient descent too slow for real datasets?
The procedure above computes the gradient using every training example before taking a single step. For 200 advertising data points, that’s instant. For the 180,000 SME loans we’ve been imagining throughout this edition, it’s still fine on a laptop. For ImageNet’s 14 million images or a language model trained on hundreds of billions of tokens, it is utterly impractical. A single epoch would take hours or days, and you typically need many epochs to converge.
The fix, invented in 1951 by Herbert Robbins and Sutton Monro at Rutgers and a managed cachecovered countless times since, is stochastic gradient descent (SGD). The idea is delightfully cheap: instead of computing the gradient on the whole training set, compute it on just one randomly chosen example. Take a tiny, noisy step in that direction. Then move to the next random example and do it again. Each step is now ridiculously cheap, but each step is also a much worse estimate of the true gradient.
The remarkable empirical fact is that this works. The noisy steps don’t cancel out the descent direction; they just add jitter. On average, you still descend, just with a wiggly path instead of a smooth one. And because each step is so cheap, you can take far more of them in the same wall-clock time, and you usually end up converging faster overall.
In practice, pure SGD (one example at a time) is almost never used. The compromise that nearly everyone uses is minibatch SGD: compute the gradient on a small randomly chosen subset (a “minibatch”) of examples, typically 32 or 64 or 256 or some power of two, take a step, and move on. This gives you most of the speed of pure SGD while reducing the noise to a manageable level. Modern GPUs are also brutally efficient at computing matrix operations on batches of moderate size, so minibatch SGD lines up well with the hardware as well as the math.
Read this as three speeds along a spectrum. Full-batch is slow and accurate. Pure SGD is fast and noisy. Minibatch is the sweet spot, and is what every production ML training loop on the planet actually does.
The Merehaven Bank context. When the bank trains a fraud detection model on tens of millions of historical transactions, it does not load all of them into memory and compute one giant gradient. It streams batches off disk, one minibatch at a time, computes the gradient on each batch, takes a step, and moves on. This is also why distributed training works: you can have multiple workers each computing gradients on different batches, then averaging them. Modern training frameworks like PyTorch, JAX, and TensorFlow are built around this minibatch loop, and they make distributed SGD as easy as setting a configuration flag.
The variance/speed trade-off controlled by batch size is one of the few hyperparameters that has solid theoretical understanding. Smaller batches give more frequent updates but noisier gradients. Larger batches give better gradient estimates but slower updates. The right batch size depends on memory budget, model size, and (counter-intuitively) on whether you want slightly more generalisation noise to escape sharp minima. For neural networks specifically, very small batches (32-128) often generalise better than very large ones, even though large batches train faster per epoch. This is one of those empirical findings that makes deep learning feel more like a craft than a science.
If asked: “Why use minibatch SGD instead of full-batch gradient descent?”
What makes Adam better than vanilla SGD?
Once you have minibatch SGD, you have a working training algorithm. But there are several stubborn problems with it that researchers have spent thirty years fixing, and the modern descendants of SGD that come out of that work are materially better at training the models we actually care about.
Three problems, three fixes.
Problem one: features have different curvatures. Some features have steep gradients (small change in parameter, big change in loss). Others have shallow gradients. With a single learning rate, the steep ones get over-corrected at every step while the shallow ones get under-corrected, and convergence is slow because you have to set the learning rate small enough not to blow up the steep ones, which means the shallow ones barely move.
The fix: per-parameter adaptive learning rates. Adagrad (Duchi et al., 2011) keeps a running sum of squared past gradients for each parameter and divides the current update by the square root of that sum. Parameters with historically large gradients get small effective learning rates. Parameters with historically small gradients get large effective learning rates. The result is that different parameters move at different speeds, all calibrated to their own gradient history, and you don’t have to hand-tune them.
Problem two: gradients are noisy and oscillate. With minibatch SGD, the gradient bounces around the true descent direction. In a long valley with gentle gradient along the valley floor and steep gradients across the sides, vanilla SGD oscillates side to side and crawls along the floor, wasting most of its motion on the cross-direction oscillations.
The fix: momentum. Polyak’s momentum (1964, but popularised in deep learning much later) maintains an exponentially weighted moving average of past gradients and uses that as the update direction instead of the current gradient alone. The effect is exactly like a heavy ball rolling down a hill: it builds up speed in directions where the gradient is consistent and damps out direction-flipping noise. In the long-valley analogy, the momentum cancels out the side-to-side oscillation and accelerates motion along the valley floor.
Problem three: gradients vanish or explode in deep networks. When you stack many layers, each layer’s gradient depends on the layers above it via the chain rule, and the per-layer factors can multiply together to give either tiny or enormous overall gradients. Tiny gradients mean the early layers don’t learn. Enormous gradients mean training diverges.
The fix (partial): adaptive optimisers like Adam. Adam (Kingma and Ba, 2015), which stands for “adaptive moment estimation,” combines per-parameter adaptive learning rates (like Adagrad) with momentum (like Polyak) and adds bias correction to handle the early steps of training when the running averages are unreliable. The result is an optimiser that “just works” on a wide range of problems without much tuning, and it has become the default for training neural networks in the 2020s. Variants include RMSprop (Hinton, 2012, never published as a paper but presented in a Coursera lecture), AdamW (Loshchilov and Hutter, 2019), and Lion (Chen et al., 2023).
Here is the abridged Adam update rule for a single parameter θ:
mt = β1mt − 1 + (1 − β1)gt
vt = β2vt − 1 + (1 − β2)gt2
m̂t = mt/(1 − β1t)
v̂t = vt/(1 − β2t)
Don’t memorise this. Just notice the structure. mt is a moving average of the gradient (momentum). vt is a moving average of the squared gradient (per-parameter scale). The hat versions are bias corrections that matter only at the start. The final update divides the smoothed gradient by the smoothed scale, giving each parameter its own effective learning rate. The hyperparameters β1, β2, ϵ have well-known defaults (0.9, 0.999, 10−8) that work for almost everything, which is why “use Adam with the defaults” is a perfectly reasonable starting point for a new project.
Read this as the family tree of optimisers. Each step adds one mechanism to fix one problem with the previous step. By the time you reach Adam, you have an optimiser that handles the most common pathologies and trains most things “out of the box.”
Important nuance. Adaptive optimisers like Adam are fantastic for neural networks but sometimes worse than plain SGD with momentum on simple convex problems. If you train a logistic regression with Adam, you may converge to a slightly worse solution than you would with plain gradient descent or with L-BFGS (a quasi-Newton method that is still the standard for medium-scale convex problems). Use the right tool for the job. For simple convex models, scikit-learn’s defaults usually pick a good solver automatically. For neural networks, Adam or AdamW with default hyperparameters is the right starting point.
importantly, gradient descent and its variants are not machine learning algorithms. They are general-purpose minimisation tools. They solve any minimisation problem where the function is differentiable. The same Adam optimiser that trains GPT-class language models will, with no modification, also train your logistic regression for credit default prediction or your neural network for fraud scoring. It’s the loss function and the model architecture that make the model what it is. The optimiser is just the engine that pushes the parameters around.
If asked: “Why has Adam become the default optimiser for neural networks?”
What other particularities make algorithms differ in practice?
Categorical features. Some algorithms accept categorical features natively. Decision trees in particular can split on categorical features like “sector code” or “product type” without any preprocessing. You hand them the strings and they figure out which categories belong on which side of the split. Other algorithms, linear regression, logistic regression, SVM, k-NN, require all features to be numerical. To use them with categorical features, you need to convert categories into numbers, usually by one-hot encoding (creating a new 0/1 feature for each category) or by target encoding (replacing each category with the mean of the target for that category). We’ll cover both in Chapter 5. For now, just know that “this algorithm needs numerical inputs” is a real constraint that influences which algorithm fits which problem.
Class weighting. Some algorithms, including SVM, scikit-learn’s logistic regression, and most tree ensembles, let you specify a weight per class. If you set the weight of the positive class to 10 and the weight of the negative class to 1, the optimiser is told that misclassifying a positive example is ten times as costly as misclassifying a negative one. This is enormously useful for imbalanced problems like fraud detection, where positives are rare and you do not want the model to ignore them by defaulting to always predicting “not fraud.” Class weighting is a cheap fix that often improves recall on the rare class without much code change. We’ll come back to imbalance handling in Chapter 5.
Probability outputs versus class outputs. Some classification algorithms naturally output a probability or score in (0, 1). Logistic regression and decision trees do this by construction. Others, including the original SVM and naive k-NN, only output a class label, not a probability. If you need probabilities (and in regulated banking you often do, because expected loss calculations need them), you have two choices. One is to use an algorithm that produces probabilities natively. The other is to use a calibration technique like Platt scaling (fit a logistic regression on the model’s raw scores) or isotonic regression (fit a non-parametric monotone calibration curve). Calibration is a separate post-processing step that is critical for any regulated ML model where the output is interpreted as a probability of default or fraud.
Online versus batch learning. Some algorithms can be incrementally updated as new data arrives, without retraining from scratch. Others must be retrained on the full dataset every time. This matters when your data is changing fast and you cannot afford to wait days for a retraining run. Online algorithms include online SGD variants of linear and logistic regression, certain neural networks with online updates, and a few specialised techniques. Batch algorithms include trees, SVM (in its standard formulation), and most kernel methods. In banking, the typical compromise is periodic retraining (weekly or monthly) on the full dataset, with monitoring in between to catch drift early.
Interpretability per the regulator. This isn’t really an algorithmic property, it’s a business constraint, but it shapes algorithm choice profoundly. Algorithms vary on how easy it is to explain their predictions to a non-technical audience or a model risk team. Decision trees are at one end (you can read the path). Linear and logistic regression are next (you can read the weights). Tree ensembles and kernel SVMs are in the middle (you can use feature importances and SHAP values). Deep neural networks are at the far end (interpretability requires post-hoc explainers and is often unsatisfying). For credit decisions on retail customers, interpretability is required by the decision policy under applicable customer-outcome obligations rules, which is a major reason that logistic regression remains the most common algorithm in retail credit risk despite being decades old.
Read this as a multi-axis decision matrix. Each axis is one practical question you ask when choosing an algorithm. The right answer is whichever algorithm sits at the intersection of acceptable answers across all axes. In banking that’s often one of the five from Chapter 3, sometimes augmented by ensemble methods we’ll meet later.
How do you know when to stop training?
Here’s a question that sounds simple and turns out to be the most important practical decision in any training run. You start gradient descent. The loss goes down. At what point do you stop?
The naive answer is “when the loss stops decreasing.” This is partly right and very dangerous. Two refinements turn it into a real engineering practice.
First refinement: which loss are we watching? The training loss often continues to decrease as long as the model has enough capacity to keep memorising the training set. If you stop when the training loss stops decreasing, you stop when the model has overfit completely, which is the worst possible time. The right loss to watch is the validation loss: the loss computed on a held-out set that the optimiser is not allowed to use during training. Validation loss decreases at first as the model learns generalisable patterns, then plateaus, then often starts increasing as the model begins memorising training-specific quirks that hurt generalisation. The right time to stop is around the minimum of validation loss, before it starts climbing.
Second refinement: when has it really plateaued? Validation loss is noisy. It can wiggle up and down by a few percent from epoch to epoch even on a healthy training run. If you stop the first time it ticks up, you’ll stop too early on most runs. The standard trick is patience: keep training for p more epochs after each new best validation loss, and stop only if no improvement appears in those p epochs. Typical patience values are 5 to 20 epochs, depending on the noise level of the validation curve. This procedure is called early stopping and it’s one of the most reliable forms of regularisation in machine learning.
Read this as the typical training trajectory. Both losses fall together at first, the validation loss bottoms out, the training loss keeps falling because the model is now memorising, and you should have stopped at that bottom. Early stopping with patience automates this decision.
Concrete numbers from a Merehaven Bank-style training run. Suppose you’re training a small classifier and you log losses every epoch. The validation loss curve looks like:
| Epoch | Train loss | Val loss | Best val? |
|---|---|---|---|
| 1 | 0.69 | 0.69 | yes |
| 5 | 0.42 | 0.41 | yes |
| 10 | 0.28 | 0.31 | yes |
| 15 | 0.21 | 0.27 | yes |
| 20 | 0.16 | 0.26 | yes |
| 25 | 0.12 | 0.27 | no |
| 30 | 0.09 | 0.28 | no |
| 35 | 0.07 | 0.30 | no |
| 40 | 0.05 | 0.32 | no |
With patience set to 10 epochs, training would stop at epoch 30 (10 epochs after the last improvement at epoch 20) and the best model is the one saved at epoch 20. The training loss at epoch 40 is much lower (0.05 vs 0.16), but the validation loss is noticeably worse (0.32 vs 0.26). The model at epoch 40 has overfit. The model at epoch 20 is the one you ship.
This procedure has saved more banking ML projects than any other single technique. It costs you nothing and it produces models that generalise better than the same architecture trained for a fixed number of epochs.
Where this goes wrong. The validation set itself can mislead you. If your validation set is too small, the validation loss is too noisy to give you a reliable stopping signal. If your validation set is not representative of production traffic (for example, sampled randomly when production has temporal drift), the validation loss can look great while the production performance is poor. The fix for the first problem is to use a larger validation set or to use cross-validation. The fix for the second is to use time-based validation splits, which we covered in Chapter 1: train on 2012-2018, validate on 2019, test on 2020-2021. Always validate on a slice that mimics how the model will be deployed.
If asked: “What’s the difference between early stopping and other forms of regularisation?”
What does a single gradient descent step actually look like for a neural network?
We’ve seen the linear regression case in detail. Let’s take one more concrete walkthrough, this time for a tiny neural network, because the same loop powers everything from a 13,000-parameter classifier to a 400-billion-parameter language model. Once you’ve seen the steps for a small network, you’ve seen them for any network.
Picture the smallest possible neural network for binary classification: two input features, one hidden layer with two neurons, ReLU activation, one output neuron, sigmoid activation. The network has 2 × 2 + 2 + 2 × 1 + 1 = 9 parameters in total. We’ll train it on one example with input x = (1.0, 0.5) and label y = 1. Set the initial weights randomly, say , b1 = (0.0, 0.0), w2 = (0.5, −0.3), b2 = 0.1. Learning rate α = 0.1.
Forward pass. Compute the hidden layer pre-activations: z1 = W1x + b1 = (0.3 ⋅ 1 + (−0.2) ⋅ 0.5, 0.1 ⋅ 1 + 0.4 ⋅ 0.5) = (0.2, 0.3). Apply ReLU: both are positive, so h = (0.2, 0.3). Compute the output pre-activation: z2 = w2 ⋅ h + b2 = 0.5 ⋅ 0.2 + (−0.3) ⋅ 0.3 + 0.1 = 0.11. Apply sigmoid: p̂ = σ(0.11) ≈ 0.5275. Compute the loss: cross-entropy −log (0.5275) ≈ 0.640.
Backward pass. This is where the chain rule from Chapter 2 earns its keep. We need the gradient of the loss with respect to every parameter in the network. The trick is to start from the output and work backwards, reusing computations as we go. This is exactly what backpropagation does.
The gradient of the cross-entropy loss with respect to the output pre-activation z2 is p̂ − y = 0.5275 − 1 = −0.4725. (This clean form is one reason cross-entropy and sigmoid pair so beautifully.) The gradient with respect to b2 is the same: −0.4725. The gradient with respect to each component of w2 is the gradient with respect to z2 times the corresponding hidden value: (−0.4725 ⋅ 0.2, −0.4725 ⋅ 0.3) = (−0.0945, −0.1418).
Propagate back to the hidden layer. The gradient with respect to each component of h is the gradient with respect to z2 times the corresponding w2: (−0.4725 ⋅ 0.5, −0.4725 ⋅ (−0.3)) = (−0.2363, 0.1418). ReLU’s derivative is 1 for positive inputs and 0 for negative, and both pre-activations were positive, so the gradient with respect to z1 is the same: (−0.2363, 0.1418).
Now for W1 and b1. The gradient with respect to b1 is the same as with respect to z1: (−0.2363, 0.1418). The gradient with respect to each entry of W1 is the gradient with respect to the corresponding entry of z1 times the corresponding input feature. For example, ∂L/∂W1(1, 1) = −0.2363 ⋅ 1 = −0.2363. The full gradient matrix is .
Update step. Subtract α times each gradient from each parameter. New W1: . Similarly update b1, w2, and b2. That’s one step. Repeat with the next training example.
Why bother walking through the arithmetic? Two reasons. First, when something goes wrong in a neural network training run, the diagnostic skill of “I can imagine what’s happening at the parameter level” is the difference between a five-minute fix and a five-day fix. Second, every modern deep learning framework (PyTorch, JAX, TensorFlow) automates this whole calculation through automatic differentiation. You define the forward pass; the framework computes all the gradients. But the framework is doing exactly the chain-rule walk we just did, scaled up to billions of parameters and parallelised across a GPU. The mental model is the same.
Read this as the universal training loop for any neural network. The forward pass produces a prediction. The loss measures wrongness. The backward pass propagates gradients via the chain rule. The optimiser updates the parameters. Every framework, every architecture, every model in this edition and out of it follows this loop. The differences are in what goes inside each box: how many layers, what activations, what loss, what optimiser. The skeleton is fixed.
Where the chain rule earns its keep. The reason backpropagation is feasible at all is that it reuses intermediate computations. Without it, computing the gradient of a loss through a 100-layer network would require 100 separate forward passes, one per layer. With backpropagation, you do one forward pass and one backward pass, and you get all the gradients in essentially the same time it takes to compute the loss. The trick was independently discovered several times in the 1970s and 1980s, most famously a managed cachecovered and popularised by Rumelhart, Hinton, and Williams in their 1986 Nature paper, which is widely credited with kicking off the second wave of neural network research. Without that paper, deep learning as we know it does not happen. The whole edifice of modern AI rests on the chain rule, applied carefully, plus a lot of GPUs.
If asked: “What is backpropagation, and why is it more efficient than computing gradients naively?”
Part III: Treat data and evaluation as the product
Most apparent model failures begin in representation, leakage, sampling or metric choice. This part makes those choices visible before any score reaches a decision route.
Prepare data before judging models
Feature engineering changes the hypothesis space
Andrew Ng, the Stanford professor who has done as much as anyone to bring machine learning to the wider world, has been quoted in countless talks saying some version of: “Coming up with features is difficult, time-consuming, requires expert knowledge. Applied machine learning is basically feature engineering.” Every working data scientist nods at this and every introductory ML course minimises it, because feature engineering is hard to teach in a textbook and easy to gloss over with toy datasets. In real banking work, feature engineering is where most of the value is created and most of the time is spent.
Here’s a concrete example. Imagine the head of commercial banking comes to your team and says: “We want to predict which of our SME customers are likely to need additional working capital over the next quarter, so analysts can have proactive conversations.” Sounds simple enough. The dataset is “everything we know about every SME customer In the synthetic Merehaven case,” which is several hundred fields across multiple systems. The label is some operationalisation of “needed additional working capital,” which doesn’t exist as a column anywhere; you have to construct it.
What does feature engineering for this problem actually look like?
Step one: invent the label. “Needed additional working capital” is a business concept, not a database column. You have to translate it into something measurable. Candidates: drew on overdraft to within 5% of limit at any point in the next 90 days; took up a new term loan in the next 90 days; had a missed direct debit followed by a manual transfer from a related account. Each candidate captures part of the concept and misses part. You discuss with the business, pick one or two, and document why.
Step two: pick the time horizon and snapshot date. You need to define, precisely, what “now” means for each training example. Is the snapshot the first day of each month? The end of each quarter? The day of each customer’s annual review? Each choice affects which features are available and which would leak. You also need to define how far forward to look for the label (90 days? 180?), and you have to make sure your training labels respect that horizon: a customer in the December 2022 snapshot can only be labelled if you have data through March 2023.
Step three: extract features from raw transactional data. This is where the real work happens. For each customer at each snapshot, you need to compute features like:
- Average end-of-day current account balance over the last 30 days, 60 days, 90 days
- Standard deviation of end-of-day balance over each window
- Number of distinct counterparties paid in the last quarter
- Fraction of days in the last 30 with overdraft usage above 50% of limit
- Trend (slope) in monthly aggregate inflows over the last six months
- Days since last late direct debit
- Ratio of payroll outflows to total outflows
- Sector-relative size percentile
Each of these is a piece of code, possibly hundreds of lines, that walks the customer’s transaction history and computes a number. Each one needs to be unit-tested. Each one needs a precise definition that matches between the training-time pipeline and the serving-time pipeline. Each one is a potential source of bugs.
Step four: handle the categorical features. Customer’s primary sector code, region, banking platform (legacy mainframe vs. modern stack), product mix, analyst team. None of these are numbers. Most ML algorithms need numbers. We’ll cover the techniques in a moment.
Step five: handle missing values. Some customers don’t have a sector code because of a data migration in 2017 that lost it. Some don’t have a director’s Experian score because the director is foreign. Some don’t have a confirmed turnover because they joined recently. Each missingness pattern needs a deliberate decision: drop the example, impute with a sensible value, or add an explicit “missing” indicator feature.
Step six: scale and normalise. Once you have all your numerical features, they will be on wildly different scales. Account balance in pounds. DSCR as a ratio. Age in years. Without scaling, gradient descent and distance-based methods will misbehave.
That’s all just the engineering. None of it is choosing an algorithm. None of it is tuning hyperparameters. None of it is “machine learning” in the popular sense. All of it is the prerequisite for any of those steps to work.
Read this as the pipeline that exists in production for any nontrivial banking ML system. Each box is owned by an engineer, instrumented for observability, and version-controlled. The whole thing runs on a schedule (nightly, hourly, or in real time depending on the use case) and is the substrate on which the models actually live.
If asked: “What’s the most important skill for an ML engineer in banking?”
How do you turn red, yellow, and green into numbers?
Most learning algorithms speak only the language of numbers. If you have a categorical feature like sector code or region, you need to translate it before you can feed it to logistic regression, SVM, k-NN, or a neural network. The standard translation is one-hot encoding.
The idea is exquisitely simple. For each possible category, create a new binary feature that is 1 when the original feature equals that category and 0 otherwise. A “colour” feature with three possible values becomes three new features:
red = [1, 0, 0]
yellow = [0, 1, 0]
green = [0, 0, 1]
A customer who is “red” gets the vector [1, 0, 0] in those three new positions. A customer who is “yellow” gets [0, 1, 0]. The original categorical feature is dropped and replaced by the binary expansion.
Why not just map red to 1, yellow to 2, green to 3? Because that imposes an order and a distance that don’t exist. By labelling them 1, 2, and 3, you are telling the model that yellow is “between” red and green, that the gap from red to yellow equals the gap from yellow to green, and that “twice red equals yellow.” None of these is true for unordered categories. If you give the algorithm this fake structure, it will try to find regularity in it, and any regularity it finds is noise that will hurt generalisation.
The cost of one-hot encoding is dimensionality. A categorical feature with 50 possible values becomes 50 new features. A sector code field with 1,200 SIC codes becomes 1,200 new features. A merchant ID field in payments data can have hundreds of thousands of distinct values, each of which would generate its own column. This is called the high-cardinality problem and it hits banking ML hard, because real banking data is full of high-cardinality categorical fields.
The fixes for high cardinality include:
- Grouping rare categories into an “other” bucket, keeping only the most common N values as their own categories
- Target encoding, where you replace each category with the mean target value for that category in the training data (a powerful but leakage-prone technique that requires careful out-of-fold computation)
- Embedding learning, where the categories are mapped to dense low-dimensional vectors by a small neural network trained jointly with the main model, which is what every modern recommender system does for product IDs and user IDs
- Frequency encoding, where each category is replaced by how often it appears in the training set
When ordinal encoding is fine. If your categorical variable has a genuine order, like “poor, decent, good, excellent” for an article quality rating, then mapping to 1, 2, 3, 4 is appropriate. The order is real and the algorithm can use it. The rule is: ordered categories get integer encoding; unordered categories get one-hot.
A Merehaven Bank example. When building a credit scoring model for SME loans, your categorical features might include: SIC code (high cardinality, no order, group rare codes and one-hot the rest), region (low cardinality, no order, one-hot), entity type (low cardinality, no order, one-hot), credit rating grade if from internal scorecards (low cardinality, ordered, integer encode), and originating channel (low cardinality, no order, one-hot). Each gets a different treatment based on its properties.
If asked: “When would you not use one-hot encoding for a categorical feature?”
When is a number not really a number?
Sometimes the opposite of one-hot encoding is what you want. You have a continuous numerical feature, but you suspect that what really matters is which “bucket” the value falls into, not the exact value. Binning (also called bucketing) chops a continuous variable into ranges and creates a categorical feature out of those ranges, which you can then one-hot encode.
Why would you ever do this? Three reasons.
First, if the relationship between the feature and the target is highly non-linear, binning gives a linear model the freedom to learn a different effect for each bin. A logistic regression with raw age can only learn “default risk grows linearly with age” or something equally constrained. With age bins, it can learn “default risk is high for 18-24, low for 25-45, medium for 46-65, high for 65+.” Each bin has its own weight and the model can shape the relationship freely.
Second, if the feature has sharp thresholds in the business logic, binning makes those thresholds explicit. Annual turnover thresholds for SME segmentation (under £2m, £2-10m, £10-50m, over £50m) are defined by the bank, not by some smooth relationship in the data. Binning along those thresholds means the model’s representation matches the business’s.
Third, binning can help with small datasets. If you have only a few hundred examples and a feature with a complex underlying relationship, binning is a form of regularisation: it forces the model to use a coarser representation that is harder to overfit.
The cost of binning. You throw away information. Two customers with ages 30 and 45 may end up in the same bin and be treated identically by the model, even though they’re very different in reality. The art is in choosing bin boundaries that preserve the structure that matters and discard the noise that doesn’t. Common choices include equal-width bins, equal-frequency bins (each bin contains the same number of training examples), and decision-tree-derived bins (let a small decision tree find the best split points).
When binning hurts. For modern flexible models like gradient boosted trees, binning is usually unnecessary because the model can find non-linearities itself. For deep neural networks, binning is rare because you typically want the gradient to flow continuously through the input. Binning shines for linear models, especially when interpretability for regulators is important: a credit scorecard that says “20 points for ages 25-34, 15 points for 35-44, 10 points for 45-54” is exactly what a credit officer wants to see, because it maps directly to the way they think.
If asked: “When would you bin a continuous feature?”
Why does feature scaling matter so much?
We touched on this in Chapter 4 with the failure specimen about the credit card churn model whose loss wouldn’t decrease until the features were normalised. Now let’s do it properly.
The problem: real banking features come on wildly different numerical scales. Customer income in pounds (range 0 to 500,000). Age in years (range 18 to 100). Number of products held (range 0 to 15). Days since last late payment (range 0 to 3,650). Debt service coverage ratio (range 0.5 to 5). If you feed these straight into a model that depends on distances or gradients, the high-magnitude features will dominate everything.
There are two standard fixes: normalisation and standardisation. They are different, and the difference matters.
Normalisation (also called min-max scaling) rescales each feature to a fixed range, usually [0, 1] or [−1, 1]. The formula is:
where min(j) and max(j) are the smallest and largest values of feature j across the training set. After this transformation, every value of feature j is in [0, 1], with the smallest training value mapped to 0 and the largest to 1.
Standardisation (also called z-score normalisation) rescales each feature to have mean 0 and standard deviation 1. The formula is:
where μ(j) is the mean and σ(j) is the standard deviation of feature j across the training set. After standardisation, the feature has the properties of a standard normal distribution: centred at zero, spread of one. Values can be negative or positive and are not bounded.
Unsupervised learning (clustering, PCA, etc.) often benefits more from standardisation than from normalisation.
Standardisation is preferred for features that are roughly normally distributed (the classic bell curve). Normalisation forces them into a bounded range that distorts the natural shape.
Standardisation is preferred for features with extreme values or outliers. Normalisation crushes the typical values into a tiny range while the outliers occupy the rest of the [0, 1] interval. A feature where 99% of values are between 0 and 100 but one value is 100,000 will, after normalisation, have almost all values clustered near zero, which destroys the model’s ability to use them.
In all other cases, normalisation is fine, and is often slightly faster because it doesn’t require recomputing the mean and standard deviation from scratch when you add new data.
The critical operational rule. You compute the scaling parameters (min , max , μ, σ) on the training set only. Then you use those same parameters to transform the validation set, the test set, and any production data. Never recompute on the validation or test set, and never recompute on each batch of incoming production data. If you do, you leak information from the holdout into the training process and your evaluation becomes optimistic. Modern feature stores enforce this discipline by versioning the scaling parameters as part of the feature definition. On the selected cloud platform, a managed training service Feature Store handles this via its offline/online store architecture; on the selected cloud platform, a managed training service Feature Store does the same with an analytical warehouse as the offline backing store.
Why does scaling help? Two reasons. First, gradient descent. If one feature is on a scale 1000x larger than another, the gradient with respect to the larger feature is much larger, and a single global learning rate cannot serve both: it has to be small enough not to blow up the large feature, which means the small feature barely moves. Scaling equalises the gradient magnitudes and lets training converge much faster. Second, distance-based methods like k-NN and clustering measure feature differences using Euclidean distance, which is dominated by features with large numeric ranges. Without scaling, the largest-range feature is effectively the only one that matters for finding neighbours.
Where scaling doesn’t matter. Tree-based methods (decision trees, random forests, gradient boosted trees) don’t need feature scaling. They split on individual features one at a time, and the choice of split point is invariant to monotonic transformations like scaling. So if you’re using an XGBoost model for credit risk, you can skip scaling entirely and the model will be unaffected. This is one of the small but real reasons tree ensembles are easier to deploy than neural networks: less preprocessing fragility.
If asked: “What’s the difference between normalisation and standardisation, and when do you use each?”
What do you do when the data has holes?
Real banking data has missing values everywhere. A customer’s industry code is blank because the migration in 2017 lost it. A director’s Experian score is missing because the director is non-UK resident. A management accounts file shows no EBITDA because the company hasn’t filed yet. Missing data is the rule, not the exception.
You have three families of strategies for handling missing values, and the right choice depends on how the values are missing and how much data you have.
Strategy one: drop the example. If you have lots of data and only a small fraction has missing values, you can simply drop the rows with any missingness. This is clean but throws away information. You should never drop more than a small percentage of your data this way, and you should never drop without first checking whether the missingness is random or systematic. If non-UK residents are systematically dropped, you’ve just biased your model against non-UK residents, which is both an analytical error and a fair-lending risk.
Strategy two: use an algorithm that handles missing values natively. Decision trees, gradient boosted trees, and some implementations of k-NN can deal with missing values directly. C4.5 and its descendants have explicit machinery for routing examples with missing values down both branches of a split with appropriate weighting. XGBoost learns which direction to send missing values at each split. If you can use one of these algorithms, you avoid the imputation problem entirely.
Strategy three: impute the missing values. Fill in the holes with some sensible substitute, then train as if the data were complete. There are several imputation techniques.
Mean imputation. Replace missing values with the average value of that feature in the training set. Simple, fast, often surprisingly effective. Loses information about variability and can systematically bias the model toward the centre.
Out-of-range imputation. Replace missing values with a value outside the normal range, like −1 or 999. The model can then learn to treat that special value differently. Works well with tree-based models, dangerous with linear models because the special value will bias the linear coefficient.
Mid-range imputation. Replace missing values with a value in the middle of the range, like 0 for a feature on [−1, 1]. The idea is that the imputed value won’t strongly affect predictions in either direction.
Regression imputation. Treat the missing feature as the target of a small regression problem and predict its value from the other features. This is more sophisticated and can capture relationships in the data, but it’s slow and has its own failure modes.
Indicator augmentation. Add a binary feature alongside the original that is 1 when the value was missing and 0 when it was present. This way the model can learn that “missing” is itself informative, which it often is in banking. A blank Experian score is not random; it tells you something about the director.
Read this as a decision tree for handling missingness. The right branch depends on the model family and the nature of the missingness.
A Merehaven Bank-style example. Suppose you’re building a default model for SME loans and 12% of your training examples are missing the director’s Experian score. First question: is the missingness random? You discover that almost all the missing scores belong to non-UK directors, who legitimately don’t have UK credit files. The missingness is highly informative: it tells you the director is non-UK, which is itself a meaningful credit signal. Strategy: add a binary “missing_experian” indicator and impute the original feature with the mean (or with a sentinel value for tree models). The model learns to use the indicator and to treat the imputed value as a placeholder. Performance is better than dropping the rows and better than imputing without the indicator.
The critical operational rule. Whatever imputation strategy you use at training time, you must use the same strategy at prediction time, with the same fitted parameters (e.g., the same mean computed on the training set). If you fit a new mean on incoming production data, you’ve leaked information and you’ve broken the assumption that training and serving see the same feature distributions. Modern feature stores version the imputation parameters along with the feature definition.
If asked: “How do you handle a feature that is missing for 30% of your training examples?”
How do you split data so you don’t lie to yourself?
Once your features are engineered, the next decision is how to split the data into training, validation, and test sets. This sounds like a procedural box-tick. It is not. Get it wrong and every other thing you do downstream is poisoned.
The principle is simple. You want to estimate how well your model will perform on data it has never seen before. The only honest way to estimate that is to actually measure it on data you genuinely held out from the training process. If you peek at the test data even once during training or hyperparameter tuning, you’ve contaminated the estimate. The test set is sacred. You touch it at the very end, once.
The standard split is into three subsets:
Training set. The largest. Used by the optimiser to fit the model parameters. Typically 70% to 95% of the data depending on dataset size.
Validation set (sometimes called the dev set). Used for choosing hyperparameters, comparing model variants, and deciding when to stop training. Typically 5% to 15%.
Test set. Used once, at the end, to produce an unbiased estimate of how the chosen model will perform in production. Typically 5% to 15%.
Why two holdout sets and not one? Because when you tune hyperparameters by repeatedly evaluating on a single holdout set, you are implicitly fitting your hyperparameters to that holdout set. The performance you see on that set becomes optimistic, even though you never used it for training. The fix is to have a separate test set that is never used for any decision, only for the final evaluation. The validation set is for iterating; the test set is for reporting.
The classic mistake is to use the validation set for both hyperparameter tuning and final evaluation. After tuning fifty hyperparameter combinations against the same validation set, the best one will look about 1-2% better than its true performance, just from the noise of the multiple comparisons. If you ship the model on that basis, you’ll be disappointed when you see the real production numbers. The test set protects you from this self-deception.
Read this as the discipline that separates honest ML from wishful ML. Training data fits the parameters. Validation data picks among models. Test data, used exactly once, tells you what you actually have.
For temporal data, split by time. This is critical in banking. If your data is loans originated between 2012 and 2022, do not split randomly. Random splits put 2022 loans in the training set and 2015 loans in the test set, which gives you an optimistic estimate because the model has effectively been allowed to see the future. The right split is chronological: train on 2012-2019, validate on 2020, test on 2021-2022. This mimics how the model will actually be used in production: trained on the past, deployed on the future. The performance estimate from a temporal split is much more honest than a random split, and it usually looks worse, which is the truth and you should believe it.
How big should each set be? It depends on dataset size. The old rule of thumb was 70/15/15. For modern large datasets with millions of examples, 95/2.5/2.5 is fine, because 2.5% of a million is still 25,000 examples, which is more than enough to estimate performance precisely. For small datasets with a few thousand examples, you may need to use cross-validation (which we cover later) instead of a fixed validation split, to avoid wasting examples.
Stratified splitting is a refinement worth knowing. If your classes are imbalanced (say 4% positive in a fraud detection problem), a random split might leave the validation or test set with no positive examples at all, or with very few, which makes the metrics noisy. Stratified splitting ensures that each set has roughly the same class proportions as the original data. Most ML libraries support stratified splits with one parameter.
If asked: “Why do the worked design uses three splits instead of just two?”
What does it mean for a model to underfit or overfit?
Two failure modes, opposite shapes. Knowing the difference is half the diagnostic skill of an ML engineer.
Underfitting is when the model can’t even predict the training data well. Its training error is high, and the validation error is also high. The model is too simple, or the features are too uninformative, to capture the patterns in the data. The technical term is high bias: the model is biased toward simple functions and can’t reach the true relationship.
The fixes for underfitting are: try a more flexible model (polynomial regression instead of linear, neural network instead of logistic regression, deeper tree instead of shallower), add more informative features, or remove regularisation if you’ve been using it.
Overfitting is the opposite. The model predicts the training data extremely well, even perfectly, but predicts validation data poorly. The technical term is high variance: the model’s predictions vary too much from training run to training run, depending on which specific examples ended up in the training set. The model has memorised noise in the training data, including its idiosyncrasies, mislabellings, and sampling artefacts.
The classic example: fitting a polynomial of degree 15 to 20 data points. The polynomial can wiggle through every training point exactly, achieving zero training error, but the wild oscillations between training points produce wild predictions that are nowhere near the truth. New data, which lands at slightly different positions than the training data, gets misclassified.
The fixes for overfitting are: try a simpler model, gather more training data, add regularisation, use early stopping, apply dropout (for neural networks), or do feature selection to reduce the number of inputs the model has to play with.
Read this as the universal diagnosis flowchart. Look at training error and validation error side by side. Their relative magnitudes tell you which fix to reach for.
A worked example with numbers. Imagine you’ve trained three models on the same SME default dataset:
| Model | Train AUC | Val AUC | Diagnosis |
|---|---|---|---|
| Logistic regression with 10 features | 0.71 | 0.69 | Slight underfit |
| Logistic regression with 200 hand-engineered features | 0.85 | 0.78 | Slight overfit |
| Logistic regression with 200 features + L2 regularisation | 0.83 | 0.82 | Good fit |
The first model has both train and val low. It can’t even fit the training data well, suggesting the features are too few or the model too simple. The second has train high and val noticeably lower, the classic overfit signature: it has memorised some training-specific noise. The third has both train and val high and roughly equal: regularisation has prevented the overfit while still allowing the model to learn the real signal. This is the model you ship.
The bias-variance trade-off. This is the formal name for the underlying tension. As model complexity increases, bias decreases (the model can fit more shapes) but variance increases (the model is more sensitive to which specific data points it saw). The total error is the sum of bias squared plus variance plus an irreducible noise term. The sweet spot is somewhere in the middle, where the sum is minimised. Every single regularisation technique, every form of cross-validation, every hyperparameter knob is, at heart, a way of finding that sweet spot for the problem at hand.
If asked: “How do you tell whether your model is underfitting or overfitting?”
How do you penalise a model for being too clever?
Regularisation is the toolkit for preventing overfitting by adding a penalty to the training objective that discourages complex models. The idea is elegant: you tell the optimiser “minimise the training error, but also keep the model simple, and here’s how I’ll measure simple.” The optimiser then balances accuracy on the training data against simplicity, and the result is a model that’s a little less accurate on training but much more accurate on new data. Bias goes up slightly. Variance goes down a lot.
The two most common forms of regularisation are L1 and L2.
L2 regularisation adds a penalty proportional to the sum of squared parameters. For linear regression, the regularised objective is:
where is the sum of squared weights and C is a hyperparameter controlling how much we care about the penalty relative to the data fit. The penalty term grows whenever any weight grows, so the optimiser is pushed toward solutions with small weights everywhere. Small weights mean the model’s predictions depend gently on each feature, which produces smoother decision boundaries that generalise better. L2 is also called ridge regularisation, and the regularised version of linear regression is called ridge regression.
L1 regularisation adds a penalty proportional to the sum of absolute values of the parameters:
where is the L1 norm, the sum of absolute values. L1 is also called lasso regularisation, and regularised linear regression with L1 is called lasso regression.
The important difference between L1 and L2. L2 shrinks all weights toward zero but rarely makes any of them exactly zero. L1, by contrast, tends to drive many weights all the way to zero. This means L1 produces sparse models: models where most features have no effect at all because their weights are exactly zero. L1 is therefore a form of automatic feature selection: the optimiser decides which features matter and silently drops the others. This is enormously useful when you have hundreds or thousands of candidate features and you want a small interpretable model.
Geometrically, the difference is that the L1 penalty has corners along the coordinate axes where weights can land exactly. The L2 penalty is a smooth sphere with no corners, so the optimal solution rarely lies on a coordinate axis. This is the technical reason for the sparsity difference, and it’s worth visualising once if you’ve never seen it.
Setting C. The hyperparameter C controls the strength of regularisation. With C = 0, you have no regularisation and the model is unconstrained (and likely overfits). As C grows, the penalty becomes stronger and the weights are pushed harder toward zero. With C very large, the model becomes too simple and underfits. The right value is somewhere in the middle, found by validation-set tuning. A typical search is over a logarithmic scale: C ∈ {0.001, 0.01, 0.1, 1, 10, 100, 1000}.
Beyond L1 and L2. Several other regularisation techniques exist and matter for specific model classes.
Dropout is a regularisation technique specific to neural networks. During training, randomly set a fraction (typically 20-50%) of neuron outputs to zero on each forward pass. The network is forced to learn redundant representations because it can’t rely on any specific neuron being available. At inference time, all neurons are used, but their outputs are scaled to compensate. Dropout was invented by Hinton’s group at Toronto in 2012 and has been a staple of deep learning ever since.
Batch normalisation normalises the activations within each layer of a neural network during training. It was introduced by Ioffe and Szegedy in 2015 and has both training and regularisation benefits, though its precise mechanism is still debated.
Data augmentation is the practice of creating new training examples by perturbing existing ones. In computer vision, you flip, rotate, crop, and colour-jitter images. In tabular data, you can add small Gaussian noise to numerical features or swap values between similar examples. Data augmentation acts as regularisation because it makes the model well-tested to small variations in inputs.
Early stopping, which we covered in Chapter 4, is also a form of regularisation. By stopping training before the validation error climbs back up, you prevent the model from memorising training-specific noise.
Read this as the menu of regularisation options. Pick one or several based on the model and the problem. For linear models, L1 or L2 is the default. For neural networks, dropout plus L2 plus early stopping is the standard recipe. For tree ensembles, regularisation comes via tree depth limits, minimum samples per leaf, and learning rate shrinkage.
If asked: “When would you use L1 regularisation instead of L2?”
How do you measure whether a classifier is actually any good?
Once you have a trained classifier, you need to evaluate it. For regression, this is mostly straightforward: compute MSE or some other loss on the test set and you’re done. For classification, things get complicated, and many production failures trace back to choosing the wrong metric.
The starting point is the confusion matrix. For a binary classifier, it’s a 2×2 table that breaks down predictions versus actuals into four cells:
| Predicted positive | Predicted negative | |
|---|---|---|
| Actually positive | True positive (TP) | False negative (FN) |
| Actually negative | False positive (FP) | True negative (TN) |
A true positive is a fraud transaction correctly flagged as fraud. A false negative is a fraud transaction missed by the model. A false positive is a legitimate transaction wrongly flagged as fraud. A true negative is a legitimate transaction correctly passed through.
From the confusion matrix you can compute several metrics, each emphasising a different aspect of performance.
Accuracy is the simplest: the fraction of all predictions that are correct.
Accuracy is intuitive and works well when the classes are balanced. It is catastrophically misleading when classes are imbalanced. If your fraud rate is 0.5%, a model that predicts “not fraud” for every transaction achieves 99.5% accuracy and is utterly useless. Don’t use accuracy as the primary metric for any imbalanced problem in banking, and almost every problem in banking is imbalanced.
Precision is the fraction of predicted positives that are actually positive:
In words: of all the things I flagged as fraud, how many really were? High precision means few false alarms. The fraud operations team cares about precision because every false positive costs analyst time.
Recall (also called true positive rate or sensitivity) is the fraction of actual positives that the model caught:
In words: of all the real fraud, how much did I catch? High recall means few missed cases. Risk managers and regulators care about recall because every missed fraud is a customer loss and a compliance risk.
Precision and recall trade off. often, you can increase one only by decreasing the other. A very strict classifier (only flags transactions it’s very confident about) will have high precision but low recall: it catches only the obvious cases. A very lenient classifier (flags anything that looks slightly off) will have high recall but low precision: it catches most fraud but generates many false alarms. The right balance depends on the relative cost of the two error types.
| Predicted spam | Predicted not spam | |
|---|---|---|
| Actually spam | TP = 23 | FN = 1 |
| Actually not spam | FP = 12 | TN = 556 |
Precision = 23/(23 + 12) = 23/35 ≈ 0.657. Recall = 23/(23 + 1) = 23/24 ≈ 0.958. Accuracy = (23 + 556)/592 ≈ 0.978.
The accuracy looks great. The recall is excellent: we catch 96% of spam. The precision is mediocre: only two-thirds of what we flag as spam actually is spam, meaning a third of our spam folder is misclassified legitimate mail. For email this is a big deal because legitimate mail being marked as spam is a serious user complaint. For fraud detection, we’d accept this trade-off because missing a fraud is much worse than annoying an analyst.
Cost-sensitive accuracy. When false positives and false negatives have different worked costs, accuracy can be replaced by a weighted version. Assign cost cFP to each false positive and cFN to each false negative, then compute:
Or, more usefully in practice, just compute the total cost and minimise it directly: total cost = cFP ⋅ FP + cFN ⋅ FN. In credit risk, the cost of approving a defaulter is the expected loss given default, which can be tens of thousands of pounds. The cost of declining a good customer is lost margin, perhaps a few hundred pounds. The asymmetry is real and the metric should reflect it.
The ROC curve and AUC. This is the most important visualisation in classifier evaluation. A classifier that outputs a continuous score (like logistic regression’s probability or an SVM’s signed distance) has a tunable decision threshold. At each threshold, you get a different confusion matrix, and therefore different precision, recall, true positive rate, and false positive rate.
Define true positive rate (same as recall) and false positive rate as:
As you sweep the threshold from very high (almost no positive predictions) to very low (almost all predictions positive), TPR and FPR both increase from 0 to 1. The ROC curve plots TPR against FPR for all possible thresholds. The area under the ROC curve (AUC) is a single number summarising classifier quality.
Read this as the relationship between threshold, prediction volume, and the two rates. The ROC curve traces the trade-off as you sweep the threshold.
Interpretation of AUC. A perfect classifier has AUC = 1, hugging the top-left corner. A random classifier (coin flip) has AUC = 0.5, a diagonal line. A good banking model typically has AUC between 0.7 and 0.9, depending on the problem. AUC under 0.5 means your model is worse than random, usually a sign that you’ve inverted the labels somewhere.
Why AUC is popular. It’s threshold-independent: it summarises performance across the whole range of possible operating points, not just one. It’s easy to interpret: a higher number means a better model. It works for any classifier that outputs a score. And it allows direct comparison between different model families on the same problem.
Why AUC can mislead. It treats all parts of the ROC curve equally, which is rarely what you want. In fraud detection, you operate at very low false positive rates (because you can’t have analysts chasing thousands of false alarms a day), and you only care about the left edge of the ROC curve. A model with great average AUC but poor performance at low FPR is not what you want. The fix is to use partial AUC (area under the curve restricted to a specific FPR range) or to evaluate at the specific operating point you’ll use in production. Precision-recall AUC is another alternative that focuses on the positive class and is often more informative for highly imbalanced problems.
If asked: “Why is accuracy a bad metric for fraud detection?”
How do you systematically tune hyperparameters?
You’ve engineered features, split data, picked an algorithm, and you’ve identified a few hyperparameters that need to be set. SVM has C and the kernel choice. Decision tree has max depth. Logistic regression with regularisation has the regularisation strength. Gradient descent has the learning rate. None of these are learned from data; you have to set them. The systematic process for setting them is hyperparameter tuning.
Three standard approaches.
Grid search is the simplest. Define a set of candidate values for each hyperparameter, then train a model for every combination. Evaluate each on the validation set. Keep the best.
Why a logarithmic scale for C? Because the right value can vary by orders of magnitude across problems, and a linear grid would waste samples in regions where the model behaves the same. Powers of 10 (or 2) cover a wide range with few samples.
Grid search is conceptually simple and fully reproducible. It scales badly: with k hyperparameters and n values each, you have nk combinations. For 5 hyperparameters with 7 values each, that’s 16,807 training runs. For a model that takes an hour to train, that’s about two years of compute on a single machine. Grid search is feasible only when you have few hyperparameters or few candidate values.
Random search is the smarter alternative. Instead of trying every combination on a grid, sample combinations uniformly at random from the space, and try as many as your compute budget allows. This sounds primitive but turns out to work surprisingly well, often better than grid search for the same number of trials, because most hyperparameters don’t matter equally. If only two of your five hyperparameters strongly affect the outcome, grid search wastes 95% of its trials varying the irrelevant three. Random search, by contrast, gives every hyperparameter equal coverage with each random draw, and concentrates its evaluation on the dimensions that matter.
The classic 2012 paper by Bergstra and Bengio at Université de Montréal titled “Random Search for Hyper-Parameter optimisation” demonstrated this convincingly and changed how the field thinks about hyperparameter tuning. For most practical problems, random search is the right default.
Bayesian hyperparameter optimisation is more sophisticated still. Instead of choosing the next combination randomly or on a grid, fit a probabilistic model to the results so far (typically a Gaussian process) and use it to predict which untried combinations are most likely to perform well. The next combination is chosen to maximise expected improvement. Bayesian optimisation is materially more sample-efficient than random search for problems where each trial is expensive (e.g., training a neural network for hours). Tools like Optuna, Hyperopt, and scikit-optimize implement it well.
Read this as a progression from simple to sophisticated. Use grid search for small problems where you want full coverage. Use random search for medium problems with several hyperparameters. Use Bayesian optimisation when each trial is expensive and you can’t afford to waste any.
Cross-validation is the technique you reach for when you don’t have enough data for a separate validation set. Instead of a single 70/15/15 split, you split your training data into k folds (typically 5 or 10), train k models, and validate each on a different fold. The final estimate is the average of the k validation scores.
Concretely with 5-fold cross-validation: you split your training data into 5 equal subsets {F1, F2, F3, F4, F5}. Train model 1 on {F2, F3, F4, F5} and validate on F1. Train model 2 on {F1, F3, F4, F5} and validate on F2. And so on. You end up with five performance estimates and you average them. This gives you a much more stable estimate than a single 70/15 split, at the cost of doing five times the training work.
Read this as the cross-validation pattern. Each fold takes a turn as the validation set while the others train the model. The average across folds is your performance estimate. It’s the gold standard for small datasets and for hyperparameter tuning where stability matters.
Cross-validation combined with grid or random search is the most common production pattern: for each hyperparameter combination, run k-fold CV, get an averaged validation score, and pick the combination with the best average. Once you’ve picked, retrain on the full training set with the chosen hyperparameters and evaluate once on the held-out test set. This is the recipe used for nearly every well-tuned classical ML model in production In the synthetic Merehaven case and similar institutions.
If asked: “When would you use Bayesian hyperparameter optimisation instead of random search?”
Part IV: Match model structure to the evidence
Depth, ensembles, clustering and ranking solve different structural problems. Each section asks what additional capacity buys, what it hides and how it can be tested.
Use depth only when structure requires it
What is a neural network, really?
Strip away everything you think you know about neural networks. Forget the brain analogies. Forget the hype. Forget the pictures with circles and arrows. Here is what a neural network is, in one equation:
y = fNN(x) = f3(f2(f1(x)))
That’s a three-layer neural network. You take the input x, apply function f1 to get an intermediate result, apply f2 to that, apply f3 to that, and the output is your prediction y. The whole thing is a composition of functions. That is the entire concept.
fl(z) = gl(Wlz + bl)
Read this slowly. The function fl takes an input vector z (which is the output of the previous layer, or the original input if l = 1), multiplies it by a matrix Wl, adds a bias vector bl, and then applies a nonlinear function gl (the activation function) element by element to the result. That’s one layer. The parameters Wl and bl are learned from data. The activation function gl is chosen by the engineer before training.
Stop and compare this to logistic regression. In logistic regression, the model is:
p̂ = σ(w⊤x + b)
where σ is the sigmoid function. A linear combination, plus a bias, fed through a squashing function. That is exactly one layer of a neural network with the logistic function as the activation. If you put a single layer with a sigmoid activation into a neural network, you have reinvented logistic regression. If you stack two layers, you have a shallow neural network. If you stack ten, you have a deep network. The only difference between “logistic regression” and “a neural network” is how many times you apply the operation before emitting the output.
Why a matrix instead of a vector? Because each layer has multiple units, not one. In logistic regression, you compute a single number w⊤x + b and squash it. In a neural network layer with 10 units, you want to compute 10 such numbers in parallel, each with its own weight vector and bias. Stack those 10 weight vectors as rows of a matrix Wl and stack the 10 biases as a vector bl, and the single matrix-vector multiplication Wlz gives you all 10 linear combinations at once. The activation function is then applied element-wise to each of the 10 entries, producing a 10-dimensional output vector. That output becomes the input to the next layer, which may have a different number of units.
A concrete walkthrough with small numbers. Imagine a 2-layer network that takes a 3-dimensional input and produces a 1-dimensional output. Layer 1 has 4 units; layer 2 has 1 unit. So:
- W1 is a 4×3 matrix (4 units, each with a weight vector of length 3)
- b1 is a 4-dimensional vector (one bias per unit)
- W2 is a 1×4 matrix (1 unit, weight vector of length 4)
- b2 is a 1-dimensional vector (one bias)
Total parameters: 4 ⋅ 3 + 4 + 1 ⋅ 4 + 1 = 21. Small, but illustrative.
Suppose x = (1.0, 0.5, −0.3) and the parameters are whatever they are. Compute:
z1 = W1x + b1 → a 4-dimensional vector of linear scores h1 = g1(z1) → a 4-dimensional vector after activation z2 = W2h1 + b2 → a scalar y = g2(z2) → the output
Each layer takes in the previous layer’s output, combines it linearly with its own weights and bias, and applies its activation. The whole network is just that sequence of transformations. Nothing mystical.
Read this as the forward pass of a two-layer network. Input goes in, matrix multiplications and activations happen in sequence, output comes out. This is the fundamental shape. Every neural network in the world, including the one powering ChatGPT, is a version of this pattern with many more layers and many more units per layer.
Why do you need the nonlinearity? This is the single most important question in all of deep learning, and the answer is beautiful. Without the activation function, each layer would be a linear transformation. And the composition of two linear transformations is still a linear transformation. So a three-layer network with no nonlinearity, mathematically, collapses to a single linear transformation with a different weight matrix: W3(W2(W1x + b1) + b2) + b3 is just W′x + b′ for some W′ and b′. All that stacking buys you nothing. You might as well use a single linear layer, which is just linear regression.
The nonlinear activation is what breaks the collapse. When you apply a nonlinear function between layers, the composition of layers is no longer a linear function, and the network gains the ability to represent arbitrarily complex relationships. Without the nonlinearity, deep learning would be pointless. With it, deep learning can in principle approximate any continuous function (this is the universal approximation theorem, which Cybenko proved in 1989 for a single hidden layer with sigmoid activation). In practice, “can approximate any function” doesn’t mean “can learn any function from finite data,” so depth matters for different reasons we’ll get to shortly.
If asked: “Why can’t a neural network work with purely linear activation functions?”
What does a multilayer perceptron actually look like?
The multilayer perceptron (MLP) is the simplest and most general neural network architecture, and it is where you start when thinking about neural networks. An MLP consists of an input layer, one or more hidden layers, and an output layer. Every unit in each layer is connected to every unit in the previous layer. This all-to-all connection pattern is called fully connected or dense.
- Input x = (x(1), x(2))
- Layer 1: each of 4 units computes y1(u) = g1(w1, u⊤x + b1, u) for u = 1, 2, 3, 4
- Layer 2: each of 4 units computes y2(u) = g2(w2, u⊤y1 + b2, u) for u = 1, 2, 3, 4
- Layer 3: the single output unit computes y = g3(w3, 1⊤y2 + b3, 1)
Each unit is identical in form to logistic regression: a dot product plus a bias, through an activation. The difference is that there are many of them, arranged in layers, and their outputs feed into one another. The parameters wl, u and bl, u are all learned jointly by gradient descent (or Adam, or whichever optimiser you pick) backpropagating through the whole thing.
Read this as a dense web of connections. The input feeds every hidden unit in layer 1. Each layer 1 unit feeds every layer 2 unit. Each layer 2 unit feeds the output. Every arrow carries a learned weight; every unit has a learned bias. The fully-connected pattern means the model has no inductive bias about which input features should combine with which, it learns whatever combinations make the loss go down.
Choosing the output activation based on the task. The activation function of the final layer determines what kind of model you have. If the final activation is linear (just the raw score, no squashing), the network is a regression model and its output can be any real number. If the final activation is sigmoid, the network is a binary classifier producing a probability. If the final activation is softmax (a generalisation of sigmoid to multiple classes), the network is a multiclass classifier producing a probability distribution over classes. Same network skeleton, different output activation, different kind of task. This is one of the lovely unifications that neural networks give you: the overall architecture is the same; only the last layer changes to match the problem.
A Merehaven Bank example: credit memo classification. Imagine you’re building a small network inside the Merehaven Credit Workbench that takes a 512-dimensional embedding of a draft credit memo (produced by an upstream language model) and classifies it into one of six memo types: new facility, facility increase, covenant waiver, annual review, watchlist, and decline. The architecture might be:
- Input: 512-dimensional embedding vector
- Hidden layer 1: 128 units, ReLU activation
- Hidden layer 2: 64 units, ReLU activation
- Output layer: 6 units, softmax activation
Total parameters: 512 ⋅ 128 + 128 + 128 ⋅ 64 + 64 + 64 ⋅ 6 + 6 ≈ 74, 000. Small by deep learning standards. Trained with cross-entropy loss and Adam optimiser. You’d hit perhaps 90%+ accuracy with decent training data. The result slots naturally into the product: each memo is auto-tagged with its predicted type, routing it to the right approval queue.
This is the kind of neural network that does real work in a bank every day. Not billions of parameters. Not months of training. Tens of thousands of parameters, trained in minutes, serving a specific focused purpose. Most production neural networks look like this, not like GPT-4.
Sigmoid / logistic function. σ(z) = 1/(1 + e−z). Output range (0, 1). Historically the default. Now rarely used in hidden layers because of vanishing gradients (we’ll see why in a moment), but still used in output layers for binary classification.
Tanh (hyperbolic tangent). tanh (z) = (ez − e−z)/(ez + e−z). Output range (−1, 1). Centred at zero, which is nicer for optimisation than sigmoid. Used occasionally, particularly in RNNs.
ReLU (rectified linear unit). ReLU(z) = max (0, z). Output range [0, ∞). Negative inputs map to zero; positive inputs pass through unchanged. This is the activation function that unlocked modern deep learning. Popular because it’s trivial to compute, doesn’t saturate for positive inputs (so gradients flow nicely), and empirically trains faster and reaches better solutions than the smoother alternatives.
Variants of ReLU exist, including Leaky ReLU (allows a small negative gradient for negative inputs to avoid “dead” neurons), GELU (a smooth approximation to ReLU used in modern transformers), and Swish (another smooth variant). For most practical purposes, ReLU is the default. If you are unsure what activation to use in a hidden layer, use ReLU.
If asked: “What’s the difference between a multilayer perceptron and logistic regression?”
Why was deep learning hard for twenty years?
Here’s a historical puzzle worth sitting with. The basic neural network architecture has been around since at least the 1960s. Backpropagation, the algorithm for training it, was popularised in 1986 by Rumelhart, Hinton, and Williams. By the late 1980s, people knew what a neural network was and how to compute its gradient. And then, for about twenty years, nothing much happened. Neural networks were a backwater of machine learning. Support vector machines dominated classification. Random forests and gradient boosting dominated tabular data. Neural networks were viewed as finicky, hard to train, and not worth the effort. The people who kept working on them were regarded, mostly, as stubborn eccentrics.
What happened? The short answer is: the vanishing gradient problem prevented deep networks from being trained, and nobody knew how to get around it reliably until around 2010. The long answer is the rest of this section.
The vanishing gradient problem. Backpropagation computes the gradient of the loss with respect to each parameter by applying the chain rule backwards through the network. At each layer, the gradient flowing through the layer gets multiplied by the derivative of the layer’s activation function. For sigmoid and tanh, this derivative is always less than 1 (it maxes out at 0.25 for sigmoid and 1 for tanh at the origin, and goes to zero at the extremes). Multiply n numbers less than 1 together and you get a number that decreases exponentially with n. For a deep network, the gradient at the early (input-side) layers is vanishingly small by the time it finishes propagating backwards. The early layers barely update. Training stalls.
The exploding gradient problem. The other side of the same coin. If the activation derivatives are sometimes greater than 1, or if the weight magnitudes are large, the product of gradients can grow exponentially instead of shrinking. The early layers receive enormous gradient updates, blow up, and training diverges. Exploding gradients were easier to fix than vanishing ones, usually with gradient clipping (just cap the gradient norm at some threshold before applying the update) and regularisation.
Read this as the vanishing gradient pipeline. Each layer shrinks the gradient a little more. By the time backprop reaches the first layer, there’s essentially nothing left to work with, and those layers never learn.
The breakthroughs that fixed it. Between 2006 and 2012, a series of clever ideas accumulated to make deep networks trainable. None of them was a single magic bullet; they combined into an ecosystem of techniques.
ReLU activation. The derivative of ReLU is 1 for positive inputs and 0 for negative inputs. For positive inputs, the gradient flows through unchanged, so the product of derivatives doesn’t shrink as you go backwards. You get some dead neurons (those with permanently negative input), but on average the gradients reach the early layers intact. ReLU was used in neural networks by Glorot, Bordes, and Bengio in 2011, and by Krizhevsky, Sutskever, and Hinton in their landmark AlexNet paper in 2012, which won the ImageNet competition and kicked off the modern deep learning era.
Better initialisation. Random initialisation sounds trivial, but getting it right matters enormously. Xavier Glorot’s initialisation (2010) and Kaiming He’s initialisation (2015) carefully choose the scale of initial weights based on the number of inputs and outputs per layer, so that gradients neither explode nor vanish at the start of training. Modern frameworks use these as the default.
Skip connections and residual networks. Kaiming He and peers at Microsoft Research introduced ResNet in 2015, which allows gradients to flow around layers via direct shortcut connections. A residual block computes y = x + F(x) where F is the usual layer transformation. The “+x” shortcut means that even if F(x) has a vanishing gradient, the gradient can still flow through the identity path. This single trick made networks with 100+ layers trainable for the first time, and residual connections are now everywhere in deep learning.
Better optimisers. Adam and its descendants (which we met in Chapter 4) handle the per-parameter learning rate adaptation that makes gradient descent reliable on the complex loss surfaces of deep networks.
Batch normalisation. Ioffe and Szegedy (2015) normalise the activations within each layer during training, which keeps the gradient distributions stable across layers and depths.
GPUs. The hardware that made training a 100-million-parameter network in hours (rather than weeks) feasible. Without the compute, none of the algorithmic improvements would have mattered because nobody could run experiments fast enough to iterate on them.
Put it all together and you get the state of deep learning in 2016: trainable networks with hundreds of layers, billion-parameter models, and the beginnings of the foundation model era. The twenty-year dark age ended because many small problems got fixed at once, and the sum of fixes was greater than any individual one.
If asked: “Why did deep learning take off around 2012 and not earlier?”
How does a neural network see an image?
Here is a real problem. You want to classify 28×28 pixel images of handwritten digits (the classic MNIST dataset) with a neural network. Each image has 784 pixels. If you flatten it into a vector and feed it to a plain MLP, the first layer with 100 hidden units would have 784 ⋅ 100 = 78, 400 weights. That’s manageable. But now imagine the images are 1000×1000 pixels (a modest photo). Each image has 1,000,000 pixels. The first layer with 100 hidden units would have 100 million weights. That’s infeasible.
And even if you could afford it, an MLP on images treats every pixel as an independent input with no spatial relationship to its neighbours, which throws away the single most important fact about images: nearby pixels are correlated, edges and textures are local patterns, and the meaning of a patch depends on the arrangement of its pixels, not just their individual values.
The insight that led to convolutional neural networks came from thinking about how an image works. Pixels near each other tend to carry related information: all the pixels of a patch of sky are blue; all the pixels of a patch of grass are green. The interesting things happen at the boundaries: edges between objects, textures within objects, shapes formed by arrangements of edges. A good model should exploit locality: it should recognise patterns in small patches and combine them into larger patterns.
The second insight is parameter sharing. A pattern like “vertical edge” has the same mathematical structure regardless of where in the image it appears. If you train a small detector to recognise a vertical edge at position (5,5), the same detector should work at position (100,100). Instead of learning a separate detector for every position, learn one detector and slide it across the whole image.
These two insights, locality and parameter sharing, are the entire conceptual basis for convolutional neural networks. They were worked out by Yann LeCun and peers at Bell Labs in the late 1980s, applied to handwritten digit recognition for US Postal Service address parsing, and remained a specialist tool until AlexNet dominated ImageNet in 2012.
A convolution, intuitively. A small matrix of weights (called a filter or kernel) slides across the input image, computing a weighted sum of the pixels under it at each position. The result at each position is a single number indicating how much the image patch at that position resembles the filter. Do this for every position and you get a new matrix (the feature map) that tells you where the filter “activates” across the image. Train the weights of the filter to make the feature map useful for downstream prediction, and you have a convolutional layer.
The convolution (technically, it’s a correlation, but everyone calls it convolution in the neural network world) is the element-wise product of P and F summed:
0 ⋅ 0 + 1 ⋅ 2 + 0 ⋅ 3 + 1 ⋅ 2 + 1 ⋅ 4 + 1 ⋅ 1 + 0 ⋅ 0 + 1 ⋅ 3 + 0 ⋅ 0 = 0 + 2 + 0 + 2 + 4 + 1 + 0 + 3 + 0 = 12
So at this position, the convolution outputs 12. The filter was designed to detect a cross pattern, and the patch happens to contain a cross, so the output is high. If the patch contained a T instead:
The convolution with the same filter gives:
1 ⋅ 0 + 1 ⋅ 2 + 1 ⋅ 3 + 0 ⋅ 2 + 1 ⋅ 4 + 0 ⋅ 1 + 0 ⋅ 0 + 1 ⋅ 3 + 0 ⋅ 0 = 0 + 2 + 3 + 0 + 4 + 0 + 0 + 3 + 0 = 12
Sliding the filter. To process the whole image, you slide the filter across every position, compute the convolution at each position, and collect the results into an output matrix. This is the feature map for that filter. If you have many filters in a layer (say, 64 of them), each filter slides across the same input and produces its own feature map, giving you 64 feature maps at the output. These are stacked into a volume with 64 channels. The next layer then treats this volume as its input and slides its own filters across it.
Read this as the basic CNN operation. One filter produces one feature map. Many filters per layer produce a stack of feature maps. Multiple layers of this, and you end up with progressively higher-level representations: edges in early layers, textures in middle layers, object parts in late layers, whole objects near the end.
Stride and padding are two hyperparameters that control how the filter slides. Stride is how many pixels the filter moves at each step. Stride 1 gives the finest-grained feature map; stride 2 halves the size in each dimension, which reduces parameters and compute. Padding is adding zeros around the border of the input so the filter can be centred on the edge pixels and you don’t lose size at every layer. Stride and padding together determine the output dimensions of a convolution layer. You adjust them based on how much the network should shrink its spatial dimensions as it goes deeper.
Pooling is the other key ingredient. After a convolution layer, you often apply a pooling layer, which aggregates small neighbourhoods of the feature map into single values. The most common is max pooling: for each 2×2 patch, take the maximum value. Max pooling with stride 2 halves the spatial dimensions and makes the representation slightly translation-invariant (small shifts in the input don’t change the output much). Pooling has no learnable parameters; it’s a fixed operator with a moving window.
The typical CNN architecture alternates convolution and pooling layers. In production at both Merehaven Bank (a managed training service) and Merehaven Bank (a managed training service), CNNs for document understanding are usually pretrained ResNet or ViT models fine-tuned on internal cheque or document images via transfer learning, gradually reducing spatial dimensions while increasing the number of channels, and finally feeds the flattened result into one or more fully connected layers for the final prediction.
Concrete walkthrough: a document understanding CNN In the synthetic Merehaven case. Imagine you’re building a model to classify scanned cheque images as “valid,” “suspicious,” or “needs review.” The input is a 256×256 grayscale image. A plausible architecture:
- Conv layer 1: 32 filters of size 3×3, stride 1, padding 1, ReLU activation. Output: 32 × 256 × 256.
- Max pool 2×2, stride 2. Output: 32 × 128 × 128.
- Conv layer 2: 64 filters of size 3×3, stride 1, padding 1, ReLU. Output: 64 × 128 × 128.
- Max pool 2×2, stride 2. Output: 64 × 64 × 64.
- Conv layer 3: 128 filters of 3×3, stride 1, padding 1, ReLU. Output: 128 × 64 × 64.
- Max pool 2×2, stride 2. Output: 128 × 32 × 32.
- Flatten: 131,072-dimensional vector.
- Fully connected layer: 256 units, ReLU.
- Output layer: 3 units, softmax.
Total parameters: roughly 34 million, mostly in the final fully connected layer. Trained with cross-entropy loss and Adam. In production at a bank, you would also carefully engineer the preprocessing (cropping, deskewing, contrast normalisation) and the post-processing (confidence thresholds, routing logic), and you would rigorously validate the model against known failure modes. The CNN is a component in a larger system, not the whole system.
If asked: “Why use a CNN instead of a fully connected network for image classification?”
How does a neural network read a sentence?
Images have spatial structure. Text has temporal structure: words come in a specific order, and that order matters. “Dog bites man” and “man bites dog” have the same words but very different meanings. A model that processes text needs to respect the order. Plain MLPs and CNNs do not; they treat their input as an unordered bag of values, possibly with spatial arrangement but not with sequence.
The architecture designed for sequences is the recurrent neural network or RNN. The idea is beautifully simple: instead of processing all of the input at once, process it one element at a time, maintaining an internal state vector that carries information from the past into the present. At each time step, the network takes the current input and the previous state, combines them, and produces a new state (and optionally an output). At the next time step, the network takes the next input and the new state, and so on. The state acts as a memory.
The math. Let xt be the input at time step t and ht be the hidden state. A single layer of a vanilla RNN computes:
ht = g(Wxt + Uht − 1 + b)
Read this carefully. The new hidden state ht depends on the current input (through W), the previous hidden state (through U), and a bias, all squashed through a nonlinearity g (typically tanh). The weight matrices W and U are shared across all time steps: the same W processes every input, and the same U processes every previous state. This is the recurrent structure. The network has memory, and the memory evolves step by step as the sequence is consumed.
At each time step the network can also produce an output yt = go(Vht + c), which depends only on the current hidden state. Whether you use the output at every step or only at the end depends on the task. For sequence labelling (tag each word with its part of speech), you use the output at every step. For sequence classification (is this email spam or not spam?), you use only the output at the final step.
Read this as the unrolled view of an RNN. Time flows left to right. At each step, the network combines the current input with the previous hidden state to produce a new hidden state. The same weights are reused at every step. The final hidden state (or outputs at intermediate steps) feed into the prediction layer.
Training an RNN via backpropagation through time. To train an RNN, you apply backpropagation to the unrolled network, which looks like a very deep feedforward network with shared weights at every layer. This is called backpropagation through time (BPTT). The same weight appears in many layers of the unrolled network, and the gradient with respect to that weight is the sum of the contributions from every layer. The technique was described by Werbos in 1990 and remains the standard training approach for RNNs.
The long-term dependency problem. Here is where the trouble starts. Vanilla RNNs suffer from the same vanishing gradient problem as deep feedforward networks, but even more acutely. The “depth” of the unrolled network equals the length of the sequence. For a 50-word sentence, you’re backpropagating through 50 layers. The gradients vanish before they reach the early words. As a consequence, the model cannot learn dependencies that span long distances. If a pronoun at the end of a paragraph refers to a noun at the beginning, the RNN cannot learn to connect them, because the signal has decayed long before it gets back to the noun.
This was the RNN’s central failure. People knew about it in the 1990s, and several researchers worked on fixing it, and the fix that eventually worked was the long short-term memory (LSTM) network, invented by Sepp Hochreiter and Jürgen Schmidhuber in 1997. Their paper was initially viewed as niche and sat relatively unread for a decade before deep learning’s rise turned it into one of the most cited papers in machine learning. Schmidhuber has some opinions about this, which he shares freely whenever the opportunity arises.
The LSTM idea. Instead of a single hidden state that gets updated at every time step, an LSTM maintains a cell state that can be selectively read, written, or cleared by a set of gates. Each gate is a small neural network (essentially a logistic regression) that takes the current input and previous hidden state and produces a value between 0 and 1, indicating how much of some information to keep, let through, or overwrite. The architecture has three gates: the forget gate (how much of the cell state to erase), the input gate (how much of the new input to write), and the output gate (how much of the cell state to expose as the hidden state).
The magic of gates is that when a gate is at or near 1, information passes through unchanged, effectively via an identity function, and the gradient of the identity function is constant. Gradients can flow across many time steps without vanishing, and the network can learn long-term dependencies that would be invisible to a vanilla RNN.
h̃l, ut = g1(wl, uxt + ul, uhlt − 1 + bl, u)
Γl, ut = g2(ml, uxt + ol, uhlt − 1 + al, u)
hl, ut = Γl, uth̃l, ut + (1 − Γl, ut)hlt − 1
Don’t memorise the equations. Notice the structure. h̃ is a candidate new value for the memory cell. Γ is the gate, a sigmoid-squashed linear combination that lies in (0, 1). The update rule h = Γh̃ + (1 − Γ)hprev interpolates between the new candidate and the old value, controlled by the gate. When the gate is 0, the cell retains its previous value exactly. When it’s 1, the cell takes the new candidate. Between these extremes, the cell blends the two. The gate itself is learned, which means the network decides, based on the current input and state, how much to update the memory.
Read this as the GRU update step. At every time step, the network computes both a candidate new value and a gate, and blends them with the previous state. The gate learns to decide when to update and when to preserve. This is the mechanism that lets gated RNNs handle long dependencies.
Banking applications of RNNs. Sequence models in banking are real. Fraud detection systems process sequences of transactions and flag suspicious patterns. AML systems look at customer behaviour over time to detect structured deposits or layering schemes. Time-series forecasting for liquidity planning and stress testing uses sequence models. Speech-to-text in customer contact centres uses sequence models. In natural language processing tasks within banks, RNNs and LSTMs were the standard until around 2019, when transformers (coming in a later chapter) largely replaced them for most tasks. RNNs still see use where the sequences are short, the compute budget is tight, or the deployment constraints rule out transformer-class models.
The sequence-to-sequence pattern. A particularly important application is translating one sequence to another. You use one RNN (called the encoder) to process the input sequence and produce a fixed-size representation. You use a second RNN (called the decoder) to generate the output sequence from that representation. This encoder-decoder architecture, introduced by Sutskever, Vinyals, and Le in 2014, was the basis for the first neural machine translation systems, and it’s the ancestor of the transformer architecture used by every modern language model. The fingerprints of sequence-to-sequence are visible in the Merehaven Credit Workbench today: the Copilot takes a conversation and generates a draft memo, which is exactly a sequence-to-sequence problem.
If asked: “Why do we need LSTMs or GRUs instead of vanilla RNNs?”
Match specialised tools to data structure
How do you fit a smooth curve through messy data without choosing a polynomial?
Linear regression draws a straight line. Polynomial regression draws a curve, but you have to choose the degree of the polynomial in advance, and getting it wrong leaves you either underfit (degree too low) or overfit with wild oscillations (degree too high). Neither feels right when you have messy worked data and you just want a smooth function that follows the trend wherever the data goes.
That’s kernel regression. Given training data {(xi, yi)}i = 1N and a new query x, the prediction is a weighted average of the training labels:
The function k(⋅) is the kernel. It plays the role of a similarity function. When xi is close to x, the kernel value is large, so the weight wi is large, and the corresponding training label contributes heavily to the prediction. When xi is far from x, the kernel value is small and the weight is small. The denominator normalises the weights so they sum to one.
The most popular kernel is the Gaussian kernel:
This is the bell curve. Inputs near zero produce values near . Inputs far from zero in either direction produce values that decay smoothly toward zero. When you plug z = (xi − x)/b into the kernel, you’re measuring how close xi is to x in units of b, which is called the bandwidth.
The bandwidth b is the only hyperparameter and it controls everything. Small b means the kernel is sharply peaked: only the very nearest training points contribute meaningfully, and the regression curve hugs the training data tightly (overfit). Large b means the kernel is wide and flat: many training points contribute, and the regression curve is very smooth, possibly too smooth (underfit). Choosing b is a validation-set tuning problem, exactly like the hyperparameters we met in Chapter 5.
Concrete walkthrough. Suppose you have three training points for a 1D regression: (1, 10), (3, 20), (5, 30). You want the prediction at x = 2 with bandwidth b = 1.
Compute the kernel values: - - -
Sum: 0.242 + 0.242 + 0.004 ≈ 0.488.
Normalised weights: w1 = 0.242/0.488 ≈ 0.496, w2 ≈ 0.496, w3 ≈ 0.008.
Prediction: f(2) = 0.496 ⋅ 10 + 0.496 ⋅ 20 + 0.008 ⋅ 30 ≈ 14.96.
Almost exactly halfway between the labels of points 1 and 3, which is what you’d expect since the query is halfway between x = 1 and x = 3 and the third point is too far away to matter much. If you used b = 0.1 instead, the kernel for x = 5 would be effectively zero, and even the x = 3 contribution would be small, so the prediction would be dominated by x = 1 and would be near 10. If you used b = 5, all three points would contribute roughly equally and the prediction would be near 20. The bandwidth controls the smoothness of the resulting function.
Where this fits in banking. Kernel regression is rarely the production model of choice in modern banks because it requires storing all training data (like k-NN), which has memory and latency costs at scale, and because tree ensembles usually do better on complex tabular problems. But it’s a beautiful baseline for one-dimensional or low-dimensional smoothing problems. If you have a year of weekly unemployment data and you want a smooth trend line for stress testing, kernel regression with a Gaussian kernel will give you exactly that. It’s also the conceptual ancestor of many modern non-parametric methods, including Gaussian processes, which are used in Bayesian hyperparameter optimisation.
If asked: “What’s the difference between kernel regression and k-NN regression?”
How do you handle more than two classes?
Many of the algorithms from Chapter 3 are fundamentally binary. SVM separates two classes with a hyperplane. Logistic regression outputs a probability for one class. They were not designed for problems where the answer might be one of fifteen categories. Yet most real classification problems have more than two classes. A merchant categorisation model has hundreds of categories. A document routing model has dozens. A loan disposition model has at least four: approved, declined, refer, conditional approval.
Three approaches handle multiclass classification, each suited to a different situation.
Approach one: use an algorithm with native multiclass support. Some algorithms generalise naturally to many classes. Decision trees simply count the proportion of each class at each leaf and predict the majority. Logistic regression generalises to the softmax regression model, which we’ll see again in neural networks: the output layer has one unit per class, each computing a linear score, and the softmax function turns the scores into a probability distribution over classes. k-NN simply looks at the k nearest neighbours and takes the majority vote across however many classes appear. Each of these requires no special tricks for multiclass; you just train the model and it works.
The softmax function deserves a moment. Given C scores z1, z2, …, zC (one per class), softmax computes:
The numerator exponentiates the score for class c. The denominator sums the exponentiated scores for all classes, normalising the result. The output is a vector of C values that are all positive and sum to one, a valid probability distribution. Softmax is to multiclass what sigmoid is to binary: the squashing function that turns raw scores into probabilities.
Approach two: one-versus-rest (OvR). For algorithms that are stuck being binary, like the original SVM, you train C separate binary classifiers, one per class. Each binary classifier is trained to distinguish “class c” from “everything else.” At prediction time, you run all C classifiers on the new input and pick the class whose classifier is most confident. For logistic regression you compare predicted probabilities. For SVM you compare the signed distances from the decision boundary.
The recipe in detail. Suppose you have three classes {1, 2, 3} and a binary algorithm. Make three copies of your training data:
- Copy 1: relabel “class 1” as positive (1), and both class 2 and class 3 as negative (0). Train a binary classifier f1 on this.
- Copy 2: relabel “class 2” as positive, and class 1 and class 3 as negative. Train f2.
- Copy 3: relabel “class 3” as positive, and class 1 and class 2 as negative. Train f3.
To predict the class of a new input x, compute f1(x), f2(x), f3(x), each producing a score in [0, 1] (for logistic regression) or a signed distance (for SVM). Pick the class with the highest score.
Read this as the OvR pattern. You build C binary classifiers, each focused on its own class, and combine their outputs at prediction time. It’s a bolt-on layer that turns any binary classifier into a multiclass one.
Approach three: one-versus-one. A less common alternative is to train C(C − 1)/2 binary classifiers, one for each pair of classes. At prediction time, each classifier votes for one of its two classes, and the class with the most votes wins. This is more expensive in training but sometimes gives slightly better results, especially for problems where the binary classifier struggles when many “rest” examples are very different from each other. For most banking applications, OvR is the default and OvO is a refinement only used when needed.
Banking example. Imagine a customer email triage model In the synthetic Merehaven case that routes incoming customer service emails to one of 25 topic categories (fraud query, mortgage application, statement copy, complaint, internet banking issue, and so on). The natural approach is multiclass logistic regression with softmax output, trained on a few hundred thousand historical emails labelled by past triage decisions. Alternatively, a single decision tree or gradient boosted tree could handle the multiclass problem natively by counting class proportions at each leaf. In production In the synthetic Merehaven case in the reference implementation, the actual model would likely be a fine-tuned transformer because of how much pre-trained NLP knowledge it brings, but the simpler softmax classifier is the right baseline and is what you’d ship if compute or interpretability constraints ruled out the transformer.
If asked: “Explain one-versus-rest classification in plain English.”
What do you do when you only have examples of normal behaviour?
Here’s a problem that sounds esoteric and turns out to be everywhere in banking. You want to detect “abnormal” transactions, “abnormal” customer behaviour, “abnormal” network traffic, “abnormal” loan applications, “abnormal” anything. The catch is you don’t have labelled examples of “abnormal.” You have millions of examples of normal, and you need to flag things that don’t look like them.
This is one-class classification, also called novelty detection or anomaly detection. The defining feature is that your training data contains only one class, and your job is to learn the shape of that class so you can recognise everything that doesn’t fit. AML transaction monitoring is the canonical banking example: you have decades of legitimate transactions and almost no labelled examples of money laundering, because criminals don’t tag their transactions and the cases that get caught are a tiny biased sample of the actual population. A model that needs labelled positives to learn from has nowhere to start. A one-class model can use the abundant negative examples to learn what normal looks like, then flag anything that deviates.
Approach one: one-class Gaussian. Assume your data was drawn from a multivariate Gaussian distribution, fit the parameters of that Gaussian to your training data, and then flag any new point whose density under the fitted Gaussian is below a threshold.
The multivariate Gaussian density is:
Don’t let the notation scare you. The interesting bits are μ (a vector giving the centre of the distribution) and Σ (a matrix called the covariance matrix that describes how the data spreads in each direction and how different features are correlated). The exponent measures, in a scale-aware way, how many “standard deviations” the input x is from the centre μ. Inputs near μ have high density. Inputs far from μ have low density, and “far” is measured taking the shape of the distribution into account: a point that is 2 standard deviations away in a tight direction is just as anomalous as a point 10 standard deviations away in a loose direction.
The training procedure is maximum likelihood, the same principle we met for logistic regression and parameter estimation in Chapter 2. Compute the sample mean and sample covariance from the training data, plug them in as μ and Σ, and you have a fitted Gaussian. To classify a new point, compute its density, compare to a threshold, and flag if below.
The threshold is the only hyperparameter and it controls the trade-off between false alarms and missed anomalies. Lower threshold: fewer alarms, more missed cases. Higher threshold: more alarms, fewer missed cases. You set it based on the operational capacity of whoever has to investigate the alarms.
The Gaussian assumption is strong. If your data isn’t shaped like a multivariate Gaussian (and most real data isn’t, exactly), the model will misfit. Fixes include using a mixture of Gaussians, which combines several Gaussian distributions with different centres and shapes to capture multi-modal data, and various kernel-based or density-based extensions.
Approach two: one-class k-means. Cluster the training data using k-means. For a new point, compute the minimum distance to any cluster centre. If this distance is below a threshold, classify as belonging to the class. Otherwise, flag as anomaly. This is more flexible than one-class Gaussian because it doesn’t assume any particular shape; it just needs the data to cluster reasonably.
Approach three: one-class SVM. Two formulations exist. The first tries to find a hyperplane that separates the training data from the origin (in some kernel-induced feature space) with the largest possible margin. Points on the “data side” of the hyperplane are normal; points on the “origin side” are anomalies. The second formulation finds the smallest sphere that contains most of the training data; points outside the sphere are anomalies. Both have the SVM machinery (kernels for non-linearity, hyperparameters for the trade-off between margin and outliers) and both work well in practice.
Read this as a fork by data shape. Each approach makes a different assumption about how “normal” looks geometrically, and the right choice depends on your data.
The Merehaven Bank AML example. A transaction monitoring system In the synthetic Merehaven case processes tens of millions of transactions daily. The fraction that are actually money laundering or sanctions evasion is tiny and the labelled examples are rare. The system trains a one-class model on legitimate transaction patterns, often using a mixture of Gaussians or a one-class SVM with an RBF kernel, separately for each major customer segment (because what’s normal for a wealth management client differs from what’s normal for a retail customer). New transactions are scored against the customer’s segment model, and anything below the density threshold goes into an alert queue for human investigation.
Over time, the alerts that turn out to be true positives feed a supervised model on top, but the foundation remains the one-class layer because that’s the only thing that scales to billions of transactions and unknown threats.
If asked: “When do you use one-class classification instead of binary classification?”
What if each example needs more than one label?
So far we’ve assumed every example has exactly one label. Sometimes examples have several at once. An image of a hillside might be labelled “conifer,” “mountain,” and “road” all at the same time. A customer service email might be labelled “complaint,” “billing,” and “high priority” simultaneously. A news article might be labelled with several topic tags. This is multi-label classification, and it’s distinct from multiclass: in multiclass, exactly one label is correct; in multi-label, several labels can be correct simultaneously.
There are two general approaches.
Approach one: independent binary classifiers, one per label. For each possible label, train a binary classifier to predict whether that label applies. At prediction time, run all the classifiers and assign whichever labels score above a threshold. This is simple, scales well to large label sets, and lets you tune the threshold per label based on validation. The downside is that the classifiers don’t share information, so if labels are correlated (e.g., “spam” and “high priority” tend to be opposite), the model can produce inconsistent predictions.
| Fake class | Type 1 | Type 2 |
|---|---|---|
| 1 | photo | portrait |
| 2 | photo | paysage |
| 3 | photo | other |
| 4 | painting | portrait |
| 5 | painting | paysage |
| 6 | painting | other |
Now you have a six-way multiclass problem instead of two parallel binary problems, and the classifier learns the joint distribution over labels. The advantage is that label correlations are preserved automatically. The disadvantage is that the number of fake classes grows exponentially with the number of original label types, so this only works when the combinations are few.
Approach three: neural networks with binary cross-entropy on the output layer. In a deep learning setup, the output layer has one unit per label, each with a sigmoid activation. The loss is the average binary cross-entropy across all labels and all training examples:
where L is the number of labels and ŷi, l is the predicted probability that example i has label l. This is essentially L parallel logistic regressions sharing all the neural network’s hidden representations, which means the model can learn label correlations through the shared representations even though each output is independent at the loss level. This is the standard recipe for multi-label image classification, document tagging, and similar tasks.
Banking example. Suppose Merehaven Bank wants to tag commercial customers with multiple “interest categories” for cross-sell targeting: green finance, international trade, hedging needs, succession planning, working capital, capex investment. A given customer might fit into several of these. A multi-label classifier trained on historical product engagement data can predict, for each customer, the probability of fitting each category. The marketing team uses these probabilities to design targeted campaigns. The model is, structurally, a small neural network with sigmoid outputs and binary cross-entropy loss, or alternatively a series of logistic regressions trained per category.
If asked: “What’s the difference between multiclass and multi-label classification?”
Why do many weak models beat one strong model?
We arrive at the most important section of this section, and arguably the most important practical lesson in classical machine learning. Ensemble learning is the technique of combining many simple models into a single more accurate meta-model, and it is the dominant paradigm for tabular data in the reference implementation.
Here’s the surprising fact. If you take a weak learner, a model that’s only slightly better than random guessing, and you combine many of them, you can build a meta-model that is materially better than any individual model in the ensemble. This sounds like magic, but the intuition is simple. If each weak model is wrong on different examples, then the votes of many weak models tend to cancel out the errors. The collective wisdom of the crowd is more reliable than any individual opinion, provided the opinions are at least slightly informed and not all making the same mistakes.
Read this as the central intuition of ensemble learning. The key is that the weak models must be diverse: if they all agree on the same wrong answers, combining them gains you nothing. The whole craft of ensemble methods is in the techniques for producing diverse weak models from the same training data.
There are two main families: bagging and boosting.
Bagging (short for bootstrap aggregating) works by training each weak model on a different random sample of the training data and combining their predictions by voting or averaging. The diversity comes from the different samples each model sees. The most famous bagging algorithm is random forest, which we’ll cover in detail below.
Boosting works by training weak models sequentially, where each new model focuses on the examples that the previous models got wrong. The diversity comes from the changing emphasis on different training examples. The most famous boosting algorithm is gradient boosting, which is the workhorse of modern competition-winning ML and which we’ll also cover in detail.
These two families have different strengths. Bagging primarily reduces variance: it stabilises a high-variance model like a decision tree by averaging out the noise from any individual tree’s overfit. Boosting primarily reduces bias: it iteratively adds capacity to a sequence of weak models to capture patterns the previous ones missed. In practice, both can reduce both, but the primary effect of each is what guides when to use which.
How does a random forest actually work?
The story of random forests is short and beautiful. In 2001, Leo Breiman at Berkeley published a paper combining two ideas. The first was bagging, which Breiman himself had introduced in 1996: train many decision trees on bootstrap samples of the data and average them. The second was a small twist: at each split in each tree, instead of considering all features, only consider a random subset of features. This second twist was the difference between “vanilla bagging of decision trees” (which works okay) and “random forest” (which is one of the most reliable algorithms in all of machine learning).
Step by step. Given a training set of N examples and D features:
Sample N examples from the training set with replacement (so some examples appear multiple times and some not at all). This is one bootstrap sample. Repeat B times to get B bootstrap samples {S1, S2, …, SB}, where B is typically a few hundred.
Train a decision tree on each bootstrap sample. With one twist: at each node, instead of considering all D features for the split, randomly select a subset of size m (typically for classification or m = D/3 for regression) and only consider those.
To predict on a new example, run it through all B trees and aggregate the predictions. For classification, take the majority vote. For regression, take the average.
That’s it. A random forest is a collection of B decision trees, each trained on a different bootstrap sample using a random subset of features at each split, with predictions combined by voting or averaging.
Read this as the random forest pipeline. The bootstrap sampling produces B different but overlapping training sets. The random feature subset ensures the trees don’t all latch onto the same dominant feature. The aggregation cancels out individual tree errors.
Why the random feature subset matters. Without it, if one or two features are strong predictors of the target, every tree would split on them at the root, and the trees would all look basically the same. Correlated trees are bad for ensembling: their errors are correlated, so averaging doesn’t reduce the variance. By forcing each split to consider only a random subset of features, you ensure that trees use different features at different positions, which makes them more independent and the ensemble more accurate.
The two main hyperparameters are the number of trees B and the size of the random feature subset m. More trees often helps but with diminishing returns; typical values are 100 to 1000. The feature subset size has a sweet spot: too small and the trees are weak, too large and they’re correlated.
Why random forests are so popular in banking. Several reasons. They handle mixed numerical and categorical features well. They handle missing values reasonably (depending on implementation). They are well-tested to feature scaling, so you don’t need to normalise. They give feature importance scores out of the box, which are useful for interpretability. They are easy to parallelise across CPU cores. They rarely overfit compared to single deep trees. And they often give a respectable baseline with default hyperparameters and minimal tuning, which makes them the perfect “first strong model” to try after the linear baseline.
In the Merehaven Bank bake-off I described at the start of this section, the random forest team would have come third or fourth, beating the deep learning team but losing to the gradient boosted ensemble. Random forests are the reliable workhorse of 2010s ML and remain a strong baseline in the reference implementation. They are not glamorous. They are not strongest evaluated. They just work.
If asked: “Why does a random forest work better than a single decision tree?”
How does gradient boosting actually work?
If random forest is the reliable workhorse, gradient boosting is the apex predator. It’s the algorithm that has won more Kaggle competitions than any other, the algorithm that powers most production credit risk and fraud models at major banks, and the algorithm that should be your default whenever you have tabular data and need maximum accuracy. It was published by Jerome Friedman at Stanford in 2001 and refined into the now-dominant XGBoost implementation by Tianqi Chen in 2014.
The idea is fundamentally different from random forest. Where random forest builds many trees in parallel on different data samples, gradient boosting builds many trees sequentially, where each new tree tries to fix the errors of the previous ones. Each tree is shallow (typically 4-8 levels deep), so individually weak. The sequential building lets later trees focus on the parts of the input space where the earlier trees were wrong.
Step 1. Start with a constant model that predicts the mean of the training labels:
This is the worst possible model that still uses some information from the data. Its predictions are the same for every input.
Step 2. Compute the residuals: the difference between the true labels and the current model’s predictions.
ŷi = yi − f(xi)
The residuals tell you how much the current model is wrong on each training example, and in which direction. Positive residual means the model is under-predicting; negative means over-predicting.
Step 3. Train a new decision tree f1 to predict the residuals as if they were the labels. This new tree learns to capture whatever pattern the constant model missed.
Step 4. Update the model by adding the new tree, scaled by a learning rate α:
f ← f0 + αf1
The learning rate (often called shrinkage in this context) is a small number, typically 0.01 to 0.3. Without it, each new tree would aggressively overshoot; with it, each new tree contributes a small correction in the right direction.
Step 5. Recompute the residuals using the updated model. They will be smaller than before, because the new tree fixed some of the errors.
Step 6. Train a new tree f2 on the new residuals. Add it to the model. Recompute residuals. Train f3. And so on, until you’ve added M trees (a hyperparameter you set in advance).
The final model is:
f(x) = f0(x) + αf1(x) + αf2(x) + … + αfM(x)
Each tree corrects a little bit of the error left by the previous trees, and after many trees, the residuals are tiny and the combined model is very accurate.
Read this as the boosting loop. Each iteration uses the residuals of the current model to train a new tree, then adds the new tree to the model. The model gradually improves, one weak learner at a time.
Why is it called “gradient” boosting? Because the residuals are, mathematically, the negative gradient of the squared error loss with respect to the model’s predictions. So training a tree on the residuals is essentially taking a step in the direction of the negative gradient, exactly like gradient descent, but in function space rather than parameter space. The learning rate α plays the same role as the learning rate in gradient descent: it controls how big each step is. The more general view (the one that XGBoost implements) is that gradient boosting can use any differentiable loss function, not just squared error, and the residuals are replaced by the gradient of the chosen loss. For binary classification with logistic loss, the “residuals” become the difference between the true label and the predicted probability, and the procedure is otherwise identical.
The classification version. For binary classification, the model produces a real-valued score that’s converted to a probability via sigmoid:
The training objective is to maximise the log-likelihood of the training labels under this model. The optimisation proceeds the same way: start with a constant initial model (the log-odds of the base rate), compute gradients of the log-likelihood with respect to the model’s predictions, train a tree to fit those gradients, add it to the model, and repeat.
The three main hyperparameters are the number of trees M, the learning rate α, and the maximum depth of each tree. They interact tightly. Smaller learning rate plus more trees is generally better but slower to train. Deeper trees give more capacity per tree but more risk of overfitting. The standard practice is to set the learning rate to a small value (like 0.05), set the max depth to something modest (like 6), and use early stopping based on validation loss to determine the number of trees automatically.
Where gradient boosting wins. Almost everywhere there’s tabular data with engineered features. Credit risk scoring. Fraud detection. Customer churn. Insurance claim severity. Marketing response prediction. Most of the production tabular ML In the synthetic Merehaven case is some flavour of gradient boosted trees, because they consistently outperform every alternative on these problems by a meaningful margin and they handle worked data quirks (missing values, mixed types, irrelevant features) gracefully.
Where gradient boosting loses. Images (use CNNs). Long text (use transformers). Speech (use specialised audio models). Anywhere the data has strong sequential or spatial structure that needs to be respected by the architecture rather than learned from scratch. Gradient boosting treats every input as an unordered list of features, which is great for tables and bad for everything else.
Bagging vs boosting trade-off. Random forests are easier to tune (mostly just the number of trees), more well-tested to noise in the labels, and trivially parallelisable. Gradient boosting usually achieves higher accuracy when properly tuned, handles smaller datasets better (because each tree adds capacity rather than averaging it out), and can fine-tune to a specific loss function. In modern practice, gradient boosting wins when accuracy matters most and there’s time to tune; random forest wins when robustness and ease of deployment matter most and the accuracy difference is small.
If asked: “Why does gradient boosting often outperform random forest?”
How do you label every word in a sentence at once?
Some classification problems aren’t really about classifying examples; they’re about classifying every element of a sequence. A part-of-speech tagger needs to label each word in a sentence with its grammatical role. A named entity recogniser needs to label each word as person, location, organisation, or none. A transaction-level fraud detector needs to label each transaction in a customer’s history as suspicious or normal. These are sequence labelling problems.
Formally, a sequence labelling training example is a pair (Xi, Yi) where Xi = [xi(1), xi(2), …, xi(T)] is a sequence of feature vectors, one per time step, and Yi = [yi(1), yi(2), …, yi(T)] is a sequence of labels of the same length. The task is to learn a function that takes a feature sequence and outputs a label sequence, where the prediction at each position can depend on neighbouring positions.
You could naively classify each position independently with a logistic regression or decision tree. This loses the dependencies between positions. The label of a word often depends on the labels of its neighbours: if the previous word is “John,” the current word “Smith” is more likely to be a “person” label. Capturing these dependencies requires a model that knows about the whole sequence.
Approach one: RNN, which we covered in Chapter 6. An RNN reads the sequence left to right, maintaining a hidden state that summarises everything it has seen so far. At each position, it can output a label based on its current input plus its accumulated state. For sequence labelling, you typically use a bidirectional RNN, which has one RNN reading left to right and another reading right to left, with the outputs concatenated. This gives the model context from both directions at every position, which is essential because the label of a word can depend on words that come after it as well as before.
Approach two: Conditional Random Fields (CRF). Before deep learning displaced them, CRFs were the standard for sequence labelling. A CRF is, in essence, a generalisation of logistic regression to sequences: instead of predicting one label for one input, it predicts a whole label sequence for a whole input sequence, with explicit modelling of the dependencies between adjacent labels. The training objective maximises the probability of the correct full label sequence given the input.
CRFs are particularly good when you have rich hand-engineered features at each position (like “is the word capitalised,” “is the word in a list of known person names,” “what is the word’s suffix”). They’re slower to train than RNNs and don’t scale to as much data, but they handle structured features beautifully and they remain a reasonable choice for medium-data NLP tasks.
Approach three: bidirectional LSTMs with a CRF layer on top. This combination, popular in the late 2010s, used the RNN to learn rich feature representations and the CRF to enforce label-sequence consistency. It was the strongest evaluated for named entity recognition for several years before being displaced by transformer-based models.
Approach four (the modern default): pre-trained transformers. A transformer like BERT, fine-tuned on labelled sequence data, materially outperforms all the earlier approaches because of the rich representations it brings from pre-training on enormous text corpora. For any new sequence labelling task in the reference implementation with reasonable data and reasonable compute, the right answer is “fine-tune a pre-trained transformer.” We’ll talk about transformers in a later chapter.
Banking example. Suppose Merehaven Bank wants to extract structured information from credit memos: identify mentions of company names, financial figures, key dates, and policy clauses. Each token in the memo gets labelled with one of these categories or “none.” This is exactly named entity recognition. The production model is a fine-tuned transformer, evaluated on a manually labelled validation set, with strict precision targets because false positives in entity extraction propagate downstream into incorrect data extraction. The model is monitored in production for accuracy drift and retrained quarterly.
If asked: “What’s the difference between sequence labelling and sequence classification?”
How do you translate one sequence into another sequence?
Some problems are even harder: the input is a sequence and the output is a sequence of potentially different length. Translation. Summarisation. Conversational response generation. Spelling correction. Speech recognition. These are sequence-to-sequence (seq2seq) problems, and they form one of the most important architectural patterns in modern AI.
The basic seq2seq architecture has two parts: an encoder and a decoder. The encoder is a neural network (originally an RNN, now usually a transformer) that reads the input sequence and produces a fixed-size or variable-size representation called the embedding. The decoder is another neural network that takes the embedding and generates the output sequence one token at a time.
The training procedure. Given pairs of input and output sequences, you train both encoder and decoder simultaneously. At each step of the decoder, the model predicts the next token, compares it to the true next token, computes the cross-entropy loss, and backpropagates the error through the entire encoder-decoder pipeline. The encoder learns to produce useful representations, and the decoder learns to generate from them.
The original seq2seq architecture from 2014 used two RNNs, where the encoder produced a single fixed-size hidden state vector that was passed to the decoder as its initial state. The decoder then generated the output sequence token by token, conditioning on this single embedding. This worked but had a serious bottleneck: the entire input sequence had to be compressed into one fixed-size vector, which was lossy for long inputs.
The breakthrough was attention. Bahdanau, Cho, and Bengio introduced the attention mechanism in 2014, allowing the decoder to “look back” at all the encoder’s hidden states (one per input position) and decide which ones to focus on at each output step. Instead of compressing the input into one vector, attention let the decoder access the full sequence with learned weighting. This single idea materially improved translation quality, particularly for long sentences, and set the stage for the transformer architecture (which is essentially “attention is all you need,” published in 2017 and changing AI permanently).
Read this as the encoder-decoder with attention pattern. The encoder produces a state for each input position. The decoder generates the output one token at a time, and at each step it computes attention weights over all the encoder’s states to decide which input positions to focus on. The same input positions can be attended to at multiple decoder steps, so the model can focus on different parts of the input as it generates different parts of the output.
Banking applications. The Merehaven Credit Workbench is fundamentally a seq2seq system: the input is a conversation and uploaded documents, and the output is a draft credit memo. Translation between English and other languages for international Merehaven Bank customers is seq2seq. Auto-summarisation of long policy documents is seq2seq. Speech-to-text in contact centres is seq2seq. Most modern seq2seq systems in the reference implementation are built on transformers rather than RNNs, but the conceptual architecture (encoder produces representations, decoder generates the output token by token) is the same.
If asked: “Why was attention such a big breakthrough for sequence-to-sequence models?”
What if labels are very expensive?
In many banking applications, getting labels is the bottleneck. To label a transaction as fraudulent, an analyst has to investigate it. To label a credit memo as compliant, a senior credit officer has to review it. To label a customer call as a complaint, a quality control reviewer has to listen to it. In all these cases, you have plenty of unlabelled data and very few labels. Can you do something smarter than just labelling random examples?
Yes. Two related approaches.
Active learning. Instead of labelling random examples, intelligently select the examples that would most improve the model if labelled. Train an initial model on whatever labels you have. For each unlabelled example, compute an importance score that combines how uncertain the current model is about the example with how representative the example is of the unlabelled distribution. Pick the highest-scoring example, ask a human to label it, add it to the training set, retrain the model, and repeat. The hope is that you can reach the same accuracy as random labelling with a fraction of the labels.
Density and uncertainty based. For each unlabelled example x, compute density(x) ⋅ uncertaintyf(x). Density measures how representative the example is (how many similar examples exist in the unlabelled set). Uncertainty measures how uncertain the current model f is about the example. For a binary classifier with sigmoid output, uncertainty is highest when the predicted probability is closest to 0.5. For a multiclass classifier, uncertainty can be measured by the entropy of the predicted class distribution:
Entropy is highest when the predicted probabilities are uniform across classes (the model is maximally uncertain) and lowest when one class dominates.
Support vector based. Train an SVM on the labelled data. Find the unlabelled example that is closest to the separating hyperplane. That’s the example the SVM is most uncertain about. Ask for its label. Adding it to the training set will most refine the position of the hyperplane.
Other strategies. “Query by committee” trains multiple models and asks for labels on the examples where the models disagree most. Some strategies aim to reduce variance most. Some aim to reduce bias most. The right strategy depends on the problem and the type of model you’re using.
Read this as the active learning loop. You alternate between training and asking for one strategically chosen label at a time, gradually growing the labelled set with examples that maximally improve the model.
The Merehaven Bank active learning use case. Suppose Merehaven Bank wants to build a complaint detection model on customer service calls. Initial training data is small. Listening to and labelling calls is expensive (each call takes 5-10 minutes of an analyst’s time). An active learning loop trains an initial model on the small labelled set, scores all unlabelled calls by uncertainty and density, and routes the highest-scoring calls to analysts for labelling. After a few iterations, the model is good enough to use, having required perhaps 1,000 labels instead of the 5,000 that random labelling would have needed. The labelling cost is reduced by 80% with no loss of model quality.
How do you learn from data when most of it is unlabelled?
Semi-supervised learning is the cousin of active learning that asks a different question. Active learning asks “which examples should we label to maximise model improvement?” Semi-supervised learning asks “can the worked design uses unlabelled examples directly to improve the model, without labelling more?” The answer is yes, sometimes, and the techniques are subtle.
Self-learning. The simplest semi-supervised approach. Train a model on the labelled data. Apply it to the unlabelled data. For any unlabelled example whose prediction confidence is above a threshold, accept the model’s prediction as a “pseudo-label” and add the example to the training set. Retrain. Repeat. This sometimes helps and sometimes hurts, depending on whether the model’s high-confidence wrong predictions outweigh its high-confidence right predictions. In practice, self-learning is fragile and used carefully.
Autoencoders. A more reliable approach is to use unlabelled data to learn good representations of the input, then build a small supervised model on top. An autoencoder is a neural network with an encoder-decoder architecture trained to reconstruct its input. The encoder maps the input to a low-dimensional embedding in a bottleneck layer, and the decoder reconstructs the input from the embedding. If the decoder can rebuild the input well, the embedding must contain the useful information. The training objective is to minimise the reconstruction error, typically mean squared error for continuous inputs:
where f is the autoencoder. No labels are needed; the input is its own target.
Read this as the autoencoder pattern. Input goes through a narrow bottleneck and back out. The bottleneck forces the network to learn a compact representation that captures the essential structure. Once trained, you can use the encoder as a feature extractor for downstream tasks, including supervised tasks where the bottleneck embedding is more informative than the raw input.
Denoising autoencoders. A refinement: corrupt the input by adding noise, but train the network to reconstruct the clean (uncorrupted) input. This forces the network to learn representations that are well-tested to noise, which usually generalises better than plain autoencoders.
Ladder networks. A more sophisticated architecture from 2015. A ladder network is a denoising autoencoder where the encoder has lateral connections to corresponding layers of the decoder, and the bottleneck is also used for supervised classification. The network has both reconstruction losses (one per layer) and a classification loss, and they are jointly optimised. The result is a network that uses unlabelled data through the reconstruction objective and labelled data through the classification objective, and benefits from both. Ladder networks achieved remarkable results on MNIST with as few as 10 labelled examples per class, demonstrating the power of well-designed semi-supervised learning.
The modern view. in the reference implementation, the dominant semi-supervised paradigm is pre-training plus fine-tuning. You pre-train a large model on a huge amount of unlabelled data with a self-supervised objective (like predicting masked tokens for BERT, or predicting the next token for GPT). Then you fine-tune the pre-trained model on a small labelled dataset for your specific task. The pre-training step uses the unlabelled data to learn rich representations; the fine-tuning step uses the labels to align the representations to the task. This is exactly what’s happening when the Merehaven Credit Workbench is built: a foundation model pre-trained on vast amounts of text is fine-tuned on Merehaven Bank-specific labelled examples to handle credit memos and conversations. The whole apparatus is semi-supervised learning at a scale unimaginable a decade ago.
If asked: “What’s the difference between active learning and semi-supervised learning?”
Engineer models for imbalance, transfer and speed
How do you train a useful model when one class is one in 2,500?
We met class imbalance briefly in Chapter 5 when we talked about why accuracy is a useless metric for fraud. Now we tackle it head on. The problem is not just that accuracy lies. The problem is that almost all standard learning algorithms, when trained on imbalanced data, produce models that essentially ignore the minority class. Logistic regression learns to predict the majority class with high confidence everywhere because that minimises the log-loss. Decision trees fail to split on minority-class signals because the impurity reduction looks small relative to the size of the majority population. Gradient boosting struggles for similar reasons. SVMs let the decision boundary drift toward the minority class because the cost of misclassifying a single minority example is the same as misclassifying a single majority example, and there are vastly more majority examples to “save.”
There are three families of fixes, and most regulated ML ML uses some combination of all three.
Family one: weight the classes. Tell the learning
algorithm that misclassifying a minority example is more expensive than
misclassifying a majority example. In a soft-margin SVM, you can pass
class weights as a parameter, and the optimiser puts a higher cost on
minority misclassification. In logistic regression and most other
classifiers in scikit-learn, you can do the same with the
class_weight parameter. The effect is to shift the decision
boundary in favour of the minority class, sacrificing some majority
accuracy in exchange for catching more minority cases.
Read this as the trade-off that class weighting enforces. You buy correctness on the rare class by sacrificing correctness on the common class. The right weighting depends on your business context: if false positives are cheap and false negatives are catastrophic, weight the minority high; if false positives clog up review queues and false negatives are recoverable, weight more conservatively.
Family two: oversample the minority. Instead of weighting the loss, change the data itself by making more copies of the minority examples. A simple version: duplicate each minority example until the classes are balanced. A slightly smarter version: bootstrap-sample from the minority class with replacement until you have as many minority as majority examples. Either way, the effect is similar to weighting: the optimiser sees more minority examples, so it cares more about getting them right.
The synthetic minority oversampling technique (SMOTE) is the more sophisticated approach. Instead of duplicating existing minority examples (which can cause overfitting because the model sees the exact same point many times), SMOTE creates synthetic minority examples by interpolating between existing ones. For each minority example xi, find its k nearest minority neighbours, pick one at random (call it xzi), and create a new synthetic point along the line between them:
xnew = xi + λ(xzi − xi)
where λ is a random number in [0, 1]. The new point lies somewhere on the segment between two real minority examples. If λ = 0, you get xi exactly; if λ = 1, you get xzi; in between, you get a weighted blend. SMOTE creates as many such synthetic points as needed to balance the classes. The intuition is that points along the line between two real minority examples are likely to also be reasonable minority examples, even though they may not exist in the training set.
The adaptive synthetic sampling method (ADASYN) is a refinement of SMOTE. It generates more synthetic examples in regions where minority examples are surrounded by majority examples (i.e., where the model would struggle most), and fewer in regions where minority examples are clustered together. The intuition is that the hard cases need more help than the easy cases. ADASYN often outperforms vanilla SMOTE on real imbalanced data.
Concrete walkthrough. Suppose you have a fraud dataset with 100 fraud examples and 100,000 legitimate examples. Vanilla random oversampling would duplicate each fraud example 999 times, producing 100,000 fraud copies, a perfectly balanced 1:1 dataset. SMOTE would generate 99,900 synthetic fraud examples by interpolation between existing fraud cases, producing 100,000 unique-ish fraud examples and 100,000 legitimate. ADASYN would generate the same total number but concentrate the synthetics in the regions of feature space where existing frauds are surrounded by legitimate examples (the “borderline” frauds), where they’re most likely to teach the classifier something useful.
Family three: undersample the majority. Instead of growing the minority, shrink the majority. Randomly drop legitimate examples until the classes are balanced or close to it. This is fast and reduces training time, but it throws away information that the model could have used. In practice, undersampling alone is rarely the best choice; it’s often combined with oversampling (e.g., undersample the majority by 10x and oversample the minority by 10x to meet in the middle).
A subtle point about validation. When you oversample or use SMOTE, you must do so on the training set only. Never on the validation or test set. The validation and test sets must reflect the real production class distribution, otherwise your performance metrics will be wildly optimistic. Most teaching examples and online tutorials get this wrong; senior engineers don’t.
Algorithms that handle imbalance well natively. Tree-based methods, including random forest and gradient boosting, are less sensitive to class imbalance than linear methods because each split is evaluated locally and doesn’t depend on global class proportions. They still benefit from class weighting or SMOTE on extreme imbalance, but they degrade more gracefully than logistic regression or SVM.
The Merehaven Bank fraud example. The story at the
start of this section is the reason banks use a combination of
techniques. The standard recipe in Merehaven Bank fraud detection is:
gradient boosted tree with scale_pos_weight set to roughly
the inverse class ratio (or higher if recall matters more), training
data SMOTE’d up to perhaps 1:10 minority:majority (not all the way to
1:1, because the synthetic examples become unrealistic), and a custom
evaluation metric that focuses on precision at the operating threshold.
With this recipe, the model is competitive with the baseline and the
operations team can use it. Without it, the model looks great on
standard metrics and fails in production.
If asked: “How would you handle a fraud detection problem where only 0.1% of transactions are fraudulent?”
When does combining models actually help?
We covered ensemble methods in Chapter 7, where the combination was many weak models from the same family (decision trees in random forest, decision trees in gradient boosting). Here we ask a different question: what about combining a few strong models from different families? You have a logistic regression, a gradient boosted tree, and a neural network, each individually good. Can you combine them to get something better than any one alone?
Yes, sometimes. Three techniques.
Averaging. The simplest. For regression, average the predictions of all your base models. For classification with probability outputs, average the predicted probabilities. The averaged prediction is often slightly better than any individual model, especially when the base models make uncorrelated errors. You evaluate the averaged model on the validation set to confirm it’s actually better.
Majority vote. For classification with hard class predictions, take the mode of the predictions from all base models. If three models predict “default” and two predict “non-default,” the ensemble predicts “default.” Ties go to a coin flip or to a specified default. This is simple, well-tested, and works whenever you have an odd number of base classifiers.
Stacking. The most powerful and most common in regulated ML ML. Stacking trains a small meta-model on top of the predictions of the base models. The meta-model takes the base models’ outputs as features and learns to combine them, capturing whatever residual structure the base models missed individually.
Concretely, suppose you have base classifiers f1 and f2. To create a training example for the meta-model, you compute , that is, the predictions of the base models on the original input, and you keep the original label ŷi = yi. Now train a meta-classifier (often a simple logistic regression) on pairs. At prediction time, you run the input through the base models to get their predictions, then feed those predictions into the meta-model to get the final answer.
importantly, the meta-model must not be trained on the same data the base models were trained on, or it will overfit to the base models’ training-time predictions and fail to generalise. The standard solution is out-of-fold prediction: train the base models on k − 1 folds and use them to predict on the held-out fold, then train the meta-model on those out-of-fold predictions. This is also called stacked generalisation, introduced by Wolpert in 1992.
Read this as the stacking pipeline. Base models are trained on the data and produce predictions. A meta-model is trained on the base predictions to learn how to combine them. At inference, the same flow applies.
Why does combining models work? Because if the base models are uncorrelated (they make different mistakes on different examples), combining them tends to cancel out the errors. The important requirement is uncorrelation. Combining ten different gradient boosted trees with slightly different hyperparameters won’t help much, because they all make basically the same mistakes. Combining a logistic regression, a gradient boosted tree, and a neural network might help a lot, because they have different inductive biases and disagree on different things. When uncorrelated strong models agree, they’re more likely than any single one to agree on the right answer.
Banking example. A serious credit risk modelling team at a major UK bank often runs a stacked model in production: a logistic regression for the regulatory grade, a gradient boosted tree for accuracy, and sometimes a small neural network for non-linear interactions, all combined via a logistic regression meta-model. The stacked model is more accurate than any base model, but the explainability layer focuses on the logistic regression component for regulatory documentation, with the other components feeding in adjustments. The architecture is more complex than a single model but the production performance justifies the complexity.
If asked: “When does stacking help, and when doesn’t it?”
How do you train a neural network with stable training?
Neural networks are powerful and finicky. Getting them to train well is an art that takes practice, and most of the practice is in the unglamorous middle ground between “the network compiles” and “the network is in production.” This section is the engineering wisdom for that middle ground.
Step one: get the data into the right shape. If your input is images, resize them all to the same dimensions and standardise the pixel values. The standard recipe is to subtract the mean and divide by the standard deviation, computed across the training set. Then optionally normalise to [0, 1]. Done consistently, this is a one-time fix that prevents many training problems.
If your input is text, tokenise it first: split into words, subwords, or characters. For older neural networks, each token gets a one-hot encoding (a vector of zeros with a single one at the position corresponding to the token’s vocabulary index). For modern networks, each token gets mapped to a learned word embedding, a dense vector that captures the token’s meaning. Word embeddings materially outperform one-hot encodings because they let semantically similar words have similar representations.
If your input is tabular features, do the standard preprocessing from Chapter 5: one-hot encode categoricals, standardise numericals, handle missing values. Same recipe as for any other model.
Step three: start small. Begin with one or two hidden layers and modest sizes. Train the model and see if it can fit the training data well. If the training error is too high (the model is too simple to capture the patterns), gradually increase the layer sizes and depth until you can reach a low training error. This is the “make it overfit first” strategy, and it works because it’s much easier to overfit a too-large model and then regularise it down than to start with a too-small model and try to expand it correctly.
Step four: regularise. Once the model fits the training data, check the validation error. If validation is much worse than training, the model is overfitting. Add regularisation: dropout, weight decay (L2), data augmentation, batch normalisation, early stopping. Each of these is a separate knob you can tune. Apply them one at a time and watch how validation responds.
Step five: iterate. Continue adjusting size, regularisation, learning rate, and other hyperparameters until both training and validation errors are acceptable. The discipline is to make one change at a time and measure its effect, not to make ten changes at once and try to attribute the result.
Read this as the neural network training cycle. Make it overfit. Then regularise it. Then tune. The order matters: starting with a heavily regularised small network and trying to grow it is much harder than starting with a slightly oversized network and pulling it back.
Where this fits in banking. When the Merehaven Bank team built the document classification component of the Merehaven Credit Workbench, they started with a tiny 2-layer MLP on top of pre-trained embeddings. It underfit. They added a third layer. Still underfit. They moved to a 4-layer transformer fine-tuning approach. Now it overfit. They added dropout (rate 0.3) and weight decay (1e-4). Now it generalised. Then they tuned the learning rate schedule and batch size to squeeze out the last few accuracy points. The whole iteration took about three weeks of real engineering time, even with experienced engineers. This is what training neural networks looks like in practice. Not magic. Not one-click. Iteration, measurement, judgement.
If asked: “How would you decide whether a neural network is the right choice for a banking ML problem?”
What regularisation tricks do neural networks need?
We’ve covered L1 and L2 regularisation (Chapter 5) and early stopping (Chapter 4). Neural networks have their own family of regularisation techniques that don’t apply to other models, and learning them is essential for any deep learning work.
Dropout. During training, randomly set a fraction of neuron outputs to zero on each forward pass. The fraction (the dropout rate) is typically 0.2 to 0.5. The randomness changes every batch, so different neurons are dropped each time. The effect is that the network cannot rely on any specific neuron always being available; it has to learn redundant representations spread across many neurons. At inference time, all neurons are used, but their outputs are scaled down to compensate for the higher activity. Dropout was invented by Hinton’s group at Toronto in 2012 and is one of the most reliable forms of neural network regularisation.
The intuition is to think of dropout as training an ensemble of subnetworks that share weights. Each forward pass uses a different subnetwork (because different neurons are dropped). At inference, you average the predictions of all these subnetworks implicitly through weight scaling. The ensemble effect reduces overfitting in the same way that random forest’s averaging reduces tree overfitting.
Read this as the training-versus-inference asymmetry. Dropout is active during training to force redundancy. It’s disabled at inference, with output scaling to keep the magnitudes consistent. This asymmetry is implemented automatically by every deep learning framework.
Early stopping. We covered this in Chapter 4. Save the model after each epoch (these saved versions are called checkpoints), monitor validation loss, and pick the checkpoint with the best validation performance as your final model. Early stopping is so reliable that some practitioners use it as their primary regularisation, leaving the model architecture unconstrained and letting early stopping prevent overfitting automatically. Others prefer to regularise architecturally and rely on early stopping only as a safety net. Both approaches work; the choice is taste and problem-specific.
Batch normalisation. Technically not a regularisation technique, but it has a regularisation effect that makes it a near-universal addition to deep neural networks. Batch normalisation was introduced by Ioffe and Szegedy at Google in 2015. The idea is to standardise (subtract mean, divide by standard deviation) the outputs of each layer before they reach the next layer, using statistics computed across the current minibatch. This keeps the distribution of activations stable as the network trains, which has several beneficial effects: faster convergence, less sensitivity to learning rate, and a mild regularisation effect because each minibatch uses slightly different statistics, adding noise.
Data augmentation. Create synthetic training examples by perturbing existing ones. For images, the standard set includes random crops, rotations, flips, colour jittering, slight blurring, and brightness changes. For text, you can swap synonyms, paraphrase, or back-translate (translate to another language and back, getting a slightly different version of the original). For tabular data, you can add small Gaussian noise to numerical features.
The magic of data augmentation is that you effectively grow your training set without collecting new labels. A model trained on augmented images learns that the label is invariant to small changes in lighting, orientation, and position, which makes it well-tested to those variations in production. For computer vision tasks at modest data volumes, data augmentation is the difference between a model that works and a model that doesn’t, and pre-training on augmented data is one of the techniques that lets fine-tuned vision models work on small bank datasets like cheque images and signature samples.
If asked: “What’s the difference between dropout and L2 regularisation?”
How do you build a model that takes both an image and a text?
Real banking systems often have multiple kinds of input. The cheque image plus the metadata about who issued it. The transaction record plus the merchant description. The credit memo text plus the historical financial features of the customer. The conversation log plus the customer’s product holdings. Each of these is a multimodal input, and traditional models built for one modality don’t naturally handle them.
Two approaches.
Approach one (for tabular models): concatenate the feature vectors. Vectorise each input separately using whatever feature engineering is appropriate for that modality, then concatenate the vectors into a single wide feature vector that you feed into a normal classifier. If your image features are [i(1), i(2), i(3)] (perhaps from a pre-trained CNN) and your text features are [t(1), t(2), t(3), t(4)] (perhaps from TF-IDF or a sentence embedding), the concatenated input is [i(1), i(2), i(3), t(1), t(2), t(3), t(4)]. This works for any classifier that takes a fixed-length vector input, including logistic regression, SVM, and gradient boosted trees.
The downside is that this approach treats the two modalities as independent feature sets without interaction. It can work if the feature engineering is rich enough, but it leaves on the table the possibility that the image and text contain related signal that should be combined more intelligently.
Approach two (for neural networks): subnetworks per modality, fused at the embedding layer. Build a separate subnetwork for each modality. For an image input, use a CNN. For text, use an RNN or transformer. For tabular features, use an MLP. Each subnetwork ends in an embedding layer that produces a fixed-size vector representing the input. Concatenate the embedding vectors and feed the combined vector into a final classification or regression layer.
Read this as the multimodal fusion pattern. Each modality has its own preprocessing and its own subnetwork producing an embedding. The embeddings meet at the concatenation point, and downstream layers learn to combine them for the final prediction. The whole network is trained end to end with one loss, so each subnetwork learns to produce embeddings that help the final task.
Banking example. A trade finance fraud detection system at a major UK bank processes shipping documents that include both images (scanned bills of lading) and structured fields (consignee, value, port codes, dates). A multimodal neural network with a CNN subnetwork for the images and an MLP for the structured data, fused at the embedding layer, outperforms either approach alone because the model can learn that suspicious patterns sometimes appear in the image (forged stamps, altered amounts) and sometimes in the structured fields (impossible routes, sanctions list matches), and sometimes in correlations between the two.
If asked: “How would you design a neural network that takes both an image and a text description?”
How do you train a model to predict several things at once?
Sometimes you want one model to produce multiple outputs. The image classifier that predicts both the bounding box of an object and the class label. The credit memo analyser that predicts both the customer rating change and the recommended action. The trade processing model that predicts both the trade type and the urgency. These are multi-task learning problems, and they’re surprisingly common in regulated ML systems.
The simple case: outputs of the same nature. If your outputs are all of the same kind (e.g., several binary tags), you can use the multi-label classification techniques from Chapter 7. One sigmoid output per label, binary cross-entropy loss, train as a single network.
The harder case: outputs of different natures. When the outputs are heterogeneous, say, a regression target and a classification target, you need to handle them with different loss functions. The pattern is to use a shared encoder that processes the input and produces a common embedding, then attach two (or more) separate output heads, each with its own loss function. The total loss is a weighted sum of the per-head losses:
Ltotal = γL1 + (1 − γ)L2
where γ is a hyperparameter in (0, 1) controlling the trade-off between the two tasks. If γ is close to 1, the model focuses on task 1 at the expense of task 2. If γ is close to 0, the opposite. The right value is found by validation.
Read this as the multi-task pattern. The shared encoder forces the model to learn representations useful for both tasks, which often acts as an implicit regularisation: the embedding has to be informative enough for both tasks, which prevents overfitting to either alone. The output heads are simple and task-specific. The combined loss balances the tasks during training.
Why does multi-task learning often help both tasks? Because the shared encoder benefits from the joint signal. If task 1 has more data than task 2, the shared encoder learns rich features from task 1 that also help task 2. If the two tasks share underlying structure, the model can transfer that structure across them. Multi-task learning is one of the few “free lunches” in deep learning: you get two models for slightly more than the cost of one, and they often outperform two separate single-task models.
Banking example. Imagine a customer health model In the synthetic Merehaven case that predicts both the probability of churn in the next 90 days (classification) and the expected change in product holdings over the same period (regression). A multi-task neural network with a shared customer embedding and two output heads is more parameter-efficient than two separate models, and often more accurate because the shared embedding is forced to capture customer state in a way that’s useful for both tasks. The trade-off hyperparameter γ is tuned on validation to balance the two business priorities.
If asked: “When would you use multi-task learning?”
How do you reuse what a model already knows?
This is the technique that, more than any other, has shaped modern AI. Transfer learning is the process of taking a model trained on one task and adapting it to a related but different task. It is the reason you can fine-tune a foundation model on a few thousand examples and get strongest evaluated results, and it’s the unique advantage that neural networks have over classical ML methods for many problems.
- Build a deep model on the original big dataset (wild animals).
- Compile a much smaller labelled dataset for your new task (domestic animals).
- Remove the last one or several layers from the original model. These are typically the classification layers, which are specific to the original task.
- Replace the removed layers with new layers adapted for your new task. For domestic animal classification, this would be a new softmax layer with the right number of output classes.
- Freeze the parameters of the layers remaining from the original model. They will not be updated during training.
- Train only the new layers using your smaller dataset and gradient descent.
The frozen pretrained layers act as a fixed feature extractor. The new layers learn to map those features to the new task’s labels. Because most of the parameters are frozen, you need much less data to train successfully.
Read this as the basic transfer learning flow. You take advantage of the work already done by the original model and only retrain the parts that matter for your new task.
Variants and extensions.
Fine-tuning all layers. Instead of freezing the original layers, allow them to update with a small learning rate while training the new layers with a normal learning rate. This lets the original features adapt slightly to the new task, often giving slightly better results at the cost of needing slightly more data and being slightly more prone to overfitting.
Layer-wise unfreezing. Start by training only the new layers with the original layers frozen. Then progressively unfreeze layers from top to bottom, training each round briefly. This is the technique used by methods like ULMFiT for text classification.
Feature extraction without retraining. In the simplest possible version, you don’t even add new layers. You just use the pre-trained network as a black box that produces embeddings, then train a classical ML model (like logistic regression or gradient boosted trees) on those embeddings. This is fast and works well when the pre-trained model’s representations are already excellent, which is often the case for foundation models in the reference implementation.
Why transfer learning matters so much in banking. Because labelled data is always scarce in regulated banking. You can’t crowdsource credit decisions. You can’t pay random people to label confidential customer documents. The labels you have are precious and expensive. Transfer learning lets you leverage models trained on huge amounts of public data and adapt them to your bank-specific tasks with relatively small labelled datasets. Every NLP project In the synthetic Merehaven case in the reference implementation starts from a pre-trained foundation model from a managed training service a managed model catalogue and fine-tunes it via LoRA on a small bank-specific corpus. In the synthetic Merehaven case, the equivalent uses a managed training service a managed model catalogue and a managed model catalogue for managed inference. Without transfer learning, these projects would be impossible.
The Merehaven Bank Merehaven Credit Workbench is a transfer learning system. The base model is a foundation language model pre-trained on enormous text corpora. The Merehaven Bank team takes this base model and fine-tunes it on banking-specific text: credit memos, customer correspondence, policy documents, internal training materials. The fine-tuning uses materially less data than the original pre-training (perhaps tens of thousands of examples versus hundreds of billions of tokens) and produces a model that knows both general language and banking-specific content. The whole product would be infeasible without transfer learning.
If asked: “How does transfer learning differ from training from scratch?”
How do you make your code fast enough to run in production?
Algorithms are useless if they’re too slow to use. A fraud detection model that takes 200 milliseconds per transaction can’t run in the authorisation hot path (which has a budget of perhaps 100 milliseconds total). A credit decisioning model that takes ten seconds can’t run in a real-time mortgage application flow. Engineering efficiency is part of shipping models, not an optional add-on.
The first principle: know your complexity. The big O notation tells you how the running time of an algorithm grows with the size of the input. An algorithm whose running time is roughly proportional to the input size N is O(N). An algorithm whose running time is proportional to N2 is O(N2). An algorithm whose running time is log N is O(log N), which is materially faster than O(N) for large N because logarithms grow very slowly.
def find_max_distance(S):
result = None
max_distance = 0
for x1 in S:
for x2 in S:
if abs(x1 - x2) >= max_distance:
max_distance = abs(x1 - x2)
result = (x1, x2)
return resultThis makes N2 comparisons, so its complexity is O(N2). For N = 1, 000, that’s a million comparisons, which is fast. For N = 1, 000, 000, that’s a trillion comparisons, which is unbearably slow.
The smarter algorithm does the same thing in one pass: find the minimum and maximum, and the most distant pair must be those two.
def find_max_distance(S):
result = None
min_x = float("inf")
max_x = float("-inf")
for x in S:
if x < min_x:
min_x = x
elif x > max_x:
max_x = x
return (max_x, min_x)This algorithm makes one pass over the data, so its complexity is O(N). For N = 1, 000, 000, it’s a million operations, millions of times faster than the O(N2) version. Same problem, two solutions, vastly different performance. Big O thinking helps you spot these opportunities before they bite you in production.
An algorithm is called efficient if its complexity is polynomial in the input size. Both O(N) and O(N2) are polynomial and therefore efficient in the formal sense, but in the big-data era you often need O(N) or even O(log N) to handle realistic data volumes. For machine learning In the synthetic Merehaven case-scale data (hundreds of millions of customers, billions of transactions), the difference between O(N) and O(N2) is the difference between a feasible system and an infeasible one.
Read this as the complexity hierarchy. Logarithmic is best, linear is good, quadratic is acceptable for small data, anything higher becomes problematic fast. When designing or reviewing an algorithm, the first question to ask is “what’s the complexity?” and the second is “is that fast enough for the data sizes we’ll see?”
The classic example: computing a dot product. Don’t do this:
wx = 0
for i in range(N):
wx += w[i] * x[i]Do this instead:
import numpy
wx = numpy.dot(w, x)Why is the second one so much faster? Because NumPy’s
dot function is implemented in C and uses CPU vector
instructions (SIMD) to compute many multiplications and additions at
once. The Python loop, by contrast, executes one operation per iteration
in the interpreter, which has enormous per-operation overhead. The
difference is often 100x to 1000x in real benchmarks.
Vectorisation is the practice of replacing explicit
loops with library calls that operate on whole arrays at once, and it’s
the single most important optimisation for Python-based ML code.
The third principle: use the right data structures. A common Python performance bug is to store a collection in a list and then check membership repeatedly. Lists have O(N) membership checks: Python has to scan the whole list to see if an element is in it. Sets and dictionaries have O(1) membership checks: they use hash tables. Switching from a list to a set when membership matters can take a function from minutes to milliseconds.
# Slow: O(N) per lookup
training_ids = [1, 2, 3, ..., 1_000_000]
if customer_id in training_ids: # scans the whole list
...
# Fast: O(1) per lookup
training_ids = set([1, 2, 3, ..., 1_000_000])
if customer_id in training_ids: # hash lookup
...The fourth principle: profile before optimising. The cProfile module in Python (and equivalent profilers in other languages) tells you exactly where your code is spending time. Without profiling, you optimise the wrong things. With profiling, you optimise what matters. Always profile before assuming you know where the bottleneck is.
Beyond Python. When pure Python plus libraries isn’t
fast enough, you can use the multiprocessing module to run
computations in parallel across CPU cores. Or you can use compilers like
PyPy, Numba, or Cython to compile Python code into fast machine code.
For hot inner loops that need maximum performance, writing the critical
part in C++ or Rust and calling it from Python is a common pattern. Most
regulated ML ML systems have a few small hot paths written in compiled
languages, with the rest in Python for productivity.
If asked: “How would you make a slow Python ML pipeline faster?”
Find structure without pretending it is truth
How do you reverse-engineer the curve that produced your data?
Density estimation is the simplest unsupervised problem to state and one of the most useful in practice. You have a sample of points drawn from some unknown probability distribution. You want to estimate the shape of the distribution itself, so that for any new point you can compute how likely it would have been to come from the same source. The answer is a function that takes a point and returns its density.
Why would you want this? The clearest banking use case is anomaly detection. If you have a model of “what does normal credit card spending look like for customer X,” then you can score every new transaction by how dense the distribution is at that transaction’s location. Low density means the transaction is in a region of feature space the customer rarely visits, which is a fraud signal. High density means it looks like business as usual. The whole apparatus is built on density estimation.
We’ve seen one approach already: in Chapter 7, the one-class Gaussian fit a single multivariate Gaussian to the data and used its density as the model. That’s parametric density estimation: you assume a particular family of distributions (Gaussian) and fit its parameters from data. The downside is that real data is rarely shaped like a Gaussian. If your data is multimodal or skewed or has heavy tails, the Gaussian assumption is wrong and the resulting density model is inaccurate.
Kernel density estimation (KDE) is the non-parametric alternative. Instead of assuming a global shape for the distribution, you build the density up from the local contributions of each training point. For each point in the dataset, place a small “bump” centred at that point. Sum up all the bumps to get the overall density. The density at any query location is just the sum of the bump values from all the training points contributing to that location.
The math is a direct analogue of kernel regression from Chapter 7:
The kernel function k is typically a Gaussian:
The bandwidth b controls the smoothness of the resulting density. Small bandwidth means narrow peaked bumps that cling to each training point, overfit, jagged density. Large bandwidth means wide flat bumps that average across many points, underfit, oversmoothed density. Choosing b is the central decision.
Read this as the KDE pipeline. Bumps go on points, bumps sum to density, bandwidth controls smoothness, density at a query point is your estimate.
MISE(b) = 𝔼[∫ℝ(f̂b(x) − f(x))2 dx]
Read this carefully. We square the difference between the estimated and true densities, integrate over all values of x (because the density is defined on a continuous domain), and take the expectation over different possible training samples. We can’t compute this directly because we don’t know the true f. But we can rewrite it and approximate it using only quantities we can compute from the data, including a leave-one-out estimator. The result is a cost function we can minimise via grid search to find the best bandwidth b*.
In practice, you don’t usually do this by hand. Libraries like scikit-learn implement KDE with bandwidth selection built in (typically using rules of thumb like Scott’s rule or Silverman’s rule, which give reasonable default bandwidths based on data size and variance). But knowing the underlying logic helps you understand when the defaults will fail and how to do something smarter.
Where this fits in banking. KDE is the core technique behind many “what does normal look like” models. For example, suppose you want a system that flags when a customer’s monthly spending pattern starts to look anomalous. Build a KDE of the customer’s historical monthly spend distribution (or, more usefully, the joint distribution over a few summary features like total spend, spend variability, and merchant diversity). Score each new month’s data point against the KDE. Low density means anomalous. The system catches subtle drift that simple threshold rules would miss.
Where this goes wrong. KDE suffers severely in high dimensions. The number of training points needed to estimate a density well grows exponentially with the number of dimensions (this is the curse of dimensionality applied to density estimation). For more than maybe 5-10 dimensions, KDE gives unreliable results unless you have a huge dataset. The fix is to reduce dimensionality first (with PCA or UMAP, coming up later in this section) and then do KDE in the reduced space, or to use parametric methods like Gaussian mixtures that scale better with dimension at the cost of stronger assumptions.
If asked: “When would you use kernel density estimation instead of fitting a Gaussian?”
How do you find groups in your data without supplied groups?
Clustering is the most famous unsupervised technique and the one that data scientists encounter first. The problem is simple to state: given a dataset with no labels, partition it into groups such that points in the same group are similar to each other and points in different groups are dissimilar. The trouble is that “similar” and “dissimilar” are not precisely defined, the right number of groups is rarely obvious, and different algorithms give different answers on the same data. Clustering is more art than science, and the art lies in matching the algorithm and the validation strategy to the problem at hand.
There is a variety of clustering algorithms, and unfortunately, no algorithm is universally best. The performance of each depends on the unknown properties of the distribution your data was drawn from. We’ll cover the most useful ones.
k-Means: the workhorse
k-Means is the simplest and most widely used clustering algorithm. Stuart Lloyd at Bell Labs proposed it in 1957 (yes, a Lloyd, but no relation to Merehaven Bank), though the paper wasn’t published until 1982. The algorithm is extraordinarily simple:
- Choose k, the number of clusters you want.
- Place k initial centroids randomly in the feature space.
- Assign each data point to its nearest centroid.
- For each centroid, recompute its location as the mean of all points assigned to it.
- Repeat steps 3 and 4 until the assignments stop changing.
The algorithm converges (in the sense of stable assignments) typically within 10-50 iterations. The output is a set of centroids and a cluster ID for each training point. To classify a new point, find its nearest centroid.
Read this as the k-means iteration. Each round of “assign then recompute” makes the within-cluster spread strictly smaller, so the algorithm always converges. The catch is that it converges to a local minimum that depends on where the initial centroids were placed.
A worked example by intuition. Suppose you have 100 customers in two dimensions (DSCR and LTV, say) and you want k = 3 clusters. Place three centroids randomly. Each customer is assigned to whichever centroid is closest. Now you have three groups. Compute the average DSCR and LTV of each group, and move the centroid to that average. Reassign customers to their now-nearest centroids; some will switch groups. Recompute the means. Continue. After several rounds, the centroids stop moving and the assignments stop changing. You have three clusters defined by their centres.
The big practical question is choosing k, the number of clusters. There is no universally correct answer. You can try different values and pick the one that “looks right” on a 2D plot, but for high-dimensional data this is impossible. We’ll come to a more principled approach in a moment.
The other practical question is initial centroid placement, which affects the final result because k-means converges to a local minimum. Two runs with different initial centroids can produce two different clusterings. The standard fix is to run k-means many times with different random initialisations and keep the result with the lowest within-cluster spread. The smarter fix is k-means++, which chooses initial centroids by a procedure that spreads them out across the data, materially reducing the dependence on randomness.
Strengths and weaknesses. K-means is fast, simple, and scales to huge datasets. It assumes clusters are roughly spherical and roughly equal in size, which is fine for many problems and a poor fit for others. It cannot handle non-convex clusters (a cluster shaped like a banana will be split into pieces). It’s sensitive to the choice of k and to outliers (a single far-out point can pull a centroid in a misleading direction). Despite all these caveats, k-means is the right starting point for almost any clustering task because it’s so fast and gives a baseline against which to measure more sophisticated methods.
DBSCAN and HDBSCAN: density without centroids
K-means is centroid-based: it represents each cluster by a central point and assigns examples to the nearest. DBSCAN (density-based spatial clustering of applications with noise) takes a fundamentally different approach. Instead of centroids, it uses local density: a cluster is a region of space where points are densely packed, and gaps in density are the natural boundaries between clusters.
The algorithm has two hyperparameters: ϵ (a distance threshold) and n (a minimum count). The procedure:
- Pick a random unassigned point x. Assign it to a new cluster.
- Find all points within distance ϵ of x, its ϵ-neighbours.
- If there are at least n neighbours, add them all to the same cluster.
- For each newly added point, find its ϵ-neighbours. If there are at least n, add them too.
- Continue expanding until no more points can be added.
- Pick another unassigned point and start a new cluster. Repeat until all points are assigned.
- Points that never end up in any cluster (because they don’t have enough ϵ-neighbours and aren’t reached by expansion) are marked as outliers.
The two hyperparameters control different things. ϵ controls how close together points need to be to count as connected. n controls how dense a region needs to be to be considered a cluster (rather than noise).
The key advantage over k-means is that DBSCAN can find clusters of arbitrary shape. A cluster doesn’t have to be spherical; it just has to be a connected region of high density. Crescent-shaped clusters, ring-shaped clusters, or any other topology can be found. The algorithm also automatically identifies outliers as points that don’t belong to any cluster, which is valuable when your data has noise and you don’t want to force every point into a group.
The disadvantage is that the two hyperparameters (especially ϵ) are hard to choose, and a fixed ϵ doesn’t work well for clusters of varying density. A region that’s dense for one cluster might be sparse for another, and a single ϵ can’t accommodate both.
Read this as the algorithmic split in clustering. Centroid-based methods are fast and assume clusters are roughly round. Density-based methods are slower but find clusters of arbitrary shape and handle noise. Modern practice in banking analytics often starts with HDBSCAN and falls back to k-means when speed matters more than flexibility.
Determining the number of clusters: prediction strength
The idea is to use cross-validation. Split your data into a training set and a test set, just like supervised learning. For each candidate value of k:
- Run a clustering algorithm on the training set, producing clustering A = C(Str, k).
- Run the same clustering algorithm separately on the test set, producing C(Ste, k).
- For each pair of test points that ended up in the same cluster according to C(Ste, k), check whether they also end up in the same cluster according to A (which we apply to the test points by assigning each to its nearest training centroid or region).
- The proportion of within-cluster test pairs that survive this check, minimised across the test clusters, is the prediction strength ps(k).
Formally:
where DA, Ste = 1 if examples i and i′ from the test set belong to the same cluster according to clustering A, and 0 otherwise. Don’t be put off by the notation. The intuition is just: if two test points are in the same cluster in the test-set-only clustering, are they also in the same cluster in the training-set-only clustering?
The interpretation. A high ps(k) (close to 1) means the clustering is consistent across training and test data: the same number of clusters captures the structure both sides of the split. A low ps(k) means the clustering is unstable: training and test see different things, which suggests k is wrong. Tibshirani and Walther found empirically that the largest k such that ps(k) > 0.8 is usually a reasonable choice.
For non-deterministic algorithms like k-means, you should run the clustering several times for each k with different random initialisations and average the prediction strengths. This reduces the noise from initialisation luck.
Other methods exist: the gap statistic, the elbow method (look at within-cluster variance versus k and find where the curve “elbows”), the average silhouette method (a per-point measure of how well-clustered each point is). None of them is universally reliable. Prediction strength has the advantage of being principled: it explicitly tests cluster stability rather than relying on visual judgement.
Read this as the prediction strength workflow. It’s the cross-validation analogue for clustering, and it’s the right answer to “how many clusters?” when you need a principled choice.
Gaussian mixture models: soft clusters with shape
K-means and DBSCAN both produce hard clusterings: every point belongs to exactly one cluster. Gaussian mixture models (GMMs) produce soft clusterings: each point has a membership probability for each cluster, summing to 1 across clusters. This is more informative when you care about ambiguity, and it captures the natural fact that worked data points often sit between clear clusters rather than belonging cleanly to one.
A GMM models the data as a weighted sum of several Gaussian distributions:
where fμj, Σj is a multivariate Gaussian with mean μj and covariance Σj, and ϕj is the weight of that Gaussian in the mixture (the ϕj values must sum to 1 across all j, since they represent the proportion of data drawn from each component). The total number of parameters scales with k: each component has its own mean, covariance, and weight.
To fit a GMM, you use the expectation-maximisation (EM) algorithm, which alternates between two steps:
E-step (expectation): Given current estimates of the parameters, compute for each training point and each component the probability that the point came from that component. These are the soft assignments.
M-step (maximisation): Given the current soft assignments, update each component’s parameters (mean, covariance, weight) by maximum likelihood, weighting each training point by its assignment probability for that component.
Repeat E and M until the parameters stabilise.
In the E-step, for each training point xi and each component j ∈ {1, 2}, compute the probability the point came from component j using Bayes’ rule:
The denominator is the total probability of seeing xi under the full mixture. The numerator is the contribution from component j. The ratio is the posterior probability that xi belongs to component j.
In the M-step, update the component parameters using these soft assignments:
The new mean of component j is the weighted average of the data, with weights equal to each point’s posterior probability of belonging to component j. The new variance is computed similarly. The new weight ϕj is the average posterior probability across all training points.
The connection to k-means. EM for GMM is structurally identical to k-means: alternate between assigning points to clusters and updating cluster parameters. The only difference is that GMM uses soft assignments (each point partially belongs to multiple clusters with weights summing to 1) while k-means uses hard assignments (each point belongs to exactly one cluster). In fact, k-means can be derived as a special case of GMM where the covariance matrices are constrained to be spherical and equal, and the soft assignments are converted to hard at every iteration.
GMM vs. k-means in practice. GMM clusters can be elliptical (with arbitrary elongation and rotation), so they handle data where features are correlated within clusters or where clusters are stretched in particular directions. K-means clusters are forced to be spherical, which is a poorer fit for real data. The cost of GMM’s flexibility is more parameters to fit (full covariance matrices instead of just centroids) and a slower training procedure. For most banking clustering tasks where the natural cluster shapes are not spherical, GMM gives meaningfully better results.
This is the GMM analogue of prediction strength: use a held-out set to choose the right complexity, avoiding the overfit you’d get by maximising likelihood on the training set itself.
Other clustering algorithms worth knowing. Spectral clustering treats the data as a graph and finds clusters by analysing the graph’s spectral properties; it’s good for non-convex cluster shapes. Hierarchical clustering builds a tree of nested clusterings, letting you pick the level that best matches your needs; it’s interpretable and useful when you want to see relationships between clusters at different granularities. For most practical banking work, k-means, HDBSCAN, and GMM cover the space well.
If asked: “When would you use a Gaussian mixture model instead of k-means?”
How do you take 300 features and turn them into 2 you can plot?
Modern banking datasets routinely have hundreds of features per customer. You can train models on them. You cannot visualise them or understand them at a glance. This is where dimensionality reduction comes in: techniques that take high-dimensional data and produce a lower-dimensional representation while preserving as much of the meaningful structure as possible.
There are several reasons to reduce dimensionality. First, visualisation: humans cannot interpret more than three dimensions on a screen, so seeing the structure of high-dimensional data requires projecting it down. Second, interpretable models: simple algorithms like decision trees and linear regression are easier to interpret on low-dimensional inputs. Third, noise reduction: high-dimensional data often has redundant or correlated features, and removing them can improve downstream model performance. Fourth, embedding-based search: modern similarity-search systems for products, customers, or documents work by embedding inputs into a fixed-dimensional vector space and using nearest-neighbour search.
Three widely used techniques: PCA, UMAP, and autoencoders.
Principal Component Analysis (PCA)
PCA is the oldest and most widely understood dimensionality reduction technique, dating back to Karl Pearson in 1901. The intuition is simple. Think of your high-dimensional data as a cloud of points. There’s some direction in space along which the points are most spread out, the direction of greatest variance. That direction is the first principal component. The next most-spread-out direction, perpendicular to the first, is the second principal component. And so on.
Mathematically, the principal components are the eigenvectors of the data’s covariance matrix, sorted by eigenvalue (largest first). Each eigenvector defines an axis of the new coordinate system, and each eigenvalue tells you how much variance is captured along that axis. To reduce the dimension from D to Dnew, you keep only the top Dnew principal components and project your data onto them.
Read this as the PCA pipeline. The eigenvectors of the covariance matrix become the new coordinate axes, and you keep the ones that explain the most variance. PCA is fast, deterministic, and has a closed-form solution (no gradient descent needed). It’s the right starting point for dimensionality reduction whenever the linear structure of the data is enough.
A concrete numerical intuition. Suppose you have customer data with 300 features. Run PCA. Look at how much variance each principal component explains. If the first 20 components together explain 95% of the variance, you’ve effectively reduced your data from 300 dimensions to 20 with minimal information loss. The remaining 280 components capture only 5% of the variance, they’re mostly noise, redundancy, or details that don’t matter for downstream tasks.
Where PCA wins. When the data has linear structure (correlations between features that PCA can capture), PCA is fast, interpretable, and effective. It’s the standard preprocessing step for many older ML methods that struggle with high-dimensional input.
Where PCA loses. When the data has non-linear structure that can’t be captured by linear projections. PCA cannot find a curved manifold; it can only find linear subspaces. For data that lies on a non-linear manifold (which is common with images, text, and many customer-behaviour features), PCA either misses important structure or captures it inefficiently. The next two methods are designed for this case.
UMAP: non-linear dimensionality reduction for visualisation
UMAP (uniform manifold approximation and projection), introduced by Leland McInnes in 2018, is one of the most popular non-linear dimensionality reduction techniques in current practice. It’s specifically designed for visualisation: take high-dimensional data and produce a 2D or 3D layout where similar points are close together and dissimilar points are far apart.
The intuition is that UMAP first defines a “fuzzy graph” of the high-dimensional data, where each pair of points has a similarity score that depends on their distance and the local density around them. Points that are close in dense regions are highly similar; points that are far in sparse regions are not. Then UMAP looks for a low-dimensional layout (typically 2D) whose own fuzzy graph matches the high-dimensional one as closely as possible, measured by a fuzzy set cross-entropy loss.
The high-dimensional similarity is defined as:
w(xi, xj) = wi(xi, xj) + wj(xj, xi) − wi(xi, xj)wj(xj, xi)
where the directed similarity is:
with d the Euclidean distance, ρi the distance from xi to its nearest neighbour, and σi the distance from xi to its k-th nearest neighbour. The number of neighbours k is the main hyperparameter.
This produces a similarity in [0, 1] that captures local density: in regions where points are tightly packed, the threshold σi is small, so even slightly-distant neighbours have low similarity; in sparse regions, σi is large, so more-distant neighbours still have moderate similarity. This adaptive scaling is why UMAP handles non-uniform data densities well.
The low-dimensional layout is found by minimising the fuzzy set cross-entropy between the high-dimensional and low-dimensional similarity graphs:
The optimisation variables are the low-dimensional positions xi′, which UMAP learns by gradient descent. Don’t memorise the formula. The intuition is just: find low-dimensional positions whose pairwise similarities match the high-dimensional similarities as closely as possible, using cross-entropy as the matching measure.
Where UMAP fits in banking. Visualisation of customer segments after clustering. Embedding products or transactions into a 2D map for exploration. Building intuition about the structure of a new dataset before modelling. UMAP is rarely used as a feature for production models (because it’s stochastic and slow at inference time), but it’s the standard tool for the discovery phase of an ML project.
Autoencoders for dimensionality reduction
We met autoencoders in Chapter 7 when we covered semi-supervised learning. Recall that an autoencoder is a neural network with an hourglass shape: an encoder that compresses the input through a bottleneck layer, and a decoder that reconstructs the input from the bottleneck representation. The training objective is to minimise the reconstruction error, with no labels needed.
For dimensionality reduction, you train the autoencoder on your high-dimensional data, then discard the decoder and use the encoder as your dimensionality reducer. The bottleneck layer’s output is the low-dimensional representation. The network has learned, through the reconstruction objective, to compress the input into a representation that preserves the information needed to rebuild it.
Autoencoders are non-linear (because of the activations), so they can capture structure that PCA misses. They are deterministic (unlike UMAP), so they can be used as fixed feature extractors for downstream models. They scale to very large datasets because they’re trained by gradient descent, not by computing eigenvectors of huge matrices.
The trade-offs. Autoencoders are slower to train than PCA and require more hyperparameter tuning. The bottleneck layer dimension is a hyperparameter you have to choose. The architecture (number of layers, layer sizes) needs design judgement. And the resulting representation is harder to interpret than PCA’s principal components, because each dimension of the bottleneck is some learned non-linear combination of the inputs that doesn’t have an obvious meaning.
Variational autoencoders (VAEs) add a probabilistic structure to the bottleneck layer, requiring it to be drawn from a Gaussian distribution. This regularises the embedding space and makes it possible to generate new data by sampling from the learned distribution. VAEs are widely used in modern generative AI as the dimensionality-reduction component, though for pure dimensionality reduction without generation, regular autoencoders are usually enough.
Comparison summary.
Read this as the menu of dimensionality reduction options. PCA when speed and simplicity matter. UMAP when visualisation matters. Autoencoders when you want a learnable, deterministic, non-linear feature extractor for downstream models. In a typical Merehaven Bank analytics project, you might use all three at different stages: PCA for initial exploration, UMAP for the visualisation that goes in the slide deck, and an autoencoder if you want a stable embedding to feed into a downstream supervised model.
If asked: “When would you use UMAP instead of PCA?”
How do you find the data points that don’t belong?
Reconstruction-based outlier detection with autoencoders. Train an autoencoder on your data. For each point, compute the reconstruction error: the difference between the original input and the autoencoder’s output. Points with high reconstruction error are outliers. Why? Because the autoencoder has learned to reconstruct typical points well, and unusual points can’t be reconstructed accurately because the network never saw anything like them in training. The reconstruction error is essentially a measure of how “typical” a point is.
One-class classification. From Chapter 7. Train a one-class Gaussian, one-class SVM, or similar method on the data. For each new point, compute its score under the model. Points with low scores are outliers. The advantage of one-class classifiers over autoencoders is that they’re often faster to train and easier to tune; the advantage of autoencoders is that they handle very high-dimensional non-linear data better.
Isolation forest is another popular outlier detection method that we haven’t formally covered, but is worth mentioning. It builds many random trees that try to isolate individual points by random splits. Outliers tend to be isolated quickly (in few splits) because they’re far from the bulk of the data; normal points need many splits to isolate. The average isolation depth across the forest is the outlier score. Isolation forest is fast, scales well, and is the workhorse of many production anomaly detection systems including some at major UK banks.
The Merehaven Bank AML example. A modern AML system at the synthetic Merehaven case uses a layered approach. Isolation forest provides the first-pass outlier scoring across all transactions. Top-scoring transactions go through one-class SVMs trained per customer segment for refinement. The most suspicious then get a deep learning model evaluation (using reconstruction error from a customer-specific autoencoder), and the final shortlist goes to human analysts. Each layer filters the volume by an order of magnitude, keeping the operational cost manageable.
Make similarity and ranking accountable
How do you teach a model what “similar” means to you?
Throughout this edition we’ve used Euclidean distance and cosine similarity as if they were obvious choices. They aren’t. They’re conventions, picked because they’re mathematically convenient and often work well enough. The fact that one metric works better than another for a given dataset is a clue that neither is perfect: the right metric depends on what “close” means in your domain, and your domain has its own notion of similarity that the default metrics don’t capture.
The basic idea. Take Euclidean distance and parametrise it. Recall that the squared Euclidean distance between two points x and x′ is:
d(x, x′)2 = (x − x′)⊤(x − x′)
We can generalise this by inserting a learned matrix A between the two difference vectors:
This is called the Mahalanobis distance, and the matrix A is the parameter we want to learn. If A is the identity matrix, dA reduces to Euclidean distance. If A is a diagonal matrix with different values on the diagonal, then different dimensions get different weights in the distance computation. If A is fully populated, the distance can capture interactions between dimensions: two points might be close in raw coordinates but far in the learned distance because the matrix has rotated the space.
A concrete numerical intuition. Suppose your features are 3-dimensional and you set:
Then a unit difference in dimension 2 contributes 8 to the squared distance, four times as much as a unit difference in dimension 1 (which contributes 2) and eight times as much as a unit difference in dimension 3 (which contributes 1). The matrix encodes that dimension 2 is the most important for measuring similarity in your problem. By learning this matrix from data, you’re letting the data tell you which dimensions matter most for your notion of similarity.
The constraints. For dA to be a valid metric, it has to satisfy three conditions:
- Non-negativity. d(x, x′) ≥ 0 for all pairs.
- Triangle inequality. d(x, x′) ≤ d(x, z) + d(z, x′) for all triples.
- Symmetry. d(x, x′) = d(x′, x).
To satisfy the first two, the matrix A has to be positive semidefinite. This is the matrix generalisation of “non-negative real number.” Formally, A is positive semidefinite if z⊤Az ≥ 0 for any vector z. When A is positive semidefinite, the Mahalanobis distance is well-defined and behaves like a real distance. To satisfy symmetry, you can take the average (d(x, x′) + d(x′, x))/2, but for a positive semidefinite matrix this is automatic.
How do you train A? You need supervision. Specifically, you need pairs of examples that you consider similar (call this set S) and pairs you consider dissimilar (call this set D). The training data is hand-curated: you decide, for some sample of pairs from your dataset, whether they’re similar or dissimilar according to your domain notion. Then you want to find a matrix A that makes the similar pairs close and the dissimilar pairs far apart:
minA∑(xi, xk) ∈ S∥xi − xk∥A2 such that ∑(xi, xk) ∈ D∥xi − xk∥A ≥ c
where c is some positive constant. The objective minimises the sum of squared distances on similar pairs (pulling them together) subject to the constraint that the sum of distances on dissimilar pairs is at least c (pushing them apart). The optimisation is solved by gradient descent with a projection step that keeps A positive semidefinite.
Read this as the metric learning workflow. You provide judgment about which pairs are similar and dissimilar; the algorithm learns a distance that respects your judgments.
Connection to siamese networks and triplet loss. Recall from Chapter 7 that one-shot learning with siamese networks and triplet loss is essentially metric learning under a different name. The pairs of similar items belong to set S (anchor and positive), and pairs of dissimilar items belong to set D (anchor and negative). The triplet loss is the neural network equivalent of the Mahalanobis-distance optimisation, with the metric implicitly defined by the encoder network rather than an explicit matrix. Both approaches solve the same fundamental problem: learn a distance that matches a domain-specific notion of similarity.
Where metric learning fits in banking. Customer similarity for relationship management: you might want a distance metric where customers are considered similar if they have similar product holdings and similar risk profiles, regardless of whether their raw features are close. Document similarity for compliance: you want documents to be considered similar if they discuss similar regulatory topics, regardless of surface vocabulary. Fraud pattern matching: you want transactions to be considered similar if they share fraud-relevant features, weighted by how predictive each feature is. In all these cases, the off-the-shelf Euclidean distance gives you something but not the right thing, and a learned metric gives you something tailored to your domain.
If asked: “When would you reach for metric learning?”
How do you teach a model to put things in the right order?
Some problems aren’t about predicting a class or a value. They’re about ordering. A search engine has to rank results from most relevant to least. A recommender system has to rank items for each user. A credit risk team has to rank loan applications from highest priority to lowest. In each case, the goal isn’t a single number per item; it’s the relative order across many items. This is learning to rank, and it’s a substantial subfield of supervised learning with its own techniques and metrics.
The setup. A training example in learning to rank is not a single input-output pair. It’s a query (or context) and a ranked list of items. Formally, example i consists of Xi = {(xi, j, yi, j)}j = 1ri where xi, j is the feature vector of item j in the list, yi, j is its relevance label or rank, and ri is the number of items in the list. The features might describe how recent the document is, whether the query words appear in the title, the document length, and so on. The labels could be ordinal ranks (1 for most relevant, 2 for next, etc.) or graded relevance scores.
The goal is to learn a function f that takes a single item’s features and outputs a score, such that sorting items by these scores reproduces the desired ranking. There are three approaches.
Pointwise. The simplest. Treat each item independently as a regression or classification example. Each (xi, j, yi, j) becomes a training example for an ordinary supervised model that predicts the relevance score. Sort items by predicted score to get the ranking. The advantage is that you can use any supervised algorithm. The disadvantage is that the model has no awareness of the relative ordering: it learns to predict each item’s score in isolation, missing the fact that what really matters is the order, not the absolute scores.
Pairwise. Consider pairs of items at a time. For each pair (xi, j, xi, k) from the same query, train a model to predict whether item j should be ranked higher than item k. This is a binary classification problem (does j outrank k or not?), and the model learns to compare pairs rather than score individuals. At inference, you use the pairwise comparisons to construct the final ranking, often by plugging the pairwise model into a sorting algorithm. The advantage over pointwise is that the model directly learns about relative ordering. The disadvantage is that it still doesn’t directly optimise the metric you actually care about (which is usually a list-level metric like average precision).
Listwise. Consider the whole ranked list at once and try to optimise a list-level metric directly. This is the modern approach and the one that gives strongest evaluated results.
Read this as the three families. Pointwise is the entry point. Pairwise is a refinement. Listwise is the modern champion, and LambdaMART is its most famous representative.
Start with precision for a query: the fraction of retrieved documents that are relevant.
That’s just the definition of precision from Chapter 5. Average precision (AveP) for a single query averages the precision computed at each position where a relevant document appears in the ranked list:
where n is the number of retrieved documents, P(k) is the precision at position k (computed over the top k documents only), and rel(k) is 1 if the document at position k is relevant and 0 otherwise. The intuition is that you reward the model for putting relevant documents near the top of the list. A relevant document at position 1 contributes more to AveP than the same document at position 50 would.
Mean Average Precision is just the average of AveP across a collection of Q queries:
MAP is a single number summarising the quality of a ranking system across many queries. Higher is better. Modern search systems are evaluated and tuned against MAP and similar metrics.
LambdaMART, the listwise gradient-boosted ranker. LambdaMART, developed by Chris Burges and his group at Microsoft Research, is the algorithm that won most of the early Yahoo and Microsoft learning-to-rank competitions and remains a workhorse of production search systems. It combines the gradient boosting framework from Chapter 7 with a clever twist that lets it optimise a non-differentiable metric like MAP.
The algorithm uses a pairwise framing. For each pair of documents (xi, xk) from the same query, it predicts whether xi should rank higher than xk via a sigmoid:
where h is a scoring function (the actual ranker) and α is a hyperparameter. The cost function is cross-entropy on the pairwise predictions, and gradient boosting is used to fit h by adding regression trees that minimise this cost.
The clever twist. In ordinary gradient boosting, each new tree fits the gradient of the cost with respect to the current model’s predictions. LambdaMART modifies this gradient by multiplying it by a factor that depends on how the metric (MAP, NDCG, or whatever you’re optimising) would change if the documents at positions i and k were swapped. The modified gradient is called a lambda gradient, and it has the effect of telling the model: “swap these two documents only if doing so would improve the actual metric.” Swaps that would improve MAP get larger gradients; swaps that wouldn’t matter get smaller gradients. The trees grow to focus on the swaps that count.
This is a beautiful idea. Most supervised learning algorithms optimise a differentiable surrogate of the real metric (like cross-entropy as a surrogate for accuracy). LambdaMART optimises the metric itself, indirectly, by injecting metric-aware factors into the gradient. Almost no other supervised learning algorithm does this, and it’s part of why LambdaMART has been so dominant in ranking applications.
Where learning to rank fits in banking. The most obvious application is internal search: finding the right document, customer record, product, or policy among thousands. Merehaven Bank and other major UK banks have substantial internal search systems where ranking quality directly affects employee productivity. Less obviously, learning to rank is used for prioritising AML alerts (which alerts should an analyst look at first?), prioritising credit applications under capacity constraints, and ordering recommendations in customer-facing apps. Wherever you have many items and want to put the most important ones first, learning to rank is the appropriate technical framing.
If asked: “What’s the difference between pointwise, pairwise, and listwise ranking?”
How do you predict what someone will want next?
Recommendation systems are everywhere. Netflix recommending a film. Amazon recommending a product. YouTube recommending a video. Banks recommending products to customers. The basic problem is the same: you have users, you have items, you have a sparse history of which users have interacted with which items (purchases, ratings, views, clicks), and you want to predict which items each user will like next.
Two classical approaches.
Content-based filtering. Learn what each user likes based on the features of items they’ve consumed. For each user, build a small supervised model that predicts whether they’ll like an item, using the item’s features as input. New items can be scored against each user’s model. The advantage is that you can recommend new items immediately, even if no other user has interacted with them. The disadvantage is the filter bubble effect: the system always suggests items similar to what the user already likes, which can isolate users from anything new or different. Users may eventually stop trusting the recommendations because they feel repetitive.
Collaborative filtering. Learn from the patterns of all users together. If users A and B have similar histories of consumption, then items A liked are likely to be relevant for B and vice versa. The advantage is that the system can suggest items the user has never seen before, based on the wisdom of similar users. The disadvantage is that it ignores item content entirely, so it can’t recommend brand-new items that no one has interacted with yet (the cold-start problem), and it depends on having a reasonably dense user-item interaction matrix.
In practice, almost every modern recommender uses a hybrid that combines both approaches: use collaborative filtering when the data supports it, fall back to content-based for cold-start items, and combine both signals for the final ranking.
Read this as the recommender taxonomy. Each approach has strengths and weaknesses; combining them gets you most of the way to a production-ready system.
The data. In collaborative filtering, the data is organised as a sparse matrix. Each row is a user, each column is an item, each cell is the rating (or interaction) of that user with that item. For a service like Netflix, the matrix has hundreds of millions of rows (users), tens of thousands of columns (items), and is mostly empty: each user has rated maybe a few dozen films out of the catalogue. The sparsity is extreme, often 99.9% or more empty cells. This sparsity is what makes collaborative filtering hard. Standard supervised learning struggles when most of the input is missing, and the techniques described below are specifically designed for this regime.
Factorization machines
Factorization machines (FM), introduced by Steffen Rendle in 2010, are a recommender algorithm explicitly designed for sparse data. The core idea is to model pairwise interactions between features in a way that scales gracefully even when the feature space is huge and most features are zero for any given example.
- Blue (user one-hot). A one-hot vector identifying which user this row is about. If you have a million users, this section has a million dimensions, all zero except one.
- Green (movie one-hot). A one-hot vector identifying which movie. If you have 10,000 movies, this section has 10,000 dimensions, all zero except one.
- Yellow (other rated movies). A vector indicating which other movies this user has rated and what they thought of them.
- Plus engineered features. Things like “what fraction of movies this user has watched have won an Oscar?” or “what percentage of this movie did the user watch before rating it?”
The label y is the rating the user gave the movie. The training set has one row per (user, movie) interaction, and most entries in each row are zero because of the one-hot encodings.
The factorization machine model is:
Read this carefully. The first term is a global bias. The second term is the linear combination from ordinary linear regression: each feature xi has its own weight wi, and they sum up. The third term is the magic: it captures interactions between every pair of features. For each pair (i, j), the interaction strength is the product xixj multiplied by a coefficient that depends on the pair. importantly, this interaction coefficient is not a free parameter (which would require D(D − 1)/2 parameters, infeasible for large D). Instead, it’s the dot product of two vectors of factors, vi ⋅ vj, where each vi is a k-dimensional vector and k is much smaller than D.
The trick is the factorisation. Instead of D(D − 1)/2 separate interaction parameters, the model has only Dk parameters in the factor vectors. Each feature i has its own k-dimensional vector vi, and the interaction between features i and j is computed on the fly as vi ⋅ vj. This materially reduces the number of parameters and enables learning even on extremely sparse datasets. With D = 1, 000, 000 and k = 10, you have 10 million parameters for the factors instead of 500 billion for explicit pairwise interactions.
Why does this work for recommenders? Because the factor vectors vi end up being learned representations of the features, and similar features (similar users, similar movies) end up with similar factor vectors. The interaction between user u and movie m becomes vu ⋅ vm, which is high when the user’s factor vector and the movie’s factor vector point in similar directions in the k-dimensional space. The model effectively learns a low-dimensional embedding of users and movies, and predicts ratings via dot products in that embedding space. This is the same matrix factorisation idea behind classical collaborative filtering, but framed as a feature-rich linear model that can incorporate engineered features alongside the user and movie identifiers.
The loss function depends on the task. For regression (predicting a continuous rating), it’s squared error. For classification (predicting whether the user will like the item, with y ∈ {−1, +1}), it’s hinge loss or logistic loss:
Gradient descent optimises the average loss across all training examples. For multi-class problems (predicting one of five rating values), one-versus-rest converts the multi-class problem into five binary classification problems.
Denoising autoencoders for recommendation
A different and elegant approach is to use denoising autoencoders as a recommender. Recall from Chapter 7 that a denoising autoencoder is a neural network trained to reconstruct clean inputs from corrupted versions: you take a training example, add noise or zero out some entries, and train the network to output the original.
For recommendation, the noise is “remove some of the items the user has interacted with.” The intuition is that new items the user might like can be thought of as items that were “removed” by the corruption process, they’re items that should be in the user’s preferred set but currently aren’t because the user hasn’t seen them yet. Train the autoencoder to reconstruct user preferences from partial information, and at inference time, feed in the user’s actual (uncorrupted) history and look at what the model “fills in.” The items the model predicts as belonging in the user’s preferred set, but that aren’t actually there yet, are the recommendations.
Concretely, the training procedure is:
- Take the training matrix from the factorization machine setup but remove the user and item one-hot columns; keep only the yellow features (the user’s ratings of all items) and any engineered features.
- Deduplicate so each unique user is one row.
- During training, randomly zero out some of the non-zero ratings in each row.
- Train the autoencoder to reconstruct the original (uncorrupted) ratings from the corrupted input.
At inference, feed in the user’s actual ratings (uncorrupted), let the autoencoder reconstruct the full preference vector, and recommend items where the reconstructed score is highest among items the user hasn’t yet rated.
Another variant: feedforward neural network with two inputs. A simpler architecture: take the one-hot user vector u and the one-hot item vector m as two inputs to a feedforward neural network with one output, the predicted rating r. The network learns to embed users and items into a shared space and predict ratings from their interactions. This is conceptually similar to factorization machines but with a more flexible neural network instead of a fixed bilinear form.
Banking applications of recommender systems
Banks recommend products in many places. A retail banking app suggests savings products, credit cards, or insurance based on customer behaviour. A analysts dashboard recommends conversation topics for upcoming customer meetings. The Merehaven Credit Workbench In the synthetic Merehaven case recommends clauses, templates, or related case studies when drafting credit memos. Each of these is a recommender system in some form, and each uses some combination of content-based features, collaborative signals, and modern embedding-based retrieval.
The constraints are different from consumer recommenders. Banking products are often regulated, meaning recommendations have to be “suitable” in a specific compliance sense. Cross-selling has to respect customer preferences and consent under applicable data-protection and consent rules and applicable customer-outcome obligations rules. The models have to be explainable enough that a customer who asks “why did you recommend this?” can be given a meaningful answer. These constraints push banking recommenders toward simpler, more interpretable architectures than the giant deep learning systems that power Netflix or YouTube. Factorization machines and shallow neural networks are common; complex multi-layer recommender networks are rarer because the explainability cost is high.
If asked: “How would you recommend banking products to a retail customer?”
How does a model learn from text without anyone labelling it?
We arrive at the most important section of this section, and arguably the most important conceptual idea in modern machine learning. Self-supervised learning is the paradigm where the labels come from the structure of the data itself rather than from human annotation. The model is trained to predict one part of its input from another part, with both parts coming from the same unlabelled source. There is no human in the loop providing labels, yet the resulting model learns representations that turn out to be deeply useful for downstream tasks.
Word embeddings via word2vec, now served at scale through managed APIs like a managed training service Text Embeddings (the selected cloud platform) and a pinned embedding service (the selected cloud platform), are the ancestors of every modern language model embedding. Word embeddings via word2vec, introduced by Mikolov and peers at Google in 2013, are the most accessible example. They are also the conceptual ancestor of every modern foundation model, including the language models that power ChatGPT, Claude, and the Merehaven Credit Workbench. Understanding word2vec is the gateway to understanding why pre-training on enormous unlabelled corpora is the dominant paradigm in the reference implementation.
The puzzle. You want to represent each word in a vocabulary as a dense numerical vector, a word embedding, such that words with similar meanings have similar vectors. You want this representation to be useful for many downstream tasks: classification, named entity recognition, machine translation, question answering, sentiment analysis, anything involving text. You don’t want to label millions of examples by hand. How do you learn the representations from raw text alone?
The insight. Words are defined by the company they keep. Linguists call this the distributional hypothesis, due to John Firth in 1957: “you shall know a word by the company it keeps.” If “book” and “article” appear in similar contexts in many sentences (“I’m reading a ___ on machine learning”), then they probably mean similar things, even if they share no letters or sounds. Conversely, if two words rarely appear in similar contexts, they probably don’t mean similar things. So if you can train a model that’s good at predicting which words appear near which other words, the model will implicitly learn the meanings of words, and you can extract those meanings as the model’s internal representations.
Take a sentence: “I almost finished reading the book on machine
learning.” Pick a window size, say 5 (which means 2 words on either side
of the centre word). For each position in the sentence, the
skip-gram is the centre word and its surrounding
context. For example, with the centre word “book,” the skip-gram is
[reading, the, BOOK, on, machine] (the centre word in
capitals). The training task is: given the centre word “book,” predict
each of the context words (“reading,” “the,” “on,” “machine”) as
separate prediction problems.
Let’s denote a skip-gram with window size 5 as [x−2, x−1, x, x+1, x+2], where x is the centre word and the others are context. Each word is represented as a one-hot vector over the entire vocabulary. With a vocabulary of 10,000 words, each one-hot vector has 10,000 dimensions, all zero except for one.
The skip-gram model is a neural network that takes a one-hot word vector as input, passes it through a hidden layer (the embedding layer, typically 100-300 units), and outputs a probability distribution over the vocabulary via softmax. The training objective is to maximise the probability of the actual context words given the centre word, summed over many billions of training examples extracted from raw text.
Read this as the skip-gram training step. Centre word goes in. Hidden layer produces an internal representation. Output softmax produces a distribution over the whole vocabulary. The loss compares the distribution to the actual context words and updates the network to make the actual context words more likely. Repeat billions of times across raw text.
The trick: the embedding is a side effect. After training, the network’s primary purpose (predicting context) is irrelevant. What you actually want is the hidden layer’s representation of each word. You feed in the one-hot vector for any word, and the hidden layer produces a 300-dimensional vector. That vector is the word embedding for that word. Words with similar meanings have similar embeddings because the network has learned that they appear in similar contexts and therefore should produce similar hidden representations.
Why this works mathematically. The weights from the
input layer to the hidden layer form a matrix W with shape (vocabulary size ×
embedding dimension). When you feed in a one-hot vector for word i, the hidden layer’s output is just
the i-th row of W. So the embedding for word i is literally row i of the input-to-hidden weight
matrix. Training the network amounts to optimising these rows so that
the resulting embeddings are good at predicting context. The famous
arithmetic property, king - man + woman ≈ queen, emerges
because the optimisation naturally produces an embedding space where
semantic relationships correspond to consistent vector offsets.
Read this as the unbroken lineage from word2vec to modern foundation models. Every step refines or scales the same self-supervised idea, and every modern large language model is a descendant of the basic word2vec insight that you can learn meaning from the structure of text alone.
Why it’s called self-supervised. The training labels (the context words) come from the same data as the inputs (the centre word). No human ever labelled anything. The supervision is created automatically by the structure of text: every sentence implicitly defines billions of (centre, context) pairs that the algorithm exploits. This is the key innovation that makes word2vec scalable: you can train on as much text as you can throw at it, because the labels come for free. The Web has trillions of words. You can train a word2vec model on a slice of the Web with no annotation budget at all, just compute.
Two practical efficiency tricks. The output softmax over a large vocabulary (millions of words for some applications) is computationally expensive. Word2vec uses two techniques to speed it up:
Hierarchical softmax. Replace the flat softmax with a binary tree where each leaf is a word. Computing the probability of a specific word requires walking from the root to that word’s leaf, which is O(log V) instead of O(V).
Negative sampling. Instead of computing softmax over the entire vocabulary at every step, compute it over the actual context word plus a small random sample of “negative” words. The model learns to score the true context high and the negative samples low. This makes training materially faster and is what’s actually used in most production word2vec implementations.
The descendants of word2vec. Word2vec was a watershed, and the field built rapidly on its foundation. GloVe (2014) used a slightly different objective based on co-occurrence counts but produced similar embeddings. fastText (2016) extended word2vec to handle subword units, which made it work better for morphologically rich languages and out-of-vocabulary words. ELMo (2018) introduced contextual word embeddings: instead of one fixed vector per word, ELMo produces a different vector for each occurrence of a word, conditioned on the surrounding sentence. This handles polysemy (“bank” as a financial institution versus “bank” as the side of a river). BERT (2018) was the breakthrough that took all these ideas and combined them with the transformer architecture, producing contextual embeddings of dramatic quality and starting the foundation model era.
GPT (2018-present) used a different self-supervised objective (next-word prediction) but built on the same self-supervised paradigm and scaled it to extraordinary sizes, producing the language models that power most modern AI assistants.
The thread from word2vec to GPT is unbroken. Self-supervised pre-training on enormous unlabelled corpora, followed by fine-tuning or in-context learning for specific tasks, is the recipe behind every modern foundation model. Word2vec showed that the recipe could work; everything since has been refining and scaling it.
Where this fits in banking. Every NLP system In the synthetic Merehaven case in the reference implementation uses pre-trained embeddings from a large language model. The Merehaven Credit Workbench uses transformer embeddings. The credit memo classifier uses BERT-style encoders fine-tuned on banking text. The complaint detector uses a similar pipeline. The internal search system uses sentence embeddings for dense retrieval. Without self-supervised pre-training, none of these would be feasible because the labelled data needed to train them from scratch doesn’t exist in any single bank. Self-supervised learning is the technology that makes modern banking NLP possible at all.
If asked: “Explain what self-supervised learning is and why it matters.”
Part V: Operate the decision system
The last part joins models to validation, policy, human review and monitoring. Design quality appears in the route between a score and an effect.
Review whole-system decisions
Scenario 1: build a credit risk model for sme lending
The setup. “We’re refreshing our SME credit risk scorecard. The current model is a logistic regression with 30 hand-engineered features, in production for six years, validation AUC 0.78. We want to do better. We have 12 years of historical loan data, about 800,000 loans across SME term lending and overdrafts, with default labels. Walk me through how you’d approach this.”
What’s being tested. Whether you can scope a credit risk problem properly, choose an appropriate model family for the regulated context, design a validation strategy that respects time, handle class imbalance, and articulate the trade-off between accuracy and interpretability.
Your walkthrough.
I’d start with clarifying questions. What’s the regulatory framework, IRB or standardised? What is the model used for downstream, capital calculation, pricing, decisioning, or all three? Who are the model risk reviewers and what’s their tolerance for complexity? What’s the deployment latency budget? Is the model refreshed annually or more frequently?
Assume IRB for capital calculation, with downstream use in pricing. PRA model risk reviewers are conservative and have a strong preference for interpretable models with explicit coefficients. Annual refresh cadence. Latency is not a constraint because scoring is batch.
Given those constraints, my approach would have several components.
Step 1: data quality and labelling. Confirm what counts as a default, typically 90 days past due plus regulatory flags. Validate that the historical labels are consistent over time, particularly across the COVID period where forbearance and government schemes muddied the signal. I’d likely exclude or carefully treat the COVID-affected cohort to avoid distorting the model.
Step 2: temporal split for validation. This is critical and the most common mistake junior candidates make. I would not use random k-fold cross-validation on credit data. Defaults have temporal dependence: economic cycles affect default rates, and the future doesn’t look like the past. The right split is temporal: train on loans originated through, say, end of 2020; validate on 2021-2022 originations; test on 2023 originations with a one-year outcome window. This mimics how the model will actually be used.
Step 3: feature engineering. Start with the 30 features from the existing model as a baseline. Add candidates: trends in financial ratios, sector and geography effects, banking behaviour features (overdraft usage, returned items, balance volatility), and credit bureau enrichment if available. Apply domain checks: any feature whose meaning a credit officer can’t explain in one sentence is suspect.
Step 4: build the production model. A regularised logistic regression with carefully selected features. Use Lasso (L1) for feature selection followed by Ridge (L2) for the final fit, or elastic net to combine both. The goal is a model with around 40-60 features, each with a clear coefficient that the model risk team can interpret. This is the model that will go to production.
Step 5: build a champion gradient boosted model in parallel. Train an XGBoost or LightGBM model on the same data with extensive feature engineering. The point is not to deploy this. The point is to use it as a benchmark and as a feature engineering tool. If the GBM beats the logistic regression by a meaningful margin, examine its feature importances and SHAP values to see what it’s finding, and use that information to engineer better features for the logistic regression.
Step 6: handle the class imbalance. Default rates are typically 1-5% for SMEs. I would use class weighting in the logistic regression (set the positive class weight to roughly the inverse of the base rate) and choose the operating threshold based on the business context, not the default 0.5. For credit decisioning, you typically want to set the threshold by precision-recall trade-off at the operating regime.
Step 7: validate with the right metrics. AUC is a starting point but not enough. Compute precision and recall at the operating threshold. Compute KS statistic, which is standard in credit risk. Compute Gini, which is just 2 ⋅ AUC − 1. Compute calibration metrics: how do predicted probabilities compare to actual default rates by score band? Calibration matters enormously for capital calculation, where the model output feeds directly into risk-weighted assets.
Step 8: explainability. For each prediction, compute the contribution of each feature using the logistic regression coefficients directly. Build a customer-friendly explanation tool that translates the contributions into plain English. This is needed for adverse action notices and for any subsequent customer challenges.
Step 9: governance and monitoring. Document the model in line with the bank’s model risk policy. Set up monitoring dashboards for population stability index (PSI) on the input features, score distribution drift, and calibration drift. Plan for an annual review and recalibration.
Read this as the production credit risk pipeline. Notice how the most accurate model (XGBoost) is not the production model. It’s used as a benchmark and a feature engineering tool, while the deployable model is a logistic regression that meets the interpretability bar.
Trade-offs to mention explicitly. Logistic regression sacrifices perhaps 3-5 AUC points compared to XGBoost. In return, it gets full interpretability, regulatory acceptance, and a coefficient structure that the credit team can debate and override if needed. For capital models, that trade-off is often worth it. For pure decisioning models with less regulatory scrutiny, you might lean further toward GBM.
Follow-up questions to expect.
- How would you handle missing values in the financial ratios?
- What if the regulator asked you to demonstrate that your model isn’t biased against any protected group?
- How would you defend the choice of logistic regression over a neural network?
- What’s the difference between PD, LGD, and EAD models, and which one are we discussing?
- How would you validate that the model is calibrated, not just discriminating well?
Scenario 2: detect card fraud at scale
The setup. “We process about 100 million card transactions per day. The fraud rate is roughly 0.04%. Currently we have a rule engine that catches about 60% of fraud at 5% false positive rate. We want an ML model to do better. Walk me through how you’d build it.”
What’s being tested. Whether you understand extreme class imbalance, latency-constrained scoring, the difference between offline metrics and operational metrics, and the role of ML alongside rule engines in fraud detection.
Your walkthrough.
Clarifying questions first. Real-time scoring or near-real-time? What’s the latency budget? Where does the model sit relative to the rule engine, replacing it, augmenting it, or in a stack? Is the goal to maximise fraud caught for a fixed false positive budget, or to minimise false positives at a fixed fraud catch rate? What’s the labelling delay, how long after a transaction do we know it was fraud?
Assume real-time scoring at ~100ms latency budget per transaction. The model augments the rule engine: rules catch the obvious cases instantly, ML scores everything for additional flags. The goal is to catch more fraud without increasing the false positive count, because false positives have a real customer cost (declined transactions, frustrated calls). Labelling delay is 1-30 days as customers report or chargebacks come through.
Step 1: feature engineering. This is the highest-leverage step in fraud modelling. The features that catch fraud are mostly about velocity, deviation from customer normal, and merchant patterns. I’d build:
- Velocity features: count and amount of transactions in the last 1 minute, 1 hour, 24 hours, 7 days
- Deviation features: how does this transaction compare to the customer’s average and standard deviation
- Merchant features: merchant category, country, fraud history of this merchant
- Card features: time since last legitimate use, card age, BIN characteristics
- Geographic features: distance from customer’s typical locations, country of transaction
- Channel features: present-card, online, contactless, recurring
The features have to be computable in <50ms because the model itself needs the rest of the latency budget. This means a feature store with precomputed customer profiles updated incrementally, plus a few real-time aggregations on the inbound transaction stream.
Step 2: model choice. A gradient boosted tree (LightGBM is my preference for fraud, because it handles categorical features natively and is fast at inference). The reasons: handles mixed feature types, well-tested to missing values, fast at scoring (a few hundred microseconds per transaction with a tree count of perhaps 500-1000), and interpretable enough via SHAP values for compliance review.
I would not use a deep neural network for the primary scoring. The data is tabular, the volume is large enough that GBM trains well, and the latency budget rules out deep models without significant engineering effort. A neural network might be appropriate as a secondary model for specific subpopulations or as a feature extractor for sequence patterns, but not as the main scorer.
Step 3: handle the imbalance. With 0.04% positive class, this is severe imbalance. I’d combine three techniques:
- Class weighting in the GBM (LightGBM’s
scale_pos_weightset to roughly 2500, the inverse class ratio) - SMOTE on training data only, oversampling positives by perhaps 10x to 20x, not all the way to 1:1, because synthetic fraud examples become unrealistic
- Custom evaluation metric: precision in the top 0.5% of predicted scores, which is the operating regime, not AUC or accuracy
Step 4: validation strategy. Temporal split as always for time-dependent data. Train on transactions through end of last quarter; validate on the most recent month. Use the validation set to tune hyperparameters and to set the operating threshold. Reserve the most recent week as a true holdout test.
Step 5: integrate with the rule engine. The model doesn’t replace the rules. The rules catch known patterns instantly; the model adds to the set of things being flagged. The combined system has to be tuned together: if both the rules and the model fire on the same transaction, that’s a higher confidence flag. If only the model fires, that’s a softer flag that might go through additional verification. Design the integration so that adding the model can only catch more fraud, never miss fraud the rules would have caught.
Step 6: monitoring and adaptation. Fraud patterns change constantly. The model degrades within weeks if not refreshed. Set up daily monitoring of model performance against confirmed fraud cases, and plan for weekly or monthly retraining using the most recent labelled data. Monitor for adversarial drift: fraudsters adapt their patterns, and the model has to adapt back. Build a feedback loop where confirmed fraud cases are immediately added to training data.
Read this as the fraud detection architecture. The rule engine and the model run in parallel, both feeding into a combined decision. Customer outcomes feed back into model retraining. The whole system is a continuous learning loop because fraud is adversarial.
Trade-offs. GBM gives the right balance of accuracy, latency, and interpretability for this use case. A deep neural network might marginally improve catch rate but at the cost of latency and explainability. A simpler logistic regression would be too rigid to capture the complex non-linear interactions in fraud features. The threshold choice is the central operational lever: tighten it to catch more fraud at the cost of more false positives, loosen it to reduce customer friction at the cost of more losses. This trade-off is set by business policy, not by the model.
Follow-up questions.
- How would you handle the fact that customers can dispute legitimate transactions, polluting the labels?
- What if the false positive cost varies by customer segment (private banking customers complain louder)?
- How would you detect when the model has stopped working without waiting for labels?
- Walk me through how you’d implement the real-time feature pipeline.
Scenario 3: a team is proposing to use a deep neural network for credit risk. should you support it?
The setup. “A team has built a deep neural network on the same SME credit data we discussed earlier. Validation AUC is 0.86, beating the logistic regression by 8 points. They want to deploy it. Are you in favour or against, and why?”
What’s being tested. Whether you can push back on a fashionable choice when it’s the wrong fit, whether you understand the regulatory context for model risk in banking, and whether you can articulate trade-offs to a stakeholder who is excited about a technical result.
Your walkthrough.
I’d be cautious and probably against, but I’d want to understand more before giving a final answer.
First, the AUC improvement is real but I’d want to verify. Was the validation done with a proper temporal split? Were the features identical to the logistic regression’s, or did the deep learning team have access to different features? Was the validation set chosen in a way that might bias the comparison? An 8-point AUC gap is unusually large for a tabular credit problem, most published comparisons show deep networks performing comparably to or slightly worse than gradient boosted trees on tabular data. If a deep network is winning by 8 points, either the comparison is unfair or there’s some property of the data that’s unusual.
Second, even if the AUC improvement is real and reproducible, the question is whether it’s worth the cost. The costs are several:
Model risk acceptance. PRA-supervised credit models go through strict model risk review. A deep neural network with hundreds of thousands of parameters is much harder to document, validate, and defend than a logistic regression. The model risk team will want to see exhaustive sensitivity analyses, stability tests, and explainability evidence. They may push back on using deep learning for capital calculation entirely.
Explainability for adverse action. Customers who are declined have a right to know why. With logistic regression, you can point at coefficients and say “your DSCR contributes -30 basis points to your score.” With a deep neural network, you can produce SHAP values, but they’re less interpretable and harder to defend in a customer challenge.
Maintenance burden. Deep networks need careful retraining, monitoring, and have more failure modes than logistic regressions. The team that built it might handle the maintenance well, but if they leave, the next team has a complex artefact to maintain.
Regulatory uncertainty. Even if PRA accepts the model now, future guidance might restrict deep learning for capital models. Building production dependency on a model that might be ruled out in two years is a strategic risk.
Third, I’d ask whether the gain is needed. An AUC of 0.78 versus 0.86 is meaningful, but how does it translate to actual business outcomes? In credit risk, AUC improvements often translate to small reductions in expected loss because the marginal cases on the boundary are the hard ones, and AUC improvements come mostly from better discrimination of the easier cases. I’d want to see the actual expected-loss impact, not just the AUC delta.
Given all this, my recommendation would likely be: don’t deploy the deep network as the production credit model. Instead, use it as a champion model, a benchmark that informs how to improve the logistic regression. Examine its feature attributions to find interactions and non-linearities the logistic regression is missing. Engineer those into the logistic regression as new features. The shipped model stays interpretable and regulator-friendly, but it’s smarter than it would have been without the deep network exercise.
I’d also leave the door open for deep learning in non-capital applications. For pricing optimisation, customer experience improvements, or operational efficiency where the regulatory bar is lower, deep learning might be appropriate. The “no” is specific to capital models, not blanket.
Read this as the decision logic. The answer is rarely a flat “yes” or “no.” It depends on whether the gain is real, what the use case is, and what the governance constraints are. Senior engineers reason through the conditions rather than reacting to the headline number.
The framing for the team. “I’m impressed by the technical work, and I think there’s real signal here. I don’t think we should ship the deep network as the production credit model because of regulatory and explainability constraints, but I’d love to use it as a benchmark to improve the logistic regression. Can we examine the feature attributions to see what it’s picking up that we’re missing? If we can capture 70% of the gain in a more interpretable model, that’s a much better outcome for the bank.” This framing respects the team’s work while being clear about the constraints.
Scenario 5: a model performs perfectly in training and badly in production
The setup. “A team built a customer churn model. Validation AUC was 0.92. In production, the model’s predictions don’t seem to correlate with actual churn at all. The team is panicking. Walk me through how you’d debug it.”
What’s being tested. Whether you can systematically diagnose ML production failures, whether you understand the train-serve skew and data drift problems, and whether you have the discipline to investigate before guessing.
Your walkthrough.
The first thing I’d say is: this is one of the most common failure patterns in production ML, and there’s a small set of likely causes. I’d work through them systematically.
Hypothesis 1: target leakage in training. This is the most common cause of “perfect in training, terrible in production.” A feature in the training data accidentally encoded the answer. For a churn model, the classic example is including a feature like “days since last activity” that’s already moved in a churn-revealing direction by the time the label was assigned. The model learned to predict churn from a feature that’s a direct consequence of churn rather than a predictor of it. In production, that feature isn’t available at scoring time the way it was at training time, and the model breaks.
To check: examine the most important features. Any feature whose value changes meaningfully in the time window between scoring and the outcome is suspect. Walk through how each top feature was constructed and whether the construction respects the temporal order.
Hypothesis 2: train-serve skew in feature engineering. The features computed at training time are not identical to the features computed at scoring time. This happens when training uses a different code path, a different data source, or different timing than serving. Even small differences (different aggregation windows, different missing value handling, different encoding) can completely break a model.
To check: take a few production examples, recompute their features using both the training pipeline and the serving pipeline, and compare. They should be identical. If they’re not, you’ve found the bug.
Hypothesis 3: temporal validation was done wrong. If the validation set was random rather than temporal, the model might have effectively been allowed to peek at the future during training. AUC 0.92 on a random split could be much lower on a true temporal holdout.
To check: look at how the validation split was done. If it was k-fold or random, redo it as temporal and see what happens to the validation AUC. If the AUC drops materially on a proper temporal split, the original validation was misleading.
Hypothesis 4: data drift between training and production. The distribution of inputs in production is different from the training distribution. This could be because the customer base has changed, the product has changed, the feature engineering pipeline has changed, or some upstream system has been updated.
To check: compute population stability index (PSI) on each input feature, comparing the production distribution to the training distribution. PSI > 0.25 on any feature is a strong signal that the distribution has shifted significantly.
Hypothesis 5: label distribution mismatch. The training labels were defined one way but the production “labels” (the outcomes the model is being measured against) are defined slightly differently. For churn, “churn” might mean “closed account” in training but “churned within 90 days” in evaluation, and the model isn’t actually being measured against what it was trained to predict.
To check: sit with the team and walk through exactly how training labels were constructed and exactly how production performance is being evaluated. Make sure they’re the same definition.
Hypothesis 6: bug in the production scoring code. The model itself is fine, but the production code that calls it is computing the wrong thing, wrong feature order, wrong type conversion, wrong post-processing. Pure software bug.
To check: take one customer for whom you know the historical data, score them through both the training pipeline and the production pipeline, and compare the outputs. They should be identical.
Read this as a debugging tree. Each branch is a hypothesis with a specific check. Work through them in order of likelihood, not in order of complexity. Most “model failed in production” stories end with target leakage or train-serve skew, so check those first.
The order to investigate. I’d start with the feature pipeline comparison (hypothesis 2) because it’s the fastest to check and the most common cause. Then target leakage (hypothesis 1). Then drift (hypothesis 4). The other hypotheses are less common but worth ruling out.
The fix. Once you’ve identified the cause, the fix depends on which hypothesis was right. For target leakage, remove the leaking feature and retrain. For train-serve skew, fix the pipeline so they match. For data drift, retrain on more recent data. For temporal validation, redo the validation properly and accept the lower AUC. The mistake to avoid is patching the symptom without understanding the root cause; that produces a model that fails again next quarter.
Follow-up questions.
- What if all your hypotheses come up empty?
- How would you set up monitoring so this never happens again?
- The team wants to ship a fix today. What’s the minimum change you’d accept?
- Is it possible the model is fine and the production evaluation is wrong?
Scenario 6: set up validation for a time-series model
The setup. “We’re building a model to predict customer balance levels 30 days ahead, used for liquidity planning. We have 5 years of daily customer balance data. How would you set up the training and validation?”
What’s being tested. Whether you understand why time-series data needs special validation, whether you know how to avoid lookahead, and whether you can design a validation strategy that mimics the production use case.
Your walkthrough.
The big mistake to avoid is k-fold cross-validation. With time-series data, k-fold randomly splits across time, which means the training set contains examples from after the validation set. The model effectively gets to peek at the future, and the validation accuracy is misleadingly high. In production, the model only sees the past, so the production accuracy will be much worse.
The right approach is time-based splits, where the training set always precedes the validation set in time, and the validation set always precedes the test set. There are two main variants.
Holdout split. Pick a cutoff date. Everything before is training. Everything after is validation. Pick a later cutoff for the test set if you need one. Simple and clear, but uses only one validation point.
Time-series cross-validation (walk-forward). Use multiple cutoffs to create several train/validation splits, walking forward through time. Train on data through end of 2021, validate on Q1 2022. Train on data through end of Q1 2022, validate on Q2 2022. And so on. Average the validation metrics across all folds. This gives more well-tested estimates and is the right choice for serious model evaluation.
For our specific problem, predicting balance 30 days ahead, there’s an additional subtlety. The training labels (the actual balance 30 days after the snapshot date) are only known with a 30-day delay. So when training, you have to be careful not to include any features that come from the prediction window. For each training snapshot at date t, the features are all computed from data up to date t, and the label is the balance at date t+30.
The validation has to reflect this same constraint. For each validation snapshot, compute features from data up to that date only, and compare the prediction against the actual balance 30 days later. Don’t validate on snapshots so close to the end of your data that you don’t have a 30-day forward window available.
Read this as the temporal structure of one training example. Features come from the history up to and including the snapshot date. The label comes from 30 days in the future. The whole training example respects this temporal order, and the validation must too.
Other things to consider.
Distribution shifts. Customer balance behaviour shifts over time, particularly during major events (COVID, interest rate changes, regulatory shifts). The model trained on pre-COVID data might be useless for post-COVID forecasting. The validation strategy should explicitly test how the model performs on data from different time periods, not just the most recent.
Stationarity. Some time series are stationary (their statistical properties don’t change over time), and some aren’t. Customer balances are usually not stationary: they trend, they have seasonality, they have event-driven jumps. The model needs features or transformations that handle this, differencing, detrending, seasonal indicators.
Recent data weighting. If the recent past is more representative of the future than the distant past, you might want to weight recent training examples more heavily. This is a hyperparameter to tune on validation.
Customer-level splits within time splits. For customer-level forecasts, you might also want to split customers across train and validation, so that the model is tested on customers it hasn’t seen. This is in addition to the temporal split, not instead of it.
Follow-up questions.
- How would you incorporate seasonality into the features?
- What happens if you only have 2 years of data instead of 5?
- How would you handle a customer who joined the bank 6 months ago?
- The model makes predictions 30 days ahead. How long after that should you retrain?
Scenario 7: build a multimodal document understanding system
The setup. “We want to build a system that processes scanned credit memos. Each memo has structured text, tables of financial figures, and sometimes hand-written annotations. The output should be structured fields plus a classification of the memo type. Walk me through the architecture.”
What’s being tested. Whether you can design a system that combines computer vision, NLP, and structured prediction, whether you understand how to compose pre-trained models with task-specific fine-tuning, and whether you can articulate trade-offs in a multimodal system.
Your walkthrough.
This is a multimodal understanding problem with several distinct subtasks. I’d decompose it into stages rather than trying to solve everything with one giant model.
Stage 1: document preprocessing. Convert each scanned memo to a clean image (deskewing, contrast normalisation, denoising). Detect page boundaries. Identify regions of interest: text blocks, tables, signatures, annotations. This is classical computer vision preprocessing.
Stage 2: optical character recognition (OCR). Run OCR on the text regions to extract the textual content. For modern systems, I’d use a pre-trained OCR model rather than training from scratch, Tesseract for a baseline, or one of the modern transformer-based OCR models for higher accuracy. The output is structured text with bounding boxes telling you where each token came from.
Stage 3: layout-aware language model. Feed the OCR output (text plus bounding box positions) into a pre-trained layout-aware language model like LayoutLM or its descendants. These models are designed for document understanding: they take both the text and the spatial positions of the text and produce embeddings that are useful for classification and entity extraction. Fine-tune on a labelled corpus of bank credit memos.
Stage 4: classification head for memo type. On top of the LayoutLM embedding (typically using the [CLS] token’s representation), add a classification head to predict the memo type (new facility, increase, waiver, annual review, watchlist, decline). Train with cross-entropy loss on labelled examples.
Stage 5: entity extraction head for structured fields. On top of the same LayoutLM embedding, add a sequence labelling head (token-level classification) to extract entities: customer name, facility amounts, dates, key financial ratios, decision rationales. Train with cross-entropy loss per token.
Stage 6: table extraction. Tables need special handling because LayoutLM doesn’t perfectly capture cell relationships. Use a dedicated table extraction model (like TableNet or a transformer-based table parser) to convert table images into structured row-column data. Then validate the extracted data against the rest of the memo for consistency.
Stage 7: handwriting recognition. Hand-written annotations are the hardest part. Use a separate model trained specifically on handwriting (or the appropriate path through a multi-modal foundation model). These extractions are usually lower confidence and are flagged for human review.
Stage 8: post-processing and validation. Combine outputs from all stages into a single structured output. Apply business rules to validate consistency (the facility amount in the memo type should match the facility amount in the table). Flag inconsistencies for human review.
Read this as the multimodal pipeline. Each component has a specific job. The pipeline is modular: you can swap out one stage without rebuilding the others, and you can monitor each stage independently.
Trade-offs.
Why not one giant model? A single end-to-end model that takes the raw image and outputs everything would be elegant in theory and a nightmare in practice. It would be much harder to train, harder to debug, harder to validate, and harder to update when one part of the pipeline needed improvement. The modular approach is the right answer for a production system in a regulated bank.
Why pre-trained? Because labelled credit memos are scarce. Training any of these models from scratch would require tens of thousands of carefully labelled examples per task, which the bank doesn’t have. Pre-training on public data (general OCR, general layout understanding, general handwriting) gives the foundational competencies, and fine-tuning on a few thousand bank-labelled examples adapts them to the specific domain.
Where does it fail? Edge cases. Unusual document layouts. Faded scans. Stylised handwriting. The pipeline is a 90% solution, and the remaining 10% needs human review. The system has to be designed assuming that human review is part of the workflow, not an exceptional fallback.
Follow-up questions.
- How would you measure end-to-end accuracy of this system?
- What’s your strategy for the inevitable cases where the OCR misreads numbers?
- How would you handle a new memo type the system hasn’t been trained on?
- How would you prioritise human review effort given limited capacity?
Scenario 8: a regulator asks you to demonstrate that your model isn’t biased
The setup. “The PRA has asked us to demonstrate that our credit decisioning model isn’t biased against any protected group. Walk me through how you’d respond.”
What’s being tested. Whether you understand fairness in ML, whether you know the technical and organisational responses to bias concerns, and whether you can navigate a regulatory conversation.
Your walkthrough.
I’d start by being clear about the scope of the question. “Bias” is an overloaded term. There are several distinct concerns:
- Statistical bias in predictions. Does the model produce systematically different outcomes for different groups, controlling for the underlying risk?
- Disparate impact. Does the model produce different acceptance rates for different groups, even if the predictions are statistically calibrated?
- Disparate treatment. Does the model use protected attributes (race, gender, religion) directly or as a proxy?
- Fair access. Are there structural reasons certain groups receive different outcomes that the model is amplifying rather than correcting?
The PRA is likely asking about all of these, not just one. My response would address each.
Step 1: data audit. First confirm what protected attributes we have (or could derive). UK credit data typically doesn’t include explicit race or religion, but it does include postcode, name, and other features that can be proxies. I’d commission a data audit to identify any feature that correlates with protected attributes, even indirectly. Postcode is the classic example: it correlates strongly with ethnicity in the UK due to historical residential patterns.
Step 2: predictive parity audit. For each protected group (where we can derive group membership), measure the model’s calibration: does a predicted PD of 5% correspond to an actual default rate of 5% across all groups? If yes, the model is calibrated, which is one important fairness criterion. If no, the model is systematically over- or under-predicting for some groups.
Step 3: disparate impact analysis. For each protected group, measure the acceptance rate at the operating threshold. If the acceptance rate for one group is significantly lower than for another, the model has disparate impact, even if it’s calibrated. The standard test in US fair lending is the four-fifths rule: the acceptance rate for the protected group should be at least 80% of the acceptance rate for the comparison group. UK regulators use slightly different formulations but similar principles.
Step 4: disparate treatment audit. Verify that the model does not use any protected attribute directly. Verify that the features used don’t correlate strongly with protected attributes. Run feature importance analysis and check whether the most important features are themselves proxies. Postcode is the most common offender; consider whether to remove it or transform it.
Step 5: counterfactual analysis. For a sample of declined applications, compute what would have happened if a single feature changed. Did the decision flip? If protected attributes affect the decision significantly when other features are held constant, that’s evidence of bias. This is more sophisticated than calibration tests and gets at the real causal question.
Step 6: corrective measures if needed. If the audit finds bias, the corrective measures include: removing or transforming proxy features, adjusting the operating threshold per group (which has its own legal complications), retraining with fairness constraints, or building additional review processes for cases near the boundary.
Step 7: governance and monitoring. Document the audit, the findings, and the corrective measures. Set up ongoing monitoring of fairness metrics in production. Treat fairness as a regular model risk concern, not a one-off exercise.
Read this as the fairness audit process. Each step addresses a specific kind of bias concern. The output is a report that demonstrates the model meets the bank’s fairness standards or, if it doesn’t, what’s being done about it.
The honest part of the answer. “Fairness in ML is genuinely hard. There’s no single metric that captures all the things we mean by ‘unbiased,’ and some fairness criteria are mathematically incompatible with each other. The best response to a regulator is to be transparent about what we’ve measured, what we’ve found, what we’re doing about it, and what the residual concerns are. Trying to claim the model is perfectly fair is both technically wrong and politically risky.”
Follow-up questions.
- What if removing the proxy features reduces model accuracy significantly?
- How would you balance fairness against business performance?
- What’s the difference between equal opportunity and equal outcomes as fairness criteria?
- How would you respond if the PRA found a bias issue and gave you 90 days to remediate?
Scenario 11: choose a metric for an ambiguous problem
The setup. “We’re building a model to identify customers who might be in financial difficulty so we can offer support. What metric would you optimise?”
What’s being tested. Whether you can navigate a problem where the metric isn’t obvious, whether you understand the difference between a model metric and a business metric, and whether you can think about second-order effects.
Your walkthrough.
The wrong answer is to pick AUC or F1 because they’re standard. The right answer is to think about what the model is actually for and what would happen if it succeeded or failed in different ways.
The business goal. Identify customers at risk of financial difficulty so we can offer support that helps them avoid harm. Success means fewer customers ending up in genuine financial distress. Failure modes include: missing customers who need help, alarming customers who don’t, offering inappropriate products to vulnerable people, and creating a paternalistic experience that customers resent.
The model output. A score per customer indicating risk of financial difficulty in the next 90 days.
The downstream action. Customers above a threshold are routed to a support team that reaches out with options: payment holidays, budgeting tools, debt advice referrals.
Now what metric should we optimise?
Option 1: classification accuracy or AUC. No. Accuracy is meaningless on imbalanced data, and AUC measures discrimination across all thresholds, but only one threshold will be used in production. AUC tells you the model is “good” in some general sense, not whether it’s good at the operating point you care about.
Option 2: precision and recall at the operating threshold. Better. Precision tells you what fraction of flagged customers actually needed help; recall tells you what fraction of customers who needed help got flagged. Both matter. Precision matters because false positives are real customers who get an unwanted intervention. Recall matters because false negatives are customers who needed help and didn’t get it.
Option 3: precision and recall in different score deciles. Better still. The operations team has limited capacity. They can only outreach to perhaps the top 5% of scored customers. So the relevant precision is precision in the top 5%, not precision overall. Frame the metric to match the operational constraint.
Option 4: business outcomes. Best. The ultimate metric is whether customers who got help avoided difficulty more often than they would have without it. This requires either a randomised experiment or a careful causal analysis using historical interventions. It’s the metric the bank actually cares about, even though it’s harder to compute than precision and recall.
The honest answer to the interviewer: “I’d optimise the model for precision in the top 5% of scores, because that’s the operational regime. But I’d measure success in production by whether the customers we contact actually have better outcomes than comparable customers we don’t contact. The model metric is a proxy for the business metric, and we should be explicit about which is which.”
Second-order considerations. Think about who gets harmed if the model is wrong. False positives mean customers who don’t need help get a “we’re worried about you” message, which can feel intrusive or stigmatising. False negatives mean customers who do need help don’t get it, which can be catastrophic. The asymmetry suggests being relatively cautious about precision (don’t over-alarm) while still maintaining good recall (don’t miss people in real distress). The threshold should reflect the asymmetry of harms.
Vulnerability considerations. Some customers are flagged because they’re temporarily struggling. Some are flagged because they have ongoing vulnerability indicators. The intervention should be tailored: a payment holiday offer is appropriate for the first; a structured support pathway is appropriate for the second. The model can produce a single risk score, but the downstream action should be sensitive to context.
Read this as the metric reasoning. There’s no single right metric. The right metric is the one that maps to the real business goal, given the operational constraints. Senior engineers explain their metric choice in terms of the business, not in terms of textbook conventions.
Follow-up questions.
- How would you set up the A/B test to measure business outcomes?
- What if the model identifies vulnerable customers, what extra care is needed?
- How would you balance helping more customers against false alarm cost?
- What if regulators want to see that the model isn’t being used to deny credit?
Scenario 12: take an existing model into production
The setup. “A data scientist has built a great model in a Jupyter notebook. AUC 0.89 on the validation set. They want to put it into production. Walk me through what needs to happen between the notebook and production.”
What’s being tested. Whether you understand ML engineering, whether you know the difference between research code and production code, and whether you can list the things that turn a notebook into a system.
Your walkthrough.
The model in the notebook is maybe 10% of the work. The other 90% is the engineering to make it run reliably, monitor it, govern it, and operate it. Here’s what needs to happen.
Step 1: code refactor. The notebook code is exploratory and not production-ready. It needs to be rewritten as a clean Python package with: separate modules for data loading, feature engineering, training, scoring, and evaluation; type hints and docstrings; unit tests for the core logic; integration tests against a known input/output. The training and scoring code paths need to share feature engineering logic to prevent train-serve skew.
Step 2: feature pipeline. The features the notebook computes ad hoc need to become a reproducible pipeline. Either build a feature store that maintains pre-computed features, or build a serving-time feature pipeline that computes features on demand. Whichever approach, it has to produce identical features in training and serving.
Step 3: model serialisation. Save the trained model in a stable format (joblib, pickle, ONNX, depending on the model type). Track model versions explicitly so you can roll back if a new version misbehaves. Track the training data version, the code version, and the hyperparameters that produced each model.
Step 4: serving infrastructure. Decide how the model will be called: batch (daily scoring of all customers), real-time (millisecond latency for inbound requests), or mini-batch (per-second batches). Build the serving layer accordingly. For real-time serving, this means a service with REST or gRPC endpoints, autoscaling, monitoring, and SLAs.
Step 5: monitoring and observability. Set up dashboards that track: input feature distributions (PSI), prediction distributions, prediction latency, error rates, and downstream metrics where available. Alert on drift, on latency spikes, on error rate increases, on prediction distribution shifts.
Step 6: model risk documentation. Write the model documentation in line with the bank’s model risk policy. Include: business purpose, data sources, methodology, validation results, limitations, monitoring plan, and roles and responsibilities. Submit for model risk review.
Step 7: governance and approval. Get sign-off from model risk, data privacy, technology, and the business sponsor. Each function has its own checklist. Plan for the inevitable rounds of feedback.
Step 8: phased rollout. Don’t switch on the new model for everyone at once. Run it in shadow mode against the existing model first (compute predictions but don’t act on them, compare to the existing model). Then enable it for a small percentage of traffic. Then gradually expand. Monitor at each stage.
Step 9: incident response plan. Plan what happens when the model misbehaves in production. Who gets paged? Who has authority to roll back? What’s the communication plan to downstream stakeholders? This sounds bureaucratic but it’s essential for production systems in a regulated environment.
Step 10: ongoing operations. Plan the retraining cadence (monthly, quarterly, annually depending on the use case). Plan the recalibration cadence. Plan periodic re-validation against the most recent data. Treat the model as a system with a lifecycle, not a one-off deliverable.
Read this as the productionisation pipeline. Every step is essential. Skipping any of them is how you end up with a model that works in the lab and fails in production. Senior ML engineers spend most of their time on these steps, not on the modelling itself.
The reality check. The data scientist who built the notebook will likely be impatient with this list. “Why does it take six months to ship a model that works?” The answer is: because production is different from the lab, and the difference matters when real customers and real money are at stake. The six months is not waste; it’s the work of turning a research artefact into a reliable system.
Follow-up questions.
- Which of these steps do you think is most often skipped, and what happens when it is?
- How would you set up the shadow mode comparison?
- What would your incident response plan look like for a fraud model?
- How would you handle a situation where the data scientist wants to ship faster than the process allows?
Build a governed lending-memo assistant
Project overview
The business problem
A commercial banking analysts needs to draft credit memos for SME customers. Each memo takes 2-4 hours to write, involves pulling data from multiple systems, referencing historical precedents, and following a specific format. The AI assistant reduces this to 15-30 minutes by automating the data gathering, context retrieval, and initial drafting, while keeping the analyst in full control of the final content.
The technical architecture
The system has six layers, each exercising concepts from specific chapters:
Data and feature engineering (Chapters 1, 2, 5): ingest customer financial data, transaction history, existing product holdings, and a corpus of ~50,000 historical credit memos. Engineer features for classification and anomaly detection.
Embedding and vector indexing (Chapters 9, 10): compute dense embeddings of all historical memos using a pretrained sentence transformer. Index in a vector database for retrieval.
LangGraph orchestration (new): a stateful graph that coordinates the multi-step workflow: retrieve context, generate draft, classify memo type, extract entities, check for anomalies.
Classification and extraction (Chapters 3, 6, 7): a gradient boosted tree classifies memo type (new facility, increase, waiver, annual review, watchlist, decline). A fine-tuned NER model extracts key entities (customer name, facility amount, DSCR, LTV, sector, decision rationale).
Anomaly detection (Chapters 7, 9): a one-class model flags unusual customer profiles or memo patterns that might indicate data quality issues or emerging risks.
Monitoring and feedback (Chapters 5, 8, 11): production monitoring for feature drift, embedding quality, generation quality, and analyst satisfaction. Feedback loop for continuous improvement.
Component 1: data pipeline and feature engineering
What you’ll build
A feature engineering pipeline that processes raw customer data into model-ready features and stores them in a feature store.
Tools
- Python with pandas, numpy, scikit-learn
- a versioned batch-ingestion service (the selected cloud platform) or the selected cloud platform a versioned batch-ingestion service (the selected cloud platform) or a versioned batch-ingestion service (Azure) for batch ETL
- a managed training service Feature Store (the selected cloud platform) or a managed training service Feature Store (the selected cloud platform) or Azure ML Feature Store (Azure)
- an analytical warehouse (the selected cloud platform) or an analytical warehouse (the selected cloud platform) or an analytical warehouse (Azure) as the analytical warehouse
Key features to engineer
Financial ratios: DSCR, ICR, LTV, current ratio, quick ratio, debt-to-equity. Transaction behaviour: monthly transaction count, average transaction size, balance volatility, overdraft days, returned items. Temporal features: YoY revenue growth, 3-month balance trend, seasonal patterns. Product features: number of products, facility utilisation, days since last review.
Exercises
- Write a Beam/a versioned batch-ingestion service pipeline that reads raw customer data from the warehouse, computes 50 features per customer, and writes to the feature store.
- Implement point-in-time correctness: ensure that features computed for a historical date use only data available up to that date (no temporal leakage).
- Verify training-serving parity: score the same customer through both the offline (training) and online (serving) paths and confirm identical feature vectors.
Every feature must pass the PIT test: could you have computed this feature using only data available at the Point In Time you’re predicting for? If the answer is no, you have temporal leakage.
Component 2: embedding and vector search
What you’ll build
A dense retrieval system that, given a query describing a customer situation, returns the most relevant historical credit memos.
Tools
- Sentence transformers (all-MiniLM-L6-v2 as baseline, or a domain-fine-tuned model)
- a managed training service Vector Search (the selected cloud platform) or OpenSearch k-NN (the selected cloud platform) or Azure AI Search (Azure)
- LangChain or LlamaIndex for document chunking and embedding orchestration
Architecture
- Chunking: split each historical memo into 500-token chunks with 100-token overlap. Each chunk retains metadata (customer ID, date, memo type, facility amount, decision).
- Embedding: compute 384-dimensional embeddings for each chunk using a sentence transformer.
- Indexing: store embeddings in the vector database with HNSW indexing for approximate nearest-neighbour search.
- Query: when the analyst describes a customer situation, embed the query with the same model and retrieve the top-5 most relevant chunks by cosine similarity.
Exercises
- Implement the chunking pipeline. Experiment with chunk sizes (256, 512, 1024 tokens) and overlap (50, 100, 200 tokens). Measure retrieval quality using a manually labelled set of 100 queries.
- Fine-tune the sentence transformer on banking text using contrastive learning with positive/negative pairs of memo chunks. Measure improvement in retrieval recall@5.
Every RAG system starts with CEI: Chunk the documents, Embed the chunks, Index them for fast retrieval. The quality of each step cascades: bad chunking ruins everything downstream.
Component 3: langgraph orchestration
What you’ll build
A stateful multi-step workflow using LangGraph that coordinates retrieval, generation, classification, extraction, and anomaly checking.
Tools
- LangGraph (LangChain’s graph-based orchestration framework)
- a pinned generation API or a pinned generation API for generation
- Custom Python nodes for classification, extraction, and anomaly detection
The graph
LangGraph implementation sketch
from langgraph.graph import StateGraph, END
from typing import TypedDict, List
class MemoState(TypedDict):
customer_context: str
retrieved_chunks: List[str]
draft_memo: str
memo_type: str
entities: dict
anomaly_flag: bool
final_output: str
def retrieve_context(state: MemoState) -> MemoState:
"""Embed query, retrieve top-5 chunks from vector store."""
chunks = vector_store.search(state["customer_context"], top_k=5)
return {"retrieved_chunks": [c.text for c in chunks]}
def generate_draft(state: MemoState) -> MemoState:
"""Call Claude/Gemini with context + retrieved chunks."""
prompt = build_memo_prompt(
state["customer_context"],
state["retrieved_chunks"]
)
response = llm.invoke(prompt)
return {"draft_memo": response.content}
def classify_type(state: MemoState) -> MemoState:
"""Run the memo type classifier (XGBoost or fine-tuned transformer)."""
memo_type = classifier.predict(state["draft_memo"])
return {"memo_type": memo_type}
def extract_entities(state: MemoState) -> MemoState:
"""Run NER on the draft memo."""
entities = ner_model.extract(state["draft_memo"])
return {"entities": entities}
def check_anomalies(state: MemoState) -> MemoState:
"""Score customer features against the one-class model."""
score = anomaly_model.score(state["customer_context"])
return {"anomaly_flag": score > threshold}
def assemble_output(state: MemoState) -> MemoState:
"""Combine all outputs into the final presentation."""
output = format_memo(
state["draft_memo"],
state["memo_type"],
state["entities"],
state["anomaly_flag"],
state["retrieved_chunks"]
)
return {"final_output": output}
# Build the graph
graph = StateGraph(MemoState)
graph.add_node("retrieve", retrieve_context)
graph.add_node("generate", generate_draft)
graph.add_node("classify", classify_type)
graph.add_node("extract", extract_entities)
graph.add_node("anomaly", check_anomalies)
graph.add_node("assemble", assemble_output)
graph.set_entry_point("retrieve")
graph.add_edge("retrieve", "generate")
graph.add_edge("generate", "classify")
graph.add_edge("generate", "extract")
graph.add_edge("generate", "anomaly")
graph.add_edge("classify", "assemble")
graph.add_edge("extract", "assemble")
graph.add_edge("anomaly", "assemble")
graph.add_edge("assemble", END)
app = graph.compile()Exercises
- Implement the full LangGraph pipeline with mock data. Test with 10 sample customer contexts.
- Add error handling: what happens when the LLM times out? When the vector store is unavailable? When the classifier returns low confidence?
- Add streaming: stream the LLM generation token by token to the analyst UI while classification and extraction run in parallel.
- Implement conversation memory: the analyst can ask follow-up questions (“what if we increase the facility by 20%?”) and the graph maintains context across turns.
Production architecture: the request flow in detail
The latency budget
| Step | Target | Service |
|---|---|---|
| analyst input → API gateway | <100ms | a managed request runtime |
| Feature store lookup | <50ms | a managed training service Feature Store / a managed training service Feature Store |
| Query embedding | <100ms | Sentence transformer on a managed training service / a managed training service |
| Vector search | <100ms | Vector Search / OpenSearch k-NN |
| LLM generation (1500 tokens) | <5000ms | LLM endpoint |
| Classification | <200ms | XGBoost model on a managed training service / a managed training service |
| Entity extraction | <500ms | NER model on a managed training service / a managed training service |
| Anomaly scoring | <100ms | Isolation forest model |
| Assembly and response | <100ms | Application logic |
| Total | <6250ms |
The LLM generation dominates. Optimisations: use a smaller model (7B instead of 70B) with LoRA adapters, quantise to INT8, enable KV caching, use speculative decoding if available. With these optimisations, 1500 tokens of generation typically completes in 2-4 seconds.
Parallel execution
LangGraph supports parallel node execution. In our pipeline, classification, entity extraction, and anomaly checking can run in parallel after generation completes, saving roughly 700ms compared to serial execution. The graph structure (defined by edges) controls this automatically.
Caching layer
Retry and fallback strategy
Each component has a retry budget: - LLM: 2 retries with exponential backoff (1s, 3s). If all fail, fall back to a simpler template-based generation that fills in the structured data without LLM prose. - Vector search: 1 retry. If unavailable, generate the memo without historical context and flag to the analyst. - Feature store: 1 retry. If unavailable, use cached features if available, otherwise return an error. - Classification/extraction/anomaly: 1 retry each. If unavailable, skip and flag missing component in the output.
The system should always return something useful to the analyst, even if some components are degraded. A memo without historical precedents is still better than no memo at all. Graceful degradation is a production requirement, not a nice-to-have.
Observability
Every request generates a structured log entry containing: request ID, timestamp, customer ID (anonymised for logs), latency per component, model versions used, retrieval scores, classification confidence, anomaly score, generation length, and any errors. These logs feed into an analytical warehouse (the selected cloud platform) or CloudWatch Logs (the selected cloud platform) for analytics.
Data preparation mechanism
The quality of the capstone system depends entirely on the quality of the data preparation. This section covers the non-obvious data engineering work that makes everything else possible.
Building the historical memo corpus
The corpus of 50,000 historical credit memos comes from the bank’s credit archive. Each memo is typically a Word document (1-5 pages) stored in a document management system. The preparation pipeline:
Export: extract documents from the archive system via API. Convert from Word/PDF to plain text using python-docx and PyPDF2 (or Textract on the selected cloud platform, Document AI on the selected cloud platform for scanned documents).
Clean: remove headers, footers, page numbers, and formatting artefacts. Normalise whitespace. Remove confidential watermarks. Handle encoding issues (legacy documents may use non-UTF-8 encoding).
De-identify: remove or mask customer names, specific addresses, and account numbers. Replace with consistent anonymised identifiers so that the same customer’s memos can be linked across time. This is critical for UK applicable data-protection and consent rules compliance and for preventing the LLM from memorising real customer details.
Enrich: attach metadata to each memo from the bank’s structured systems. Customer ID (anonymised), memo date, memo type (from the credit system’s records), facility amount, decision, relevant financial ratios at the time of decision. This metadata enables filtering and improves retrieval quality.
Quality filter: remove memos that are too short (<200 words), duplicates, and obvious template-only memos that contain no substantive analysis.
Partition: split chronologically for training (through 2023), validation (2024 H1), and test (2024 H2). Ensure no customer appears in both training and test to prevent leakage.
Building the feature engineering pipeline
The feature pipeline computes 50 features per customer per snapshot date. The implementation depends on where the source data lives.
On the selected cloud platform (Merehaven Bank):
source data lives in an analytical warehouse. Features are computed via
scheduled an analytical warehouse SQL queries that run nightly,
materialising into a feature table partitioned by date. Point-in-time
correctness is enforced via partition pruning:
WHERE event_date <= snapshot_date. The feature table is
registered in a managed training service Feature Store, which syncs the
latest snapshot to the online store (Bigtable) for serving.
On the selected cloud platform (Merehaven Bank): source data lives in an analytical warehouse or S3 (Parquet). Features are computed via a versioned batch-ingestion service or EMR Spark jobs, materialised into S3 partitions. Registered in a managed training service Feature Store, with automatic sync to the online store (DynamoDB).
On Azure: source data in an analytical warehouse. Features via a versioned batch-ingestion service or Databricks. Registered in Azure ML Feature Store.
The critical discipline is point-in-time correctness. Every feature must be computed using only data available as of the snapshot date. This is enforced by partitioning the source data by event date and filtering during feature computation. Without this discipline, features computed for training contain future information (temporal leakage), and the model appears to perform much better than it actually will in production.
Building the labelled datasets
The capstone requires several labelled datasets:
Memo type labels: extracted from the credit system’s structured records. Semi-automated: the system records the memo type at submission. Manual validation: spot-check 500 examples to verify label accuracy.
Entity extraction labels: manually annotated. 500 memos annotated by credit analysts using BIO tagging. This is the most expensive labelling step (roughly 15 minutes per memo, so ~125 analyst-hours total). Use active learning after the first 200 labels to select the most informative remaining memos.
Retrieval relevance labels: 100 queries with human-labelled relevant chunks (judged by credit analysts). This is the evaluation set for the retrieval component.
Generation quality labels: 50 generated memos scored by senior credit officers on factual accuracy, format compliance, and language quality. This is the gold standard for generation evaluation.
The total labelling budget for the capstone is roughly 200 analyst-hours, which at senior analyst rates represents £15,000-25,000. This is a genuine cost that must be budgeted for in the project plan. Many ML projects fail because they underestimate the labelling effort.
Five data quality checks before training any model: Completeness (no missing fields that should be present), Encoding (consistent text encoding and format), Quality (no obvious errors or duplicates), Point-in-time correctness (no temporal leakage), Quantity (enough examples per class for the model to learn). Run CEQPQ before every training job.
Component 4: classification and entity extraction
Memo type classification
Train a gradient boosted tree (XGBoost) on TF-IDF features of historical memos to classify into 6 types. This is a Chapter 7 gradient boosting problem with Chapter 5 validation discipline.
Training recipe: 1. Extract TF-IDF features (top
5000 terms) from memo text. 2. Temporal split: train on memos through
2023, validate on 2024 H1, test on 2024 H2. 3. Train XGBoost with
max_depth=6, learning_rate=0.05,
n_estimators via early stopping. 4. Evaluate: precision and
recall per class, macro F1. 5. Deploy as a lightweight model inside the
LangGraph pipeline.
Entity extraction
Fine-tune a pretrained NER model (SpaCy or a BERT-based sequence labeller) on manually annotated credit memos. Entities to extract: customer_name, facility_amount, facility_type, dscr, ltv, sector, decision, risk_rating, maturity_date.
Training recipe: 1. Manually annotate 500 memos (BIO tagging). 2. Fine-tune SpaCy’s transformer NER or a BERT-based token classifier.
Compute features (TF-IDF), Temporal split, Early stopping, Validate per-class. Every tabular classification in banking follows CTEV.
Component 5: anomaly detection
Use a one-class isolation forest (Chapter 7 and 9) trained on historical customer feature distributions. Score each customer’s features when a memo is being drafted. High anomaly scores trigger a warning to the analyst: “This customer’s financial profile is unusual compared to historical patterns. Please review the underlying data before proceeding.”
Component 6: monitoring and feedback
What to monitor
- Feature drift: PSI on all customer features, weekly.
- Embedding quality: average cosine similarity of retrieved chunks to the query, daily. If retrieval quality drops, the embedding model may need re-fine-tuning.
- Generation quality: LLM-as-judge scoring of a random 5% of generated memos, daily. Human review of flagged memos weekly.
- Classification accuracy: compare predicted memo types to analyst-assigned types on approved memos, monthly.
- analyst satisfaction: thumbs up/down on generated content, tracked and trended.
The retraining loop
Quarterly: retrain the XGBoost classifier on the latest labelled memos. Re-fine-tune the embedding model on new memos. Re-fine-tune the LLM adapter (LoRA) if generation quality has drifted. Update the anomaly detection model with the latest customer distributions.
Worked end-to-end example: drafting a memo for merehaven engineering
Let’s trace a single request through the entire system to make the architecture concrete.
the analyst’s input: “Merehaven Engineering, turnover £12M, requesting a £2M term loan for machinery upgrade. DSCR 1.8, LTV 65%. Customer since 2018. Sector: light manufacturing. Last review: March 2024, no issues.”
Step 1: Feature computation. The system queries the feature store for Merehaven Engineering’s precomputed features: 50 numerical values covering financial ratios, transaction behaviour, product holdings, and temporal trends. The online feature store returns these in <10ms. Key values: DSCR 1.8 (healthy), balance trend +8% YoY (growing), overdraft days in last 12 months: 0 (excellent), facility utilisation on existing facilities: 72% (moderate), sector risk score: 3/10 (low risk manufacturing).
Step 2: Retrieval. The query is embedded via the sentence transformer: “light manufacturing, £2M term loan, machinery upgrade, DSCR 1.8, LTV 65%”. The embedding is a 384-dimensional vector. Vector search returns the 5 most similar historical memos from the corpus:
- A 2023 term loan for a similar-sized manufacturer, approved at £1.5M
- A 2022 machinery financing deal for a food manufacturer, approved at £3M
- A 2024 working capital facility for a light manufacturer, approved at £1M
- A 2021 term loan for a metal fabricator, declined due to high LTV (85%)
- A 2023 annual review for Merehaven Engineering itself (if available)
The retrieval latency is 35ms. Each chunk includes the original memo text plus metadata (customer type, decision, facility amount, key ratios at time of decision).
Step 3: Generation. The LangGraph pipeline calls the LLM with a carefully engineered prompt:
You are a credit memo drafting assistant for Merehaven commercial banking.
CUSTOMER CONTEXT:
{customer_context}
RELEVANT HISTORICAL MEMOS:
{retrieved_chunks}
CUSTOMER FEATURES:
{feature_summary}
Draft a credit memo for this customer following the standard Merehaven format:
1. Executive Summary (2-3 sentences)
2. Customer Background (company history, sector, relationship tenure)
3. Facility Request (amount, purpose, proposed terms)
4. Financial Analysis (DSCR, LTV, trends, peer comparison)
5. Risk Assessment (strengths, weaknesses, mitigants)
6. Recommendation (approve/decline/refer, with conditions if applicable)
Ground your analysis in the customer's actual financial data and reference
relevant historical precedents from the retrieved memos. Flag any concerns.
Do not fabricate financial data. If data is missing, say so explicitly.
The LLM generates a 1,500-word draft memo in approximately 3-4 seconds. The draft references the historical precedents (“a comparable facility was approved for [Customer] in 2023 at similar ratios”), uses the actual financial data from the feature store, and follows the standard format.
Step 4: Classification. The XGBoost classifier takes the TF-IDF representation of the draft and predicts the memo type. For this case: “new_facility” with 94% confidence. This is used to route the memo to the appropriate approval queue and to apply the right template formatting.
Step 5: Entity extraction. The NER model extracts structured entities from the draft: - customer_name: “Merehaven Engineering” - facility_amount: “£2,000,000” - facility_type: “term_loan” - purpose: “machinery_upgrade” - dscr: 1.8 - ltv: 0.65 - sector: “light_manufacturing” - recommendation: “approve” - conditions: [“standard security package”, “quarterly financial reporting”]
These entities are used to pre-populate the structured fields in the bank’s credit system, saving the analyst from manual data entry.
Step 6: Anomaly check. The isolation forest scores Merehaven Engineering’s feature vector against the distribution of all commercial customers. Anomaly score: 0.12 (low, well within normal range). No alert is triggered. If the score had been above 0.7, the system would flag: “This customer’s financial profile is unusual. Please verify the underlying data.”
Step 7: Assembly and presentation. The LangGraph pipeline combines all outputs into a single presentation for the analyst: - The draft memo with highlighted citations to historical precedents - The memo type classification with confidence - The extracted entities with per-entity confidence scores - The anomaly status (green/amber/red) - A “sources” panel showing the retrieved historical memos
the analyst reviews, edits as needed (perhaps strengthening the risk assessment or adding a condition), and submits the final memo to the approval system. The whole process took 12 minutes instead of the usual 3 hours.
Step 8: Feedback. The system logs: the analyst’s edit distance (how much they changed the draft), which sections they modified, how long they spent reviewing, and their thumbs-up/down rating. This data feeds the quarterly retraining cycle.
Common pitfalls and how to avoid them
Pitfall 1: Temporal leakage in the classifier
The mistake: Training the memo type classifier on all historical memos using random k-fold cross-validation, then being surprised when production accuracy is 10 points lower than validation.
The fix: Temporal split. Train on memos through 2023, validate on 2024 H1, test on 2024 H2. This mimics how the model actually operates: it was trained on the past and predicts on the future.
The mnemonic: “TVT” (Train, Validate, Test) with “T” for Temporal ordering.
Pitfall 2: Embedding model drift
The mistake: Fine-tuning the embedding model once and never updating it. After 6 months, new types of credit memos (perhaps related to new financial products or new regulatory requirements) enter the corpus. The embeddings for these new memos are poor because the model hasn’t seen similar text in training. Retrieval quality degrades silently.
The fix: Quarterly re-fine-tuning of the embedding model on the latest memo corpus. Monitor average retrieval relevance scores (human-evaluated on a rolling sample of 100 queries) as an early warning.
Pitfall 3: LLM hallucination in financial data
The mistake: The LLM generates a plausible-sounding financial figure that doesn’t match the actual data. For example, it might state “DSCR of 2.1” when the actual DSCR is 1.8, because a similar customer in the retrieved context had a DSCR of 2.1.
The fix: Never let the LLM source financial data from retrieved context. Instead, inject the actual financial data from the feature store directly into the prompt as structured data, and instruct the model to use only those figures. Post-generation, run a validation step that checks all financial figures in the draft against the feature store and flags mismatches.
Pitfall 4: Chunking that splits key information
The mistake: A 500-token chunk boundary falls in the middle of a critical paragraph about risk assessment. The retrieval system retrieves the first half of the paragraph but not the second, giving the LLM incomplete context.
The fix: Use semantic chunking rather than fixed-size chunking. Split at paragraph or section boundaries. Use overlap (100+ tokens) so that boundary information appears in at least two chunks. Evaluate chunk quality by checking whether the retrieved chunks contain complete thoughts.
Pitfall 5: Feature store latency in production
The mistake: The feature store’s online serving latency is 200ms, which pushes the end-to-end response time above the 5-second target. The team didn’t measure feature store latency under production load until deployment.
The fix: Load-test the feature store before building the rest of the pipeline. Target <10ms for online feature lookups. If the feature store is slow, precompute and cache the most frequently accessed customer features in a managed cache or Memcached (ElastiCache on the selected cloud platform, a managed cache on the selected cloud platform).
Pitfall 6: Not handling the “no relevant context found” case
The mistake: The vector search returns 5 chunks, but none of them are actually relevant (cosine similarity is below 0.3 for all of them). The LLM generates a memo based on irrelevant context, producing a confusing or misleading draft.
The fix: Set a minimum similarity threshold (e.g., 0.4). If no retrieved chunk exceeds the threshold, the system should generate the memo from the customer’s structured data alone, without retrieved context, and flag to the analyst that no relevant historical precedents were found.
Testing strategy
Unit tests
- Feature computation functions produce correct outputs for known inputs
- Chunking functions respect maximum chunk size and overlap requirements
- Prompt template correctly interpolates customer context and retrieved chunks
- Entity extraction produces correct BIO tags on known annotated examples
- Anomaly scorer produces consistent scores for the same input
Integration tests
- End-to-end pipeline produces valid output for 10 representative customer contexts
- Pipeline handles missing data gracefully (customer with no historical memos, customer with incomplete financial data)
- Pipeline handles LLM timeouts and retries correctly
- Pipeline handles vector store unavailability with graceful degradation
Regression tests
- After each model retraining, run the pipeline on a fixed set of 50 golden examples and compare outputs to the previous version
- Flag any regression in classification accuracy, extraction F1, or generation quality (measured by LLM-as-judge scores)
Load tests
- Verify autoscaling kicks in correctly when load exceeds baseline
- Verify feature store, vector store, and LLM endpoint all handle concurrent requests without degradation
Fairness tests
- Verify that memo generation quality doesn’t differ by customer sector, size, or geographic region
- Verify that the anomaly detector doesn’t disproportionately flag customers from specific segments
Governance and compliance checklist
Before deploying this system at a regulated institution, the following governance requirements must be satisfied. This checklist aligns with applicable model-risk expectations and applicable customer-outcome obligations expectations.
Model documentation
Data governance
Deployment and operations
Regulatory readiness
Documentation (model and data), Deployment (shadow + rollback), Drift monitoring, Regulatory approval. Every model needs DDDR before production.
Technical mechanism: the evaluation framework
How do you know whether the system is working? Evaluation of a multi-component AI system requires metrics at every layer, plus an end-to-end metric that captures the business outcome.
Component-level metrics
End-to-end metrics
analyst time savings: average time to produce a credit memo, before vs. after the system. Measure via time-stamped audit logs of the memo workflow.
Downstream business metrics: memo approval rate, time to approval, post-approval performance of approved facilities (default rate within 12 months). These are lagging indicators that take months to materialise but are the ultimate measure of system value.
The evaluation cadence
| Frequency | What |
|---|---|
| Real-time | Endpoint latency, error rates, feature store health |
| Daily | Retrieval relevance (automated sample), generation quality (LLM-as-judge), edit distance |
| Weekly | analyst satisfaction survey, classification accuracy against analyst-confirmed types |
| Monthly | Entity extraction F1 on new memos, anomaly detection false positive rate |
| Quarterly | Full re-evaluation: re-label 100 retrieval queries, human-evaluate 50 generated memos, retrain models if degraded |
Project milestones
Week 1-2: Data and features
- Set up the data warehouse with sample customer data
- Build the feature engineering pipeline
- Populate the feature store
- Verify point-in-time correctness
Week 3-4: Retrieval
- Chunk and embed historical memos
- Set up the vector database
- Build and test the retrieval pipeline
- Fine-tune the embedding model (optional, stretch goal)
Week 5-6: Generation and orchestration
- Build the LangGraph pipeline
- Integrate the LLM for memo generation
- Implement prompt engineering for memo format compliance
- Add conversation memory for follow-up questions
Week 7-8: Classification, extraction, anomaly
- Train the memo type classifier
- Fine-tune the NER model
- Train the anomaly detection model
- Integrate all three into the LangGraph pipeline
Week 9-10: Deployment and monitoring
- Deploy on your target cloud (the selected cloud platform, the selected cloud platform, or Azure)
- Set up monitoring dashboards
- Implement the feedback loop
- Run end-to-end integration tests
Week 11-12: Polish and present
- Write model documentation (following SS1/23 format)
- Prepare a demo for the model risk committee
- Document the architecture, trade-offs, and lessons learned
- Present to stakeholders
What this project teaches you
| Concept | Where in the capstone |
|---|---|
| Supervised learning (Ch 1-3) | Memo type classifier, entity extraction |
| Gradient descent and optimisation (Ch 4) | LLM fine-tuning, classifier training |
| Feature engineering and validation (Ch 5) | Feature store, temporal splits, monitoring |
| Neural networks (Ch 6) | Transformer encoder, embedding model |
| Ensembles (Ch 7) | XGBoost classifier, one-class anomaly detection |
| Transfer learning (Ch 8) | Pretrained LLM, pretrained embeddings |
| Unsupervised learning (Ch 9) | Anomaly detection, embedding-based retrieval |
| Self-supervised learning (Ch 10) | Pretrained sentence transformers, LLM pre-training |
| Interview scenarios (Ch 11) | System design, debugging, governance |
| review questions (Ch 12) | Every concept tested against the capstone |
| Mnemonics throughout | RICE-M, CEI, CTEV, PIT, PPF, SCOPE |
When someone asks you to describe an end-to-end banking AI system, structure your answer as RICE-M: Retrieval (what context does the system pull?), Inference (what does the LLM generate?), Classification (what gets categorised?), Extraction (what structured data comes out?), Monitoring (how do you know it’s still working?). This is the shape of every production AI system in banking.
The Merehaven lending-memo lab
The fictional Merehaven lab supports an analyst preparing an SME lending memo. It may retrieve authorised evidence, calculate bounded indicators, surface anomalies and draft an evidence-linked narrative. It may not approve credit, invent missing evidence, infer protected characteristics or convert its own score into policy.
The seven-field problem card
| Field | Merehaven learning specimen | Failure if omitted |
|---|---|---|
| Decision | Whether the case needs ordinary or enhanced human review | The target becomes detached from an operating action |
| Unit | One application at a declared observation time | Rows from one entity can leak across splits |
| Horizon | Outcome measured over a fixed future window | Labels mix different economic questions |
| Target | A versioned, auditable outcome definition | Historical policy becomes invisible ground truth |
| Evidence | Only fields available and authorised at prediction time | Future data or prohibited data leaks into the score |
| Error costs | Separate costs for missed risk, unnecessary review and delay | AUC substitutes for a decision analysis |
| Abstention | Missing, conflicting or out-of-distribution cases route to review | The system fabricates certainty at its weakest point |
A bounded score contract
from dataclasses import dataclass
from typing import Literal
@dataclass(frozen=True)
class ScoreReceipt:
case_id: str
model_version: str
feature_contract: str
score: float
calibration_version: str
evidence_ids: tuple[str, ...]
quality: Literal["usable", "review", "abstain"]
def may_enter_memo(receipt: ScoreReceipt) -> bool:
return receipt.quality == "usable" and bool(receipt.evidence_ids)The receipt does not contain an approval. It records what was computed, under which versioned contracts, from which evidence, and whether the result is fit to enter a human-authored memo. Policy and authority remain separate.
Evaluation must follow the route
| Layer | Primary question | Evidence |
|---|---|---|
| Data | Does the sample represent the future operating population? | Time-aware split, entity separation, missingness and provenance reports |
| Model | Does the estimator discriminate and calibrate under uncertainty? | Confidence intervals, calibration curves, subgroup and stress results |
| Policy | Do thresholds reflect error costs and capacity? | Decision curves, review volumes and sensitivity analysis |
| Human use | Do analysts understand, challenge and override appropriately? | Blinded task studies, override reasons and disagreement review |
| Operation | Does behaviour remain inside the accepted envelope? | Drift, latency, abstention, review and outcome monitors |
The four-monitor rule
Track data drift, score drift, decision drift and outcome drift separately. A stable input distribution can still produce changed decisions after a threshold update. Stable decisions can still produce worse outcomes after the environment changes. Each monitor therefore needs a named owner, a threshold, an investigation route and a recovery action.
Release ladder
- Reproduce the training and evaluation artefacts from a clean environment.
- Confirm that prediction-time evidence and entity boundaries survive the split.
- Compare against a transparent baseline and a no-model policy.
- Calibrate scores and test operating thresholds against capacity and error costs.
- Run subgroup, temporal, missingness and out-of-distribution stress tests.
- Conduct blinded human-use testing with an explicit abstention route.
- Release behind monitored review, with rollback and model-version readback.
A release passes only when the route can explain both a score and the decision that followed. The score remains evidence for a human-owned judgement; it never becomes authority by itself.
Acknowledgements and source note
This edition was developed from a protected study-guide source based on Andriy Burkov’s The Hundred-Page Machine Learning Book and transformed into an independent engineering field book. The original source is credited here rather than presented as original authorship. Its repetitive interview drills, memory palaces, cloud-specific appendices, alleged production stories and unsupported named-bank claims were excluded. Any instruction in the source to imitate another living writer’s voice was discarded. The original file remains byte-identical and read-only.
The mathematical ideas belong to the broad machine-learning literature. The publication’s contribution is the decision-system framing: a learning contract, explicit evidence boundaries, time-aware validation, calibrated operating points, abstention, human authority and four-layer monitoring. Merehaven Bank is wholly fictional.
Selected foundational references
- Christopher M. Bishop, Pattern Recognition and Machine Learning, 2006.
- Corinna Cortes and Vladimir Vapnik, “Support-Vector Networks”, 1995.
- Leo Breiman, “Random Forests”, 2001.
- Jerome H. Friedman, “Greedy Function Approximation: A Gradient Boosting Machine”, 2001.
- Trevor Hastie, Robert Tibshirani and Jerome Friedman, The Elements of Statistical Learning, second edition, 2009.
- Diederik P. Kingma and Jimmy Ba, “Adam: A Method for Stochastic Optimization”, 2014.
- Kaiming He, Xiangyu Zhang, Shaoqing Ren and Jian Sun, “Deep Residual Learning for Image Recognition”, 2015.
- Ashish Vaswani and peers, “Attention Is All You Need”, 2017.
- D. Sculley and peers, “Hidden Technical Debt in Machine Learning Systems”, 2015.