Item2Vec:Word2vec方法的推广
在 Word2vec 诞生之后,Embedding 的思想迅速从自然语言处理领域扩散到几乎所有机器学习领域,推荐系统也不例外。既然 Word2vec 可以对词“序列”中的词进行 Embedding,那么对于用户购买“序列”中的一个商品,用户观看“序列”中的一个电影,也应该存在相应的 Embedding 方法。
于是,微软于 2015 年提出了 Item2Vec 方法,它是对 Word2vec 方法的推广,使 Embedding 方法适用于几乎所有的序列数据。Item2Vec 模型的技术细节几乎和 Word2vec 完全一致,只要能够用序列数据的形式把我们要表达的对象表示出来,再把序列数据“喂”给 Word2vec 模型,我们就能够得到任意物品的 Embedding 了。Item2vec 的提出对于推荐系统来说当然是至关重要的,因为它使得“万物皆 Embedding”成为了可能。对于推荐系统来说,Item2vec 可以利用物品的 Embedding 直接求得它们的相似性,或者作为重要的特征输入推荐模型进行训练,这些都有助于提升推荐系统的效果。
互联网中有哪些图结构数据?
我们知道,只要是能够被序列数据表示的物品,都可以通过 Item2vec 方法训练出 Embedding。但是,互联网的数据可不仅仅是序列数据那么简单,越来越多的数据被我们以图的形式展现出来。这个时候,基于序列数据的 Embedding 方法就显得“不够用”了。但在推荐系统中放弃图结构数据是非常可惜的,因为图数据中包含了大量非常有价值的结构信息。那我们怎么样才能够基于图结构数据生成 Embedding 呢?基于图结构的 Embedding 方法,它也被称为 Graph Embedding。
可能有的同学还不太清楚图结构中到底包含了哪些重要信息,为什么我们希望好好利用它们,并以它们为基础生成 Embedding?下面,我就先带你认识一下互联网中那些非常典型的图结构数据。 图一:
事实上,图结构数据在互联网中几乎无处不在,最典型的就是我们每天都在使用的社交网络(如图 1-a)。从社交网络中,我们可以发现意见领袖,可以发现社区,再根据这些“社交”特性进行社交化的推荐,如果我们可以对社交网络中的节点进行 Embedding 编码,社交化推荐的过程将会非常方便。
知识图谱也是近来非常火热的研究和应用方向。像图 1-b 中描述的那样,知识图谱中包含了不同类型的知识主体(如人物、地点等),附着在知识主体上的属性(如人物描述,物品特点),以及主体和主体之间、主体和属性之间的关系。如果我们能够对知识图谱中的主体进行 Embedding 化,就可以发现主体之间的潜在关系,这对于基于内容和知识的推荐系统是非常有帮助的。
还有一类非常重要的图数据就是行为关系类图数据。这类数据几乎存在于所有互联网应用中,它事实上是由用户和物品组成的“二部图”(也称二分图,如图 1-c)。用户和物品之间的相互行为生成了行为关系图。借助这样的关系图,我们自然能够利用 Embedding 技术发掘出物品和物品之间、用户和用户之间,以及用户和物品之间的关系,从而应用于推荐系统的进一步推荐。
毫无疑问,图数据是具备巨大价值的,如果能将图中的节点 Embedding 化,对于推荐系统来说将是非常有价值的特征。那下面,我们就进入正题,一起来学习基于图数据的 Graph Embedding 方法。
基于随机游走的 Graph Embedding 方法:Deep Walk
我们先来学习一种在业界影响力比较大,应用也很广泛的 Graph Embedding 方法,Deep Walk,它是 2014 年由美国石溪大学的研究者提出的。它的主要思想是在由物品组成的图结构上进行随机游走,产生大量物品序列,然后将这些物品序列作为训练样本输入 Word2vec 进行训练,最终得到物品的 Embedding。因此,DeepWalk 可以被看作连接序列 Embedding 和 Graph Embedding 的一种过渡方法。下图 2 展示了 DeepWalk 方法的执行过程。
接下来,我就参照图 2 中 4 个示意图,来为你详细讲解一下 DeepWalk 的算法流程。 图二:
-
首先,我们基于原始的用户行为序列(图 2-a),比如用户的购买物品序列、观看视频序列等等,来构建物品关系图(图 2-b)。从中,我们可以看出,因为用户 Ui先后购买了物品 A 和物品 B,所以产生了一条由 A 到 B 的有向边。如果后续产生了多条相同的有向边,则有向边的权重被加强。在将所有用户行为序列都转换成物品相关图中的边之后,全局的物品相关图就建立起来了。
-
然后,我们采用随机游走的方式随机选择起始点,重新产生物品序列(图 2-c)。其中,随机游走采样的次数、长度等都属于超参数,需要我们根据具体应用进行调整。
-
最后,我们将这些随机游走生成的物品序列输入图 2-d 的 Word2vec 模型,生成最终的物品 Embedding 向量。
在上述 DeepWalk 的算法流程中,唯一需要形式化定义的就是随机游走的跳转概率,也就是到达节点 vi后,下一步遍历 的邻接点 的概率。如果物品关系图是有向有权图,那么从节点 跳转到节点 的概率定义如下:
其中, 是节点 所有的出边集合,是节点 到节点边的权重,即 DeepWalk 的跳转概率就是跳转边的权重占所有相关出边权重之和的比例。如果物品相关图是无向无权重图,那么跳转概率将是上面这个公式的一个特例,即权重 将为常数 1,且 应是节点 所有“边”的集合,而不是所有“出边”的集合。
再通过随机游走得到新的物品序列,我们就可以通过经典的 Word2vec 的方式生成物品 Embedding 了。
在同质性和结构性间权衡的方法,Node2vec
2016 年,斯坦福大学的研究人员在 DeepWalk 的基础上更进一步,他们提出了 Node2vec 模型。Node2vec 通过调整随机游走跳转概率的方法,让 Graph Embedding 的结果在网络的同质性(Homophily)和结构性(Structural Equivalence)中进行权衡,可以进一步把不同的 Embedding 输入推荐模型,让推荐系统学习到不同的网络结构特点。
我这里所说的网络的“同质性”指的是距离相近节点的 Embedding 应该尽量近似,如图 3 所示,节点 u 与其相连的节点 s1、s2、s3、s4的 Embedding 表达应该是接近的,这就是网络“同质性”的体现。在电商网站中,同质性的物品很可能是同品类、同属性,或者经常被一同购买的物品。
而“结构性”指的是结构上相似的节点的 Embedding 应该尽量接近,比如图 3 中节点 u 和节点 s6都是各自局域网络的中心节点,它们在结构上相似,所以它们的 Embedding 表达也应该近似,这就是“结构性”的体现。在电商网站中,结构性相似的物品一般是各品类的爆款、最佳凑单商品等拥有类似趋势或者结构性属性的物品。
理解了这些基本概念之后,那么问题来了,Graph Embedding 的结果究竟是怎么表达结构性和同质性的呢?
首先,为了使 Graph Embedding 的结果能够表达网络的“结构性”,在随机游走的过程中,我们需要让游走的过程更倾向于 BFS(Breadth First Search,宽度优先搜索),因为 BFS 会更多地在当前节点的邻域中进行游走遍历,相当于对当前节点周边的网络结构进行一次“微观扫描”。当前节点是“局部中心节点”,还是“边缘节点”,亦或是“连接性节点”,其生成的序列包含的节点数量和顺序必然是不同的,从而让最终的 Embedding 抓取到更多结构性信息。
而为了表达“同质性”,随机游走要更倾向于 DFS(Depth First Search,深度优先搜索)才行,因为 DFS 更有可能通过多次跳转,游走到远方的节点上。但无论怎样,DFS 的游走更大概率会在一个大的集团内部进行,这就使得一个集团或者社区内部节点的 Embedding 更为相似,从而更多地表达网络的“同质性”。
那在 Node2vec 算法中,究竟是怎样控制 BFS 和 DFS 的倾向性的呢?其实,它主要是通过节点间的跳转概率来控制跳转的倾向性。图 4 所示为 Node2vec 算法从节点 t 跳转到节点 v 后,再从节点 v 跳转到周围各点的跳转概率。这里,你要注意这几个节点的特点。比如,节点 t 是随机游走上一步访问的节点,节点 v 是当前访问的节点,节点 是与 v 相连的非 t 节点,但节点 x1还与节点 t 相连,这些不同的特点决定了随机游走时下一次跳转的概率。
这些概率我们还可以用具体的公式来表示,从当前节点 v 跳转到下一个节点 x 的概率 ,其中 是边 vx 的原始权重, 是 Node2vec 定义的一个跳转权重。到底是倾向于 DFS 还是 BFS,主要就与这个跳转权重的定义有关了。这里我们先了解一下它的精确定义,我再作进一步的解释:
里的是指节点 t 到节点 x 的距离,比如节点 x1其实是与节点 t 直接相连的,所以这个距离 就是 1,节点 t 到节点 t 自己的距离 就是 0,而 x2、x3这些不与 t 相连的节点,就是 2。
此外, 中的参数 p 和 q 共同控制着随机游走的倾向性。参数 p 被称为返回参数(Return Parameter),p 越小,随机游走回节点 t 的可能性越大,Node2vec 就更注重表达网络的结构性。参数 q 被称为进出参数(In-out Parameter),q 越小,随机游走到远方节点的可能性越大,Node2vec 更注重表达网络的同质性。反之,当前节点更可能在附近节点游走。你可以自己尝试给 p 和 q 设置不同大小的值,算一算从 v 跳转到 t、和的跳转概率。这样一来,应该就不难理解我刚才所说的随机游走倾向性的问题啦。
Node2vec 这种灵活表达同质性和结构性的特点也得到了实验的证实,我们可以通过调整 p 和 q 参数让它产生不同的 Embedding 结果。图 5 上就是 Node2vec 更注重同质性的体现,从中我们可以看到,距离相近的节点颜色更为接近,图 5 下则是更注重结构性的体现,其中结构特点相近的节点的颜色更为接近。
毫无疑问,Node2vec 所体现的网络的同质性和结构性,在推荐系统中都是非常重要的特征表达。由于 Node2vec 的这种灵活性,以及发掘不同图特征的能力,我们甚至可以把不同 Node2vec 生成的偏向“结构性”的 Embedding 结果,以及偏向“同质性”的 Embedding 结果共同输入后续深度学习网络,以保留物品的不同图特征信息。
Embedding是如何应用在推荐系统的特征工程中的?
到这里,我们已经学习了好几种主流的 Embedding 方法,包括序列数据的 Embedding 方法,Word2vec 和 Item2vec,以及图数据的 Embedding 方法,Deep Walk 和 Node2vec。那你有没有想过,我为什么要在特征工程这一模块里介绍 Embedding 呢?Embedding 又是怎么应用到推荐系统中的呢?这里,我就来做一个统一的解答。
-
第一个问题不难回答,由于 Embedding 的产出就是一个数值型特征向量,所以 Embedding 技术本身就可以视作特征处理方式的一种。只不过与简单的 One-hot 编码等方式不同,Embedding 是一种更高阶的特征处理方法,它具备了把序列结构、网络结构、甚至其他特征融合到一个特征向量中的能力。
-
而第二个问题的答案有三个,因为 Embedding 在推荐系统中的应用方式大致有三种,分别是“直接应用”、“预训练应用”和“End2End 应用”。
其中,“直接应用”最简单,就是在我们得到 Embedding 向量之后,直接利用 Embedding 向量的相似性实现某些推荐系统的功能。典型的功能有,利用物品 Embedding 间的相似性实现相似物品推荐,利用物品 Embedding 和用户 Embedding 的相似性实现“猜你喜欢”等经典推荐功能,还可以利用物品 Embedding 实现推荐系统中的召回层等。
“预训练应用”指的是在我们预先训练好物品和用户的 Embedding 之后,不直接应用,而是把这些 Embedding 向量作为特征向量的一部分,跟其余的特征向量拼接起来,作为推荐模型的输入参与训练。这样做能够更好地把其他特征引入进来,让推荐模型作出更为全面且准确的预测。
第三种应用叫做“End2End 应用”。看上去这是个新的名词,它的全称叫做“End to End Training”,也就是端到端训练。不过,它其实并不神秘,就是指我们不预先训练 Embedding,而是把 Embedding 的训练与深度学习推荐模型结合起来,采用统一的、端到端的方式一起训练,直接得到包含 Embedding 层的推荐模型。这种方式非常流行,比如图 6 就展示了三个包含 Embedding 层的经典模型,分别是微软的 Deep Crossing,UCL 提出的 FNN 和 Google 的 Wide&Deep。
如何使用Spark生成Item2vec和Graph Embedding?
在 Spark 的机器学习包 Spark MLlib 中,还包含了大量成熟的机器学习模型,这其中就包括我们讲过的 Word2vec 模型。基于此,我们会在 Spark 平台上,完成 Item2vec 和基于 Deep Walk 的 Graph Embedding 的训练。
对其他机器学习平台有所了解的同学可能会问,TensorFlow、PyTorch 都有很强大的深度学习工具包,我们能不能利用这些平台进行 Embedding 训练呢?当然是可以的。但是 Spark 作为一个原生的分布式计算平台,在处理大数据方面还是比 TensorFlow 等深度学习平台更具有优势,而且业界的很多公司仍然在使用 Spark 训练一些结构比较简单的机器学习模型,所以,我们继续使用 Spark 来完成 Embedding 的实践。
首先,我们来看看怎么完成 Item2vec 的训练。
Item2vec:序列数据的处理
我们知道,Item2vec 是基于自然语言处理模型 Word2vec 提出的,所以 Item2vec 要处理的是类似文本句子、观影序列之类的序列数据。那在真正开始 Item2vec 的训练之前,我们还要先为它准备好训练用的序列数据。在 MovieLens 数据集中,有一张叫 rating(评分)的数据表,里面包含了用户对看过电影的评分和评分的时间。既然时间和评分历史都有了,我们要用的观影序列自然就可以通过处理 rating 表得到啦。
不过,在使用观影序列编码之前,我们还要再明确两个问题。一是 MovieLens 这个 rating 表本质上只是一个评分的表,不是真正的“观影序列”。但对用户来说,当然只有看过这部电影才能够评价它,所以,我们几乎可以把评分序列当作是观影序列。二是我们是应该把所有电影都放到序列中,还是只放那些打分比较高的呢?
这里,我是建议对评分做一个过滤,只放用户打分比较高的电影。为什么这么做呢?我们要思考一下 Item2vec 这个模型本质上是要学习什么。我们是希望 Item2vec 能够学习到物品之间的近似性。既然这样,我们当然是希望评分好的电影靠近一些,评分差的电影和评分好的电影不要在序列中结对出现。
好,那到这里我们明确了样本处理的思路,就是对一个用户来说,我们先过滤掉他评分低的电影,再把他评论过的电影按照时间戳排序。这样,我们就得到了一个用户的观影序列,所有用户的观影序列就组成了 Item2vec 的训练样本集。
那这个过程究竟该怎么在 Spark 上实现呢?其实很简单,我们只需要明白这 5 个关键步骤就可以实现了:
-
读取 ratings 原始数据到 Spark 平台;
-
用 where 语句过滤评分低的评分记录;
-
用 groupBy userId 操作聚合每个用户的评分记录,DataFrame 中每条记录是一个用户的评分序列;
-
定义一个自定义操作 sortUdf,用它实现每个用户的评分记录按照时间戳进行排序;
-
把每个用户的评分记录处理成一个字符串的形式,供后续训练过程使用。
具体的实现过程,我还是建议你来参考我下面给出的代码,重要的地方我也都加上了注释,方便你来理解。
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)
通过这段代码生成用户的评分序列样本中,每条样本的形式非常简单,它就是电影 ID 组成的序列,比如下面就是 ID 为 11888 用户的观影序列:
296 380 344 588 593 231 595 318 480 110 253 288 47 364 377 589 410 597 539 39 160 266 350 553 337 186 736 44 158 551 293 780 353 368 858
Item2vec:模型训练
训练数据准备好了,就该进入模型训练了。手写 Item2vec 的整个训练过程肯定是一件让人比较“崩溃”的事情,好在 Spark MLlib 已经为我们准备好了方便调用的 Word2vec 模型接口。我先把训练的代码贴在下面,然后再带你一步步分析每一行代码是在做什么。
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()
从上面的代码中我们可以看出,Spark 的 Word2vec 模型训练过程非常简单,只需要四五行代码就可以完成。接下来,我就按照从上到下的顺序,依次给你解析其中 3 个关键的步骤。
-
首先是创建 Word2vec 模型并设定模型参数。我们要清楚 Word2vec 模型的关键参数有 3 个,分别是 setVectorSize、setWindowSize 和 setNumIterations。其中,setVectorSize 用于设定生成的 Embedding 向量的维度,setWindowSize 用于设定在序列数据上采样的滑动窗口大小,setNumIterations 用于设定训练时的迭代次数。这些超参数的具体选择就要根据实际的训练效果来做调整了。
-
其次,模型的训练过程非常简单,就是调用模型的 fit 接口。训练完成后,模型会返回一个包含了所有模型参数的对象。
-
最后一步就是提取和保存 Embedding 向量,我们可以从最后的几行代码中看到,调用 getVectors 接口就可以提取出某个电影 ID 对应的 Embedding 向量,之后就可以把它们保存到文件或者其他数据库中,供其他模块使用了。
在模型训练完成后,我们再来验证一下训练的结果是不是合理。我在代码中求取了 ID 为 592 电影的相似电影。这部电影叫 Batman 蝙蝠侠,我把通过 Item2vec 得到相似电影放到了下面,你可以从直观上判断一下这个结果是不是合理。
Graph Embedding:数据准备
到这里,我相信你已经熟悉了 Item2vec 方法的实现。接下来,我们再来说说基于随机游走的 Graph Embedding 方法,看看如何利用 Spark 来实现它。这里,我们选择 Deep Walk 方法进行实现。图三:
在 Deep Walk 方法中,我们需要准备的最关键数据是物品之间的转移概率矩阵。图 3 是 Deep Walk 的算法流程图,转移概率矩阵表达了图 3(b) 中的物品关系图,它定义了随机游走过程中,从物品 A 到物品 B 的跳转概率。所以,我们先来看一下如何利用 Spark 生成这个转移概率矩阵。
//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 : _
通过随机游走产生了我们训练所需的 sampleCount 个样本之后,下面的过程就和 Item2vec 的过程完全一致了,就是把这些训练样本输入到 Word2vec 模型中,完成最终 Graph Embedding 的生成。你也可以通过同样的方法去验证一下通过 Graph Embedding 方法生成的 Embedding 的效果。关于 Item2vec 的 Spark 实现,你应该注意的是训练 Word2vec 模型的几个参数 VectorSize、WindowSize、NumIterations 等,知道它们各自的作用。它们分别是用来设置 Embedding 向量的维度,在序列数据上采样的滑动窗口大小,以及训练时的迭代次数。而在 Deep Walk 的实现中,我们应该着重理解的是,生成物品间的转移概率矩阵的方法,以及通过随机游走生成训练样本过程。
总结
以上就是关于词嵌入的相关知识,以上的内容均已通过实践测试。
本人也是这方面的小白,还有不对的地方请大佬指示。
有小伙伴不懂的地方可以评论,大家一块讨论解决!