多工學習模型之ESMM介紹與實現

阿里云云棲號發表於2022-11-23

簡介:本文介紹的是阿里巴巴團隊發表在 SIGIR’2018 的論文《Entire Space Multi-Task Model: An Effective Approach for Estimating Post-Click Conversion Rate》。文章基於 Multi-Task Learning (MTL) 的思路,提出一種名為ESMM的CVR預估模型,有效解決了真實場景中CVR預估面臨的資料稀疏以及樣本選擇偏差這兩個關鍵問題。後續還會陸續介紹MMoE,PLE,DBMTL等多工學習模型。

多工學習背景

目前工業中使用的推薦演算法已不只侷限在單目標(ctr)任務上,還需要關注後續的轉換鏈路,如是否評論、收藏、加購、購買、觀看時長等目標。

本文介紹的是阿里巴巴團隊發表在 SIGIR’2018 的論文《Entire Space Multi-Task Model: An Effective Approach for Estimating Post-Click Conversion Rate》。文章基於 Multi-Task Learning (MTL) 的思路,提出一種名為ESMM的CVR預估模型,有效解決了真實場景中CVR預估面臨的資料稀疏以及樣本選擇偏差這兩個關鍵問題。後續還會陸續介紹MMoE,PLE,DBMTL等多工學習模型。

論文介紹

CVR預估面臨兩個關鍵問題:

1. Sample Selection Bias (SSB)

轉化是在點選之後才“有可能”發生的動作,傳統CVR模型通常以點選資料為訓練集,其中點選未轉化為負例,點選並轉化為正例。但是訓練好的模型實際使用時,則是對整個空間的樣本進行預估,而非只對點選樣本進行預估。即訓練資料與實際要預測的資料來自不同分佈,這個偏差對模型的泛化能力構成了很大挑戰,導致模型上線後,線上業務效果往往一般。

image.png

2. Data Sparsity (DS)

CVR預估任務的使用的訓練資料(即點選樣本)遠小於CTR預估訓練使用的曝光樣本。僅使用數量較小的樣本進行訓練,會導致深度模型擬合困難。

一些策略可以緩解這兩個問題,例如從曝光集中對unclicked樣本抽樣做負例緩解SSB,對轉化樣本過取樣緩解DS等。但無論哪種方法,都沒有從實質上解決上面任一個問題。

由於點選=>轉化,本身是兩個強相關的連續行為,作者希望在模型結構中顯示考慮這種“行為鏈關係”,從而可以在整個空間上進行訓練及預測。這涉及到CTR與CVR兩個任務,因此使用多工學習(MTL)是一個自然的選擇,論文的關鍵亮點正在於“如何搭建”這個MTL。

首先需要重點區分下,CVR預估任務與CTCVR預估任務。

CVR = 轉化數/點選數。是預測“假設item被點選,那麼它被轉化”的機率。CVR預估任務,與CTR沒有絕對的關係。一個item的ctr高,cvr不一定同樣會高,如標題黨文章的瀏覽時長往往較低。這也是不能直接使用全部樣本訓練CVR模型的原因,因為無法確定那些曝光未點選的樣本,假設他們被點選了,是否會被轉化。如果直接使用0作為它們的label,會很大程度上誤導CVR模型的學習。
CTCVR = 轉換數/曝光數。是預測“item被點選,然後被轉化”的機率。

image.png

其中x,y,z分別表示曝光,點選,轉換。注意到,在全部樣本空間中,CTR對應的label為click,而CTCVR對應的label為click & conversion,這兩個任務是可以使用全部樣本的。因此,ESMM透過學習CTR,CTCVR兩個任務,再根據上式隱式地學習CVR任務。具體結構如下:

image.png

網路結構上有兩點值得強調:

共享Embedding。 CVR-task和CTR-task使用相同的特徵和特徵embedding,即兩者從Concatenate之後才學習各自獨享的引數;

隱式學習pCVR。這裡pCVR 僅是網路中的一個variable,沒有顯示的監督訊號。
具體地,反映在目標函式中:

image.png

程式碼實現

基於EasyRec推薦演算法框架,我們實現了ESMM演算法,具體實現可移步至github:EasyRec-ESMM。

EasyRec介紹:EasyRec是阿里雲端計算平臺機器學習PAI團隊開源的大規模分散式推薦演算法框架,EasyRec 正如其名字一樣,簡單易用,整合了諸多優秀前沿的推薦系統論文思想,並且有在實際工業落地中取得優良效果的特徵工程方法,整合訓練、評估、部署,與阿里雲產品無縫銜接,可以藉助 EasyRec 在短時間內搭建起一套前沿的推薦系統。作為阿里雲的拳頭產品,現已穩定服務於數百個企業客戶。

模型前饋網路:

def build_predict_graph(self):
   """Forward function.

   Returns:
     self._prediction_dict: Prediction result of two tasks.
   """
   # 此處從Concatenate後的tensor(all_fea)開始,省略其生成邏輯

   cvr_tower_name = self._cvr_tower_cfg.tower_name
   dnn_model = dnn.DNN(
       self._cvr_tower_cfg.dnn,
       self._l2_reg,
       name=cvr_tower_name,
       is_training=self._is_training)
   cvr_tower_output = dnn_model(all_fea)
   cvr_tower_output = tf.layers.dense(
       inputs=cvr_tower_output,
       units=1,
       kernel_regularizer=self._l2_reg,
       name='%s/dnn_output' % cvr_tower_name)

   ctr_tower_name = self._ctr_tower_cfg.tower_name
   dnn_model = dnn.DNN(
       self._ctr_tower_cfg.dnn,
       self._l2_reg,
       name=ctr_tower_name,
       is_training=self._is_training)
   ctr_tower_output = dnn_model(all_fea)
   ctr_tower_output = tf.layers.dense(
       inputs=ctr_tower_output,
       units=1,
       kernel_regularizer=self._l2_reg,
       name='%s/dnn_output' % ctr_tower_name)

   tower_outputs = {
       cvr_tower_name: cvr_tower_output,
       ctr_tower_name: ctr_tower_output
   }
   self._add_to_prediction_dict(tower_outputs)
   return self._prediction_dict

loss計算:

注意:計算CVR的指標時需要mask掉曝光資料。

def build_loss_graph(self):
   """Build loss graph.

   Returns:
     self._loss_dict: Weighted loss of ctr and cvr.
   """
   cvr_tower_name = self._cvr_tower_cfg.tower_name
   ctr_tower_name = self._ctr_tower_cfg.tower_name
   cvr_label_name = self._label_name_dict[cvr_tower_name]
   ctr_label_name = self._label_name_dict[ctr_tower_name]

   ctcvr_label = tf.cast(
       self._labels[cvr_label_name] * self._labels[ctr_label_name], 
       tf.float32)
   cvr_loss = tf.keras.backend.binary_crossentropy(
       ctcvr_label, self._prediction_dict['probs_ctcvr'])
   cvr_loss = tf.reduce_sum(cvr_losses, name="ctcvr_loss")

   # The weight defaults to 1.
   self._loss_dict['weighted_cross_entropy_loss_%s' %
                     cvr_tower_name] = self._cvr_tower_cfg.weight * cvr_loss

   ctr_loss = tf.reduce_sum(tf.nn.sigmoid_cross_entropy_with_logits(
       labels=tf.cast(self._labels[ctr_label_name], tf.float32),
       logits=self._prediction_dict['logits_%s' % ctr_tower_name]
       ), name="ctr_loss")

   self._loss_dict['weighted_cross_entropy_loss_%s' %
                   ctr_tower_name] = self._ctr_tower_cfg.weight * ctr_loss
   return self._loss_dict

note: 這裡loss是 weighted_cross_entropy_loss_ctr + weighted_cross_entropy_loss_cvr, EasyRec框架會自動對self._loss_dict中的內容進行加和。

metric計算:

注意:計算CVR的指標時需要mask掉曝光資料。

def build_metric_graph(self, eval_config):
  """Build metric graph.

  Args:
    eval_config: Evaluation configuration.

  Returns:
    metric_dict: Calculate AUC of ctr, cvr and ctrvr.
  """
  metric_dict = {}

  cvr_tower_name = self._cvr_tower_cfg.tower_name
  ctr_tower_name = self._ctr_tower_cfg.tower_name
  cvr_label_name = self._label_name_dict[cvr_tower_name]
  ctr_label_name = self._label_name_dict[ctr_tower_name]
  for metric in self._cvr_tower_cfg.metrics_set:
    # CTCVR metric
    ctcvr_label_name = cvr_label_name + '_ctcvr'
    cvr_dtype = self._labels[cvr_label_name].dtype
    self._labels[ctcvr_label_name] = self._labels[cvr_label_name] * tf.cast(
        self._labels[ctr_label_name], cvr_dtype)
    metric_dict.update(
        self._build_metric_impl(
            metric,
            loss_type=self._cvr_tower_cfg.loss_type,
            label_name=ctcvr_label_name,
            num_class=self._cvr_tower_cfg.num_class,
            suffix='_ctcvr'))

    # CVR metric
    cvr_label_masked_name = cvr_label_name + '_masked'
    ctr_mask = self._labels[ctr_label_name] > 0
    self._labels[cvr_label_masked_name] = tf.boolean_mask(
        self._labels[cvr_label_name], ctr_mask)
    pred_prefix = 'probs' if self._cvr_tower_cfg.loss_type == LossType.CLASSIFICATION else 'y'
    pred_name = '%s_%s' % (pred_prefix, cvr_tower_name)
    self._prediction_dict[pred_name + '_masked'] = tf.boolean_mask(
        self._prediction_dict[pred_name], ctr_mask)
    metric_dict.update(
        self._build_metric_impl(
            metric,
            loss_type=self._cvr_tower_cfg.loss_type,
            label_name=cvr_label_masked_name,
            num_class=self._cvr_tower_cfg.num_class,
            suffix='_%s_masked' % cvr_tower_name))

  for metric in self._ctr_tower_cfg.metrics_set:
    # CTR metric
    metric_dict.update(
        self._build_metric_impl(
            metric,
            loss_type=self._ctr_tower_cfg.loss_type,
            label_name=ctr_label_name,
            num_class=self._ctr_tower_cfg.num_class,
            suffix='_%s' % ctr_tower_name))
  return metric_dict

實驗及不足

我們基於開源AliCCP資料,進行了大量實驗,實驗部分請期待下一篇文章。實驗發現,ESMM的蹺蹺板現象較為明顯,CTR與CVR任務的效果較難同時提升。

參考文獻

Entire Space Multi-Task Model: An Effective Approach for Estimating Post-Click Conversion Rate
阿里CVR預估模型之ESMM
EasyRec-ESMM使用介紹多工學習模型之ESMM介紹與實現
注:本文圖片及公示均引用自論文:Entire Space Multi-Task Model: An Effective Approach for Estimating Post-Click Conversion Rate。

原文連結
本文為阿里雲原創內容,未經允許不得轉載。

相關文章