大規模特徵構建實踐總結

大快搜尋DKH發表於2018-11-19


背景

一般大公司的機器學習團隊,才會嘗試構建大規模機器學習模型,如果去看百度、頭條、阿里等分享,都有提到過這類模型。當然,大家現在都在說深度學習,但在推薦、搜尋的場景,據我所知, ROI 並沒有很高,大家還是參考 wide&deep 的套路做,其中的 deep 並不是很 deep 。而大規模模型,是非常通用的一套框架,這套模型的優點是一種非常容易加特徵,所以本質是拼特徵的質和量,比如百度、頭條號稱特徵到千億規模。可能有些朋友不太瞭解大規模特徵是怎麼來的,舉個簡單的例子,假設你有百萬的商品,然後你有幾百個使用者側的 profile ,二者做個交叉特徵,很容易規模就過 10 億。特徵規模大了之後,需要 PS 才能訓練,這塊非常感謝騰訊開源了 Angel ,拯救了我們這種沒有足夠資源的小公司,我們的實踐效果非常好。

網上有非常多介紹大規模機器學習的資料,大部分的內容都集中在為何要做大規模機器學習模型以及 Parameter Server 相關的資料,但我們在實際實踐中,發現大規模的特徵預處理也有很多問題需要解決。有一次和明風(以前在阿里,後來去了騰訊做了開源的 PS angel )交流過這部分的工作為何沒有人開源,結論大致是這部分的工作和業務相關性大,且講明白了技術亮點不多,屬於苦力活,所以沒有開源的動力。

本文總結了蘑菇街搜尋推薦在實踐大規模機器學習模型中的特徵處理系統的困難點。我們的技術選型是 spark ,雖然 spark 的機器學習部分不能支援大規模(我們的經驗是 LR 模型的特徵大概能到 3000w 的規模),但是它非常適合做特徵處理。非常感謝組裡的小夥伴 @ 玄德 貢獻此文。

整體流程圖

這套方法論的特點是,雖然特徵規模很大,但是非常稀疏。我們對特徵集合進行 onehot 編碼,每條樣本的儲存需求很小。由於規模太大,編碼就變成一個比較嚴峻的問題。

連續統計類特徵:電商領域裡面,統計的 ctr gmv 是非常重要的特徵。

特徵構建遇到的問題

1. 特徵值替換成對應的數值索引過慢

組合後的訓練樣例中的特徵值需要替換成對應的數值索引,生成 onehot 的特徵格式。

特徵索引對映表 1 的格式如下 :

 

為了實現這種計算,我們需要對所有的特徵做 unique 編碼,然後將這個索引表 join 回原始的日誌表,替換原始特徵,後續流程使用編碼的值做 onehot ,但這部分容易 OOM ,且效能有問題。於是我們著手最佳化這個過程 .

首先我們想到的點是將索引表廣播出去 , 這樣就不用走 merge join, 不用對樣例表進行 shuffle 操作 , 索引表在比較小的時候 , 大概是 4KW 的規模 , 廣播出去是沒有問題的 , 實際內部實現走的是 map-side join, 所以速度也是非常快的 , 時間減少到一個小時內 .

當索引表規模達到 5KW 的時候 , 直接整表廣播 , driver gc 就非常嚴重了 , executor 也非常不穩定 , 當時比較費解 , 單獨把這部分資料載入到記憶體裡面 , 佔用量只有大約 executor 記憶體的 20% 左右 , 為啥 gc 會這麼嚴重呢 ? 後面去看了下 saprk 的原理 , 解決了心中的疑惑 . 因為 spark2.x 已經移除 HTTPBroadcast, 僅有的一種實現是 TorrentBroadcast. 實現原理類似於大家常用的 BT 下載技術。基本思想就是將資料分塊成 data blocks ,如果 executor 取到了一些 data blocks ,那麼這個 executor 就可以被當作 data server 了,隨著取到資料的 executor 越來越多,有更多的 data server 加入,資料就很快能傳播到全部的 executor 那裡去了 .

在廣播的過程中會將資料冗餘一份到 blockManager, 供其他 executor 進行讀取 . 其原理如圖所示 :

 

在廣播的過程中 , driver 端和 executor 端都會有短暫的時間達到 2 倍的記憶體佔用

dirver
driver 端先把資料序列成 byteArray, 切割成小塊的 data block 再廣播出去 , 切割的過程 , 記憶體會不斷接近 2 byteArray 的大小 , 直到切割完之後 , byteArray 釋放掉 .

executor
executor 裝載廣播的資料是 driver 的反過程 , 每次拿到一個 data block 之後 , 就將其存放到 blockManager, 同時通知 driver blockManagerMaster 說這個 block 多了一個儲存的地方 , 供其他 executor 下載 . executor 把所有的 block 都從其他地方拿全之後 , 先申請一個 Array[Byte], block 的資料進行反序列化之後得到原始資料 . 這個過程中和 driver 端應用 , 記憶體會不斷接近 2 倍資料的大小 , 直到反序列化完成 .

透過了解了 spark 廣播的實現 , 可以解釋廣播 5kw 維特徵的 gc 嚴重的問題 .

隨著實驗特徵的迭代 , 2 的列數會不斷的增多 , 處理時間會隨著列數的增多而線性增加 , 特徵索引的規模增多 , 會導致廣播的過程中 gc 問題越來越嚴重 , 直到 OOM 頻繁出現 .

這個階段需要解決 2 個問題

1. 需要高效得將表 1 的資料廣播到各個 executor

2. 不能使用 join 列的方式來實現替換索引值

綜合這兩個問題 , 我們想出了一個解決方案 , 將表 1 先按照特徵值排好序 , 然後再重新編碼 , 用一個長度為 max( 索引值 ) 長度的陣列去儲存 , 索引值作為下標 , 對應的元素為特徵值 , 將其廣播到 executor 之後 , 遍歷日誌的每一行的每一列 , 實際上就是對應的特徵值 , 去上面的陣列中二分查詢出對應的索引值並替換掉 .

使用下標資料儲存表 1, 特徵值按照平均長度 64 個字元計算 , 每個字元佔用 1 個位元組 , 5 千萬維特徵需要 3.2G 記憶體 , 廣播的實際表現 ok 1 億維特徵的話需要佔用 6.4G 記憶體 , 按照廣播的時候會有雙倍記憶體佔用的情況 ,gc 會比較嚴重 . 我們又想了一個辦法 , 將字串 hash long,long 僅佔用 8 位元組 , 比起儲存字串來說大大節省了空間 , hash 的有個問題是可能會衝突 , 由於 8 位元組的 hash 對映空間有 -2^63 2^63-1, 我們使用的是 BKDRHash, 實際測下來衝突率很少 , 在業務可接受的範圍 , 這個方法可以大大節省佔用的記憶體 ,1 億特徵僅佔用 800M 的記憶體 , 廣播起來毫無壓力 , 對應的在遍歷表 2 的時候 , 需要先將特徵值用同樣的演算法 hash 之後再進行查詢 . 經過這一輪最佳化之後 , 相同資源的情況下 , 處理 10 億行 , 5KW 維特徵的時候 , 耗時已經降低到半個小時了 , 且記憶體情況相對穩定 .

這種情況跑了一段時間之後 , 特徵規模上到億了 , 發現這一步的耗時已經上升到 45 分鐘了 , 分析了下特徵的分佈 , 發現連續特徵離散化的特徵在日誌出現的頻率很高 , 由於是連續統計值,本身非常稠密,基本每一條資料都有其出現,但是這類特徵在表 1 的分佈不多 , 這完全可以利用快取把這類特徵對應的索引值儲存下來 , 而沒必要走 hash 之後再二分搜尋 , 完全可以用少量的空間節省大量的時間 . 實際實現的時候 , 判斷需要查詢的特徵值是否符合以上的這種情況 , 如果符合的話 , 直接用 guava 快取表 2 的特徵值 -> 1 的索引值 , 實際統計的快取命中率是 99.98888%, 實際耗時下降得也很明顯 , 從之前的 45 分鐘降到 17 分鐘 . 當然快取並不是銀彈 , 在算 hash 的時候誤用了快取 , 導致這一步的計算反而變得慢了 , 因為 hash 的組合實在是太多了 , 快取命中率只有 10% 左右 , 而且 hash 計算複雜度並不高 . 在實際使用快取的時候 , 有必要去統計一下快取的命中率 .

2. Spark 的一些經驗

1. 利用好 spark UI SQL 預覽 , 做類似特徵處理的 ETL 任務多關注下 SQL, 做這類特徵處理的工作的時候 , 這個功能絕對是一把利器 , 前期實現時間比較趕 , 測試用例比較少 , 在查實際執行邏輯錯誤的問題時 , 可以利用前期對資料的分析結論結合 SQL 選項的流程圖來定位資料出錯的位置 .

2. 利用 spark UI 找出傾斜的任務 , 找到耗時比較長的 Stages, 點進去看 Aggregated Metrics by Executor

3. 對單個 task 可以不用太關注 , 如果某些 Executor 的耗時比起其他明顯多了 , 一般都是資料清洗導致的 ( 不排除某些慢節點 )

4. 利用 UI 確認是否需要快取 , 如果一個任務重複步驟非常頻繁的 , 且任務的資料本地性都是 RACK_LOCAL 的 則要考慮將其上游結果快取下來 . 比如我們這裡會統計單列特徵的頻次的時候

5. 會將上游資料快取下來 , 但是資料量相對比較大 , 我們選擇將其快取到磁碟, spark 實現的自動分配記憶體和磁碟的方法有點問題,不知道是我們的姿勢問題還是他的實現有 bug

生產版本小結

億級別特徵維度,幾十億樣本(對全樣本做了取樣,效果損失不明顯),二十分鐘左右跑完。不過這個時間資料參考意義不大,和跑的資源和機器效能有關,而大廠在這塊優勢太大了。而本文核心解決的點是特徵處理過程中,特徵編碼的索引達到億級別時,資料處理效能差或者 spark OOM 的問題。  


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31555587/viewspace-2220513/,如需轉載,請註明出處,否則將追究法律責任。

相關文章