Chapter 4 — Anatomy of a Learning Algorithm¶
In the last chapter you met several ready-made models — linear regression, logistic regression, SVM, decision trees, and kNN. Under the hood, almost every one of them is assembled from the same handful of parts. This chapter opens up that "black box" and shows you what is inside, so that when you use a library later you actually understand what it is doing.
In this chapter you will learn:
- The four building blocks every learning algorithm is made of: a loss function, an optimization algorithm, a parameterized model, and an output.
- How gradient descent works — the single most important optimization method in machine learning — including the update rule $w \leftarrow w - \eta \nabla L$ and why the learning rate $\eta$ matters.
- How to code gradient descent from scratch with NumPy and watch it converge (and explode) on real plots.
- The typical workflow a machine learning engineer follows, from raw data to a deployed, monitored model.
- Practical differences between algorithms — speed, memory, interpretability, and the kinds of data they accept.
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rcParams
# Consistent, readable figures for every plot in this notebook.
rcParams['figure.figsize'] = (7, 4)
rcParams['figure.dpi'] = 110
rcParams['axes.grid'] = True
rcParams['grid.alpha'] = 0.3
print("imports ready")
imports ready
4.1 Building Blocks of a Learning Algorithm¶
Pick any learning algorithm and you will find the same four ingredients inside it. Think of them as the organs that keep the algorithm alive:
- A loss function — a way to score "how wrong is the model right now?"
- An optimization algorithm — the engine that tweaks the model's knobs to make that score smaller.
- A parameterized model $f(x)$ — the recipe that turns an input $x$ into a prediction, controlled by adjustable numbers called parameters (like $w$ and $b$).
- An output (prediction) — the answer the trained model gives for a new input.
They fit together like this:
training data --> (3) model f(x) --> prediction
| ^ |
v | v
(1) loss function ------+ (4) final output
|
v
(2) optimizer: tweak the parameters to shrink the loss
Everyday analogy: Imagine tuning a shower faucet. The model is "how far I turn the hot and cold handles." The loss is "how far the water temperature is from what I want." The optimizer is your hand adjusting the handles a little at a time. The output is the water that comes out.
The four ingredients in plain words¶
(1) The loss function. This is a single number that measures the gap between what the model predicts and what the data actually says. For regression the usual choice is the mean squared error (MSE) — the average of the squared differences between predictions and true values. A smaller loss means a better fit. Without a loss function the algorithm has no way to know whether a change is an improvement.
(2) The optimization algorithm. Once we can measure "wrongness," we need a procedure that adjusts the parameters to make it smaller. The star of this chapter is gradient descent. Some algorithms optimize an explicit criterion (linear/logistic regression, SVM); others, like decision trees and kNN, were built from intuition first and only later shown to be optimizing something.
(3) The parameterized model $f(x)$. This is the shape of the prediction rule, with numbers we get to learn. For linear regression it is $f(x) = wx + b$, where $w$ (the slope, or weight) and $b$ (the intercept, or bias) are the parameters. Different parameter values give different lines; learning means finding the values that minimize the loss.
(4) The output (prediction). After training, we feed the model a new input $x$ and it returns $f(x)$ — a number for regression, a class label (and sometimes a probability) for classification. This is the part the user actually sees.
# A toy look at the four building blocks, written out explicitly.
# (3) A parameterized model f(x) with parameters w and b
def f(x, w, b):
return w * x + b
# (1) A loss function: how wrong is the model on some data?
def mse_loss(x, y, w, b):
return ((y - f(x, w, b)) ** 2).mean()
x_demo = np.array([1.0, 2.0, 3.0])
y_demo = np.array([2.0, 4.1, 5.9]) # roughly y = 2x
# (2) An optimization algorithm tweaks w to reduce the loss.
# (Here we simply try several values and see which scores lowest.)
print("Trying different values of w (with b fixed at 0.5):")
for w_try in [0.5, 1.0, 1.5, 2.0, 2.5]:
print(f" w={w_try}: loss = {mse_loss(x_demo, y_demo, w_try, 0.5):.3f}")
# (4) An output: once we believe w=2 is best, the model predicts for new x
print("Prediction for x=4 with w=2, b=0.5:", f(4.0, 2.0, 0.5))
Trying different values of w (with b fixed at 0.5): w=0.5: loss = 7.657 w=1.0: loss = 2.857 w=1.5: loss = 0.390 w=2.0: loss = 0.257 w=2.5: loss = 2.457 Prediction for x=4 with w=2, b=0.5: 8.5
Notice how the loss changes as we try different values of $w$: it drops, reaches a lowest point around $w=2$, then rises again. That bowl shape is exactly what an optimizer exploits — it walks downhill until it can't go any lower. All four building blocks are present in the cell above: the model f, the loss mse_loss, the (very simple) optimizer loop, and the prediction at the end.
4.2 Gradient Descent¶
Gradient descent is the optimization algorithm behind linear regression, logistic regression, SVM, and — as you'll see later — neural networks. The idea is beautifully simple.
- The gradient $\nabla L$ is a vector of partial derivatives. Each entry tells you how steeply the loss increases if you nudge one parameter a tiny bit. It points in the steepest uphill direction.
- To go downhill, we move in the opposite direction: the negative gradient.
- The learning rate $\eta$ (pronounced "eta") controls how big each step is.
Putting it together gives the update rule, applied again and again:
$$w \leftarrow w - \eta \,\nabla L$$Read it as: "new parameter = old parameter minus a small step in the uphill direction." Because we subtract the uphill direction, we head downhill.
Everyday analogy: You are blindfolded on a hill and want to reach the bottom. You feel the slope under your feet (the gradient), then take a step in the downhill direction. A bigger learning rate means longer strides. Too small and you crawl forever; too large and you leap clear across the valley and end up higher than before.
# Gradient descent from scratch on a simple convex (bowl-shaped) loss.
# Loss: L(w, b) = (w - 3)^2 + (b + 1)^2
# Its minimum is at w = 3, b = -1, where the loss is 0.
def loss(w, b):
return (w - 3) ** 2 + (b + 1) ** 2
# The gradient: a vector of partial derivatives (points uphill).
def grad(w, b):
dL_dw = 2 * (w - 3) # dL/dw
dL_db = 2 * (b + 1) # dL/db
return dL_dw, dL_db
# The gradient-descent loop: start somewhere, step downhill repeatedly.
def gradient_descent(eta, epochs, w0=0.0, b0=0.0):
w, b = w0, b0
history = [loss(w, b)]
for _ in range(epochs):
gw, gb = grad(w, b)
w = w - eta * gw # update rule: w <- w - eta * gradient
b = b - eta * gb
history.append(loss(w, b))
return w, b, history
# Run with a sensible learning rate.
w_opt, b_opt, hist = gradient_descent(eta=0.1, epochs=60)
print(f"Converged to w={w_opt:.4f}, b={b_opt:.4f} (true minimum: w=3, b=-1)")
print(f"Final loss: {hist[-1]:.2e}")
fig, ax = plt.subplots()
ax.plot(hist, marker='o', ms=3)
ax.set_title("Gradient descent on $(w-3)^2 + (b+1)^2$ (learning rate $\eta=0.1$)")
ax.set_xlabel("Iteration")
ax.set_ylabel("Loss")
ax.set_yscale('log')
plt.show()
Converged to w=3.0000, b=-1.0000 (true minimum: w=3, b=-1) Final loss: 2.35e-11
The loss falls quickly at first, then slows as we approach the flat bottom of the bowl — exactly what we want. After 60 steps we land at $w \approx 3$, $b \approx -1$, the true minimum. Because this function is convex (a single bowl with one bottom), gradient descent is guaranteed to find that global minimum from any starting point, as long as $\eta$ is not too big.
# Now use a learning rate that is TOO LARGE.
# For this quadratic, eta > 1 makes each step overshoot and climb the
# opposite side -- the loss grows instead of shrinks.
w_div, b_div, hist_div = gradient_descent(eta=1.1, epochs=30)
fig, ax = plt.subplots()
ax.plot(hist_div, marker='o', color='crimson', ms=4)
ax.set_title("Too-large learning rate ($\eta=1.1$): loss EXPLODES")
ax.set_xlabel("Iteration")
ax.set_ylabel("Loss")
plt.show()
print(f"Loss went from {hist_div[0]:.2e} to {hist_div[-1]:.2e} in 30 steps")
Loss went from 1.00e+01 to 5.63e+05 in 30 steps
With $\eta = 1.1$ the steps are so large that each one overshoots the bottom and lands higher up the other side. The loss grows instead of shrinks — the algorithm has diverged. This is the central practical lesson: the learning rate is the most important knob. Pick it too small and training crawls; pick it too large and training blows up.
Gradient descent for linear regression¶
Now let's use the same idea to actually learn a model. Our model is $f(x) = wx + b$ and our loss is the MSE:
$$L = \frac{1}{N}\sum_{i=1}^{N}\big(y_i - (wx_i + b)\big)^2$$Using the chain rule from calculus, the partial derivatives (the gradient entries) are:
$$\frac{\partial L}{\partial w} = \frac{1}{N}\sum_{i=1}^{N} -2x_i\big(y_i - (wx_i + b)\big), \qquad \frac{\partial L}{\partial b} = \frac{1}{N}\sum_{i=1}^{N} -2\big(y_i - (wx_i + b)\big)$$In words: $\partial L/\partial w$ tells us how the loss changes when we wiggle the slope $w$, and $\partial L/\partial b$ does the same for the intercept $b$. We update both with the same rule, $w \leftarrow w - \eta\,\partial L/\partial w$ and $b \leftarrow b - \eta\,\partial L/\partial b$, looping over the whole dataset many times. One full pass over the data is called an epoch.
Let's code it from scratch and watch it learn.
# A tiny regression dataset: y = 1.8*x + 0.4 + small noise
np.random.seed(42)
x = np.linspace(0, 3, 30)
y = 1.8 * x + 0.4 + np.random.randn(30) * 0.15
N = len(x)
w, b = 0.0, 0.0 # start the parameters at zero
eta = 0.05 # learning rate
epochs = 200 # cap on passes over the data
losses = []
for epoch in range(epochs):
preds = w * x + b
error = y - preds
# Gradients of the Mean Squared Error
grad_w = (-2.0 / N) * np.sum(x * error)
grad_b = (-2.0 / N) * np.sum(error)
# Update rule
w = w - eta * grad_w
b = b - eta * grad_b
# Record the loss AFTER this epoch's update
losses.append(np.mean((y - (w * x + b)) ** 2))
print(f"Learned w={w:.3f}, b={b:.3f} (true w=1.8, b=0.4)")
print(f"Final MSE: {losses[-1]:.4f}")
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
ax1.plot(losses)
ax1.set_title("Training loss (MSE) vs. epoch")
ax1.set_xlabel("Epoch"); ax1.set_ylabel("MSE")
ax2.scatter(x, y, s=25, label="data")
ax2.plot(x, w * x + b, 'r-', label=f"fit: y={w:.2f}x+{b:.2f}")
ax2.set_title("Data and learned regression line")
ax2.set_xlabel("x"); ax2.set_ylabel("y"); ax2.legend()
plt.show()
Learned w=1.746, b=0.454 (true w=1.8, b=0.4) Final MSE: 0.0155
Two things happened: the MSE curve on the left drops and levels off — that is convergence. On the right, the red line learned by gradient descent sits right on top of the data, recovering $w \approx 1.8$ and $b \approx 0.4$, which are (up to noise) the true values we baked in. We built a working learning algorithm from nothing but NumPy and the update rule.
from sklearn.linear_model import SGDRegressor, LinearRegression
X = x.reshape(-1, 1)
# Practical version 1: scikit-learn's SGD-based regressor (same idea,
# library-grade, uses mini-batches internally).
sgd = SGDRegressor(learning_rate='constant', eta0=0.05, max_iter=200,
penalty=None, random_state=42)
sgd.fit(X, y)
# Practical version 2: the closed-form (normal equation) solution.
lr = LinearRegression().fit(X, y)
print("From-scratch GD : w = {:.3f}, b = {:.3f}".format(w, b))
print("SGDRegressor : w = {:.3f}, b = {:.3f}".format(
sgd.coef_[0], sgd.intercept_[0]))
print("LinearRegression : w = {:.3f}, b = {:.3f}".format(
lr.coef_[0], lr.intercept_))
From-scratch GD : w = 1.746, b = 0.454 SGDRegressor : w = 1.753, b = 0.462 LinearRegression : w = 1.749, b = 0.448
All three methods agree closely — our hand-written gradient descent matches scikit-learn's SGDRegressor and the closed-form LinearRegression. That is the point: the libraries are doing the same math you just wrote, only faster and with more polish.
Stochastic gradient descent (SGD) speeds things up on large datasets by estimating the gradient from a small random batch of examples instead of the whole dataset. Several upgrades exist:
| Variant | What it adds |
|---|---|
| SGD | Approximates the gradient with mini-batches — faster, slightly noisy |
| Momentum | Builds up velocity in a consistent direction to dampen oscillation |
| Adagrad | Adapts the learning rate per parameter based on gradient history |
| RMSprop | Adapts the rate using a moving average of squared gradients |
| Adam | Combines momentum + adaptive rates; the default choice for neural nets |
One important reminder: gradient descent and its variants are not machine learning algorithms — they are general solvers for any minimization problem where the function has a gradient.
4.3 How Machine Learning Engineers Work¶
In practice you rarely code gradient descent by hand. You reach for libraries (scikit-learn being the most common) and follow a workflow that looks roughly like this:
raw data
|
v
[ clean & explore ] --> [ extract / pick features ]
| |
| v
| [ choose a model ]
| |
+<---------------------------+
|
v
[ split: train / validation / test ]
|
v
[ train -> tune hyperparameters on validation ]
|
v
[ evaluate on the test set ]
|
v
[ deploy to production ]
|
v
[ monitor performance, retrain on new data ]
A few plain-language notes on each stage:
- Clean & explore: look at the data, fix errors, handle missing values, spot obvious patterns.
- Features: decide which columns the model is allowed to see; convert categories to numbers, scale values (coming in Chapter 5).
- Choose a model: pick a family — linear model, tree, kNN, etc. — often starting simple.
- Split the data: train on one portion, tune on another (validation), and keep a third (test) untouched until the very end to get an honest score.
- Train & tune: fit the model, then adjust its hyperparameters (settings chosen by you, not learned from data) using the validation set.
- Evaluate: report the test-set score. If it is good enough, proceed.
- Deploy & monitor: put the model where it can make real predictions, then watch it. If the world changes, retrain.
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_classification
# A miniature end-to-end workflow on synthetic data.
X, y = make_classification(n_samples=300, n_features=4, n_informative=3,
n_redundant=0, random_state=42)
# 1) Split into train / test
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25,
random_state=42)
# 2) Build a pipeline: scale features, then fit a model
pipe = make_pipeline(StandardScaler(),
LogisticRegression(max_iter=200, n_jobs=1))
# 3) Train (fit) on the training split
pipe.fit(X_tr, y_tr)
# 4) Evaluate on the held-out test split
print(f"Train accuracy: {pipe.score(X_tr, y_tr):.3f}")
print(f"Test accuracy : {pipe.score(X_te, y_te):.3f}")
Train accuracy: 0.849 Test accuracy : 0.867
That snippet is the workflow in miniature: make data, split it, build a Pipeline that scales then classifies, fit on the train split, and score on the held-out test split. The test accuracy is the honest number we report; the gap between train and test accuracy hints at whether we are overfitting (a topic for later chapters).
4.4 Learning Algorithms' Particularities¶
Not all algorithms are interchangeable. In practice they differ in ways that matter when you pick one:
| Property | What it means | Example |
|---|---|---|
| Hyperparameters | Settings you choose before training | $C$ in SVM, tree depth, learning rate $\eta$ |
| Input data type | Categorical vs. numerical features | Trees accept "red/yellow/green"; SVM & kNN need numbers |
| Class weighting | Let you care more about rare classes | SVM can up-weight a minority class |
| Output type | Label only vs. label + probability/score | SVM/kNN give a class; logistic regression gives a probability |
| Retraining | Rebuild from scratch vs. update incrementally | Trees/SVM/kNN rebuild; SGD models & Naive Bayes update online |
| Task types | Classification only, regression only, or both | Trees, SVM, kNN do both; logistic regression classifies |
| Speed & memory | Training/prediction time and RAM footprint | kNN is slow at predict time; trees are fast |
Two extra points worth memorizing:
- Interpretability matters when a human must justify a decision. A small decision tree or linear model is easy to read; a neural network is not.
- Data scaling is required by distance- and gradient-based methods (kNN, SVM, linear/logistic regression, neural nets). Tree-based methods mostly do not care.
Every library documents these properties, so reading the docs for an algorithm before using it is time well spent.
Key Takeaways¶
- Every learning algorithm is built from four parts: a loss function, an optimization algorithm, a parameterized model $f(x)$, and an output (prediction).
- Gradient descent repeatedly applies $w \leftarrow w - \eta\,\nabla L$ to walk downhill on the loss surface. The gradient points uphill, so subtracting it goes downhill.
- The learning rate $\eta$ is the most important knob: too small crawls, too large diverges. On a convex loss, a reasonable $\eta$ finds the global minimum.
- You can implement gradient descent from scratch in a few lines of NumPy and recover the same answer scikit-learn's
SGDRegressorgives. - One full pass over the data is an epoch; SGD estimates the gradient with mini-batches, and variants like Momentum, Adagrad, RMSprop, and Adam refine the idea.
- Gradient descent is an optimizer, not a learning algorithm itself — it is a general minimization tool.
- The engineer's workflow is: clean data → choose features → pick a model → split into train/validation/test → train & tune → evaluate → deploy → monitor.
- Algorithms differ in hyperparameters, accepted data types, class weighting, output scores, retraining style, speed, and interpretability — read the docs before choosing.
What's Next¶
Now that you know what is inside a learning algorithm, Chapter 5 — "Basic Practice" — shows you what it looks like to actually apply one to real, messy data: how to turn categories into numbers, scale features, split data properly, and pick the right model for the job.
Exercises¶
These exercises cover the anatomy of a learning algorithm from Chapter 4: the four building blocks, gradient descent, the engineer's workflow, and the practical differences between algorithms. Try each before reading the hint.
- (Conceptual) Name the four building blocks every learning algorithm is made of, and map each one onto linear regression. Hint: loss = MSE, optimizer = gradient descent, model = $f(x)=wx+b$, output = the predicted number.
- (Conceptual) The update rule is $w \leftarrow w - \eta\,\nabla L$. Why do we subtract the gradient rather than add it? Hint: the gradient points in the steepest uphill direction, so subtracting it steps downhill.
- (Conceptual) Why is the learning rate $\eta$ called the most important knob? Describe both failure modes. Hint: too small ⇒ training crawls and may never finish; too large ⇒ steps overshoot and the loss diverges.
- (Conceptual) Why is gradient descent guaranteed to find the global minimum on linear regression's MSE, but not on an arbitrary loss? Hint: MSE is convex (a single bowl with one bottom); non-convex losses can have local minima.
- (Conceptual) Distinguish a parameter from a hyperparameter with one example each, and explain why hyperparameters are tuned on a validation set rather than the test set. Hint: $w,b$ are learned parameters; $\eta$, $C$,
max_depthare hyperparameters; tuning on the test set leaks information and overstates performance. - (Conceptual) Which algorithms require feature scaling and which mostly do not, and why? Give one example of each. Hint: distance- and gradient-based methods (kNN, SVM, linear/logistic regression, neural nets) need scaling; tree-based methods do not.
Hands-On Coding Problems¶
- (Coding) Implement gradient descent on the 1D quadratic $L(w)=(w-3)^2$: start at $w=0$, run 60 steps with $\eta=0.1$, and print the final $w$ (should be ≈3). Hint: gradient $= 2(w-3)$; update
w -= eta * 2 * (w - 3). - (Coding) Show divergence: on the same $L(w)=(w-3)^2$, set $\eta=1.1$ and print the loss every step for ~10 steps — watch it grow instead of shrink. Hint: loss
= (w - 3) ** 2; with too-large $\eta$ each step lands higher on the opposite side. - (Coding) Code gradient descent from scratch for linear regression on
make_regression(n_samples=100, n_features=1, noise=10, random_state=0): implement the MSE gradients for $w$ and $b$, loop over ~100 epochs, and recover $w$ and $b$. Hint: $\partial L/\partial w = -(2/N)\sum x_i(y_i - wx_i - b)$ and $\partial L/\partial b = -(2/N)\sum (y_i - wx_i - b)$. - (Coding) Compare three solutions for the same data: your from-scratch GD,
LinearRegression(closed form), andSGDRegressor(max_iter=1000, tol=1e-3). Print all three $(w, b)$ pairs and confirm they agree. Hint: scaleXfirst soSGDRegressorbehaves well, and compare slopes/intercepts. - (Coding) Mini end-to-end workflow: build
make_classification, split withtrain_test_split, fit aPipelineofStandardScaler+LogisticRegression, and print both train and test accuracy; comment on what a large gap would mean. Hint:from sklearn.pipeline import make_pipeline. - (Coding) Demonstrate scaling's effect: create a
make_classificationdataset, then artificially multiply one feature by 1000. FitKNeighborsClassifieron the unscaled vsStandardScaler-scaled data and print both test accuracies. Hint: kNN uses Euclidean distance, so the giant feature dominates until you scale.
# Exercise 7: gradient descent on a 1D quadratic
w = 0.0
eta = 0.1
for step in range(60):
# TODO: gradient of L(w) = (w - 3)^2 is 2*(w - 3)
grad = 0.0 # placeholder
w -= eta * grad
print("final w =", w, " (expected ~3)")
# Exercise 8: divergence with too-large learning rate
w = 0.0
eta = 1.1
for step in range(10):
loss = (w - 3) ** 2
print(f"step {step}: w={w:.4f}, loss={loss:.4f}")
# TODO: update w using grad = 2*(w - 3)
grad = 0.0 # placeholder
w -= eta * grad
# Exercise 9: from-scratch gradient descent for linear regression
import numpy as np
from sklearn.datasets import make_regression
X, y = make_regression(n_samples=100, n_features=1, noise=10, random_state=0)
x = X.ravel()
N = len(y)
w, b = 0.0, 0.0
eta = 0.1
epochs = 100
for epoch in range(epochs):
# TODO: compute gradients and update w, b
# dL/dw = -(2/N) * sum( x_i * (y_i - w*x_i - b) )
# dL/db = -(2/N) * sum( y_i - w*x_i - b )
dw = 0.0 # placeholder
db = 0.0 # placeholder
w -= eta * dw
b -= eta * db
print(f"learned w={w:.3f}, b={b:.3f}")
# Exercise 10: compare GD vs LinearRegression vs SGDRegressor
import numpy as np
from sklearn.linear_model import LinearRegression, SGDRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import make_regression
X, y = make_regression(n_samples=100, n_features=1, noise=10, random_state=0)
# Closed form
lr = LinearRegression().fit(X, y)
print("LinearRegression:", lr.coef_[0], lr.intercept_)
# SGD (scale features first)
scaler = StandardScaler().fit(X)
Xs = scaler.transform(X)
sgd = SGDRegressor(max_iter=1000, tol=1e-3, random_state=0).fit(Xs, y)
# TODO: convert SGD's slope/intercept back to the unscaled feature space, or just compare on scaled data
print("SGDRegressor (scaled space):", sgd.coef_[0], sgd.intercept_)
# TODO: paste your from-scratch GD w, b here and compare
# Exercise 11: mini end-to-end workflow
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
X, y = make_classification(n_samples=500, n_features=5, n_informative=4,
n_redundant=0, random_state=0)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, random_state=0)
# TODO: build make_pipeline(StandardScaler(), LogisticRegression(max_iter=500)) and fit on Xtr
pipe = None # placeholder
# TODO: print .score on Xtr and Xte, and comment on what a big gap would mean
# Exercise 12: effect of feature scaling on kNN
import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
X, y = make_classification(n_samples=400, n_features=3, n_informative=3,
n_redundant=0, random_state=0)
X_scaled_bad = X.copy()
X_scaled_bad[:, 0] *= 1000 # blow up one feature's scale
Xtr, Xte, ytr, yte = train_test_split(X_scaled_bad, y, test_size=0.3, random_state=0)
# TODO: fit a plain KNeighborsClassifier and a make_pipeline(StandardScaler(), KNeighborsClassifier)
# Print both test accuracies.
unscaled_acc = 0.0 # placeholder
scaled_acc = 0.0 # placeholder
print("unscaled test accuracy:", unscaled_acc)
print("scaled test accuracy :", scaled_acc)