機械学習

近傍探索

クエリに距離が近いデータを探す処理を、厳密さ・速度・メモリの制約から選び分けるための整理です。

  • B|標準
  • 機械学習

数式の記号で止まったら 記号の読み方 (∂・⊙・転置・上付き添字を、読み方から)

ひとことで言うと

近傍探索は、クエリ q\mathbf{q} に対して、データ集合の中から距離が小さい点を探す処理です。最も近い1点だけでなく、近い順に kk 点を返す方法や、半径 rr の内側を返す方法も同じ問題として扱えます。分類や回帰の前段だけでなく、埋め込みベクトルの検索にも現れます。

地図上で「現在地から近い店舗」を探すとき、全店舗までの距離を測って並べ替えるのが厳密な探索です。住所を地域ごとの索引に分ければ候補を減らせますが、境界付近では隣の地域も確認しなければなりません。速さは、候補をどれだけ安全に捨てられるかで決まります。

なぜ必要か

最も単純な厳密探索は、クエリと全データ点の距離を計算し、最小値または上位 kk 個を選びます。データ点数を nn、特徴量の次元を dd とすれば、1クエリの距離計算は O(nd)O(nd) です。クエリが増えるほど全探索の時間も線形に増え、検索回数が多いサービスでは距離計算が支配的になります。

そこで、検索前にデータを空間的な構造へ整理します。KD-treeのような木は、クエリから遠い領域を候補から外し、残った葉を調べます。ただし、次元が高いと領域を十分に絞れず、木をたどっても多くの点を確認することになります。低次元で効いた構造を、高次元データへそのまま移せるとは限りません。

探索方式だけでなく、距離の定義も検索結果を決めます。ユークリッド距離とコサイン系の距離では「近い」の意味が違うため、尺度や学習目的に合わない距離を選ぶと、速くても欲しい候補を返しません。

仕組み

データ点を xi∈Rd\mathbf{x}_i \in \mathbb{R}^{d}、クエリを q\mathbf{q} とし、ユークリッド距離を使うなら

d(q,xi)=∑j=1d(qj−xij)2d(\mathbf{q}, \mathbf{x}_i)=\sqrt{\sum_{j=1}^{d}(q_j-x_{ij})^2}

です。ここで dd は特徴量の次元、qjq_j と xijx_{ij} はそれぞれクエリと ii 番目の点の jj 番目の成分です。厳密な最近傍は、この距離が最小になるインデックスを

i∗=argmin⁡i∈{1,…,n} d(q,xi)i^*=\underset{i\in\{1,\ldots,n\}}{\operatorname{argmin}}\ d(\mathbf{q},\mathbf{x}_i)

で選びます。kk近傍なら距離を昇順に並べた先頭 kk 個、半径探索なら d(q,xi)≤rd(\mathbf{q},\mathbf{x}_i)\le r を満たす点です。

KD-treeは各分割で1つの座標軸を使い、点を部分領域へ分けます。ある領域が現在の候補より遠いと判定できれば訪問せず、葉では点の距離を計算します。葉サイズを変えてもクエリ結果は変わらず、探索の計算と木の大きさのバランスが変わります。高次元で除外できる領域が少なくなると枝刈りの効果が薄れ、厳密な全探索に近づきます。

近似最近傍探索は、すべての候補を確認する条件を緩め、候補を絞って近い点を返します。厳密な最近傍を必ず返す保証を、検索時間やメモリと交換する設計です。評価では速度だけでなく、厳密解との一致率や上位 kk の再現率を測ります。どこまで誤差を許せるかを先に決めないと、近似の効果を判定できません。

試験でどう問われるか

問われ方正解に寄る条件引っかけ
厳密探索の計算量1クエリで全 nn 点との距離を調べるため、dd を含めて O(nd)O(nd)kk が小さいからデータ数に依存しないとする
KD-treeの効果空間分割で遠い領域を枝刈りできるときに候補を減らす次元やデータ分布に関係なく常に高速とする
近似最近傍の意味速度・メモリと、最近傍の厳密さや再現率を交換する近似でも厳密解を必ず保証するとする
APIの使い分けkneighborsは個数、radius_neighborsは半径で問い合わせる半径を指定すれば必ず固定個数が返るとする

実装で確かめる

NumPyだけで厳密な1近傍を求めます。全点との距離を計算しているため、データ数に対して線形に仕事が増える形が現れます。平方根は順位を変えないので、比較だけなら二乗距離で十分です。

import numpy as np

X = np.array([[0., 0.], [2., 1.], [5., 4.], [1., 3.]])
q = np.array([1.2, 0.8])
squared = ((X - q) ** 2).sum(axis=1)
order = np.argsort(squared)
print(order[:2], np.sqrt(squared[order[:2]]))

取り違えやすいもの

用語近傍探索との切り分け
最近傍探索距離に基づいて候補の点を返す検索処理。ラベルの予測は含まない
木構造探索候補を絞るためのデータ構造。厳密探索の実装にも使えるが、近似そのものではない
全探索(brute)全点との距離を計算する方式。結果は厳密だが、1クエリの計算は nn に比例する
近似最近傍候補を限定して速度を得る方式。返す点の厳密さを評価する必要がある
埋め込み検索文書や画像などをベクトル化した後の検索用途。内部の核心はベクトル間の近傍探索

想起チェック

厳密な全探索がデータ数に対して線形になる理由は何か

1クエリごとに全 nn 点との距離を計算するためです。次元を dd とすれば距離計算までの目安は O(nd)O(nd) です。

KD-treeが高次元で効きにくくなるのはなぜか

空間分割しても、クエリから遠いと安全に捨てられる領域が少なくなり、多くの点を確認する必要が出るためです。

近似最近傍を採用するとき、速度以外に何を測るか

厳密解との一致率や、上位 kk の再現率です。近似は速度やメモリと検索結果の厳密さを交換するため、許容する品質を数値で確認します。

出典