Chương 2·Bài 2.4

k-Nearest Neighbors

kNN (k-Nearest Neighbors) dự đoán cho một mẫu mới bằng cách tìm kk mẫu gần nó nhất trong dữ liệu đã có rồi lấy nhãn chiếm đa số. Ví dụ: xếp thành viên mới vào nhóm luyện dựa trên những thành viên có điểm thuật toán và số giờ tự học gần giống. Vì dựa hoàn toàn vào khoảng cách, kNN rất nhạy với đơn vị đo: đổi giờ sang phút có thể làm đổi kết quả. Bài này học cách kNN dự đoán, cách chọn kk và vì sao phải chuẩn hóa dữ liệu trước.

Thuật toán

Cho tập huấn luyện {(xi,yi)}i=1n\{(\mathbf{x}_i, y_i)\}_{i=1}^n và một điểm cần dự đoán q\mathbf{q}:

  1. Tính khoảng cách d(q,xi)d(\mathbf{q}, \mathbf{x}_i) đến mọi điểm.
  2. Lấy tập Nk(q)N_k(\mathbf{q}) gồm kk điểm gần nhất.
  3. Phân loại: chọn nhãn xuất hiện nhiều nhất trong NkN_k. Hồi quy: lấy trung bình yy của NkN_k.

Ví dụ năm điểm A(1,1)A(1, 1) nhãn đỏ, B(2,1)B(2, 1) đỏ, C(4,3)C(4, 3) xanh, D(5,4)D(5, 4) xanh, E(3,2)E(3, 2) xanh. Truy vấn q=(2,2)\mathbf{q} = (2, 2), k=3k = 3, khoảng cách Euclid bình phương:

Điểm A B C D E
d2d^2 2 1 5 13 1

Ba láng giềng gần nhất là BB, EE, AA: hai đỏ, một xanh, nên dự đoán đỏ. Với k=1k = 1 thì BB và EE cùng khoảng cách, nên cần quy tắc phá hòa, chẳng hạn chọn điểm có chỉ số nhỏ hơn. Đề chấm tự động thường ghi rõ quy tắc phá hòa; đọc sót nó là đủ để kết quả lệch đáp án.

Ba láng giềng gần q nhất là B, E, A; đỏ thắng 2–1.123412345ABCDEq112đỏ: A, Bxanh: C, D, Ek = 3: B, E, A→ đỏ thắng 2–1
Ba láng giềng gần q nhất là B, E, A; đỏ thắng 2–1.

Biến thể có trọng số cho láng giềng càng gần càng có tiếng nói lớn, ví dụ trọng số 1/d1/d. Một láng giềng lớp A cách 1 và hai láng giềng lớp B cách 2, 3: bỏ phiếu đều thì B thắng 2–1, còn theo 1/d1/d thì A được 1, B chỉ được 1/2+1/3≈0.831/2 + 1/3 \approx 0.83. Hồi quy cũng vậy: giá trị 10, 20, 40 ở khoảng cách 1, 2, 4 cho (10+10+10)/1.75≈17.1(10 + 10 + 10)/1.75 \approx 17.1, gần láng giềng sát nhất hơn trung bình thường 23.323.3.

kNN là học lười (lazy learning): “huấn luyện” chỉ là cất dữ liệu đi, mọi công việc dồn vào lúc dự đoán. Ngược lại, mô hình tuyến tính là học chăm (eager): học xong chỉ giữ w\mathbf{w}, dữ liệu gốc có thể bỏ. kNN vẫn là học có giám sát, vì các láng giềng phải có nhãn mới bỏ phiếu được. Chữ kk ở đây là số láng giềng, khác hẳn KK của K-means ở bài 2.10, là số cụm.

Hỏi nhanh

Có ba láng giềng: một láng giềng nhãn 0 ở khoảng cách 1, hai láng giềng nhãn 1 ở khoảng cách 4 và 5. Bỏ phiếu đều thì ra nhãn nào? Bỏ phiếu theo trọng số 1/d1/d thì ra nhãn nào?

Khoảng cách

Tên Công thức Hợp với
Euclid (L2) ∑j(qj−xj)2\sqrt{\sum_j (q_j - x_j)^2} Đặc trưng số đã chuẩn hóa
Manhattan (L1) ∑j∣qj−xj∣\sum_j \lvert q_j - x_j\rvert Ít nhạy với một chiều lệch lớn
Cosine 1−q⊤x∥q∥ ∥x∥1 - \dfrac{\mathbf{q}^\top\mathbf{x}}{\lVert\mathbf{q}\rVert\,\lVert\mathbf{x}\rVert} Embedding, văn bản: quan tâm hướng, không quan tâm độ dài

Với vector đã chuẩn hóa về độ dài 1, ∥q−x∥2=2−2cos⁡(q,x)\lVert\mathbf{q} - \mathbf{x}\rVert^2 = 2 - 2\cos(\mathbf{q}, \mathbf{x}), nên xếp hạng theo Euclid và theo cosine là như nhau. Thư viện tìm kiếm embedding dùng đúng mẹo này. Đổi cách đo có thể đổi láng giềng: từ (0,0)(0, 0), điểm (3,0)(3, 0) cách 3 theo cả hai cách, còn (2,2)(2, 2) cách 8≈2.83\sqrt 8 \approx 2.83 theo Euclid nhưng cách 4 theo Manhattan. Muốn một cột nặng ký hơn thì đánh trọng số ngay trong khoảng cách, ∑jwj(qj−xj)2\sqrt{\sum_j w_j (q_j - x_j)^2}.

Các điểm cách gốc đúng 1 theo Euclid, Manhattan; cosine chỉ xét hướng.EuclidManhattanBA = 2BCCosine
Các điểm cách gốc đúng 1 theo Euclid, Manhattan; cosine chỉ xét hướng.

Hỏi nhanh

Văn bản A là văn bản B chép lại hai lần, nên vector đếm từ của A đúng bằng 2 lần vector của B. Theo cosine và theo Euclid, A và B có gần nhau không?

Thang đo quyết định tất cả. Hai đặc trưng “tuổi” (20 đến 60) và “thu nhập tháng” tính bằng đồng (5 triệu đến 50 triệu): khoảng cách gần như chỉ còn là chênh lệch thu nhập, tuổi chẳng còn tiếng nói nào. Khi đơn vị các cột không so sánh được, chuẩn hóa hoặc thiết kế trọng số khoảng cách là bước cần kiểm bằng validation.

Chọn k

  • k=1k = 1: lỗi trên train thường bằng 0 nếu không có hồ sơ trùng nhau mang nhãn mâu thuẫn, ranh giới lởm chởm theo từng điểm nhiễu. Variance cao.
  • kk lớn: ranh giới mượt, nhưng k=nk = n thì đoán mọi điểm là lớp đông nhất. Bias cao.

Chọn kk bằng cross-validation. Với phân loại nhị phân, kk lẻ tránh hòa phiếu. Leave-one-out (bài 1.3) rất rẻ với kNN: bỏ từng điểm ra rồi hỏi láng giềng trong phần còn lại, không phải học lại gì. Trên một trục số, 1-NN chỉ đổi dự đoán ở trung điểm giữa hai điểm liền kề khác nhãn, nên nó chính là một dãy ngưỡng.

Hỏi nhanh

Một bạn chọn k=1k = 1, vì trong các kk đã thử, k=1k = 1 có lỗi train thấp nhất, bằng 0. Chọn như vậy có ổn không?

Phỏng theoCMU 10-601 · Exam 1 Practice · Q2.3(f)

k = 1 bám từng điểm nhiễu; k = 15 cho ranh giới mượt.k = 1k = 15
k = 1 bám từng điểm nhiễu; k = 15 cho ranh giới mượt.
Cùng 200 điểm của hình trên, tăng k từ 1 tới 120: ranh giới đi từ bám từng điểm nhiễu tới quá phẳng.

Lời nguyền số chiều

Khi số chiều tăng, với dữ liệu trải đều, điểm gần nhất và điểm xa nhất cách truy vấn gần bằng nhau, nên “láng giềng” mất ý nghĩa. Dữ liệu thật thường nằm gần một cấu trúc ít chiều hơn nhiều, nên mức độ tệ còn tùy số chiều thực chất của nó. Một phép tính nhỏ cho thấy hiện tượng này. Dữ liệu rải đều trong khối lập phương đơn vị dd chiều. Muốn một khối lập phương nhỏ chứa 10% dữ liệu thì cạnh của nó phải dài 0.11/d0.1^{1/d}:

  • d=1d = 1: cạnh 0.10.1, đúng là “lân cận”.
  • d=10d = 10: cạnh 0.790.79, gần hết cả khoảng.
  • d=100d = 100: cạnh 0.980.98.
Gom 10% dữ liệu: cạnh khối dài 0.1 ở 1 chiều, 0.98 ở 100 chiều.00.51số chiều d (thang log)1101000.10.790.98cạnh khối chứa 10% dữ liệu
Gom 10% dữ liệu: cạnh khối dài 0.1 ở 1 chiều, 0.98 ở 100 chiều.

Láng giềng gần nhất trong 100 chiều thực chất chẳng gần chút nào. Nhìn theo chiều ngược lại: khối lập phương đồng tâm cạnh 0.90.9 chỉ chứa 0.9100≈0.003%0.9^{100} \approx 0.003\% thể tích, gần như mọi điểm nằm sát biên. Muốn láng giềng gần nhất gần gấp đôi thì mỗi điểm phải phủ một quả cầu bán kính bằng nửa, thể tích nhỏ đi 2d2^d lần, nên cần khoảng 2d2^d lần số điểm. Vì vậy kNN chạy tốt trên đặc trưng ít chiều, hoặc trên embedding mà mô hình đã học sẵn để những thứ giống nhau nằm gần nhau, thường kèm giảm chiều bằng PCA (bài 2.9).

Hỏi nhanh

Dữ liệu rải đều trong một khối lập phương đơn vị 50 chiều. Một khối lập phương nhỏ chứa 1% dữ liệu thì có cạnh dài khoảng bao nhiêu?

Chi phí

Dự đoán một điểm tốn O(nd)O(nd). Với nn lớn, có ba cách tăng tốc:

  • Cây không gian (KD-tree, Ball tree): nhanh khi dd nhỏ, dưới khoảng 20.
  • Vector hóa bằng phép nhân ma trận trên GPU: tính một lần khoảng cách giữa cả lô truy vấn và cả tập dữ liệu.
  • Tìm gần đúng (approximate nearest neighbor) như FAISS, HNSW: chấp nhận đôi khi sót láng giềng thật để nhanh hơn hàng trăm lần. Đây là nền của các hệ thống tìm kiếm ảnh và văn bản. Chúng có núm đổi tốc độ lấy độ đúng, như ef_search của HNSW: giữ nhiều ứng viên hơn khi duyệt thì ít sót láng giềng thật nhưng chậm hơn.

Vài lưu ý khi tự cài:

  • Chọn kk phần tử nhỏ nhất: sort toàn bộ tốn O(nlog⁡n)O(n \log n); np.argpartition chọn trước rồi chỉ sort kk phần tử thì rẻ hơn.
  • Với mm truy vấn một lúc, ma trận khoảng cách m×nm \times n có thể hết RAM dù code chỉ một dòng; chia truy vấn thành từng batch.
  • KD-tree không phải lúc nào cũng O(log⁡n)O(\log n): ở nhiều chiều nó có thể phải duyệt gần hết cây.

Hỏi nhanh

Cần tìm ảnh giống nhau trong 1 triệu ảnh, mỗi ảnh là một embedding 512 chiều, và mỗi truy vấn phải trả lời trong vài mili giây. Cách nào hợp lý?

Cài kNN bằng NumPy

Cài lại ví dụ năm điểm ở mục Thuật toán, chưa cần thư viện. Mỗi hàng là một hồ sơ, hai cột là hai đặc trưng. Trừ truy vấn khỏi từng hàng bằng broadcasting, bình phương từng độ lệch, rồi cộng theo axis=1 để được một khoảng cách cho mỗi hàng. Không lấy căn vì căn bậc hai tăng theo đối số: xếp hạng khoảng cách bình phương cho cùng thứ tự, tránh thêm một phép tính.

Python
import numpy as np

points = np.array([[1, 1], [2, 1], [4, 3], [5, 4], [3, 2]], dtype=float)
labels = np.array([0, 0, 1, 1, 1])
query = np.array([2, 2], dtype=float)
distance2 = np.sum((points - query) ** 2, axis=1)
order = np.argsort(distance2, kind="stable")
neighbors = order[:3]
counts = np.bincount(labels[neighbors], minlength=2)
print(distance2.astype(int).tolist())
print(neighbors.tolist(), counts.tolist(), int(np.argmax(counts)))
assert neighbors.tolist() == [1, 4, 0]

Chỉ số trong code bắt đầu từ 0, nên [1, 4, 0] chính là B, E, A. stable giữ thứ tự ban đầu khi khoảng cách bằng nhau; argmax chọn nhãn có chỉ số nhỏ nhất khi số phiếu hòa. Đây là hai quy tắc của bản cài này, không phải quy tắc chung của mọi thư viện. Với k=1, đổi thứ tự dữ liệu có thể đổi dự đoán ở điểm hòa khoảng cách. Trong đề chấm chính xác, phải cài quy ước đề yêu cầu.

Hỏi nhanh

Vì sao không cần lấy căn của distance2 trước khi sắp xếp?

Thang đo thay đổi người hàng xóm

Xét hồ sơ truy vấn (8 điểm, 5 giờ), hai hồ sơ A (9, 5) và B (8, 6). Trong đơn vị hiện tại cả hai cách truy vấn 1. Nếu đổi giờ thành phút, B cách 60 còn A vẫn cách 1; thông tin thực tế không đổi mà thuật toán đã thiên hẳn về A. Hình dưới cho thấy đổi đơn vị kéo một trục dài ra. StandardScaler đưa các cột về cùng thang đo; còn nếu biết một cột quan trọng hơn, có thể nhân cột đó với một hệ số lớn hơn, rồi kiểm bằng validation.

Đổi giờ sang phút: B từ cách q 1 thành cách 60.giờ tự học tính bằng giờA (9, 5)B (8, 6)11qgiờ tự học tính bằng phútA (9, 5)B: cách 60, xa ngoài hình1q
Đổi giờ sang phút: B từ cách q 1 thành cách 60.

Cần phân biệt chuẩn hóa theo cột với chuẩn hóa độ dài từng vector. StandardScaler thường dùng cho các đặc trưng số khác đơn vị. Với embedding dùng cosine, ta thường chia mỗi hàng cho norm của chính hàng đó. Vector toàn 0 không có hướng, nên cosine chưa xác định; cần quy tắc xử lý riêng trước khi tính.

Hỏi nhanh

Mỗi ảnh là một embedding 512 chiều, và ta tìm ảnh giống nhau bằng cosine. Muốn chạy kNN theo Euclid cho nhanh, ta chuẩn hóa thế nào?

Dùng thư viện

Python
from sklearn.datasets import load_wine
from sklearn.model_selection import GridSearchCV
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

X, y = load_wine(return_X_y=True)
for scale in [False, True]:
    steps = [StandardScaler()] if scale else []
    model = make_pipeline(*steps, KNeighborsClassifier())
    grid = GridSearchCV(model, {"kneighborsclassifier__n_neighbors": [1, 3, 5, 9, 15]}, cv=5)
    grid.fit(X, y)
    print("chuẩn hóa" if scale else "không chuẩn hóa", round(grid.best_score_, 3), grid.best_params_)

Chênh lệch lớn có lý do cụ thể: cột proline chạy từ 278 đến 1680, trong khi phần lớn các cột khác chỉ cỡ 0.1 đến 30. Chưa chuẩn hóa, proline chiếm khoảng 99.8% bình phương khoảng cách, nên kNN gần như chỉ so một cột và phải dùng k=1k=1 để vớt vát.

Chưa chuẩn hóa, cột proline chiếm 99.8% khoảng cách.chưa chuẩn hóa99.8%proline0.2%magnesium<0.1%alcalinity_of_ash<0.1%color_intensity<0.1%malic_acid<0.1%flavanoidssau StandardScaler7.7%7.7%7.7%7.7%7.7%7.7%phần của mỗi cột trong bình phương khoảng cách (6 cột lớn nhất trên 13)
Chưa chuẩn hóa, cột proline chiếm 99.8% khoảng cách.

Hỏi nhanh

Trong bộ wine chưa chuẩn hóa, riêng cột proline chiếm khoảng 99.8% bình phương khoảng cách. Thêm một feature hữu ích nằm trong khoảng [0,1][0, 1], thì kNN với khoảng cách thô sẽ ra sao?

Khi đi thi

  • Đọc kỹ kiểu khoảng cách và quy tắc phá hòa trong đề.
  • Chuẩn hóa các cột trước khi tính khoảng cách; đặt scaler và kNN trong cùng Pipeline khi dò kk.
  • Có embedding thì chuẩn hóa độ dài rồi dùng kNN theo cosine làm baseline.

Tóm tắt

  • kNN dự đoán theo kk điểm gần nhất; không có giai đoạn học tham số.
  • Chuẩn hóa đặc trưng và chọn khoảng cách đúng quan trọng hơn chọn kk.
  • kk nhỏ thì variance cao, kk lớn thì bias cao; chọn bằng cross-validation.
  • Nhiều chiều thì “gần” mất nghĩa; kNN mạnh nhất trên embedding tốt.

Kiểm tra cuối bài

Mười câu kiểm cả cách hiểu, phép tính và quy trình. Câu sai chỉ ra phần cần đọc lại; hãy làm trước khi mở lời giải.

Câu 1

Năm láng giềng có nhãn [1, 0, 0, 1, 0], bỏ phiếu đều. Nhãn dự đoán là gì, và xác suất lớp 1 tính theo tỉ lệ láng giềng là bao nhiêu?

Câu 2

Câu nào về kNN luôn đúng?

Phỏng theoCMU 10-601 · Exam 1 Practice · Q2.4

Câu 3

Hai láng giềng có khoảng cách d1=1d_1 = 1 và d2=2d_2 = 2, target lần lượt là 0 và 6. Nếu trọng số là 1/d1/d, dự đoán hồi quy bằng bao nhiêu?

Câu 4

Dữ liệu một chiều, nn điểm đã sắp xếp, phân loại hai lớp bằng 1-NN với khoảng cách Euclid. Có dựng được một quy tắc chỉ gồm các phép so sánh dạng x≥tx \ge t, nối tiếp nhau, mà cho dự đoán giống hệt 1-NN trên mọi đầu vào không?

Phỏng theoCMU 10-601 · Exam 1 Practice · Q2.5

Câu 5

Một kNN dùng StandardScaler fit trên train. Đổi cột thu nhập từ triệu đồng sang đồng ở cả train lẫn validation, rồi fit lại scaler đúng trên train. Bỏ qua sai số làm tròn, khoảng cách sau chuẩn hóa thay đổi thế nào?

Câu 6

Đề phân loại hai lớp, 30 feature số, chấm bằng macro-F1. Đề bắt buộc kNN là mô hình học có giám sát duy nhất, còn tiền xử lý thì tùy ý. Hướng làm nào hợp lý nhất?

Phỏng theoUSA-NA-AIO 2026 · Vòng 1 · Problem 9

Câu 7

k=3k = 3, ba láng giềng gần nhất mang ba nhãn khác nhau A, B, C, bỏ phiếu đều. Trước khi áp quy tắc xử lý hòa của thư viện, kết quả là gì?

Câu 8

Hai vector đơn vị u\mathbf u, v\mathbf v có cosine bằng 0.5. Bình phương khoảng cách Euclid giữa chúng bằng bao nhiêu?

Câu 9

Có 10 000 truy vấn và 10 000 điểm tham chiếu. Ma trận khoảng cách kiểu float64 tốn bao nhiêu byte?

Câu 10

Trên cùng các fold, kk = 1, 5, 21 cho accuracy trên validation lần lượt là 0.76, 0.84 và 0.79. Chỉ chọn trong ba giá trị này thì chọn gì?

Đọc thêm

  • James, Witten, Hastie, Tibshirani — An Introduction to Statistical Learning, chương 2 và 4. kNN đặt cạnh các bộ phân loại khác, với lập luận bias–variance khi chọn kk.
  • Cover, Hart (1967) — Nearest Neighbor Pattern Classification, IEEE Transactions on Information Theory. Bài báo gốc của cận hai lần lỗi Bayes.
  • scikit-learn — Nearest Neighbors. So sánh metric, brute force, KD-tree và Ball tree; chú ý phần chi phí theo số chiều.
  • ISLP — Classification lab. Đối chiếu kNN với mô hình xác suất và thực hành lựa chọn k.