結構化數據處理比較直接,然而非結構化數據(好比:文本、語音)處理就比較具備挑戰。對於文本如今比較成熟的技術是搜索引擎,它能夠幫助人們從給定的詞語中快速找到包含關鍵詞的文本。可是,一些狀況下人們但願找到某一個概念的文本,而不關心文本里面是否包含某個關鍵詞。這種狀況下應該如何是好?
隱語義分析(Latent Semantic Analysis,簡稱:LSA)是一種尋找更好的理解語料庫中詞和文檔之間關係的天然語言和信息檢索的技術。它試圖經過語料庫提取一系列概念。每一個概念對應一系列單詞而且一般對應語料庫中討論的一個主題。先拋開數據而言,每個概念由三個屬性構成:css
好比:LSA可能會發現某個概念和單詞「股票」、「炒股」有很高的相關性而且和「互聯網金融系列文章」有很高的相關性。經過選擇最重要的概念,LSA能夠去掉一些噪音數據。 在不少場合均可以使用這種簡潔的表示,好比計算詞與詞、文檔與文檔、詞與文檔的類似性。經過LSA獲得的關於概念的得分,能夠對語料庫有更加深刻的理解,而不僅是簡單的計算單詞或者共現詞。這種類似性度量能夠解決同義詞查詢、文本按照相同主題聚類、給文本添加標籤等。 LSA主要用到的技術就是奇異值分解。首先獲得詞-文檔重要性矩陣(通常是TF-IDF矩陣),而後利用svd奇異值分解技術獲得原矩陣近似相等的三個矩陣的乘積:SVD,其中 S 能夠看出概念與文件的關係,V 表示概念的重要程度,D 表示概念與詞的關係。
下面將完整講述經過爬蟲抓取豌豆莢App信息以後,如何利用Spark讀取數據,對文本分詞、去除噪音詞、將數據轉換爲數字格式、最後計算SVD而且解釋如何理解和使用獲得的結果。java
爬蟲不是本文的重點,有興趣的讀者能夠查看做者構建的開源爬蟲nlp-spider,本文集中抓取的是豌豆莢關於金融理財大類的數據。只提取了三個信息:package_name(包名),description(app 描述信息),categories(類別名),示例以下:android
com.zmfz.app "影視製片過程管理系統,對演員,設備,道具,劇本進行分類管理" [{level: 1, name: "金融理財"},{level: 2, name: "記帳"}] cn.fa.creditcard "辦信用卡,方便快捷" [{level: 1, name: "金融理財"},{level: 2, name: "銀行"}]
public static void clearWandoujiaAppData( String categoryFile, //肯定哪些類的數據才須要 String filePath, //保存抓取數據的文件 String filedsTerminated //文件的分割符號 ) { List<String> changeLines; File wdj = new File(filePath); if (!wdj.exists()) { LOGGER.error("file:" + wdj.getAbsolutePath() + " not exists, please check!"); } try { List<String> categories = FileUtils.readLines(new File(categoryFile)); List<String> lines = FileUtils.readLines(wdj, fileEncoding); changeLines = new ArrayList<String>(lines.size()*2); for (String line : lines) { String[] cols = StringUtils.split(line, filedsTerminated); //去掉樣本中格式錯誤的 if (cols.length != 3) { LOGGER.warn("line:" + line + ", format error!"); continue; } //去掉描述信息爲空白、包含亂碼、不包含中文、短文本 if (StringUtils.isBlank(cols[1]) || StringUtils.isEmpty(cols[1])){ LOGGER.warn("line:" + line + ", content all blank!"); continue; } if (StringUtils.contains(cols[1], "?????")){ LOGGER.warn("line:" + line + ", content contains error code!"); continue; } if (!isContainsChinese(cols[1])){ LOGGER.warn("line:" + line + ", content not contains chinese word!"); continue; } if (cols[1].length() <= 10){ LOGGER.warn("line:" + line + ", content length to short!"); continue; } List<String> cates = JsonUtil.jsonParseAppCategories(cols[2], "name"); if (cates.contains("金融理財")) { if (isForClass) { for (String cate : cates) { if (StringUtils.equals(cate, "金融理財")) continue; else { if (categories.contains(cate)) { String[] newLines = new String[]{cols[0], StringUtils.trim(cols[1]), cate}; changeLines.add(StringUtil.mkString(newLines, filedsTerminated)); } } } } else { String[] newLines = new String[]{cols[0], cols[1]}; changeLines.add(StringUtil.mkString(newLines, filedsTerminated)); } } } FileUtils.writeLines(new File(wdj.getParent(), wdj.getName() + ".clear"), changeLines); } catch (IOException e) { e.printStackTrace(); } }
上面會清洗掉不須要的數據,只保留金融理財的數據,注意上面使用的類的來源以下:git
分詞主要使用的是 HanLP(https://github.com/hankcs/HanLP) 這個天然語言處理工具包,下面貼出關鍵代碼:github
public static List<String> segContent(String content) { List<String> words = new ArrayList<String>(content.length()); List<Term> terms = HanLP.segment(content); for (Term term : terms) { String word = term.word; //單詞必須包含中文並且長度必須大於2 if (word.length() < 2 || word.matches(".*[a-z|A-Z].*")) continue; String nature = term.nature.name(); //詞性過濾 if (nature.startsWith("a") || nature.startsWith("g") || nature.startsWith("n") || nature.startsWith("v") ) { //停用詞去除 if (!sw.isStopWord(word)) words.add(word); } } return words; }
SVD計算以前必須獲得一個矩陣,本文使用的是TF-IDF矩陣,TF-IDF矩陣能夠理解以下:json
TF-IDF = TF*LOG(IDF)api
下面給出整個計算的詳細流程,代碼都有註釋,請查看:微信
object SVDComputer { val rootDir = "your_data_dir"; //本地測試 def main(args: Array[String]) { val conf = new SparkConf().setAppName("SVDComputer").setMaster("local[4]") val sc = new SparkContext(conf) val rawData = sc.textFile(rootDir + "/your_file_name") val tables = rawData.map { line => val cols = line.split("your_field_seperator") val appId = cols(0) val context = cols(1) (appId, context.split(" ")) } val numDocs = tables.count() //獲得每一個單詞在文章中的次數 -> 計算 tf val dtf = docTermFreqs(tables.values) val docIds = tables.keys.zipWithIndex().map{case (key, value) => (value, key)}.collect().toMap dtf.cache() //獲得單詞在全部文檔中出現的不一樣次數->計算 idf val termFreq = dtf.flatMap(_.keySet).map((_, 1)).reduceByKey(_ + _) //計算 idf val idfs = termFreq.map { case (term, count) => (term, math.log(numDocs.toDouble/count)) }.collect().toMap //將詞編碼 spark 不接受字符串的 id val termIds = idfs.keys.zipWithIndex.toMap val idTerms = termIds.map{case (term, id) => (id -> term)} val bIdfs = sc.broadcast(idfs).value val bTermIds = sc.broadcast(termIds).value //利用詞頻(dtf),逆文檔頻率矩陣(idfs)計算tf-idf val vecs = buildIfIdfMatrix(dtf, bIdfs, bTermIds) val mat = new RowMatrix(vecs) val svd = mat.computeSVD(1000, computeU = true) println("Singular values: " + svd.s) val topConceptTerms = topTermsInTopConcepts(svd, 10, 10, idTerms) val topConceptDocs = topDocsInTopConcepts(svd, 10, 10, docIds) for ((terms, docs) <- topConceptTerms.zip(topConceptDocs)) { println("Concept terms: " + terms.map(_._1).mkString(", ")) println("Concept docs: " + docs.map(_._1).mkString(", ")) println() } //dtf.take(10).foreach(println) } /** * * @param lemmatized * @return */ def docTermFreqs(lemmatized: RDD[Array[String]]): RDD[mutable.HashMap[String, Int]] = { val dtf = lemmatized.map(terms => { val termFreqs = terms.foldLeft(new mutable.HashMap[String, Int]){ (map, term) => { map += term -> (map.getOrElse(term, 0) + 1) map } } termFreqs }) dtf } /** * 創建 tf-idf 矩陣 * @param termFreq * @param bIdfs * @param bTermIds * @return */ def buildIfIdfMatrix(termFreq: RDD[mutable.HashMap[String, Int]], bIdfs: Map[String, Double], bTermIds: Map[String, Int]) = { termFreq.map { tf => val docTotalTerms = tf.values.sum //首先過濾掉沒有編碼的 term val termScores = tf.filter { case (term, freq) => bTermIds.contains(term) }.map { case (term, freq) => (bTermIds(term), bIdfs(term) * freq / docTotalTerms) }.toSeq Vectors.sparse(bTermIds.size, termScores) } } def topTermsInTopConcepts(svd: SingularValueDecomposition[RowMatrix, Matrix], numConcepts: Int, numTerms: Int, termIds: Map[Int, String]): Seq[Seq[(String, Double)]] = { val v = svd.V val topTerms = new ArrayBuffer[Seq[(String, Double)]]() val arr = v.toArray for (i <- 0 until numConcepts) { val offs = i * v.numRows val termWeights = arr.slice(offs, offs + v.numRows).zipWithIndex val sorted = termWeights.sortBy(-_._1) topTerms += sorted.take(numTerms).map{case (score, id) => (termIds(id), score)} } topTerms } def topDocsInTopConcepts(svd: SingularValueDecomposition[RowMatrix, Matrix], numConcepts: Int, numDocs: Int, docIds: Map[Long, String]): Seq[Seq[(String, Double)]] = { val u = svd.U val topDocs = new ArrayBuffer[Seq[(String, Double)]]() for (i <- 0 until numConcepts) { val docWeights = u.rows.map(_.toArray(i)).zipWithUniqueId topDocs += docWeights.top(numDocs).map{case (score, id) => (docIds(id), score)} } topDocs } }
上面代碼的運行結果以下所示,只給出了前10個概念最相關的十個單詞和十個文檔:app
Concept terms: 彩票, 記帳, 開獎, 理財, 中獎, 大方, 大樂透, 競彩, 收入, 開發 Concept docs: com.payegis.mobile.energy, audaque.SuiShouJie, com.cyht.dcjr, com.xlltkbyyy.finance, com.zscfappview.jinzheng.wenjiaosuo, com.wukonglicai.app, com.goldheadline.news, com.rytong.bank_cgb.enterprise, com.xfzb.yyd, com.chinamworld.bfa Concept terms: 茂日, 廁所, 洗浴, 圍脖, 郵局, 樂得, 大王, 藝龍, 開開, 茶館 Concept docs: ylpad.ylpad, com.xh.xinhe, com.jumi, com.zjzx.licaiwang168, com.ss.app, com.yingdong.zhongchaoguoding, com.noahwm.android, com.ylink.MGessTrader_QianShi, com.ssc.P00120, com.monyxApp Concept terms: 彩票, 開獎, 投注, 中獎, 雙色球, 福彩, 號碼, 彩民, 排列, 大樂透 Concept docs: ssq.random, com.wukonglicai.app, com.cyht.dcjr, com.tyun.project.app104, com.kakalicai.lingqian, com.wutong, com.icbc.android, com.mzmoney, com.homelinkLicai.activity, com.pingan.lifeinsurance Concept terms: 開戶, 證券, 行情, 股票, 交易, 炒股, 資訊, 基金, 期貨, 東興 Concept docs: com.byp.byp, com.ea.view, com.hmt.jinxiangApp, cn.com.ifsc.yrz, com.cgbsoft.financial, com.eeepay.bpaybox.home.htf, com.gy.amobile.person, wmy.android, me.xiaoqian, cn.eeeeeke.iehejdleieiei Concept terms: 貸款, 彩票, 開戶, 抵押, 證券, 信用, 銀行, 申請, 小額, 房貸 Concept docs: com.silupay.silupaymr, com.zscfandroid_guoxinqihuo, com.jin91.preciousmetal, com.manqian.youdan.activity, com.zbar.lib.yijiepay, com.baobei.system, com.caimi.moneymgr, com.thinkive.mobile.account_yzhx, com.qianduan.app, com.bocop.netloan Concept terms: 支付, 理財, 銀行, 信用卡, 刷卡, 金融, 收益, 商戶, 硬件, 收款 Concept docs: com.unicom.wopay, com.hexun.futures, com.rapidvalue.android.expensetrackerlite, OTbearStockJY.namespace, gupiao.caopanshou.bigew, com.yucheng.android.yiguan, com.wzlottery, com.zscfappview.shanghaizhongqi, com.wareone.tappmt, com.icbc.echannel Concept terms: 行情, 理財, 投資, 比特幣, 黃金, 匯率, 資訊, 原油, 財經, 貴金屬 Concept docs: com.rytong.bankps, com.souyidai.investment.android, com.css.sp2p.invest.activity, com.lotterycc.android.lottery77le, com.sub4.caogurumen, com.feifeishucheng.canuciy, com.hundsun.zjfae, cn.cctvvip, com.mr.yironghui.activity, org.zywx.wbpalmstar.widgetone.uex11328838 Concept terms: 信用卡, 行情, 刷卡, 硬件, 匯率, 比特幣, 支付, 交易, 商戶, 黃金 Concept docs: com.unicom.wopay, com.hexun.futures, gupiao.caopanshou.bigew, com.rytong.bankps, com.souyidai.investment.android, com.feifeishucheng.canuciy, com.css.sp2p.invest.activity, org.zywx.wbpalmstar.widgetone.uex11328838, com.net.caishi.caishilottery, com.lotterycc.android.lottery77le Concept terms: 行情, 刷卡, 硬件, 記帳, 貸款, 交易, 資訊, 支付, 易貸, 比特幣 Concept docs: com.unicom.wopay, com.shengjingbank.mobile.cust, com.rytong.bankps, com.souyidai.investment.android, com.silupay.silupaymr, aolei.sjcp, com.css.sp2p.invest.activity, com.megahub.brightsmart.fso.mtrader.activity, com.manqian.youdan.activity, gupiao.caopanshou.bigew Concept terms: 刷卡, 硬件, 支付, 匯率, 換算, 貸款, 收款, 商戶, 貨幣, 易貸 Concept docs: com.unicom.wopay, OTbearStockJY.namespace, com.yucheng.android.yiguan, com.wzlottery, com.zscfappview.shanghaizhongqi, com.junanxinnew.anxindainew, com.bitjin.newsapp, com.feifeishucheng.canuciy, org.zywx.wbpalmstar.widgetone.uexYzxShubang, com.qsq.qianshengqian
從上面的結果能夠看出,效果還行,這個和語料庫太少也有關係。每一個概念都比較集中一個主題,好比第一個概念關心的是彩票等。具體應用就不展開了。dom
基於 SVD 的 LSA 技術對理解文檔含義仍是有侷限的,我的認爲這方面效果更好的技術應該是 LDA 模型,接下來也有有專門的文章講解基於 Spark 的 LDA 模型,盡情期待。
歡迎關注本人微信公衆號,會定時發送關於大數據、機器學習、Java、Linux 等技術的學習文章,並且是一個系列一個系列的發佈,無任何廣告,純屬我的興趣。