• 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")))
        }
    }

     

  • 相关阅读:
    19. 各种提权姿势总结
    18. 各种数据库查询基础
    Kafka——分布式消息系统
    跳表
    Linux的io机制
    Make命令
    ElasticSearch之二——集群
    ElasticSearch之一——索引
    Scribe日志收集工具
    Thrift 的原理和使用
  • 原文地址:https://www.cnblogs.com/SysoCjs/p/11466724.html
Copyright © 2020-2023  润新知