軟件:IDEA2014、Maven、HanLP、JDK;
用到的知識:HanLP、Spark TF-IDF、Spark kmeans、Spark mapPartition;
用到的數據集:http://www.threedweb.cn/thread-1288-1-1.html(不需要下載,已經包含在工程裏面);
工程下載:https://github.com/fansy1990/hanlp-test 。
1. 問題描述
2. 解決思路:
2.1 文本預處理:
2.2 分詞
2.3 詞轉換爲詞向量
2.4 使用每個文檔的詞向量進行聚類建模
2.5 對聚類後的結果進行評估
3. 具體步驟:
3.1 開發環境--Maven
<!-- 中文分詞框架 -->
<dependency>
<groupId>com.hankcs</groupId>
<artifactId>hanlp</artifactId>
<version>${hanlp.version}</version>
</dependency>
<!-- Spark dependencies -->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.10</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-mllib_2.10</artifactId>
<version>${spark.version}</version>
</dependency>
其版本爲:<hanlp.version>portable-1.3.4</hanlp.version>、 <spark.version>1.6.0-cdh5.7.3</spark.version>。3.2 文件轉爲UTF-8編碼及存儲到一個文件
這部分內容可以直接參考:src/main/java/demo02_transform_encoding.TransformEncodingToOne 這裏的實現,因爲是Java基本的操作,這裏就不加以分析了。
3.3 Scala調用HanLP進行中文分詞
import com.hankcs.hanlp.dictionary.CustomDictionary
import com.hankcs.hanlp.dictionary.stopword.CoreStopWordDictionary
import com.hankcs.hanlp.tokenizer.StandardTokenizer
import scala.collection.JavaConversions._
/**
* Scala 分詞測試
* Created by fansy on 2017/8/25.
*/
object SegmentDemo {
def main(args: Array[String]) {
val sentense = "41,【 日 期 】19960104 【 版 號 】1 【 標 題 】合巢蕪高速公路巢蕪段竣工 【 作 者 】彭建中 【 正 文 】 安徽合(肥)巢(湖)蕪(湖)高速公路巢蕪段日前竣工通車並投入營運。合巢蕪 高速公路是國家規劃的京福綜合運輸網的重要幹線路段,是交通部確定1995年建成 的全國10條重點公路之一。該條高速公路正線長88公里。(彭建中)"
CustomDictionary.add("日 期")
CustomDictionary.add("版 號")
CustomDictionary.add("標 題")
CustomDictionary.add("作 者")
CustomDictionary.add("正 文")
val list = StandardTokenizer.segment(sentense)
CoreStopWordDictionary.apply(list)
println(list.map(x => x.word.replaceAll(" ","")).mkString(","))
}
}
運行完成後,即可得到分詞的結果,如下:
/**
* String 分詞
* @param sentense
* @return
*/
def transform(sentense:String):List[String] ={
val list = StandardTokenizer.segment(sentense)
CoreStopWordDictionary.apply(list)
list.map(x => x.word.replaceAll(" ","")).toList
}
}
輸入即是一箇中文的文本,輸出就是分詞的結果,同時去掉了一些常用的停用詞。
3.4 求TF-IDF
val docs = sc.textFile(input_data).map{x => val t = x.split(".txt\t");(t(0),transform(t(1)))}
.toDF("fileName", "sentence_words")
// 3. 求TF
println("calculating TF ...")
val hashingTF = new HashingTF()
.setInputCol("sentence_words").setOutputCol("rawFeatures").setNumFeatures(numFeatures)
val featurizedData = hashingTF.transform(docs)
// 4. 求IDF
println("calculating IDF ...")
val idf = new IDF().setInputCol("rawFeatures").setOutputCol("features")
val idfModel = idf.fit(featurizedData)
val rescaledData = idfModel.transform(featurizedData).cache()
變量docs是一個DataFrame[fileName, sentence_words] ,經過HashingTF後,變成了變量 featurizedData ,同樣是一個DataFrame[fileName,sentence_words, rawFeatures]。這裏通過setInputCol以及SetOutputCol可以設置輸入以及輸出列名(列名是針對DataFrame來說的,不知道的可以看下DataFrame的API)。3.5 建立KMeans模型
println("creating kmeans model ...")
val kmeans = new KMeans().setK(k).setSeed(1L)
val model = kmeans.fit(rescaledData)
// Evaluate clustering by computing Within Set Sum of Squared Errors.
println("calculating wssse ...")
val WSSSE = model.computeCost(rescaledData)
println(s"Within Set Sum of Squared Errors = $WSSSE")
這裏有計算cost值的,但是這個值評估不是很準確,比如我numFeature設置爲2000的話,那麼這個值就很大,但是其實其正確率會比較大的。
3.6 模型評估
這裏的模型評估直接使用一個小李子來說明:比如,現在有這樣的數據:val data = sc.parallelize(t)
val file_index = data.map(_._1.charAt(0)).distinct.zipWithIndex().collect().toMap
println(file_index)
val partitionData = data.partitionBy(MyPartitioner(file_index))
這裏的file_index,是對不同類的文檔進行編號,這個編號就對應每個partition,看MyPartitioner的實現:case class MyPartitioner(file_index:Map[Char,Long]) extends Partitioner{
override def getPartition(key: Any): Int = key match {
case _ => file_index.getOrElse(key.toString.charAt(0),0L).toInt
}
override def numPartitions: Int = file_index.size
}
2. 針對每個partition進行整合操作:val tt = partitionData.mapPartitionsWithIndex((index: Int, it: Iterator[(String,Int)]) => it.toList.map(x => (index,x)).toIterator)
tt.collect().foreach(println(_))
運行如下:// firstCharInFileName , firstCharInFileName - predictType
val combined = partitionData.map(x =>( (x._1.charAt(0), Integer.parseInt(x._1.charAt(0)+"") - x._2),1) )
.mapPartitions{f => var aMap = Map[(Char,Int),Int]();
for(t <- f){
if (aMap.contains(t._1)){
aMap = aMap.updated(t._1,aMap.getOrElse(t._1,0)+1)
}else{
aMap = aMap + t
}
}
val aList = aMap.toList
val total= aList.map(_._2).sum
val total_right = aList.map(_._2).max
List((aList.head._1._1,total,total_right)).toIterator
// aMap.toIterator //打印各個partition的總結
}
在整合之前先執行一個map操作,把數據變成((fileNameFirstChar, fileNameFirstChar.toInt - predictId), 1),其中fileNameFirstChar代表文件的第一個字符,其實也就是文件的所屬實際類別,後面的fileNameFirstChar.toInt-predictId 其實就是判斷預測的結果是否對了,這個值的衆數就是預測對的;最後一個值代碼前面的這個鍵值對出現的次數,其實就是統計屬於某個類別的實際文件個數以及預測對的文件個數,分別對應上面的total和total_right變量;輸出結果爲:(4,6,3)
(1,6,4)
(2,6,4)
發現其打印的結果是正確的,第一列代表文件名開頭,第二個代表屬於這個文件的個數,第三列代表預測正確的個數for(re <- result ){
println("文檔"+re._1+"開頭的 文檔總數:"+ re._2+",分類正確的有:"+re._3+",分類正確率是:"+(re._3*100.0/re._2)+"%")
}
val averageRate = result.map(_._3).sum *100.0 / result.map(_._2).sum
println("平均正確率爲:"+averageRate+"%")
輸出結果爲:4. 實驗
5. 總結
腳踏實地,專注
轉載請註明blog地址:http://blog.csdn.net/fansy1990