雲音樂預估系統建設與實踐

雲音樂技術團隊發表於2022-06-15
作者:大人物

1. 什麼是預估系統?

    預估系統的核心任務是完成模型計算,可以認為模型就是一個函式(舉例:f(x1, x2)= ax1 + bx2 +c)。其中引數a、b、c是通過模型訓練得出的權重值,自變數x1與x2就是特徵,模型計算就是使用自變數x1與x2求解的過程。
    因此預估框架需要做的是:建構函式輸入(特徵),計算函式得到結果(模型計算)。即:特徵抽取、模型計算

  • 特徵抽取
    特徵是對某個行為相關資訊的抽象表達。推薦過程中某個行為資訊必須轉換成某種數學形式才能被機器學習模型所學習。為了完成這種轉換,就必須將行為過程中的資訊以特徵的形式抽取出來。
  • 模型計算
    整合機器學習庫,載入機器學習平臺訓練出的模型,按照模型結構執行既定的矩陣、向量等計算。

2. 預估系統的設計思考

2.1 通用方案的侷限

    業內常見的方案是特徵處理服務 + tfserving模型計算服務,這種方案是將特徵處理和模型計算分服務部署。在特徵數量不大時可以提供較好的支援,但特徵數量一旦變多,就會出現明顯的效能問題,主要是因為特徵要跨網路(跨程式)傳遞給底層機器學習庫,這樣就會帶來多次編解碼與記憶體拷貝,會造成巨大的效能開銷。

2.2、新系統的思考

根據業務需求,並吸取通用方案存在的優缺點,我們構建了新的高效能預估系統,該系統指導思路是:

  • 高效能:追求特徵抽取與模型計算高效能,提供大規模特徵抽取和大規模模型計算的能力。
  • 抽象與複用:系統分層設計,分層複用;線上線下特徵抽取邏輯複用;特徵抽取提出運算元概念,建立運算元庫,實現特徵複用。
  • 實時化:實現樣本採集實時化、模型訓練實時化和模型更新實時化。
  • 可擴充套件性:特徵抽取提供自定義運算元介面,方便自定義運算元的實現;模型計算提供整合多種機器學習庫的能力。

下圖展示了雲音樂預估系統架構:

3. 預估系統的建設過程

一個優秀的預估系統需要解決如下三個問題:

  • 如何解決特徵和模型的高效迭代?
  • 如何解決預估計算的效能問題?
  • 有沒有機會通過工程手段提升演算法效果?

3.1 特徵與模型的高效迭代

3.1.1 系統分層設計

系統分層設計,僅暴露上層介面層,不同業務間完全複用中間層和底層實現,較大程度減少程式碼開發。

  • 底層框架層:該層提供非同步機制、任務佇列、session管理、多執行緒併發排程、通絡通訊相關的邏輯、外部檔案載入與解除安裝等
  • 中間邏輯層:封裝查詢管理、快取管理、模型更新管理、模型計算管理等功能
  • 上層介面層:按照執行流程提供HighLevel介面,演算法在此層實現邏輯

下圖展示了框架分層設計:

3.1.2 配置化完成模型計算全流程

按照執行流程,把處理過程分成三個階段,分別是:資料查詢、特徵抽取、模型計算。在框架中對每個階段進行抽取和封裝,並提供配置化描述語言來完成各個階段的邏輯表達。

(1) 資料查詢

通過XML配置表名、查詢key、快取時間、查詢依賴等就能實現特徵資料的外部查詢、解析、快取全流程。如下所示:

<feature_tables>
    <table name="music-rec-fm_set_action" alias="trash_song" tag="user" key="user_id"/>
    <table name="music_fm_dsin_user_static_ftr_dpb" alias="u_static" tag="user" key="user_id"/>
    <table name="alg_song_ua_rt" alias="u_rt_red" tag="user" key="user_id" subkey="1"/>
    <table name="fm_dsin_song_promoted_info_feature_dpb_mdb" alias="item_promoted" tag="item" key="item_id" cache_time="7200" cache_size="800000" query_type="sync"/>
    <table name="fm_dsin_song_static_feature_dpb_mdb" alias="item_static" tag="item" key="item_id" cache_time="7200" cache_size="800000" query_type="asyc"/>
</feature_tables>

特徵資料查詢配置化帶來了開發效率的大幅提升,用幾行配置就實現了以往需要大量編碼才能實現的特徵查詢功能。

(2) 特徵抽取

開發特徵抽取庫,封裝特徵抽取運算元,開發特徵計算DSL語言,通過配置化完成整個特徵抽取過程。如下所示:

<feature_extract_config cache="true" log_level="3" log_echo="false" version="2">
    <fea name="isfollowedaid" dataType="int64" default="0L" extractor="StringHit($item_id, $uLikeA.followed_anchors)"/>
    <fea name="rt_all_all_pv" dataType="int64" default="LongArray(0, 5)" extractor="RtFeature($all_all_pv.f, 2)"/>
    <fea name="anchor_all_impress_pv" dataType="int64" default="0" extractor="ReadIntVec($rt_all_all_pv, 0)"/>
    <fea name="anchor_all_click_pv" dataType="int64" default="0" extractor="ReadIntVec($rt_all_all_pv, 1)"/>
    <fea name="anchor_all_impress_pv_id" dataType="int64" default="0" extractor="Bucket($anchor_all_impress_pv, $bucket.all_impress_pv)"/>
    <fea name="anchor_all_ctr_pv" dataType="float" default="0.0" extractor="Smooth($anchor_all_click_pv, $anchor_all_impress_pv, 1.0, 1000.0, 100.0)"/>
    <fea name="user_hour" dataType="int64" extractor="Hour()" default="0L"/>
    <fea name="anchor_start_tags" dataType="int64" extractor="Long2ID($live_anchor_index.start_tags,0L,$vocab.start_tags)" default="0L"/>
</feature_extract_config>

特徵抽取庫的會在下面詳細介紹。

(3) 模型計算

對模型載入、引數輸入、模型計算等進行封裝,通過配置化實現模型載入與計算全流程。具體特點如下:

  • 預估框架整合tensorflow core,支援多種模型形態。
  • 支援多模型載入,支援多模型融合打分。
  • 支援多buf模型更新,自動實現模型預熱載入。
  • 支援多種格式的引數輸入(Example和Tensor),內建Example構造器和Tensor構造器,對外遮蔽複雜的引數構造細節,簡單易用。
  • 擴充套件多種機器庫,例如paddle、gbm等。
<model_list>
    <!-- pb模型,tensor輸入,指定out_names -->
    <id model_name="model1" model_type="pb" input_type="tensor" out_names="name1;name2" separator=";" />

    <!-- savedmodel模型,tensor輸入,指定out_names -->
    <id model_name="model2" model_type="mdl" input_type="tensor" out_names="name1;name2" separator=";" />
    
    <!-- savedmodel模型,example輸入,指定out_aliases -->
    <id model_name="model3" model_type="mdl" input_type="example" out_aliases="aliase1;aliase2" separator=";" signature="serving_default" />
</model_list>

通過上述配置,可以完成模型載入,模型輸入構造,模型計算全流程。用幾行配置實現了之前需要大量編碼才能實現的模型計算功能。

3.1.3 封裝特徵抽取框架

特徵抽取目的是將非標準的資料轉換成標準的資料,然後提給機器學習訓練平臺和線上計算平臺使用。特徵抽取分為離線過程和線上過程。

下圖展示了什麼是特徵抽取:

3.1.3.1 特徵抽取存在哪些個問題?
  • 一致性難以保證

線上抽取與線下抽取因平臺不同(語言不同)需要開發多套程式碼,從而會引發邏輯不一致的問題。一致性問題一方面會影響演算法效果,另一方面一致性校驗會帶來高昂工程落地成本。

  • 開發效率低

特徵生產和特徵應用要對應多套系統,新增一個特徵要改多處程式碼,開發效率低下。

  • 複用難

框架缺乏對複用能力的支撐,特徵計算邏輯差異大,導致團隊間資料複用難,資源浪費、特徵價值無法充分發揮。

3.1.3.2 如何解決上述問題?
(1) 抽象運算元

提出運算元概念,將特徵計算邏輯封裝成運算元。為了實現運算元抽象,首先必須定義統一的資料協議(標準化運算元的輸入與輸出),這裡我們採用動態PB技術,根據特徵的後設資料資訊,採用統一的方式處理任意格式的特徵。
並且建立平臺通用運算元庫和業務運算元庫,實現了特徵資料和特徵計算的複用能力。

(2) 定義特徵計算DSL語言

基於運算元,我們設計了特徵計算表示語言DSL,通過該語言可以支援多階抽取和抽取依賴, 通過基礎運算元的多種組合可以實現複雜的特徵計算邏輯,提供豐富的表達能力。DSL表示式如下:


(3) 解決邏輯不一致問題

    出現特徵計算邏輯不一致根本原因是因為特徵計算分為離線過程和線上過程,離線特徵計算一般是在Spark平臺和Flink平臺(使用scala或java語言),而線上特徵計算一般c++環境。平臺和語言的不一樣,就導致相同的特徵計算邏輯要開發多套程式碼,多套程式碼間就可能出現邏輯不一致的問題。
    為了解決上述問題,就必須實現特徵處理邏輯多平臺相容。考慮到線上程計算平臺使用的是c++語言,以及c++的高效能和多平臺(多語言)執行的相容性,於是特徵處理核心庫採用c++語言實現,對外提供c++介面(支援線上計算平臺)、java介面(支援spark和flink平臺)。並提供一鍵編譯的方式,編譯後生成so庫和jar包,方便整合到各個平臺中執行。

下圖展示了特徵抽取跨平臺解決方案:

    通過封裝特徵抽取庫,提供特徵抽取跨平臺執行能力,實現一次程式碼編寫,多平臺(多語言)執行,從而解決多平臺特徵抽取邏輯不一致的問題;通過運算元封裝、運算元庫建立,實現了特徵與特徵計算的高度複用;通過開發特徵計算DSL語言,實現配置化開發特徵抽取的能力。基於上述建設,極大的提升了我們在特徵抽取上的開發效率。

3.2 高效能預估計算

    預估服務是計算密集型服務,對效能要求極高,尤其在處理複雜特徵和複雜模型時,效能問題表現的尤為突出,我們在開發預估系統時有很多效能上的思考和嘗試,下面將詳細介紹。

3.2.1 無縫整合高效能機器學習庫

    前面介紹過傳統預估方案是將特徵處理服務和模型計算服務分程式部署,這樣就會涉及到特徵的跨網路傳輸、序列化、反序列化和頻繁且大量的記憶體申請和釋放,尤其是推薦場景特徵量大,會帶來較大的效能開銷。下圖展示了服務部署和特徵傳輸的過程:

下圖展示了傳統方案服務部署和特徵傳輸:

    為了解決上述問題,在新的預估系統中,將高效能機器學習庫TF-core無縫整合到預估系統內,即特徵處理和模型計算同程式部署。這樣可以實現特徵指標化操作,避免序列化、反序列化、網路傳輸等開銷,較大程度提升效能。

下圖展示了新預估系統特徵傳輸:

3.2.2 全非同步架構

    為了提升計算能力,我們採用了全非同步架構設計。計算框架非同步處理、外部呼叫無阻塞等待,最大程度將執行緒資源留給實際計算。機器過載時,會自動丟棄任務佇列中超時的任務,避免機器/程式被拖垮。

下圖展示了非同步架構設計:

3.2.3 多級快取

    推薦場景的請求由一個User和一批候選Item組成(50-1000個不等),而熱門Item的數量僅在十萬或者百萬級別,Item的特徵分為離線特徵(小時級或者天級更新)和實時特徵(秒級或者分鐘級更新)。基於推薦場景的請求特點,Item特徵完全可以通過請求觸發的方式快取在程式內,在超時時間內可以直接使用快取中的內容,避免無效的外部查詢、特徵解析和特徵抽取。不僅能大大降低外部儲存的查詢量,也能大大降低預估服務的資源佔用。
    目前預估服務在特徵資料查詢和特徵抽取計算緩環節使用了快取機制。在快取設計上採用了執行緒池非同步處理、分多個桶儲存快取結果減少寫鎖碰撞等措施提升快取查詢效能。並且提供多種特徵查詢及快取策略:

  • 同步查詢&LRU快取 適用特徵重要且特徵規模龐大的特徵(效能低)
  • 非同步查詢&LRU快取 適用於特徵不太重要且規模龐大的特徵(效能中)
  • 特徵批量匯入 適用與特徵規模在千萬以下的特徵(效能高)

下圖展示了預估系統快取機制:

3.2.4 合理的模型輸入建議

    針對Tensorflow的模型輸入,Tensor輸入的效能會優於Example輸入的效能。下圖一展示了Example輸入的timeline圖,下圖二展示了Tensor輸入的timeline圖。可以明顯的發現使用Example作為模型輸入時,在模型內有較長的解析過程,並且在解析完成前,後面的計算op無法併發執行。

下圖展示了Example輸入的Timeline:

下圖展示了Tensor輸入的Timeline:

(1) 為什麼使用Tensor輸入可以提高效能?

主要是可以減少Example的序列化和反序列化,以及ParseExample的耗時。

(2) 相比Example,用Tensor輸入存在哪些問題?

Tensor構造邏輯邏輯複雜,開發效率低
需要關注Tensor維度細節,如果Tensor改變(新增、刪除、維度改變等)都需要重新開發Tensor構造程式碼

為了兼顧Tensor輸入的高效能和Example輸入的開發便捷性,在預估系統中開發了一套Tensor構造器,即保證了效能,也降低開發難度。

3.2.5 模型載入與更新優化

    在模型的載入與更新上嘗試多種優化策略,其中包括:支援模型雙buf熱更新、支援模型自動預熱載入、舊模型延時解除安裝等方法提升模型載入效能。通過上述優化方式可實現大模型分鐘級熱更新,線上請求耗時無抖動,為模型實時化提供基礎。

下圖展示了模型自動預熱方案:

3.3 通過工程手段提升演算法效果

預估系統除了要保證高可用、低延時等核心要素外,能否通過一些工程手段帶來演算法效果的提升呢?我們從特徵、樣本、模型等維度做了一些思考和嘗試。

3.3.1 傳統預估方案中存在哪些可以優化的點?

(1) 特徵穿越的問題

    傳統的樣本生產方式,存在特徵穿越的問題。樣本拼接是將使用者行為和特徵進行拼接,使用者行為是使用者在t-1時刻對預估結果產生的行為,正確的做法是t-1時刻的使用者行為與t-1時刻的特徵進行拼接,但是傳統做法是隻能用t時刻的特徵進行拼接,即導致t時刻的特徵出現在t-1時刻的樣本中,從而導致特徵穿越的產生。出現特徵穿越會導致訓練樣本不準,從而導致演算法效果下降。

(2) 模型實時性不夠

    傳統推薦系統是天級別更新使用者推薦結果,實時性差,無法滿足實時性要求很高的場景。例如直播中主播開播狀態、直播內容變化、業務環境調整等都需要推薦系統能實時感知。

3.3.2 如何解決上述問題?

    為了解決上述問題,我們開發了模型實時化方案。該方案是基於預估服務,將預估時的狀態(請求內容、當時使用的特徵等)實時儲存到kafka,並通過traceid與ua迴流日誌做關聯。這樣就能確保使用者標籤與特徵能一一對應,從而解決特徵穿越的問題。
    並且該方案也實現樣本流秒級落盤,為模型增量訓練提供了樣本基礎。在訓練側實現了模型增量訓練,並將訓練產出的模型實時推送到線網服務。具體實現如下如圖:

下圖展示了模型實時化方案:

通過模型實時化方案,實現樣本實時儲存,從而解決特徵穿越的問題;實現模型分鐘級訓練和更新,能讓模型及時捕捉新動態和新熱點。從而提升演算法效果。

本文釋出自網易雲音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 staff.musicrecruit@service.ne...

相關文章