K-最近邻(KNN)算法的基础是您有一个数据矩阵,它由
N
行和
M
列组成,其中
N
是我们拥有的数据点数,而
M
是每个数据点的维度。例如,如果我们将笛卡尔坐标放入数据矩阵中,这通常是一个
N x 2
或
N x 3
矩阵。使用此数据矩阵,您提供一个查询点,并在该数据矩阵中搜索距离该查询点最近的
k
个点。
通常,我们使用查询点与数据矩阵中其余点之间的欧几里得距离来计算距离。但是,也会使用其他距离,如L1或城市块/曼哈顿距离。完成此操作后,您将拥有
N
个欧几里得或曼哈顿距离,这些距离表示查询与数据集中每个对应点之间的距离。找到这些距离后,您只需按升序排序距离并检索那些与数据集和查询之间距离最小的
k
个点即可。
假设您的数据矩阵存储在
x
中,而
newpoint
是一个样本点,其中它有
M
列(即
1 x M
),这是您将遵循的一般过程:
- 计算
newpoint
与x
中每个点之间的欧几里得或曼哈顿距离。
- 按升序对这些距离进行排序。
- 返回
x
中最接近newpoint
的k
个数据点。
让我们慢慢地完成每个步骤。
步骤 #1
有一种方法可以使用 for
循环来实现:
N = size(x,1);
dists = zeros(N,1);
for idx = 1 : N
dists(idx) = sqrt(sum((x(idx,:) - newpoint).^2));
end
如果你想要实现曼哈顿距离,这将非常简单:
N = size(x,1);
dists = zeros(N,1);
for idx = 1 : N
dists(idx) = sum(abs(x(idx,:) - newpoint));
end
将是一个包含
x
中每个数据点与
newpoint
之间距离的
N
元素向量。我们对
newpoint
和
x
中的数据点进行逐元素相减,平方差值,然后将它们全部
sum
在一起。然后对这个总和进行平方根运算,就完成了欧几里得距离的计算。对于曼哈顿距离,您需要执行逐元素相减,取绝对值,然后将所有分量相加。这可能是最简单易懂的实现方法,但对于较大的数据集和更高维度的数据来说,可能效率不高...
另一个可能的解决方案是复制newpoint
并使该矩阵与x
具有相同的大小,然后对此矩阵进行逐元素相减,然后对每行进行列求和并进行平方根运算。因此,我们可以这样做:
N = size(x, 1);
dists = sqrt(sum((x - repmat(newpoint, N, 1)).^2, 2));
对于曼哈顿距离,您将执行以下操作:
N = size(x, 1);
dists = sum(abs(x - repmat(newpoint, N, 1)), 2);
repmat
接受矩阵或向量,并在给定方向上重复它们一定次数。在我们的情况下,我们想要取出我们的newpoint
向量,并将其在顶部重复N
次,以创建一个N x M
矩阵,其中每行长度为M
。我们将这两个矩阵相减,然后平方每个元素。完成后,我们对每行的所有列进行sum
,最后对所有结果取平方根。对于曼哈顿距离,我们执行减法,取绝对值,然后求和。
然而,在我看来,最有效的方法是使用bsxfun
。这实际上是在单个函数调用中在内部执行了我们谈论过的复制操作。因此,代码将简单地如下所示:
dists = sqrt(sum(bsxfun(@minus, x, newpoint).^2, 2));
对我来说,这看起来更加简洁明了。对于曼哈顿距离,您可以执行以下操作:
dists = sum(abs(bsxfun(@minus, x, newpoint)), 2);
步骤 #2
现在我们有了距离,我们只需要对它们进行排序。我们可以使用 sort
来对距离进行排序:
[d,ind] = sort(dists);
d
会按升序包含距离,而ind
告诉您在未排序数组中每个值在排序结果中出现的位置。我们需要使用ind
,提取该向量的前k
个元素,然后使用ind
索引到我们的x
数据矩阵中返回那些最接近newpoint
的点。
第3步
最后一步是现在返回那些最接近newpoint
的k
数据点。我们可以通过以下方法非常简单地完成:
ind_closest = ind(1:k);
x_closest = x(ind_closest,:);
ind_closest
应该包含原始数据矩阵x
中最接近newpoint
的索引。具体而言,ind_closest
包含你需要从x
中抽样的哪些行以获得最接近newpoint
的点。x_closest
将包含这些实际数据点。
为了方便复制粘贴,以下是代码的样子:
dists = sqrt(sum(bsxfun(@minus, x, newpoint).^2, 2));
[d,ind] = sort(dists);
ind_closest = ind(1:k);
x_closest = x(ind_closest,:);
运行您的示例,让我们看看我们的代码实际效果:
load fisheriris
x = meas(:,3:4);
newpoint = [5 1.45];
k = 10;
dists = sqrt(sum(bsxfun(@minus, x, newpoint).^2, 2));
[d,ind] = sort(dists);
ind_closest = ind(1:k);
x_closest = x(ind_closest,:);
通过检查ind_closest
和x_closest
,我们得到以下结果:
>> ind_closest
ind_closest =
120
53
73
134
84
77
78
51
64
87
>> x_closest
x_closest =
5.0000 1.5000
4.9000 1.5000
4.9000 1.5000
5.1000 1.5000
5.1000 1.6000
4.8000 1.4000
5.0000 1.7000
4.7000 1.4000
4.7000 1.4000
4.7000 1.5000
如果你运行了
knnsearch
函数,你会发现变量
n
与
ind_closest
匹配。然而,变量
d
返回的是从
newpoint
到每个点
x
的
距离,而不是实际的数据点本身。如果你想要实际的距离,只需在我写的代码后面执行以下操作:
dist_sorted = d(1:k);
请注意,上面的答案仅在一批
N
示例中使用一个查询点。很频繁地,KNN会同时用于多个示例。假设我们有
Q
个查询点要在KNN中进行测试。这将导致一个
k x M x Q
矩阵,在每个示例或每个切片中,我们返回具有
M
维度的
k
个最接近的点。或者,我们可以返回
k
个最接近点的
ID,从而得到一个
Q x k
矩阵。让我们计算两者。
一种天真的方法是在循环中应用上述代码并循环遍历每个示例。
像这样的东西可以工作,我们分配一个
Q x k
矩阵,并应用基于
bsxfun
的方法来将输出矩阵的每一行设置为数据集中
k
个最接近的点,其中我们将使用与之前相同的Fisher Iris数据集。我们还将保持与之前示例中相同的维度,并使用四个示例,因此
Q = 4
和
M = 2
:
load fisheriris;
x = meas(:,3:4);
newpoints = [5 1.45; 7 2; 4 2.5; 2 3.5];
Q = size(newpoints, 1);
M = size(x, 2);
k = 10;
x_closest = zeros(k, M, Q);
ind_closest = zeros(Q, k);
for ii = 1 : Q
newpoint = newpoints(ii, :);
dists = sqrt(sum(bsxfun(@minus, x, newpoint).^2, 2));
[d,ind] = sort(dists);
ind_closest(ii, :) = ind(1 : k).';
x_closest(:, :, ii) = x(ind_closest(ii, :), :);
end
尽管这很好,但我们可以做得更好。有一种方法可以高效地计算两组向量之间的平方欧几里得距离。如果您想使用曼哈顿距离,请参考
此博客。假设
A
是一个
Q1 x M
矩阵,其中每一行都是一个维度为
M
的点,具有
Q1
个点,并且
B
是一个
Q2 x M
矩阵,其中每一行也是一个维度为
M
的点,具有
Q2
个点,我们可以使用以下矩阵公式高效地计算距离矩阵
D(i, j)
,其中第
i行和第j列的元素表示
A
的第
i行和
B
的第j行之间的距离:
nA = sum(A.^2, 2);
nB = sum(B.^2, 2);
D = bsxfun(@plus, nA, nB.') - 2*A*B.';
D = sqrt(D);
因此,如果我们让
A
成为查询点的矩阵,
B
成为由原始数据组成的数据集,我们可以通过逐行排序并确定每行中最小的
k
个位置来确定
k
个最近的点。我们还可以使用这种方法检索实际的数据点。
load fisheriris;
x = meas(:,3:4);
newpoints = [5 1.45; 7 2; 4 2.5; 2 3.5];
k = 10;
Q = size(newpoints, 1);
M = size(x, 2);
nA = sum(newpoints.^2, 2);
nB = sum(x.^2, 2);
D = bsxfun(@plus, nA, nB.') - 2*newpoints*x.';
D = sqrt(D);
[d, ind] = sort(D, 2);
ind_closest = ind(:, 1:k);
x_closest = permute(reshape(x(ind_closest(:), :).', M, k, []), [2 1 3]);
我们看到,用于计算距离矩阵的逻辑相同,但一些变量已更改以适应示例。我们还使用两个输入版本的
sort
对每行进行独立排序,因此
ind
将包含每行的ID,
d
将包含相应的距离。然后,我们通过简单地将此矩阵截断为
k
列来确定哪些索引最接近每个查询点。然后,我们使用
permute
和
reshape
确定相关联的最接近点是什么。我们首先使用所有最接近的索引,并创建一个点矩阵,将所有ID堆叠在一起,以便我们获得一个
Q * k x M
矩阵。使用
reshape
和
permute
允许我们创建我们的3D矩阵,使其成为我们指定的
k x M x Q
矩阵。如果您想获取实际距离本身,我们可以索引
d
并获取所需内容。要做到这一点,您需要使用
sub2ind
获取线性索引,以便我们可以一次性地索引到
d
中。
ind_closest
的值已经给出了我们需要访问哪些列。我们需要访问的行只是1,
k
次,2,
k
次,等等,直到
Q
。
k
是我们想要返回的点数:
row_indices = repmat((1:Q).', 1, k);
linear_ind = sub2ind(size(d), row_indices, ind_closest);
dist_sorted = D(linear_ind);
当我们运行上述代码来处理以上查询点时,我们获得的是以下索引、点和距离:
>> ind_closest
ind_closest =
120 134 53 73 84 77 78 51 64 87
123 119 118 106 132 108 131 136 126 110
107 62 86 122 71 127 139 115 60 52
99 65 58 94 60 61 80 44 54 72
>> x_closest
x_closest(:,:,1) =
5.0000 1.5000
6.7000 2.0000
4.5000 1.7000
3.0000 1.1000
5.1000 1.5000
6.9000 2.3000
4.2000 1.5000
3.6000 1.3000
4.9000 1.5000
6.7000 2.2000
x_closest(:,:,2) =
4.5000 1.6000
3.3000 1.0000
4.9000 1.5000
6.6000 2.1000
4.9000 2.0000
3.3000 1.0000
5.1000 1.6000
6.4000 2.0000
4.8000 1.8000
3.9000 1.4000
x_closest(:,:,3) =
4.8000 1.4000
6.3000 1.8000
4.8000 1.8000
3.5000 1.0000
5.0000 1.7000
6.1000 1.9000
4.8000 1.8000
3.5000 1.0000
4.7000 1.4000
6.1000 2.3000
x_closest(:,:,4) =
5.1000 2.4000
1.6000 0.6000
4.7000 1.4000
6.0000 1.8000
3.9000 1.4000
4.0000 1.3000
4.7000 1.5000
6.1000 2.5000
4.5000 1.5000
4.0000 1.3000
>> dist_sorted
dist_sorted =
0.0500 0.1118 0.1118 0.1118 0.1803 0.2062 0.2500 0.3041 0.3041 0.3041
0.3000 0.3162 0.3606 0.4123 0.6000 0.7280 0.9055 0.9487 1.0198 1.0296
0.9434 1.0198 1.0296 1.0296 1.0630 1.0630 1.0630 1.1045 1.1045 1.1180
2.6000 2.7203 2.8178 2.8178 2.8320 2.9155 2.9155 2.9275 2.9732 2.9732
与使用
knnsearch
进行比较,您需要指定一个点矩阵作为第二个参数,其中每一行都是查询点,并且您会发现此实现和
knnsearch
之间的索引和排序距离匹配。
希望这能帮到你。祝好运!