← Course overview
Week 8 of 12 · Data & software

Building a Colour Classifier

Write a working classifier from your own data, and learn to trust an accuracy number because you understand how it was measured.

~5 hrs Python

Concept Normalise, average, compare

Raw sensor readings depend on brightness as well as colour — a Skittle held slightly closer to the sensor reads brighter across every channel, even though its colour hasn't changed. Normalising (dividing each channel by the sum of all eight) removes that brightness effect and leaves just the shape of the spectrum, which is what colour actually is. Averaging every normalised reading for a colour gives a centroid — a typical fingerprint for that class — and cosine similarity measures how closely a new reading's shape matches each centroid, ignoring overall magnitude. Leave-one-out validation is a way to get an honest accuracy estimate from a small dataset: train on everything except one example, test on that one, and repeat for every example in turn.

Kit What you need this week

  • Your Week 7 TSV dataset
  • A laptop or the Pi with Python

Build Step by step

Step 1

Load the data

Read the TSV and print the first few rows to confirm it parsed correctly before writing anything else:

import csv
with open("skittles.tsv") as f:
    reader = csv.DictReader(f, delimiter="\t")
    rows = list(reader)
print(rows[0])
Step 2

Write normalize()

Divide each value in a vector by the vector's sum, guarding against a divide-by-zero:

def normalize(vec):
    total = sum(vec)
    return [v / total if total > 0 else 0.0 for v in vec]
Step 3

Write cosine_similarity()

Compare two normalised vectors by the angle between them:

import math
def cosine_similarity(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    na = math.sqrt(sum(x * x for x in a))
    nb = math.sqrt(sum(y * y for y in b))
    return dot / (na * nb) if na > 0 and nb > 0 else 0.0
Step 4

Compute centroids

Group your normalised readings by colour label, average each group, and treat the result as that colour's fingerprint.

Step 5

Write classify()

Normalise a new reading and compare it against every centroid, returning the best match:

def classify(vec, centroids):
    x = normalize(vec)
    scores = [(label, cosine_similarity(x, c)) for label, c in centroids.items()]
    scores.sort(key=lambda s: s[1], reverse=True)
    return scores[0], scores  # best match, all scores
Step 6

Test it by hand

Pick one of your own logged readings and run it through classify() before building any automatic evaluation — does the top match look right?

Step 7

Run leave-one-out evaluation

For every row in your dataset, retrain the centroids on everything else, test on that one row, and tally correct vs incorrect per colour. Report both an overall accuracy and a per-class breakdown.

Step 8

Explain one mistake

Find one misclassified example, look at its full ranked score list, and write a sentence or two on why it was likely confused — e.g. two colours that share a lot of spectral overlap.

Make it yours
  • Try normalising by vector length (L2) instead of by sum, and see whether accuracy changes.
  • If two colours are genuinely hard to tell apart with this sensor, consider merging them into one class — a legitimate engineering decision, not cheating.

If it breaks Troubleshooting

Accuracy is suspiciously lowDouble-check the TSV column order actually matches the channel order your code assumes.
Accuracy is suspiciously perfectUsually means too few samples, or scans taken in a rapid burst without re-seating the Skittle between them — collect a more varied dataset.
One colour is never predicted correctlyCheck it has enough training examples (aim for at least 3–5) and that its scans were taken under the same lighting as everything else.
This week's checkpointYour classifier is trained on your own dataset, reports a leave-one-out accuracy, and you can explain one misclassification using the ranked similarity scores.
Next week: The sorting mechanism — turning a colour label into a physical action.