本文主要是《Mahout算法解析与案例实战》一书(出版于2014年底)的笔记。
概述
Mahout是一个分布式机器学习算法的集合,包括:被称为Taste的分布式协同过滤的实现、分类、聚类等,可用于数据挖掘。Mahout基于Hadoop实现,把很多以前运行于单机上的算法,转化为MapReduce模式,大大提升算法可处理的数据量和处理性能。
Taste引擎提供两种模式:
- 以Jar包形式嵌入到程序里在进程内运行;
- 以MR Job形式在Hadoop集群上运行。
实战
引入如下最新GAV:
<dependency>
<groupId>org.apache.mahout</groupId>
<artifactId>mahout-mr</artifactId>
<version>0.13</version>
</dependency>
测试程序:
public static void main(String[] args) {
try {
DataModel model = new FileDataModel(new File("D:\\mahout\\data.csv"));
// 指定用户相似度计算方法,这里采用皮尔森相关度
UserSimilarity similarity = new PearsonCorrelationSimilarity(model);
// 指定用户邻居数量,这里为2
UserNeighborhood neighborhood = new NearestNUserNeighborhood(2, similarity, model);
// 构建基于用户的推荐系统
Recommender recommender = new GenericUserBasedRecommender(model, neighborhood, similarity);
// 得到指定用户的推荐结果,这里是得到用户1的两个推荐
List<RecommendedItem> recommendations = recommender.recommend(1, 2);
// 打印推荐结果
for (RecommendedItem recommendation : recommendations) {
System.out.println(recommendation);
}
} catch (Exception e) {
System.out.println(e);
}
}
其中data.csv
内容如下(有省略):
1,101,5
逗号分割的三个字段分别是:用户id、商品id,评分。
算法
聚类
Mahout里包含的算法有:
- Canopy:简单、快速,常用于其他聚类算法的初始步骤
- K-Means:最广为人知,一般聚类问题都可以使用此算法。需要提前知道要聚类的数据的类别个数
- Fuzzy K-Means:K-means的扩展,相比于K-Means聚类方法用于发现严格的聚类中心(即一个数据点只属于一个聚类中心),Fuzzy K-Means聚类方法用于发现松散的聚类中心(即一个数据点可能属于几个聚类中心)。
- Mean Shift:最开始应用于图像平滑、图像分割和跟踪方面。不需要提前知道要聚类的类别个数,该算法形成的聚类形状是任意的且与要聚类的数据是相关的
- Spectral:相对于K-Means来说更有效和专业,常用于处理图像谱分类的算法
- Minhash:只负责将原始内容尽量均匀随机地映射为一个签名值,原理上相当于伪随机数产生算法。对于传统hash算法产生的两个签名,如果相等,说明原始内容在一定概率下是相等的;如果不相等,除了说明原始内容不相等外,不再提供任何信息,因为即使原始内容只相差一个字节,所产生的签名也很可能差别极大。从这个意义上来说,要设计一个hash算法,使相似的内容产生的签名也相近,是更为艰难的任务,因为它的签名值除了提供原始内容是否相等的信息外,还能额外提供不相等的原始内容的差异程度的信息。
- Top Down:分层聚类的一种,首先寻找比较大的聚类中心,然后对这些中心进行细粒度分类
Canopy
数据变得很大指的是:
- 数据的条目很多,整个数据集包含的样本数据向量很多;
- 针对①中的每个样本数据向量其维度很大,即包含多个属性;
- 要聚类的中心向量很多。
主要思想是把聚类分为两个阶段:
- 通过使用一个简单、快捷的距离计算方法把数据分为可重叠的子集,称为canopy;
- 通过使用一个精准、严密的距离计算方法来计算出现在阶段一中同一个canopy的所有数据向量的距离。
这种方式和之前的聚类方式不同的地方在于使用两种距离计算方式,同时因为只计算重叠部分的数据向量,所以达到减少计算量的目的。
具体来说,阶段一,使用一个简单距离计算方法来产生具有一定数量的可重叠的子集。canopy就是一个样本数据集的子集,子集中的样本数据是通过一个粗糙的距离计算方法来计算样本数据向量和canopy的中心向量的距离,设定一个距离阈值,当计算的距离小于这个阈值时,就把样本数据向量归为此canopy。每个样本数据向量有可能存在于多个canopy里面,但是每个样本数据向量至少要包含于一个canopy中。canopy的创建基于不存在于同一个canopy中的样本数据向量彼此很不相似,不能被分为同一个类的这样的观点考虑的。由于距离计算方式是粗糙的,因此不能够保证性能(计算精确度)。但是通过允许存在可叠加的canopy和设定一个较大的距离阈值,在某些情况下可以保证该算法的性能。
分类
Mahout包含的算法:
- Logistic Regression:一种利用预测变量(预测变量可以是数值型、离散型)来预测事件出现概率的模型。主要应用于生产欺诈检测、广告质量估计、定位产品预测等。在Mahout中主要使用随机梯度下降(Stochastic Gradient Decent,SGD)思想来实现该算法
- Bayesian:在Mahout中,目前已经有两种实现的贝叶斯分类器,朴素贝叶斯算法,和互补型朴素贝叶斯算法;
- SVM:Support Vector Machine,支持向量机,属于一般化线性分类器,也可以认为是提克洛夫规范化(Tikhonov Regularization)方法的特例。能够同时最小化经验误差与最大化几何边缘区,因此SVM也称为最大边缘区分类器
- RF:Random Forests,随机森林,一个包含多个决策树的分类器,其输出的类别由个别树输出的类别的众数而定。众数是指个别树输出类别重复最多的一个类别数值。RF在决策树的基础上发展而来,继承其优点同时弱化其缺点
- HMM:Hidden Markov Models,隐马尔科夫模型,主要用在机器学习上,如语音识别、手写识别、自然语音处理等。HMM是一个包含两个随机变量O和Y(O和Y可以按照顺序改变它们自身的状态)的分析模型。变量Y是隐含变量,包含{ y 1 , ⋯ , y n {y_1,\cdots,y_n} y1,⋯,yn}个状态,其状态不能被直接检测出来。变量Y的状态按照一定的顺序改变,其状态改变的概率只与当前状态有关而不随时间改变。变量O称为可观察变量,包含{ o 1 , ⋯ , o m {o_1,\cdots,o_m} o1,⋯,om}个状态,其状态可以被直接检测出来。变量O的状态与当前变量Y的状态有关。
协同过滤
也称为推荐算法,Mahout中包括的算法如下:
- Distributed Item-Based Collaborative Filtering:基于项目的协同过滤算法,其简单思想就是利用项目之间的相似度来为用户进行项目推荐。项目之间的相似度通过不同用户对该项目的评分来求出,每个项目都有一个用户向量,两个项目之间的相似度便是根据这个用户向量求得的。求得项目之间的相似度,便可以针对用户对项目的评分清单来推荐与清单中极为相似的项目
- Collaborative Filtering using a parallel matrix factorization:Mahout中带ALS-WR的协同过滤。该算法最核心的思想就是把所有的用户及项目想象成一个二维表格,该表格中有数据的单元格
(
i
,
j
)
(i,j)
(i,j),便是第
i
个用户对第j
个项目的评分,利用该算法使用表格中有数据的单元格来预测为空的单元格。预测得到的数据即为用户对项目的评分,然后按照预测的项目评分从高到低排序进行推荐。
带ALS-WR的协同过滤,主要包括4个过程:
- 分数据集;
- 并行ALS算法,循环计算用户特征矩阵U和项目特征矩阵M;
- 计算用户预测评价矩阵和原始矩阵误差,评价算法模型;
- 利用用户特征矩阵U和项目特征矩阵M进行推荐。
模式挖掘
频繁项集挖掘算法主要是指FP树关联规则算法。传统关联规则算法是根据数据集建立FP树,然后对FP树进行挖掘,得到数据库的频繁项集。在Mahout中实现并行FP树关联规则算法的主要思路是按照一定的规则把数据集分开,然后在每个分开的部分数据集建立FP树,然后再对FP树进行挖掘,得到频繁项集。这里使用的是把数据集分开的规则,可以保证最后通过所有FP树挖掘出来的频繁项集全部加起来没有遗漏,但是会有少量重叠。
其他
其他算法,如回归、进化、向量相似度计算、降维。
降维算法主要有:
- PCA:Principal Component Analysis,主成分分析
- ED:Eigen Decomposition,特征分解,对应Mahout里的Lanczos Algorithm
- SVD:Singular Value Decomposition,奇异值分解
Mahout中还添加Stochastic Singular Value Decomposition with PCA workflow(SSVD)和Gaussian Discriminative Analysis(GDA)两种算法
源码
Recommender
顶级接口:
public interface Recommender extends Refreshable {
// 给用户userID推荐howMany个Item
List<RecommendedItem> recommend(long userID, int howMany) throws TasteException;
// 根据includeKnownItems,给用户userID推荐howMany个Item
List<RecommendedItem> recommend(long userID, int howMany, boolean includeKnownItems) throws TasteException;
// 给userID推荐howMany个Item,可根据rescorer对结果重新排序
List<RecommendedItem> recommend(long userID, int howMany, IDRescorer rescorer) throws TasteException;
// 综合
List<RecommendedItem> recommend(long userID, int howMany, IDRescorer rescorer, boolean includeKnownItems)
throws TasteException;
// 估计用户对物品的喜好度/评分
float estimatePreference(long userID, long itemID) throws TasteException;
// 设置评分
void setPreference(long userID, long itemID, float value) throws TasteException;
// 删除用户对物品的评分
void removePreference(long userID, long itemID) throws TasteException;
// 提取推荐数据
DataModel getDataModel();
}
核心推荐算法,在子类的estimatePreference()方法中进行实现。
其实现类包括:
- GenericUserBasedRecommender:基于用户的推荐器,用户数量少时速度快;
- ItemUserAverageRecommender:
- GenericItemBasedRecommender:基于商品推荐器,商品数量少时速度快,尤其当外部提供商品相似度数据后效率更好;
- ItemAverageRecommender:
- SlopeOneRecommender:基于slope-one算法的推荐器,在线推荐或更新较快,需要事先大量预处理运算,物品数量少时较好;在0.13.0版本中已废弃;
- SVDRecommender:奇异值分解,推荐效果较好,但之前需要大量预处理运算;
- KnnRecommender:基于K近邻算法(KNN),适合于物品数量较小时,已废弃;
- TreeClusteringRecommender:基于聚类的推荐器,在线推荐较快,之前需要大量预处理运算,用户数量较少时效果好;已废弃。
推荐使用GenericUserBasedRecommender,根据传入的DataModel和UserNeighborhood进行推荐,流程分成三步:
- 使用UserNeighborhood获取跟指定用户Ui最相似的K个用户
{U1,…,Uk}
; {U1,…,Uk}
喜欢的item集合中排除掉Ui喜欢的item,得到集合{Item0,…,Itemm}
;- 对
{Item0,…,Itemm}
每个itemj计算Ui可能喜欢的程度值perf(Ui,Itemj)
,并把item按这个数值从高到低排序,把前N个item推荐给Ui。计算公式如下:
其中 是用户Ul对Itemj的喜好值。
GenericItemBasedRecommender
根据传入的DateModel和ItemSimilarity去推荐,item之间的相似度比较固定,相似度可事先算好,大幅提高推荐的速度。其流程分成三步:
- 获取用户Ui喜好的item集合{It1…Itm};
- 使用MostSimilarItemsCandidateItemsStrategy(有多种策略,功能类似UserNeighborhood),获取跟用户喜好集合里每个item最相似的其他Item构成集合 {Item1…Itemk};
- 对
{Item1…Itemk}
里的每个itemj计算Ui可能喜欢的程度值perf(Ui,Itemj)
,并把item按这个数值从高到低排序,把前N个Item推荐给Ui。计算公式如下:
其中 是用户Ul对Iteml的喜好值。
SlopeOneRecommender
SlopeOne,一种简单高效的协同过滤算法,通过均差计算进行评分。适用于用户对item的打分是具体数值的情况。Slopeone算法不同于前面提到的基于相似度的算法,计算简单快速,对新用户推荐效果不错,数据更新和扩展性都很不错,预测能达到和基于相似度的算法差不多的效果,很适合在实际项目中使用。
算法实现 | 参数 | 特性 |
---|---|---|
GenericUserBasedReconnender | 用户相似度矩阵、近邻算法和数量 | 容易实现,当用户数量不大时,计算很快 |
GenericItenBasedReconnender | 物品相似度矩阵 | 当用户数量不大时,计算很快;当构建一个新的物品相似时非常有用 |
SlopeOneRecomnender | 差值数据 | 计算非常快,会有大量的预处理,适用于物品数量较少的场景 |
SYDReconnender | 维度数 | 结果比较好,会有大量的预处理 |
KmnItemBasedReconnender | means的数量、物品相似度矩阵、近邻数量 | 当物品数量较少时,结果比较好 |
TreeClusteringReconmender | 集群节点数据、集群相似度定义、用户相似度矩阵 | 运行快速,有大量的预处理,当用户数量较少时,结果比较好 |
DataModel
顶级接口:
public interface DataModel extends Refreshable, Serializable {
LongPrimitiveIterator getUserIDs() throws TasteException;
PreferenceArray getPreferencesFromUser(long userID) throws TasteException;
FastIDSet getItemIDsFromUser(long userID) throws TasteException;
LongPrimitiveIterator getItemIDs() throws TasteException;
PreferenceArray getPreferencesForItem(long itemID) throws TasteException;
Float getPreferenceValue(long userID, long itemID) throws TasteException;
Long getPreferenceTime(long userID, long itemID) throws TasteException;
int getNumItems() throws TasteException;
int getNumUsers() throws TasteException;
int getNumUsersWithPreferenceFor(long itemID) throws TasteException;
int getNumUsersWithPreferenceFor(long itemID1, long itemID2) throws TasteException;
void setPreference(long userID, long itemID, float value) throws TasteException;
void removePreference(long userID, long itemID) throws TasteException;
boolean hasPreferenceValues();
float getMaxPreference();
float getMinPreference();
}
Mahout为DataModel提供以下几种实现类:
- GenericDataModel
- GenericBooleanPrefDataModel
- PlusAnonymousUserDataModel
- PlusAnonymousConcurrentUserDataModel
- FileDataModel
- JDBCDataModel:接口,可自定义不同数据库实现类
HDFS的DataModel:https://issues.apache.org/jira/browse/MAHOUT-1579
算法评价
评价推荐算法有很多指标,如召回率(recall)与查准率(precision)。
Mahout提供2个评估推荐器的指标,查准率和召回率(查全率),也是搜索引擎中经典的度量方法。
相关与检索矩阵
- | 相关 | 不相关 |
---|---|---|
检索到 | A | C |
未检索到 | B | D |
解读:
- A:检索到的,相关的 (想要的搜到了)
- B:未检索到的,但是相关的 (想要的没搜到)
- C:检索到的,但是不相关的 (搜到了但没用)
- D:未检索到的,也不相关的 (没搜到也没用)
被检索到的越多越好,这是追求查全率,即
A
/
(
A
+
B
)
A/(A+B)
A/(A+B)越大越好。
被检索到的,越相关的越多越好,不相关的越少越好,这是追求查准率,即
A
/
(
A
+
C
)
A/(A+C)
A/(A+C)越大越好。
在大规模数据集合中,这两个指标是相互制约的。当希望索引出更多的数据时,查准率就会下降,当希望索引更准确时,会索引更少的数据。
写在最后
Maven仓库显示mahout-mr
最后一个发布版本停留在2017年4月15日:
也就是说不再推荐使用Mahout。
如果还是要用JVM语言(比如Java、Scala)来做机器学习的话,可考虑Spark提供的MLlib:
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-mllib_2.13</artifactId>
<version>3.5.3</version>
</dependency>
事实上,在机器学习这一领域,Python可谓是最适合的语言,生态也非常丰富:TensorFlow、PyTorch、Scikit-learn。
参考
- Mahout算法解析与案例实战