介绍
关于本出版物
你好,NEC数字技术开发实验室我是数据分析加速组的 Kodera。
我通常做研究以加速机器学习。
快速准确的 k 近邻 (k-NN) 计算我的研究已经接近尾声,所以我想分享一下内容。谢谢你。
k-NN是从数据库中选择最接近查询数据的k个数据的算法,是用于回归和类分类的基本算法。
k-NN 的简单实现需要较少的时间来创建模型,但是使用这种实现的 k-NN 进行分类和回归存在计算成本高的问题,因为它涉及到整个数据库数据。
因此,在本出版物中,虽然输入数据被限制在最多 6 维的低维,但我们将介绍一种通过使用 Z 曲线来降低 k-NN 计算成本的方法。 (基本思想见[1])
Z 曲线
Z曲线是一种以任意精度填充多维空间的空间填充曲线,可以将多维数据映射到一维空间。
特别地,当使用Z曲线将数据从多维空间映射到一维空间时,可以在保持多维空间上数据之间的局部位置关系信息的同时进行映射。
如图所示,多维空间邻域的数据也映射到一维空间的邻域。
此外,将多维数据映射到一维数据时的一维值称为 Z 值。多维空间中的附近数据具有相似的 Z 值。
作为 Z 曲线的一个重要性质,如果我们在多维空间的一个矩形中连接起点(Z 值最小的矩形的顶点)和终点(Z 值最大的矩形的顶点), Z 曲线通过矩形内的所有点。
下面介绍 Z 值以及如何计算它们。
Z值
Z值是以二进制表示的多维数据的交错值的十进制表示。
例如,假设多维数据为 (23245, 35159)。 (此时的多维值可以用负数或浮点数计算,但为简单起见,使用非负整数。)
这种多维数据的二进制表示法是
(0101101011001101, 0101101011001101)
变成。
接下来,如果从最高位依次交替混合这些二进制值,则第一维数(0101101011001101)的最高位的个数为0,
第 2 维数(1000100101010111)的最大位数为 1,第 1 维数的第 2 位数为 1,
由于第二维数的第二大位的数为0,所以数以0、1、1、0等连续,并将这些连接起来
01100010110010011011000110110111
您将获得一个 32 位数字。如果将此数字转换为十进制表示法,则可以得到 Z 值。
目前的程序可以总结如下。
- 多维数据的二进制表示。
- 对于以二进制表示法表示的多维数据,交替每个数字的编号。
- 以十进制表示法表示具有交替数字的数字。
也可以通过这种类似字符串操作的方法获取Z值,但是在处理大量数据时,这种方法比较耗时,不推荐使用。因此,它通过位操作来加速。
(*交替混合数字时,数字从第 1 期开始混合,但您可以从第 2 期开始混合。)
(*当从多维数据计算 Z 值时,将 Z 值表示为多维向量,可以在不为多维数据创建上限的情况下进行计算。例如,从 16 位二维数据计算 Z 值 创建时,Z值表示为最大 32 位数字。通过将 Z 值表示为 16 位二维向量,实际上可以表示 32 位数字。)下面,我们描述Z值的计算(位操作的图像)以及在C++中的具体实现。该实现假定 uint64_t 作为多维数据的输入。使用 reinterpret_cast 也可以扩展负数和浮点数。
Z 值计算
为简单起见,使用 16 位计算进行说明。
多维数据的二进制表示法 (23245,35159)。
(0101101011001101, 0101101011001101) ag{0.0}最终的一维 Z 值(二进制符号)表示为公式的多维向量。
(0110001011001001, 1011000110110111) ag{Z}表示为
下面,第一维的数字0110001011001001 ag{Z_1}目标是计算
定义 mask_bit 作为准备。
egin{align} m{mask}_1=(0000111100001111, 0000111100001111)\ m{mask}_2=(0011001100110011, 0011001100110011)\ m{mask}_3=(0101010101010101, 0101010101010101)\ end{align}将二进制表示的多维数 (0.0) 右移 8 位。
egin{align} 0000000001011010, 0000000001011010 ag{1.0} end{align}现在我们准备计算(Z_1)。
首先,将 (1.0) 向左移动 4 位。
egin{align} 0000010110100000, 0000010110100000 ag{1.1} end{align}接下来,取 (1.0) 和 (1.1) 的逻辑和。
egin{align} 0000010111111010, 0000100010011001 ag{1.2} end{align}然后取 (1.2) 和 mask_1 的逻辑乘积。
egin{align} 0000010100001010, 0000100000001001 ag{2.0} end{align}重复同样的操作。
(2.0) 左移 2 位。egin{align} 0001010000101000, 0010000000100100 ag{2.1} end{align}取 (2.0) 和 (2.1) 的逻辑和。
egin{align} 0001010100101010, 0010100000101101 ag{2.2} end{align}(2.2) 和 mask_bit2 的逻辑乘积。
egin{align} 0001000100100010, 0010000000100001 ag{3.0} end{align}大约 (3.0) 左移 1 位。
egin{align} 0010001001000100, 0100000001000010 ag{3.1} end{align}取 (3.0) 和 (3.1) 的逻辑和。
egin{align} 0011001101100110, 0110000001100011 ag{3.2} end{align}(3.2) 和 mask_bit3 的逻辑乘积。
egin{align} 0001000101000100, 0100000001000001 ag{4} end{align}最后,对于 (4),将第一个数字向左移动 1 位。
egin{align} 0010001010001000, 0100000001000001 ag{5} end{align}将 (5) 的两个数字进行或运算得到所需的
egin{align} 0110001011001001 end{align}要得到
(Z)的第二维数
1011000110110111 ag{Z_2}计算时提取最后8位数字
将 (0.0) 向左移动 8 位,然后向左移动 16 位。
之后,如果我们执行与 (Z_1) 相同的计算,我们得到 (Z_2)。对于 32 位、64 位计算或 3 或更大的多维数的计算,需要更复杂的计算处理,但可以使用与上述相同的过程进行计算。
执行
计算计算 Z 值的实现。
在此代码中,Z 值的计算最多支持 6 维数据。可以计算任意维度的数据的Z值,但是由于多维空间中的距离信息随着维度的增加而变得毫无意义,所以这个实现最多只能携带6个维度。Z 值计算
Z_value.cppstd::vector<uint64_t> Z_value(std::vector<uint64_t> & input_vector, int& input_vector_num, int& input_vector_dim) { int init_data_shift = 64 / input_vector_dim; int morton_dim = input_vector_dim + (64 % input_vector_dim != 0); std::vector<uint64_t> morton_vec(input_vector_num * morton_dim); std::vector<uint64_t> origin_input_vector(input_vector_num * input_vector_dim); std::vector<uint64_t> Mask_bit = Mask_Table(input_vector_dim); int left_shift_init = std::pow(2,Mask_bit.size()-1)* (input_vector_dim - 1); int left_shift, inv_morton_dim; for (int md = 0; md < morton_dim; md++) { for (int nd = 0; nd < input_vector_num*input_vector_dim; nd++) { origin_input_vector[nd] = ( input_vector[nd] >> init_data_shift * md); } left_shift = left_shift_init; for (int m = 0; m < Mask_bit.size(); m++) { for (int nd = 0; nd < input_vector_num*input_vector_dim; nd++) { origin_input_vector[nd] = (origin_input_vector[nd] |( origin_input_vector[nd] << left_shift)) & Mask_bit[m]; } left_shift /= 2; } inv_morton_dim = morton_dim - 1 -md; for (int d = 0; d < input_vector_dim; d++) { for (int n = 0; n < input_vector_num; n++) { morton_vec[n * morton_dim + inv_morton_dim] |= (origin_input_vector[n * input_vector_dim + d] << (input_vector_dim -1 -d)); } } } return morton_vec; }mask_bit 的计算
掩码表std::vector<uint64_t> Mask_Table(int Dim) { int msk_size = 0; if(Dim==2){ msk_size =6; } if(Dim==3){ msk_size =6; } if(Dim==4){ msk_size =5; } if(Dim==5){ msk_size =5; } if(Dim==6){ msk_size =5; } std::vector<uint64_t> Mask_bit(msk_size); if (Dim == 2) { Mask_bit = { 0xFFFFFFFF, 0xFFFF0000FFFF, 0xFF00FF00FF00FF, 0xF0F0F0F0F0F0F0F, 0x3333333333333333, 0x5555555555555555}; } else if (Dim == 3) { Mask_bit = { 0x1FFFFF, 0x1F00000000FFFF, 0x1F0000FF0000FF, 0x100F00F00F00F00F, 0x10C30C30C30C30C3, 0x1249249249249249}; } else if (Dim == 4) { Mask_bit = { 0xFFFF, 0xFF000000FF, 0xF000F000F000F, 0x303030303030303, 0x1111111111111111}; } else if (Dim == 5) { Mask_bit = { 0xFFF, 0xF00000000FF, 0xF0000F0000F, 0xC0300C0300C03, 0x84210842108421}; } else if (Dim == 6) { Mask_bit = { 0x3FF, 0x30000000000FF, 0x300000F00000F, 0x3003003003003, 0x41041041041041}; } else { exit(1); } return Mask_bit; }既然已经介绍了Z值的计算方法,下面我们将解释k-NN的计算。
k-NN的计算
k-NN 通过简单计算
首先,我们介绍通过非快速方法(朴素方法)计算k-NN。这可以通过计算所有训练数据和所有查询数据之间的距离,然后选择 k 个最近邻来计算。
如果训练数据和查询数据的数量足够少,这种方法可以快速计算,但是当数据数量很大时,计算成本会变得巨大。
(图中的例子中,1个查询数据有7个训练数据,以1×7进行7次距离计算,但是如果有100万个查询数据点和100万个训练数据点,计算量1,000,000 × 1,000,000 距离计算的成本是巨大的。)使用 Z 曲线计算 k-NN
接下来,我将介绍使用 Z-curve 计算 k-NN。
作为 Z 曲线的属性,
“当使用 Z 曲线将数据从多维空间映射到一维空间时,
可以在维护多维空间中数据之间的局部位置关系信息的同时进行映射。”
我介绍了以下内容。此属性用于加快计算速度。
为了加快这个过程,粗略地选择附近的点,计算查询数据P和训练之间的距离,最后搜索k个邻居点。然后,通过计算确定这个粗略的邻域点。 (近点对人眼来说是明显的,但对计算机来说不一定。另外,图中是一个二维的例子,但是当涉及到四维和五维时,人眼将很难找到一个松散的点在 。)
现在,我们来看看具体的计算过程。
使用Z-curve进行k-NN计算的处理大致分为两种。
它们是预处理计算和查询处理计算。在预处理计算中,将训练数据转换为 Z 值,并按 Z 值排序。而这个排序后的训练数据在本文中也表示为 Z 曲线上的训练数据。预处理
- 计算训练数据的 Z 分数。
- 按 Z 值对训练数据进行排序。
- 由于Z值是在多维数组中给出的,所以排序是通过基数排序来进行的。
接下来,我将介绍查询处理计算。
查询处理
-
计算查询数据 P 的 Z 值。
-
将查询数据 P(z 值)放在已排序的训练数据(z 值)中。
- 比较排序后的训练数据的Z值和预处理计算的查询数据的Z值,根据排序后的训练数据的顺序确定查询数据的位置。这可以通过二分搜索快速计算。
-
选择查询数据前后k个点的训练数据(Z值)。
-
计算所选训练数据和查询数据在原始多维空间中的距离。
- 计算图中浅蓝色包围的训练数据(2k个)与查询数据P的距离。
-
计算出的距离中第 k 个最小的距离称为临时邻域球半径 R。
-
考虑一个超球面,其中心是查询数据,半径是 R。
-
考虑包含这个超球面的最小矩形。在这个矩形的顶点中,最小的点和Z值最大的点称为起点和终点。
- 如果查询数据P的坐标为(X,Y),则起点坐标为(X-R,Y-R),终点坐标为(X+R,Y+R)。
-
计算起点和终点的 Z 值。
-
计算 Z 曲线上起点和终点之间所有训练数据和查询数据 P 之间的距离。
- 此时选择的训练数据总是包含查询数据P的真正k近邻。
-
可以通过从计算的距离中找到 k 个最近邻来计算精确的 k-NN。
由于篇幅较长,我不会在本文中介绍具体实现,但如果您有兴趣,请尝试一下。
为什么我们可以计算精确的 k-NN?
在这里,我们描述了可以计算精确 k-NN 的原因。在第三步查询处理执行的操作中,计算查询数据与2k个训练数据的距离。第四步,从中计算出第k个距离,此时计算的第k个训练数据与查询数据之间的距离大于查询数据的确切k个邻居之间的距离。因此,由于步骤 5 中的超球面总是包含真正的 k 最近邻,我们通过扫描包含超球面的矩形来找到 k 最近邻(因为 Z 曲线穿过矩形内的所有点)。
评估
在评估中,我们正在计算 k=3 时的 k-NN 图。对于使用训练数据作为查询数据的输入数据集中的每个数据,计算三个最接近的数据。 (其中一个是数据本身,当时距离为0)
评估是使用人工数据计算的。使用的数据条件为数据数为100万,数据的维数为2到6。数据范围为[0,10000],使用统一随机数生成数据。
此外,用于计算的核心数量是 x86 的 12 个核心和 VE 的 8 个核心。
(VE:矢量引擎)
首先,我们将它与开头介绍的简单计算方法(蛮力)进行比较。
在此计算中,VE 用于蛮力和建议的方法。2 3 4 5 6 蛮力(VE) 631.01[秒] 648.63[秒] 630.25[秒] 618.06[秒] 630.75[秒] 建议的方法(VE) 1.24[秒] 1.43[秒] 1.84[秒] 3.05[秒] 6.72[秒] 与蛮力法相比,该方法的计算速度可提高约 100 到 600 倍。
接下来,让我们将其与流行的 Python 机器学习库 scikit-learn 中的 k-NN 进行比较。
scikit-learn 的 k-NN 也有加速选项,scikit-learn 可以使用 kd-trees 来降低计算成本。
计算结果如下。2 3 4 5 6 scikit-learn(x86) 3.37[秒] 5.91[秒] 13.22[秒] 21.89[秒] 44.77[秒] 建议的方法(x86) 1.33[秒] 2.45[秒] 4.16[秒] 9.50[秒] 23.8[秒] 建议的方法(VE) 1.24[秒] 1.43[秒] 1.84[秒] 3.05[秒] 6.72[秒] 所提出的方法 (x86) 可以比 scikit-learn (x86) 快 2 到 3 倍执行计算。
此外,提出的方法(VE)在二维上比 scikit-learn(x86)快大约 3 倍,但是计算时间的差异随着维度的增加而增加,在六维的情况下,计算时间是 7 倍更快。计算速度现在是原来的两倍。
随着维度的增加,VE擅长的计算次数增加(向量长度增加),所以速度通过scikit-learn比来提高。综上所述
在这篇文章中,我解释了 Z-curve 的计算以及使用 Z-curve 对 k-NN 的加速。
虽然仅限于低维,但我希望您已经了解使用 Z 曲线的 k-NN 可以高速计算。
通过 Z 曲线保留位置信息的映射功能强大,但 Z 值计算本身很复杂(尤其是在 3 维或更多维中)。由于日语中关于如何计算 Z 值的详细说明很少,所以写这篇文章的目的是帮助读者逐步理解计算。
如果有任何错别字或不正确的描述,请告诉我。此外,在评测中发布的计算中,通过进一步减少作为解决方案候选点的训练数据,计算速度更快,但内容相当复杂,偏离了故事的重点,所以我将这篇文章不解释了,我就省略了。基本思路见参考文献[2],有兴趣的请阅读。
参考
- Michael Connor, Piyush Kumar:快速构建点云的 k-最近邻图,IEEE Transactions on Visualization and Computer Graphics,16:599-608,2010。
- Duncan Bates:COTS 嵌入式数据库解决动态兴趣点,A Raima Inc. 技术白皮书,2008 年 9 月。
原创声明:本文系作者授权爱码网发表,未经许可,不得转载;
原文地址:https://www.likecs.com/show-308629448.html