為什麼Spark將成為資料科學家的統一平臺

2shou發表於2015-03-22

翻譯自:Why Apache Spark is a Crossover Hit for Data Scientists,有刪減。

Spark是一個超有潛力的通用資料計算平臺,無論是對統計科學家還是資料工程師。

資料科學是一個廣闊的領域。我自認是一個資料科學家,但和另外一批資料科學家又有很多的不同。資料科學家通常分為統計科學家和資料工程師兩個陣營,而我正處於第二陣營。
統計科學家使用互動式的統計工具(比如R)來回答資料中的問題,獲得全景的認識。與之相比,資料工程師則更像一名程式設計師,他們在伺服器上編寫程式碼,建立和應用機器學習模型,熟悉C++和Java等系統級語言,經常需要和企業級資料中心的某些元件打交道,比如Hadoop。
而有的資料科學家專注於更細的領域,就像精通R但從未聽說過Python或者scikit-learn(反之亦然),即便兩者都提供了豐富的統計庫。

不完美的統計工具

如果可以提供一種統一的工具,執行在統一的架構,用統一的語言程式設計,並可以同時滿足統計科學家和資料工程師的需求,那該多好啊。我一開始就精通Java,難道為了研究資料,我就必須去學一種像Python或R的語言?我一直使用傳統的資料分析工具,難道為了應對大規模計算,就必須去懂MapReduce?正是統計工具的不完美造就了這種局面:

  • R提供了一個豐富的統計分析和機器學習的直譯器。但R難以在分散式條件下執行資料的分析和清洗,以便開展其所擅長的資料分析,也不以一種主流的開發語言為人所知。
  • Python是一種通用的程式語言,也不乏出色的第三方資料分析庫(像Pandas和scikit-learn),但Python也有和R一樣的缺陷:只能侷限在處理單機能負載的資料量。
  • 在經典的MapReduce計算框架上開發分散式的機器學習演算法是可行的(參考Mahout),但程式設計師需要從零開始,更別說移植複雜計算的難度。
  • 為降低複雜計算移植到MapReduce的難度,Crunch提供一個簡單的、傻瓜式的Java API,但MapReduce天生決定了它在迭代計算方面是低效的,儘管大多數機器學習演算法都需要迭代計算。

其他的資料科學工具一樣無法盡善盡美。基於Java和Hadoop的背景,我開始幻想一個理想的資料科學利器:一個像R和Python的能實現RPEL(讀取-估值-列印-迴圈)的自帶統計庫函式的命令列直譯器,又具備天然的分散式可擴充套件的屬性;擁有像Crunch一樣的分散式集合,而且能通過命令列直譯器呼叫。

Spark的優勢

這就是Spark讓我興奮的原因。大部分人討論到Spark時,總是注意到將資料駐留記憶體以提高計算效率的方面(相對MapReduce),但對我來說這根本不是關鍵。Spark擁有許多的特徵,使之真正成為一個融合統計科學和資料工程的交叉點:

  • Spark附帶了一個機器學習庫MLib,雖然只是在初始階段。
  • Spark是用Scala語言編寫的,執行在Java虛擬機器上,同時也提供像R和Python的命令列直譯器。
  • 對Java程式設計師,Scala的學習曲線是比較陡峭的,但所幸Scala可以相容一切的Java庫。
  • Spark的RDD(彈性分散式資料集),是Crunch開發者熟知的一種資料結構。
  • Spark模仿了Scala的集合計算API,對Java和Scala開發者來說耳熟能詳,而Python開發者也不難上手,而Scala對統計計算的支援也不錯。
  • Spark和其底層的Scala語言,並不只是為機器學習而誕生的,除此之外,像資料訪問、日誌ETL和整合都可以通過API輕鬆搞定。就像Python,你可以把整個資料計算流程搬到Spark平臺上來,而不僅僅是模型擬合和分析。
  • 在命令列直譯器中執行的程式碼,和編譯後執行的效果相同。而且,命令列的輸入可以得到實時反饋,你將看到資料透明地在叢集間傳遞與計算。

Spark和MLib還有待完善:整個專案有不少bug,效率也還有提升的空間,和YARN的整合也存在問題。Spark還沒辦法提供像R那樣豐富的資料分析函式。但Spark已然是世界上最好的資料平臺,足以讓來自任何背景的資料科學家側目。

實戰:Stack Overflow問題的自動標註

Stack Overflow是一個著名的軟體技術問答平臺,在上面提的每個問題有可能被打上若干個短文字的標籤,比如java或者sql,我們的目標在於建立一套系統,使用ALS推薦演算法,為新問題的標籤提供預測和建議。從推薦系統的角度,你可以把問題想象成user,把標籤想象成item
首先,從Stack Overflow下載官方提供的截至20140120的問答資料stackoverflow.com-Posts.7z
這是一個能夠直接用於分散式計算的bzip格式檔案,但在我們的場景下,必須先解壓並拷貝到HDFS:

bzcat stackoverflow.com-Posts.7z | hdfs dfs -put - /user/srowen/Posts.xml

解壓後的檔案大約是24.4GB,包含210萬個問題,1800萬個回答,總共標註了930萬個標籤,這些標籤排重之後大概是34000個。
確認機器安裝了Spark之後,輸入spark-shell即可開啟Scala的REPL環境。首先,我們讀取一個儲存在HDFS的Posts.xml檔案:

scalaval postsXML = sc.textFile("hdfs:///user/srowen/Posts.xml")

這時命令列工具會返回:

postsXML: org.apache.spark.rdd.RDD[String] = MappedRDD[1] at textFile at :12

顯示文字檔案已轉化為一個String型的RDD,你可以通過呼叫RDD的函式,實現任意的查詢運算。比如統計檔案的行數:

scalapostsXML.count

這條指令生成大量的輸出,顯示Spark正在利用分散式的環境計數,最終列印出18066983
下一步,將XML檔案的每一行都存入形如(questionID, tag)的元組。得益於Scala的函數語言程式設計的風格,RDD和Scala集合一樣可以使用map等方法:

scalaval postIDTags = postsXML.flatMap { line =>
  // Matches Id="..." ... Tags="..." in  line
  val idTagRegex = "Id="(\d+)".+Tags="([^"]+)"".r

  // // Finds tags like <TAG> value from above
  val tagRegex = "&lt;([^&]+)&gt;".r

  // Yields 0 or 1 matches:
  idTagRegex.findFirstMatchIn(line) match {
    // No match -- not a  line
    case None => None
    // Match, and can extract ID and tags from m
    case Some(m) => {
      val postID = m.group(1).toInt
      val tagsString = m.group(2)
      // Pick out just TAG matching group
      val tags = tagRegex.findAllMatchIn(tagsString).map(_.group(1)).toList
      // Keep only question with at least 4 tags, and map to (post,tag) tuples
      if (tags.size >= 4) tags.map((postID,_)) else None
     }
  }
  // Because of flatMap, individual lists will concatenate
  // into one collection of tuples
}

你會發現這條指令的執行是立即返回的,而不像count一樣需要等待,因為到目前為止,Spark並未啟動任何主機間的資料變換。
ALS的MLib實現必須使用數值ID而非字串作為惟一標識,而問題的標籤資料是字串格式的,所以需要把字串雜湊成一個非負整數,同時保留非負整數到字串的對映。這裡我們先定義一個雜湊函式以便複用。

scaladef nnHash(tag: String) = tag.hashCode & 0x7FFFFF
var tagHashes = postIDTags.map(_._2).distinct.map(tag =>(nnHash(tag),tag))

現在把元組轉換為ALS計算所需的輸入:

scalaimport org.apache.spark.mllib.recommendation._
// Convert to Rating(Int,Int,Double) objects
val alsInput = postIDTags.map(t => Rating(t._1, nnHash(t._2), 1.0))
// Train model with 40 features, 10 iterations of ALS
val model = ALS.trainImplicit(alsInput, 40, 10)

這一步生成特徵矩陣,可以被用來預測問題與標籤之間的關聯。由於目前MLib還處於不完善的狀態,沒有提供一個recommend的介面來獲取建議的標籤,我們可以簡單定義一個:

scaladef recommend(questionID: Int, howMany: Int = 5): Array[(String, Double)] = {
  // Build list of one question and all items and predict value for all of them
  val predictions = model.predict(tagHashes.map(t => (questionID,t._1)))
  // Get top howMany recommendations ordered by prediction value
  val topN = predictions.top(howMany)(Ordering.by[Rating,Double](_.rating))
  // Translate back to tags from IDs
  topN.map(r => (tagHashes.lookup(r.product)(0), r.rating))
}

通過上述函式,我們可以獲得任意一個問題比如ID為7122697的How to make substring-matching query work fast on a large table?的至少4個標籤:

scalarecommend(7122697).foreach(println)

推薦結果如下所示:

scala(sql,0.17745152481166354)
(database,0.13526622226672633)
(oracle,0.1079428707621154)
(ruby-on-rails,0.06067207312463499)
(postgresql,0.050933613169706474)

注意:
– 每次執行得到的結果不盡相同,是因為ALS是從隨機解開始迭代的
– 如果你希望獲得實時性更高的結果,可以在recommend前輸入tagHashes = tagHashes.cache

真實的問題標籤是postgresqlquery-optimizationsubstringtext-search。不過,預測結果也有一定的合理性(postgresql經常和ruby-on-rails一起出現)。
當然,以上的示例還不夠優雅和高效,但是,我希望所有來自R的分析師、鼓搗Python的黑客和熟悉Hadoop的開發者,都能從中找到你們熟悉的部分,從而找到一條適合你們的路徑去探索Spark,並從中獲益。

來自:建造者說

相關文章