SPL实践:高维二值向量查找
A | |
1 | =directory@p("raw_data") |
2 | =A1.(file(~).read@b()) |
3 | =A2.(blob(~)) |
4 | =A3.new(filename@n(A1(#)):name,~.bits():data) |
5 | =file("longdata.btx").export@b(A4) |
A3:将每份数据转成长度为 512 的二值序列,形式如下:
A4:创建序表,name 字段是向量的标识,即文件名,data 是 512 维二值序列转成的由 8 个 long 型整数构成的序列,形式如下:
下面是第一行的 data 序列:
A | ||
1 | =512.(rand(2)) | /二值向量 1 |
2 | =512.(rand(2)) | /二值向量 2 |
3 | =A1.bits() | /long型数据序列 1 |
4 | =A2.bits() | /long型数据序列 2 |
5 | =bit1(A3,A4) | /异或距离 |
异或距离还有 1 个好处,相较于 cos 相似度,它只有位运算,没有除法运算,可以更快速地算出结果。
ANN算法通常分为两部分:
聚类,即把数据按照某种规则分到不同的子集,使这些子集内的点距离较近,同时使两个子集内的点距离较远,通俗点说就是把扎堆的数据分成一组, 同时使各堆尽量远。
查询,查询目标向量的最相似向量时,只要遍历距目标向量最近的部分子集即可,不必遍历全部数据,这样做有时可能会找不到最近的成员,但大概率能找到,这是权衡准确率和查找效率的结果。
我们决定自行研发聚类方法,大概过程如下:
尝试将数据拆分成N个子集,每个子集的成员数阈值是M。
所有数据是集合X,随机选 1 个向量作为第一个基向量x1,此时的集合也可以称作第一个子集X1。
从X1中找到和基向量距离最远的向量,把它作为第二个子集X2的基向量x2。
当有i个子集时,计算所有向量到每个基向量xi的距离,距离哪个基向量最近就将该向量划分到哪个子集。
第i+1 个子集Xi+1的基向量xi+1是从已有的i个子集中找到成员数超过M且半径最大的子集中选择与其基向量最远的向量作为第i+1 个基向量xi+1。(子集的半径定义为子集中所有成员到其基向量的最远距离)
重复第 3、4 步,直到所有子集成员数都小于M或子集数达到N。
完成聚类后,目标向量x’的比对过程如下:
计算x’与所有基向量xi的距离,并按距离排序,取前K个最近子集。
遍历K个子集的成员,计算他们与x’的距离,找到最近距离成员。
A | B | C | |
1 | longdata.btx | ||
2 | =s_no=600 | ||
3 | =sub_no_thd=30000 | ||
4 | =rad_thd=100 | ||
5 | =file(A1).import@b() | ||
6 | =X=A5.run(data=data.i()).i() | ||
7 | =G=create(base,num,rad) | ||
8 | =x1=X.maxp(data.sum(bit1(~))).data | ||
9 | =X.len() | ||
10 | =A8.len()*64 | ||
11 | =G.record([A8,A9,A10]) | ||
12 | =X.derive@o(1:grp,bit1(data,x1):dis,:new_dis,:flag) | ||
13 | for | ||
14 | =G.count(rad==0) | ||
15 | =G.max(num) | ||
16 | =G.max(rad) | ||
17 | if G.len()>=sub_no_thd||B15<=s_no||B16<=rad_thd | break | |
18 | =x1=X.groups@m(;maxp(if(G(grp).num>s_no||G(grp).rad>rad_thd, dis+rand(), 0)):mx).mx.data | ||
19 | =G.record([x1]) | ||
20 | =G.len() | ||
21 | =X.run@m(bit1(data,x1):new_dis,new_dis-dis<2*rand()-1:flag,if(flag,new_dis,dis):dis,if(flag,B20,grp):grp) | ||
22 | =X.groups@nm(grp;count(1):cnt,max(dis):rad) | ||
23 | =G.run@nm(B22(#).cnt:num,B22(#).rad:rad) | ||
24 | =X.new(A5(#).name:name,data,grp,dis).o() | ||
25 | =A24.group(grp) | ||
26 | =A25.align@a([false,true],~.len()==1) | ||
27 | =A26(2).conj().new(name,data,int(grp):grp) | ||
28 | =A27.len() | ||
29 | =A26(1).conj().group([grp,dis]).conj().new(name,data,int(grp):grp) | ||
30 | =file("indexes_1.btx").export@b(A27) | ||
31 | =file("indexes_m.btx").export@b(A29) |
A6、A7:设置两个变量 X 和 G,分别存储每个向量的信息和聚类子集的信息。
A | B | |
1 | =file("target_vec.btx").import@b().run(data=data.i()).i() | |
2 | =sub_no=512 | |
3 | =X1=file("indexes_1.btx").import@b().run(data=data.i()).i() | |
4 | =X=file("indexes_m.btx").import@b().run(data=data.i()).i() | |
5 | =XG=A4.group@v0(grp).(~.run(data=data.i()).i()) | |
6 | =A5.(~(1)).run(data=data.i()).i() | |
7 | =(A3|A4).i() | |
8 | =ath_data_num=0 | |
9 | =ath_time=0 | |
10 | =all_time=0 | |
11 | =miss_bit=0 | |
12 | for A1 | =xx=A12.data.i() |
13 | =now() | |
14 | =A6.ptop(sub_no;bit1(xx,data)) | |
15 | =XG(B14) | |
16 | =B15.conj@v() | |
17 | =B16|X1 | |
18 | =B17.minp(bit1(xx,data)) | |
19 | =interval@ms(B13,now()) | |
20 | =now() | |
21 | =A7.minp(bit1(data,xx)) | |
22 | =interval@ms(B20,now()) | |
23 | =B18.(bit1(data,xx)) | |
24 | =B21.(bit1(data,xx)) | |
25 | =ath_data_num+=B17.len()+A6.len() | |
26 | =ath_time+=B19 | |
27 | =all_time+=B22 | |
28 | =if(B23!=B24,1,0) | |
29 | =miss_bit+=B28 | |
30 | =A1.len() | |
31 | =ath_data_num/A30 | |
32 | =ath_time/A30 | |
33 | =all_time/A30 | |
34 | =miss_bit/A30 | |
35 | =file("target_vec.btx").import@b().run(data=data.i()).i() | |
36 | =sub_no=512 |
A1:读入目标向量数据;
B21:全遍历最近距离成员。
变量 ath_data_num、ath_time、all_time、miss_hit 分别统计算法遍历数据量、算法耗时、全遍历耗时和算法最近距离不等于全遍历最近距离的次数。
A33-A36是上述统计量的平均值。
我们把算法找出的最近距离不等于全遍历计算出来最近距离的次数占全部目标向量数的比例(A36 的结果)叫做击不中率,作为算法有效性的考虑指标,显然击不中率越低说明这个算法有效性更高。
下面是 1 万个目标向量在聚类输出文件 "indexes_1.btx" 和 "indexes_m.btx" 上的统计结果:
数据量 | 遍历子集数 | 算法遍历数据量 | 算法耗时 | 全遍历耗时 | 击不中率 |
90万 | 512 | 80683 | 2.02ms | 18.15ms | 5.68% |
1024 | 114384 | 3.92ms | 17.48ms | 2.46% |
数学分析表明,这个算法的理论复杂度和数据量的平方根成正比,数千万数据量的耗时不超过 100ms, 比如数据量扩大到 9000 万,相当于 90 万的 100 倍,算法耗时只扩大到原来的 10 倍,遍历 512 个子集时耗时在 20ms 左右,遍历 1024 个子集时耗时在 40ms 左右,而全遍历耗时会扩大 100 倍,需要 1800ms 左右。
有鉴于此,我们又设计了逐步聚类的方法,过程如下:
设子集成员数阈值为M
1. 第一个基向量是第一个向量,记为xi,第一个子集记为X1。
2. 遍历整个集合X,把X的成员加到最近的子集Xi中(距离相等时,随机加)。
3. 加到有Xi的成员数超过M时,对遍历过的数据拆分,使所有Xi成员数都小于等于M(拆分过程就是拆分式聚类过程)。
A | B | C | D | E | |
1 | longdata.btx | ||||
2 | =s_no=600 | ||||
3 | =file(A1).import@b() | ||||
4 | =to(A3.len()).psort(rand()) | ||||
5 | =X=A3(A4).new(data).run(data=data.i()).derive@o(:grp,:dis,:new_dis,:flag).i() | ||||
6 | =G=create(base,num,rad,max_rad_v) | ||||
7 | =x1=X.data | ||||
8 | =G.record([A7,0,0,null]) | ||||
9 | for X | =xid=#A9 | |||
10 | =sub_num_step=sqrt(xid*1024) | ||||
11 | =v0=A9.data | ||||
12 | =G.@m(bit1(base,v0)) | ||||
13 | =B12.pmin(~+rand()) | ||||
14 | =B12(B13) | ||||
15 | =G(B13).run(num=num+1,(dst=B14,flg=dst+rand()>rad+rand(),rad=if(flg,dst,rad)),max_rad_v=if(flg,v0,max_rad_v)) | ||||
16 | =A9.run(B13:grp,B14:dis) | ||||
17 | =G(B13).num | ||||
18 | =G.len() | ||||
19 | if B18>sub_num_step | next | |||
20 | if B17>s_no | =X(to(#A9)) | |||
21 | for | =G.len() | |||
22 | if D21>sub_num_step | break | |||
23 | =G.groups@m(;maxp(if(num<=s_no||rad==0,-1,rad+rand())):mx).mx | ||||
24 | if D23.num>s_no | =x1=D23.max_rad_v | |||
25 | else | break | |||
26 | =G.record([x1]) | ||||
27 | =C20.run@m(bit1(data,x1):new_dis,new_dis-dis<2*rand()-1:flag,if(flag,new_dis,dis):dis,if(flag,D21+1,grp):grp) | ||||
28 | =D27.groups@nm(grp;count(1):cnt,max(dis):rad,maxp(dis).data:max_rad_v) | ||||
29 | =G.run@m((rcd=D28(#),num=rcd.cnt,rad=rcd.rad,max_rad_v=rcd.max_rad_v)) | ||||
30 | =A3(A4).derive(X(#).grp:grp,X(#).dis,G(grp).rad:rad) | ||||
31 | =A30.group(grp) | ||||
32 | =A31.align@a([false,true],~.len()==1) | ||||
33 | =A32(2).conj().run(grp=int(grp)).derive(int(#):ID) | ||||
34 | =A33.len() | ||||
35 | =A32(1).conj().group([grp,dis]).conj().run(grp=int(grp)).derive(int(A34+#):ID) | ||||
36 | =file("indexes2_1.btx").export@b(A33) | ||||
37 | =file("indexes2_m.btx").export@b(A35) |
C21代码块:拆分式聚类过程。
查询过程和之前相同,这里不再赘述。
下面是同样数据逐步聚类的实验结果:
数据量 | 遍历子集数 | 算法遍历数据量 | 算法耗时 | 全遍历耗时 | 击不中率 |
90万 | 512 | 81212 | 2.33ms | 16.63ms | 2.43% |
1024 | 128311 | 4.37ms | 17.20ms | 0.82% |
效果比拆分式聚类更好一些。
交集方法
具体做法如下:
按原来的比对方法找到每次随机聚类的最近距离成员;
再从两个最近距离成员中选出距离更近的成员(相当于取两次结果的击不中成员的交集)。
A | B | C | |
1 | =file("target_vec.btx").import@b().run(data=data.i()).i() | ||
2 | =sub_no=1024 | ||
3 | indexes2_1_1.btx | ||
4 | indexes2_1_2.btx | ||
5 | indexes2_m_1.btx | ||
6 | indexes2_m_2.btx | ||
7 | =[A3:A6].(file(~).import@b(name,data,grp).run(data=data.i()).i()) | ||
8 | =A7.to(2) | ||
9 | =A7.to(3,) | ||
10 | =A7.to(3,).(~.group@v0(grp).(~.run(data=data.i()).i())) | ||
11 | =A10.(~.(~(1)).run(data=data.i()).i()) | ||
12 | =(A7(1)|A7(3)) | ||
13 | =ath_data_num=0 | ||
14 | =ath_time=0 | ||
15 | =all_time=0 | ||
16 | =miss_hit=0 | ||
17 | for A1 | =xx=A17.data.i() | |
18 | =s=0 | ||
19 | =now() | ||
20 | for 2 | ||
21 | =X1=A8(B20) | ||
22 | =X=A9(B20) | ||
23 | =XG=A10(B20) | ||
24 | =A11(B20) | ||
25 | =C24.ptop(sub_no;bit1(xx,data)) | ||
26 | =XG(C25) | ||
27 | =C26.conj@v() | ||
28 | =C27|X1 | ||
29 | =s+=C28.len()+C24.len() | ||
30 | =C28.minp(bit1(xx,data)) | ||
31 | =@|C30 | ||
32 | =C31.minp(bit1(xx,data)) | ||
33 | =interval@ms(B19,now()) | ||
34 | =C31=[] | ||
35 | =now() | ||
36 | =A12.minp(bit1(data,xx)) | ||
37 | =interval@ms(B35,now()) | ||
38 | =B32.(bit1(data,xx)) | ||
39 | =B36.(bit1(data,xx)) | ||
40 | =if(B38!=B39,1,0) | ||
41 | =ath_time+=B33 | ||
42 | =all_time+=B37 | ||
43 | =miss_hit+=B40 | ||
44 | =ath_data_num+=s | ||
45 | =A1.len() | ||
46 | =ath_time/A45 | ||
47 | =all_time/A45 | ||
48 | =miss_hit/A45 | ||
49 | =ath_data_num/A45 |
B20代码块:两次次聚类结果的比对过程。
遍历子集数 | 遍历数据量 | 算法耗时 | 全遍历耗时 | 击不中率 |
512 | 163771 | 6.16ms | 16.62ms | 0.18% |
1024 | 260452 | 9.34ms | 17.11ms | 0.06% |
维度排序
A | B | C | |
1 | =directory@p("raw_data") | ||
2 | =512*[0] | ||
3 | =A1.run(A2=A2++blob(file(~).read@b())) | ||
=A1.len() | |||
5 | =A2.psort(abs(~/A4-0.5)) | ||
6 | =func(A12,A1,A5) | ||
7 | =directory@p("x0_data") | ||
8 | =func(A12,A7,A5) | ||
9 | =file("longdata1.btx").export@b(A6) | ||
10 | =file("div_index1.btx").export@b(A5) | ||
11 | =file("target_vec1.btx").export@b(A8) | ||
12 | func | ||
13 | =A12.group((#-1)\100000) | ||
14 | =A12.len() | ||
15 | =create(name,data) | ||
16 | for B13 | =B16.(blob(file(~).read@b())(B12).bits()) | |
17 | =B16.(filename@n(~)) | ||
18 | =C17.(~|[C16(#)]).conj() | ||
19 | =B15.record(C18) | ||
20 | return B15 |
A3:统计各维度 1 的个数;
比对过程也差不多,有以下几点区别:
目标向量要按敏感度排序后再转换成 long 型数据,同样取前p个 long 型数据;
按原来的方式从前p个 long 型数据选出距离最近的topn个向量,因为只计算前p个 long 的距离,无法确定前p个 long 的最近距离成员一定是全维度最近距离成员,所以取前topn个向量,使其大概率包含全维度最近距离成员,结合交集方法,可以进一步增加这一概率;
计算这topn个向量的全维度距离,找到最近距离成员;
用交集方法降低击不中率。
A | B | C | |
1 | =pre_n_long=5 | ||
2 | =topn=50 | ||
3 | =sub_no=1024 | ||
4 | indexes3_1_1.btx | ||
5 | indexes3_1_2.btx | ||
6 | indexes3_m_1.btx | ||
7 | indexes3_m_2.btx | ||
8 | =[A4:A7].(file(~).import@b(name,data,grp).derive(data.to(pre_n_long):data1).run(data=data.i(),data1=data1.i()).i()) | ||
9 | =A8.to(2) | ||
10 | =A8.to(3,) | ||
11 | =A8.to(3,).(~.group@v0(grp).(~.run(data=data.i(),data1=data1.i()).i())) | ||
12 | =A11.(~.(~(1)).run(data=data.i(),data1=data1.i()).i()) | ||
13 | =(A8(1)|A8(3)) | ||
14 | =ath_data_num=0 | ||
15 | =ath_time=0 | ||
16 | =all_time=0 | ||
17 | =miss_hit=0 | ||
18 | =file("target_vec1.btx").import@b() | ||
19 | for A18 | =xx=A19.data.i() | |
20 | =xx.to(pre_n_long) | ||
21 | =s=0 | ||
22 | =now() | ||
23 | for 2 | ||
24 | =X1=A9(B23) | ||
25 | =X=A10(B23) | ||
26 | =XG=A11(B23) | ||
27 | =A12(B23) | ||
28 | =C27.ptop(sub_no;bit1(B20,data1)) | ||
29 | =XG(C28) | ||
30 | =C29.conj@v() | ||
31 | =C30|X1 | ||
32 | =s+=C31.len()+C27.len() | ||
33 | =C31.top(topn;bit1(B20,data1)) | ||
34 | =C33.minp(bit1(xx,data)) | ||
35 | =@|C34 | ||
36 | =C35.minp(bit1(xx,data)) | ||
37 | =interval@ms(B22,now()) | ||
38 | =C35=[] | ||
39 | =now() | ||
40 | =A13.minp(bit1(data,xx)) | ||
41 | =interval@ms(B39,now()) | ||
42 | =B36.(bit1(data,xx)) | ||
43 | =B40.(bit1(data,xx)) | ||
44 | =ath_time+=B37 | ||
45 | =all_time+=B41 | ||
46 | =if(B42!=B43,1,0) | ||
47 | =miss_hit+=B46 | ||
48 | =ath_data_num+=s | ||
49 | =A18.len() | ||
50 | =ath_time/A50 | ||
51 | =all_time/A50 | ||
52 | =miss_hit/A50 | ||
53 | =ath_data_num/A49 |
B23代码块:比对过程;
维度排序减少了横向计算量,聚类也提速不少。
下面是 90 万数据综合采用逐步聚类、交集方法、维度排序的比对结果:
数据量 | 遍历子集数 | 遍历数据量 | 算法耗时 | 全遍历耗时 | 击不中率 |
90万 | 512 | 159381 | 5.60ms | 17.46ms | 0.68% |
重磅!开源SPL交流群成立了
简单好用的SPL开源啦!
为了给感兴趣的技术人员提供一个相互交流的平台,
特地开通了交流群(群完全免费,不广告不卖课)
需要进群的朋友,可长按扫描下方二维码
本文感兴趣的朋友,请到阅读原文去收藏 ^_^