(寫於2014/06/30, 這是第一篇)
背景
之前提到一直在寫一個數據挖掘工具包的小項目dami(該項目不再維護,並在未來刪除),由於忙碌等各種原因進度很慢甚至停滯不前,索性把項目調整,以提高技術爲目的,重新做成一個解決方案的平臺,也就是這裏要介紹了feluca;之後會將dami算法部分調整移動到這裏。
feluca是JAVA實現的,目標就是搭建一個簡易使用,簡易編程的數據挖掘工具平臺,並且包含分佈式計算, 目標有點類似於weka和老hadoop。當然“簡易“這兩字只是我自己的願望,未必符合大衆標準。然而這個目標確實太大了,尤其只有我一個人而且是業餘時間慢慢做,因此這個項目我也把他預期爲一個精緻的玩具就滿足了。有興趣的朋友歡迎一起來練手,項目在慢慢進行中,代碼放在github上:https://github.com/lgnlgn/feluca。
設計概覽
分佈式思考
架構
寫於2014/07/30
架構
接着之前說到的架構設計。架構設計其實很早就在心裏畫好了,當然草稿紙上也畫過,之後爲了鞏固記憶在processon上畫了幾幅圖。而在實現架構的編碼過程卻比想象中的難,想象太美好實際上難以在想象的時間內完成,可以說是功力不夠或者經驗不足。現在終於體會到一個項目最初要想做成一個完整可用的東西,是多麼的難(這估計也是很多老闆創業失敗的一個直接原因,高估自己低估困難),在寫博文的時間點,我已經決定不再繼續把這個項目做下去了,只是做些收尾工作,但是文檔會慢慢記錄下來,作爲一個過程的見證。
feluca包含4個部分:feluca-core [功能與分佈式管理]、feluca-cargo [數據處理]、 feluca-sail [單機算法]、feluca-paddle [分佈式模型與算法]。sail和paddle分別代表帆和槳,單機和分佈式上取名上選擇:"風越大越快;有槳就能劃" VS "船帆只有一塊;槳越多越快"。最後選了後者。feluca要交互可用,所以選擇HTTP的REST的方式,這在core中實現。如果只想用單機算法(依賴cargo),那也可以支持嵌入式開發,也就是其中的數據處理和單機算法可以以jar的方式使用,這兩個很大程度繼承dami的代碼。
feluca節點分爲leader和worker,任務提交在leader,可以有單機任務和分佈式任務。我只實現了基本的功能,而且限制一次只能進行一個單機和分佈式任務。job管理也十分簡單,主job生成之後,提交到後臺線程,它會不斷的與子任務探測結果。這裏我代碼級抽象了一下導致做了很多應該沒什麼意義的工作。leader和worker都持有一樣的jobmanager, 當leader接受的是單機任務時候,需要轉換成一到多個子任務在新線程或者process中完成;當leader接受的job是分佈式時候,要首先生成很多子任務,這些子任務在worker那就變成"主"任務,worker也做一遍單機子任務那樣的事。雖然看起來沒問題,但是分佈式任務leader的子任務和worker的主任務是一樣的,而兩種節點jobmanager也是一樣,那麼中間就得協調和判斷。
feluca的分佈式文件系統其實只有簡單的邏輯,即數據再leader節點的硬盤上切割好,需要做分佈式計算的時候,分發到worker節點硬盤上。分發方式是用ftp,即在leader開了ftp,分發其實是worker的拉取。feluca節點管理通過zookeeper來協調,在這只是簡簡單單的可見而已,當然zookeeper功能還很多,用來做跨進程協調很合適,我在分佈式算法中用到了。
起初feluca設計成可以在及其普通的機器上能用的平臺,支持單機和分佈式。那麼單機算法最少也是半邊天了。但是單機算法其實好做,除了數據格式有自己的定義之外,算法只要公開了那就是一定能實現的,最多性能上有差異。而算法分佈式化,對我來說才新鮮,需要在很多點上充分考慮計算機部件性能。想做得通用一點就要有一定抽象,我懂的算法不多,當然也抽不出太高層的,借鑑改進是最好的。一個算法能分佈式或者並行化首先得算法本身支持,或者近似支持,只有在算法細節上分析出並行的地方,和必須同步交換的地方纔能最大化進行分佈式。mapreduce當然是一個很基礎的範式,另外hadoop的計算本地化的思路也很有借鑑意義,計算是跟隨數據一起的。
分佈式算法
數據挖掘算法就是要從數據中抽取出能反映或表達數據的模型,模型通常遠遠小於數據,比如121212121212121212 的模型就可以表示成(12)^9 。數據挖掘側面看就是壓縮,當然壓縮也沒有統一標準,之前有博文提過。從空間角度看算法過程可以簡單劃分爲 數據---(通過計算)-->模型。分佈式的情況下,數據必然是需要切分的,模型也可能根據需要需要被切分到多個節點,計算通常空間需要不大則可以選擇是依附在數據還是模型節點。mapreduce中 mapper和reducer中的計算是獨立可並行的,map完成到reduce才實現了數據一次交換。對於很多算法來說粒度太大。對很多算法來說一次迭代可能需要多次或者不同粒度的數據交換,要想讓算法飛起來,就得自己控制數據同步的方式和節奏。
因爲切分了數據和模型,它倆肯定不在一個進程中。以LR爲例,公式是:y= 1/(1+ e^[ w0x0 + w1x1 +... w9x9...]) 需要把x向量學習出來,學習保準是預測y和真實偏差最少,問題正好可以用SGD方式求解。算法模型是一個向量,基本過程是:每一個或者多個樣本獲取當前模型,根據樣本特徵和模型計算出error之後根據error計算出需要特徵調整的偏移量從而更新模型。如果按計算在數據還是模型這邊分:
計算依附數據方式,一次數據迭代:1.本地讀取出數據,2.本地跨進程獲取數據所需要的模型,3.本地計算出模型error和偏移量,4.本地跨進程更新模型偏移量。
耗時的部分只有4個,這樣的方式簡單直接;需要一次本地IO、一次CPU計算、兩次跨進程通信,即RPC;但如果考慮模型佔空間就會引出幾個問題,1如果太大,每次迭代需要的通信的數據量是否可能過大,反而比數據還多?2 模型是否切分?如果不切分就是每個計算節點都一樣大的模型只是每次同步不一樣的數據,模型太大就不行了;3切分的話那麼每次傳輸回來的小模型讀取起來就要耗時了,例如特徵的權重都是拿id作爲下標,一旦需要壓縮到小模型,id就需要進行hash等操作,而且需要進行不斷垃圾回收;
這幾個問題其實在實現上已經遇到了;首先我是按切分-壓縮的方式來做;在SGDLR中每次迭代消耗看起來還行,還遠比不上單機效率。但是這個方式在處理推薦的SVD時候就很慢了,估計是因爲模型很大每次都要傳輸幾十倍於向量的數據量導致的。這裏嚴重挫敗了我的積極性,思考之後,分析只能靠取消壓縮來提高,但對於傳輸量沒有任何辦法,只能考慮另一種方式:計算依附在模型這邊。
計算依附模型方式,一次數據迭代:1. 數據節點讀取數據,2. 按照特徵RPC分發到不同模型切片節點,3. 模型節點計算出部分結果RPC回原來的數據節點,4.數據節點收回所有結果合併得出error,並將偏差通知模型節點,5.模型節點根據error計算出模型需要更新的偏移量並更新。
這種方式與上一種最大不同就是,計算可以保證與模型在一起並且不需要hash等其他方式轉換,因此計算效率保證了,還有個好處就是過程其實和流計算非常接近。這種方式就是RPC傳輸主要是數據,傳數據恐怕更不科學了。這兩種方式從原則上就沒有優劣而是取決於數據與模型的關係,通常模型遠小於數據但是對於特定不同計算來說未必通信的模型量未必會小。而且實現起來肯定比第一種要難,所以我一開始就考慮如何優化,後來想到了其實1 2 兩步可以合併到一起,一開始就按特徵切分好,以本地讀取的方式進行,那麼本地其實也可以和模型合在一起了。第3步 變成只需要合併所有切分集上的模型數據w0x0這些,也就是 任意計算節點在第4第5步必要的error需要知道全局的w * x 纔算完整。 所以整個過程變得更簡單了: 1. 計算節點讀取本地只有部分特徵的數據, 2 根據這些部分特徵獲取部分模型發送到中介節點,等待其合併好,還原完整結果回來。 3. 根據這個結果計算error 並計算自己的模型上的偏移量。
整個過程變得步驟更少,而實際上是利用了數據已經按特徵切分的這點優勢。所以這個方式比第一種佔了便宜,也是一個重要區分,即計算依附於數據的,數據是水平切分;計算依附於模型的,數據是要縱向切分。縱向切分數據也有自己要考慮的地方,比如水平切分可以起任意的節點,而縱向的則不行,一旦切好就意味着模型也得這麼切。不管哪種方式都沒逃出現有計算框架的範疇,只是粒度和控制上交給自己了而已,所以流計算現在很火很成功,未來估計會更加。
寫在最後
我在實現了第二種方式的SGDLR看到比第一種方式效率更高一些,也思考過如何切分數據,但已經不打算繼續做了,最多把SVD部分補上比較一下,以及在多臺機器上感受一下有限內存下的並行化的算法是否真能如願提升效率。項目沒完成是一個遺憾,未來我將把時間用來進行一些代碼整理和文檔記錄。在代碼過程中因爲不熟悉別人的輪子而自己造,感覺只有辛苦和挫敗。比如cargo中數據讀取本來可以簡單用一些開源的序列化方式,但我爲了講究效率希望IO能與CPU計算疊加起來,就自己折騰了一套數據格式和生產者消費者實現。前者只有一兩天的工作量,而後者得數十倍;再比如期間我還想用akka來實現這套系統,本來能用的zkclient想用curator替換;就跑去看了些文檔浪費了時間缺沒得到有價值的結果;還比如上面所說的Job管理。只是後來慢慢發現這樣會拖死自己纔開始拋棄太好的設想,分佈式算法的實現部分開發效率就明顯高了,這也是年輕經驗不足的表現,必然要挖的坑吧。