文章目录
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、思考
用词频代表一个词的特征值,这种做法是极为不妥当的。
因为一个词在一个文档中出的的次数多,难道就代表这个文档的主题思想吗?
不一定!那该怎么处理呢?