Sulba
000 / 100

k-nearest neighbours and naive Bayes

Two classifiers trained without gradient descent. k-nearest neighbours labels a new example by the vote of the examples most like it, and naive Bayes turns word counts into probabilities with Bayes’ rule. Learn how to choose k, why the inputs must be scaled, and how smoothing and logs keep naive Bayes working.

Lesson 8 stepsPractice 5 problemsExercise Python, 8 functionsQuiz 5 questions

Warm-up

One question before the lesson. Choose an answer and check it.

A new student studied 5.2 hours and slept 6 hours before a test. To guess whether they will pass, which earlier students would you look at first?

Show the answer

B: The students who studied and slept about as much as they did. Students with similar habits tend to get similar results. k-nearest neighbours does just that: it finds the k students nearest to the new one and lets them vote.

Step 1 Students as points

Each student has two inputs:the hours they studied, and the hours they slept.20 students, each a point. 11 passed and 9 failed.A new student: 5.2 hours of study, 6 of sleep.Will they pass?k-nearest neighbours does not train a model.It keeps every student, and asks the onesmost like the new one.0246810468hours studiedhours sleptpassedfailednew
01/08

Lessons 1 to 5 predicted from one input. Here each student has two: the hours they studied and the hours they slept the night before the test. 20 students are known, 11 of whom passed:

StudentStudy (hours)Sleep (hours)Result
11.44.2failed
22.65.7failed
31.68.7failed
43.66.4failed
56.94.1passed
60.85.9failed
768.5passed
87.18.1passed
96.15.5passed
101.94.1failed
116.27.6passed
123.17.2passed
132.84failed
148.28.3passed
158.74.6passed
168.65.7passed
177.68.3passed
186.46.1failed
191.97.9passed
201.44.7failed

On the board each student is a point, with study across and sleep up. A new student studied 5.2 hours and slept 6. Will they pass?

Logistic regression would fit a line first. k-nearest neighbours, called kNN for short, does not train at all: it keeps every student, and for a new one it looks at the students most like them. Most like them means nearest on the board, so the next step measures distance.

Try it yourself

Drag the new student and change k. The k nearest students are circled, and their vote decides. Then measure study in minutes and watch the neighbours change, and standardise both inputs.

0246810456789hours studiedhours slept

Filled dots passed, hollow dots failed. The shaded area is where the vote would say pass.

k = 3: 1 passed, 2 failed. The vote says fail.

New student: 5.2 hours of study, 6 hours of sleep

Nearest: student 9, 1.0296 away, passed

Distance in hours: an hour of study and an hour of sleep count the same.

Show the calculation
NearestStudentStudySleepDistance (hours)Result
1, votes96.15.5√((−0.9)² + 0.5²) = 1.0296passed
2, votes186.46.1√((−1.2)² + (−0.1)²) = 1.2042failed
3, votes43.66.4√(1.6² + (−0.4)²) = 1.6492failed
4116.27.6√((−1)² + (−1.6)²) = 1.8868passed
5123.17.2√(2.1² + (−1.2)²) = 2.4187passed
656.94.1√((−1.7)² + 1.9²) = 2.5495passed
722.65.7√(2.6² + 0.3²) = 2.6173failed
8768.5√((−0.8)² + (−2.5)²) = 2.6249passed
987.18.1√((−1.9)² + (−2.1)²) = 2.832passed
10132.84√(2.4² + 2²) = 3.1241failed
11177.68.3√((−2.4)² + (−2.3)²) = 3.3242passed
12168.65.7√((−3.4)² + 0.3²) = 3.4132passed
13158.74.6√((−3.5)² + 1.4²) = 3.7696passed
14148.28.3√((−3)² + (−2.3)²) = 3.7802passed
15101.94.1√(3.3² + 1.9²) = 3.8079failed
16191.97.9√(3.3² + (−1.9)²) = 3.8079passed
17201.44.7√(3.8² + 1.3²) = 4.0162failed
1811.44.2√(3.8² + 1.8²) = 4.2048failed
1960.85.9√(4.4² + 0.1²) = 4.4011failed
2031.68.7√(3.6² + (−2.7)²) = 4.5failed

Each difference is in hours.

Try k = 1 and look at the shaded area’s ragged edge, then k = 19. k = 5 made the fewest mistakes on the validation students.

Drag the new student, or select it and use the arrow keys.

Practice problems

Work each problem out on paper, then type your answer and press Check. Every problem has hints and a full solution.

Score: 0 of 10 points

  1. Problem 1

    1 point

    Student A studied 2 hours and slept 6. Student B studied 5 hours and slept 10. What is the distance between them?

    Hint 1

    d = √((difference in study)² + (difference in sleep)²).

    Solution

    d = √((5 − 2)² + (10 − 6)²)

    = √(3² + 4²) = √(9 + 16) = √25 = 5

  2. Problem 2

    2 points

    Here are the results of the 5 students nearest to a new one, nearest first: passed, failed, failed, passed and passed. What does kNN predict with k = 3? Type 1 for pass or 0 for fail.

    Hint 1

    Only the 3 nearest vote: the first 3 in the list.

    Solution

    the 3 nearest: passed, failed, failed

    1 passed and 2 failed, so the vote says fail: 0

  3. Problem 3

    2 points

    A student studied 7 hours. Over the training students, study has a mean of 4.5 hours and a standard deviation of 2 hours. What is the student’s standardised study, z?

    Hint 1

    z = (x − mean) ÷ standard deviation.

    Solution

    z = (7 − 4.5) ÷ 2

    = 2.5 ÷ 2 = 1.25

  4. Problem 4

    2 points

    In a spam filter’s training emails, the spam has 20 words in all, and “prize” is 4 of them. The emails use 10 different words. With Laplace smoothing, what is P(prize | spam)? Give it to 2 decimal places.

    Hint 1

    Add 1 to the word’s count, and add the number of different words, V, to the total.

    Solution

    P(prize | spam) = (4 + 1) ÷ (20 + 10)

    = 5 ÷ 30 = 0.1667

  5. Problem 5

    3 points

    A test flags 90% of people who have an illness and 10% of people who do not. 2% of people have the illness. What is P(ill | flagged)? Give it to 3 decimal places.

    Hint 1

    P(ill | flagged) = P(flagged | ill) × P(ill) ÷ P(flagged).

    Hint 2

    P(flagged) = P(flagged | ill) × P(ill) + P(flagged | healthy) × P(healthy).

    Solution

    P(flagged) = 0.9 × 0.02 + 0.1 × 0.98

    = 0.018 + 0.098 = 0.116

    P(ill | flagged) = 0.018 ÷ 0.116 = 0.1552

Programming exercise

Write k-nearest neighbours and a naive Bayes spam filter in plain Python. Save classify.py and test_classify.py in the same folder, fill in each function in classify.py, and run the tests:

python test_classify.py

"""k-nearest neighbours and naive Bayes: programming exercise. Write two classifiers in plain Python: k-nearest neighbours, which labels anew example by the vote of the k nearest ones, and a naive Bayes spam filterwith Laplace smoothing, worked in logs. Run the tests from this folder:     python test_classify.py A point is a list of numbers, such as [hours studied, hours slept]. A labelis 1 or 0: pass or fail, spam or not spam. An email is a list of words."""  def distance(a, b):    """Return the straight-line distance between points a and b: the square root of the sum of (a[i] - b[i]) squared."""    raise NotImplementedError  def nearest(points, query, k):    """Return the positions in points of the k points nearest to query, nearest first.     If two points are the same distance away, the one earlier in the list comes first.    """    raise NotImplementedError  def knn_predict(points, labels, query, k):    """Return the label the k nearest points vote for: 1 if more than half of their labels are 1, otherwise 0.     k is odd, so the vote of two labels cannot tie.    """    raise NotImplementedError  def standardise(values):    """Return each value standardised: (value - mean) / standard deviation.     Work out the standard deviation dividing by n, the number of values, as    scikit-learn's StandardScaler does.    """    raise NotImplementedError  def count_words(emails):    """Return a dictionary from each word to how many times it appears in all the emails together."""    raise NotImplementedError  def word_prob(word, counts, total, vocab_size):    """Return P(word | class) with Laplace smoothing: (count of word + 1) / (total + vocab_size).     counts is the class's dictionary from count_words, total is how many words    the class's emails have in all, and vocab_size is how many different words    all the training emails use. A word the class never saw has a count of 0.    """    raise NotImplementedError  def log_score(words, prior, counts, total, vocab_size):    """Return the class's log score for an email: ln(prior) plus ln P(word | class) for each word in it."""    raise NotImplementedError  def nb_predict(emails, labels, words):    """Train naive Bayes on emails and their labels (1 for spam, 0 for not spam), and classify words.     The prior of each class is its share of the emails. Return 1 if the spam    log score is higher than the other class's, otherwise 0.    """    raise NotImplementedError 

Stuck? sorted(range(len(points)), key=lambda i: distance(points[i], query)) lists the positions from nearest to furthest. math.log(x) is ln x. The Solution tab has one way to write each function.

In practice: neighbours and Bayes at scale

Finding neighbours fast. Comparing a query with every stored example takes time in proportion to how many there are, which is too slow for millions of them. Libraries build an index first: a k-d tree for a few inputs, or an approximate nearest-neighbour index such as FAISS or HNSW for embeddings with hundreds of numbers. The retrieval step of the AI applications in Module 7 is a nearest-neighbour search over embeddings, by cosine similarity.

Too many inputs. With hundreds of inputs, the distances between random examples all come out nearly the same, so the nearest neighbour is hardly nearer than any other. This is called the curse of dimensionality. kNN works best with a few inputs, or on embeddings trained so that distance means similarity.

Naive Bayes as a baseline. It trains in one pass of counting, needs little data and is hard to beat on short texts, so it is a good first model to compare anything fancier against. Its probabilities are poor, though: by treating words as independent it counts the evidence of words that go together more than once, and its chances come out too close to 0 or 1. In scikit-learn it is MultinomialNB, where alpha=1.0 is Laplace smoothing.

Test your knowledge

  1. 01Why is k usually odd when there are two classes?Show answer

    So the vote cannot tie. With k = 4, two votes each way would leave the prediction undecided.

  2. 02What happens with k = 1, and with k equal to the number of training examples?Show answer

    With k = 1 the model copies the nearest example, noise and all, so it overfits: its training error is 0. With k = n every new example gets the majority class, so it underfits. The best k is in between, chosen on a validation set.

  3. 03Why must the inputs be scaled for kNN?Show answer

    Distance adds up the squared difference of every input, so an input measured in big units swamps the others. Standardising each input, (x − mean) ÷ standard deviation, makes a difference of 1 mean the same in every input.

  4. 04What does “naive” mean in naive Bayes?Show answer

    It treats the words as independent once the class is known, so the probability of an email’s words is the product of each word’s probability. Real words are not independent, but the model often classifies well anyway.

  5. 05Why does naive Bayes need smoothing?Show answer

    A word never seen in a class gets probability 0 there, and one 0 makes the whole product 0, whatever the other words say. Adding 1 to every count keeps every probability above 0.

Exit ticket

One last question on the main idea of the lesson.

k-nearest neighbours with k = 1 gets every training student right. Is k = 1 the best choice?

Show the answer

C: Not necessarily. Each student is their own nearest neighbour, so k = 1 copies the noise. Choose k on a validation set. In this lesson k = 1 gets all 20 training students right and 15 of the 40 validation students wrong. k = 5 gets 2 of them wrong.