现有user、item矩阵,如何计算两两用户的相似度呢?最直接的方法就是夹角余弦,计算用户向量之间的cos值,来度量相似度。因为实际问题中,矩阵通常是很稀疏的,所以真正实现cos计算相似度计算的时候,为了减少计算量,采用的的是倒排索引的数据结构。即:
虽然采用的倒排的结构,但是用户量和item量很大,且有些item对应的用户量很大的时候,就会出现严重的数据倾斜问题。以MapReduce实现过程为例,如果大多数item对应用户量都是几十万的级别,少量item对应user量很大,例如百万以上,则聚集到这些item上(即对应的reduce上)的数据量就会很大,此时就出现数据倾斜的问题,整体速度方面就会很慢。如何解决这种数据倾斜的问题呢?
解决上面提到的数据倾斜问题,可采用矩阵分块的思想,当一个item下用户量特别大,将其打散到多个reduce当中进行处理,可大大增加运行的速度。但带来的负面影响就是,网络通信量增加,分k块,通信量就增加原始数据量的k倍。实际运行的时候,可以多种权衡,例如下面这种:
1) 当item的用户量不大的时候(设定一个阈值),即小于阈值,则不进行分块
2) 当item对应用户量比较大的时候,可以分k1块
3) 当item对应用户量特别大的时候,可以分k2块,这里k2>k1
简单起见,下面介绍固定分块的思路:
假定分5块,即一个item对应的用户分五块,在多个reduce里面完成相似度的计算。伪代码如下:
userinfo.set(0, 用户id); userinfo.set(1, item id); userinfo.set(2, value); int index = (int) (user_id % blocks); System.out.println("map阶段,取模分块:\t" + blocks); /*下面的if、else,保证了只计算上三角矩阵(因为这里的关系是双向) 同时,注意要实现Comparator,保证不同flag的key是有序到reduce中*/ for (int i = 0; i < blocks; i++) { if (index <= i) { key.setKeyPrior(item + "_A" + index + "A" + i, 1); flag.set(1); userinfo.set(3, flag); } else if (index > i) { key.setKeyPrior(item + "_A" + i + "A" + index, 2); flag.set(2); userinfo.set(3, flag); } context.write(key, userinfo); }
在reduce中,要处理两种情况,一种是对角线对应矩阵的用户相似度计算,另外一种情况是上三角其他两两矩阵用户相似度的计算。
(1)对角线的矩阵相似度计算时:可采用容器存储所有用户信息,然后:
for (int i = 0; i < this.userList.size(); i++) { user1 = this.userList.get(i)); for (int j = i + 1; j < this.userList.size(); j++) { user2 = this.userList.get(j)); 计算相似度... } }