Spark实现TF-IDF——文本相似度计算

        在Spark1.2之后,Spark自带实现TF-IDF接口,只要直接调用就可以,但实际上,Spark自带的词典大小设置较于古板,如果设置小了,则导致无法计算,如果设置大了,Driver端回收数据的时候,容易发生OOM,所以更多时候都是自己根据实际情况手动实现TF-IDF。不过,在本篇文章中,两种方式都会介绍。

数据准备:

        val df = ss.sql("select * from bigdatas.news_seg")
        //如果hive表的数据没有切词,则先对数据进行切词操作(hive里面每一行是用空格将各个词连接的字符串,或者说是一篇文章,结尾使用##@@##标识),得到一个数组类型数据
        val df_seg = df.selectExpr("split(split(sentence,'##@@##')[0],' ') as seg")

一、Spark自带TF-IDF

1、Spark自带TF实现

        首先需要实例化HashingTF,这个类用于根据给传入的各篇已经分好词的文章,对里面的每个词进行hashing计算,每个hashing值对应词表的一个位置,以及对每个词在每篇文章中的一个统计;

        这个类有一个方法setBinaty()可以设置其统计时的计算方式:多项式分布计算和伯努利分布计算:

  • setBinary(false):多项式分布计算,一个词在一篇文章中出现多少次,计算多少次;
  • setBinary(true):伯努利分布计算,一个词在一篇文章中,不管多少次,只要出现了,就为1,否则为0

        还有一个重要方法setNumFeatures(),用于设置词表的大小,默认是2^18。

        实例化HashingTF之后,使用transform就可以计算词频(TF)。

TF代码实现:

//            多项式分布计算
        val hashingTF = new HashingTF()
            .setBinary(false)
            .setInputCol("seg")
            .setOutputCol("feature_tf")
            .setNumFeatures(1<<18)
//            伯努利分布计算
        val hashingTF_BN = new HashingTF()
            .setBinary(true)
            .setInputCol("seg")
            .setOutputCol("feature_tf")
            .setNumFeatures(1<<18)
 
        /**
          * hashingTF.transform(df_seg):转换之后会在原来基础上增加一列,就是setOutputCol("feature_tf")设置的列
          * 新增列的数据结构为:(词表大小,[该行数据的每个词对应词表的hashCode],[该行数据的每个词在该行数据出现的次数,即多项式统计词频])
          */
        val df_tf = hashingTF.transform(df_seg).select("feature_tf")

最后列“feature_tf”的数据结构为(词典大小, [hashingCode], [term freq])。

2、Spark自带实现TF-IDF

对word进行idf加权(完成tf计算的基础上)

        实现原理跟上一步的TF类似,但多出一步,这一步是用于扫描一次上一步计算出来的tf数据。

代码如下:

        val idf = new IDF()
            .setInputCol("feature_tf")
            .setOutputCol("feature_tfidf")
            .setMinDocFreq(2)
        //fit():内部对df_tf进行遍历,统计doc Freq,这个操作是在tf完成后才做的
        val idfModel = idf.fit(df_tf)
        val df_tfidf = idfModel.transform(df_tf).select("feature_tfidf")

二、自己实现TF-IDF

手动实现,有计算的先后问题,必须先算DocFreq(DF),再算TermFreq(TF)。

1、doc Freq 文档频率计算 -> 同时可以得到所有文章的单词集合(词典)

        df的数据是每一行代表一篇文章,那么在计算某个词出现的文章次数,那么转化为某个词的统计。即将一篇文章切好词之后,放在一个set集合里面,表示这个set集合的每个词出现1次;那么将所有文章的词都切好,放在set集合里面,每篇文章拥有一个set集合,然后再根据词groupBy,Count,就可以得到每个词的DocFreq。

        val setUDF = udf((str:String)=>str.split(" ").distinct)
//        1.1、对每篇文章的词进行去重操作,即set集合
        val df_set = df.withColumn("words_set",setUDF(df("sentence")))
//        1.2、行转列,groupby、count,顺带可以求出词典大小,以及每个词对应在词典的位置index
        val docFreq_map = df_set.select(explode(df_set("words_set")).as("word"))
            .groupBy("word")
            .count()
            .rdd
            .map(x=>(x(0).toString,x(1).toString))
            .collectAsMap() //collect有一个重要特性,就是会将数据回收到Driver端,方便分发

上面计算出来的数据格式为:Map(word->wordCount),或理解为:Map(word->DocFreq)

        除了计算出DF外,还要顺便计算词典大小,因为词典大小代表了向量的大小,以及每个词对应词典的位置。

        val dictSize = docFreq_map.size
//        对单词进行编码,得到索引,类型为int,每个词对应于[0,dictSize-1]区间的一个位置
        val wordEncode = docFreq_map.keys.zipWithIndex.toMap

2、term Freq 词频计算 -> 同时计算tf-idf

        词频计算其实就是做wordCount,这里重点还需要顺带计算TF-IDF。

        val docCount = df_seg.count()
        val mapUDF = udf { str: String =>
    //每一行处理就是处理一篇文章
//            wordCOunt
            val tfMap = str.split("##@##")(0).split(" ")
                .map((_,1L))
                .groupBy(_._1)
                .mapValues(_.length)
 
//            tfMap{term->termCount}
//            docFreq_map{term->termDocCount}
//            wordEncode{term->index}
//            处理后的tfIDFMap数据结果为:[(index:int,tf_idf:Double)]     //必须要处理成这种形式
            val tfIDFMap = tfMap.map{x=>
                val idf_v = math.log10(docCount.toDouble/(docFreq_map.getOrElse(x._1,"0.0").toDouble+1))
                (wordEncode.getOrElse(x._1,0),x._2.toDouble * idf_v)
            }
//              做成向量:第一个参数为向量大小(词典大小);第二个参数用于给指定的index赋值为tf-idf
            Vectors.sparse(dictSize,tfIDFMap.toSeq)
        }
        val dfTF = df.withColumn("tf_idf",mapUDF(df("sentence")))

三、完整Demo代码

package com.cjs
 
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.ml.feature.HashingTF
import org.apache.spark.ml.feature.IDF
import org.apache.spark.ml.linalg.Vectors
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions._
 
object TFIDFTransform {
    def main(args: Array[String]): Unit = {
        Logger.getLogger("org.apache.spark").setLevel(Level.ERROR)
 
        val conf = new SparkConf()
            .set("spark.some.config.option","some-value")
 
        val ss = SparkSession
            .builder()
            .config(conf)
            .enableHiveSupport()
            .appName("test_tf-idf")
            //.master("local[2]") //单机版配置,集群情况下,可以去掉
            .getOrCreate()
 
        val df = ss.sql("select * from bigdatas.news_seg")
 
        //如果hive表的数据没有切词,则先对数据进行切词操作(hive里面每一行是用空格将各个词连接的字符串,或者说是一篇文章,结尾使用##@@##标识),得到一个数组类型数据
        val df_seg = df.selectExpr("split(split(sentence,'##@@##')[0],' ') as seg")
 
        val docCount = df_seg.count()
        //一、spark自带tf-idf实现
        //词典默认是2^20,先给一个词的一个hashCode,对应于词典的一个位置
        //词典空间过大,Driver进行数据回收时,容易出现OOM
//        1、spark自带TF实现
        /**
          setBinary:
             false:多项式分布 -> 一个词在一篇文章中出现多少次,计算多少次
             true:伯努利分布 -> 一个词在一篇文章中,不管多少次,只要出现了,就为1,否则为0
          **/
        /**
          * setInputCol("seg") :输入参数(DF)的列名
          * setOutputCol("feature_tf"):输出结果(结果)的列名
          * setNumFeatures(1<<18):设置词表大小,默认是1<<18
          */
//            多项式分布计算
        val hashingTF = new HashingTF()
            .setBinary(false)
            .setInputCol("seg")
            .setOutputCol("feature_tf")
            .setNumFeatures(1<<18)
//            伯努利分布计算
        val hashingTF_BN = new HashingTF()
            .setBinary(true)
            .setInputCol("seg")
            .setOutputCol("feature_tf")
            .setNumFeatures(1<<18)
 
        /**
          * hashingTF.transform(df_seg):转换之后会在原来基础上增加一列,就是setOutputCol("feature_tf")设置的列
          * 新增列的数据结构为:(词表大小,[该行数据的每个词对应词表的hashCode],[该行数据的每个词在该行数据出现的次数,即多项式统计词频])
          */
        val df_tf = hashingTF.transform(df_seg).select("feature_tf")
 
//        2、spark自带IDF实现, 对word进行idf加权(完成tf计算的基础上)
        val idf = new IDF()
            .setInputCol("feature_tf")
            .setOutputCol("feature_tfidf")
            .setMinDocFreq(2)
        //fit():内部对df_tf进行遍历,统计doc Freq,这个操作是在tf完成后才做的
        val idfModel = idf.fit(df_tf)
        val df_tfidf = idfModel.transform(df_tf).select("feature_tfidf")
 
 
        //二、自己实现tf-idf,有顺序的实现
        //1、doc Freq 文档频率计算 -> 同时可以得到所有文章的单词集合(词典)
        val setUDF = udf((str:String)=>str.split(" ").distinct)
//        1.1、对每篇文章的词进行去重操作,即set集合
        val df_set = df.withColumn("words_set",setUDF(df("sentence")))
//        1.2、行转列,groupby、count,顺带可以求出词典大小,以及每个词对应在词典的位置index
        val docFreq_map = df_set.select(explode(df_set("words_set")).as("word"))
            .groupBy("word")
            .count()
            .rdd
//            .map(x=>(x(0).toString,x(1).toString))
            .map(x=>(x(0).toString,math.log10(docCount.toDouble/(x(1).toString.toDouble+1))))   //顺带计算idf
            .collectAsMap() //collect有一个重要特性,就是会将数据回收到Driver端,方便分发
        val dictSize = docFreq_map.size
//        对单词进行编码,得到索引,类型为int,每个词对应于[0,dictSize-1]区间的一个位置
        val wordEncode = docFreq_map.keys.zipWithIndex.toMap
        //2、term Freq 词频计算
//        返回数据结构:
        val mapUDF = udf { str: String =>
    //每一行处理就是处理一篇文章
//            wordCOunt
            val tfMap = str.split("##@##")(0).split(" ")
                .map((_,1L))
                .groupBy(_._1)
                .mapValues(_.length)
 
//            tfMap{term->termCount}
//            docFreq_map{term->termDocCount}
//            wordEncode{term->index}
//            处理后的tfIDFMap数据结果为:[(index:int,tf_idf:Double)]     //必须要处理成这种形式
            val tfIDFMap = tfMap.map{x=>
//                val idf_v = math.log10(docCount.toDouble/(docFreq_map.getOrElse(x._1,"0.0").toDouble+1))
//                (wordEncode.getOrElse(x._1,0),x._2.toDouble * idf_v)
                val idf_v = docFreq_map.getOrElse(x._1,0.0)
                (wordEncode.getOrElse(x._1,0),x._2.toDouble * idf_v)  //已经在第1步算出idf情况下使用
            }
//              做成向量:第一个参数为向量大小(词典大小);第二个参数用于给指定的index赋值为tf-idf
            Vectors.sparse(dictSize,tfIDFMap.toSeq)
        }
        val dfTF = df.withColumn("tf_idf",mapUDF(df("sentence")))
    }
}

 

posted @ 2019-09-05 16:10  KamShing  阅读(2555)  评论(0编辑  收藏  举报