Machine Learning Basics

Generative AI & LLM Fundamentals · AI and machine learning basics · 13 min read · Updated

How a model is trained and tested

  1. Start with data: many examples, ideally with the right answers attached.
  2. Split it. Most of it goes into the training set that the model learns from.
  3. A separate validation set is used to compare settings while you build.
  4. The model learns patterns from the training set.
  5. The test set is kept locked away until the end, so the score is an honest check on data the model never saw.
  6. If the training score is high but the test score is low, the model has overfitted: it memorised instead of learning.

Machine learning, or ML, is the part of AI where a program learns from data instead of following rules written by a person. You met the idea in lesson 1. In this lesson we go one level deeper: the main types of learning, the basic steps of a machine learning project, and the most important warning in the whole field, overfitting. You will build a small classifier that decides whether a candidate should be shortlisted, and you will watch it succeed and fail in a way that mirrors real projects.

Three ways a machine can learn

Supervised learning is learning from labelled examples. Each example comes with the right answer: this resume was shortlisted, this one was not; this email is spam, this one is not. The model learns to predict the label for new examples. Most business uses of ML are supervised: price prediction, spam filters, document classification, and so on.

Unsupervised learning has no labels. You give the model data and ask it to find structure. A typical use is clustering: grouping customers or job postings into sets of similar items, without anyone saying what the groups should be. Another is finding unusual items that do not fit any group.

Reinforcement learning learns by trying things and receiving rewards or penalties. A game-playing program wins or loses and adjusts. This method is also used to refine chat assistants: people rate answers, and the model is nudged towards answers that people prefer.

Large language models use a mix. Their first stage, predicting the next word in huge amounts of text, is often called self-supervised: the text itself supplies the answers, so no one has to label anything. You will learn more about this in lessons 5 and 6.

The steps of a supervised project

  1. Collect data. Gather examples with correct labels. This is usually the hardest and most expensive step.
  2. Clean and prepare. Fix errors, remove duplicates, turn text into numbers, and scale the numbers.
  3. Split. Keep some examples hidden for testing. More on this below.
  4. Train. Let the model learn from the training part.
  5. Evaluate. Check the model on the test part, which it has never seen.
  6. Use and watch. Put the model to work and keep checking it, because the real world changes.

Train and test: the most important habit

Imagine a student who gets the exam questions in advance and memorises the answers. Their exam score will be perfect, but it tells you nothing about what they understand. Machine learning has the same problem. If you test a model on the same examples it learned from, the score is meaningless.

So we split the data. The larger part is the training set, used for learning. The smaller part, often 20 to 30 percent, is the test set. The model never sees it while learning. The score on the test set is an honest estimate of how the model will do on new data.

Overfitting and underfitting

Overfitting happens when a model learns the training examples too closely, including their accidents and noise, and then does poorly on new data. The sign is a big gap: very high accuracy on the training set and much lower accuracy on the test set. Underfitting is the opposite: the model is too simple to catch the real pattern, and it does poorly on both sets.

Think about a resume screening model trained on only 30 past hires. If three of the shortlisted people happened to have the same hobby, a model that fits too closely might decide the hobby matters. On new candidates it would be wrong in odd ways. More data, simpler models and honest testing are the usual cures.

Measuring the result: accuracy, precision and recall

The simplest measure is accuracy: the share of predictions that are correct. But accuracy can mislead. If only 5 percent of candidates are a good match, a lazy model that says “no” every time is 95 percent accurate and totally useless. So we look at a table called the confusion matrix, which counts four outcomes: true positives (said yes, correctly), false positives (said yes, wrongly), true negatives (said no, correctly) and false negatives (said no, wrongly).

  • Precision answers: of everyone the model said yes to, how many were really yes? It matters when a false alarm is costly.
  • Recall answers: of everyone who was really yes, how many did the model find? It matters when missing a good case is costly.

In a hiring tool, low recall means good candidates are rejected without anyone noticing. That is why human review of rejected cases matters.

A simple model: k nearest neighbours

The model in this lesson is k-nearest neighbours, or k-NN. It is one of the simplest ML methods. To judge a new candidate, find the k training examples that are closest to them (using a distance, like in lesson 2), and let those neighbours vote. If most of the neighbours were shortlisted, predict “shortlisted”. The number k is a setting you choose. With k = 1 the model copies its single nearest neighbour, which follows every accident in the data and overfits easily. With a very large k the model averages too much and underfits. A medium k often works best, and the test set helps you find it.

Your task: a shortlisting classifier

You will build a page that generates a fake set of candidates, splits them into training and test sets, classifies with k-NN, and shows accuracy, precision, recall and the confusion matrix. You will use sliders for k, the test share and the amount of noise.

What you will build

  • A seeded random number generator, so that the same seed always gives the same data. This makes experiments repeatable.
  • A function makeData that creates candidates with years of experience and a skills score. A candidate is shortlisted when a mix of the two is high, but noise flips some labels.
  • A split function that shuffles and divides the data.
  • A predict function for k-NN and an evaluate function that returns the confusion matrix, accuracy, precision and recall.
  • A page with sliders and results for the training and the test data.

Steps

  1. Write lib/rng.ts with makeRng(seed), which returns a function that gives a new number between 0 and 1 on each call, and shuffle(items, rng).
  2. Write lib/knn.ts. In makeData, create the requested number of people with random years (0 to 15) and skills (0 to 10). Decide the label from a simple formula, then flip it with a probability equal to the noise setting.
  3. Write split(data, testShare, seed) that shuffles a copy and returns the train and test arrays.
  4. Write predict(train, who, k): measure the distance to every training example, sort, take the k nearest and vote.
  5. Write evaluate(train, test, k) that counts tp, fp, tn and fn and calculates accuracy, precision and recall. Avoid dividing by zero.
  6. Build the page. Show the accuracy on the training data and on the test data side by side.

Hints

  • When you test on the training data with k = 1, accuracy is 100 percent or very close to it, because each point’s nearest neighbour is itself. This is a good way to see why a separate test set matters.
  • Set the noise to a high value. The test accuracy falls, and the gap between train and test grows with small k.
  • Use an odd k to avoid tied votes.
  • Change only one slider at a time, so you can see the cause of each change.

You know it works when the training accuracy with k = 1 is 100 percent (or very close) while the test accuracy is clearly lower, a medium k closes the gap between training and test scores (the test score is not always higher, since small test sets are noisy), and changing the seed gives a different but similar picture.

Solution

The random generator and the shuffle:

// A seeded random number generator. The same seed always gives the same numbers,
// which makes experiments repeatable: you can compare two settings fairly.
export function makeRng(seed: number) {
  let a = seed >>> 0;
  return () => {
    a = (a + 0x6d2b79f5) >>> 0;
    let t = a;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296; // a number from 0 up to (not including) 1
  };
}

export function shuffle<T>(items: T[], rng: () => number): T[] {
  const out = [...items];
  for (let i = out.length - 1; i > 0; i--) {
    const j = Math.floor(rng() * (i + 1));
    [out[i], out[j]] = [out[j]!, out[i]!];
  }
  return out;
}

The data, the split, the k-NN prediction and the evaluation:

import { makeRng, shuffle } from './rng';

// A candidate described by two numbers, and whether the recruiter shortlisted them.
export type Person = { years: number; skills: number; shortlisted: boolean };

// Make an imaginary but sensible dataset: more experience and more matching skills usually means shortlisted,
// but with some randomness, because real hiring is never perfectly predictable.
export function makeData(count: number, seed: number, noise = 0.25): Person[] {
  const rng = makeRng(seed);
  return Array.from({ length: count }, () => {
    const years = Math.round(rng() * 150) / 10; // 0 to 15 years
    const skills = Math.round(rng() * 10); // 0 to 10 matching skills
    const score = 0.5 * (years / 15) + 0.5 * (skills / 10) + (rng() - 0.5) * noise * 2;
    return { years, skills, shortlisted: score > 0.5 };
  });
}

// Training data teaches the model. Test data is kept hidden and used only to check it.
export function split(data: Person[], testShare: number, seed: number) {
  const mixed = shuffle(data, makeRng(seed));
  const testSize = Math.max(1, Math.round(mixed.length * testShare));
  return { test: mixed.slice(0, testSize), train: mixed.slice(testSize) };
}

const distance = (a: Person, b: Person) => Math.hypot((a.years - b.years) / 15, (a.skills - b.skills) / 10);

// k-nearest neighbours: look at the k most similar people in the training data and let them vote.
export function predict(train: Person[], who: Pick<Person, 'years' | 'skills'>, k: number): boolean {
  const near = train
    .map((p) => ({ p, d: distance(p, who as Person) }))
    .sort((x, y) => x.d - y.d)
    .slice(0, Math.max(1, k));
  const yes = near.filter((n) => n.p.shortlisted).length;
  return yes * 2 > near.length;
}

export function evaluate(train: Person[], test: Person[], k: number) {
  const matrix = { tp: 0, fp: 0, tn: 0, fn: 0 };
  for (const p of test) {
    const guess = predict(train, p, k);
    if (guess && p.shortlisted) matrix.tp++;
    else if (guess && !p.shortlisted) matrix.fp++;
    else if (!guess && !p.shortlisted) matrix.tn++;
    else matrix.fn++;
  }
  const total = test.length || 1;
  const accuracy = (matrix.tp + matrix.tn) / total;
  const precision = matrix.tp / ((matrix.tp + matrix.fp) || 1);
  const recall = matrix.tp / ((matrix.tp + matrix.fn) || 1);
  return { matrix, accuracy, precision, recall };
}

And the page with the sliders:

import { Component, computed, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { evaluate, makeData, split } from '../lib/knn';

@Component({
  selector: 'app-classifier',
  imports: [FormsModule],
  template: `
    <h1>Train, test and overfit</h1>
    <p class="hint">A model learns to guess who gets shortlisted from years of experience and matching skills. Watch the gap between its score on data it has seen and data it has not.</p>

    <div class="card">
      <div class="grid2">
        <div><label for="k">Neighbours that vote (k): {{ k() }}</label><input id="k" type="range" min="1" max="31" step="2" [ngModel]="k()" (ngModelChange)="k.set(+$event)" /></div>
        <div><label for="t">Share kept for testing: {{ (testShare() * 100).toFixed(0) }}%</label><input id="t" type="range" min="0.1" max="0.5" step="0.05" [ngModel]="testShare()" (ngModelChange)="testShare.set(+$event)" /></div>
      </div>
      <div class="grid2">
        <div><label for="n">Noise in the data: {{ noise() }}</label><input id="n" type="range" min="0" max="0.6" step="0.05" [ngModel]="noise()" (ngModelChange)="noise.set(+$event)" /></div>
        <div><label for="s">Random seed: {{ seed() }}</label><input id="s" type="range" min="1" max="20" step="1" [ngModel]="seed()" (ngModelChange)="seed.set(+$event)" /></div>
      </div>
    </div>

    <div class="card">
      <h2>Results ({{ parts().train.length }} training, {{ parts().test.length }} test)</h2>
      <table><tbody>
        <tr><td>Accuracy on training data (seen)</td><td><strong>{{ pct(trainResult().accuracy) }}</strong></td></tr>
        <tr><td>Accuracy on test data (not seen)</td><td><strong>{{ pct(testResult().accuracy) }}</strong></td></tr>
        <tr><td>Gap</td><td [class]="gap() > 0.12 ? 'bad' : 'ok'">{{ pct(gap()) }} {{ gap() > 0.12 ? '(overfitting)' : '' }}</td></tr>
      </tbody></table>
      <h2>Confusion matrix (test data)</h2>
      <table><thead><tr><th></th><th>Predicted: shortlisted</th><th>Predicted: not</th></tr></thead><tbody>
        <tr><td>Really shortlisted</td><td>{{ testResult().matrix.tp }}</td><td>{{ testResult().matrix.fn }}</td></tr>
        <tr><td>Really not</td><td>{{ testResult().matrix.fp }}</td><td>{{ testResult().matrix.tn }}</td></tr>
      </tbody></table>
      <p class="muted">Precision {{ pct(testResult().precision) }}: when it says shortlisted, how often is it right. Recall {{ pct(testResult().recall) }}: of the people really shortlisted, how many it found.</p>
      <p class="muted">Try k = 1. It memorises the training data (100% on seen data) but does worse on new data. A larger k smooths out the noise.</p>
    </div>
  `,
})
export class ClassifierPage {
  k = signal(1);
  testShare = signal(0.3);
  noise = signal(0.25);
  seed = signal(7);

  data = computed(() => makeData(200, this.seed(), this.noise()));
  parts = computed(() => split(this.data(), this.testShare(), this.seed()));
  trainResult = computed(() => evaluate(this.parts().train, this.parts().train, this.k()));
  testResult = computed(() => evaluate(this.parts().train, this.parts().test, this.k()));
  gap = computed(() => Math.max(0, this.trainResult().accuracy - this.testResult().accuracy));

  pct = (x: number) => `${(x * 100).toFixed(1)}%`;
}

What you learned

  • Supervised learning uses labelled examples, unsupervised learning finds structure without labels, and reinforcement learning uses rewards.
  • Always keep a test set that the model never sees during training.
  • Overfitting means memorising the training data, and shows as a gap between training and test scores.
  • Accuracy can hide problems. Precision, recall and the confusion matrix tell you which mistakes happen.
  • k-NN predicts by letting the nearest examples vote, and k controls how closely it follows the data.

In the next lesson you will move from simple models to neural networks, and see how layers of tiny units can learn something that a single layer cannot.

Download the solution (genai-basics-lesson-03.zip)

Frequently asked questions

What is the difference between supervised and unsupervised learning?

Supervised learning uses examples with correct answers (labels) and learns to predict them. Unsupervised learning has no labels and looks for structure, such as groups of similar items.

How much data do I need for machine learning?

It depends on the task. Simple patterns with few features can work with hundreds of examples. Complex tasks like language need millions. More important than the amount is that the data is correct and similar to what the model will meet in real use.

How can I tell that a model is overfitting?

Compare the score on the training data with the score on the test data. A high training score and a much lower test score is the classic sign.

Is a high accuracy always good?

No. If one class is rare, a model that always predicts the common class has high accuracy and no value. Check precision, recall and the confusion matrix.