Spark推荐系列-Word2vec算法介绍、实现和应用说明

1. 背景

word2vec 是Google 2013年提出的用于计算词向量的工具,在论文Efficient Estimation of Word Representations in Vector Space中,作者提出了Word2vec计算工具,并通过对比NNLM、RNNLM语言模型验证了word2vec的有效性。

word2vec工具中包含两种模型:CBOW和skip-gram。论文中介绍的比较简单,如下图所示,CBOW是通过上下文的词预测中心词,Skip-gram则是通过输入词预测上下文的词。

CBOW和skip-gram

Word2vec 开启了Embedding的相关工作,自从embedding开始大规模走进推荐系统中,下面我们就来看一下Word2vec算法的原理、Spark实现和应用说明。

2. 算法原理

Word2vec包含了两种模型,分别是CBOW和Skip-gram,CBOW又分为:

  • One-word context

  • multi-word context

Cbow_One-word context

其中单词的总个数为 ,隐藏层的神经元个数为 ,输入层到隐藏层的权重矩阵为 ,隐藏层到输出层的权重矩阵为

multi-word context

此时的 表达式为:

其中 表示上下文单词的个数, 表示上下文单词, 表示单词的输入向量(注意和输入层 区别)。

目标函数为:

Skip-gram 对应的图如下:

Skip-gram

从输入层到隐藏层:

从隐藏层到输出层:

其中:

  • 表示的是输入词

  • 表示输出层第 个词实际落在了第 个神经元

  • 表示输出层第 个词应该落在第 个神经元

  • 表示输出层第 个词实际落在了第 个神经元上归一化后的概率

  • 表示输出层第 个词实际落在了第 个神经元上未归一化的值

因为基于word2vec框架进行模型训练要求语料库非常大,这样才能保证结果的准确性,但随着预料库的增大,随之而来的就是计算的耗时和资源的消耗。那么有没有优化的余地呢?比如可以牺牲一定的准确性来加快训练速度,答案就是 hierarchical softmax 和 negative sampling。

在论文《Distributed Representations of Words and Phrases and their Compositionality》中介绍了训练word2vec的两个技(同样在论文《word2vec Parameter Learning Explained》中进行了详细的解释和说明),下面来具体看一下。

这里就不展开叙述了,可以参考之前的文章:论文|万物皆可Vector之Word2vec:2个模型、2个优化及实战使用

3.Spark实现

在spark的mllib实现了对word2vec的封装,基于MLLib进行实现和应用

主函数

    def main(args: Array[String]): Unit = {
        val spark = SparkSession.builder().master("local[10]").appName("Word2Vec").enableHiveSupport().getOrCreate()
        Logger.getRootLogger.setLevel(Level.WARN)

        val dataPath = "data/ml-100k/u.data"
        val data: RDD[Seq[String]] = spark.sparkContext.textFile(dataPath)
            .map(_.split("\t"))
            .map(l => (l(0), l(1)))
            .groupByKey()
            .map(l => l._2.toArray.toSeq)

        val word2vec = new Word2Vec()
            .setMinCount(minCount)
            .setNumPartitions(numPartitions)
            .setNumIterations(numIterations)
            .setVectorSize(vectorSize)
            .setLearningRate(learningRate)
            .setSeed(seed)

        val model = word2vec.fit(data)

        // 输出item id对应的embedding向量到
        saveItemEmbedding(spark, model)

        // 使用spark自带的防范计算item 相似度
        calItemSim(spark, model)

        // 自定义余弦相似度 计算item 相似度
        calItemSimV2(spark, model)

        spark.close()
    }

保存item embedding

    def saveItemEmbedding(spark: SparkSession, model: Word2VecModel) = {
        val normalizer1 = new Normalizer()

        val itemVector = spark.sparkContext.parallelize(
            model.getVectors.toArray.map(l => (l._1, normalizer1.transform(new DenseVector(l._2.map(_.toDouble)))))
        ).map(l => (l._1, l._2.toArray.mkString(",")))

        println(s"itemVector count: ${itemVector.count()}")
        itemVector.take(10).map(l => l._1 + "\t" + l._2).foreach(println)

        import spark.implicits._
        val itemVectorDF = itemVector.toDF("itemid", "vector")
            .select("itemid", "vector")
        itemVectorDF.show(10)
        // itemVectorDF.write.save("xxxx")
    }

计算item相似度

   def calItemSim(spark: SparkSession, model: Word2VecModel) = {
        // 这种方法离线对比 使用余弦计算item相似度 好像没有后者好
        val itemsRDD: RDD[String] = spark.sparkContext.parallelize(model.getVectors.keySet.toSeq)

        import spark.implicits._
        val itemSimItemDF = itemsRDD.map(l => (l, model.findSynonyms(l, 500)))
            .flatMap(l => for(one <- l._2) yield (l._1, one._1, one._2) )
            .toDF("source_item", "target_item", "sim_score")
        itemSimItemDF.show(10)
    }

    def calItemSimV2(spark: SparkSession, model: Word2VecModel) = {
        val normalizer1 = new Normalizer()

        val itemVector = spark.sparkContext.parallelize(
            model.getVectors.toArray.map(l => (l._1, normalizer1.transform(new DenseVector(l._2.map(_.toDouble)))))
        ).map(l => (l._1, l._2.toArray.toVector))

        import spark.implicits._
        val itemSimItem = itemVector.cartesian(itemVector)
            .map(l => (l._1._1, (l._2._1, calCos(l._1._2, l._2._2))))
            .groupByKey()
            .map(l => (l._1, l._2.toArray.sortBy(_._2).reverse.slice(0, 500)))
            .flatMap(l => for(one <- l._2) yield (l._1, one._1, one._2))
            .toDF("source_item", "target_item", "sim_score")
        itemSimItem.show(10)
    }

    def calCos(vector1: Vector[Double], vector2: Vector[Double]): Double = {
        //对公式部分分子进行计算
        val member = vector1.zip(vector2).map(d => d._1 * d._2).sum

        //求出分母第一个变量值
        val temp1 = math.sqrt(vector1.map(num => {
            math.pow(num, 2)
        }).sum)
        //求出分母第二个变量值
        val temp2 = math.sqrt(vector2.map(num => {
            math.pow(num, 2)
        }).sum)

        //求出分母
        val denominator = temp1 * temp2

        //进行计算
        member / denominator
    }

4.应用

其实在产出Item Embedding之后,在召回阶段,可以进行i2i的召回,或者u2i的召回,具体使用方式如下描述:

  • i2i:我们可以离线计算出Item 的相似 item列表或者实时通过es、faiss检索得到i2i,这样线上可以进行u2i & i2i的实时触发召回(实时召回一般效果都是比较好的,只要挖掘的i2i别太离谱就行)

  • u2i:可以根据用户最近点击的若干个spu,来做一个avg pooling,得到用户的embedding,继而离线或者在线进行embedding的相似计算&检索,得到u2i的召回

在排序阶段,可以用item embedding的数据作为特征来使用,但是需要注意,在产出embedding之后,使用时一般进行vector的正则(normalizer),进入算法后更方便算法使用

如果是基于语义信息产出的item embedding,也可以在展示机制方面进行使用,其大概使用原理为:避免相邻的item 相似性过高(具体可以参考MMR算法)

word2vec想要达到一个好的效果前提是:系统数据比较丰富,对于数据比较稀疏的序列,word2vec学习出来的item embedding表达能力并不好。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值