Final AI Project
Reviewed & published by Brayan K
In this capstone you'll build one complete machine learning project end-to-end — loading data, training a model from scratch, evaluating it, and making predictions — so you finish able to ship real AI work.
Part of the free AI & Machine Learning course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Load and inspect a dataset using plain Python lists and dicts
- Preprocess features and split data into train and test sets
- Train a perceptron classifier from scratch — no libraries
- Evaluate with accuracy, precision, recall, and a confusion matrix
- Make predictions on brand-new, unseen data correctly
- Map your from-scratch code onto the scikit-learn equivalent
1 The Project: A Fruit Classifier, Built Across 5 Milestones
You'll build one project the whole way through: a classifier that decides whether a fruit is an apple or an orange from two numbers — its weight and how bumpy its skin is. It's deliberately tiny so the data fits on screen and you can check every number by hand. The workflow, though, is identical to a million-row production system.
The five milestones — the universal ML workflow:
- Load & inspect — get the data into Python and look at it
- Preprocess & split — clean, scale, and hold out a test set
- Train — fit a perceptron from scratch
- Evaluate — accuracy, precision, recall, confusion matrix
- Predict — classify brand-new fruit
All code below is plain Python — no numpy, no scikit-learn — so nothing is hidden. At the end you'll see the same project in scikit-learn to connect what you built to the tools the industry uses.
2 Milestone 1 — Load & Inspect the Dataset
Every project starts by getting data into a shape you can work with and looking at it. Here the dataset is a list of dictionaries — one dict per fruit. Before modelling anything, you check how many samples there are and whether the classes are balanced. A lopsided dataset (say 95% apples) changes every decision that follows.
Run it and read the comments — the # Expected output at the bottom tells you exactly what you should see.
# ============================================
# MILESTONE 1: LOAD & INSPECT THE DATASET
# ============================================
# A tiny fruit dataset. Each row = one fruit.
# Two features: weight (grams) and bumpiness (0 = smooth, 10 = very bumpy).
# Label: "apple" or "orange".
dataset = [
{"weight": 150, "bumpiness": 2, "label": "apple"},
{"weight": 170, "bumpiness": 3, "label": "apple"},
{"weight": 140, "bumpiness": 1, "label": "apple"},
{"weight": 130, "bumpiness": 2, "label": "apple"},
{"weight": 180, "bumpiness": 7, "label": "orange"},
{"weight": 200, "bumpiness": 8, "label": "orange"},
{"weight": 190, "bumpiness": 9, "label": "orange"},
{"weight": 210, "bumpiness": 7, "label": "orange"},
]
# Inspect: how many rows, and how many of each label?
print("Total samples:", len(dataset))
counts = {}
for row in dataset:
label = row["label"]
counts[label] = counts.get(label, 0) + 1
print("Class counts:", counts)
# Peek at the first 3 rows (like df.head() in pandas)
print("First 3 rows:")
for row in dataset[:3]:
print(" ", row)
# Expected output:
# Total samples: 8
# Class counts: {'apple': 4, 'orange': 4}
# First 3 rows:
# {'weight': 150, 'bumpiness': 2, 'label': 'apple'}
# {'weight': 170, 'bumpiness': 3, 'label': 'apple'}
# {'weight': 140, 'bumpiness': 1, 'label': 'apple'}3 Milestone 2 — Preprocess & Split
Models work on numbers, so first you encode the text labels (apple → 0, orange → 1). Then you scale the features to a 0-1 range — without this, weight (around 200) would completely drown out bumpiness (around 10) and the model would barely notice the bumpiness at all.
Finally you split the data: most rows train the model, a few are held back to test it. The golden rule of ML is that you never test on data the model trained on — that's like giving students the exam answers in advance.
# ============================================
# MILESTONE 2: PREPROCESS + TRAIN/TEST SPLIT
# ============================================
dataset = [
{"weight": 150, "bumpiness": 2, "label": "apple"},
{"weight": 170, "bumpiness": 3, "label": "apple"},
{"weight": 140, "bumpiness": 1, "label": "apple"},
{"weight": 130, "bumpiness": 2, "label": "apple"},
{"weight": 180, "bumpiness": 7, "label": "orange"},
{"weight": 200, "bumpiness": 8, "label": "orange"},
{"weight": 190, "bumpiness": 9, "label": "orange"},
{"weight": 210, "bumpiness": 7, "label": "orange"},
]
# 1) Encode labels as numbers: apple -> 0, orange -> 1
def encode(label):
return 0 if label == "apple" else 1
# 2) Scale features to roughly 0-1 so weight (~200) does not
# dominate bumpiness (~10). This is "min-max normalisation".
weights = [r["weight"] for r in dataset]
bumps = [r["bumpiness"] for r in dataset]
w_min, w_max = min(weights), max(weights)
b_min, b_max = min(bumps), max(bumps)
def scale(row):
w = (row["weight"] - w_min) / (w_max - w_min)
b = (row["bumpiness"] - b_min) / (b_max - b_min)
return [w, b]
# Build feature rows X and label list y
X = [scale(r) for r in dataset]
y = [encode(r["label"]) for r in dataset]
# 3) Train/test split. Keep it deterministic: take 1 apple + 1
# orange out for testing so the test set has both classes.
test_index = [3, 7] # one apple, one orange
train_index = [i for i in range(len(X)) if i not in test_index]
X_train = [X[i] for i in train_index]
y_train = [y[i] for i in train_index]
X_test = [X[i] for i in test_index]
y_test = [y[i] for i in test_index]
print("Train size:", len(X_train), "Test size:", len(X_test))
print("First scaled train row:", [round(v, 2) for v in X_train[0]])
print("y_train:", y_train)
print("y_test: ", y_test)
# Expected output:
# Train size: 6 Test size: 2
# First scaled train row: [0.25, 0.12]
# y_train: [0, 0, 0, 1, 1, 1]
# y_test: [0, 1]4 Milestone 3 — Train a Perceptron From Scratch
A perceptron is the simplest learning unit there is — the great-grandparent of every neural network. It multiplies each feature by a weight, adds a bias, and fires 1 if the total is positive, else 0.
"Training" just means: for each example, make a guess, and if it's wrong, nudge the weights a little in the direction that would have been right. Repeat over the whole dataset a few times (each pass is an epoch) until it stops making mistakes. That single line — weights[i] += learning_rate * error * features[i] — is the entire learning algorithm.
# ============================================
# MILESTONE 3: TRAIN A PERCEPTRON FROM SCRATCH
# ============================================
# A perceptron is the simplest neural unit: it multiplies each
# feature by a weight, adds a bias, and fires 1 if the total is
# positive, else 0. Training nudges the weights toward correct answers.
# Scaled training data from Milestone 2 (apple=0, orange=1)
X_train = [
[0.25, 0.12], [0.50, 0.25], [0.12, 0.00], # apples
[0.62, 0.75], [0.88, 0.88], [0.75, 1.00], # oranges
]
y_train = [0, 0, 0, 1, 1, 1]
# Start with zero weights and bias
weights = [0.0, 0.0]
bias = 0.0
learning_rate = 0.1
def predict(features, weights, bias):
total = bias
for i in range(len(features)):
total += features[i] * weights[i]
return 1 if total >= 0 else 0 # step activation
# Train for several passes over the data (epochs)
for epoch in range(20):
errors = 0
for features, target in zip(X_train, y_train):
guess = predict(features, weights, bias)
error = target - guess # 0 if correct, +1 or -1 if wrong
if error != 0:
errors += 1
# Perceptron update rule: move weights toward the right answer
for i in range(len(weights)):
weights[i] += learning_rate * error * features[i]
bias += learning_rate * error
if errors == 0:
print("Converged at epoch", epoch)
break
print("Final weights:", [round(w, 3) for w in weights])
print("Final bias:", round(bias, 3))
# Expected output:
# Converged at epoch 2
# Final weights: [0.087, 0.15]
# Final bias: -0.15 Milestone 4 — Evaluate the Model
A trained model is worthless until you know how good it is on data it has never seen. You run it on the held-out test set and count four outcomes into a confusion matrix:
- TP (true positive): predicted orange, actually orange
- FP (false positive): predicted orange, actually apple — a false alarm
- TN (true negative): predicted apple, actually apple
- FN (false negative): predicted apple, actually orange — a miss
From those four numbers you compute accuracy (overall correctness), precision (when it says orange, how often it's right), and recall (of all the real oranges, how many it caught).
# ============================================
# MILESTONE 4: EVALUATE THE MODEL
# ============================================
# Trained perceptron from Milestone 3 (orange is the positive class)
weights = [0.087, 0.15]
bias = -0.1
# Held-out test set from Milestone 2 (apple=0, orange=1)
X_test = [[0.00, 0.25], [1.00, 0.75]] # one apple, one orange
y_test = [0, 1]
def predict(features, weights, bias):
total = bias
for i in range(len(features)):
total += features[i] * weights[i]
return 1 if total >= 0 else 0
# Collect predictions, then build a confusion matrix.
# Positive class = orange (1).
tp = fp = tn = fn = 0
for features, actual in zip(X_test, y_test):
pred = predict(features, weights, bias)
if pred == 1 and actual == 1:
tp += 1 # caught an orange
elif pred == 1 and actual == 0:
fp += 1 # false alarm
elif pred == 0 and actual == 0:
tn += 1 # correct apple
else:
fn += 1 # missed an orange
accuracy = (tp + tn) / len(y_test)
precision = tp / (tp + fp) if (tp + fp) else 0
recall = tp / (tp + fn) if (tp + fn) else 0
print("Confusion matrix (positive = orange):")
print(" TP:", tp, " FP:", fp, " TN:", tn, " FN:", fn)
print("Accuracy: ", round(accuracy, 2))
print("Precision:", round(precision, 2))
print("Recall: ", round(recall, 2))
# Expected output:
# Confusion matrix (positive = orange):
# TP: 1 FP: 0 TN: 1 FN: 0
# Accuracy: 1.0
# Precision: 1.0
# Recall: 1.06 Milestone 5 — Predict on New Data
This is the payoff: feeding the model fruit it has never seen and getting a label back. The one trick beginners miss is that new data must be scaled with exactly the same min/max you used in training — re-fitting the scaler on new data would shift everything and corrupt the predictions.
Notice the third fruit (160g, bumpiness 6) sits near the boundary — a great reminder that real models output a decision even when the answer is genuinely uncertain.
# ============================================
# MILESTONE 5: PREDICT ON NEW, UNSEEN FRUIT
# ============================================
weights = [0.087, 0.15]
bias = -0.1
# The SAME scaling we fit in Milestone 2 (do not re-fit on new data!)
w_min, w_max = 130, 210
b_min, b_max = 1, 9
def scale(weight, bumpiness):
w = (weight - w_min) / (w_max - w_min)
b = (bumpiness - b_min) / (b_max - b_min)
return [w, b]
def classify(weight, bumpiness):
features = scale(weight, bumpiness)
total = bias
for i in range(len(features)):
total += features[i] * weights[i]
return "orange" if total >= 0 else "apple"
# Three new fruits the model has never seen
new_fruits = [
{"weight": 145, "bumpiness": 2}, # light + smooth -> apple
{"weight": 205, "bumpiness": 8}, # heavy + bumpy -> orange
{"weight": 160, "bumpiness": 6}, # in between
]
for fruit in new_fruits:
label = classify(fruit["weight"], fruit["bumpiness"])
print(f"weight={fruit['weight']}g bumpiness={fruit['bumpiness']} -> {label}")
# Expected output:
# weight=145g bumpiness=2 -> apple
# weight=205g bumpiness=8 -> orange
# weight=160g bumpiness=6 -> orange🏭 The Industry Version (scikit-learn) — Read-Only
You just built every piece by hand. In a real job you'd reach for scikit-learn, which collapses all five milestones into a handful of lines. Read this and match each call to the milestone it replaces — MinMaxScaler is Milestone 2, Perceptron().fit() is Milestone 3, the metric functions are Milestone 4, and .predict() is Milestone 5.
# ============================================
# THE SAME PROJECT, THE PROFESSIONAL WAY (scikit-learn)
# Read-only: this is what your from-scratch code maps to in industry.
# ============================================
from sklearn.linear_model import Perceptron
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, precision_score, recall_score
# Features [weight, bumpiness] and labels (apple=0, orange=1)
X = [[150, 2], [170, 3], [140, 1], [130, 2],
[180, 7], [200, 8], [190, 9], [210, 7]]
y = [0, 0, 0, 0, 1, 1, 1, 1]
# Split, scale, train — the four lines that replace ~80 of ours
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25, random_state=0)
scaler = MinMaxScaler().fit(X_tr)
model = Perceptron().fit(scaler.transform(X_tr), y_tr)
# Evaluate
pred = model.predict(scaler.transform(X_te))
print("Accuracy: ", round(accuracy_score(y_te, pred), 2))
print("Precision:", round(precision_score(y_te, pred, zero_division=0), 2))
print("Recall: ", round(recall_score(y_te, pred, zero_division=0), 2))
# Predict a brand-new fruit
print("New fruit ->", model.predict(scaler.transform([[205, 8]]))[0])
# Expected output:
# Accuracy: 1.0
# Precision: 1.0
# Recall: 1.0
# New fruit -> 1Same result, a fraction of the code. The point of building it from scratch first is that none of this is a black box to you anymore — you know precisely what each line is doing.
🎯 Your Turn 1 — Build a k-NN Classifier
You've built a perceptron. Now build the other classic classifier — k-Nearest Neighbours — by filling in two blanks. k-NN classifies a point by letting its closest labelled neighbours vote. Fill in the squared difference in the distance function and the majority threshold.
# 🎯 YOUR TURN 1 — k-NN: measure how "close" two fruits are
# k-Nearest Neighbours classifies a point by looking at the labelled
# points nearest to it. The engine is a distance function. Fill it in.
X_train = [[0.25, 0.12], [0.50, 0.25], [0.12, 0.00],
[0.62, 0.75], [0.88, 0.88], [0.75, 1.00]]
y_train = [0, 0, 0, 1, 1, 1] # apple=0, orange=1
def distance(a, b):
total = 0
for i in range(len(a)):
diff = a[i] - b[i]
total += ___ # 👉 add the SQUARED difference (diff * diff)
return total ** 0.5 # square root = Euclidean distance
def knn_predict(point, k=3):
# Pair every training point with its distance to 'point'
pairs = [(distance(point, X_train[i]), y_train[i]) for i in range(len(X_train))]
pairs.sort() # nearest first
top = pairs[:k] # k closest neighbours
votes = sum(label for _, label in top)
return 1 if votes > ___ else 0 # 👉 majority of k=3 means more than 1 vote
# A mystery fruit, already scaled
mystery = [0.80, 0.90] # heavy + bumpy
print("k-NN says:", "orange" if knn_predict(mystery) == 1 else "apple")
# ✅ Expected output:
# k-NN says: orange🎯 Your Turn 2 — Report the F1 Score
On imbalanced problems (fraud, disease, spam) accuracy is misleading and the F1 score rules. Finish the recall and F1 formulas below, then check your answer against the expected output.
# 🎯 YOUR TURN 2 — report the F1 score
# Accuracy lies when classes are imbalanced. F1 balances precision
# and recall into one number. Finish the two blanks.
# Results from evaluating a model (positive class = fraud)
tp, fp, fn = 8, 2, 4
precision = tp / (tp + fp) # of flagged items, how many were right
recall = tp / (tp + ___) # 👉 of all real positives, how many we caught
# F1 is the harmonic mean of precision and recall
f1 = 2 * precision * recall / (precision + ___) # 👉 add 'recall' here
print("Precision:", round(precision, 2))
print("Recall: ", round(recall, 2))
print("F1 score: ", round(f1, 2))
# ✅ Expected output:
# Precision: 0.8
# Recall: 0.67
# F1 score: 0.73🏁 Stretch Challenge — Package It Into a Class
Support has faded — only a comment outline remains. Combine Milestones 2, 3, and 5 into a single reusable FruitClassifier class with fit() and predict() methods. This mirrors exactly how scikit-learn estimators are structured.
# 🏁 STRETCH CHALLENGE — wrap the whole pipeline into one class
# Support has faded: only the outline is here. You write the logic.
#
# Goal: a FruitClassifier you can train once and reuse.
#
# class FruitClassifier:
# def fit(self, X, y):
# # 1. store min/max of each column for scaling (Milestone 2)
# # 2. run the perceptron training loop (Milestone 3)
# # 3. keep self.weights and self.bias
# ...
#
# def predict(self, sample):
# # 1. scale the sample with the stored min/max
# # 2. compute weights . features + bias
# # 3. return "orange" if total >= 0 else "apple"
# ...
#
# Then:
# model = FruitClassifier()
# model.fit(raw_X, raw_y)
# print(model.predict([205, 8])) # -> orange
#
# ✅ Expected (heavy, bumpy fruit): orange
# your code here7 Common Pitfalls (And How to Fix Them)
These are the mistakes that quietly ruin ML projects. Spotting them is what separates a working model from a misleading one.
❌ Data leakage — testing on training data
Evaluating on rows the model already saw gives a fake high score:
model.fit(X, y)
score = evaluate(model, X, y) # ❌ same data — score is meaningless✅ Fix: always hold out a test set the model never trains on.
model.fit(X_train, y_train)
score = evaluate(model, X_test, y_test) # ✅ honest score❌ Forgetting to scale new data the same way
Re-fitting the scaler on new data shifts the numbers and breaks predictions:
new_min, new_max = min(new_X), max(new_X) # ❌ different scale!✅ Fix: reuse the training set's min/max for every prediction.
scaled = (value - train_min) / (train_max - train_min) # ✅ same scale❌ Trusting accuracy on imbalanced data
With 98% legitimate transactions, predicting "always legitimate" scores 98% and catches zero fraud.
✅ Fix: report precision, recall, and F1 — not accuracy alone.
❌ Expecting a perceptron to learn non-linear data
A single perceptron can only draw one straight line. If two classes can't be split by a line, it will never converge.
✅ Fix: cap the epochs, or switch to k-NN or a multi-layer network for tangled data.
📋 Quick Reference — The ML Workflow
| Step | What You Do | scikit-learn equivalent |
|---|---|---|
| 1. Load & inspect | Read data, check size & class balance | pd.read_csv(), df.head() |
| 2. Preprocess & split | Encode labels, scale features, hold out test set | MinMaxScaler, train_test_split |
| 3. Train | Fit the model's weights on training data | model.fit(X_train, y_train) |
| 4. Evaluate | Confusion matrix, accuracy, precision, recall, F1 | accuracy_score, f1_score |
| 5. Predict | Scale new data, output a label | model.predict(new_X) |
Where This Takes You — AI/ML Career Paths
| Career Path | Core Skills | Average Salary (USD) |
|---|---|---|
| ML Engineer | Python, PyTorch, MLOps, Cloud | $120K – $200K |
| Data Scientist | Statistics, ML, SQL, Visualisation | $100K – $170K |
| AI Research Scientist | Deep Learning, Math, Publications | $150K – $300K+ |
| NLP Engineer | Transformers, LLMs, RAG, Embeddings | $130K – $220K |
| Computer Vision Engineer | CNNs, Detection, Segmentation | $120K – $190K |
| MLOps Engineer | Pipelines, Docker, Kubernetes, CI/CD | $110K – $180K |
🪙 Business Opportunities You Can Build
- • AI-powered SaaS tools for content generation, analytics, or automation
- • Custom chatbots & RAG systems for enterprises and e-commerce
- • Computer vision APIs for quality control, medical imaging, or security
- • Recommendation engines for e-commerce, music, or content platforms
- • Fraud detection services for fintech and banking
Turn This Into a Portfolio Project 🧠
📚 Structure Your ML Project
project/
┣ data/
┃ ┣ raw/ # Original datasets
┃ ┗ processed/ # Cleaned data
┣ notebooks/
┃ ┗ exploration.ipynb # EDA and analysis
┣ src/
┃ ┣ data_pipeline.py # Preprocessing
┃ ┣ features.py # Feature engineering
┃ ┣ model.py # Model architecture
┃ ┣ train.py # Training loop
┃ ┗ evaluate.py # Metrics + reporting
┣ api/
┃ ┗ main.py # FastAPI inference server
┣ tests/ # Unit tests
┣ models/ # Saved model weights
┣ requirements.txt
┗ README.md🧩 Start Small, Iterate
Begin with a baseline model (logistic regression or random forest), evaluate properly, then improve step by step. Track every experiment with MLflow.
🧪 Evaluate Thoroughly
Use appropriate metrics — F1 for imbalanced data, RMSE for regression, NDCG for rankings. Never rely on accuracy alone.
🌐 Deploy & Showcase
- • Deploy with FastAPI + Docker on AWS/GCP/Azure
- • Upload to GitHub with README, screenshots, and metrics
- • Write a blog post or LinkedIn article about your approach
- • Record a 2-minute demo video for your portfolio
❓ Frequently Asked Questions
Course Complete — you built a full ML system end-to-end!
You loaded data, preprocessed and split it, trained a perceptron from scratch, evaluated it with a confusion matrix, predicted on unseen fruit, and connected all of it to scikit-learn. That five-step loop — load, preprocess, train, evaluate, predict — is the backbone of every machine learning project, from spam filters to self-driving cars.
Where to go next
- • Swap the toy data for a real dataset (Iris, Titanic, or a Kaggle competition).
- • Rebuild it in scikit-learn, then try a random forest and compare F1 scores.
- • Wrap predict() in a FastAPI endpoint and deploy it with Docker.
- • Push it to GitHub with a README, metrics, and a confusion-matrix chart — your first portfolio project.
🏆 Congratulations — you've completed the entire AI & Machine Learning course. From your first linear regression to LLMs, computer vision, reinforcement learning, and production MLOps, you've mastered the full AI engineering stack. Now go build something real.
Practice quiz
What are the five milestones of the ML workflow in this project?
- Load, preprocess, train, evaluate, predict
- Import, compile, run, debug, deploy
- Collect, label, encrypt, store, delete
- Plan, design, build, test, ship
Answer: Load, preprocess, train, evaluate, predict. The universal ML workflow is: load and inspect, preprocess and split, train, evaluate, then predict.
Why do you split data into train and test sets?
- To make training faster
- To evaluate the model on data it has never seen
- To double the dataset size
- To balance the classes
Answer: To evaluate the model on data it has never seen. You hold out a test set so you can measure performance on unseen data — never test on rows the model trained on.
Why scale features before training the perceptron?
- So a large-valued feature doesn't drown out a small-valued one
- To convert text into numbers
- To remove the bias term
- Scaling is never needed
Answer: So a large-valued feature doesn't drown out a small-valued one. Without scaling, weight (~200) would dwarf bumpiness (~10), so min-max normalisation puts them on equal footing.
When fitting a scaler, what is the correct practice?
- Fit it on the whole dataset including test
- Fit it on training data only, then reuse those min/max on test and new data
- Re-fit a new scaler for every prediction
- Never reuse the scaler
Answer: Fit it on training data only, then reuse those min/max on test and new data. Fit the scaler on training data only and reuse those same min/max values on test and new data to avoid leakage.
How does a perceptron make a prediction?
- It stores all data and votes among nearest neighbours
- It multiplies features by weights, adds a bias, and fires 1 if the total is positive
- It averages all training labels
- It picks a random class
Answer: It multiplies features by weights, adds a bias, and fires 1 if the total is positive. A perceptron computes a weighted sum plus bias and applies a step activation: 1 if positive, else 0.
What is the perceptron update rule doing when it makes a mistake?
- Nudging the weights toward the correct answer
- Resetting all weights to zero
- Removing the misclassified sample
- Increasing the learning rate
Answer: Nudging the weights toward the correct answer. On an error it adjusts each weight by learning_rate * error * feature, moving toward the right answer.
In a confusion matrix, what is a false negative (FN)?
- Predicted positive, actually positive
- Predicted positive, actually negative
- Predicted negative, actually positive — a miss
- Predicted negative, actually negative
Answer: Predicted negative, actually positive — a miss. A false negative is predicting negative when the truth is positive — for example missing an actual orange.
What does recall measure?
- Of all flagged items, how many were truly positive
- Of all real positives, how many the model caught
- Overall correctness across both classes
- The model's training time
Answer: Of all real positives, how many the model caught. Recall = TP / (TP + FN): of all real positives, the fraction the model correctly identified.
Why isn't accuracy enough on imbalanced data?
- Accuracy is impossible to compute
- A model can score high by always predicting the majority class while missing the rare class
- Accuracy only works for regression
- Accuracy ignores the training set
Answer: A model can score high by always predicting the majority class while missing the rare class. With 98% legitimate transactions, always predicting 'legitimate' scores 98% but catches zero fraud — F1 exposes this.
How does k-nearest neighbours (k-NN) classify a new point?
- By learning a single straight decision boundary during training
- By voting among the closest stored labelled points at prediction time
- By averaging all feature values
- By adding Gaussian noise
Answer: By voting among the closest stored labelled points at prediction time. k-NN learns nothing up front; at prediction time it finds the closest stored points and takes a majority vote.
Continue this course
- Previous: Ethical AI, Bias Mitigation & Safety Principles in ML
- Quick reference: AI & Machine Learning cheat sheet › Common Algorithms
Frequently asked questions
Why build a machine learning model from scratch instead of just using scikit-learn?
Writing a perceptron and k-NN in plain Python forces you to understand the maths — weighted sums, the update rule, distance, and the evaluation metrics. Once that clicks, scikit-learn stops being magic: you know exactly what fit() and predict() are doing under the hood, which makes you far better at debugging real models.
What is the difference between a perceptron and k-nearest neighbours?
A perceptron learns a single straight decision boundary by adjusting weights during training, so prediction is instant afterwards. k-NN learns nothing up front — it stores all the data and, at prediction time, votes among the closest stored points. Perceptron is fast but only handles linearly separable data; k-NN is flexible but slow on large datasets.
Why do I scale features before training?
Without scaling, a feature with large values (weight in grams, ~200) dwarfs a feature with small values (bumpiness, ~10), so the model effectively ignores the small one. Min-max normalisation puts every feature in the same 0-1 range so each contributes fairly. Crucially, you fit the scaler on training data only, then reuse those same min/max values on test and new data.
Why isn't accuracy enough to evaluate a classifier?
If 98% of transactions are legitimate, a model that always predicts 'legitimate' scores 98% accuracy while catching zero fraud. Precision (how many flagged items were truly positive) and recall (how many real positives you caught) expose that failure. F1 combines both into one number, which is why imbalanced problems are judged on F1, not accuracy.
What should I build next to turn this into a portfolio project?
Swap the toy fruit data for a real CSV (the Iris or Titanic datasets are classics), rewrite the model with scikit-learn, then wrap predict() in a small FastAPI endpoint and deploy it. Add a README with your metrics and a confusion matrix image. That end-to-end story — data, model, evaluation, deployment — is exactly what hiring managers look for.