Features and labels

How does a fruit, a photo or a word become numbers a computer can learn from?

Before any learning can happen, the world has to become numbers. This page takes a table of fruit, turns every row into a vector, and shows why the way you do that changes which fruit looks "closest".

The table a person would write

Each row is one example. The last column is the label: the answer we'd want a model to learn. Everything else is a feature.

FruitWeight (g)Diameter (cm)ColourSweetnessLabel
Fuji apple1908.0red8 apple
Gala apple1607.3red7 apple
Granny Smith apple1757.8green4 apple
Small red apple1306.8red6 apple
Golden apple1707.5yellow7 apple
Navel orange2208.5orange6 orange
Valencia orange1807.6orange6 orange
Mandarin906.0orange7 orange
Blood orange1607.2red6 orange
Large orange2609.2orange5 orange
Lemon1106.2yellow1 sour citrus
Meyer lemon955.8yellow3 sour citrus
Lime655.0green1 sour citrus
Large lemon1507.0yellow1 sour citrus
Calamansi253.0green2 sour citrus

The same table, as the computer sees it

Colour became four 0/1 columns (one-hot encoding). Scaling: standardised, distance from the column average in spreads. Blue is positive, red is negative.

weightdiametersweetnesscolor=greencolor=orangecolor=redcolor=yellow
Fuji apple0.760.781.40−0.50−0.601.66−0.60
Gala apple0.250.300.98−0.50−0.601.66−0.60
Granny Smith apple0.500.64−0.282.00−0.60−0.60−0.60
Small red apple−0.26−0.040.56−0.50−0.601.66−0.60
Golden apple0.420.440.98−0.50−0.60−0.601.66
Navel orange1.271.120.56−0.501.66−0.60−0.60
Valencia orange0.590.500.56−0.501.66−0.60−0.60
Mandarin−0.94−0.590.98−0.501.66−0.60−0.60
Blood orange0.250.230.56−0.50−0.601.66−0.60
Large orange1.951.590.14−0.501.66−0.60−0.60
Lemon−0.60−0.45−1.54−0.50−0.60−0.601.66
Meyer lemon−0.86−0.72−0.70−0.50−0.60−0.601.66
Lime−1.37−1.27−1.542.00−0.60−0.60−0.60
Large lemon0.080.10−1.54−0.50−0.60−0.601.66
Calamansi−2.05−2.63−1.122.00−0.60−0.60−0.60

Settings

Describe a mystery fruit

170
7
2

Two features, drawn as a picture

−2.4 −1.8 −1.2 −0.9 0.0 −0.1 1.1 0.8 2.3 1.6 Fuji apple Gala apple Granny Smith apple Small red apple Golden apple Navel orange Valencia orange Mandarin Blood orange Large orange Lemon Meyer lemon Lime Large lemon Calamansi weight →
apple orange sour citrus◆ mystery fruit (up: sweetness)

Hover a dot to see which fruit it is. The dashed line points to the nearest fruit using all features, not just the two drawn.

Which fruit is the mystery closest to?

RankFruitLabelDistance
#1Large lemon sour citrus0.540
#2Lemon sour citrus1.230
#3Meyer lemon sour citrus1.572
#4Golden apple apple2.124
#5Granny Smith apple apple3.517

Where that distance comes from

Distance adds up the squared difference in every column. This is each column's share for the nearest fruit.

weight
40%
diameter
0%
sweetness
60%
color=green
0%
color=orange
0%
color=red
0%
color=yellow
0%

Same mystery fruit, three ways of scaling

ScalingNearest fruitSo it's probably…
No scalingGolden apple apple
Min–max (0 to 1)Large lemon sour citrus
StandardisedLarge lemon sour citrus

What to notice. Without scaling, weight is measured in hundreds while sweetness only goes from 1 to 10. A 20 g difference counts 400 in the distance, but the difference between "very sour" and "very sweet" counts at most 81. So the raw model mostly compares weights.

With the default mystery (170 g, yellow, sweetness 2), raw distance says it's closest to the Golden apple, just because they weigh the same. Once every column is scaled, sourness gets a fair vote and it lands next to the lemons.

Every model in this course, including LLMs, has the same problem. Neural networks rescale the numbers inside every layer for exactly this reason (you'll meet layer normalisation in lesson 13).

The code that just ran

Everything on this page came from calling this code, which lives at the top of this same file.

Show or hide the source
final class Featurizer
{
    private array $vocab = [];   // for each word column: every value we've seen
    private array $min = [], $max = [], $mean = [], $std = [];

    public function __construct(private array $numeric, private array $categorical) {}

    // Learn what's needed to encode: the list of possible words, and the
    // min/max/mean/spread of every column (needed for scaling).
    public function fit(array $rows): void
    {
        foreach ($this->categorical as $col) {
            $values = array_values(array_unique(array_column($rows, $col)));
            sort($values);
            $this->vocab[$col] = $values;
        }
        $vectors = array_map(fn($r) => $this->encode($r), $rows);
        foreach (array_keys($this->names()) as $j) {
            $column = array_column($vectors, $j);
            $this->min[$j]  = min($column);
            $this->max[$j]  = max($column);
            $this->mean[$j] = array_sum($column) / count($column);
            $var = 0.0;
            foreach ($column as $v) $var += ($v - $this->mean[$j]) ** 2;
            $this->std[$j] = sqrt($var / count($column)) ?: 1.0;
        }
    }

    // Names of the columns in the vector, e.g. "weight", "color=red"
    public function names(): array
    {
        $names = $this->numeric;
        foreach ($this->categorical as $col) {
            foreach ($this->vocab[$col] as $value) $names[] = "$col=$value";
        }
        return $names;
    }

    // ONE-HOT ENCODING: a word column becomes several 0/1 columns,
    // one per possible word. "orange" → color=orange: 1, every other colour: 0.
    // (Why not red=1, orange=2, yellow=3? Because then yellow would be
    //  "more" than red, and red–yellow would be "farther" than red–orange.)
    public function encode(array $row): array
    {
        $v = [];
        foreach ($this->numeric as $col) $v[] = (float) $row[$col];
        foreach ($this->categorical as $col) {
            foreach ($this->vocab[$col] as $value) $v[] = $row[$col] === $value ? 1.0 : 0.0;
        }
        return $v;
    }

    // SCALING: put every column on a similar range so no column dominates.
    public function scale(array $v, string $how): array
    {
        foreach ($v as $j => $x) {
            if ($how === 'minmax') {            // squeeze into 0..1
                $range = $this->max[$j] - $this->min[$j];
                $v[$j] = $range ? ($x - $this->min[$j]) / $range : 0.0;
            } elseif ($how === 'standard') {    // "how many spreads from average?"
                $v[$j] = ($x - $this->mean[$j]) / $this->std[$j];
            }
        }
        return $v;
    }
}

function euclidean(array $a, array $b): float
{
    $sum = 0.0;
    foreach ($a as $j => $x) $sum += ($x - $b[$j]) ** 2;
    return sqrt($sum);
}