[阿里DIEN] 深度興趣進化網路原始碼分析 之 Keras版本

羅西的思考發表於2021-01-26

[阿里DIEN] 深度興趣進化網路原始碼分析 之 Keras版本

0x00 摘要

DIEN是阿里深度興趣進化網路(Deep Interest Evolution Network)的縮寫。

之前我們對DIEN的原始碼進行了解讀,那是基於 https://github.com/mouna99/dien 中的實現。

後來因為繼續看DSIN,發現在DSIN程式碼https://github.com/shenweichen/DSIN中,也有DIEN的新實現。

於是閱讀整理,遂有此文。

0x01 背景

1.1 程式碼進化

大家都知道,阿里此模型是的演化脈絡是:DIN,DIEN,DSIN,......

隨之程式碼就有三個版本。

第一個版本作者直接就說明效率不行,推薦第二個版本 https://github.com/mouna99/dien 。此版本程式碼就是純tensorflow程式碼,一招一式紮紮實實,讀起來也順爽。

第三個版本則是基於Keras與deepctr,同前一版本相比則是從游擊隊升級為正規軍,各種高大上和套路。

1.2 Deepctr

之所以成為正規軍,deepctr作用相當大,下面就介紹下。

DeepCtr是一個簡易的CTR模型框架,整合了深度學習流行的所有模型,適合學推薦系統模型的人蔘考。

這個專案主要是對目前的一些基於深度學習的點選率預測演算法進行了實現,如PNN,WDL,DeepFM,MLR,DeepCross,AFM,NFM,DIN,DIEN,xDeepFM,AutoInt等,並且對外提供了一致的呼叫介面。

1.2.1 統一視角

Deepctr的出現不僅僅降低了廣告點選率預測模型的上手難度,方便進行模型對比,也讓給了我們機會從這些優秀的原始碼中學習到構建模型的方式。

DeepCTR的設計主要是面向那些對深度學習以及CTR預測演算法感興趣的同學,使他們可以利用這個包:

  • 從一個統一視角來看待各個模型
  • 快速地進行簡單的對比實驗
  • 利用已有的元件快速構建新的模型

1.2.2 模組化

DeepCTR通過對現有的基於深度學習的點選率預測模型的結構進行抽象總結,在設計過程中採用模組化的思路,各個模組自身具有高複用性,各個模組之間互相獨立。 基於深度學習的點選率預測模型按模型內部元件的功能可以劃分成以下4個模組:

  • 輸入模組
  • 嵌入模組
  • 特徵提取模組
  • 預測輸出模組

所有的模型都是嚴格按照4個模組進行搭建的,輸入和嵌入以及輸出基本都是公用的,每個模型的差異之處主要在特徵提取部分。

1.2.3 框架優點

  • 整體結構清晰靈活,linear返回logit,FM層返回logit,deep包含中間層結果,在每一種模型中打包deep的最後一層,判斷linear,fm和deep是否需要,最後接入全連線層。
  • 主要用到的模組和架構: keras的Concatenate(list轉tensor),Dense(最後的全連線層和dense),Embedding(sparse,dense,sequence),Input(sparse,dense,sequce)還有常規操作:優化器,正則化項
  • 複用了過載了Layer層,重寫了build,call,compute_output_shape,compute_mask,get_config

下面我們就開始深入看看最新版本的DIEN。

0x2 測試資料

DIEN使用的是天池的資料,readme中提示下載:

1. Download Dataset [Ad Display/Click Data on Taobao.com](https://tianchi.aliyun.com/dataset/dataDetail?dataId=56)
2. Extract the files into the ``raw_data`` directory

Ali_Display_Ad_Click是阿里巴巴提供的一個淘寶展示廣告點選率預估資料集。

2.1 資料集介紹

資料名稱 說明 屬性
raw_sample 原始的樣本骨架 使用者ID,廣告ID,時間,資源位,是否點選
ad_feature 廣告的基本資訊 廣告ID,廣告計劃ID,類目ID,品牌ID
user_profile 使用者的基本資訊 使用者ID,年齡層,性別等
raw_behavior_log 使用者的行為日誌 使用者ID,行為型別,時間,商品類目ID,品牌ID

2.2 原始樣本骨架raw_sample

從淘寶網站中隨機抽樣了114萬使用者8天內的廣告展示/點選日誌(2600萬條記錄),構成原始的樣本骨架。
欄位說明如下:

  • (1) user_id:脫敏過的使用者ID;
  • (2) adgroup_id:脫敏過的廣告單元ID;
  • (3) time_stamp:時間戳;
  • (4) pid:資源位;
  • (5) noclk:為1代表沒有點選;為0代表點選;
  • (6) clk:為0代表沒有點選;為1代表點選;

我們用前面7天的做訓練樣本(20170506-20170512),用第8天的做測試樣本(20170513)。

2.3 廣告基本資訊表ad_feature

本資料集涵蓋了raw_sample中全部廣告的基本資訊。欄位說明如下:

  • (1) adgroup_id:脫敏過的廣告ID;
  • (2) cate_id:脫敏過的商品類目ID;
  • (3) campaign_id:脫敏過的廣告計劃ID;
  • (4) customer_id:脫敏過的廣告主ID;
  • (5) brand:脫敏過的品牌ID;
  • (6) price: 寶貝的價格

其中一個廣告ID對應一個商品(寶貝),一個寶貝屬於一個類目,一個寶貝屬於一個品牌。

2.4 使用者基本資訊表user_profile

本資料集涵蓋了raw_sample中全部使用者的基本資訊。欄位說明如下:

  • (1) userid:脫敏過的使用者ID;
  • (2) cms_segid:微群ID;
  • (3) cms_group_id:cms_group_id;
  • (4) final_gender_code:性別 1:男,2:女;
  • (5) age_level:年齡層次;
  • (6) pvalue_level:消費檔次,1:低檔,2:中檔,3:高檔;
  • (7) shopping_level:購物深度,1:淺層使用者,2:中度使用者,3:深度使用者
  • (8) occupation:是否大學生 ,1:是,0:否
  • (9) new_user_class_level:城市層級

2.5 使用者的行為日誌behavior_log

本資料集涵蓋了raw_sample中全部使用者22天內的購物行為(共七億條記錄)。欄位說明如下:

  • (1) user:脫敏過的使用者ID;
  • (2) time_stamp:時間戳;
  • (3) btag:行為型別, 包括以下四種:
型別 說明
ipv 瀏覽
cart 加入購物車
fav 喜歡
buy 購買
  • (4) cate:脫敏過的商品類目;
  • (5) brand: 脫敏過的品牌詞;

這裡以user + time_stamp為key,會有很多重複的記錄;這是因為我們的不同的型別的行為資料是不同部門記錄的,在打包到一起的時候,實際上會有小的偏差(即兩個一樣的time_stamp實際上是差異比較小的兩個時間)。

2.6 典型科研場景

根據使用者歷史購物行為預測使用者在接受某個廣告的曝光時的點選概率。

基線
AUC:0.622

0x03 目錄結構

程式碼目錄結構如下,前面五個是資料處理,models下面是模型,train_xxx是訓練程式碼。

.
├── 0_gen_sampled_data.py
├── 1_gen_sessions.py
├── 2_gen_dien_input.py
├── 2_gen_din_input.py
├── 2_gen_dsin_input.py
├── config.py
├── config.pyc
├── models
│   ├── __init__.py
│   ├── dien.py
│   ├── din.py
│   └── dsin.py
├── train_dien.py
├── train_din.py
└── train_dsin.py

0x04 資料構造

下面我們分析資料構造部分。

4.1 生成取樣資料

0_gen_sampled_data.py 的作用是生成取樣資料:

  • 基本邏輯如下:
  • 按照比率用使用者中提取取樣;
  • 從原始樣本骨架raw_sample中提取取樣使用者對應的資料;
  • 對於使用者資料進行去重;
  • 從行為資料中提取取樣使用者對應的資料;
  • 對於ad['brand']的缺失資料補充-1;
  • 使用 LabelEncoder對特徵進行硬編碼,將文字特徵進行編號:
    • 對ad['cate_id']和log['cate']進行去重合並,然後編碼;
    • 對ad['brand']和log['brand']進行去重合並,然後編碼;
  • log去除btag列;
  • log去除時間戳非法列;
  • 然後儲存成檔案;

4.2 生成DIEN需要的輸入

2_gen_dien_input.py的作用是生成DIEN需要的輸入,主要邏輯是:

獲取取樣資料中使用者session相關檔案( 這部分其實DIEN不需要 )。

FILE_NUM = len(
    list(
        filter(lambda x: x.startswith('user_hist_session_' + str(FRAC) + '_din_'), os.listdir('../sampled_data/'))))

遍歷檔案,把資料放入user_hist_session_

for i in range(FILE_NUM):
    user_hist_session_ = pd.read_pickle(
        '../sampled_data/user_hist_session_' + str(FRAC) + '_din_' + str(i) + '.pkl')
    user_hist_session.update(user_hist_session_)
    del user_hist_session_

使用gen_sess_feature_dien來生成session資料,並且分別生成字典,並且用進度條顯示。

生成一個dict,每個value是使用者行為(cate_id,brand,time_stamp)列表:

sess_input_dict = {'cate_id': [], 'brand': []}
neg_sess_input_dict = {'cate_id': [], 'brand': []}
sess_input_length = []
for row in tqdm(sample_sub[['user', 'time_stamp']].iterrows()):
    a, b, n_a, n_b, c = gen_sess_feature_dien(row)
    sess_input_dict['cate_id'].append(a)
    sess_input_dict['brand'].append(b)
    neg_sess_input_dict['cate_id'].append(n_a)
    neg_sess_input_dict['brand'].append(n_b)
    sess_input_length.append(c)

gen_sess_feature_dien函式獲取session資料。

  • 從後往前遍歷當前使用者的歷史session,把每個session中小於時間戳的都取出來。
  • 通過 for e in cur_sess[max(0, i + 1 - sess_max_len):i + 1]] 來取出session中最後sess_max_len個log,
  • 通過 sample 函式來生成負取樣資料,
  • 最後得到 'cate_id', 'brand' 兩個資料。
def gen_sess_feature_dien(row):
    sess_max_len = DIN_SESS_MAX_LEN
    sess_input_dict = {'cate_id': [0], 'brand': [0]}
    neg_sess_input_dict = {'cate_id': [0], 'brand': [0]}
    sess_input_length = 0
    user, time_stamp = row[1]['user'], row[1]['time_stamp']
    if user not in user_hist_session or len(user_hist_session[user]) == 0:

        sess_input_dict['cate_id'] = [0]
        sess_input_dict['brand'] = [0]
        neg_sess_input_dict['cate_id'] = [0]
        neg_sess_input_dict['brand'] = [0]
        sess_input_length = 0
    else:
        cur_sess = user_hist_session[user][0]
        for i in reversed(range(len(cur_sess))):
            if cur_sess[i][2] < time_stamp:
                sess_input_dict['cate_id'] = [e[0]
                                              for e in cur_sess[max(0, i + 1 - sess_max_len):i + 1]]
                sess_input_dict['brand'] = [e[1]
                                            for e in cur_sess[max(0, i + 1 - sess_max_len):i + 1]]

                neg_sess_input_dict = {'cate_id': [], 'brand': []}

                for c in sess_input_dict['cate_id']:
                    neg_cate, neg_brand = sample(c)
                    neg_sess_input_dict['cate_id'].append(neg_cate)
                    neg_sess_input_dict['brand'].append(neg_brand)

                sess_input_length = len(sess_input_dict['brand'])
                break
    return sess_input_dict['cate_id'], sess_input_dict['brand'], neg_sess_input_dict['cate_id'], neg_sess_input_dict[
        'brand'], sess_input_length

sample生成負取樣資料,就是隨機生成 index,然後如果對應的 category 等於 cate_id ,則重新生成index。以此獲取一個取樣資料。

def sample(cate_id):
    global ad
    while True:
        i = np.random.randint(0, ad.shape[0])
        sample_cate = ad.iloc[i]['cate_id']
        if sample_cate != cate_id:
            break
    return sample_cate, ad.iloc[i]['brand']

對於user的缺失數值用 -1 填充;把new_user_class_level重新命名;把user 重新命名為 'userid';

user = user.fillna(-1)
user.rename(
    columns={'new_user_class_level ': 'new_user_class_level'}, inplace=True)
sample_sub.rename(columns={'user': 'userid'}, inplace=True)

對sample_sub, user做連線,對 data, ad 做連線。

data = pd.merge(sample_sub, user, how='left', on='userid', )
data = pd.merge(data, ad, how='left', on='adgroup_id')

對於sparse_features進行硬編碼,將文字特徵進行編號。

對於dense_features進行標準縮放。

sparse_features = ['userid', 'adgroup_id', 'pid', 'cms_segid', 'cms_group_id', 'final_gender_code', 'age_level',
                   'pvalue_level', 'shopping_level', 'occupation', 'new_user_class_level', 'campaign_id',
                   'customer']
dense_features = ['price']

for feat in tqdm(sparse_features):
    lbe = LabelEncoder()  # or Hash
    data[feat] = lbe.fit_transform(data[feat])
mms = StandardScaler()
data[dense_features] = mms.fit_transform(data[dense_features])

對於 sparse_features和dense_features分別構建SingleFeat,這是deepCtr中構建的namedtuple。

sparse_feature_list = [SingleFeat(feat, data[feat].nunique(
) + 1) for feat in sparse_features + ['cate_id', 'brand']]

dense_feature_list = [SingleFeat(feat, 1) for feat in dense_features]
sess_feature = ['cate_id', 'brand']

對於sparse_feature中的特徵,從sess_input_dict和neg_sess_input_dict中獲取value,構建sequence,這兩個是session資料,就是行為資料。

sess_input = [pad_sequences(
    sess_input_dict[feat], maxlen=DIN_SESS_MAX_LEN, padding='post') for feat in sess_feature]
neg_sess_input = [pad_sequences(neg_sess_input_dict[feat], maxlen=DIN_SESS_MAX_LEN, padding='post') for feat in
                  sess_feature]

對於sparse_feature_list和dense_feature_list中的特徵,遍歷data中的對應value,構建成model_input。

把sess_input,neg_sess_input 和 [np.array(sess_input_length)]三個構建成sess_lists;

將sess_lists加入到model_input;

model_input = [data[feat.name].values for feat in sparse_feature_list] + \
              [data[feat.name].values for feat in dense_feature_list]
sess_lists = sess_input + neg_sess_input + [np.array(sess_input_length)]
model_input += sess_lists

接下來是把資料存入檔案。

pd.to_pickle(model_input, '../model_input/dien_input_' +
             str(FRAC) + '_' + str(DIN_SESS_MAX_LEN) + '.pkl')
pd.to_pickle(data['clk'].values, '../model_input/dien_label_' +
             str(FRAC) + '_' + str(DIN_SESS_MAX_LEN) + '.pkl')
try:
    pd.to_pickle({'sparse': sparse_feature_list, 'dense': dense_feature_list},
                 '../model_input/dien_fd_' + str(FRAC) + '_' + str(DIN_SESS_MAX_LEN) + '.pkl', )
except:
    pd.to_pickle({'sparse': sparse_feature_list, 'dense': dense_feature_list},
                 '../model_input/dien_fd_' + str(FRAC) + '_' + str(DIN_SESS_MAX_LEN) + '.pkl', )

0x05 DIEN模型

具體到模型,可以分為兩部分。

  • train_dien.py 負責把模型及相關部分構建完成。
  • dien.py 就是具體模型實現;

5.1 train_dien.py

首先讀入資料。

fd = pd.read_pickle('../model_input/dien_fd_' +
                    str(FRAC) + '_' + str(SESS_MAX_LEN) + '.pkl')
model_input = pd.read_pickle(
    '../model_input/dien_input_' + str(FRAC) + '_' + str(SESS_MAX_LEN) + '.pkl')
label = pd.read_pickle('../model_input/dien_label_' +
                       str(FRAC) + '_' + str(SESS_MAX_LEN) + '.pkl')

sample_sub = pd.read_pickle(
    '../sampled_data/raw_sample_' + str(FRAC) + '.pkl')

構建label。

sample_sub['idx'] = list(range(sample_sub.shape[0]))
train_idx = sample_sub.loc[sample_sub.time_stamp < 1494633600, 'idx'].values
test_idx = sample_sub.loc[sample_sub.time_stamp >= 1494633600, 'idx'].values

train_input = [i[train_idx] for i in model_input]
test_input = [i[test_idx] for i in model_input]

train_label = label[train_idx]
test_label = label[test_idx]

sess_len_max = SESS_MAX_LEN
BATCH_SIZE = 4096
sess_feature = ['cate_id', 'brand']
TEST_BATCH_SIZE = 2 ** 14

生成了keras模型,所以後面可以fit,predict。

model = DIEN(fd, sess_feature, 4, sess_len_max, "AUGRU", att_hidden_units=(64, 16),
             att_activation='sigmoid', use_negsampling=DIEN_NEG_SAMPLING)

keras的基本套路:compile,fit,predict。

model.compile('adagrad', 'binary_crossentropy',
              metrics=['binary_crossentropy', ])
if DIEN_NEG_SAMPLING:
    hist_ = model.fit(train_input, train_label, batch_size=BATCH_SIZE,
                      epochs=1, initial_epoch=0, verbose=1, )
    pred_ans = model.predict(test_input, TEST_BATCH_SIZE)
else:
    hist_ = model.fit(train_input[:-3] + train_input[-1:], train_label, batch_size=BATCH_SIZE, epochs=1,
                      initial_epoch=0, verbose=1, )
    pred_ans = model.predict(
        test_input[:-3] + test_input[-1:], TEST_BATCH_SIZE)

5.2 dien.py

這裡是模型程式碼,本文核心。因為DIEN總體思想不變,所以我們重點看看與之前第二版本( https://github.com/mouna99/dien )的區別。

5.2.1 第二版本

我們回憶下第二版本 主體程式碼,作為對比。

class Model_DIN_V2_Gru_Vec_attGru(Model):
    def __init__(self, n_uid, n_mid, n_cat, EMBEDDING_DIM, HIDDEN_SIZE, ATTENTION_SIZE, use_negsampling=False):
        super(Model_DIN_V2_Gru_Vec_attGru, self).__init__(n_uid, n_mid, n_cat,
                                                          EMBEDDING_DIM, HIDDEN_SIZE, ATTENTION_SIZE,
                                                          use_negsampling)

        # RNN layer(-s)
        with tf.name_scope('rnn_1'):
            rnn_outputs, _ = dynamic_rnn(GRUCell(HIDDEN_SIZE), inputs=self.item_his_eb,
                                         sequence_length=self.seq_len_ph, dtype=tf.float32,
                                         scope="gru1")
            tf.summary.histogram('GRU_outputs', rnn_outputs)

        # Attention layer
        with tf.name_scope('Attention_layer_1'):
            att_outputs, alphas = din_fcn_attention(self.item_eb, rnn_outputs, ATTENTION_SIZE, self.mask,
                                                    softmax_stag=1, stag='1_1', mode='LIST', return_alphas=True)
            tf.summary.histogram('alpha_outputs', alphas)

        with tf.name_scope('rnn_2'):
            rnn_outputs2, final_state2 = dynamic_rnn(VecAttGRUCell(HIDDEN_SIZE), inputs=rnn_outputs,
                                                     att_scores = tf.expand_dims(alphas, -1),
                                                     sequence_length=self.seq_len_ph, dtype=tf.float32,
                                                     scope="gru2")
            tf.summary.histogram('GRU2_Final_State', final_state2)

        inp = tf.concat([self.uid_batch_embedded, self.item_eb, self.item_his_eb_sum, self.item_eb * self.item_his_eb_sum, final_state2], 1)
        self.build_fcn_net(inp, use_dice=True)

然後看看 最新的第三版本。

5.2.2 輸入引數

DIEN的輸入引數如下,大致可以從註釋中知道其意義。

def DIEN(feature_dim_dict, seq_feature_list, embedding_size=8, hist_len_max=16,
         gru_type="GRU", use_negsampling=False, alpha=1.0, use_bn=False, dnn_hidden_units=(200, 80),
         dnn_activation='relu',
         att_hidden_units=(64, 16), att_activation="dice", att_weight_normalization=True,
         l2_reg_dnn=0, l2_reg_embedding=1e-6, dnn_dropout=0, init_std=0.0001, seed=1024, task='binary'):
   """Instantiates the Deep Interest Evolution Network architecture.

    :param feature_dim_dict: dict,to indicate sparse field (**now only support sparse feature**)like {'sparse':{'field_1':4,'field_2':3,'field_3':2},'dense':[]}
    :param seq_feature_list: list,to indicate  sequence sparse field (**now only support sparse feature**),must be a subset of ``feature_dim_dict["sparse"]``
    :param embedding_size: positive integer,sparse feature embedding_size.
    :param hist_len_max: positive int, to indicate the max length of seq input
    :param gru_type: str,can be GRU AIGRU AUGRU AGRU
    :param use_negsampling: bool, whether or not use negtive sampling
    :param alpha: float ,weight of auxiliary_loss
    :param use_bn: bool. Whether use BatchNormalization before activation or not in deep net
    :param dnn_hidden_units: list,list of positive integer or empty list, the layer number and units in each layer of DNN
    :param dnn_activation: Activation function to use in DNN
    :param att_hidden_units: list,list of positive integer , the layer number and units in each layer of attention net
    :param att_activation: Activation function to use in attention net
    :param att_weight_normalization: bool.Whether normalize the attention score of local activation unit.
    :param l2_reg_dnn: float. L2 regularizer strength applied to DNN
    :param l2_reg_embedding: float. L2 regularizer strength applied to embedding vector
    :param dnn_dropout: float in [0,1), the probability we will drop out a given DNN coordinate.
    :param init_std: float,to use as the initialize std of embedding vector
    :param seed: integer ,to use as random seed.
    :param task: str, ``"binary"`` for  binary logloss or  ``"regression"`` for regression loss
    :return: A Keras model instance.

    """

5.2.3 構建向量

這裡構建兩種向量,分別是密度向量(Dense Vector)和稀疏向量(Spasre Vector)。密度向量會儲存所有的值包括零值,而稀疏向量儲存的是索引位置及值,不儲存零值,在資料量比較大時,稀疏向量才能體現它的優勢和價值。

函式get_input是從輸入字典中提取向量。

def get_input(feature_dim_dict, seq_feature_list, seq_max_len):
    sparse_input, dense_input = create_singlefeat_inputdict(feature_dim_dict)
    user_behavior_input = OrderedDict()
    for i, feat in enumerate(seq_feature_list):
        user_behavior_input[feat] = Input(shape=(seq_max_len,), name='seq_' + str(i) + '-' + feat)
    user_behavior_length = Input(shape=(1,), name='seq_length')
    return sparse_input, dense_input, user_behavior_input, user_behavior_length

遍歷feature_dim_dict構建特徵字典,每一個item是name:Embedding。其作用是從sparse_embedding_dict中,獲取sparse_input中對應的具體輸入變數所對應的embedding。

sparse_embedding_dict = {feat.name: Embedding(feat.dimension, embedding_size,
                                              embeddings_initializer=RandomNormal(
                                                  mean=0.0, stddev=init_std, seed=seed),
                                              embeddings_regularizer=l2(
                                                  l2_reg_embedding),
                                              name='sparse_emb_' + str(i) + '-' + feat.name) for i, feat in
                         enumerate(feature_dim_dict["sparse"])}

獲取嵌入var,這裡每一個embedding_dict[feat]都是一個矩陣。

query_emb_list = get_embedding_vec_list(sparse_embedding_dict, sparse_input, feature_dim_dict["sparse"],
                                        return_feat_list=seq_feature_list)

把這些拼接起來。

query_emb = concat_fun(query_emb_list)
keys_emb = concat_fun(keys_emb_list)
deep_input_emb = concat_fun(deep_input_emb_list)

5.2.4 興趣進化層

下面開始呼叫生成興趣進化層。

hist, aux_loss_1 = interest_evolution(keys_emb, query_emb, user_behavior_length, gru_type=gru_type,
                                      use_neg=use_negsampling, neg_concat_behavior=neg_concat_behavior,
                                      embedding_size=embedding_size, att_hidden_size=att_hidden_units,
                                      att_activation=att_activation,
                                      att_weight_normalization=att_weight_normalization, )

其中:

  • DynamicGRU 相當於 第二版的dynamic_rnn,就是第一層 ‘rnn_1’;
  • auxiliary_loss與第二版幾乎一樣;
  • auxiliary_net只是最後一步 y_hat = tf.nn.sigmoid(dnn3) 不同;

具體程式碼如下:

def interest_evolution(concat_behavior, deep_input_item, user_behavior_length, gru_type="GRU", use_neg=False,
                       neg_concat_behavior=None, embedding_size=8, att_hidden_size=(64, 16), att_activation='sigmoid',
                       att_weight_normalization=False, ):

    aux_loss_1 = None

    rnn_outputs = DynamicGRU(embedding_size * 2, return_sequence=True,
                             name="gru1")([concat_behavior, user_behavior_length])
    
    if gru_type == "AUGRU" and use_neg:
        aux_loss_1 = auxiliary_loss(rnn_outputs[:, :-1, :], concat_behavior[:, 1:, :],

                                    neg_concat_behavior[:, 1:, :],

                                    tf.subtract(user_behavior_length, 1), stag="gru")  # [:, 1:]

    if gru_type == "GRU":
        rnn_outputs2 = DynamicGRU(embedding_size * 2, return_sequence=True,
                                  name="gru2")([rnn_outputs, user_behavior_length])
        hist = AttentionSequencePoolingLayer(att_hidden_units=att_hidden_size, att_activation=att_activation,
                                             weight_normalization=att_weight_normalization, return_score=False)([
            deep_input_item, rnn_outputs2, user_behavior_length])

    else:  # AIGRU AGRU AUGRU

        scores = AttentionSequencePoolingLayer(att_hidden_units=att_hidden_size, att_activation=att_activation,
                                               weight_normalization=att_weight_normalization, return_score=True)([
            deep_input_item, rnn_outputs, user_behavior_length])

        if gru_type == "AIGRU":
            hist = multiply([rnn_outputs, Permute([2, 1])(scores)])
            final_state2 = DynamicGRU(embedding_size * 2, gru_type="GRU", return_sequence=False, name='gru2')(
                [hist, user_behavior_length])
        else:  # AGRU AUGRU
            final_state2 = DynamicGRU(embedding_size * 2, gru_type=gru_type, return_sequence=False,
                                      name='gru2')([rnn_outputs, user_behavior_length, Permute([2, 1])(scores)])
        hist = final_state2
    return hist, aux_loss_1
5.2.4.1 DynamicGRU 1

DynamicGRU 相當於 第二版的dynamic_rnn,就是第一層 ‘rnn_1’。

這一層對應架構圖中黃色部分,即興趣抽取層(Interest Extractor Layer),主要元件是 GRU。

主要作用是通過模擬使用者的興趣遷移過程,基於行為序列提取使用者興趣序列。即將使用者行為歷史的item embedding輸入到dynamic rnn(第一層GRU)中。

rnn_outputs = DynamicGRU(embedding_size * 2, return_sequence=True,
                         name="gru1")([concat_behavior, user_behavior_length])
5.2.4.2 auxiliary_loss

輔助loss的計算其實是一個二分類模型,對應論文中:

在這裡插入圖片描述

auxiliary_loss與第二版幾乎一樣。

def auxiliary_loss(h_states, click_seq, noclick_seq, mask, stag=None):
    #:param h_states:
    #:param click_seq:
    #:param noclick_seq: #[B,T-1,E]
    #:param mask:#[B,1]
    #:param stag:
    #:return:
    hist_len, _ = click_seq.get_shape().as_list()[1:]
    mask = tf.sequence_mask(mask, hist_len)
    mask = mask[:, 0, :]
    mask = tf.cast(mask, tf.float32)
    # 倒數第一維度concat,其餘不變
    click_input_ = tf.concat([h_states, click_seq], -1)
    # 倒數第一維度concat,其餘不變
    noclick_input_ = tf.concat([h_states, noclick_seq], -1)
    # 獲取正樣本最後一個y_hat
    click_prop_ = auxiliary_net(click_input_, stag=stag)[:, :, 0]
    # 獲取負樣本最後一個y_hat
    noclick_prop_ = auxiliary_net(noclick_input_, stag=stag)[
                    :, :, 0]  # [B,T-1]
    # 對數損失,並且mask出真實歷史行為
    click_loss_ = - tf.reshape(tf.log(click_prop_),
                               [-1, tf.shape(click_seq)[1]]) * mask
    noclick_loss_ = - \
                        tf.reshape(tf.log(1.0 - noclick_prop_),
                                   [-1, tf.shape(noclick_seq)[1]]) * mask
    loss_ = tf.reduce_mean(click_loss_ + noclick_loss_)

    return loss_
5.2.4.3 auxiliary_net

auxiliary_net只是最後一步 y_hat = tf.nn.sigmoid(dnn3) 不同。

def auxiliary_net(in_, stag='auxiliary_net'):
    bn1 = tf.layers.batch_normalization(
        inputs=in_, name='bn1' + stag, reuse=tf.AUTO_REUSE)
    dnn1 = tf.layers.dense(bn1, 100, activation=None,
                           name='f1' + stag, reuse=tf.AUTO_REUSE)
    dnn1 = tf.nn.sigmoid(dnn1)
    dnn2 = tf.layers.dense(dnn1, 50, activation=None,
                           name='f2' + stag, reuse=tf.AUTO_REUSE)
    dnn2 = tf.nn.sigmoid(dnn2)
    dnn3 = tf.layers.dense(dnn2, 1, activation=None,
                           name='f3' + stag, reuse=tf.AUTO_REUSE)
    y_hat = tf.nn.sigmoid(dnn3)
    return y_hat
5.2.4.4 AttentionSequencePoolingLayer

這部分是deepctr完成的,對應第二版本的din_fcn_attention,關於din_fcn_attention可以參見前文[論文解讀] 阿里DIEN整體程式碼結構

DIEN 中,‘Attention_layer_1’ 層的作用是:通過在興趣抽取層基礎上加入Attention機制,模擬與當前目標廣告相關的興趣進化過程,對與目標物品相關的興趣演化過程進行建模。即將第一層的輸出,喂進第二層GRU,並用attention score(基於第一層的輸出向量與候選物料計算得出)來控制第二層的GRU的update gate。

class AttentionSequencePoolingLayer(Layer):
    """The Attentional sequence pooling operation used in DIN.

      Input shape
        - A list of three tensor: [query,keys,keys_length]
        - query is a 3D tensor with shape:  ``(batch_size, 1, embedding_size)``
        - keys is a 3D tensor with shape:   ``(batch_size, T, embedding_size)``
        - keys_length is a 2D tensor with shape: ``(batch_size, 1)``

      Output shape
        - 3D tensor with shape: ``(batch_size, 1, embedding_size)``.

      Arguments
        - **att_hidden_units**:list of positive integer, the attention net layer number and units in each layer.
        - **att_activation**: Activation function to use in attention net.
        - **weight_normalization**: bool.Whether normalize the attention score of local activation unit.
        - **supports_masking**:If True,the input need to support masking.

      References
        - [Zhou G, Zhu X, Song C, et al. Deep interest network for click-through rate prediction[C]//Proceedings of the 24th ACM SIGKDD International Conference on Knowledge Discovery & Data Mining. ACM, 2018: 1059-1068.](https://arxiv.org/pdf/1706.06978.pdf)
    """

    def __init__(self, att_hidden_units=(80, 40), att_activation='sigmoid', weight_normalization=False,
                 return_score=False,
                 supports_masking=False, **kwargs):
        self.att_hidden_units = att_hidden_units
        self.att_activation = att_activation
        self.weight_normalization = weight_normalization
        self.return_score = return_score
        super(AttentionSequencePoolingLayer, self).__init__(**kwargs)
        self.supports_masking = supports_masking

    def build(self, input_shape):
        if not self.supports_masking:
            if not isinstance(input_shape, list) or len(input_shape) != 3:
                raise ValueError('...)
            if len(input_shape[0]) != 3 or len(input_shape[1]) != 3 or len(input_shape[2]) != 2:
                raise ValueError(...)
            if input_shape[0][-1] != input_shape[1][-1] or input_shape[0][1] != 1 or input_shape[2][1] != 1:
                raise ValueError(...)
        else:
            pass
        self.local_att = LocalActivationUnit(
            self.att_hidden_units, self.att_activation, l2_reg=0, dropout_rate=0, use_bn=False, seed=1024, )
        super(AttentionSequencePoolingLayer, self).build(
            input_shape)  # Be sure to call this somewhere!

    def call(self, inputs, mask=None, training=None, **kwargs):
        if self.supports_masking:
            if mask is None:
                raise ValueError(...)
            queries, keys = inputs
            key_masks = tf.expand_dims(mask[-1], axis=1)

        else:
            queries, keys, keys_length = inputs
            hist_len = keys.get_shape()[1]
            key_masks = tf.sequence_mask(keys_length, hist_len)

        attention_score = self.local_att([queries, keys], training=training)
        outputs = tf.transpose(attention_score, (0, 2, 1))

        if self.weight_normalization:
            paddings = tf.ones_like(outputs) * (-2 ** 32 + 1)
        else:
            paddings = tf.zeros_like(outputs)
        outputs = tf.where(key_masks, outputs, paddings)

        if self.weight_normalization:
            outputs = tf.nn.softmax(outputs)
        if not self.return_score:
            outputs = tf.matmul(outputs, keys)
        outputs._uses_learning_phase = attention_score._uses_learning_phase

        return outputs
5.2.4.5 DynamicGRU 2

前面attention的score作為本GRU的一部分輸入。

    if gru_type == "AIGRU":
        hist = multiply([rnn_outputs, Permute([2, 1])(scores)])
        final_state2 = DynamicGRU(embedding_size * 2, gru_type="GRU", return_sequence=False, name='gru2')(
            [hist, user_behavior_length])
    else:  # AGRU AUGRU
        final_state2 = DynamicGRU(embedding_size * 2, gru_type=gru_type, return_sequence=False,
                                  name='gru2')([rnn_outputs, user_behavior_length, Permute([2, 1])(scores)])
    hist = final_state2

這部分是deepctr完成的,對應第二版本的GRU,把VecAttGRUCell等遷移到這裡。

class DynamicGRU(Layer):
    def __init__(self, num_units=None, gru_type='GRU', return_sequence=True, **kwargs):

        self.num_units = num_units
        self.return_sequence = return_sequence
        self.gru_type = gru_type
        super(DynamicGRU, self).__init__(**kwargs)

    def build(self, input_shape):
        # Create a trainable weight variable for this layer.
        input_seq_shape = input_shape[0]
        if self.num_units is None:
            self.num_units = input_seq_shape.as_list()[-1]
        if self.gru_type == "AGRU":
            self.gru_cell = QAAttGRUCell(self.num_units)
        elif self.gru_type == "AUGRU":
            self.gru_cell = VecAttGRUCell(self.num_units)
        else:
            self.gru_cell = tf.nn.rnn_cell.GRUCell(self.num_units)

        # Be sure to call this somewhere!
        super(DynamicGRU, self).build(input_shape)

    def call(self, input_list):
        """
        :param concated_embeds_value: None * field_size * embedding_size
        :return: None*1
        """
        if self.gru_type == "GRU" or self.gru_type == "AIGRU":
            rnn_input, sequence_length = input_list
            att_score = None
        else:
            rnn_input, sequence_length, att_score = input_list

        rnn_output, hidden_state = dynamic_rnn(self.gru_cell, inputs=rnn_input, att_scores=att_score,sequence_length=tf.squeeze(sequence_length,), dtype=tf.float32, scope=self.name)
        if self.return_sequence:
            return rnn_output
        else:
            return tf.expand_dims(hidden_state, axis=1)

5.2.5 DNN全連線層

現在我們得到了連線後的稠密表示向量,接下來就是利用全連通層自動學習特徵之間的非線性關係組合。

於是通過一個多層神經網路,得到最終的ctr預估值,這部分就是一個函式呼叫。

對應論文中的:

在這裡插入圖片描述

程式碼如下:

deep_input_emb = Concatenate()([deep_input_emb, hist])

deep_input_emb = tf.keras.layers.Flatten()(deep_input_emb)
if len(dense_input) > 0:
    deep_input_emb = Concatenate()(
        [deep_input_emb] + list(dense_input.values()))

output = DNN(dnn_hidden_units, dnn_activation, l2_reg_dnn,
             dnn_dropout, use_bn, seed)(deep_input_emb)
final_logit = Dense(1, use_bias=False)(output)
output = PredictionLayer(task)(final_logit)

model_input_list = get_inputs_list(
    [sparse_input, dense_input, user_behavior_input])

if use_negsampling:
    model_input_list += list(neg_user_behavior_input.values())

model_input_list += [user_behavior_length]

model = tf.keras.models.Model(inputs=model_input_list, outputs=output)

if use_negsampling:
    model.add_loss(alpha * aux_loss_1)
tf.keras.backend.get_session().run(tf.global_variables_initializer())
return model

至此, Keras 版本分析基本完成。

0xFF 參考

2019-11-06 廣告點選率預測:DeepCTR 庫的簡單介紹

DeepCTR:易用可擴充套件的深度學習點選率預測演算法包

Deepctr框架程式碼閱讀

相關文章