使用Mahout搭建推荐系统之入门篇4-Mahout实战
2015-12-17 11:05
656 查看
原始地址:http://my.oschina.net/Cfreedom/blog/201828
目录[-]
一、基本内容
二、运行环境
三、程序运行
3.1 调整N值和Threshold值对推荐结果的影响:
3.2. 针对DataModel做一些数据分析,
类似于博文2, 判断item和user数量, value范围, 方差等.
3.3 选择DataModel, 并计算内存使用情况
3.4. 选择相似性矩阵和调参
3.5 slope-one
四、总结
五、Similarity和Algorithm相关总结
六、参考资料
用意: 结合上篇博客,写写代码熟悉一下Mahout。很多地方想法都比较粗糙,亟待指正。
代码放在了: https://github.com/xiaoqiangkx/qingRS
2. 过滤数据: 评分较少的用户直接过滤掉, 那些评分均一致且评分数量多的用户过滤掉. 计算过滤百分比, 如果过滤过多, 则需要考虑其它方法了.
3. DataModel选择: 选择数据库存储还是文件存储; 选择GenericDataModel还是GenericBooleanDataModel
4. 选择相似矩阵和参数, 如N值和门限值; 可视化(可选).
Mahout环境搭建
本篇使用mahout 0.8的taste等相关jar包进行开发, jar包可以从 http://mirror.bit.edu.cn/apache/mahout/mahout-distribution-0.8.tar.gz中摘取,也可以在百度网盘上下载 http://pan.baidu.com/s/1iSOWk. 与上次不同, 0.8版本的distribution合并了两个包, 上次漏了两个log包, 最终只需要引入7个包即可.
mahout核心类不变: 提供推荐Model等核心类
mahout-core-0.8.jar
mahout-math-0.8.jar
辅助类: 提供Log和部分数学公式类.
slf4j-api-1.7.5.jar commons-logging-1.1.1.jar slf4j-jcl-1.7.5.jar提供Log服务
guava-14.1.0.jar合并了两个google相关的数学类google-collections.jar和guava.jar
commons-math3-3.2.jar包取代了uncommons-maths-1.2.jar类
我在博文1的框架下做了一点小改动, 从而说明推荐算法算法的结果不稳定性以及调参的重要性. 推荐系统不像一般的业务逻辑, 搭建好系统只完成了极小的一部分, 重点在于调参和响应速度.
类似于博客1中叙述所述, 搭建基本的框架, 并引入movielens 100K中的u.data数据,运行成功.
工程目录结构:
[数据格式说明: movielens u.data数据格式为"244 51 2 880606923", 以tab隔开. 表示ID为244的用户对ID为51的物品打分为2分, 时间为880606923, 猜测类似于从1970年1月1日开始记的秒数, 数量级差不多, 暂时不使用此参数.]
首先介绍User-based和Item-based的方法.
以User-based为例, 将每一个物品表示为一个维度, 那么每个用户都可以表示为一个向量. 如果一个有{101, 102, 103, 104, 105}五个物品, 用户1对101评分为2.0, 对105评分为3.0, 那么用户1可以表示为[2.0, 0, 0, 0, 3.0]. 那么用户之间就有距离, 距离由Similarity相似性决定, 常见的如欧拉距离. 如果我们确定了所有用户间的距离, 那么可以使用N近邻法或者门限法确定每个人的相邻圈子,
如下所示.
如何选择每个item或者user响铃圈子:
常见的有N近邻法和门限值法. 如下面2图所示:
此图表示N = 3时,选择与1最近的前三位2, 4, 5而排除3. 1的圈子由2, 4, 5组成.
此图表示门限(Threshold)选择法, 4, 5 在门限之内, 而2. 3在门限之外. 1的圈子由4, 5组成.
总结: 那么接下来的问题就是如何定义相似性, 即计算距离了.
重要代码片段如下:
运行结果:
A. 当neigherhood从2到9变化时, 推荐的物品前期在变化, 后期趋于稳定.
neigherNum=2
Recommend=313 4.5
neigherNum=3
Recommend=286 5.0
neigherNum=4
Recommend=286 5.0
neigherNum=5
Recommend=990 5.0
neigherNum=6
Recommend=990 5.0
neigherNum=7
Recommend=990 5.0
neigherNum=8
Recommend=990 5.0
neigherNum=9
Recommend=990 5.0
解释: neigherhood一开始变化时, 参考的人数增多了, 所谓三个臭皮匠顶过一个诸葛亮, 推荐将会变化, 但是随着neigherhood的变大, 加再多的人进来也只是凑人数而已没有多大的决定能力.
B. 当rankNum从2到10变化时, 感觉上rankNum的改变不应该影响推荐结果.
List<RecommendedItem> recommendations = recommender.recommend(userid,
rankNum);
但是: 我们发现除了neigherNum = 2以外, 推荐结果均发生了变化, 而且数据开始震荡, 如果将neigherNum放大到30, 推荐结果依旧不停地震荡.
neigherNum=2
Recommend=313 4.5
neigherNum=3
Recommend=323 5.0
neigherNum=4
Recommend=898 5.0
neigherNum=5
Recommend=323 5.0
neigherNum=6
Recommend=323 5.0
neigherNum=7
Recommend=898 5.0
neigherNum=8
Recommend=326 5.0
neigherNum=9
Recommend=326 5.0
解释???: 问题应该出在排序算法上, Mahout为了节约内存使用了qSort, 因此排序算法不稳定. 但是我去查看Mahout源代码发现GenericUserBasedRecommender中使用了Collections.sort(), sort默认使用的是MergeSort, 所以排序应该是稳定的. 依旧存在着疑问.
代码如下:
设置门限过滤数据
在代码中加入过滤模块
运行结果如下
user warning(0)
item warning(743)
Preference=(1.0, 5.0)
usersNum=943, userMean=106.04453870625663, userVar=100.87821227051644
itemsNum=1682, itemsMean=59.45303210463734, itemsVar=80.3599467406018
分析:与官方的1000个用户, 1700部电影的说法一致. http://www.grouplens.org/datasets/movielens/
user warning(0)
item warning(743) 表示有743个item评分个数小于20.
物品评分较为稀疏程度和物品总数大小是一致的. 使用user-based则用户少,节约内存, 且矩阵致密。
设置门限为20时, 发现物品矩阵稀疏、方差大和过滤器的统计结果item warning(743)大是一致, 此处先不过滤数据, 后期再说.
注:当然优秀的过滤器需要改变门限值来不停的调试
由于数据有rate, 所以不使用Boolean形式的存储.
预估内存开销:
由上文分析可知: Preference ~= usersNum * userMean ~= 100K, 每个Preference消耗28bytes,
预估内存开销= 28bytes * 100K = 2.8 Mbytes. 此外相似矩阵如果使用邻接矩阵方式存储, max{usersNum, itemsNum}**2 * 4bytes(float) = 8Mbytes左右. 因此内存总结开销在10M左右.
[但是查看Mahout源代码org.apache.mahout.cf.taste.impl中相关文件发现, 相似矩阵是临时计算的, 每次recommend时通过重写Estimator接口的estimate方法来具体实现. 可以mahout还是考虑到内存开销, 牺牲了计算速度吧. 所以估计程序运行内存开销依旧在2.8Mbytes左右. 究竟哪个是正确的理解呢?]
因此我使用in-memory形式的GenericDataModel将数据直接加载到内存中.
实验测试内存开销:
通过多次调用System.gc()来回收内存, 通过Rumtime.totalMemory和Runtime.freeMemory()查看内存使用状态.
http://docs.oracle.com/javase/6/docs/api/
代码如下:
运行结果如下:
start: jvm used-memory= 0.5967178344726562MB
after dataModel: jvm used-memory= 19.2872314453125MB
after similarity: jvm used-memory= 19.2872314453125MB
after neighborhood: jvm used-memory= 19.58240509033203MB
after recommender: jvm used-memory= 19.58240509033203MB
recommend=340
after recommend first: jvm used-memory= 19.877883911132812MB
after gc: jvm used-memory= 9.829483032226562MB
recommend=340
after recommend second: jvm used-memory= 9.829483032226562MB
分析: 由上述数据可见,gc回收内存后, JVM内存消耗回收了10Mbytes, 与猜测一致.
问题: 回收完数据后, 为什么recommender还可以进行推荐, 而且没有额外的内存开销???
数据增长10倍, 即使用1M数据进行测试
简单统计分析结果:
user warning(0)
item warning(663)
Preference=(1.0, 5.0)
usersNum=6040, userMean=165.5975165562914, userVar=192.73107252940773
itemsNum=3706, itemsMean=269.88909875876953, itemsVar=383.9960197430679
估计内存消耗: usersNum和itemsNum增长了3到6倍, 而相似矩阵消耗内存为平方级别, 那么内存消耗上线为9到36倍; 此外数据增长10倍, DataModel内存消耗为线性增长, 增长10倍内存消耗. 那么估计内存消耗= 2.8M * 10 + (9~36)*8M = 100M ~ 316M内存之间. 如果不存储相似矩阵, 那么内存消耗为28M左右.
由于数据以"::"作为分割符, 用python简单处理一下,替换为\t
运行结果如下
start: jvm used-memory= 0.5967178344726562MB
after dataModel: jvm used-memory= 204.9770050048828MB
after similarity: jvm used-memory= 204.9770050048828MB
after neighborhood: jvm used-memory= 204.9770050048828MB
after recommender: jvm used-memory= 204.9770050048828MB
recommend=2908
after recommend first: jvm used-memory= 208.10643768310547MB
after gc: jvm used-memory= 76.12030029296875MB
recommend=2908
after recommend second: jvm used-memory= 76.12030029296875MB
分析: 由上述数据可以: 数据回收了132Mbytes, 76M为运行开销. 与估计内存消耗移植. DataModel线性增长, 相似矩阵平方级别增长.
结论: 如果评分数增加到10M级别, 用户或者物品数增长3~10倍, 那么需要4G到40G的内存才能快速的计算出推荐结果, 需要增加内存条, 设置JVM配置以及使用hadoop来实现. 另外真实的数据用户数达到GB级别, 总数达到TB级别, 需要的内存数量和运算量是十分恐怖的. 传统地算法已经无法满足要求, 需要借助Hadoop这种分布式来实现运算.
当然内存不够大, 硬盘可以很大, 处理10M级别以上的推荐数据时, 选择使用MysqlJDBCDataModel来实现存储.
另外: 据数盟的一位Q友说, "淘宝有8kw的商品(记忆也许有出入),用户2亿,多大的矩阵啊". 每次想到这里, 都会默默地闭上双眼, 遥想远方的宇宙, 数据又是多么地浩淼. 在上帝眼中, 我们也许还只是玩过家家, 学1+1的小孩子吧.
此外, 后期希望考虑user-based, item-based, slope-one算法的比较, 同时参考运行时间.
相似矩阵选择下面4种
PearsonCorrelationSimilarity EuclideanDistanceSimilarity TanimotoCoefficientSimilarity LogLikeLihoodSimilarity
[ 注:其中EuclideanDistanceSimilarity比较特殊, 它没有实现UserSimilarity接口, 所以不能放到一个Collection<UserSimilarity>容器中 ]
[注: 勿看了org.apache.mahout.math.hadoop.similarity.cooccurrence.measures文件]
参数调整只选择近邻N和threashold
这里给出代码原型, 但是在普通PC上跑100K的数据集都太慢了, 使用intro.csv这个toy数据跑一跑.
N选择[2, 4, 8, ... 64], Threshold选择[0.9, 0.85, ... 0.7];
代码如下:
运行结果如下:
SIM NeighborNum Threshold score
1 2 0.75 0.4858379364013672
1 2 0.7 NaN
1 4 0.75 0.4676065444946289
1 4 0.7 NaN
1 8 0.75 0.8704338073730469
1 8 0.7 0.014162302017211914
1 16 0.75 NaN
1 16 0.7 0.7338032722473145
1 32 0.75 0.7338032722473145
1 32 0.7 0.4858379364013672
1 64 0.75 NaN
1 64 0.7 1.0
The best parameter
1 8 0.7 0.014162302017211914
分析: 运行最佳的结果为N = 8, Threshold = 0.7 当然, 这个方法, 十分的粗糙, 但是也说明了参数的重要性, 毕竟推荐系统上线了必须有优秀的A\B Test结果, 要不然还不如使用打折, 优惠券来的简单实在.
顺便截一张Mahout in Action上一个真实案例的数据, 如下图所示
item-based与user_based一致, 基本上就是就Similarity, Neighborhood和Recommender的User换成Item即可.
运行结果为: 1.3571428571428572 当然这个结果意义不大, 因为数据集很小.
常见的方法如下表所示: Similarity只是描述计算方法, 并不计算并保存相似矩阵.
相似性的基本思路就是不适用欧式距离的, 都得加上权重或者门限来防止交集较小的相似距离.
如何选择推荐算法:
user-based算法: 最古老的算法, 计算相似的人群, 最大的问题是存储相似矩阵, 由于每个用户喜欢的物品在变化, 导致相似矩阵不停的变化. 更新相似矩阵计算量可能较大. 针对搜索引擎来说, 搜索词如果比用户数目多的话,可以考虑user-based.
item-based算法: 与user-based类似, 每个物品被喜欢的用户个数不停地变化, 相似矩阵持续地更新. 在互联网时代,商品上百万, 用户上亿. 那么使用item-based比较靠谱, 物品相似矩阵变化较小, Amazon的推荐算法就是使用item-based为基础的.
SVD: 现在比较流行的算法, 因为可以进行降维. 发掘有价值的特征维度来取代用户维度或者商品维度. 举个例子: 例如两个人分别喜欢保时捷和法拉利, user-based和item-based计算的相似性都很低, 但是SVD引入跑车或者奢侈品这种潜在的特征后, 两者就有相似性了. 当然缺点在于, SVD需要将整个矩阵加载到内存进行矩阵分解, 对内存消耗大, 不知道SVD的矩阵分解有没有Map-Reduce实现方法.
Slope-One算法: 上述三种算法都不太适合作为在线算法和更新数据, 但是Slope-One可以. 举个例子, 假设所有用户评价电影A比电影B高1.0分, 评价电影C和电影A一致. 如果一个用户评价电影B为2.0分, 评价电影C为4.0分, 那么用户评价电影A为3.0分或者4.0分, 最佳的方法的取两者的加权平均值, 权重由同时出现次数决定. Slope-One可以离线计算所有的n*(n-1)/2中相关性, 当一个用户更新了电影时,
相关性更新快捷; 通过遍历一遍电影即可获得所有电影的评分,从而排序给出推荐. 缺点是相关性计算复杂. [个人觉得这个计算量也不小, 取决于电影个数以及用户评分电影个数]
目录[-]
一、基本内容
二、运行环境
三、程序运行
3.1 调整N值和Threshold值对推荐结果的影响:
3.2. 针对DataModel做一些数据分析,
类似于博文2, 判断item和user数量, value范围, 方差等.
3.3 选择DataModel, 并计算内存使用情况
3.4. 选择相似性矩阵和调参
3.5 slope-one
四、总结
五、Similarity和Algorithm相关总结
六、参考资料
用意: 结合上篇博客,写写代码熟悉一下Mahout。很多地方想法都比较粗糙,亟待指正。
代码放在了: https://github.com/xiaoqiangkx/qingRS
一、基本内容
1. 加载数据: 判断userID和itemID的大小关系2. 过滤数据: 评分较少的用户直接过滤掉, 那些评分均一致且评分数量多的用户过滤掉. 计算过滤百分比, 如果过滤过多, 则需要考虑其它方法了.
3. DataModel选择: 选择数据库存储还是文件存储; 选择GenericDataModel还是GenericBooleanDataModel
4. 选择相似矩阵和参数, 如N值和门限值; 可视化(可选).
二、运行环境
JAVA MYSQL等配置参考"最美的词" 基于mahout的电影推荐系统Mahout环境搭建
本篇使用mahout 0.8的taste等相关jar包进行开发, jar包可以从 http://mirror.bit.edu.cn/apache/mahout/mahout-distribution-0.8.tar.gz中摘取,也可以在百度网盘上下载 http://pan.baidu.com/s/1iSOWk. 与上次不同, 0.8版本的distribution合并了两个包, 上次漏了两个log包, 最终只需要引入7个包即可.
mahout核心类不变: 提供推荐Model等核心类
mahout-core-0.8.jar
mahout-math-0.8.jar
辅助类: 提供Log和部分数学公式类.
slf4j-api-1.7.5.jar commons-logging-1.1.1.jar slf4j-jcl-1.7.5.jar提供Log服务
guava-14.1.0.jar合并了两个google相关的数学类google-collections.jar和guava.jar
commons-math3-3.2.jar包取代了uncommons-maths-1.2.jar类
三、程序运行
搭建基本框架并进行简单测试我在博文1的框架下做了一点小改动, 从而说明推荐算法算法的结果不稳定性以及调参的重要性. 推荐系统不像一般的业务逻辑, 搭建好系统只完成了极小的一部分, 重点在于调参和响应速度.
类似于博客1中叙述所述, 搭建基本的框架, 并引入movielens 100K中的u.data数据,运行成功.
工程目录结构:
[数据格式说明: movielens u.data数据格式为"244 51 2 880606923", 以tab隔开. 表示ID为244的用户对ID为51的物品打分为2分, 时间为880606923, 猜测类似于从1970年1月1日开始记的秒数, 数量级差不多, 暂时不使用此参数.]
首先介绍User-based和Item-based的方法.
以User-based为例, 将每一个物品表示为一个维度, 那么每个用户都可以表示为一个向量. 如果一个有{101, 102, 103, 104, 105}五个物品, 用户1对101评分为2.0, 对105评分为3.0, 那么用户1可以表示为[2.0, 0, 0, 0, 3.0]. 那么用户之间就有距离, 距离由Similarity相似性决定, 常见的如欧拉距离. 如果我们确定了所有用户间的距离, 那么可以使用N近邻法或者门限法确定每个人的相邻圈子,
如下所示.
如何选择每个item或者user响铃圈子:
常见的有N近邻法和门限值法. 如下面2图所示:
此图表示N = 3时,选择与1最近的前三位2, 4, 5而排除3. 1的圈子由2, 4, 5组成.
此图表示门限(Threshold)选择法, 4, 5 在门限之内, 而2. 3在门限之外. 1的圈子由4, 5组成.
总结: 那么接下来的问题就是如何定义相似性, 即计算距离了.
3.1 调整N值和Threshold值对推荐结果的影响:
重要代码片段如下:public static void main(String[] args) throws Exception { int userId = 1; int rankNum = 2; QingRS qingRS = new QingRS(); for(int neighberNum = 2; neighberNum < 10; neighberNum++) { System.out.println("neigherNum=" + neighberNum); qingRS.initRecommenderIntro(filename, neighberNum); String resultStr = qingRS.getRecommender(userId, rankNum); System.out.println(resultStr); } }
运行结果:
A. 当neigherhood从2到9变化时, 推荐的物品前期在变化, 后期趋于稳定.
neigherNum=2
Recommend=313 4.5
neigherNum=3
Recommend=286 5.0
neigherNum=4
Recommend=286 5.0
neigherNum=5
Recommend=990 5.0
neigherNum=6
Recommend=990 5.0
neigherNum=7
Recommend=990 5.0
neigherNum=8
Recommend=990 5.0
neigherNum=9
Recommend=990 5.0
解释: neigherhood一开始变化时, 参考的人数增多了, 所谓三个臭皮匠顶过一个诸葛亮, 推荐将会变化, 但是随着neigherhood的变大, 加再多的人进来也只是凑人数而已没有多大的决定能力.
B. 当rankNum从2到10变化时, 感觉上rankNum的改变不应该影响推荐结果.
List<RecommendedItem> recommendations = recommender.recommend(userid,
rankNum);
但是: 我们发现除了neigherNum = 2以外, 推荐结果均发生了变化, 而且数据开始震荡, 如果将neigherNum放大到30, 推荐结果依旧不停地震荡.
neigherNum=2
Recommend=313 4.5
neigherNum=3
Recommend=323 5.0
neigherNum=4
Recommend=898 5.0
neigherNum=5
Recommend=323 5.0
neigherNum=6
Recommend=323 5.0
neigherNum=7
Recommend=898 5.0
neigherNum=8
Recommend=326 5.0
neigherNum=9
Recommend=326 5.0
解释???: 问题应该出在排序算法上, Mahout为了节约内存使用了qSort, 因此排序算法不稳定. 但是我去查看Mahout源代码发现GenericUserBasedRecommender中使用了Collections.sort(), sort默认使用的是MergeSort, 所以排序应该是稳定的. 依旧存在着疑问.
3.2. 针对DataModel做一些数据分析,
类似于博文2, 判断item和user数量, value范围, 方差等.
代码如下:package com.qingfeng.rs.test; import java.io.File; import java.io.IOException; import org.apache.mahout.cf.taste.common.TasteException; import org.apache.mahout.cf.taste.impl.common.LongPrimitiveIterator; import org.apache.mahout.cf.taste.impl.model.file.FileDataModel; import org.apache.mahout.cf.taste.model.DataModel; public class QingDataModelTest { private final static String filename = "data/u.data"; public static void main(String[] args) throws IOException, TasteException { DataModel dataModel = new FileDataModel(new File(filename)); // compute the max and min value // 计算最大最小值 float maxValue = dataModel.getMaxPreference(); float minValue = dataModel.getMinPreference(); // compute the number of usersNum and itemsNum // 计算用户和物品总数 int usersNum = dataModel.getNumUsers(); int itemsNum = dataModel.getNumItems(); int[] itemsNumForUsers = new int[usersNum]; int[] usersNumForItems = new int[itemsNum]; LongPrimitiveIterator userIDs = dataModel.getUserIDs(); int i = 0; while (userIDs.hasNext()) { itemsNumForUsers[i++] = dataModel.getPreferencesFromUser( userIDs.next()).length(); } assert (i == usersNum); LongPrimitiveIterator itemIDs = dataModel.getItemIDs(); i = 0; while (itemIDs.hasNext()) { usersNumForItems[i++] = dataModel.getPreferencesForItem( itemIDs.next()).length(); } assert (i == itemsNum); // compute mean and variance // 计算平均值和方差 double usersMean; double usersVar; int sum = 0; int sqSum = 0; for (int num : itemsNumForUsers) { sum += num; sqSum += num * num; } usersMean = (double) sum / usersNum; double userSqMean = (double) sqSum / usersNum; usersVar = Math.sqrt(userSqMean - usersMean * usersMean); double itemsMean; double itemsVar; sum = 0; sqSum = 0; for (int num : usersNumForItems) { sum += num; sqSum += num * num; } itemsMean = (double) sum / itemsNum; double itemsSqMean = (double) sqSum / itemsNum; itemsVar = Math.sqrt(itemsSqMean - itemsMean * itemsMean); System.out.println("Preference=(" + minValue + ", " + maxValue + ")"); System.out.println("usersNum=" + usersNum + ", userMean=" + usersMean + ", userVar=" + usersVar); System.out.println("itemsNum=" + itemsNum + ", itemsMean=" + itemsMean + ", itemsVar=" + itemsVar); } }
设置门限过滤数据
在代码中加入过滤模块
for (int num : itemsNumForUsers) { sum += num; if (num < 20) { countLower++; // System.out.println("user warning(" + countLower + ")=" + num); } sqSum += num * num; } System.out.println("user warning(" + countLower + ")"); for (int num : usersNumForItems) { sum += num; if (num < 20) { countLower++; //System.out.println("item warning(" + countLower + ")=" + num); } sqSum += num * num; } System.out.println("item warning(" + countLower + ")");
运行结果如下
user warning(0)
item warning(743)
Preference=(1.0, 5.0)
usersNum=943, userMean=106.04453870625663, userVar=100.87821227051644
itemsNum=1682, itemsMean=59.45303210463734, itemsVar=80.3599467406018
分析:与官方的1000个用户, 1700部电影的说法一致. http://www.grouplens.org/datasets/movielens/
user warning(0)
item warning(743) 表示有743个item评分个数小于20.
物品评分较为稀疏程度和物品总数大小是一致的. 使用user-based则用户少,节约内存, 且矩阵致密。
设置门限为20时, 发现物品矩阵稀疏、方差大和过滤器的统计结果item warning(743)大是一致, 此处先不过滤数据, 后期再说.
注:当然优秀的过滤器需要改变门限值来不停的调试
3.3 选择DataModel, 并计算内存使用情况
由于数据有rate, 所以不使用Boolean形式的存储.预估内存开销:
由上文分析可知: Preference ~= usersNum * userMean ~= 100K, 每个Preference消耗28bytes,
预估内存开销= 28bytes * 100K = 2.8 Mbytes. 此外相似矩阵如果使用邻接矩阵方式存储, max{usersNum, itemsNum}**2 * 4bytes(float) = 8Mbytes左右. 因此内存总结开销在10M左右.
[但是查看Mahout源代码org.apache.mahout.cf.taste.impl中相关文件发现, 相似矩阵是临时计算的, 每次recommend时通过重写Estimator接口的estimate方法来具体实现. 可以mahout还是考虑到内存开销, 牺牲了计算速度吧. 所以估计程序运行内存开销依旧在2.8Mbytes左右. 究竟哪个是正确的理解呢?]
因此我使用in-memory形式的GenericDataModel将数据直接加载到内存中.
实验测试内存开销:
通过多次调用System.gc()来回收内存, 通过Rumtime.totalMemory和Runtime.freeMemory()查看内存使用状态.
http://docs.oracle.com/javase/6/docs/api/
代码如下:
public class QingMemoryTest { private static final String filename = "data/u.data"; public static void main(String[] args) throws Exception { DataModel dataModel = new FileDataModel(new File(filename)); UserSimilarity similarity = new PearsonCorrelationSimilarity(dataModel); UserNeighborhood neighborhood = new NearestNUserNeighborhood(5, similarity, dataModel); Recommender recommender = new GenericUserBasedRecommender(dataModel, neighborhood, similarity); System.out.println("1: jvm free-memory= " + Runtime.getRuntime().freeMemory() + "Bytes"); System.gc(); System.out.println("2: jvm free-memory= " + Runtime.getRuntime().freeMemory() + "Bytes"); // dataModel被回收, 所以推荐结果错误. System.out.println(recommender.recommend(1, 2).get(1).getValue()); } }
运行结果如下:
start: jvm used-memory= 0.5967178344726562MB
after dataModel: jvm used-memory= 19.2872314453125MB
after similarity: jvm used-memory= 19.2872314453125MB
after neighborhood: jvm used-memory= 19.58240509033203MB
after recommender: jvm used-memory= 19.58240509033203MB
recommend=340
after recommend first: jvm used-memory= 19.877883911132812MB
after gc: jvm used-memory= 9.829483032226562MB
recommend=340
after recommend second: jvm used-memory= 9.829483032226562MB
分析: 由上述数据可见,gc回收内存后, JVM内存消耗回收了10Mbytes, 与猜测一致.
问题: 回收完数据后, 为什么recommender还可以进行推荐, 而且没有额外的内存开销???
数据增长10倍, 即使用1M数据进行测试
简单统计分析结果:
user warning(0)
item warning(663)
Preference=(1.0, 5.0)
usersNum=6040, userMean=165.5975165562914, userVar=192.73107252940773
itemsNum=3706, itemsMean=269.88909875876953, itemsVar=383.9960197430679
估计内存消耗: usersNum和itemsNum增长了3到6倍, 而相似矩阵消耗内存为平方级别, 那么内存消耗上线为9到36倍; 此外数据增长10倍, DataModel内存消耗为线性增长, 增长10倍内存消耗. 那么估计内存消耗= 2.8M * 10 + (9~36)*8M = 100M ~ 316M内存之间. 如果不存储相似矩阵, 那么内存消耗为28M左右.
由于数据以"::"作为分割符, 用python简单处理一下,替换为\t
f = open("result.dat", "w") for line in open("ratings.dat", "r"): newLine = line.replace("::", "\t") f.write(newLine)
运行结果如下
start: jvm used-memory= 0.5967178344726562MB
after dataModel: jvm used-memory= 204.9770050048828MB
after similarity: jvm used-memory= 204.9770050048828MB
after neighborhood: jvm used-memory= 204.9770050048828MB
after recommender: jvm used-memory= 204.9770050048828MB
recommend=2908
after recommend first: jvm used-memory= 208.10643768310547MB
after gc: jvm used-memory= 76.12030029296875MB
recommend=2908
after recommend second: jvm used-memory= 76.12030029296875MB
分析: 由上述数据可以: 数据回收了132Mbytes, 76M为运行开销. 与估计内存消耗移植. DataModel线性增长, 相似矩阵平方级别增长.
结论: 如果评分数增加到10M级别, 用户或者物品数增长3~10倍, 那么需要4G到40G的内存才能快速的计算出推荐结果, 需要增加内存条, 设置JVM配置以及使用hadoop来实现. 另外真实的数据用户数达到GB级别, 总数达到TB级别, 需要的内存数量和运算量是十分恐怖的. 传统地算法已经无法满足要求, 需要借助Hadoop这种分布式来实现运算.
当然内存不够大, 硬盘可以很大, 处理10M级别以上的推荐数据时, 选择使用MysqlJDBCDataModel来实现存储.
另外: 据数盟的一位Q友说, "淘宝有8kw的商品(记忆也许有出入),用户2亿,多大的矩阵啊". 每次想到这里, 都会默默地闭上双眼, 遥想远方的宇宙, 数据又是多么地浩淼. 在上帝眼中, 我们也许还只是玩过家家, 学1+1的小孩子吧.
3.4. 选择相似性矩阵和调参
此外, 后期希望考虑user-based, item-based, slope-one算法的比较, 同时参考运行时间.相似矩阵选择下面4种
PearsonCorrelationSimilarity EuclideanDistanceSimilarity TanimotoCoefficientSimilarity LogLikeLihoodSimilarity
[ 注:其中EuclideanDistanceSimilarity比较特殊, 它没有实现UserSimilarity接口, 所以不能放到一个Collection<UserSimilarity>容器中 ]
[注: 勿看了org.apache.mahout.math.hadoop.similarity.cooccurrence.measures文件]
参数调整只选择近邻N和threashold
这里给出代码原型, 但是在普通PC上跑100K的数据集都太慢了, 使用intro.csv这个toy数据跑一跑.
N选择[2, 4, 8, ... 64], Threshold选择[0.9, 0.85, ... 0.7];
代码如下:
public class QingParaTest { private final String filename = "data/intro.csv"; private double threshold = 0.95; private int neighborNum = 2; private ArrayList<UserSimilarity> userSims; private final int SIM_NUM = 4; private final int NEIGHBOR_NUM = 64; private final double THRESHOLD_LOW = 0.7; public static void main(String[] args) throws IOException, TasteException { new QingParaTest().valuate(); } public QingParaTest() { super(); this.userSims = new ArrayList<UserSimilarity>(); } private void valuate() throws IOException, TasteException { DataModel dataModel = new FileDataModel(new File(filename)); RecommenderEvaluator evaluator = new AverageAbsoluteDifferenceRecommenderEvaluator(); // populate Similarity populateUserSims(dataModel); int simBest = -1; double scoreBest = 5.0; int neighborBest = -1; double thresholdBest = -1; System.out.println("SIM\tNeighborNum\t\tThreshold\tscore"); for (int i = 0; i < SIM_NUM; i++) { for (neighborNum = 2; neighborNum <= NEIGHBOR_NUM; neighborNum *= 2) { for (threshold = 0.75; threshold >= THRESHOLD_LOW; threshold -= 0.05) { double score = 5.0; QingRecommenderBuilder qRcommenderBuilder = new QingRecommenderBuilder( userSims.get(i), neighborNum, threshold); // Use 70% of the data to train; test using the other 30%. score = evaluator.evaluate(qRcommenderBuilder, null, dataModel, 0.7, 1.0); System.out.println((i + 1) + "\t" + neighborNum + "\t" + threshold + "\t" + score); if (score < scoreBest) { scoreBest = score; simBest = i + 1; neighborBest = neighborNum; thresholdBest = threshold; } } } } System.out.println("The best parameter"); System.out.println(simBest + "\t" + neighborBest + "\t" + thresholdBest + "\t" + scoreBest); } private void populateUserSims(DataModel dataModel) throws TasteException { UserSimilarity userSimilarity = new PearsonCorrelationSimilarity( dataModel); userSims.add(userSimilarity); userSimilarity = new TanimotoCoefficientSimilarity(dataModel); userSims.add(userSimilarity); userSimilarity = new LogLikelihoodSimilarity(dataModel); userSims.add(userSimilarity); userSimilarity = new EuclideanDistanceSimilarity(dataModel); userSims.add(userSimilarity); } } class QingRecommenderBuilder implements RecommenderBuilder { private UserSimilarity userSimilarity; private int neighborNum; private double threshold; public QingRecommenderBuilder(UserSimilarity userSimilarity, int neighborNum, double threshold) { super(); this.userSimilarity = userSimilarity; this.neighborNum = neighborNum; this.threshold = threshold; } @Override public Recommender buildRecommender(DataModel dataModel) throws TasteException { UserNeighborhood neighborhood = new NearestNUserNeighborhood( neighborNum, threshold, userSimilarity, dataModel); return new GenericUserBasedRecommender(dataModel, neighborhood, userSimilarity); } }
运行结果如下:
SIM NeighborNum Threshold score
1 2 0.75 0.4858379364013672
1 2 0.7 NaN
1 4 0.75 0.4676065444946289
1 4 0.7 NaN
1 8 0.75 0.8704338073730469
1 8 0.7 0.014162302017211914
1 16 0.75 NaN
1 16 0.7 0.7338032722473145
1 32 0.75 0.7338032722473145
1 32 0.7 0.4858379364013672
1 64 0.75 NaN
1 64 0.7 1.0
The best parameter
1 8 0.7 0.014162302017211914
分析: 运行最佳的结果为N = 8, Threshold = 0.7 当然, 这个方法, 十分的粗糙, 但是也说明了参数的重要性, 毕竟推荐系统上线了必须有优秀的A\B Test结果, 要不然还不如使用打折, 优惠券来的简单实在.
顺便截一张Mahout in Action上一个真实案例的数据, 如下图所示
item-based与user_based一致, 基本上就是就Similarity, Neighborhood和Recommender的User换成Item即可.
3.5 slope-one
public class SlopeOne { public static void main(String[] args) throws IOException, TasteException { DataModel dataModel = new FileDataModel(new File("data/intro.csv")); RecommenderEvaluator evaluator = new AverageAbsoluteDifferenceRecommenderEvaluator(); double score = evaluator.evaluate(new SlopeOneNoWeighting(), null, dataModel, 0.7, 1.0); System.out.println(score); } } class SlopeOneNoWeighting implements RecommenderBuilder { public Recommender buildRecommender(DataModel model) throws TasteException { DiffStorage diffStorage = new MemoryDiffStorage(model, Weighting.UNWEIGHTED, Long.MAX_VALUE); return new SlopeOneRecommender(model, Weighting.UNWEIGHTED, Weighting.UNWEIGHTED, diffStorage); } }
运行结果为: 1.3571428571428572 当然这个结果意义不大, 因为数据集很小.
四、总结
推荐系统的难点在于各种参数、算法的选择,以及推荐系统整体架构的测试;如果希望搭建商业级别的应用,在数据和架构上所花的时间要比算法调参多一些。五、Similarity和Algorithm相关总结
如何计算相似性:常见的方法如下表所示: Similarity只是描述计算方法, 并不计算并保存相似矩阵.
相似性的基本思路就是不适用欧式距离的, 都得加上权重或者门限来防止交集较小的相似距离.
相似距离(距离越小值越大) | 优点 | 缺点 | 取值范围 |
PearsonCorrelation 类似于计算两个矩阵的协方差 | 不受用户评分偏高 或者偏低习惯影响的影响 | 1. 如果两个item相似个数小于2时 无法计算相似距离. [可以使用item相似个数门限来解决.] 没有考虑两个用户之间的交集大小[使用weight参数来解决] 2. 无法计算两个完全相同的items | [-1, 1] |
EuclideanDistanceSimilarity 计算欧氏距离, 使用1/(1+d) | 使用与评分大小较 重要的场合 | 如果评分不重要则需要归一化, 计算量大 同时每次有数据更新时麻烦 | [-1, 1] |
CosineMeasureSimilarity 计算角度 | 与PearsonCorrelation一致 | [-1, 1] | |
SpearmanCorrelationSimilarity 使用ranking来取代评分的 PearsonCorrelation | 完全依赖评分和完全放弃评分之间的平衡 | 计算rank消耗时间过大 不利于数据更新 | [-1, 1] |
CacheUserSimilarity 保存了一些tag, reference | 缓存经常查询的user-similarity | 额外的内存开销 | |
TanimotoCoefficientSimilarity 统计两个向量的交集占并集的比例 同时并集个数越多, 越相近. | 适合只有相关性 而没有评分的情况 | 没有考虑评分,信息丢失了 | [-1,1] |
LogLikeLihoodSimilarity 是TanimoteCoefficientSimilarity 的一种基于概率论改进 | 计算两者重合的偶然性 考虑了两个item相邻的独特性 | 计算复杂 | [-1,1] |
user-based算法: 最古老的算法, 计算相似的人群, 最大的问题是存储相似矩阵, 由于每个用户喜欢的物品在变化, 导致相似矩阵不停的变化. 更新相似矩阵计算量可能较大. 针对搜索引擎来说, 搜索词如果比用户数目多的话,可以考虑user-based.
item-based算法: 与user-based类似, 每个物品被喜欢的用户个数不停地变化, 相似矩阵持续地更新. 在互联网时代,商品上百万, 用户上亿. 那么使用item-based比较靠谱, 物品相似矩阵变化较小, Amazon的推荐算法就是使用item-based为基础的.
SVD: 现在比较流行的算法, 因为可以进行降维. 发掘有价值的特征维度来取代用户维度或者商品维度. 举个例子: 例如两个人分别喜欢保时捷和法拉利, user-based和item-based计算的相似性都很低, 但是SVD引入跑车或者奢侈品这种潜在的特征后, 两者就有相似性了. 当然缺点在于, SVD需要将整个矩阵加载到内存进行矩阵分解, 对内存消耗大, 不知道SVD的矩阵分解有没有Map-Reduce实现方法.
Slope-One算法: 上述三种算法都不太适合作为在线算法和更新数据, 但是Slope-One可以. 举个例子, 假设所有用户评价电影A比电影B高1.0分, 评价电影C和电影A一致. 如果一个用户评价电影B为2.0分, 评价电影C为4.0分, 那么用户评价电影A为3.0分或者4.0分, 最佳的方法的取两者的加权平均值, 权重由同时出现次数决定. Slope-One可以离线计算所有的n*(n-1)/2中相关性, 当一个用户更新了电影时,
相关性更新快捷; 通过遍历一遍电影即可获得所有电影的评分,从而排序给出推荐. 缺点是相关性计算复杂. [个人觉得这个计算量也不小, 取决于电影个数以及用户评分电影个数]
六、参考资料
[1] Sean Owen "Mahout in Action" http://book.douban.com/subject/4893547/相关文章推荐
- win7系统下文件夹重命名提示"找不到该项目:的解决!
- QT配置问题
- 判断银行账号是否输入正确
- java多线程返回函数结果
- 关于java中byte
- html#1
- 使用Mahout搭建推荐系统之入门篇3-Mahout源码初探
- leetcode刷题日记——Move Zeroes
- 【jQuery】使用append()方法向元素内追加内容
- ToggleButton和Switch改变布局
- java 杂物间 (二) Spring Web
- QT编译子目录项目出现sub-xx-make-first-ordered error5 解决方法
- 【Linux】线程
- 箭头的使用
- 延时加载
- 切换图片,增大减小图片透明度等
- TIPTOP ERP 开发视频教程
- 获取根目录
- ibatis 中的#与$的区别
- 编译安装apache-2.4.18