翻譯自: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
檔案:
scala
val postsXML = sc.textFile("hdfs:///user/srowen/Posts.xml")
這時命令列工具會返回:
postsXML: org.apache.spark.rdd.RDD[String] = MappedRDD[1] at textFile at :12
顯示文字檔案已轉化為一個String型的RDD,你可以通過呼叫RDD的函式,實現任意的查詢運算。比如統計檔案的行數:
scala
postsXML.count
這條指令生成大量的輸出,顯示Spark正在利用分散式的環境計數,最終列印出18066983
。
下一步,將XML檔案的每一行都存入形如(questionID, tag)
的元組。得益於Scala的函數語言程式設計的風格,RDD和Scala集合一樣可以使用map等方法:
scala
val postIDTags = postsXML.flatMap { line => // Matches Id="..." ... Tags="..." in line val idTagRegex = "Id="(\d+)".+Tags="([^"]+)"".r // // Finds tags like <TAG> value from above val tagRegex = "<([^&]+)>".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而非字串作為惟一標識,而問題的標籤資料是字串格式的,所以需要把字串雜湊成一個非負整數,同時保留非負整數到字串的對映。這裡我們先定義一個雜湊函式以便複用。
scala
def nnHash(tag: String) = tag.hashCode & 0x7FFFFF var tagHashes = postIDTags.map(_._2).distinct.map(tag =>(nnHash(tag),tag))
現在把元組轉換為ALS計算所需的輸入:
scala
import 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的介面來獲取建議的標籤,我們可以簡單定義一個:
scala
def 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個標籤:
scala
recommend(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
真實的問題標籤是postgresql
、query-optimization
、substring
和text-search
。不過,預測結果也有一定的合理性(postgresql
經常和ruby-on-rails
一起出現)。
當然,以上的示例還不夠優雅和高效,但是,我希望所有來自R的分析師、鼓搗Python的黑客和熟悉Hadoop的開發者,都能從中找到你們熟悉的部分,從而找到一條適合你們的路徑去探索Spark,並從中獲益。
來自:建造者說