Spark机器学习:TF-IDF实现原理

先简单地介绍下什么是TF-IDF(词频-逆文档频率),它可以反映出语料库中某篇文档中某个词的重要性。假设t表示某个词,d表示一篇文档,则词频TF(t,d)是某个词t在文档d中出现的次数,而文档DF(t,D)是包含词t的文档数目。为了过滤掉常用的词组,如"the" "a" "of" "that",我们使用逆文档频率来度量一个词能提供多少信息的数值: 

IDF(t,D)=log(|D|+1)/(DF(t,D)+1)

这里|D|表示语料库的文档总数,为了不让分母为了0,在此进行了加1平滑操作。而词频-逆文档频率就是TF和IDF的简单相乘:

TFIDF(t,d,D)=TF(t,d)*IDF(t,D)

下面使用Spark官方提供的一个例子,使用的都是mlib的包,具体可以参考:http://spark.apache.org/docs/1.6.1/mllib-feature-extraction.html

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.mllib.feature.HashingTF
import org.apache.spark.mllib.linalg.Vector
import org.apache.spark.mllib.feature.IDF

object TF_IDF_Test {

  def main(args: Array[String]) {
    val conf = new SparkConf().setAppName("TfIdfTest")
    val sc = new SparkContext(conf)
    // Load documents (one per line).
    val documents: RDD[Seq[String]] = sc.textFile("...").map(_.split(" ").toSeq)
    val hashingTF = new HashingTF()
    val tf: RDD[Vector] = hashingTF.transform(documents)
    tf.cache()
    val idf = new IDF().fit(tf)
    val tfidf: RDD[Vector] = idf.transform(tf)
  }

下面对代码进行详细的解释:

1.首先看数据源documents,它作为hashingTF.transform的参数,要求每一行为一篇文档的内容。

2.下面在看hashingTF.transform的方法源码,其调用了HashingTF类自身的transform方法对每一篇文档进行处理

  /**
   * Transforms the input document to term frequency vectors.
   */
  @Since("1.1.0")
  def transform[D <: Iterable[_]](dataset: RDD[D]): RDD[Vector] = {
    dataset.map(this.transform)
  }
3.HashingTF类自身的transform方法,这里的参数document是按空格划分了的单词序列,numFeatures为HashingTF类的成员变量默认为2^20,也就是hash的维数。

最终我们获得的是一个稀疏向量,其下index就是单词的哈希值,value就是单词的频数

* Transforms the input document into a sparse term frequency vector.
*/
@Since("1.1.0")
def transform(document: Iterable[_]): Vector = {
//hash(单词的hash码,单词频数)
val termFrequencies = mutable.HashMap.empty[Int, Double]
//遍历文档的单词
document.foreach { term =>
  val i = indexOf(term) //获得单词的hash码
  //单词频数统计
  termFrequencies.put(i, termFrequencies.getOrElse(i, 0.0) + 1.0)
}
//把结果转换成稀疏向量
Vectors.sparse(numFeatures, termFrequencies.toSeq)
}
3.1 indexof 方法,term.## 等价于获得对象term的 哈希值,使用Utils.nonNegativeMod对于获得的哈希值模numFeatures取正余

  /**
   * Returns the index of the input term.
   */
  @Since("1.1.0")
  def indexOf(term: Any): Int = Utils.nonNegativeMod(term.##, numFeatures)

Utils.nonNegativeMod

 /* Calculates 'x' modulo 'mod', takes to consideration sign of x,
  * i.e. if 'x' is negative, than 'x' % 'mod' is negative too
  * so function return (x % mod) + mod in that case.
  */
  def nonNegativeMod(x: Int, mod: Int): Int = {
    val rawMod = x % mod
    rawMod + (if (rawMod < 0) mod else 0)
  }
4.val idf = new IDF().fit(tf),这里的tf为RDD[Vector],每个稀疏向量的内容参考3。

  @Since("1.1.0")
  def fit(dataset: RDD[Vector]): IDFModel = {
    val idf = dataset.treeAggregate(new IDF.DocumentFrequencyAggregator(
          minDocFreq = minDocFreq))(
      seqOp = (df, v) => df.add(v),
      combOp = (df1, df2) => df1.merge(df2)
    ).idf()
    new IDFModel(idf)
  }

treeAggregate和Aggregate类似,它把IDF.DocumentFrequencyAggregator作为初始值,seqop为分区类的聚合操作,而comop为分区间的聚合操作。下面具体看下DocumentFrequencyAggregator的内容,其使用成员变量df(密集向量)来记录index(单词hash码)在多少个文档中出现过。使用add方法来合并一个新的文档,并更新df和m的值;因为密集和稀疏操作类似,下面以匹配密集为例,values(j) > 0.0,说明j对应的单词在这篇文档出现过,df(j)+=1。然后使用merge来合并分区间的统计结果(这里只是进行简单的相加)。最后使用idf()方法对treeAggregate的结果使用公式1得到IDF,把结果封装到IDFModel类并返回。

class DocumentFrequencyAggregator(val minDocFreq: Int) extends Serializable {
    //语料库的文档总数
    private var m = 0L
	//BDV为一个密集向量的别名,df对应的值为该index(单词hash码)在多少个文档中出现过
    private var df: BDV[Long] = _

    def this() = this(0)

   //添加一个新的文档
    def add(doc: Vector): this.type = {
      if (isEmpty) {
        df = BDV.zeros(doc.size) //初始化0操作
      }
      doc match {
		//如果是稀疏向量
        case SparseVector(size, indices, values) =>
          val nnz = indices.size
          var k = 0
          while (k < nnz) {
            if (values(k) > 0) {
              df(indices(k)) += 1L
            }
            k += 1
          }
		//如果是密集向量
        case DenseVector(values) =>
          val n = values.size
          var j = 0
          while (j < n) {
			//values(j) > 0.0,说明j对应的单词在这篇文档出现过
            if (values(j) > 0.0) {
              df(j) += 1L
            }
            j += 1
          }
        case other =>
          throw new UnsupportedOperationException(
            s"Only sparse and dense vectors are supported but got ${other.getClass}.")
      }
      m += 1L //语料库的文档数加1
      this
    }

    //合并其他的文档,对文档总数和df进行简单的相加
    def merge(other: DocumentFrequencyAggregator): this.type = {
      if (!other.isEmpty) {
        m += other.m
        if (df == null) {
          df = other.df.copy
        } else {
          df += other.df
        }
      }
      this
    }

    private def isEmpty: Boolean = m == 0L

    /** Returns the current IDF vector. */
    def idf(): Vector = {
      if (isEmpty) {
        throw new IllegalStateException("Haven't seen any document yet.")
      }
      val n = df.length
      val inv = new Array[Double](n)
      var j = 0
      while (j < n) {
        if (df(j) >= minDocFreq) {
          inv(j) = math.log((m + 1.0) / (df(j) + 1.0))
        }
        j += 1
      }
      Vectors.dense(inv)
    }
  }
}
5.val tfidf: RDD[Vector] = idf.transform(tf),对4得到的idf(IDFModel)乘上tf即得到最终的结果。 下面先把idf进行广播,然后和各个分区的tf对应相乘

def transform(dataset: RDD[Vector]): RDD[Vector] = {
    val bcIdf = dataset.context.broadcast(idf)
    dataset.mapPartitions(iter => iter.map(v => IDFModel.transform(bcIdf.value, v)))
  }

对应相乘

def transform(idf: Vector, v: Vector): Vector = {
    val n = v.size
    v match {
      case SparseVector(size, indices, values) =>
        val nnz = indices.size
        val newValues = new Array[Double](nnz)
        var k = 0
        while (k < nnz) {
          newValues(k) = values(k) * idf(indices(k))
          k += 1
        }
        Vectors.sparse(n, indices, newValues)
      case DenseVector(values) =>
        val newValues = new Array[Double](n)
        var j = 0
        while (j < n) {
          newValues(j) = values(j) * idf(j)
          j += 1
        }
        Vectors.dense(newValues)
      case other =>
        throw new UnsupportedOperationException(
          s"Only sparse and dense vectors are supported but got ${other.getClass}.")
    }
}

tfidf最后的输出格式为Sparse向量,具体的例子可以参看:Spark-MLib之TFIDF实例讲解


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值