外賣排序系統特征生產框架


背景

圖1 外賣排序系統框架

外賣的排序策略是由機器學習模型驅動的,模型迭代效率制約着策略優化效果。如上圖所示,在排序系統里,特征是最為基礎的部分:有了特征之后,我們離線訓練出模型,然后將特征和模型一起推送給線上排序服務使用。特征生產Pipeline對於策略迭代的效率起着至關重要的作用。經過實踐中的積累和提煉,我們整理出一套通用的特征生產框架,大大節省開發量,提高策略迭代效率。

外賣排序系統使用GBDT(Gradient Boosting Decision Tree)樹模型,比較復雜。受限於計算能力,除了上下文特征(如時間、地域、終端類型、距離等)之外,目前使用的主要是一些寬泛的統計特征,比如商家銷量、商家單均價、用戶的品類偏好等。這些特征的生產流程包括:離線的統計、離線到在線的同步、在線的加載等。

圖2 特征生產流程

如上圖,目前外賣排序的特征生產流程主要有:

  1. 特征統計:基於基礎數據表(如曝光表、點擊表、訂單表等),統計若干時段內特定維度的總量、分布等,如商家月均銷量、用戶不同品類下單占比。統計結果存儲於Hive表。這部分工作,簡單的可基於ETL,復雜的可基於Spark。產出的特征可供離線訓練和線上預測,本文主要圍繞線上展開

  2. 特征推送:Hive表里的數據需要存入KV,以便線上實時使用。這一步,首先要將Hive表里的記錄映射成POJO類(稱為Domain類),然后將其序列化,最后將序列化串存入KV。這部分工作比較單一,基於MapReduce實現。

  3. 特征獲取:在線服務根據需求,從KV中取出數據,並反序列化為Domain對象。

  4. 特征加載:針對模型所需特征列表,取得對應的Domain對象。這步通過調用特征獲取實現。

前兩步為離線操作,后兩步為在線操作。特征同步由離線推送和在線獲取共同完成。離線生產流程是一個周期性的Pipeline,目前是以天為周期。

為此,我們設計了一套通用的框架,基於此框架,只需要簡單的配置和少量代碼開發,就可以新增一組特征。下文將詳細介紹框架的各個部分。

特征統計

排序模型用到的特征大部分是統計特征。有些特征比較簡單,如商家的月均銷量、商家單均價等,可用ETL統計(GROUP BY + SUM/AVG);有些特征稍微復雜,如用戶的品類偏好(在不同品類上的占比)、用戶的下單額分布(不同金額區段的占比),用ETL就比較繁瑣。針對后一種情況,我們開發了一套Spark程序來統計。我們發現,這種統計需求可以規約成一種范式:針對某些統計對象(用戶、商家)的一些維度(品類、下單額),基於某些度量值(點擊、下單)做統計(比例/總和)。

同一對象,可統計不同維度;同一維度,有不同的度量角度;同一度量角度,有不同的統計方式。如下圖:


圖3 特征統計范式

例如,對於用戶點擊品類偏好、用戶下單品類偏好、用戶下單額分布、用戶下單總額等特征,可做范式分解:

圖4 特征統計范式示例

其中,

  • 統計對象、統計維度、度量值對應於Hive表中的字段(維度一般來自維度表,度量值一般來自事實表,主要是曝光、點擊、下單)。為了增加靈活性,我們還允許對原始Hive字段做加工,加工后的值作為統計維度、度量值(加工的接口我們分別稱為維度算子和度量算子)。

  • 統計量基於度量值做的一些聚合操作,如累加、求均值、拼接、求占比、算分位點(分布)。前兩者輸出一個數值,后三者輸出形如"Key1:Value1,Key2:Value2"的KeyValue列表。

另外,統計通常是在一定時間窗口內進行的,由於不同時期的數據價值不同(新數據比老數據更有價值),我們引入了時間衰減,對老數據降權。

基於以上考慮,整個統計流程可以分解為(基於Spark):

 

圖5 特征統計流程
  1. 按統計對象字段做聚合(GROUP BY)。統計對象字段由配置給定。對於外賣排序主要為uuid、poi_id。這一步可能會有數據傾斜,需要更多優化。

  2. 計算維度。支持維度算子,可以對原始維度字段做處理,如對金額字段做分段處理,以分段后的金額作為維度。

  3. 按統計維度聚合(GROUP BY)。這是在對象聚合的基礎上做的二次聚合。維度字段由配置給定,可以有多個字段,表示交叉特征統計,如不同時段的品類偏好,維度字段為:時段、品類。

  4. 時間衰減並累加。衰減各個時間的度量值,並把所有時間的度量值累加,作為加權后的度量值。時間字段和度量字段由配置給定。時間字段主要為日期,度量字段主要為曝光、點擊、下單。經過維度聚合后,度量值都在特定維度值對應的記錄集上做累加,每個維度對應一個度量值,維度和度量值是一個KeyValue的映射關系。

  5. 計算度量值。度量字段也可以通過度量算子做進一步處理,算子得到的結果作為度量值。也可以有多個字段,如點擊和曝光字段,配合除法算子,可以得到點擊率作為度量值。

  6. 計算統計量。經過對象和維度聚合后,對象、維度、度量值建立了二級映射關系:對象維度度量值,相當於一個二維Map:Map<對象, Map<維度, 度量值>>。統計量是對Map<維度, 度量值>做一個聚合操作。每個統計量對應輸出Hive表中的一個字段。現在主要支持如下幾種算子:

  • 累加:對該維度的所有度量值求和;

  • 求均值:該維度所有取值情況對應的度量值的均值;

  • 拼接:把Map<維度, 度量值>序列化為"Key1:Value1, Key2:Value2"形式,以便以字符串的形式存儲於一個輸出字段內。為了防止序列化串太長,可通過配置設定只保留度量值最大的top N;

  • 求占比:該維度所有取值情況對應的度量值占度量值總和的比重,即Map<維度, 度量值/Sum(度量值)>。然后再做拼接輸出;

  • 算分位點:有時候想直到某些維度的分布情況,比如用戶下單金額的分布以考察用戶的消費能力。分位點可以作為分布的一種簡單而有效的表示方法。該算子輸出每個分位點的維度值,形如"分位點1:維度值1, 分位點2:維度值2"。此時,度量值只是用來算比值。

維度算子度量算子統計算子都可以通過擴展接口的方式實現自定義。

如下是統計用戶點擊品類偏好、用戶下單品類偏好、用戶下單額分布的配置文件和Hive表示例([Toml][1]格式)

圖6 特征統計配置示例

相對於ETL,這套Spark統計框架更為簡單清晰,還可以同時統計多個相關的特征。通過簡單的配置就可以實現特征的統計,開發量比較小。

特征同步

離線統計得到的特征存儲在Hive表中,出於性能的考慮,不能在線上直接訪問。我們需要把特征從Hive中推送到更為高效的KV數據庫中,線上服務再從KV中獲取。整個同步過程可以分為如下步驟:

 

圖7 特征推送流程
  1. ORM:將Hive表中的每行記錄映射為Domain對象(類似於[Hibernate][2]的功能)

  2. 序列化:將Domain對象序列化,然后存儲到KV中。一個Domain類包含一組相關的、可同時在一個任務中統計的特征數據。每個Domain對象都有一個key值來作為自己唯一的標志—實現key()接口。同時,由於不同類型的Domain都會存儲在一起,我們還需要為每種類型的Domain設定一個Key值前綴prefix以示區別。因此,KV中的Key是Domain.prefix + Domain.key,Value是序列化串。我們支持json和protostuff兩種序列化方式。

  3. 反序列化:在線服務根據key和Domain.prefix從KV中得到序列化串,並反序列化為Domain對象。

前兩步為離線操作,第三步為在線操作(在預測代碼中被調用)。

我們針對Hive開發了一套ORM庫(見圖8),主要基於Java反射,除了支持基本類型(int/long/float/double/String等),還支持POJO類型和集合類型(List/Map)。因為ETL不支持json拼接,為了兼容基於ETL統計的特征數據,我們的POJO以及集合類型是基於自定義的規范做編解碼。針對Spark統計的特征數據,后續我們可以支持json格式的編解碼。

 

圖8 Hive ORM示意

特征序列化和反序列我們統一封裝為通用的KvService:負責序列化與反序列,以及讀寫KV。如下圖:

圖9 KvService

對於新特征,只需要定義一個Domain類,並實現接口key()即可,KvService自動完成Key值的拼接(以Domain的類名作為Key的prefix),序列化和反序列化,讀寫KV。

我們通過周期性的離線MapReduce任務,讀取Hive表的記錄,並調用KvService的put接口,將特征數據推送到KV中。由於KvService能夠統一處理各種Domain類型,MapReduce任務也是通用的,無需為每個特征單獨開發。

對於特征同步,只需要開發Domain類,並做少量配置,開發量也很小。目前,我們為了代碼的可讀性,采用Domain這種強類型的方式來定義特征,如果可以弱化這種需求的話,還可以做更多的框架優化,省去Domain類開發這部分工作。

特征加載

通過前面幾步,我們已經准備好特征數據,並存儲於KV中。線上有諸多模型在運行,不同模型需要不同的特征數據。特征加載這一步主要解決怎么高效便捷地為模型提供相應的特征數據。

離線得到的只是一些原始特征,在線還可能需要基於原始特征做更多的處理,得到高階特征。比如離線得到了商家和用戶的下單金額分布,在線我們可能需要基於這兩個分布計算一個匹配度,以表征該商家是否在用戶消費能力的承受范圍之內。

我們把在線特征抽象為一個特征算子:FeatureOperator。類似的,一個特征算子包含了一組相關的在線特征,且可能依賴一組相關的離線特征。它除了封裝了在線特征的計算過程,還通過兩個Java Annotation聲明該特征算子產出的特征清單(@Features)和所需要的數據清單(@Fetchers)。所有的數據獲取都是由DataFetcher調用KvService的get接口實現,拿到的Domain對象統一存儲在DataPortal對象中以便后續使用。

服務啟動時,會自動掃描所有的FeatureOperator的Annotation(@Features、@Fetchers),拿到對應的特征清單和數據清單,從而建立起映射關系:FeatureFeatureOperatorDataFetcher。而每個模型通過配置文件給定其所需要的特征清單,這樣就建立起模型到特征的映射關系(如圖9):

Model → Feature → FeatureOperator → DataFetcher

不同的在線特征可能會依賴相同的離線特征,也就是FeatureOperatorDataFetcher是多對多的關系。為了避免重復從KV讀取相同的數據以造成性能浪費,離線特征的獲取和在線特征的抽取被划分成兩步:先匯總所有離線特征需求,統一獲取離線特征;得到離線特征后,再進行在線特征的抽取。這樣,我們也可以在離線特征加載階段采用並發以減少網絡IO延時。整個流程如圖10所示:



圖10 模型和特征數據的映射關系
 

圖11 特征加載流程

對於新特征,我們需要實現對應的FeatureOperator、DataFetcher。DataFetcher主要封裝了Domain和DataPortal的關系。類似的,如果我們不需要以強類型的方式來保證代碼的業務可讀性,也可以通過優化框架省去DataFetcher和DataPortal的定制開發。

總結

我們在合理抽象特征生產過程的各個環節后,設計了一套較為通用的框架,只需要少量的代碼開發(主要是自定義一些算子)以及一些配置,就可以很方便地生產一組特征,有效地提高了策略迭代效率。

轉自美團點評技術團隊


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM