的Taste基于实现协同过滤推荐引擎的代码分析
Taste 是 提供的一个协同过滤算法的高效实现,它是一个基于Java实现的可扩展的高效的推荐引擎。
该推荐引擎是用这样简单的数据格式表达用户对物品的偏好。
以此为输入数据,计算后就可以得到为每个user推荐的items列表。
他提供了方便的单机版的编程接口,也提供了基于的分布式的实现。
单机版的编程接口主要适用于写demo和做算法的评估,若处理大规模数据,还是需分布式的实现。
以下是对org...cf.taste..item.的各步骤的一个解读。Taste 实现一个分布式的协同过滤推荐共经历了如下12个步骤。
以下分析了各步骤的和都做了哪些工作,并有什么格式的数据输出。
代码分析:
1、计算item的和最小值
1.1、.class, .class, .class,
用原始输入,将,,pref数据转成,
1.2、.class, .class, .class,
在,中找最小的,输出,
此处只是保存一个int型的索引和对应的long型的的映射
2、计算各user的item偏好向量,即
2.1、.class, .class, ? .class : .class,
用原始输入,读入偏好数据,得到,
2.2、.class, .class, .class,
将,中的变成协同过滤算法java实现 Apache Mahout的Taste基于Hadoop实现协同过滤推荐引擎的代码,得到,,后者用ctor来存。
3、统计数据中有多少个user
3.1、.class,e.class,.class,
用步骤2的输出,统计独立数目,先转换数据为,
3.2、.class,.class,.class,
通过r将所有数据发到一个区,一个来处理
由于都已排序,所以可以用极简单的方式来统计出独立数
输出只有一个值,即用户数
4、计算item的user偏好向量,即,也即拿步骤2的结果做矩阵的修剪和转置
4.1、.class,.class,..class, 用步骤2的输出,按指定的参数值来修剪的数目,目的是控制计算的规模,减少计算量然后转为以为列号、为行号、pref为值的矩阵,用表示矩阵。输出为,
4.2、.class,.class,.class,
输出为,,相当于对步骤2的结果进行了矩阵的转置,
有了偏好矩阵数据,接下来会调用来计算行的相似度
此处的行是item,所以默认是item-base的CF。
但其实可以通过传入是否转置的参数来对步骤1进行调整,将和转换,就可以实现user-base的CF。此处也可以通过参数来指定用哪种算法来计算相似度。
将通过接下来的3个步骤来实现:
5、用相似度算法给向量赋权
5.1、.class,.class,.class,
用相应的相似度算法来计算步骤4的输出,计算每个所对应的的。输出为,,是一个简单的数据封装类。
5.2、
.class,.class,ray.class,
将简单变为ray,后者只是简单继承了。最后输出结果为,ray,数组的数据项是
6、用相似度算法计算相似度,得到相似度矩阵
6.1、.class,.class,.class,
取出步骤5的结果,将ray的数据双重循环,拼装如下的KV数据结构,
6.2、
.class,yKey.class,..class, 此步骤的Map输出,也即的输入是
,
>
也即itemA和itemB的,以及不同user对itemA和itemB的pref。
相应的实例就可以利用以上数据计算itemA与itemB的相似度评分
输出结果为
yKey,
也就是不同item和itemA的俩俩相似度,得到一个相似度矩阵
7、将相似度矩阵转为向量存储
7.1、.class,yKey.class,..class,
将步骤6的结果简单读入,item相似度矩阵
7.2、cer.class,.class,.class,
输出为,,用 存储。
也就是输出为不同的其他item与itemA之间的相似度值
8、的预处理1,填充部分的数据
8.1、.class, .class, .class,
用步骤7的相似度数据,输出,(,null,null)
8.2、.class, .class, .class,
默认,直接输出的输出
9、的预处理2,填充和pref部分的数据
9.1、pper.class, .class, .class,
如果提供了一个列表文件,初始化时会先读入该文件到中
如果不在这个Set中,则会直接,也就是只会为该列表中的user做推荐
用步骤2的用户对各item的偏好数据,输出,(null,,pref)
9.2、.class, .class, .class,
默认,直接输出的输出
10、拼装两个预处理的数据
10.1、.class, .class, .class,
用.指定多个路径,将步骤8和9的输出同时作为输入
10.2、er.class, .class, le.class,
将(,null,null)和(null,,pref)
变为le(,List,List
)
最后的输出是,le(,List,List
)
11、如果设置了item过滤文件则读取,作为黑名单
11.1、.class, .class, .class,
简单读入item过滤文件,输出为,,这相当于“黑”名单,用于后面推荐结果的过滤。
11.2、.class, .class, le.class, 输出为,le(,List,List
)
其中的值为(,.NaN),pref的值都用1.0f来填充。
注意,的第二项数据,也即被设置为.NaN,后面将会用这个来判断这是否是黑名单。
12、用相似度矩阵的做推荐计算
12.1、r.class, .class, .class,
如果步骤11存在,则用.指定多个路径,将步骤10和11的输出同时作为输入
也即输入为,le(,List,List
),其中的值为
输出为,(pref,)
12.2、.class, .class, able.class,
初始化时,会读入步骤1的结果,是一个,也即index和的映射
若设置了item白名单文件,则初始化时也会读入文件到,推荐结果必须在这里边。和步骤11的黑名单相反。
在处理时会区分是否是而用不同的处理逻辑协同过滤算法java实现,此处我们主要讨论非,也即有实际pref数据的情况而不是默认用1.0f来填充的pref。
中进行,按乘积得到的推荐度的大小取出最大的几个item。
处理的过程中需要将通过转换回,并且用“黑”“白”名单进行过滤。
白名单很容易理解,用集合是否为空和集合的();
黑名单是判断Float.isNaN(),因为此前在步骤11的输出时黑名单的被设置为了.NaN。
对于非,是用pref和相似度矩阵的得到推荐度的值来进行排序。
而的pref值都是1.0f,所以去计算矩阵相乘的过程没有意义,直接累加相似度的值即可。
用这个数据排序就可得到推荐结果。
输出为,able,后者实际是List>,
这里的pref是相似度矩阵的或是相似度累加计算出来的值而非实际值。
后注:
以上提到的,,ctor等等数据结构,
是提供的一些大数据量存储和处理的一些高效实现,
针对数据的特点而做的有针对性的优化,同时解决性能和空间的问题。
在 in 的讨论CF和等的“数据的表达”章节中有专门的阐述,此处不再详细解释。
的taste框架是协同过滤算法的实现。它支持,如文件、数据库、NoSQL 存储等,也支持的。这里主要分析的基于MR的实现。
基于MR的CF实现主要流程就在org...cf.taste..item.类中(注意有两个,要看清楚是哪一个包)。这个类的run方法就包含了所有的步骤。从上到下,完整的其实有10步(中间计算item相似度其实拆分成了3个job,我们也当做是一个phase吧),也就是说,如果指定了所有的必要参数,运行一次item-based CF算法,会执行12个JOB,当然有的步骤是可以忽略的,下面会讲。以下就是详细的每一步骤的分析:
:
这步主要是将转成一个int。这里设计上其实有点小问题,如果item的数量非常多,比如超过int的最大值,那就有可能会出现重合了。所以用long其实更为合适。
input:用户评分文件(这也是我们最原始的输入了),格式一般为: t t score。注意输入必须是的。可能是为了方便测试吧,的很多包默认输出都是 格式的。
map:(index, )
: (index, )
:
input:用户评分文件
param: --如果这个参数为true,则会忽略评分列,对于买或不买这类数据有时需要指这定这个值。
map: (, ,pref)
: 以用户为key,输出成向量形式è (, )
: ,计算用户数
map: ()
: 输出总用户数count
: se
input: 的输出:
param: --
map: (,) è(,),注意如果指定了—参数,这里会有裁剪,每个最多对的 打分
这里的,分布式行矩阵:行:, 列:
: (, )
:
这一步比较关键,计算item相似度,它拆分成了三个JOB。
param: --, --,--w(默认:100)
job1:
input:的输出
map: (, ) ==>(, )
这里的,对于欧氏向量距离,或者距离等,均为.NaN,即无效。在中有用到的值。
:(, ray)
job2: item相似度计算
map: 对同一用户的所有item-,输出两两item之间的关系
==>(, ) (同上,这里的权重,B对于欧氏距离等可以忽略)
: 在这端,以为key聚合了来自不同map的所有用户的打分,最后输出itemA和B的对称相似度(即以itemA为key或以itemB为key)
==>(yKey,
>) ,(yKey, >)
job3: 汇总item的相似items
param: --w
map: (itemA, itemB, ) & (itemB,itemA, ) 这里在group的时候按相似度降序做了排序,如果有--w参数,则会做裁剪。
: (itemA, )
至此,item相似度计算完毕。
:
input: 的最后输出(即item相似度)
map: 直接输出item对应的相似items,这里用做了封装,表明有可能是相似度向量协同过滤算法java实现,也有可能是对item的打分,并且对item为自己的,将相似度设为.NaN,以过滤自身。è(,)
:
:
input: 的输出
map: 输出:(, )
这里默认考虑用户对10个item的评分,可以通过dered参数调整。
如果指定了,则在setup时会把所有的读入内存,用于过滤。如果map输入数据的不在中,则会被忽略。注意,这是的设计bug,对于比较大的数据集,很有可能造成OOM(事实上在我的测试程序中已经出现OOM了…),这种bug下面还会出现。输出的是用户的评分,同的的封装。
:
:
input: 6和7的输出:,
map: 。由于6和7的输出的key均为,因而在端同一item的相似item以及对应的用户评分会聚合到一起。
:(, le,List
>) 没做
特殊处理,直接合在一起,输出相似度矩阵,所有的及对item的打分。
:
将过滤文件输出成。如果指定了--参数,则在最后的聚合推荐中会过滤对应的items。这一步在实际中多数是可以忽略的,只要不指定这个参数即可。
: d
map: 对每个用户,输出对当前item的评分,以及与当前item的所有相似
itemsè(,
>)
: 聚合了这个用户所有的评分历史,以及相似items,计算对该用户的推荐结果è(, List)。
注意在的setup中,会将产生的所有到index的映射读入内存,这里只要Item数据集稍大,就会OOM。这是比较严重的设计bug。
事实上,如果item是正规的整数,而不是guid之类的,和这一步的读入内存是完全可以略掉的。这样的话就完全可以在企业级的数据集上使用(我的测试数据集是15亿+的user-item-,1.5亿+的用户,在最后这一步挂掉了,前面所有phase都能跑成功)。
至此,已经形成了推荐结果,CF完成。
以上的所有步骤中,的计算item相似度是最慢的(这个其实也很直觉)