海量高维向量相似度快速搜索(第七届软件杯原创算法)

  放在全文开头,这次去南京的感受。

  这一届软件杯组委会安排的酒店很舒适,志愿者小姐姐偏多很养眼,很热心,咨询的时候讲解也很周到。早中晚三餐都很丰盛,尤其是早餐很精致。南京的景点也很多,这么多天都还没有把南京玩个遍。下面是微信群里大家感谢组委会的截图。

 


  然后进入正题:

  先简单描述一下问题:

假设如下数据集D里有N个1024维的向量, N=1百万,向量中各个维度上的数据都是32位浮点数:

ID

 Vector

1

a_1,a_2, .....,a_1_0_2_4

2

b_1,b_2, ....,b_1_0_2_4

....

.........

N

n_1, , ....,n_1_0_2_4

定义向量x与向量y之间的距离为欧几里得距离,即:

d(x,y)=\sqrt{\sum_{i=1}^{1024}(x_i-y_i)^2}

定义向量x在数据集D上的最近邻为D中与x距离最小的向量,即

NearestNeighbor(x) = arg \begin{matrix} min \\ y \in D \end{matrix} d(x,y)

现有M=100个1024维向量{M_1,  M_2, ...,M_1_0_0}, 请设计程序在数据集D中分别找到其最近邻对应的ID。

 在保证一定正确率的前提下,搜索速度越快越好。

题意分析:在这个问题中最大的难点就在于1024维的维度太高了,我们在网上查到的N多种算法试用后的结果都不理想(具体原因是可以了解一个“维度灾难”的词)。然后,还有一个问题这是在和出题人交流后才知道的。原来数据时完全随机的数据,这就很麻烦。在经过聚类试验下基本上都会聚类到等分点线上,聚不聚都一样。

维度灾难:对维度灾难最简单容易的理解就是由高维度造成的灾难(好像跟没说一样......)。具体原因如下:在高维空间中随机分布的每个点的每一维度上的值都是一定范围内的随机值(以这个问题为例就是0~1之间的随机浮点数)。在求随机两点之间的距离时每一维度上的差值也会在这一范围内,唯一的差别就是分布情况可能会有所不同(以这个题为例就是每一维度上的差值都会是0~1之间的随机浮点数)。在平方又开方之后两点间的距离会渐渐趋于一个常数。换而言之就是随机两点之间的距离极为接近。就是由于这个原因导致很多在低维空间下及其有效的算法失效。

  在比赛刚开始数据规模还是1千万的时候,我们指导老师还说:“这个问题肯定有人能做到1s以内。咋们能得奖与否就看我们的对手有没有牛人。”于是,在数据量在100万的时候我们就一直以100ms为目标。结果到最后都没做到。

接下来先说算法思路:

算法分为两部分预处理数据压缩部分和最近邻查找部分。

数据压缩部分:

  需要数据压缩原因是:当数据规模为1千万时(40G),运行内存仅有16G。所以数据无法一次性载入内存中,如果从硬盘再次读取数据又会消耗大量的时间。所以,我们必须将1千万的数据压缩至14G以下(空出2G是为了不影响操作系统的运行。)。

  这一部分很好理解,常见的数据压缩方式我也就不多介绍。直接说我们使用的压缩方法。原始数据使用的是0~1之间的float型浮点数(4个字节),而我们将它量化256级。也就成了0~256之间的整数。而这一数值正好可以由unsigned char来表示(1个字节),这么做就正好把原数据压缩至四分之一便于一次性载入内存(过几天之后,出题老师就把数据规模降低到了1百万。)。不过,我们这么做还是在最终的程序中加快了最近邻的查找速度。

  到这里也许有人会疑惑,这么做数据不就改变了吗?难道不会影响距离大小的计算吗?

  答案是会影响距离大小的计算,但是,对距离大小的比较没什么影响。在每一个数值乘以一个常数后公式如下:

  d(x,y) = \sqrt{\sum_{i=1}^{1024}(kx_i - ky_i)^2}

  经过转化后公式如下:

  d(x,y) = k^2\sqrt{\sum_{i=1}^{1024}(x_i - y_i)^2}

  所以,在最近邻查找比较距离大小时都是在乘以256这一常数,理论上不会影响相对大小的比较。但是,毕竟是从浮点数转化成正数。所以,必然存在一个量化误差。例如:0.2354678乘以256后为60.0493568,最终存储为60。这个0.2797568就是量化误差。这一部分误差并不大。在这个问题中由量化造成的最近邻查找错误仅有2%(如果要快速查找,正确率的牺牲在所难免。)。解决方案是通过量化后的数据查找最近邻和次最近邻。然后,仅仅从硬盘中读取最近邻和次最近邻来进行精确计算消除量化误差。

最近邻查找部分:

  所有点开这篇博客的朋友应该都是为了看这一段,在大家看完后可能会感到失望。

  在我们经过众多尝试后发现很多算法都无法到达我们想要的效果,即:查找速度100ms以下,正确率90%以上(虽然,最后也没有达到这一目标)。于是我们就探索出了下面这个算法。我们将原始数据(包括查询集)每一维度进行2聚类,转化成0或1(在完全随机数据中就是简单的四舍五入到个位即可)。

  在经过这一处理后的数据集和查询集,我们都可以得到一个长1024的01串作为这一数据的汉明指纹。我们基于每一条查询数据和询问数据的韩明指纹做汉明距离的计算。于是,当这一汉明距离小于某一阈值时我们便可以判断出这条数据是否有可能成为查询数据的最近邻(我们选择的阈值是481)。最后,我们只需要对有可能成为最近邻的点进行欧氏距离的计算比较大小即可。

  欧氏距离相对大小比较优化,标准的欧氏距离公式如下:

  d(x,y) = \sqrt{\sum_{i=1}^{1024}(x_i - y_i)^2}

  展开后公式如下: 

  d(x,y) = \sqrt{\sum_{i=1}^{1024}x_i^2 - 2\times \sum_{i=1}^{1024}x_iy_i + \sum_{i=1}^{1024}{y_i^2}}

  对于这一公式由于我们不需要计算精确值,所以不用开根号。对于一条固定的查询集而且\sum_{i=1}^{1024}{y_i^2}是一个常数,而\sum_{i=1}^{1024}x_i^2可以在预处理阶段完成(预处理阶段不计时间)。最后我们只需要计算- 2\times \sum_{i=1}^{1024}x_iy_i即可。这一部分就是向量内积计算。

  最后,算法的复杂度为O(K*N*M*W)。N是数据集规模,M是查询集规模,W是数据维度,K是一个远小于1的常数。K这一常数具体有多小就看每个人使用的优化手段了。

  算法部分到这就算是完了,我想是不是很容易很好理解。相信大家都发现了,计算汉明距离就可以有很多种优化方式这里也就不再赘述,比较常见的应该是在异或后进行位1计数。位1计数优化参考下面这个链接:各种位1计数各种对比对比。我们采用的就是SSE/AVX2指令集。在计算向量内积时使用的就是MKL矩阵计算库。由于每条数据之间并没有依赖性。所以还可以使用多线程。

  还有一个迷之优化,说出来我自己都不信。我们仅仅把编译方式从VS2013编译换成了QT使用MinGW编译竟然就直接从312ms加速到了265ms。还有理论上在linux操作系统上还能在快一点,但是我们在实际测试中发现并没有效果。

  好了,到此为止了。主要还是做一下简单记录吧。

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 13
    评论
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值