朴素贝叶斯之自然语言语义分析(一):简单文本主题分类→手撕文本特征向量化

1、需求说明

(1)经验样本数据:
docid,label,doc
doc0,1,苹果 官网 苹果 宣布 骁龙 安卓
doc1,0,苹果 梨 香蕉 桔子 肥料 甜度
doc2,0,苹果 梨 桔子 保鲜 甜度
doc3,1,苹果 科技 手机 官网 宣布 安卓 骁龙
(2)对下面的未知类别文档做分类预测
docid,doc
doc1,苹果 香蕉 甜度
doc2,苹果 手机 安卓 骁龙

2、难点分析:特征工程

(1)如何将人类的自然语言文章,转换成数字型的特征向量 ?

我们知道,朴素贝叶斯算法模型,需要将样本文档向量化,而一旦向量化,文档中的每一个词,应该由一个“数字”来表示。

怎么让文档中的不同的词都由唯一的一个“数字”表示呢?

即:对文章中的词取hashCode值(可能会产生哈希碰撞)

(2)特征向量的长度确定

向量应该是统一长度的,但是文章中的词有的多,有的少,这样该怎么确定呢?

解决办法:将向量的长度定的比任何一篇文档中的词数都要大得多。

向量长度=取经验样本所有文档中的词语去重后的词个数 × 10(减少hash碰撞)

(3)每个词语在向量中的位置确定

有的文档中,每个词可能出现多次(词频),那么这个词出现的次数,计入特征向量的数字。

向量中位置=每一个词的hashcode % 向量长度

3、代码开干

package cn.ianlou.bayes

import org.apache.log4j.{Level, Logger}
import org.apache.spark.ml.classification.{NaiveBayes, NaiveBayesModel}
import org.apache.spark.ml.linalg
import org.apache.spark.ml.linalg.Vectors
import org.apache.spark.sql.{DataFrame, Row, SparkSession}

/**
 * @date: 2020/2/21 21:26
 * @site: www.ianlou.cn
 * @author: lekko 六水
 * @qq: 496208110
 * @description: 对自然语言应用朴素贝叶斯算法来分类
 *               文本向量化代码手写版
 */
object NLP_Bayes_HandWrite {
  def main(args: Array[String]): Unit = {

    Logger.getLogger("org.apache.spark").setLevel(Level.WARN)

    val spark: SparkSession = SparkSession
      .builder()
      .appName(this.getClass.getSimpleName)
      .master("local[*]")
      .getOrCreate()

    import spark.implicits._

    // 1、加载经验样本数据
    val sampleDF: DataFrame = spark.read
      .option("header", true)
      .csv("userProfile/data/bayes/nlp/sample/sample.txt")

    // 缓存到内存,供下面多次调用
    sampleDF.cache()


    // 2、特征工程
    // ① 求所有文档中的总词数(去重)
    sampleDF.createTempView("tmp")

    //用SparkSql 比 用rdd更高效些
    //将每一行中的doc列,炸裂到行中,然后求这列的去重词数
    val arrCons: Array[Row] = spark.sql(
      """
        |select
        |count(distinct word) as cons
        |from
        |(
        |  select
        |  explode(split(doc, ' ')) as word
        |  from
        |  tmp
        |)t
        |""".stripMargin)
      .collect() //将总数收集到Driver端

    val length: Int = (arrCons(0).getAs[Long](0) * 10).toInt //130


    // ② 每个文档中doc词语,加工成总词数*10长度的特征向量
    val final_sample: DataFrame = sampleDF.rdd.map({
      /** 匹配模式:
       * match {
       * case xx => {}
       * case xx => {}
       * }
       *
       * 在map中可省略match
       */
      case Row(docid: String, label: String, doc: String) => {
        val vector: linalg.Vector = docToVector(length, doc)

        (docid, label.toDouble, vector)
      }
    }).toDF("docid", "label", "features")

    /** 最终的特征向量(稀疏性)如下:
     * +-----+-----+-----------------------------------------------------------+
     * |docid|label|feature                                                    |
     * +-----+-----+-----------------------------------------------------------+
     * |doc0 |1.0  |(130,[20,21,26,53,124],[1.0,1.0,1.0,2.0,1.0])              |
     * |doc1 |0.0  |(130,[2,12,42,53,60,84],[1.0,1.0,1.0,1.0,1.0,1.0])         |
     * |doc2 |0.0  |(130,[12,53,60,84,97],[1.0,1.0,1.0,1.0,1.0])               |
     * |doc3 |1.0  |(130,[20,21,26,51,53,89,124],[1.0,1.0,1.0,1.0,1.0,1.0,1.0])|
     * +-----+-----+-----------------------------------------------------------+
     */


    // 3、构建朴素贝叶斯算法对象
    val bayes: NaiveBayes = new NaiveBayes()
      .setFeaturesCol("features")
      .setLabelCol("label")
      .setSmoothing(1.0)

    // 4、模型训练
    val model: NaiveBayesModel = bayes.fit(final_sample)


    // 5、加载待预测数据
    val sampleToTest: DataFrame = spark.read
      .option("header", true)
      .csv("userProfile/data/bayes/nlp/test/test.txt")

    // 6、特征工程 -> 加工成特征向量
    val final_testDF: DataFrame = sampleToTest.rdd.map({
      case Row(docid: String, doc: String) => {
        (docid, doc, docToVector(length, doc))
      }
    }).toDF("docid", "doc", "features")


    // 7、将处理好的数据,输入模型,预测结果
    val result: DataFrame = model.transform(final_testDF).drop("rawPrediction", "features")

    result.createTempView("tmp2")
    spark.sql(
      """
        |select
        |docid,
        |doc,
        |probability,
        |prediction,
        |if(prediction==0.0, "水果类", "科技类") as predict
        |from
        |tmp2
        |
        |""".stripMargin)
        .show(10, false)

    /** 最终预测结果:
     * +-----+--------------------+-------------------------------+----------+---------+
     * |docid|        doc         |probability                    |prediction|  predict|
     * +-----+--------------------+-------------------------------+----------+---------+
     * |doc1 |苹果 香蕉 甜度        |[0.82438327161,0.17561672838]  |0.0       |水果类    |
     * |doc2 |苹果 手机 安卓 骁龙    |[0.04222037175,0.95777962824]  |1.0       |科技类    |
     * +-----+--------------------+-------------------------------+----------+---------+
     */

    spark.close()
  }


  /**
   * @describe 文档加工成特征向量的方法: Hash映射位置+词频特征值
   * @param length
   * @param doc
   * @return Vector(ml.linalg.Vector)
   */
  def docToVector(length: Int, doc: String): linalg.Vector = {

    // 构建一个特征向量,长度为length,特征都是0
    val arr: Array[Double] = Vectors.zeros(length).toArray

    // 将doc这一列的单词,进行切割,得到词的频次
    // Map(官网->1, 苹果->2, 安卓->1, 骁龙->1, 宣布->1)
    val tuples: Array[(String, Int)] = doc.split(" ").map((_, 1)) //别忘了按空格
    val gropyTp: Map[String, Array[(String, Int)]] = tuples.groupBy(tp => tp._1)
    val wordCount: Map[String, Int] = gropyTp.mapValues(arr => arr.map(tp => tp._2).sum)

    //将特征向量中的0替换成词的频次,hs位置处
    for ((k, v) <- wordCount) {
      arr(k.hashCode % length) = v
    }

    // 将数组转换成特征向量(稀疏性的)返回
    val vect: linalg.Vector = Vectors.dense(arr).toSparse
    vect
  }
}

4、思考

用词频代表一个词的特征值,这种做法是极为不妥当的。
因为一个词在一个文档中出的的次数多,难道就代表这个文档的主题思想吗?
不一定!那该怎么处理呢?

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值