请选择 进入手机版 | 继续访问电脑版
搜索
查看: 577|回复: 1

基于稀疏矩阵的k近邻(KNN)如何实现?(附程序)

[复制链接]
发表于 2017-5-10 11:08 | 显示全部楼层 |阅读模式
一、概述
       这里我们先来看看当我们的数据是稀疏时,如何用稀疏矩阵的特性为KNN算法加速。KNN算法在之前的博文中有提到,当时写的测试程序是针对稠密矩阵数据的。但实际上我们也会遇到不少的稀疏数据,而且有很多是有意而为之的,因为稀疏数据具有稠密数据无法媲美的存储和计算特性,这对工程应用中的内存需求和实时需求是很重要的。所以这里我们也来关注下稀疏矩阵的存储和其在knn算法中的应用举例。
102.png
       大家都知道,所谓稀疏矩阵,就是很多元素为零的矩阵,或者说矩阵中非零元素的个数远远小于矩阵元素的总数,如上图。如果我们只存储这些少量的非零元素,就可以大大的降低这个矩阵的存储空间。例如一个1000x1000的矩阵,里面只有100个非零的元素。如果全部存储这个矩阵,那么需要1000x1000x4Byte=4MB的空间(假设矩阵元素是float型,占4个字节)。但如果只存储那100个非零元素,那只需要100x4Byte=0.4KB的空间。这强大的差别对对寸土寸金的内存来说是非常讨人喜欢的。哎哟,你雪亮的眼睛可以看出问题了,只占0.4KB的空间就可以描述这个稀疏矩阵了?矩阵每个元素不是具有位置意义的么?那不是还得存储每个元素在哪一行和哪一列么?嗯,没错,我们还需要提供辅助数组来描述非零元素在原矩阵中的位置,一个萝卜一个坑,得标记好。
       对于矩阵操作而言,不同的稀疏数据组织方式会有不同的特点,所以这就出现了多种不同的稀疏矩阵的存储格式,但也万变不离其宗,即存储矩阵所有的非零元素到一个线性数组中,并提供辅助数组来描述非零元素在原矩阵中的位置。例如著名的scipy库的稀疏矩阵模块就提供了以下几种存储方式:
       bsr_matrix:   Block Sparse Row matrix
       coo_matrix:   A sparse matrix in COOrdinate format.
       csc_matrix:   Compressed Sparse Column matrix
       csr_matrix:   Compressed Sparse Row matrix
       dia_matrix:   Sparse matrix with DIAgonal storage
       dok_matrix:   Dictionary Of Keys based sparse matrix.
       lil_matrix:    Row-basedlinked list sparse matrix
       关于其中的差别,我们可以看看后面的参考资料和scipy的官方文档。这里只介绍下主流的CSR格式。

二、CSR稀疏矩阵存储方式
       CSR全名是 CompressedSparse Row Format。它的存储方式通过下面一张图可以清晰的看出来:
103.png
       可以看到,一个稠密矩阵被存储到三个数组里面。其中把矩阵的所有非零元素值按照行的顺序保存到values数组中。就是先第一行的非零元素站出来排好队,然后第二行的跟上……这个矩阵有9个非零元素是不是,所以values数组就是9个元素。那怎么记录这些非零元素在原矩阵中的位置呢?矩阵的位置不就是哪一行哪一列吗?那我们再分别添加一个行数组和一个列数组来辅助。这是很自然的,水到渠成的想法。首先,需要记录values数组中每个元素(也就是原矩阵的非零元素)所在的行号是多少。这样我们就新建一个数组叫row indices,它的值是[0 0 1 1 2 2 2 3 3]。还需要新建一个数组column indices来记录values数组中每个元素的列号是多少,它的值是[0 1 1 2 0 2 3 1 3]。很明显,这三个数组一一对应起来就可以描述原数组了。例如,values的第三个非零元素是2,它在原矩阵的行是row indices的第三个元素,也就是1,所在的列是column indices中的第三个元素,也是1,所以非零元素在原矩阵的位置是(1,1)。
       到这里我们的目标就达到了,但需要的存储空间是“非零元素个数x3”。那还能再小点吗?Smaller than smaller!答案是可以的,CSR就是其中一种方法了。它的思想也很简单,矩阵中很多非零元素都属于同一行对不对?例如上面例子中,values数组中的第1和第2个元素都属于矩阵的第1行,第5,6,7个元素都属于矩阵的第3行。我们在row indices中也可以看出来,它的值是[0 0 1 1 2 2 2 3 3],可以看到很多元素是连续相同而且基数递增的,那我们就可以把这些连续相同的数据合并,只标记每行的开始和结尾即可。也可以说只记录偏移量。这个数组名字就叫row offset,它的大小就是原矩阵的行数+1,它用自己的第i和第i+1位置上的元素值来标记values数组中的offset到offset[i+1]-1位置上的数都是原矩阵第i行的元素。所以上面例子中row offset是[0 2 4 7 9]。不知道说清楚了没有,不清楚自己再慢慢看下图。

三、基于csr的knn实现
       基于稀疏矩阵的运算是比较快的,例如矩阵乘法,因为矩阵中很多元素为0,任何数与0相乘都等于0。目前也有很多库实现了稀疏矩阵的运算。在这里我们不涉及细节的实现过程,我们只运用现有的优化过的库来搭搭积木。这里我们使用scipy包的csr_matrix模块,关于它的说明,请参考官方文档。我们是在Python环境下做实验,建议使用Anaconda这个python发行版本,最新的版本提供了多达195个流行的python包,包含了我们常用的numpy、scipy等等科学计算的包。有了它,妈妈再也不用担心我焦头烂额地安装一个又一个依赖包了。
       这里我们拿knn来做实验。因为它涉及到稀疏矩阵的乘法等。对KNN,一般有两个矩阵,一个是存储N个训练样本的矩阵A,假设矩阵每一行代表一个训练样本。还有一个是存储M个测试样本的矩阵B。KNN要求我们计算:对每个B中的样本,计算它与矩阵A中N个样本的欧式距离(这里采用),然后找出距离最小的K个。我们知道欧式距离可以展开:|a-b|2=a2-2ab+b2.所以在程序中,我们可以先分别计算矩阵A和B每一行的平方和,也就是a2和b2。因为矩阵A中所有样本和B中所有样本的内积,所以可以统一到矩阵A和B相乘。这样就可以把所有的计算都统一到矩阵运算中去了,也就可以借助矢量化运算的神力了。故而得到了以下的程序:
knn_sparse_csr.py


  • #****************************************************
  • #*
  • #* Description: KNN with sparse data
  • #* Author: Zou Xiaoyi
  • #* Date:   2014-12-31
  • #*
  • #****************************************************
  • import numpy as np
  • from scipy.sparse import csr_matrix
  • def kNN_Sparse(local_data_csr, query_data_csr, top_k):
  •     # calculate the square sum of each vector
  •     local_data_sq = local_data_csr.multiply(local_data_csr).sum(1)
  •     query_data_sq = query_data_csr.multiply(query_data_csr).sum(1)
  •     # calculate the dot
  •     distance = query_data_csr.dot(local_data_csr.transpose()).todense()
  •     # calculate the distance
  •     num_query, num_local = distance.shape
  •     distance = np.tile(query_data_sq, (1, num_local)) + np.tile(local_data_sq.T, (num_query, 1)) - 2 * distance
  •     # get the top k
  •     topK_idx = np.argsort(distance)[:, 0:top_k]
  •     topK_similarity = np.zeros((num_query, top_k), np.float32)
  •     for i in xrange(num_query):
  •         topK_similarity = distance[i, topK_idx]
  •     return topK_similarity, topK_idx
  • def run_knn():
  •     top_k = np.array(2, dtype=np.int32)
  •     local_data_offset = np.array([0, 1, 2, 4, 6], dtype=np.int64)
  •     local_data_index = np.array([0, 1, 0, 1, 0, 2], dtype=np.int32)
  •     local_data_value = np.array([1, 2, 3, 4, 8, 9], dtype=np.float32)
  •     local_data_csr = csr_matrix((local_data_value, local_data_index, local_data_offset), dtype = np.float32)
  •     print local_data_csr.todense()
  •     query_offset = np.array([0, 1, 4], dtype=np.int64)
  •     query_index = np.array([0, 0, 1, 2], dtype=np.int32)
  •     query_value = np.array([1.1, 3.1, 4, 0.1], dtype=np.float32)
  •     query_csr = csr_matrix((query_value, query_index, query_offset), dtype = np.float32)
  •     print query_csr.todense()
  •     topK_similarity, topK_idx = kNN_Sparse(local_data_csr, query_csr, top_k)
  •     for i in range(query_offset.shape[0]-1):
  •         print "for %d image, top %d is " % (i, top_k) , topK_idx
  •         print "corresponding similarity: ", topK_similarity
  • if __name__ == '__main__':
  •     run_knn()



       程序输出如下:

  • [[ 1.  0.  0.]
  • [ 0.  2.  0.]
  • [ 3.  4.  0.]
  • [ 8.  0.  9.]]
  • [[ 1.10000002  0.          0.        ]
  • [ 3.0999999   4.          0.1       ]]
  • for 0 image, top 2 is  [[0 1]]
  • corresponding similarity:  [ 0.00999999  5.21000004]
  • for 1 image, top 2 is  [[2 1]]
  • corresponding similarity:  [  0.02000046  13.61999893]




       这个代码在这个toy数据下运行看不到什么加速的。但如果处理庞大的高维数据,而且稀疏度较高,那稀疏的计算加速比还是很惊人的。PS:上述程序在大矩阵中运行过。

四、参考资料:
[1] 稀疏矩阵的存储格式(Sparse Matrix Storage Formats)
[2] http://www.bu.edu/pasi/files/2011/01/NathanBell1-10-1000.pdf

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

站长推荐上一条 /1 下一条

facebook google plus twitter linkedin youku weibo rss
©2019 Microchip Corporation

小黑屋|手机版|Archiver|Tensilica技术社区

GMT+8, 2019-9-21 18:05 , Processed in 0.050499 second(s), 7 queries , MemCache On.

快速回复 返回顶部 返回列表