結論

k近傍法(k-NN、k-Nearest Neighbors)は、予測したい点から距離の近い訓練データを k 個選び、そのラベルの多数決(回帰なら平均)で答えを決める教師あり学習の手法です。学習の段階では何も計算せずデータを覚えるだけで、計算はすべて予測のときに行います。仕組みは単純ですが、k の決め方・距離の選び方・列の尺度・次元の多さで結果が大きく変わります。

この記事でわかること

  • numpy で十数行の k近傍法を自作し、scikit-learn の KNeighborsClassifier・KNeighborsRegressor と予測・確率まで一致させた結果
  • k の決め方:k を 1〜201 に変えたときの訓練・交差検証・テストの正解率の表と、境界が滑らかになる様子を文字で描いた図
  • 次元の呪い(ノイズの列を足すと正解率が 0.927 → 0.587)、KD木・ボール木・近似最近傍(HNSW)で予測の速さがどう変わるかの実測

2026年11月5日更新|Python 3.13.18・numpy 2.5.3・scikit-learn 1.9.1・CPU 2コアで実行確認

この記事で実際に動かしたもの:numpy だけで書いた k近傍法の分類・回帰(一様の多数決と距離の重み付き)・k-means で分けた近似最近傍の索引(IVF)と、scikit-learn 1.9.1 の KNeighborsClassifier・KNeighborsRegressor・NearestNeighbors(brute・kd_tree・ball_tree)・RadiusNeighborsClassifier・NeighborhoodComponentsAnalysis・KNNImputer・GridSearchCV、hnswlib 0.8.0。データは scikit-learn 同梱の iris・wine・breast_cancer・digits・diabetes と、make_moons・make_classification・make_blobs・乱数で作ったものです。

k近傍法は「近くにいる k 人に聞いて、多い方の答えにする」方法です。流れを図にすると次のとおりです。

flowchart TD
    A[訓練データを覚える<br>fit は保存するだけ] --> B[新しい点が来る]
    B --> C[全訓練データとの<br>距離を測る]
    C --> D[近い順に<br>k 個を選ぶ]
    D --> E{分類か回帰か}
    E -->|分類| F[k 個のラベルの<br>多数決]
    E -->|回帰| G[k 個の値の<br>平均]

よく調べられることの答えを先にまとめます。

知りたいこと答え詳しい節
k近傍法とは近い k 個の訓練データの多数決(平均)で予測する教師あり学習k近傍法とは
knn とはk-Nearest Neighbors の略。k近傍法と同じもの。ベクトル検索の「近い k 件を探す」意味でも使うk近傍法とは
k-nn の Python での書き方KNeighborsClassifier(n_neighbors=5).fit(X, y).predict(X_new)最初の5分
k の決め方候補の k を交差検証で比べて選ぶ。2クラスなら奇数にすると同票が出ないkの決め方
k が小さい・大きいと小さいと訓練データに合わせすぎ、大きいと境界がぼやけて多数派に寄るkと境界
距離の種類既定はユークリッド。マンハッタン・コサインなどに変えられる距離の種類
標準化は要る?単位の違う列があるなら要る。wine で 0.663 → 0.961標準化しないと崩れる
次元の呪いとは列が増えると「近い点」と「遠い点」の距離の差が縮み、近さが意味を失うこと次元の呪い
予測が遅い毎回全データとの距離を測るため。KD木・近似最近傍で速くする予測が遅い理由

確認環境:Ubuntu 24.04.5 LTS(Intel Xeon 2.10GHz・2コア)の上で、Python 3.13.18・numpy 2.5.3(OpenBLAS 0.3.34、OPENBLAS_NUM_THREADS=1)・scikit-learn 1.9.1・SciPy 1.18.1 を使い、hnswlib 0.8.0 は作業用の venv に pip で入れました。自作の k近傍法は scikit-learn と同じデータ・同じ k で、予測ラベルと predict_proba の確率が一致することを確かめています。時間は同じマシンで他の処理も動いている中での1回の値で、目安です。algorithm='auto' の選び方・weights・leaf_size・同じ距離の点があるときの扱いは scikit-learn 1.9.1 の公式ドキュメント「Nearest Neighbors」、手法の由来は Fix・Hodges(1951)と Cover・Hart(1967)の論文、HNSW は Malkov・Yashunin の論文で確かめました。図は mermaid で描画を確かめています(確認日 2026年11月5日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「k近傍法(k-NN)とは|Pythonで自作・kの決め方・次元の呪いを実測」

k近傍法(k-NN)とは(意味と「近傍」「knn」の使い分け)

この節では、k近傍法がどんな手法か、「近傍」「kNN」という言葉が場面でどう違うか、k-means法とどう違うかが分かります。

k近傍法は、答え(ラベル)つきの訓練データをそのまま覚えておき、新しい点が来たら、距離がいちばん近い k 個の訓練データを探して、その多数決で答えを決める手法です。「k」は参考にする近くの点の数、「近傍(きんぼう)」は「すぐ近く」という意味です。正解を教えて学ばせるので、機械学習の中では教師あり学習に入ります。分類(どのクラスか)にも回帰(いくつか)にも使えます。

学習(fit)の段階では、データを保存するだけで重みもルールも作りません。計算を予測のときまで先送りするので、怠惰学習(lazy learning)やインスタンスベース学習とも呼ばれます。考え方は Fix と Hodges の1951年の報告にさかのぼり、1967年に Cover と Hart が「データが十分に多ければ、最近傍(k=1)の誤り率はベイズ誤り率(理論上の最小)の2倍を超えない」ことを示して広く知られるようになりました。

「近傍」「kNN」は、ふだんの言葉や IT の別の場面でも使われます。検索結果で混ざりやすいので、先に整理します。

場面意味例
ふだんの言葉近所・すぐ近く「近傍の住民」
数学(位相)ある点を含む、その点のまわりの集まり「点 x の ε 近傍」
機械学習(この記事)近い k 個の訓練データで予測する手法KNeighborsClassifier
ベクトル検索・データベース問い合わせのベクトルに近い k 件を探す処理(kNN 検索)pgvector・Elasticsearch の kNN 検索

機械学習の k近傍法も、内部では「近い k 件を探す」検索をしています。違いは、検索の結果を多数決や平均で1つの答えにまとめるかどうかです。ベクトル検索の側の考え方は、関連記事「エンベディングとは?ベクトル検索とコサイン類似度を計算例で解説」で扱っています。

名前が似ている手法に k-means法があります。目的も k の意味も別物です。

k近傍法(教師あり)

目的:正解つきのデータから、新しい点のラベルや値を当てる

k は多数決に使う近くの点の数

学習の結果は訓練データそのもの

k-means法(教師なし)

目的:正解の無いデータを k 個のグループに分ける

k はグループ(クラスタ)の数

学習の結果は k 個の重心

k-means法は、この記事と同じ確認環境で「k-means法とは|Pythonで自作・クラスタ数の決め方・苦手な形を実測」(公開準備中)として詳しく扱っています。

良いところ弱いところ
仕組みが単純で、予測の理由を「この k 件が近かった」と示せる予測のたびに全データとの距離を測るので遅い。10万件で1,000件の予測に0.58秒(この記事の実測)
学習の時間がほぼ0。データを足すだけで新しい例を反映できる訓練データを丸ごと持つので、モデルが大きい(10万件×10列で 8.8MB)
境界の形を仮定しない。曲がった境界も表せる列の尺度に弱い。単位の違う列は標準化が要る
分類・回帰・欠損値の補完・推薦に同じ考え方で使える関係の無い列が多いと急に弱くなる(次元の呪い)

最初の5分:scikit-learnのKNeighborsClassifierでk-nnを動かす

この節では、Python の scikit-learn で k近傍法を動かす最小のコードと、結果の読み方が分かります。

scikit-learn が入っていれば、追加のダウンロード無しで次のコードが動きます。iris(アヤメの花の4つの寸法と3品種)を訓練7割・テスト3割に分け、k=5 で予測します。

import numpy as np
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier

X, y = load_iris(return_X_y=True)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=0, stratify=y)

knn = KNeighborsClassifier(n_neighbors=5).fit(X_tr, y_tr)
print("正解率", round(knn.score(X_te, y_te), 3))

new = [[5.0, 3.4, 1.5, 0.2], [6.0, 2.8, 4.9, 1.6]]
print(knn.predict(new))         # 予測したクラス
print(knn.predict_proba(new))   # k 個のうち各クラスが占めた割合

dist, idx = knn.kneighbors([new[1]])   # 2つめの点に近い5件
print(np.round(dist, 3)); print(idx); print(y_tr[idx])
正解率 1.0
[0 2]
[[1.  0.  0. ]
 [0.  0.4 0.6]]
[[0.224 0.3   0.3   0.3   0.316]]
[[ 31 100  32  42  27]]
[[1 2 2 2 1]]
1

predict

近い5件の多数決。2つめの点は「品種1が2票・品種2が3票」で品種2になった。

票が割れた点は境目にいる

2

predict_proba

k 件のうち各クラスが占めた割合。k=5 なら 0・0.2・0.4…の5段階にしかならない。

確率として細かく使うなら注意

3

kneighbors

近い k 件の距離と、訓練データの何行目か。予測の根拠をそのまま見せられる。

説明が要る場面で役に立つ

4

score

テストデータでの正解率。iris は分かれやすく、この分け方では1.0になった。

1回の分け方で k を決めない

iris のテスト正解率 1.0 は、データが分かれやすいことと、たまたまの分け方によるものです。k の良し悪しを1回のテストで決めると偶然に左右されるので、後の節では交差検証を使います。

k近傍法のアルゴリズム(距離→近い順→多数決)

この節では、k近傍法の手順を1段ずつと、同票や同じ距離の点があるときに scikit-learn がどう決めるかの実物が分かります。

1

訓練データを保存する

fit では入力の検査と保存をするだけ。KD木などを使う設定なら、探しやすいように並べ替えた木を作る。

fit

2

新しい点と全訓練データの距離を測る

既定はユークリッド距離(各列の差の2乗を足して平方根)。距離の種類は metric で変えられる。

metric・p

3

距離が近い順に k 個を選ぶ

全部を並べ替えなくても、上位 k 個が分かればよい。木や近似の索引はこの段を速くする。

n_neighbors・algorithm

4

多数決(回帰なら平均)で答えを出す

一様に1票ずつ数えるか、近い点ほど重くするかを選べる。

weights

同票になったらどちらになるか

k を偶数にすると、2クラスで票が半々に割れることがあります。訓練データを 0・1・3 の3点にして、0.4 の点を k=2 で予測しました。いちばん近い点(0、距離0.4)がクラス1、次(1、距離0.6)がクラス0です。

同数のとき  [0] [[0.5 0.5]]     ← 近い方はクラス1なのに、答えは0
ラベルを入れ替えたとき [0] [[0.5 0.5]]  ← 近い方がクラス0でも、やはり0

scikit-learn の一様な多数決は、同票のとき「近い方」ではなく、クラスの並び順で先にある方(ここでは0)を選びました。predict_proba の最大値の位置を取るので、同じ値なら先頭が勝つためです。近い方を優先したいなら weights="distance" にすると、近い点の票が重くなり同票が起きにくくなります。breast_cancer の訓練400件・テスト169件で数えると、同票は k=4 で7件、k=6 で3件、k=5 では0件でした。2クラスなら k を奇数にするのが簡単な対策です。

もう1つ、k 番目と k+1 番目の点がまったく同じ距離でラベルが違う場合、公式ドキュメントは「結果は訓練データの並び順に依存する」と書いています。重複した行が多いデータでは、行の順番を変えると予測が変わることがあると覚えておくと、原因を探すときに迷いません。

numpyでk近傍法を自作してscikit-learnと一致させる

この節では、k近傍法を numpy で十数行で書く方法と、scikit-learn の結果と一致することの確かめ方が分かります。

距離の表は、訓練データ X_tr(n×列数)と問い合わせ X_q(m×列数)に軸を足して引き算する「ブロードキャスト」で、m×n の表を一度に作ります。

import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier

def knn_np(X_tr, y_tr, X_q, k, weights="uniform"):
    d = np.sqrt(((X_q[:, None, :] - X_tr[None, :, :]) ** 2).sum(axis=2))  # m×n の距離表
    idx = np.argsort(d, axis=1, kind="stable")[:, :k]                     # 近い順に k 個
    classes = np.unique(y_tr)
    P = np.zeros((len(X_q), len(classes)))
    for r in range(len(X_q)):
        w = np.ones(k) if weights == "uniform" else 1 / d[r, idx[r]]       # 重み
        for c, ww in zip(y_tr[idx[r]], w):
            P[r, np.searchsorted(classes, c)] += ww                         # 票を数える
    P /= P.sum(axis=1, keepdims=True)
    return classes[P.argmax(axis=1)], P

X, y = load_breast_cancer(return_X_y=True)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=0, stratify=y)
sc = StandardScaler().fit(X_tr)
A, B = sc.transform(X_tr), sc.transform(X_te)

for w in ["uniform", "distance"]:
    for k in [1, 5, 15]:
        pred, P = knn_np(A, y_tr, B, k, w)
        sk = KNeighborsClassifier(n_neighbors=k, weights=w, algorithm="brute").fit(A, y_tr)
        print(w, k, "一致", np.array_equal(pred, sk.predict(B)),
              "確率の最大差", np.abs(P - sk.predict_proba(B)).max(), "正解率", round((pred == y_te).mean(), 4))
uniform 1 一致 True 確率の最大差 0.0 正解率 0.9474
uniform 5 一致 True 確率の最大差 0.0 正解率 0.9474
uniform 15 一致 True 確率の最大差 0.0 正解率 0.9415
distance 1 一致 True 確率の最大差 0.0 正解率 0.9474
distance 5 一致 True 確率の最大差 2.220446049250313e-16 正解率 0.9474
distance 15 一致 True 確率の最大差 1.1102230246251565e-16 正解率 0.9415

1予測は171件すべて一致

一様・距離の重み付きのどちらも、k=1・5・15 で予測ラベルが1件残らず同じになった。確率の差も 2.2e-16 以下で、足し算の順番による丸めの誤差だけ。

2「学習」は何もしていない

自作の関数には fit が無い。訓練データを引数で渡すだけで予測できることが、k近傍法が怠惰学習と呼ばれる理由そのもの。

3標準化は訓練データで決める

StandardScaler の平均と標準偏差は訓練データだけで fit し、テストには transform だけを使う。テストも混ぜて fit すると、評価が甘くなる。

回帰も同じ形で書けます。近い10件の目的変数の平均を取るだけで、diabetes(糖尿病の進行度)で KNeighborsRegressor との差は 0.0 でした。

d = np.sqrt(((X_te[:, None] - X_tr[None]) ** 2).sum(2))
idx = np.argsort(d, axis=1, kind="stable")[:, :10]
own = y_tr[idx].mean(axis=1)                                   # 近い10件の平均
sk = KNeighborsRegressor(n_neighbors=10).fit(X_tr, y_tr).predict(X_te)
print("回帰 最大差", np.abs(own - sk).max())                  # 回帰 最大差 0.0

この書き方は m×n×列数 の一時配列を作るので、訓練データが数万件を超えるとメモリを多く使います。仕組みを確かめるための書き方で、業務では scikit-learn を使うのが安全です。配列の軸やブロードキャストに慣れていない場合は、関連記事「NumPyの使い方 基本一覧|配列の作成・演算・形状変更」が役に立ちます。

kの決め方:交差検証で選ぶ(kごとの正解率の表)

この節では、k近傍法の k をどう決めるか、k を変えると訓練・交差検証・テストの正解率がどう動くかが、実測の表で分かります。

breast_cancer(乳がんの検査値30列・2クラス・569件)を訓練7割・テスト3割に分け、訓練データの中だけで5分割の交差検証をしました。標準化と k近傍法は Pipeline でつなぎ、分割ごとに標準化をやり直しています。

from sklearn.model_selection import cross_val_score, StratifiedKFold, GridSearchCV
from sklearn.pipeline import make_pipeline

cv = StratifiedKFold(5, shuffle=True, random_state=0)
for k in [1, 3, 5, 7, 9, 11, 15, 21, 31, 51, 101, 201]:
    pipe = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=k))
    s = cross_val_score(pipe, X_tr, y_tr, cv=cv)
    pipe.fit(X_tr, y_tr)
    print(k, pipe.score(X_tr, y_tr), s.mean(), s.std(), pipe.score(X_te, y_te))
k訓練の正解率交差検証の平均交差検証の標準偏差テストの正解率
11.0000.95230.01440.9474
30.9900.96230.01360.9415
50.9800.96480.01840.9474
70.9770.9698(最大)0.01300.9474
90.9770.96730.01030.9474
150.9700.96480.01660.9415
210.9650.95730.01010.9474
510.9520.94220.01270.9474
1010.9270.91720.01240.9181
2010.8590.78910.01720.8538
  • k=1 は訓練の正解率が必ず1.000です。訓練データの点は、自分自身が距離0の最近傍になるからです。訓練の正解率では k を選べません。この「訓練では満点、見ていないデータでは下がる」形は、「過学習とは|原因と対策、交差検証・正則化・リークをPythonで実測」(公開準備中)で扱う過学習と同じものです。
  • 交差検証の平均は k=7 で最大(0.9698)になり、k=3〜15 はほぼ横並びでした。差は標準偏差(0.01〜0.02)より小さく、この範囲ならどれを選んでも大きくは変わりません。
  • k を訓練データの数(398件)の半分近くまで増やすと、正解率は 0.79 まで落ちます。k が大きすぎると、遠くの点まで票に入り、多数派のクラス(このデータでは良性が63%)に寄るためです。

候補の k と重みの付け方をまとめて探すなら、GridSearchCV が使えます。

grid = GridSearchCV(
    make_pipeline(StandardScaler(), KNeighborsClassifier()),
    {"kneighborsclassifier__n_neighbors": list(range(1, 52, 2)),
     "kneighborsclassifier__weights": ["uniform", "distance"]},
    cv=cv).fit(X_tr, y_tr)
print(grid.best_params_, round(grid.best_score_, 4), "test", round(grid.score(X_te, y_te), 4))
{'kneighborsclassifier__n_neighbors': 7, 'kneighborsclassifier__weights': 'uniform'} 0.9698 test 0.9474

よく紹介される「k はデータ数の平方根」という目安では、訓練398件なら k=19 になり、交差検証の平均は 0.960 でした。最良からは少し離れるので、平方根は探し始める範囲の目安にとどめ、最後は交差検証で決めるのが確実です。

1.000k=1 の訓練の正解率自分自身が最近傍になる
7交差検証で選ばれた k平均 0.9698
0.789k=201 の交差検証の平均多数派に寄って低下

k を決める手順は次の順で進めると迷いが減ります。

  1. 1先に標準化と Pipeline を組む尺度がそろっていないと、どの k を選んでも距離が偏ったまま
  2. 2k の候補を 1〜平方根の2〜3倍まで取る2クラスなら奇数だけにする。多クラスでも同票は weights="distance" で減らせる
  3. 3層化した k 分割交差検証で平均と標準偏差を出すStratifiedKFold でクラスの割合をそろえる
  4. 4最大に近い k の中から、大きめを選ぶ差が標準偏差より小さいなら、境界が滑らかな大きい k の方がぶれにくい
  5. 5最後に一度だけテストデータで確かめるテストを見て k を選び直すと、テストに合わせたことになる

kを変えると境界はどう変わるか(make_moonsで描く)

この節では、k の大きさで分類の境界が細かくなったり滑らかになったりする様子が、文字で描いた図と数字で分かります。

三日月が2つ噛み合った形のデータ(make_moons、600点、ノイズ0.3)を半分ずつ訓練とテストに分け、平面を 44×14 のマスに区切って、各マスの予測を「.」(クラス0)と「#」(クラス1)で描きました(上から11行を抜き出しています)。「切り替わり数」は、横に並んだマスで予測が変わった回数の合計で、境界の入り組み具合の目安です。

k=1 切り替わり数 36
..................................##########
..................................##########
.................................###########
.................#..............############
...............................#############
...............................#############
..............##.##............##.##########
..........##..######.###...#...#############
..........###..########.####......##########
............################################
.........###################################

k=15 切り替わり数 22
...................................#########
..................................##########
..................................##########
.................................###########
.................................###########
................................############
................##.............#############
............##########.#........############
............###############.################
...........#################################
..........##################################

k=151 切り替わり数 14
.........................................###
......................................######
...................................#########
.................................###########
...............................#############
............................################
...........................#################
.......................#####################
..................##########################
................############################
..............##############################

k=1 では、ノイズで相手側に入り込んだ1点のまわりにも小さな島ができています(4行目の「#」1つ)。k=15 では島が消え、三日月の噛み合わせに沿った曲がった境界が残りました。k=151 では境界がほぼ1本の斜めの線になり、三日月の形をたどれなくなっています。

k訓練の正解率交差検証の平均テストの正解率境界の様子
11.0000.9330.877ノイズの点ごとに島ができる
50.9400.9230.877島が減る
150.9230.9200.887三日月に沿った曲線
310.9230.9230.890なめらかな曲線
750.9170.9230.873曲がりが弱まる
1510.8600.7900.823ほぼ直線

scikit-learn の公式ドキュメントも「一般に k を大きくするとノイズの影響が抑えられるが、分類の境界がはっきりしなくなる」と説明しています。このデータでは、訓練とテストの差がいちばん小さく、テストも高かったのは k=15〜31 でした。小さい k は「分散が大きい(データが変わると答えが大きく変わる)」、大きい k は「偏りが大きい(形をたどれない)」と言い換えられます。

k を決める

やってはいけない訓練データの正解率がいちばん高い k を選ぶ(k=1 が必ず選ばれる)
→
代わりに交差検証の平均と標準偏差で比べ、差が小さければ大きめの k を選ぶ

k=1 は訓練データを丸暗記した状態

距離の種類(ユークリッド・マンハッタン・コサイン)と重み付き

この節では、k近傍法で使う距離の種類と計算の違い、データごとにどれが良かったか、距離で重みを付ける weights="distance" の効果が分かります。

点 a=(1, 2, 3) と b=(4, 0, 3) で、各距離を SciPy で計算しました。

距離計算a と b の値scikit-learn の指定
ユークリッド差の2乗の和の平方根(直線距離)3.606metric="euclidean"(既定は minkowski の p=2)
マンハッタン差の絶対値の和(碁盤の目に沿った距離)5.0metric="manhattan" か p=1
チェビシェフ差の絶対値の最大3.0metric="chebyshev"
ミンコフスキー差の p 乗の和の p 乗根(p=1 がマンハッタン、p=2 がユークリッド)3.271(p=3)p=3
コサイン距離1 − コサイン類似度(向きの違いだけを見る)0.305metric="cosine"(brute のみ)

0/1 の列や文字の並びには、値が違う列の割合を数えるハミング距離も使えます([1,0,1,1,0] と [1,1,1,0,0] は5列中2列が違うので 0.4)。

4つのデータで、k=5・5分割交差検証の平均正解率を比べました。digits(8×8 の手書き数字の画素)以外は標準化してあります。

距離iriswinebreast_cancerdigits
ユークリッド0.9530.9610.9650.986
マンハッタン0.9470.9610.9670.984
チェビシェフ0.9400.9210.9420.973
ミンコフスキー p=30.9470.9490.9670.986
コサイン0.8870.9550.9670.983
  • ユークリッドとマンハッタンの差は、どのデータでも0.01未満でした。最初は既定のユークリッドで始め、交差検証で k と一緒に比べる程度で済みます。
  • チェビシェフは全データで最下位でした。いちばん差の大きい1列だけで距離が決まり、他の列の情報を捨ててしまうためです。
  • コサインは iris だけ大きく下がりました(0.887)。コサインは「原点から見た向き」だけを比べ、大きさを無視します。標準化した iris では、花の大きさ(原点からの遠さ)に品種の情報があったため、それを捨てて落ちました。文章の埋め込みのように「向きに意味があり、長さは文章の長さで変わる」データで使う距離です。

コサイン距離は、各行を長さ1にそろえて(L2 正規化して)からユークリッド距離を使うのと、近傍の順番が同じになります。乱数の500点×20列で確かめると、10近傍が5件の問い合わせすべてで一致しました。KD木はコサインを受け付けないので、大きなデータでは「正規化してユークリッド」にすると木や近似索引が使えます。この関係は、関連記事「Qdrant入門|ベクトル検索のコレクション作成と検索(Python)」のような専用のベクトル検索でも同じです。

重み付き k近傍法(weights="distance")

公式ドキュメントによると、weights="distance" は「問い合わせ点からの距離の逆数に比例した重み」を付けます。近い点の票ほど重くなり、遠くの点の影響が弱まります。breast_cancer(標準化、5分割)で比べました。

k一様(uniform)距離の逆数(distance)
10.9580.958
50.9650.965
150.9610.965
510.9470.951
1510.9100.925

k が小さいうちは差が無く、k が大きいほど重み付きの方が下がり方が緩やかでした(k=151 で 0.910 と 0.925)。遠くの点が票に入っても、重みが小さいので答えを動かしにくいためです。ただし重み付きでは、訓練データの点を予測すると自分自身(距離0)が無限大の重みになり、訓練の正解率が k によらず1.000になります。訓練の正解率で過学習を見る用途には向きません。

標準化しないとk近傍法の距離は崩れる

この節では、列の単位が違うデータで k近傍法がどう崩れるかを、1つの実測で短く確かめられます。

k近傍法は距離だけで判断するので、値の幅が大きい列が距離をほぼ決めてしまいます。wine(ワインの成分13列)は、proline の値が数百〜千台、他の列は1桁〜2桁です。

wine k=5 5分割交差検証
そのまま        0.6633
標準化してから  0.9608

標準化(各列を平均0・標準偏差1にそろえる)だけで、正解率が 0.663 から 0.961 に上がりました。MinMaxScaler・RobustScaler との違いや、決定木では尺度が影響しない理由、訓練データだけで fit しないとリークする例は、「標準化と正規化の違い|スケーリングで精度が変わるモデルを8種で実測」(公開準備中)で詳しく扱っています。k近傍法では make_pipeline(StandardScaler(), KNeighborsClassifier()) の形を基本にしておけば、交差検証でも予測でも同じ変換がかかります。

次元の呪い:列が増えると近さが意味を失う(実測)

この節では、次元の呪いが k近傍法にどう効くかを、距離の分布と正解率の2つの実測で確かめられます。

0〜1 の一様乱数で1,000点を作り、1つの問い合わせ点から全点までの距離を測りました。次元 d(列の数)を 1 から 10,000 まで増やしています。

次元 d最近傍 ÷ 最遠点の距離(最遠 − 最近)÷ 最近距離の標準偏差 ÷ 平均10%の点を囲む立方体の一辺
10.0002574.70.5640.10
20.01566.10.4820.32
30.03527.40.3110.46
100.2253.440.1780.79
1000.6780.4750.0550.98
1,0000.8780.1400.0171.00
10,0000.9620.0400.0061.00
  • 1,000次元では、いちばん近い点といちばん遠い点の距離の比が 0.878 でした。どの点も「だいたい同じくらい遠い」状態で、「近い k 個」を選んでも、たまたま少し近いだけの点になります。
  • 右の列は、データ全体の10%を含む小さな立方体を作るときに、一辺が全体の何割になるかです(0.1 の 1/d 乗)。10次元で 0.79、100次元で 0.98 で、「近所」を作ろうとしても範囲がほぼ全体になってしまいます。

これが次元の呪い(curse of dimensionality)です。ただし実際の正解率に効くのは、列の数そのものより「答えに関係の無い列」の数です。答えに関係する5列を持つデータ(make_classification、600件)に、ただの乱数の列を足していきました。

足した乱数の列k近傍法(k=5)ロジスティック回帰ランダムフォレスト
0(5列)0.9270.8920.915
5(10列)0.8830.8820.902
20(25列)0.7820.8720.893
50(55列)0.7230.8570.877
100(105列)0.7050.8270.872
300(305列)0.5870.7550.858

乱数の列が無いときは k近傍法がいちばん高かったのに、300列足すと 0.587(でたらめの0.5に近い)まで落ちました。ロジスティック回帰は関係の無い列の係数を小さくでき、ランダムフォレストは分割に役立つ列を選べるので、下がり方が緩やかです。k近傍法はすべての列を同じ重さで距離に入れるので、関係の無い列がそのまま雑音になります。breast_cancer に乱数の列を足しても、0列 0.965・30列 0.944・100列 0.903・300列 0.865 と同じ傾向でした。

305列のデータに対策をかけた結果です。

対策(Pipeline の中で実行)k近傍法の正解率
何もしない0.587
PCA で10次元に圧縮0.548
NCA(近傍成分分析)で5次元に(乱数50列のデータ)0.827(何もしないと 0.723)
SelectKBest(F 値)で5列を選ぶ0.892

PCA はかえって下がりました。PCA は「ばらつきの大きい方向」を残しますが、ここでは300列の乱数の方が全体のばらつきの大半を占めるため、答えに関係の無い方向を残してしまったのです。PCA が効くのは、たくさんの列が少数の要因で動いている(列どうしが相関している)データで、仕組みは関連記事「主成分分析(PCA)とは|手順・寄与率・負荷量の読み方をPythonで実演」で扱っています。一方、ラベルを使って列を選ぶ SelectKBest と、ラベルを使って「k近傍法が当たりやすい距離」を学ぶ NCA は効きました。どちらも交差検証の分割ごとに選び直すよう Pipeline の中に入れています(全データで先に選ぶと、評価が甘くなります)。

逆に、digits は64列ありますが k近傍法の正解率は 0.986 と高いままでした。画素どうしが強く相関していて、手書き数字の違いは実際には少ない要因で決まる(データが64次元の中の低い次元の面に乗っている)からです。次元の呪いは「列の数」より「答えに関係しない独立したばらつきの数」で起きると考えると、どのデータで効くかを見分けやすくなります。

予測が遅い理由とKD木・ボール木・近似最近傍(HNSW)

この節では、k近傍法の予測が遅い理由と、scikit-learn の algorithm(brute・kd_tree・ball_tree)の違い、近似最近傍で速くしたときの再現率と速さが分かります。

学習は一瞬、予測が重い

10列のデータで、件数を変えて k近傍法とロジスティック回帰の fit と predict(1,000件)の時間を測りました。

訓練の件数k近傍法 fitk近傍法 predict 1,000件ロジスティック回帰 predict 1,000件保存したモデルの大きさ
1万件21.8ms90.5ms1ms 未満k近傍法 0.88MB・ロジスティック 0.8KB
10万件19.5ms583.0ms1ms 未満k近傍法 8.80MB・ロジスティック 0.8KB

k近傍法の fit は件数が10倍になっても変わらず(保存するだけ)、predict は件数にほぼ比例して重くなりました。モデルの大きさ(pickle)も訓練データの大きさそのものです。ロジスティック回帰は係数11個を持つだけなので、件数によらず小さく速いままです。「学習は速いが予測が遅い」のが k近傍法の性質で、1件ずつすぐ返したい API や、訓練データが数百万件ある場面では、この点が選ぶかどうかの分かれ目になります。

KD木・ボール木は低い次元で効く

KD木は、空間を列ごとに半分に割り続けて木にし、問い合わせ点から遠い枝を丸ごと調べずに済ませる仕組みです。ボール木は、点のまとまりを球で包んで同じことをします。5万件の一様乱数で、10近傍を1,000件探す時間を測りました。

次元brutekd_tree(木の作成)ball_tree(木の作成)auto が選んだもの
2245ms5ms(43ms)9ms(35ms)kd_tree
8365ms179ms(134ms)339ms(86ms)kd_tree
32528ms6,683ms(473ms)4,514ms(372ms)brute
128706ms11,958ms(1,244ms)5,709ms(403ms)brute

2次元では KD木が brute の約50倍速く、32次元を超えると逆に10倍以上遅くなりました。次元が高いと、前の節のとおりどの点も同じくらい遠いので、「遠い枝を捨てる」ことができず、木をたどる手間だけが増えるからです。公式ドキュメントによると、algorithm="auto" は、入力が疎行列・metric="precomputed"・次元 D が15より大きい・k が件数の半分以上・木が対応していない距離、のどれかなら brute を選び、それ以外は kd_tree か ball_tree を選びます。実測でも8次元で kd_tree、32次元で brute が選ばれ、その時点の最速と合っていました。既定の auto のまま使うのが基本で、葉の大きさ leaf_size(既定30)は公式でも「よい妥協点」とされています。

近似最近傍:正確さを少し手放して速くする

次元が高く件数も多い場面では、「本当の上位 k 件」を少し取りこぼしてもよい代わりに速く探す近似最近傍(ANN)を使います。5万件×64次元(200個のまとまりがあるデータ)で、brute の正確な答えを正解として、上位10件のうち何件を当てたか(再現率@10)を測りました。

方法設定再現率@101,000件の検索索引の作成
brute(正確)—1.000477ms不要
IVF(自作)256区画中1区画だけ探す0.903(調べた点 0.4%)5.4秒
IVF(自作)4区画(調べた点 1.6%)1.000(Python のループのため参考外)5.4秒
HNSW(hnswlib)ef=100.88324ms7.9秒
HNSW(hnswlib)ef=500.99988ms7.9秒
HNSW(hnswlib)ef=2001.000264ms7.9秒
  • IVF は、先に k-means で点を256区画に分け、問い合わせに近い区画の中だけを調べる方法です。4区画(全体の1.6%の点)だけ調べて、再現率1.000になりました。自作は1件ずつ Python で回しているので、時間は brute より遅く出ています。調べた点の割合が、本来の速さの目安です。
  • HNSW(Malkov・Yashunin の論文)は、近い点どうしをつないだ多層のグラフをたどる方法です。ef(探索で持つ候補の数)を50にすると、再現率0.999で brute の約5倍速くなりました。代わりに索引の作成に7.9秒かかり、メモリも多く使います。

pgvector の HNSW・IVFFlat 索引も同じ考え方です。PostgreSQL での使い方は関連記事「pgvectorの使い方|PostgreSQLでベクトル検索をコピペで構築」で扱っています。k近傍法の分類器としては、scikit-learn 1.9.1 の KNeighborsClassifier に近似の索引は入っていないので、近傍の検索だけを外の索引で行い、返ってきた k 件の多数決を自分で取る形になります。

k近傍法の回帰(KNeighborsRegressor)と外挿できない弱点

この節では、k近傍法で数値を予測する回帰の使い方と、訓練データの範囲の外を予測できない弱点が分かります。

回帰では、近い k 件の目的変数の平均(weights="distance" なら距離の逆数で重み付けした平均)を答えにします。diabetes(10列・442件)で、5分割交差検証の決定係数 R² を比べました。

k一様の平均重み付きの平均
1-0.122-0.122
50.3820.384
100.4450.444
200.4520.456
500.4250.433
線形回帰(比較)0.489

k=1 は R² がマイナスで、平均値をいつも答えるより悪い結果でした。1件の値をそのまま使うので、ノイズまで写してしまうからです。k=20 前後で最良になりましたが、線形回帰(0.489)には届きませんでした。目的変数が列の和に近い形で決まるデータでは、線形のモデルの方が向いています。

もう1つの弱点は外挿です。y = 2x にノイズを足した80点(x は0〜10)で学習し、範囲の外を予測しました。

x=5    kNN 10.64  線形 9.99   正しい値 10
x=9.9  kNN 19.14  線形 19.68  正しい値 19.8
x=12   kNN 19.14  線形 23.83  正しい値 24
x=20   kNN 19.14  線形 39.64  正しい値 40
(訓練データの y の最大は 20.2)

x=12 でも x=20 でも、k近傍法は 19.14 のまま動きませんでした。範囲の外の点にとって、近い5件はいつも x=10 付近の同じ5件だからです。k近傍法は訓練データの値の平均しか返せないので、訓練データの最大を超える値は予測できません。売上の伸びや気温の上昇のように、これまでに無い大きさの値が来るデータには向きません。

k近傍法の使いどころと判断表(欠損値の補完・推薦)

この節では、k近傍法が実際にどんな場面で使われるか、ほかの手法とどう選び分けるかが分かります。

分類・回帰のほかに、「近い k 件」という考え方そのものが役に立つ場面があります。

  • 欠損値の補完:scikit-learn の KNNImputer は、欠損のある行に近い k 行の値の平均で穴を埋めます。下の例では、3列目が欠けた1行目に近い2行(2行目と3行目)の平均 4.0、1列目が欠けた3行目には 5.5 が入りました。
  • 推薦:好みの似たユーザー k 人が高く評価した商品を勧める(ユーザーベースの協調フィルタリング)。
  • 類似検索:画像や文章を埋め込みのベクトルにして、近い k 件を返す。RAG の検索部分もこの形です。
  • 異常の目安:k 番目に近い点までの距離が大きい点は、周りに仲間がいない「外れた点」とみなせる。
from sklearn.impute import KNNImputer
A = np.array([[1., 2., np.nan], [3., 4., 3.], [np.nan, 6., 5.], [8., 8., 7.]])
print(KNNImputer(n_neighbors=2).fit_transform(A))
[[1.  2.  4. ]
 [3.  4.  3. ]
 [5.5 6.  5. ]
 [8.  8.  7. ]]
状況k近傍法を選ぶか理由
列が少なく(十数列まで)、件数が数万件まで選ぶ。最初の比較の基準にもなる速く試せて、境界の形を仮定しない
予測の理由を「似た事例」で説明したい選ぶkneighbors で根拠の k 件を見せられる
データが頻繁に増え、すぐ反映したい選ぶ学習し直さず、データを足すだけで反映できる
答えに関係の無い列が多い・列が数百ある列を選ぶか NCA の後なら選ぶ。そのままなら避ける乱数300列で 0.927 → 0.587
1件ずつの予測を数ミリ秒で返したい・件数が数百万近似最近傍と組み合わせるか、別の手法予測のたびに全件と比べるのは重い
訓練データの範囲の外を予測する回帰避ける訓練データの平均しか返せない
カテゴリの列が中心距離を工夫できるときだけ数値の差としての距離に意味が無い。ハミング距離などを検討

k近傍法を業務のデータにかける前に確かめておくことです。

  • 1数値の列だけで、欠損が無い文字列は数値に変えるか外す。NaN はエラーになるので埋めるか行を落とす
  • 2標準化を Pipeline に入れた単位の違う列が混ざるなら必須。訓練データだけで fit する
  • 3関係の無い列を減らした列を選ぶ・NCA を試す。PCA は効かない場合もある
  • 4k を交差検証で選んだ2クラスなら奇数。差が小さければ大きめの k
  • 5予測の時間とモデルの大きさを測った本番の件数で predict を1回測る。遅ければ近似最近傍を検討

よくあるエラー・つまずき(KNeighborsClassifierのエラー原文)

この節では、scikit-learn 1.9.1 の k近傍法で実際に出たエラーの原文と、その直し方が分かります。

出た文言(原文)原因直し方
ValueError: Expected n_neighbors <= n_samples_fit, but n_neighbors = 20, n_samples_fit = 10, n_samples = 10訓練データの件数より大きい k を指定した。交差検証では分割後の件数で起きるk を訓練件数(交差検証なら分割後の件数)より小さくする
InvalidParameterError: The 'n_neighbors' parameter of KNeighborsClassifier must be an int in the range [1, inf) or None. Got 0 instead.k に0を指定した1以上の整数にする
ValueError: Input X contains NaN.欠損値が入っているSimpleImputer か KNNImputer で埋める
ValueError: Metric 'cosine' not valid. Use sorted(sklearn.neighbors.VALID_METRICS['kd_tree']) to get valid options.KD木が対応していない距離を指定したalgorithm="brute" にするか、L2 正規化してユークリッドを使う
ValueError: Must provide either V or VI for Mahalanobis distanceマハラノビス距離に共分散行列を渡していないmetric_params={"VI": np.linalg.inv(np.cov(X.T))} を渡す
ValueError: X has 3 features, but KNeighborsClassifier is expecting 2 features as input.学習時と列の数が違うデータで予測した学習と同じ列・同じ順番で渡す
ValueError: Expected 2D array, got 1D array instead:1件の点を1次元の配列で渡した[[1, 2]] か x.reshape(1, -1) にする
ValueError: Unknown label type: continuous. Maybe you are trying to fit a classifier, which expects discrete classes on a regression target with continuous values.小数の目的変数を分類器に渡した数値を当てるなら KNeighborsRegressor を使う
ValueError: No neighbors found for test samples array([0, 1, ...]), you can try using larger radius, giving a label for outliers, or considering removing them from your dataset.RadiusNeighborsClassifier で、半径の中に訓練データが1件も無い半径を広げるか、outlier_label を指定する
NotFittedError: This KNeighborsClassifier instance is not fitted yet.fit の前に predict した先に fit する
InvalidParameterError: The 'weights' parameter of KNeighborsClassifier must be a str among {'uniform', 'distance'}, a callable or None. Got 'dist' instead.weights の綴りの間違い"distance" と書く
  • エラーにならないつまずきの方が結果への影響が大きいです。標準化を忘れて1列だけで距離が決まっている(wine で 0.663)、関係の無い列が多い(0.587)、偶数の k で同票がクラスの並び順で決まっている、の3つは、数字を比べないと気づけません。
  • 交差検証で Expected n_neighbors <= n_samples_fit が出ると、scikit-learn は警告を出してその分割のスコアを nan にし、平均も nan になります。k の候補の上限は「訓練件数 × (分割数 − 1) ÷ 分割数」より小さくします。

よくある質問

Q. k近傍法は教師あり学習ですか、教師なし学習ですか。

A. 分類・回帰に使う k近傍法は、正解ラベルを使うので教師あり学習です。ただし、ラベルを使わずに「近い k 件を探す」だけの処理(scikit-learn の NearestNeighbors やベクトル検索)は、教師なしの道具として使われます。

Q. k はいくつから試せばよいですか。

A. 1 から「訓練件数の平方根の2〜3倍」くらいまでを、2クラスなら奇数だけで交差検証にかけます。breast_cancer では平方根の19より小さい k=7 が最良で、k=3〜15 はほぼ同じ値でした。

Q. k=1(最近傍法)はいつ使いますか。

A. ノイズがほとんど無く、同じクラスの点がまとまっているデータでは k=1 でも高い正解率になります(digits は k=3 でテスト 0.985)。ただし訓練の正解率が必ず1.0になり、1点のノイズがそのまま境界に出るので、まずは交差検証で他の k と比べます。

Q. 学習(fit)が速いのに、なぜ予測が遅いのですか。

A. fit はデータを保存するだけで、距離の計算をすべて予測のときに行うからです。10万件では、fit 19.5ms に対して1,000件の予測に583ms かかりました。

Q. 距離はユークリッドとマンハッタンのどちらを使うべきですか。

A. 既定のユークリッドから始めます。4つのデータで両者の差は0.01未満でした。外れた値を持つ列があると、2乗するユークリッドはその列に引っ張られやすいので、マンハッタンも交差検証で比べます。

Q. コサイン距離はどんなときに使いますか。

A. 文章の埋め込みや単語の出現回数のように、ベクトルの向きに意味があり長さは重要でないデータです。iris のように大きさ自体に情報があるデータでは、0.953 → 0.887 と下がりました。

Q. 次元の呪いを避けるには、列を減らせばよいのですか。

A. 減らす列を正しく選ぶことが大切です。乱数300列を足したデータで、ラベルを使って5列を選ぶと 0.892 に戻りましたが、PCA で10次元にすると 0.548 とかえって下がりました。答えに関係の無い列を外すのが目的です。

Q. 画像(MNIST など)にも使えますか。

A. 小さな画像なら使えます。scikit-learn 同梱の digits(8×8 画素)では k=3 でテスト正解率 0.985 でした。画素どうしが強く相関していて、実質的な次元が低いためです。大きな画像や、ずれ・回転のある画像では、ニューラルネットワークで特徴を取り出してから近傍を探す方が向いています。仕組みは関連記事「ニューラルネットワークとは|仕組み・学習・AttentionをNumPyで実演」で扱っています。

Q. predict_proba の値を確率として使えますか。

A. k 件のうちの割合なので、k=5 なら 0・0.2・0.4・0.6・0.8・1.0 の6段階しかありません。しきい値を細かく動かしたい用途では、k を大きくするか、確率を出すのが得意な別のモデルと比べます。

Q. 同じデータなのに、行の順番を変えたら予測が変わりました。

A. k 番目と k+1 番目が同じ距離でラベルが違う点があると、公式ドキュメントのとおり結果が訓練データの並び順に依存します。重複した行がある場合に起きやすいので、重複を確かめるか、k を変えて結果が揺れないかを見ます。

まとめ

  • k近傍法は、近い k 個の訓練データの多数決(回帰なら平均)で予測する手法です。numpy の十数行で書け、scikit-learn と予測・確率まで一致しました。学習は保存するだけで、計算は予測のときに行います。
  • k は交差検証で選びます。k=1 は訓練の正解率が必ず1.0になり、k を大きくしすぎると多数派に寄ります。breast_cancer では k=7 が最良で、k=3〜15 はほぼ同じでした。2クラスなら奇数にすると同票が出ません。
  • 距離はユークリッドから始め、マンハッタン・コサインは交差検証で比べます。単位の違う列は標準化が前提で、wine は 0.663 → 0.961 でした。
  • 次元の呪いは「答えに関係の無い列」で起きます。乱数300列で 0.927 → 0.587 に落ち、列を選ぶと 0.892 に戻りました。PCA はかえって下がることがあります。
  • 予測は件数に比例して重く、KD木は低い次元だけで効きます(2次元で約50倍速く、32次元で10倍以上遅い)。高い次元では HNSW などの近似最近傍で、再現率0.999のまま約5倍速くなりました。

参考・出典

いずれも確認日は2026年11月5日です。

  • scikit-learn 1.9.1「1.6. Nearest Neighbors」:https://scikit-learn.org/stable/modules/neighbors.html
  • scikit-learn「KNeighborsClassifier」:https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.KNeighborsClassifier.html
  • scikit-learn「KNeighborsRegressor」:https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.KNeighborsRegressor.html
  • scikit-learn「KNNImputer」:https://scikit-learn.org/stable/modules/generated/sklearn.impute.KNNImputer.html
  • scikit-learn「NeighborhoodComponentsAnalysis」:https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.NeighborhoodComponentsAnalysis.html
  • Fix, E. and Hodges, J. L.「Discriminatory Analysis. Nonparametric Discrimination: Consistency Properties」(USAF School of Aviation Medicine, 1951)
  • Cover, T. and Hart, P.「Nearest neighbor pattern classification」(IEEE Transactions on Information Theory, 1967)
  • Malkov, Y. A. and Yashunin, D. A.「Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs」(IEEE TPAMI, 2018)
  • hnswlib「README」:https://github.com/nmslib/hnswlib

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「k近傍法(k-NN)とは|Pythonで自作・kの決め方・次元の呪いを実測」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

成長のヒミツ、 ぜんぶ丸わかり