Math模块简介
Mahout中的math模块自身包含了向量和矩阵数学工具库,可以脱离Mahout单独使用,实际上是CERN的Colt库的一个改编版本.
本部分介绍math模块的关键部分,Vector和Matrix,以及相关的运算.
向量
在Mahout中,向量是在一个更抽象的意义上进行使用,这时的向量并不一定对应某个几何上的解释.可以是一个元组或者有序值列表.向量有长度(或者说是维度).向量中从0到长度减一的每个索引(或位置)上有,某个数值.通常写成(2.0,1.55,0.0)之类的数值列表,表示长度为3,索引1的位置为1.55.
向量实现
math中的Vector是一个应用时才实现的接口,有多种实现.向量的实现方式中,最重要的考虑因素为向量数据的 稀疏度(sparseness) 或 密集度(denseness).
对于长度来说,有较多位置上的值不为0,向量称为密集向量,实现为DenseVector.稀疏向量只存储非零值的索引位置和对应的值,实现为RandomAccessSpareseVector.为了快速访问的实现为SequentialAccessSpareseVector.
向量操作
返回向量长度: size()
访问和修改索引位置对应的值: get(int), set(int, duoble)
无边界检查以优化性能的方式: getQuick(int), setQuick(int, double)
对所有元素的遍历: iterator()
对所有非零元素的遍历: iteratorNonZero()
Vector复制: clone()
返回一个同类的空Vector: like()
标准向量的加减: plus(Vector),minus(Vector)
标准向量对一标量的乘除: times(double),divide(double)
向量的内积(点积): dot(Vector)
两个向量元素相乘得到矩阵: cross(Vector)
高级向量方法
assign(Vector,DoubleDoubleFunction),对Vector进行修改,将其值设置为该Vector和另一个Vector的值上的某个函数的结果.DoubleDoubleFunction封装了一个函数,接收两个输入值,返回一个结果值.
aggregate(DoubleDoubleFunction, DoubleFunction),能够简化基于向量的所有元素值的函数的计算过程.特别是当与Functions中现成的函数结合使用时.比如计算所有向量值的平方和:
double myOtherSumOfSquares(Vector A){
return A.aggregate(Funtions.PLUS, Functions.SQUARE)
}
矩阵
Mahout中矩阵的表示为接口Matrix的实现.将矩阵的行或列看成是向量.SparseMatrix和DenseMatrix分别代表非零元素很少和很多的向量.
SparseRowMatrix和SparseColumnMatrix是两个SparseMatrix的变种,分别应用于矩阵行或列常常当做一个访问单位的情况.
矩阵操作
返回特定行列号的矩阵元素值: get(int, int)
同时返回矩阵的行数和列数: size()
like(),clone(),plus(Matrix),times(Matrix),矩阵转置transpose(),行列计算determiant().
矩阵相乘
只有在第一个矩阵的列数(column)和第二个矩阵的行数(row)相同时才有意义.
结果矩阵的行数等于第一个矩阵的行数,结果矩阵的列数等于第二个矩阵的列数.
乘积的结果第m行第n列的元素,等于第一个矩阵中第m行的元素与第二个矩阵中第n列的元素的乘积之和.
即上面的例子中,A的第一行1,2,3分别与B的第一列1,2,3,第二列4,5,6计算,得到结果C的第一行,然后,A的第二行4,5,6分别与B的第一列1,3,4,第二列4,5,6计算得到C的第二行.
分布式计算
构建共现矩阵
在基于物品的推荐引擎中,他们均依赖于一个ItemSimilarity的实现,它给出了计算任意一对物品间相似度的方法.假设我们要计算出每个物品对之间的相似度,并将其结果导入一个巨大的矩阵.这应该是一个巨大的方阵,行和列的数目等于模型中的物品数.每行以及每列,表示在一个特定物品与其他所有物品之间的相似度.可以把这些行和列看做向量.该矩阵还是沿对角线对称的,因为物品X和Y之间的相似性和物品Y与X之间的相似性是一样的.即行X和列Y上的条目等于行Y和列X上的条目.
这个矩阵描述了物品之间的关联,而不涉及用户.这并非是一个用户-物品的矩阵.
有这样一中矩阵是算法所需要的: 共现矩阵.他不是计算每个物品对之间的相似性,而是计算,在某些用户偏好值列表中每个物品对共同出现的次数,以此来填充矩阵.
比如,有9个用户都为物品X和Y给出了一些偏好,那么X和Y同时出现了9次,两个在任何用户偏好中均未同时出现的物品,其共现次数为0次.而且在概念上,每当用户给出了某一个物品的偏好,就代表该物品与自身共生了一次,不过这个计数并没有什么用.
共现关系与相似性很像:两个物品同时出现得越多,他们越有可能相关或相似.共现矩阵的作用类似于基于物品的非分布式算法中的ItemSimiliarity.
做简单的计数就可以生成这个矩阵,只是注意矩阵中的条目不受偏好值的影响,而偏好值会稍后进行计算.
项 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
---|---|---|---|---|---|---|---|
101 | 5 | 3 | 4 | 4 | 2 | 2 | 1 |
102 | 3 | 3 | 3 | 2 | 1 | 1 | 0 |
103 | 4 | 3 | 4 | 3 | 1 | 2 | 0 |
104 | 4 | 2 | 3 | 4 | 2 | 2 | 1 |
105 | 2 | 1 | 1 | 2 | 2 | 1 | 1 |
106 | 2 | 1 | 2 | 2 | 2 | 2 | 0 |
107 | 1 | 0 | 0 | 1 | 1 | 0 | 1 |
除了行列名,这是一个7X7的方阵,对角线上的值,即一个物品对自身的共现值,对算法没有意义.为了完整作为保留.
计算用户向量
在推荐程序向一个基于矩阵的分布式实现的下一步,我们将一个用户的偏好视为一个向量.
在一个有n个物品的数据模型中,用户偏好就像一个n维向量,每个维度代表一个物品.用户对物品的偏好值为这个向量中值.用户没有表达偏好的物品映射我向量中的0值.这是一个典型的稀疏矩阵,大多数值为0,因为用户通常只对一小部分物品表达偏好.
例如,本例中的用户3,对应的偏好向量为: [2.0, 0.0, 0.0, 4.0, 4.5, 0.0, 5.0],这是对7中物品的偏好值序列.要生成推荐结果,每个用户都需要有这样一个向量.
生成推荐结果
要为用户3生成推荐结果,只需将用户对所有物品的偏好向量,即上一步求得的向量,作为一个列向量,用它乘以共现矩阵.
共现矩阵与一个用户向量的乘积结果是一个向量,维度等于项目的个数.可以从结果向量R中的值直接得到推荐结果,在R中最大的值对应于最佳推荐.
在R中排除掉用户已经表达过用户偏好的结果值,然后就是推荐结果.
计算过程中,R中的第三个条目为矩阵中第三行的向量与列向量U3的点积,即两个向量中每组对应条目对之间的乘积之和:
4(2.0) + 3(0.0) + 4(0.0) + 3(4.0) + 1(4.5) + 2(0.0) + 0(5.0) = 24.5
第三行包含了物品103和所有其他物品之间的共现关系,即,如果物品103和那些用户表达过偏好关的物品存在共现关系,那么它就有可能是用户3喜欢的物品,
即,2.0其实是用户对101物品的评分,但是103与101物品的共现关系为4,即共同出现的次数,共同出现了很多次,则用户对103感兴趣的可能性更大,体现在结果值(各项乘积的和)中的比重也越大.
当物品103总是与用户很喜欢(评分值高低)的物品同时出现(共现次数),这个相加结果就包含了大的共现值和大的偏好值之间的乘积,这会使得总和(R中的对应条目)更大,因此,R中最大的值成为了推荐结果(排除掉已评分的项目).
注意,R中的值并不代表一个估计偏好值,因为他们远远大于1,理想情况下,应该利用一些额外信息将他们归一化为估计偏好值.但是从我们要达成的目标来看,归一化没有必要,因为重要的是推荐的顺序,而不是排序所需要的确切值.
这个算法的各个组件每次仅处理全部数据的一个子集,生成用户向量只是为一个用户搜集全部的偏好值并构建出一个向量,统计共现关系只需要每次检查一个向量,计算作为推荐结果的向量仅需每次加载矩阵的一行或一列.
基于MapReduce实现分布式算法
MapReduce简介
MapReduce是一种思考和计算的方法:
- 输入形式为许多键值对(K1,V1),通常是一个HDFS实例的输入文件
- Map函数作用于每个(K1,V1)对,得到0个或多个与之不同的(K2,V2)
- 为每个K2合并所有的V2
- 为每个K2及其对应的V2调用Reduce函数,得到另一中不同的键值对(K3,V3)
向MapReduce转换:生成用户向量
本例中中的数据并不是采用userID,itemID,preference的形式,而是采用itemID: itemID1 itemID2 itemID3 …的形式(即维基百科中,与一个文章相关的其他链接.把文章看做用户,将相关的链接视为该用户喜欢的物品).
第一个MapReduce会形成用户向量:
- 输入文件被框架视为(Long,String)对,Long指文件中的位置,String的值为文件中的文本行,如: 239 / 98955: 590 22 9059
- 每一行被map函数解析为一个用户ID和集合物品ID,输出新的键值对: 用户ID及其对应的物品ID,这样每个物品ID都有一个用户ID,如: 98955 / 590
- 框架为每个用户ID将所有对应的物品ID收集在一起
- Reduce函数利用全部的物品ID为该用户构建一个向量,并输出这个用户ID,与该用户的偏好向量相对应.向量中的值均为0或1,如: 98955 / [590:1.0, 22:1.0, 9059:1.0]
解析Wikipedia连接文件的Mapper:
public final class WikipediaToItemPrefsMapper extends
Mapper<LongWritable, Text, VarLongWritable, VarLongWritable> {
private static final Pattern NUMBERS = Pattern.compile("(\\d+)");
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
String line = value.toString();
Matcher m = NUMBERS.matcher(line);
m.find();
VarLongWritable userID = new VarLongWritable(Long.parseLong(m.group()));
VarLongWritable itemID = new VarLongWritable();
while (m.find()) {
itemID.set(Long.parseLong(m.group()));
context.write(userID, itemID);
}
}
}
从用户的物品偏好中生成向量的Reducer:
public class WikipediaToUserVectorReducer
extends
Reducer<VarLongWritable, VarLongWritable, VarLongWritable, VectorWritable> {
public void reduce(VarLongWritable userID,
Iterable<VarLongWritable> itemPrefs, Context context)
throws IOException, InterruptedException {
Vector userVector = new RandomAccessSparseVector(Integer.MAX_VALUE, 100);
for (VarLongWritable itemPref : itemPrefs) { // 遍历用户偏好过的所有物品,将每个出现过的物品偏好设置为1.0(因为此类数据没有精确偏好值,出现过即为1,没有则为0)
userVector.set((int) itemPref.get(), 1.0f); // 填充为用户偏好向量 用户ID / [物品1:1.0, 物品2:1.0, 物品3:1.0, ..]
}
context.write(userID, new VectorWritable(userVector));
}
}
向MapReduce转换:实现共现关系
使用第一个MapReduce的输出来计算共现关系:
- 输入时用户ID和对应的用户偏好Vector,例如: 98955 / [590:1.0, 22:1.0, 9059:1.0]
- Map函数根据用户的偏好来决定所有的共现关系,并为每一个共现关系生成一个物品ID对–物品ID对应到物品ID,无论是从一个物品ID1到2,还是从2到1,都会被记录,如: 590/22
- 框架为每个物品搜集与之对应的所有共现关系
- Reducer为每个物品ID统计它收到的全部共现关系,他们可以当做共现矩阵的行货列使用.如: 590/[22:3.0, 95:1.0, 9059:1.0, …]
这个阶段的实际输出为共现矩阵.
计算共现关系的Mapper:
public class UserVectorToCooccurrenceMapper extends
Mapper<VarLongWritable, VectorWritable, IntWritable, IntWritable> {
public void map(VarLongWritable userID, VectorWritable userVector,
Context context) throws IOException, InterruptedException {
Iterator<Vector.Element> it = userVector.get().iterateNonZero();
while (it.hasNext()) { // 遍历用户的偏好向量,取出每一个物品ID
int index1 = it.next().index();
Iterator<Vector.Element> it2 = userVector.get().iterateNonZero();
while (it2.hasNext()) { // 再次取出每一个物品ID
int index2 = it2.next().index();
context.write(new IntWritable(index1), new IntWritable(index2));
} // 将每连个物品组合成一个物品对:
// [物品1/物品2, 物品1/物品3, 物品2/物品1, 物品2/物品3, 物品3/物品1, 物品3/物品2]
}
}
}
计算共生关系的Reducer:
public class UserVectorToCooccurrenceReducer extends
Reducer<IntWritable, IntWritable, IntWritable, VectorWritable> {
public void reduce(IntWritable itemIndex1, // 根据上一步的结果,收集与物品1共现的物品和次数
Iterable<IntWritable> itemIndex2s, Context context)
throws IOException, InterruptedException {
Vector cooccurrenceRow = new RandomAccessSparseVector( // 初始化共现向量
Integer.MAX_VALUE, 100);
for (IntWritable intWritable : itemIndex2s) { // 遍历物品对序列
int itemIndex2 = intWritable.get(); // 取出每一个元素的值,即: 物品1/物品2
cooccurrenceRow.set(itemIndex2, // 在共现向量中累加 物品1/物品2 的共现次数
cooccurrenceRow.get(itemIndex2) + 1.0);
} // 获得完整的物品1的共现向量
context.write(itemIndex1, new VectorWritable(cooccurrenceRow));
}
}
向MapReduce转换:重新思考矩阵乘
现在可以使用用户向量与共现矩阵相乘得到推荐向量,推测推荐结果.但是这个乘法可以使用一种更加高效的方式来做.
传统的矩阵乘是让每一行都去乘用户向量(作为一个列向量),已生成结果R中的元素:
for 共现矩阵中的每一行 i
计算行向量i和用户向量的点积
将点积结果存入R中第i个元素
需要对每一行做一次向量的点积.任何对全部输入进行处理的算法都不是最优的,因为输入可能会超级庞大,甚至无法本地化.相反,矩阵乘可以转化成一种对共现矩阵中列的函数:
将R置为空向量
for 共现矩阵中的每一个列 i
将列向量i和用户向量中的第i个元素相乘
将这个向量加到R上
此时,只要用户向量中的元素i为0,循环就完全被跳过去,因为乘积是零向量并且不会影响结果.于是只要对用户向量的非0元素执行循环即可.得到结果向量的列数等于用户给出偏好的个数,当用户向量稀疏时,它远小于列数总和.
按此方法,算法可以有效的对计算进行分布,可将列向量i输出到所有与之相乘的元素上,乘积可以彼此独立的进行计算可存储.
上面的例子中,共现矩阵作为一个7X7的矩阵(A),用户偏好向量可以可以视作一个特殊的单行矩阵(B):
第一种算法是拿A的每一行与B的每一行向量乘,即点积,进行横向求和求得结果.
第二种算法是拿A的每一列与B的每一列做向量乘,纵向求和得到结果.这个计算过程中,B的每一列是一个单元素,向量与0相乘得到一个零向量直接跳过,省去了一部分计算,而用户向量往往都是比较稀疏的(用户一般只对一小部分物品有偏好),所以更加高效.
向MapReduce转换:通过部分乘积计算矩阵乘
从前面的计算结果中,可以获得共现矩阵的列.因为这个矩阵是对称的,行与列相同,所以输出在理论上被看做做是 行,也可以是 列.这些列将物品ID作为键,算法必须将所有用户向量中的每一列和该物品中的每一个非领的偏好值相乘.也就是说,它必须将物品ID和偏好ID及偏好值对应起来,并在Reducer中汇聚在一起.在将每个值都与这个共现向量的列相乘之后,就会生成一个向量,形成面向用户的推荐向量的一部分.
难点在于要合并两种不同的数据: 共现向量和用户偏好值.但是Reducer中的值只能为Writable这一种类型,因此巧妙的实现了VectorOrPrefWritable,它含有一种或另一种类型.
这个Map阶段实际包含了两个Mapper,每个产生不同的Reducer输入:
- 第一个Mapper的输入为共现矩阵: 以物品ID为键,对应于Vector形式的列.如: 590 / [22:3.0, 95:1.0, 9059:1.0, …], Map函数简单的转发其输入,但形式上采用以VectorOrPrefWritable封装的Vector
- 第二个Mapper的输入为用户向量: 以用户ID为键,对应于Vector形式的偏好值,如: 98955/[590:1.0, 22:1.0, 9059:1.0],对于用户向量中的每个非零值,Map函数输出一个物品ID,对应的用户ID,偏好值.以VectorOrPrefWritable的形式封装,如: 590 / [98955:1.0]. 框架按照物品ID将共生关系列和所有的 用户ID-偏好值对 汇聚在一起.reducer将这些信息归并为一条输出记录并存储下来.
以VectorOrPrefWritable封装的共现关系列:
public class CooccurrenceColumnWrapperMapper extends
Mapper<IntWritable, VectorWritable, IntWritable, VectorOrPrefWritable> {
public void map(IntWritable key, VectorWritable value, Context context)
throws IOException, InterruptedException {
context.write(key, new VectorOrPrefWritable(value.get()));
}
}
用户向量被分割为其独立的偏好值和输出(根据物品ID,而非用户ID):
public class UserVectorSplitterMapper
extends
Mapper<VarLongWritable, VectorWritable, IntWritable, VectorOrPrefWritable> {
public void map(VarLongWritable key, VectorWritable value, Context context)
throws IOException, InterruptedException {
long userID = key.get(); // 获取用户ID
Vector userVector = value.get(); // 获取用户向量
Iterator<Vector.Element> it = userVector.iterateNonZero(); // 用户向量中的非0向量
IntWritable itemIndexWritable = new IntWritable();
while (it.hasNext()) { // 遍历用户向量中的所有元素
Vector.Element e = it.next();
int itemIndex = e.index(); // 获取物品ID
float preferenceValue = (float) e.get(); // 获取物品偏好值
itemIndexWritable.set(itemIndex); // 将物品ID作为键
context.write(itemIndexWritable, // 将用户ID和偏好值的对作为向量元素
new VectorOrPrefWritable(userID, preferenceValue));
}
}
}
在两个Mapper之后并没有真正的Reducer,因为不能把两个Mapper的输出导入到一个Reducer中.相反,他们独立运行,并将结果传递到一个空的Reducer中,最终保存在两个位置.这两个位置可以作为另一个MapReduce的输入,它的Mapper什么也不做,而Reducer将物品的一个共现关系向量,并和该物品对应的所有用户偏好及偏好值汇聚在一起形成一个实体,称为VectorAndPrefsWritable.该过程在ToVectorAndPrefReducer中实现.
有了共现矩阵的列和用户偏好,且他们均以物品ID为键,算法就可以将他们导入到一个mapper中,并输出该列和用户偏好的乘积:
- mapper的输入是按物品组织的所有共现矩阵和用户偏好,如: 590 / [22:3.0, 95:1.0, 9059:1.0,..]和 590 / [98955:1.0]
- mapper的输出是共现关系列乘以每个用户的偏好值,如: 590 / [22:3.0, 95:1.0, 9059:1.0, ..]
- 框架按用户将这一乘积汇聚在一起
- reducer将输入的所有向量拆开后求和,形成对该用户的最终推荐向量R,如: 590 / [22:4.0, 45:3.0, 95:11.0, 9059:10, ..]
计算部分推荐向量:
public class PartialMultiplyMapper
extends
Mapper<IntWritable, VectorAndPrefsWritable, VarLongWritable, VectorWritable> {
public void map(IntWritable key,
VectorAndPrefsWritable vectorAndPrefsWritable, Context context)
throws IOException, InterruptedException {
Vector cooccurrenceColumn = vectorAndPrefsWritable.getVector(); // 获取共线向量的每一列
List<Long> userIDs = vectorAndPrefsWritable.getUserIDs(); // 获取用户ID
List<Float> prefValues = vectorAndPrefsWritable.getValues(); // 获取该用户 物品与偏好值的对
for (int i = 0; i < userIDs.size(); i++) {
long userID = userIDs.get(i);
float prefValue = prefValues.get(i);
Vector partialProduct = cooccurrenceColumn.times(prefValue); // 共现向量的列与偏好值相乘
context.write(new VarLongWritable(userID), // 生成该用户结果的一行R
new VectorWritable(partialProduct));
}
}
}
这个mapper会写很多数据,对于每个 用户-物品 关联,它都会输出共现矩阵中一个完整列的副本.这些副本要在reducer中与其他的副本组合相加,以生成一个推荐向量.
combiner,像一个小型的reducer,当map的输出仍在内存中时执行,在输出记录未被执行写操作之前将几个记录合并为一个,以节省IO.这时,对一个用户输出两个向量A和B,就和对这个用户输出一个A+B一样,他们最后都会被合并在一起.
下面的combiner处理PartialMultiplyMapper的输出:
public class AggregateCombiner
extends
Reducer<VarLongWritable, VectorWritable, VarLongWritable, VectorWritable> {
public void reduce(VarLongWritable key, Iterable<VectorWritable> values,
Context context) throws IOException, InterruptedException {
Vector partial = null;
for (VectorWritable vectorWritable : values) { // 对上面的每一行进行向量加(+)操作,获得推荐结果R
partial = partial == null ? vectorWritable.get() : partial
.plus(vectorWritable.get());
}
context.write(key, new VectorWritable(partial));
}
}
向MapReduce转换:形成推荐
算法为每个用户合并推荐向量,生成推荐结果:
public class AggregateAndRecommendReducer
extends
Reducer<VarLongWritable, VectorWritable, VarLongWritable, RecommendedItemsWritable> {
private int recommendationsPerUser = 10;
private OpenIntLongHashMap indexItemIDMap;
static final String ITEMID_INDEX_PATH = "itemIDIndexPath";
static final String NUM_RECOMMENDATIONS = "numRecommendations";
static final int DEFAULT_NUM_RECOMMENDATIONS = 10;
protected void setup(Context context) throws IOException {
Configuration jobConf = context.getConfiguration();
recommendationsPerUser = jobConf.getInt(NUM_RECOMMENDATIONS,
DEFAULT_NUM_RECOMMENDATIONS);
indexItemIDMap = TasteHadoopUtils.readItemIDIndexMap(
jobConf.get(ITEMID_INDEX_PATH), jobConf);
}
public void reduce(VarLongWritable key, Iterable<VectorWritable> values,
Context context) throws IOException, InterruptedException {
Vector recommendationVector = null;
for (VectorWritable vectorWritable : values) {
recommendationVector = recommendationVector == null ? vectorWritable
.get() : recommendationVector.plus(vectorWritable.get());
}
Queue<RecommendedItem> topItems = new PriorityQueue<RecommendedItem>(
recommendationsPerUser + 1,
Collections.reverseOrder(ByValueRecommendedItemComparator
.getInstance()));
Iterator<Vector.Element> recommendationVectorIterator = recommendationVector
.iterateNonZero();
while (recommendationVectorIterator.hasNext()) {
Vector.Element element = recommendationVectorIterator.next();
int index = element.index();
float value = (float) element.get();
if (topItems.size() < recommendationsPerUser) {
topItems.add(new GenericRecommendedItem(indexItemIDMap
.get(index), value));
} else if (value > topItems.peek().getValue()) {
topItems.add(new GenericRecommendedItem(indexItemIDMap
.get(index), value));
topItems.poll();
}
}
List<RecommendedItem> recommendations = new ArrayList<RecommendedItem>(
topItems.size());
recommendations.addAll(topItems);
Collections.sort(recommendations,
ByValueRecommendedItemComparator.getInstance());
context.write(key, new RecommendedItemsWritable(recommendations));
}
}
输出最后以一个或多个压缩文本的形式存放在HDFS上,格式为:
3 [103:24.5, 102:18.5, 106:16.5]
每个用户ID之后跟随一个以逗号分隔的物品ID列表,冒号后为推荐向量中的条目(不管其是否有用).
考虑推荐的非传统用法
虽然Mahout推荐引擎中的API都是以用户或物品进行描述,但这个框架并不假设用户就是人,或者物品就是电影或书籍.
- 无物品推荐用户: 通过物品ID与用户ID进行交换,推荐引擎的输出变为: 哪些用户会对指定的物品更感兴趣
- 扩展物品的范畴: 给定与用户关联的地方,时间,使用模式或者其他人,就可以为之推荐相同类型的物品(地方,时间,使用模式或者其他人)
- 找到最相似的物品: Mahout中基于物品的推荐程序实现使得发现一组最相似的物品变的更容易
- 扩展偏好值的范畴: 通常无法从用户那里获得准确的偏好值,只能根据了解到的用户对事物的关系来推测
- 考虑不止一个用户或物品: 可以为一对用户进行推荐,即把一对用户视为一个用户,还可以把物品及其位置统一视为一个物品来进行推荐
重要的是基于用户的行为或其他数据来推测用户的评分,但并非Mahout关注的内容.Orz.