一個簡易的數據挖掘計算平臺項目: feluca

(這篇博文會當做記事來寫,不完全是技術文)

(寫於2014/06/30, 這是第一篇)

背景

        之前提到一直在寫一個數據挖掘工具包的小項目dami(該項目不再維護,並在未來刪除),由於忙碌等各種原因進度很慢甚至停滯不前,索性把項目調整,以提高技術爲目的,重新做成一個解決方案的平臺,也就是這裏要介紹了feluca;之後會將dami算法部分調整移動到這裏。

       feluca是JAVA實現的,目標就是搭建一個簡易使用,簡易編程的數據挖掘工具平臺,並且包含分佈式計算, 目標有點類似於weka和老hadoop。當然“簡易“這兩字只是我自己的願望,未必符合大衆標準。然而這個目標確實太大了,尤其只有我一個人而且是業餘時間慢慢做,因此這個項目我也把他預期爲一個精緻的玩具就滿足了。有興趣的朋友歡迎一起來練手,項目在慢慢進行中,代碼放在github上:https://github.com/lgnlgn/feluca

設計概覽

      1. feluca被設計成支持能算法嵌入式開發,能直接使用的兩種方式。前者繼承dami項目的高效思路,需要將文本數據轉換成特定二進制格式,提供一些算法並將結果輸出到本地;後者則需要一個應用服務來支持各種比較友好的操作,讓他人可以根據一些藉口開發jar包也能輕鬆在服務中使用。
       2. 那麼feluca就需要像hadoop一樣有一套自己的管理調度平臺,能運行單機算法,以及分佈式算法。面對分佈式環境需要考慮很多方面的問題和細節,當前我還沒有那麼深的功力,因此分佈式功能上一切從簡,只允許一個分佈式算法運行,並且先不支持容錯機制。
       3. 操作應用通過一系列的rest接口來完成,未來當然也要提供一個web瀏覽的UI界面,預計就像Kibana之於Elasticsearch那樣。
       4. 單機算法和分佈式算法採用統一的數據格式,並且儘可能保持同一算法模型在單機和分佈式兩種環境都能一樣。這其實是我對於這個項目最核心的訴求,我本來也是爲了想做一些在普通PC上能運行海量數據挖掘的可行分佈式算法,雖然對很多環境單獨的瓶頸有概念但串一起的算法缺沒有經驗,還不知道分佈式算法是否能比單機更快?所以爲了完成這些嘗試,還得先寫出一個平臺。

分佈式思考

       提到分佈式計算,就不能不提hadoop爲代表的各種解決方案,爲什麼還需要自己寫一個?
       首先 是爲了上述第四點的要求自己做一個可能會更容易直接實現一些特殊的算法,摸到算法的精髓;
       其次 爲了提高架構水平,使用hadoop spark storm等工具沒有自己實現一個對架構技術提高多。當然我認爲精通工具也是非常厲害,掌握一門可以打天下的技能很重要,可能甚至比這樣折騰更實惠。
       那爲啥不直接在那些開源工具上搭建自己的應用,就像mahout那樣?
       其實我也有思考過,可能是由於開源東西太多,要學習起來太費事。我也考慮過用現成的東西可以避免大量代碼勞動和錯誤發生,中間也反覆過,不過已經寫了一部分索性就硬着頭皮繼續下去吧,技術的提高就是要靠反覆整理才能沉澱經驗下來,當然未來也會考慮割肉情況發生,但是這個項目終究不是一個上生產環境的,可以不用那麼認真。

架構

待續……

寫於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管理。只是後來慢慢發現這樣會拖死自己纔開始拋棄太好的設想,分佈式算法的實現部分開發效率就明顯高了,這也是年輕經驗不足的表現,必然要挖的坑吧。

(寫於2014/08/08)

分佈式模型

         上一篇提到我想到了兩種模型,一種是計算依附數據方(後面簡稱COD),一種是計算依附於模型方(COM);後來簡單分析得出結論是COM更完美,因爲它佔了數據預先切分的便宜,並且減少了模型同步的數據通信量。但我覺得COD依然是有適用場合的,雖然最早的那個實現效果太差了點。於是我思考如何改良,兩個問題都是之前就意識到的,第一是之前說過模型是否切分,於是轉而使用不切分的方式:每個計算節點所用模型一致,可以避免反覆GC,另外特徵ID也不用hash來轉換;第二是每次拉取最新模型改成每次從本機推出去,這樣有兩個的好處: 1.是每次推送數據必須把當前需要處理的數據按hash規則切分到多臺機,也就是一個數組轉成多個數組,這個步驟對某一次來說沒什麼但是對反覆計算來說就增加GC壓力了,如果要避免GC就得開足夠大的緩存數組,那不就是一個模型了嗎;2是轉成推送至需要把自己的一份推往所有的節點,組織起來更簡單。於是COM方式,模型必須在每個計算節點上都有,那麼和COD就很接近了。
       這樣就完成了模型架構的設計,這樣應該是靠譜了,之前想過COD COM可以在不同場合發揮作用。後來自己也想到個反例:數據集要麼按樣本行切分(hadoop方式),要麼按特徵列來切分;不可能有一種切分方式使得一個塊裏的行和列不再出現在其他塊裏。所以雖然之前舉了個例子SGDLR或者pagerank都可以用COM來完成,但只是因爲它們對行沒有要求,而協同過濾問題就不可避免了,所以COD是必然得保留的。

合在一起

        每個計算節點都配一個算法模型,模型計算時候更新直接就在內存中完成,需要同步的時候調用RPC把本機的部分數據推到其他計算節點,其他節點也推他們部分到這裏同步;如果計算是依賴全部特徵,就通過計算中介來合併,也就是一個reduce操作。
        合在一起以後COM的部分模型變成完整模型了,似乎有點遺憾,其實不然,還是可以切的,特徵部分的模型還是可以切,樣本行的是不能的。比如協同過濾的SVD中itemspace可以切分;但userspace不能;
        不管是模型推送還是reduce操作都涉及一個問題,是同步還是異步?還是先用同步簡單,例如reduce階段必須用等到其他節點的數據都送到了才能計算返回(這裏順便提一下遇到的bug,我最早用countdownlatch來阻塞,讓當所有線程都countdown後await出去,由一個線程加鎖計算,設置標記位和重置latch,避免其他線程再次計算。類似雙檢鎖;後來測試時候發現不定期卡住,百思不得其解,一步步打印才發現是 countdown和await之間雖然是寫在一塊,但是某個線程可能先搶到加鎖計算完成返回又跳進一個新的reduce階段,先於另外一個還沒await的線程,導致標記紊亂,互相鎖住。後來改成進來出去各用一個cyclicbarrier來阻塞就好了,所以寫併發程序還是得小心)。
        合在一起的模型,只有一個要求,數據按特徵ID縱向切分。

效果

       重寫了SGDLR和SVD來看效果,即便是避免了同模型推送的僞分佈式算法,也比單機效果要差。當然這也是預料之中,畢竟增加了很多其他的調用;在一臺機器上起兩個線程另一種僞分佈式,則比上一種更慢了。感覺還是超了點預期的,可能是因爲雙線程讀取文件變得更慢了,可能是同步部分增加了過多開銷(必須等最慢的那一個,不管當前是哪個線程最慢),但比上一次那種沒法估計的要好很多了,雖然期間也遇的bug一度讓我覺得是不是模型失控,下一步就是嘗試一下多機器的環境了。另外出現了一個功能沒法推進:讀取向量時候想用一個隊列來完成生產者消費者模式,可是出了奇怪bug就放棄了。

本次總結

       打到這個效果,應該說我個人已經滿意,起初對這個項目的期待過高,以爲能做成一個弱版hadoop,帶來相關技術方面重大提升。但後來發現能力有限,做不到,就算能按預期完成功能,提升也有限,因爲簡化過的架構等於迴避不到關鍵技術了。最後發現自己通過這個項目得到的有幾個:curator、msgpack、guava的開始使用(避免各種造輪其實非常重要)、一個併發問題經驗、分佈式算法架構設計經驗(這個算是真正到手了)、簡單的分佈式任務管理,等等吧。
       前面也說過,這個項目將不再繼續了,和漫畫可以畫幾十年不同,軟件生命力非常短。這個項目的種子是2年前種下的,雖然當時覺得高大上,現在想想也不比那些還在寫分詞,寫框架的人強。項目能有這樣和預期差不是太遠的效果怎麼說也算一個可以接受的結果了,比很多半途而廢的項目還是好很多的。
       未來我會把代碼整理好,文檔寫好,給它畫上完整句號。


(寫於2016/01/27)

分佈式模型

我又快速讀了一遍之前的文字,愣是沒明白,但我感覺之前的想法實在囉嗦。然後看了下代碼,有爲模型設計了client service, 搞得我已經完全記不得當初的設計了。雖然已經不記得爲啥會有那麼麻煩,但是我現在想到應該很簡單:只交互需要通信的中間結果,而不一定需要傳什麼模型,考慮壓縮什麼亂七八糟的。之前已經說了數據預先切分是避免過程還需要傳輸的先決條件,以及最大優勢,那麼計算過程中應該傳輸哪些東西?當然是越少越好,那麼就應該重新考量算法的計算流程,看如何把需要同步的數據縮減到最小。
    以sgd實現的邏輯迴歸爲例,如果不考慮正則化因子, 計算的時候 拿到 w*x 這個值,才能繼續算梯度和更新,那麼如果並行化,每個模型節點需要的,其實也只是全局的w*x,那麼每個節點只要同步它就夠了,w是模型, x是數據,都已經縱向切分到節點上了,但是每個節點只包含部分w*x,  它們之間是求和關係,需要RPC一次進行reduce. 而這個求和,可以在本節點先完成部分w*x 成一個值,也就是預先combine, 再由一個reducer完成彙總即可。到此每個節點只需要通信一個float就夠了。reducer完成sum之後,結果發回每個節點,計算節點就能繼續完成右面的計算梯度,如果更新模型還需要一次w*x 就再通信一次。 同理用到factoriztion machine上也是一樣的, 本機也只需要完成w*x + f*f ; 後面的f*f是分解,而也是不需要傳輸的,只需要完成這個值求和而已。
    當然上面的想法不知道有沒有問題,因爲我好久沒研究過梯度求導公式,不記得是否只是本機w*x 就足夠完成LR需要的資源了。但是關鍵在於要讓模型計算所需的數據在一個地方計算,只同步通信全局的變量。換個pagerank說,之前博文有提過pagerank最好的辦法是先倒排,把from ->(to, to, to)的行表達改成 to ->(from, from, from)的列表達;然後按預先切分思想, 每個節點只留部分的to->(from, from,from); 模型也只有from節點需要的那部分。在倒排數據的計算下,每個to節點是拉取到所有的from的求和pr值, 也就是pr*from, 正好也是需要一次reduce的求和! 於是本地算好自己的pr*from, 由reducer求和完後拿回來,就是to的最新pr值了(當然不是每個節點都有這個to, 有該to模型數據的更新一下就行,沒有就不用理)。
    所以根本不用考慮模型傳輸這麼複雜的事。之前的代碼看了一下,想起來當時也真夠有勁。所以這樣纔回到了最初的目的,就是考察哪些分佈式算法能更好地分佈,把中間通信降到最低是簡化問題和實現的關鍵。我也希望改天能重新實現目前的簡單想法,雖然這個項目不再繼續了,但也不至於爛尾掉。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章