SimRank原理
图1.二部图
所谓二部图(bipartite graphs),是指图中的节点可以分这两个子集,任意一条边关联的两个节点分别来自于这两个子集。用I(v)和O(v)分别表示节点v的in-neighbors和out-neighbors。看上面的二部图,我们把A、B当成两个人,把a、b、c当成三件商品,有向边代表人购买的商品。simrank的基本思想是:如果两个实体相似,那么跟它们相关的实体应该也相似。比如在上图中如果a和c相似,那么A和B应该也相似,因为A和a相关,而B和c相关。
SimRank的基本公式:
s(a,b)=C|I(a)||I(b)|∑i=1|I(a)|∑j=1|I(b)|s(Ii(a),Ij(b))(1)
s(a,b)是节点a和b的相似度,当a=b时,s(a,b)=1。Ii(a)
表示a的第i个in-neighbor。当
I(a)=∅ 或
I(b)=∅ 时式
(1)为0。
(1)式用一句话描述就是:a和b的相似度等于a的in-neighbors和b的in-neighbors相似度的平均值。参数C是个阻尼系数,它的含义可以这么理解:假如I(a)=I(b)={A},按照
(1)式计算出sim(a,b)=C*sim(A,A)=C,所以
C∈(0,1)
。把式(1)应用于图1所示的二部图就是:
s(A,B)=C1|O(A)||O(B)|∑i=1|O(A)|∑j=1|O(B)|s(Oi(A),Oj(B)) for A≠B(2)
s(a,b)=C2|I(a)||I(b)|∑i=1|I(a)|∑j=1|I(b)|s(Ii(a),Ij(b)) for a≠b(3)
忽略C1和C2,(2)式是说买家A和B的相似度等于他们购买的物品之间相似度的平均值,(3)式是说物品a和b的相似度是购买它们的买家之间相似度的平均值。
对于非二部图的情况,一个节点既可能有in-neighbors也可能有out-neighbors,比如在论文引用的场景下,一篇论文既可能引用其他论文,也可能被其他论文引用。站在引用的角度,两篇论文的相似度为
s1(a,b)=C1|O(a)||O(b)|∑i=1|O(a)|∑j=1|O(b)|s2(Oi(a),Oj(b))(4)
站在被引用的角度,两篇论文的相似度为
s2(a,b)=C2|I(a)||I(b)|∑i=1|I(a)|∑j=1|I(b)|s1(Ii(a),Ij(b))(5)
根据实际的应用场景,最终的s(a,b)可以采用(4)和(5)其中的任何一个,或者是两者的综合。
矩阵形式
假设:
为SimRank相似度矩阵,其元素
表示相似度值
。
是一个按列归一化的图邻接矩阵,其元素
,若存在一条有向边
;否则为0。
于是,SimRank方程式可以用矩阵的形式表示如下:
代码实现
输入数据(案例图):
graph_bipartite
A a
A b
B b
B c
spark2.1代码:
package model
import org.apache.spark.graphx._
import org.apache.log4j.{Level, Logger}
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.mllib.linalg.distributed.{CoordinateMatrix, MatrixEntry}
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{DataFrame, SparkSession}
class SimRankGraph() {
/**
* 创建图的结构
*
* @param indexedNode
* @param nodes
* @return
*/
def graphStruct(indexedNode: RDD[(String, Long)], nodes: RDD[(String, String)]): Graph[String, Int] = {
val indexedNodes = nodes.join(indexedNode).map(r => (r._2._1, r._2._2)).join(indexedNode).map(r => (r._2._1, r._2._2))
val relationShips: RDD[Edge[Int]] = indexedNodes.map { x =>
val x1 = x._1
val x2 = x._2
Edge(x1, x2, 1)
}
val users: RDD[(VertexId, String)] = indexedNode.map { x =>
(x._2, x._1)
}
val graph = Graph(users, relationShips)
graph
}
}
object SimRank {
/**
* 获取item相似图
*
* @param nodes
* @param damp
*/
def getSimilarity(nodes: RDD[(String, String)], damp: Double) = {
val itemSet = nodes.map(x => (x._2, "-")).distinct()
val index2Node = (nodes.map(_._1) union (nodes.map(_._2))).distinct.zipWithIndex().cache()
val nodesNum = index2Node.count().toInt
val graph = new SimRankGraph().graphStruct(index2Node, nodes)
val outs = graph.outDegrees.map(x => (x._1, (1 / x._2.toDouble)))
val ins = graph.inDegrees.map(x => (x._1, (1 / x._2.toDouble)))
val rdd_out = graph.outerJoinVertices(outs)((id, _, degin) => (id.toString, degin.getOrElse(0)))
.triplets.map { x =>
(x.dstId, x.srcId, x.srcAttr._2.toString.toDouble * x.attr.toInt)
}
val rdd_int = graph.outerJoinVertices(ins)((id, _, degin) => (id.toString, degin.getOrElse(0)))
.triplets.map { x =>
(x.srcId, x.dstId, x.dstAttr._2.toString.toDouble * x.attr.toInt)
}
val rdd_all = rdd_out.union(rdd_int)
//概率转移矩阵Q
val transferMatrix = new CoordinateMatrix(rdd_all.map { x =>
MatrixEntry(x._1, x._2, x._3)
}).toBlockMatrix
// 单位矩阵I
val unitMatrix = new CoordinateMatrix(nodes.sparkContext.parallelize(0 until nodesNum).map { x =>
MatrixEntry(x, x, 1.0)
})
// C
val cMatrix = new CoordinateMatrix(unitMatrix.entries.map { x =>
MatrixEntry(x.i, x.j, x.value * damp)
}).toBlockMatrix
// (1-c) * I = S0
val simMatrix = new CoordinateMatrix(unitMatrix.entries.map { x =>
MatrixEntry(x.i, x.j, x.value * (1 - damp))
}).toBlockMatrix
// 初始化相似度矩阵
val S_0 = simMatrix
// K次迭代相似度矩阵
var S_k = S_0
// K+1次迭代相似度矩阵
var S_kp1 = S_k
for (i <- 0 until 5) {
//S_kp1 = c * Q(T) * S_k * Q + (1-c)* I
S_kp1 = transferMatrix.transpose.multiply(S_k).multiply(transferMatrix).multiply(cMatrix).add(simMatrix)
S_k = S_kp1
}
val node2Index = index2Node.map(x => (x._2, x._1))
val result = S_kp1.toCoordinateMatrix.entries.map {
case MatrixEntry(x, y, j) => (x, y, "%.6f" format j)
}.map(x => (x._1, (x._2, x._3)))
.join(node2Index, 500)
.map(x => (x._2._1._1, (x._2._1._2, x._2._2))).join(node2Index, 500)
.map(x => (x._2._1._2, (x._2._2, x._2._1._1)))
.join(itemSet, 500)
.map(x => (x._1, x._2._1))
result.filter(x => !x._1.equals(x._2._1)).map(x => (x._1, (x._2._1, x._2._2.toDouble)))
}
def main(args: Array[String]): Unit = {
//屏蔽日志
Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
Logger.getLogger("org.eclipse.jetty.server").setLevel(Level.OFF)
//阻尼系数
val damp = 0.6
val spark = SparkSession
.builder()
.master("local")
.appName("SimRank")
.enableHiveSupport()
.getOrCreate()
val data = spark.sparkContext.textFile("data/graph_bipartite").map(x => (x.split("\t")(0), x.split("\t")(1)))
// 计算item的图的相似度
val item_sim = getSimilarity(data , damp)
item_sim.take(100).foreach(println)
spark.close()
}}