Spark數據挖掘-基於 LSA 隱層語義分析理解APP描述信息(1)

Spark數據挖掘-基於 LSA 隱層語義分析理解APP描述信息(1)

1 前言

結構化數據處理比較直接,然而非結構化數據(好比:文本、語音)處理就比較具備挑戰。對於文本如今比較成熟的技術是搜索引擎,它能夠幫助人們從給定的詞語中快速找到包含關鍵詞的文本。可是,一些狀況下人們但願找到某一個概念的文本,而不關心文本里面是否包含某個關鍵詞。這種狀況下應該如何是好?
隱語義分析(Latent Semantic Analysis,簡稱:LSA)是一種尋找更好的理解語料庫中詞和文檔之間關係的天然語言和信息檢索的技術。它試圖經過語料庫提取一系列概念。每一個概念對應一系列單詞而且一般對應語料庫中討論的一個主題。先拋開數據而言,每個概念由三個屬性構成:css

  • 每一個文檔與概念之間的相關性
  • 每一個單詞與概念之間的相關性
  • 概念描述數據集變化程度(方差)的重要性得分

好比:LSA可能會發現某個概念和單詞「股票」、「炒股」有很高的相關性而且和「互聯網金融系列文章」有很高的相關性。經過選擇最重要的概念,LSA能夠去掉一些噪音數據。 在不少場合均可以使用這種簡潔的表示,好比計算詞與詞、文檔與文檔、詞與文檔的類似性。經過LSA獲得的關於概念的得分,能夠對語料庫有更加深刻的理解,而不僅是簡單的計算單詞或者共現詞。這種類似性度量能夠解決同義詞查詢、文本按照相同主題聚類、給文本添加標籤等。 LSA主要用到的技術就是奇異值分解。首先獲得詞-文檔重要性矩陣(通常是TF-IDF矩陣),而後利用svd奇異值分解技術獲得原矩陣近似相等的三個矩陣的乘積:SVD,其中 S 能夠看出概念與文件的關係,V 表示概念的重要程度,D 表示概念與詞的關係。
下面將完整講述經過爬蟲抓取豌豆莢App信息以後,如何利用Spark讀取數據,對文本分詞、去除噪音詞、將數據轉換爲數字格式、最後計算SVD而且解釋如何理解和使用獲得的結果。java

2 數據集(豌豆莢APP數據)

爬蟲不是本文的重點,有興趣的讀者能夠查看做者構建的開源爬蟲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: "銀行"}]

3 數據清洗

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

  • FileUtils common-io
  • StringUtils common-lang
  • JsonUtil fastjson

4 分詞

分詞主要使用的是 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;
}

5 Spark加載數據SVD計算

SVD計算以前必須獲得一個矩陣,本文使用的是TF-IDF矩陣,TF-IDF矩陣能夠理解以下:json

  • TF: token frequent 指的是每一個單詞在文檔中出現的頻率 = 單詞出現的個數/文檔中總單詞數
  • IDF:inverse document frequent 指的是逆文檔頻率 = 1/文檔頻率 = 總文檔數量/單詞在多少不一樣文檔中出現的次數

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

6 借題發揮

基於 SVD 的 LSA 技術對理解文檔含義仍是有侷限的,我的認爲這方面效果更好的技術應該是 LDA 模型,接下來也有有專門的文章講解基於 Spark 的 LDA 模型,盡情期待。

我的微信公衆號

歡迎關注本人微信公衆號,會定時發送關於大數據、機器學習、Java、Linux 等技術的學習文章,並且是一個系列一個系列的發佈,無任何廣告,純屬我的興趣。
Clebeg能量集結號

相關文章
相關標籤/搜索