上次我们讲到als对象train方法的
val solver = if (nonnegative) new NNLSSolver else new CholeskySolver
接下来是
val blockRatings = partitionRatings(ratings, userPart, itemPart)//有个方法partitionRatings,传入数据源,user的HashPartitioner,item的HashPartitioner,并缓存
.persist(intermediateRDDStorageLevel)
我们来看下partitionRatings
/**
* Partitions raw ratings into blocks.//把原始ratings分块
*
* @param ratings raw ratings//原始ratings
* @param srcPart partitioner for src IDs//源HashPartitioner
* @param dstPart partitioner for dst IDs//目的HashPartitioner
*
* @return an RDD of rating blocks in the form of ((srcBlockId, dstBlockId), ratingBlock)//以((srcBlockId, dstBlockId), ratingBlock)形式返回rating
*/
private def partitionRatings[ID: ClassTag](
ratings: RDD[Rating[ID]],
srcPart: Partitioner,
dstPart: Partitioner): RDD[((Int, Int), RatingBlock[ID])] = {
/* The implementation produces the same result as the following but generates less objects.//下面的实现结果一样但是产生更少的对象
ratings.map { r =>
((srcPart.getPartition(r.user), dstPart.getPartition(r.item)), r)//ratings的每条记录转成(user的HashPartitioner索引,item的HashPartitioner索引,rating)的形式
}.aggregateByKey(new RatingBlockBuilder)(//然后用aggregateByKey聚合,(srcPart.getPartition(r.user), dstPart.getPartition(r.item))是k,,r是value,RatingBlockBuilder是初始值,我们先进入RatingBlockBuilder
/**
* Builder for [[RatingBlock]]. [[mutable.ArrayBuilder]] is used to avoid boxing/unboxing.//构建[RatingBlock],使用mutable.ArrayBuilder避免装箱/拆箱
*/
private[recommendation] class RatingBlockBuilder[@specialized(Int, Long) ID: ClassTag]//@specialized(Int, Long) ID: ClassTag代表运行时确定推断类型
extends Serializable {
private val srcIds = mutable.ArrayBuilder.make[ID]//创建一个空的可变数组srcIds ,这种创建方法适合只声明元素类型的方式
private val dstIds = mutable.ArrayBuilder.make[ID]//创建一个空的可变数组dstIds
private val ratings = mutable.ArrayBuilder.make[Float]//创建一个空的可变数组ratings
var size = 0
/** Adds a rating. */
def add(r: Rating[ID]): this.type = {//加一个Rating[ID],数组长度size自增,srcIds 可变数组加元素r.user,dstIds 可变数组加元素r.item,ratings 可变数组加元素 r.rating,返回当前对象,这样当前对象就有了加的这个对象的信息
size += 1
srcIds += r.user
dstIds += r.item
ratings += r.rating
this
}
/** Merges another [[RatingBlockBuilder]]. */
def merge(other: RatingBlock[ID]): this.type = {//合并另一个RatingBlockBuilder,数组长度增加,两个srcIds数组合并,两个dstIds 数组合并,两个ratings 数组合并,返回当前对象
size += other.srcIds.length
srcIds ++= other.srcIds
dstIds ++= other.dstIds
ratings ++= other.ratings
this
}
/** Builds a [[RatingBlock]]. */
def build(): RatingBlock[ID] = {//构建RatingBlock[ID] 对象,传入参数,.result()方法把不可变数组变为可变数组
RatingBlock[ID](srcIds.result(), dstIds.result(), ratings.result())
}
}
回到aggregateByKey,下面就是
}.aggregateByKey(new RatingBlockBuilder)(//(srcPart.getPartition(r.user), dstPart.getPartition(r.item))是k,,r是value,RatingBlockBuilder是初始值
seqOp = (b, r) => b.add(r),//这里面b是RatingBlockBuilder,r是Rating[ID],把r的user,item,rating加到RatingBlockBuilder的数组中,这里每个value都会产生一个RatingBlockBuilder,印证了注释,所以没有采用这种方法
combOp = (b0, b1) => b0.merge(b1.build()))//相同key的RatingBlockBuilder合并,.build()是RatingBlockBuilder的方法
.mapValues(_.build())aggregateByKey的最终结果也是k-value类型,通过mapValues取value,通过_.build()转成RatingBlock[ID] ,RatingBlock成员分别是srcIds,dstIds,ratings数组
*/
我们看看更优的方法
val numPartitions = srcPart.numPartitions * dstPart.numPartitions//分块数乘积
ratings.mapPartitions { iter =>//分块计算
val builders = Array.fill(numPartitions)(new RatingBlockBuilder[ID])每个分块生成一个RatingBlockBuilder数组,长度是分块乘积数
iter.flatMap { r =>//拿出每个rating,由于是flatMap,最终结果是一个数组
val srcBlockId = srcPart.getPartition(r.user)//确定rating中user的srcBlockId
val dstBlockId = dstPart.getPartition(r.item)//确定rating中item的dstBlockId
val idx = srcBlockId + srcPart.numPartitions * dstBlockId//组成便宜索引,看来还是数组好使
val builder = builders(idx)//取出对应的RatingBlockBuilder
builder.add(r)//和并单个Rating[ID]
if (builder.size >= 2048) { // 2048 * (3 * 4) = 24k//如果RatingBlockBuilder数组的size超过2048,2048*3(3个数组)*4(数组每个元素4个字节)=24k
builders(idx) = new RatingBlockBuilder//超过就把数组对应位置的RatingBlockBuilder弄成新的,又有新的空间,旧的弄成RatingBlock[ID]返回成为结果数组的一个元素,如果没超过相当于无返回结果,这个感觉好牛逼,一下控制了RatingBlockBuilder对象的数量
Iterator.single(((srcBlockId, dstBlockId), builder.build()))
} else {
Iterator.empty
}
} ++ {//这里++的原因是flatMap 里只包含超过2048的builder,最终不满2048的还没有处理,这里就在处理不满2048的builder
builders.view.zipWithIndex.filter(_._1.size > 0).map { case (block, idx) =>//取出size>0的builder
val srcBlockId = idx % srcPart.numPartitions//获取要处理的builder对应的srcBlockId ,dstBlockId
val dstBlockId = idx / srcPart.numPartitions
((srcBlockId, dstBlockId), block.build())//和flatMap 元素同样的数据结构,最终合并,我再加个牛逼,以前确实没见过这种写法
}
}
}.groupByKey().mapValues { blocks =>//聚合只操作builder
val builder = new RatingBlockBuilder[ID]//由于blocks是iterator,所以聚合成一个builder,最终的结果就是RatingBlock[ID](srcIds.result(), dstIds.result(), ratings.result())构成的rdd,这里可以感受到一个套路,mapPartitions后groupByKey再把每个 iterator合并是一种高效的方法
blocks.foreach(builder.merge)
builder.build()
}.setName("ratingBlocks")
}
回到train方法,
val (userInBlocks, userOutBlocks) =
makeBlocks("user", blockRatings, userPart, itemPart, intermediateRDDStorageLevel)//这里又有一个makeBlocks方法,传入"user"字符串,blockRatings上一步得到的RDD[((Int, Int), ALS.RatingBlock[ID])] ,userPart用户HashPartitioner,itemPart项目itemPart,缓存级别,我们先看下makeBlocks方法
/**
* Creates in-blocks and out-blocks from rating blocks.//从rating blocks创建in-blocks和out-blocks,具体in-blocks和out-blocks是什么我也不知道,过完这个方法就清楚了
* @param prefix prefix for in/out-block names//前缀
* @param ratingBlocks rating blocks//传入的ratingBlocks
* @param srcPart partitioner for src IDs//源HashPartitioner
* @param dstPart partitioner for dst IDs//目的HashPartitioner
* @return (in-blocks, out-blocks)//返回 (in-blocks, out-blocks)元组
*/
private def makeBlocks[ID: ClassTag](
prefix: String,
ratingBlocks: RDD[((Int, Int), RatingBlock[ID])],
srcPart: Partitioner,
dstPart: Partitioner,
storageLevel: StorageLevel)(
implicit srcOrd: Ordering[ID]): (RDD[(Int, InBlock[ID])], RDD[(Int, OutBlock)]) = {//有个排序的隐式参数
val inBlocks = ratingBlocks.map {
case ((srcBlockId, dstBlockId), RatingBlock(srcIds, dstIds, ratings)) =>//展开ratingBlocks,源HashPartitioner的id,目的HashPartitioner的id,RatingBlock成员用户id数组,项目id数组,评分数组
// The implementation is a faster version of//下面的实现是val dstIdToLocalIndex = dstIds.toSet.toSeq.sorted.zipWithIndex.toMap的更高效版本,我就喜欢看到这句话
// val dstIdToLocalIndex = dstIds.toSet.toSeq.sorted.zipWithIndex.toMap//把dstIds按升序排列并构成dstIds和排序索引的map,我们看下更高效的实现
val start = System.nanoTime()//记录时间
val dstIdSet = new OpenHashSet[ID](1 << 20)//弄一个OpenHashSet,大小是1向左移动20位
dstIds.foreach(dstIdSet.add)//如果插入操作超过集合大小,默认是OpenHashSet的元素个数0.7时,集合大小*2,重新hash所有元素,这里面蛮复杂的,因为高手用的都是位操作和数组操作,这句的作用是把用户id数组弄到OpenHashSet中
val sortedDstIds = new Array[ID](dstIdSet.size)//根据OpenHashSet元素数生成数组
var i = 0
var pos = dstIdSet.nextPos(0)//返回OpenHashSet中下一个元素的位置
while (pos != -1) {//如果有值,获取这个值放入sortedDstIds 数组,如此循环
sortedDstIds(i) = dstIdSet.getValue(pos)
pos = dstIdSet.nextPos(pos + 1)
i += 1
}
assert(i == dstIdSet.size)
Sorting.quickSort(sortedDstIds)//升序排列sortedDstIds
val dstIdToLocalIndex = new OpenHashMap[ID, Int](sortedDstIds.length)//根据sortedDstIds长度弄了一个OpenHashMap
i = 0
while (i < sortedDstIds.length) {
dstIdToLocalIndex.update(sortedDstIds(i), i)//把sortedDstIds数组弄到dstIdToLocalIndex这个OpenHashMap
i += 1
}
logDebug(
"Converting to local indices took " + (System.nanoTime() - start) / 1e9 + " seconds.")//统计耗时
val dstLocalIndices = dstIds.map(dstIdToLocalIndex.apply)//取出与dstIds对应的索引,搞这么半天就是为了保证dstIds不变,求出dstIds的升序索引
(srcBlockId, (dstBlockId, srcIds, dstLocalIndices, ratings))//和开始一样,只是多了一个dstLocalIndices,其实这里优化主要是用了OpenHashSet,OpenHashMap,还有quickSort
}.groupByKey(new ALSPartitioner(srcPart.numPartitions))//这里又做了一个groupByKey,和平时用的不太一样传入了指定分区数的partitions,我们梳理一下结果,结果是相同srcBlockId的value都放到一个Iterable里,最终key是srcBlockId,代表用户分块索引,value是可迭代元组,元素第一个成员是对应产品分块索引,第二个成员是分块内用户id数组,第三个是对应产品id按次数降序的索引,最后一个是和分块内用户数组顺序对应的评分数组,由于groupByKey性能较低,这里可以说是算法性能的瓶颈,下面又有一个新类UncompressedInBlockBuilder,我们先看这是干什么的
/**
* Builder for uncompressed in-blocks of (srcId, dstEncodedIndex, rating) tuples.//构建元组的非压缩内链
* @param encoder encoder for dst indices//参数是目标索引的编码器
*/
private[recommendation] class UncompressedInBlockBuilder[@specialized(Int, Long) ID: ClassTag](
encoder: LocalIndexEncoder)(
implicit ord: Ordering[ID]) {
private val srcIds = mutable.ArrayBuilder.make[ID]//构建源id数组
private val dstEncodedIndices = mutable.ArrayBuilder.make[Int]//构建目标编码索引数组
private val ratings = mutable.ArrayBuilder.make[Float]//构建评分数组
/**
* Adds a dst block of (srcId, dstLocalIndex, rating) tuples.//第一个方式是add,添加(srcId, dstLocalIndex, rating)元组
*
* @param dstBlockId dst block ID//目标分块索引
* @param srcIds original src IDs//源id数组
* @param dstLocalIndices dst local indices//目标本地索引数组
* @param ratings ratings//评分数组
*/
def add(
dstBlockId: Int,
srcIds: Array[ID],
dstLocalIndices: Array[Int],
ratings: Array[Float]): this.type = {
val sz = srcIds.length//源id数组长度,要和目标本地索引数组,评分数组长度一致
require(dstLocalIndices.length == sz)
require(ratings.length == sz)
this.srcIds ++= srcIds//把当前对象的源id数组合并
this.ratings ++= ratings//把当前对象的评分数组合并
var j = 0
while (j < sz) {//遍历
this.dstEncodedIndices += encoder.encode(dstBlockId, dstLocalIndices(j))//这个encoder就是LocalIndexEncoder,我们在笔记一详细讲过,传入目的分块索引和目的本地索引数组元素放到声明的dstEncodedIndices数组
j += 1
}
this
}
/** Builds a [[UncompressedInBlock]]. *///第二个方法build,构建UncompressedInBlock
def build(): UncompressedInBlock[ID] = {
new UncompressedInBlock(srcIds.result(), dstEncodedIndices.result(), ratings.result())
}
}
我们看下UncompressedInBlock这个类
/**
* A block of (srcId, dstEncodedIndex, rating) tuples stored in primitive arrays.//存储元组(srcId, dstEncodedIndex, rating)的数组
*/
private[recommendation] class UncompressedInBlock[@specialized(Int, Long) ID: ClassTag](
val srcIds: Array[ID],
val dstEncodedIndices: Array[Int],
val ratings: Array[Float])(
implicit ord: Ordering[ID]) {
/** Size the of block. */
def length: Int = srcIds.length//srcId数组长度
/**
* Compresses the block into an [[InBlock]]. The algorithm is the same as converting a//压缩block,类似把坐标数组形式的稀疏矩阵转化成压缩稀疏列形式
* sparse matrix from coordinate list (COO) format into compressed sparse column (CSC) format.//使用spark内置时间排序避免产生过多对象
* Sorting is done using Spark's built-in Timsort to avoid generating too many objects.
*/
def compress(): InBlock[ID] = {
val sz = length
assert(sz > 0, "Empty in-link block should not exist.")
sort()//先排序,调用后面的方法,搞完以后UncompressedInBlock成员数组都是按用户id升序排列的且其他数组保持与用户数组对应关系
val uniqueSrcIdsBuilder = mutable.ArrayBuilder.make[ID]//搞两个可变数组
val dstCountsBuilder = mutable.ArrayBuilder.make[Int]
var preSrcId = srcIds(0)
uniqueSrcIdsBuilder += preSrcId//把第一个用户id放入uniqueSrcIdsBuilder
var curCount = 1
var i = 1
var j = 0
while (i < sz) {//遍历srcIds,不重复的放入uniqueSrcIdsBuilder,重复的把重复次数放入dstCountsBuilder,这样就压缩了
val srcId = srcIds(i)
if (srcId != preSrcId) {
uniqueSrcIdsBuilder += srcId
dstCountsBuilder += curCount
preSrcId = srcId
j += 1//这里j明显没用啊,这个不知道是不是失误?!
curCount = 0
}
curCount += 1
i += 1
}
dstCountsBuilder += curCount//把最后一个curCount放入dstCountsBuilder
val uniqueSrcIds = uniqueSrcIdsBuilder.result()//转成不可变数组
val numUniqueSrdIds = uniqueSrcIds.length//唯一用户数组的长度
val dstCounts = dstCountsBuilder.result()//转成不可变数组
val dstPtrs = new Array[Int](numUniqueSrdIds + 1)//声明一个长度+1的数组
var sum = 0
i = 0
while (i < numUniqueSrdIds) {//每个元素放累计数
sum += dstCounts(i)
i += 1
dstPtrs(i) = sum
}
InBlock(uniqueSrcIds, dstPtrs, dstEncodedIndices, ratings)//压缩后返回InBlock,InBlock是一个case class,成员是唯一用户id数组,和唯一用户id数据对应的累计条数数组,后两个参数没变
}
private def sort(): Unit = {
val sz = length
// Since there might be interleaved log messages, we insert a unique id for easy pairing.
val sortId = Utils.random.nextInt()//随机数,日志里会用到
logDebug(s"Start sorting an uncompressed in-block of size $sz. (sortId = $sortId)")
val start = System.nanoTime()
val sorter = new Sorter(new UncompressedInBlockSort[ID])//先看下UncompressedInBlockSort,这里Sorter用的是java的timSort,
timSort排序的输入的单位不是一个个单独的数字了,而一个个的分区。其中每一个分区我们叫一个“run“。针对这个 run 序列,每次我们拿一个 run 出来进行归并。每次归并会将两个 runs 合并成一个 run。归并的结果保存到 "run_stack" 上。如果我们觉得有必要归并了,那么进行归并,直到消耗掉所有的 runs。这时将 run_stack 上剩余的 runs 归并到只剩一个 run 为止。这时这个仅剩的 run 即为我们需要的排好序的结果 分区:为了在已经按升序排好序的输入面前减少回溯,我们把输入当中已经有序的这些段分组,使得它们成为一个基本单元,这样我们就不必在这个基本单元内部浪费时间进行回溯了。比如[1, 2, 3, 2] 进行分区后就变成了 [[1, 2, 3], [2]] 减少回溯:为了在已经按降序排好序的输入面前避免归并排序倒退成 O(n^2),我们把输入当中降序的部分翻转成升序,也作为一个单元。比如 [3, 2, 1, 3] 进行分区后就变成了 [[1, 2, 3], [3]]
/**
* [[SortDataFormat]] of [[UncompressedInBlock]] used by [[Sorter]].//使用Sorter排序的UncompressedInBlock的数据形式
*/
private class UncompressedInBlockSort[@specialized(Int, Long) ID: ClassTag](
implicit ord: Ordering[ID])
extends SortDataFormat[KeyWrapper[ID], UncompressedInBlock[ID]] {//继承SortDataFormat,SortDataFormat有两个参数,第一个是KeyWrapper,KeyWrapper只是主键的封装类,第二个是UncompressedInBlock,之前提到过
override def newKey(): KeyWrapper[ID] = new KeyWrapper()//复写newKey
override def getKey(//复写getKey,参数是UncompressedInBlock,位置索引,KeyWrapper
data: UncompressedInBlock[ID],
pos: Int,
reuse: KeyWrapper[ID]): KeyWrapper[ID] = {
if (reuse == null) {//如果没有KeyWrapper,声明KeyWrapper,把对应位置的用户id设为key,有KeyWrapper就放入传入的KeyWrapper
new KeyWrapper().setKey(data.srcIds(pos))
} else {
reuse.setKey(data.srcIds(pos))
}
}
override def getKey(//复写getKey的另外一种形式
data: UncompressedInBlock[ID],
pos: Int): KeyWrapper[ID] = {
getKey(data, pos, null)
}
private def swapElements[@specialized(Int, Float) T](//数组元素交换
data: Array[T],
pos0: Int,
pos1: Int): Unit = {
val tmp = data(pos0)
data(pos0) = data(pos1)
data(pos1) = tmp
}
override def swap(data: UncompressedInBlock[ID], pos0: Int, pos1: Int): Unit = {//UncompressedInBlock成员数组元素交换,注意是同时交换,对应关系保持不变
swapElements(data.srcIds, pos0, pos1)
swapElements(data.dstEncodedIndices, pos0, pos1)
swapElements(data.ratings, pos0, pos1)
}
override def copyRange(//复写拷贝方法,都是把第一个参数数组从第二个参数位置,拷贝到第三个参数数组的第四个参数位置,第五个参数是拷贝长度
src: UncompressedInBlock[ID],
srcPos: Int,
dst: UncompressedInBlock[ID],
dstPos: Int,
length: Int): Unit = {
System.arraycopy(src.srcIds, srcPos, dst.srcIds, dstPos, length)
System.arraycopy(src.dstEncodedIndices, srcPos, dst.dstEncodedIndices, dstPos, length)
System.arraycopy(src.ratings, srcPos, dst.ratings, dstPos, length)
}
override def allocate(length: Int): UncompressedInBlock[ID] = {//分配长度,生成UncompressedInBlock,成员数组长度都为参数长度
new UncompressedInBlock(
new Array[ID](length), new Array[Int](length), new Array[Float](length))
}
override def copyElement(//拷贝元素,把源UncompressedInBlock第srcPos个位置的元素拷贝到目的UncompressedInBlock的第dstPos个位置
src: UncompressedInBlock[ID],
srcPos: Int,
dst: UncompressedInBlock[ID],
dstPos: Int): Unit = {
dst.srcIds(dstPos) = src.srcIds(srcPos)
dst.dstEncodedIndices(dstPos) = src.dstEncodedIndices(srcPos)
dst.ratings(dstPos) = src.ratings(srcPos)
}
}
回到UncompressedInBlock类的sort方法
sorter.sort(this, 0, length, Ordering[KeyWrapper[ID]])//排序当前对象,比较器是Ordering[KeyWrapper[ID]]
val duration = (System.nanoTime() - start) / 1e9
logDebug(s"Sorting took $duration seconds. (sortId = $sortId)")
}
}
介绍完了UncompressedInBlock,我们回到UncompressedInBlockBuilder,UncompressedInBlockBuilder最后一个方法buile返回的就是UncompressedInBlock,UncompressedInBlockBuilder也就介绍完了,我们回到makeBlocks方法
.mapValues { iter => //mapValues的数据结构是RDD[(Int, Iterable[(Int, Array[ID], Array[Int], Array[Float])])],这里对每个iterator操作val builder =
new UncompressedInBlockBuilder[ID](new LocalIndexEncoder(dstPart.numPartitions))//构建UncompressedInBlockBuilder,传入LocalIndexEncoder,LocalIndexEncoder的参数是10
iter.foreach { case (dstBlockId, srcIds, dstLocalIndices, ratings) =>//对于每个iterator,把元素加到builder中
builder.add(dstBlockId, srcIds, dstLocalIndices, ratings)
}
builder.build().compress()//把UncompressedInBlock压缩成InBlock,这些类刚才都介绍了
}.setName(prefix + "InBlocks")//给RDD弄个名字 userInBlocks并缓存,最终赋值给inBlocks,inBlock里面的
.persist(storageLevel)
val outBlocks = inBlocks.mapValues { case InBlock(srcIds, dstPtrs, dstEncodedIndices, _) =>//由inBlocks生成outBlocks,
val encoder = new LocalIndexEncoder(dstPart.numPartitions)//搞一个LocalIndexEncoder
val activeIds = Array.fill(dstPart.numPartitions)(mutable.ArrayBuilder.make[Int])//生成一个数组,注意每个元素是一个可变数组
var i = 0
val seen = new Array[Boolean](dstPart.numPartitions)//生成一个布尔数组
while (i < srcIds.length) {//遍历用户id数组
var j = dstPtrs(i)//取用户id对应项目累计数组的第一个位置
ju.Arrays.fill(seen, false)//初始化seen每个元素为false
while (j < dstPtrs(i + 1)) {//取用户id对应项目累计数组的第二个位置,也就是在第二个位置前都是同一个用户对应的同一个项目
val dstBlockId = encoder.blockId(dstEncodedIndices(j))//根据项目排序索引计算BlockId
if (!seen(dstBlockId)) {//seen的作用是标记某个用户是否与某个产品block相连,true就是相连,false不相连
activeIds(dstBlockId) += i // add the local index in this out-block//把相连的用户id索引赋值给activeIds(dstBlockId),dstBlockId是产品BlockId
seen(dstBlockId) = true
}
j += 1
}
i += 1
}
activeIds.map { x =>//最终返回二维矩阵,行是dstBlockId,列是和这个dstBlockId有关联的用户索引数组
x.result()
}
}.setName(prefix + "OutBlocks")//这个RDD起名userOutBlocks,确实恰如其分
.persist(storageLevel)
(inBlocks, outBlocks)//返回inBlocks,outBlocks
}
我们梳理一下,inBlocks和outBlocks都是key-value形式,inBlocks的key是srcBlockId,value是InBlock(uniqueSrcIds, dstPtrs, dstEncodedIndices, ratings),uniqueSrcIds是升序去重的用户id数组,dstPtrs是和用户id相关的产品编码索引区间的数组,dstEncodedIndices是产品编码数组由dstPtrs可以找到dstEncodedIndices的索引ratings是评分数组,outBlocks的key也是srcBlockId,value是矩阵,行是dstBlockId,列是和这个dstBlockId有关联的用户索引数组,理清这个结构还是有些困难的
这篇我们就到这,下篇我们完结这个算法