08 | Embedding实战:如何使用Spark生成Item2vec和Graph Embedding?
Item2vec:序列数据的处理
我们知道,Item2vec 是基于自然语言处理模型 Word2vec 提出的,所以 Item2vec 要处理的是类似文本句子、观影序列之类的序列数据。那在真正开始 Item2vec 的训练之前,我们还要先为它准备好训练用的序列数据。在 MovieLens 数据集中,有一张叫 rating(评分)的数据表,里面包含了用户对看过电影的评分和评分的时间。既然时间和评分历史都有了,我们要用的观影序列自然就可以通过处理 rating 表得到啦。
这里,我是建议对评分做一个过滤,只放用户打分比较高的电影。为什么这么做呢?我们要思考一下 Item2vec 这个模型本质上是要学习什么。我们是希望 Item2vec 能够学习到物品之间的近似性。既然这样,我们当然是希望评分好的电影靠近一些,评分差的电影和评分好的电影不要在序列中结对出现。
def processItemSequence(sparkSession: SparkSession): RDD[Seq[String]] ={
//设定rating数据的路径并用spark载入数据
val ratingsResourcesPath = this.getClass.getResource("/webroot/sampledata/ratings.csv")
val ratingSamples = sparkSession.read.format("csv").option("header", "true").load(ratingsResourcesPath.getPath)
//实现一个用户定义的操作函数(UDF),用于之后的排序
val sortUdf: UserDefinedFunction = udf((rows: Seq[Row]) => {
rows.map { case Row(movieId: String, timestamp: String) => (movieId, timestamp) }
.sortBy { case (movieId, timestamp) => timestamp }
.map { case (movieId, timestamp) => movieId }
})
//把原始的rating数据处理成序列数据
val userSeq = ratingSamples
.where(col("rating") >= 3.5) //过滤掉评分在3.5一下的评分记录
.groupBy("userId") //按照用户id分组
.agg(sortUdf(collect_list(struct("movieId", "timestamp"))) as "movieIds") //每个用户生成一个序列并用刚才定义好的udf函数按照timestamp排序
.withColumn("movieIdStr", array_join(col("movieIds"), " "))
//把所有id连接成一个String,方便后续word2vec模型处理
//把序列数据筛选出来,丢掉其他过程数据
userSeq.select("movieIdStr").rdd.map(r => r.getAs[String]("movieIdStr").split(" ").toSeq)
Item2vec:模型训练
def trainItem2vec(samples : RDD[Seq[String]]): Unit ={
//设置模型参数
val word2vec = new Word2Vec()
.setVectorSize(10)
.setWindowSize(5)
.setNumIterations(10)
//训练模型
val model = word2vec.fit(samples)
//训练结束,用模型查找与item"592"最相似的20个item
val synonyms = model.findSynonyms("592", 20)
for((synonym, cosineSimilarity) <- synonyms) {
println(s"$synonym $cosineSimilarity")
}
//保存模型
val embFolderPath = this.getClass.getResource("/webroot/sampledata/")
val file = new File(embFolderPath.getPath + "embedding.txt")
val bw = new BufferedWriter(new FileWriter(file))
var id = 0
//用model.getVectors获取所有Embedding向量
for (movieId <- model.getVectors.keys){
id+=1
bw.write( movieId + ":" + model.getVectors(movieId).mkString(" ") + "\n")
}
bw.close()
Graph Embedding:数据准备
//samples 输入的观影序列样本集
def graphEmb(samples : RDD[Seq[String]], sparkSession: SparkSession): Unit ={
//通过flatMap操作把观影序列打碎成一个个影片对
val pairSamples = samples.flatMap[String]( sample => {
var pairSeq = Seq[String]()
var previousItem:String = null
sample.foreach((element:String) => {
if(previousItem != null){
pairSeq = pairSeq :+ (previousItem + ":" + element)
}
previousItem = element
})
pairSeq
})
//统计影片对的数量
val pairCount = pairSamples.countByValue()
//转移概率矩阵的双层Map数据结构
val transferMatrix = scala.collection.mutable.Map[String, scala.collection.mutable.Map[String, Long]]()
val itemCount = scala.collection.mutable.Map[String, Long]()
//求取转移概率矩阵
pairCount.foreach( pair => {
val pairItems = pair._1.split(":")
val count = pair._2
lognumber = lognumber + 1
println(lognumber, pair._1)
if (pairItems.length == 2){
val item1 = pairItems.apply(0)
val item2 = pairItems.apply(1)
if(!transferMatrix.contains(pairItems.apply(0))){
transferMatrix(item1) = scala.collection.mutable.Map[String, Long]()
}
transferMatrix(item1)(item2) = count
itemCount(item1) = itemCount.getOrElse[Long](item1, 0) + count
}
生成转移概率矩阵的函数输入是在训练 Item2vec 时处理好的观影序列数据。输出的是转移概率矩阵,由于转移概率矩阵比较稀疏,因此我没有采用比较浪费内存的二维数组的方法,而是采用了一个双层 Map 的结构去实现它。比如说,我们要得到物品 A 到物品 B 的转移概率,那么 transferMatrix(itemA)(itemB) 就是这一转移概率。在求取转移概率矩阵的过程中,我先利用 Spark 的 flatMap 操作把观影序列“打碎”成一个个影片对,再利用 countByValue 操作统计这些影片对的数量,最后根据这些影片对的数量求取每两个影片之间的转移概率。在获得了物品之间的转移概率矩阵之后,我们就可以进入图 3(c) 的步骤,进行随机游走采样了。
Graph Embedding:随机游走采样过程
随机游走采样的过程是利用转移概率矩阵生成新的序列样本的过程。这怎么理解呢?首先,我们要根据物品出现次数的分布随机选择一个起始物品,之后就进入随机游走的过程。在每次游走时,我们根据转移概率矩阵查找到两个物品之间的转移概率,然后根据这个概率进行跳转。比如当前的物品是 A,从转移概率矩阵中查找到 A 可能跳转到物品 B 或物品 C,转移概率分别是 0.4 和 0.6,那么我们就按照这个概率来随机游走到 B 或 C,依次进行下去,直到样本的长度达到了我们的要求。根据上面随机游走的过程,我用 Scala 进行了实现,你可以参考下面的代码,在关键的位置我也给出了注释:
//随机游走采样函数
//transferMatrix 转移概率矩阵
//itemCount 物品出现次数的分布
def randomWalk(transferMatrix : scala.collection.mutable.Map[String, scala.collection.mutable.Map[String, Long]], itemCount : scala.collection.mutable.Map[String, Long]): Seq[Seq[String]] ={
//样本的数量
val sampleCount = 20000
//每个样本的长度
val sampleLength = 10
val samples = scala.collection.mutable.ListBuffer[Seq[String]]()
//物品出现的总次数
var itemTotalCount:Long = 0
for ((k,v) <- itemCount) itemTotalCount += v
//随机游走sampleCount次,生成sampleCount个序列样本
for( w <- 1 to sampleCount) {
samples.append(oneRandomWalk(transferMatrix, itemCount, itemTotalCount, sampleLength))
}
Seq(samples.toList : _*)
}
//通过随机游走产生一个样本的过程
//transferMatrix 转移概率矩阵
//itemCount 物品出现次数的分布
//itemTotalCount 物品出现总次数
//sampleLength 每个样本的长度
def oneRandomWalk(transferMatrix : scala.collection.mutable.Map[String, scala.collection.mutable.Map[String, Long]], itemCount : scala.collection.mutable.Map[String, Long], itemTotalCount:Long, sampleLength:Int): Seq[String] ={
val sample = scala.collection.mutable.ListBuffer[String]()
//决定起始点
val randomDouble = Random.nextDouble()
var firstElement = ""
var culCount:Long = 0
//根据物品出现的概率,随机决定起始点
breakable { for ((item, count) <- itemCount) {
culCount += count
if (culCount >= randomDouble * itemTotalCount){
firstElement = item
break
}
}}
sample.append(firstElement)
var curElement = firstElement
//通过随机游走产生长度为sampleLength的样本
breakable { for( w <- 1 until sampleLength) {
if (!itemCount.contains(curElement) || !transferMatrix.contains(curElement)){
break
}
//从curElement到下一个跳的转移概率向量
val probDistribution = transferMatrix(curElement)
val curCount = itemCount(curElement)
val randomDouble = Random.nextDouble()
var culCount:Long = 0
//根据转移概率向量随机决定下一跳的物品
breakable { for ((item, count) <- probDistribution) {
culCount += count
if (culCount >= randomDouble * curCount){
curElement = item
break
}
}}
sample.append(curElement)
}}
Seq(sample.toList : _
小结
这节课,我们运用 Spark 实现了经典的 Embedding 方法 Item2vec 和 Deep Walk。它们的理论知识你应该已经在前两节课的学习中掌握了,这里我就总结一下实践中应该注意的几个要点。关于 Item2vec 的 Spark 实现,你应该注意的是训练 Word2vec 模型的几个参数 VectorSize、WindowSize、NumIterations 等,知道它们各自的作用。它们分别是用来设置 Embedding 向量的维度,在序列数据上采样的滑动窗口大小,以及训练时的迭代次数。而在 Deep Walk 的实现中,我们应该着重理解的是,生成物品间的转移概率矩阵的方法,以及通过随机游走生成训练样本过程。最后,我还是把这节课的重点知识总结在了一张表格中,希望能帮助你进一步巩固。
此文章为3月Day25学习笔记,内容来源于极客时间《深度学习推荐系统实战》,强烈推荐该课程!