小趙帶你讀論文系列14-阿里媽媽之Deep Interest Network for Click-Through Rate Prediction

前言

鬼才知道我爲什麼要學管理經濟學和管理數學,學不進去了,算了看看論文讀讀代碼放鬆一下。

這是阿里DIN系列的第一篇文章,文章讀起來不是很難,不過工業上實現會考慮時延就很麻煩,主要的工作我覺得像是引入了Attention機制(他自己解釋說有點差別)。以往都是根據用戶的Profile,Behavior Sequence做一個用戶的Embedding,然後根據這個Embedding來和待推薦的商品作比較,最終輸出用戶點擊該商品的概率。阿里覺得不好,第一,這種對待所有商品用戶的Embedding都一成不變,這顯然不行。第二,單純擴大Embeddingsize來獲取更多的信息,這樣會浪費大量的計算成本並且有過擬合現象。以往的推薦形式如下圖:

主體內容

第一,阿里媽媽覺得待推薦的物品是否會被點擊和用戶的歷史行爲有着顯著的關係,那麼我們能不能使用用戶的歷史行爲(也代表了用戶的興趣,Deep Interest大概從這裏命名)來表徵用戶對於不同商品的興趣呢,換句話說,我能不能使得用戶對於不同的商品有不同的Embedding呢。然後他就把待推薦物品和用戶歷史點擊行爲進行Attention,他死不承認用Attention的一個原因大體就是他的Attention沒有做正則化,以及他的Attention weightssum=1。模型構建如下:

第二,他在工業上做了兩個優化。第一個優化在每次批處理時,僅使用那些在當前Batch中非零(至少出現過一次)的特徵作爲正則化項(這式子翻譯過來就是對於出現頻率高的,給與較小的正則化強度;
3.對於出現頻率低的,給予較大的正則化強度。),因爲如果使用全特徵參數的話,會增大計算量,丟棄的話就會過擬合。公式如下(文中還做了一個近似的操作,如公式2,文中那個λ是個需要手動調節的超參數,不用管):

他在工業上實現的第二個優化就是使用了一個長得非常奇葩的激活函數,這個函數爲什麼要這麼寫我還真不知道,原文裏說的是分割點應該由數據決定(PRelu==LeakyRelu)而且可以防止w更新緩慢,但看着就像Sigmoid平移了。。對了他給這個激活函數的名字叫做Dice

第三,這個評價函數不像是AUC,後來發現這是推薦中常用的手法,叫做GAUC(如公式1),只需要保證用戶點擊的排在前面就可以,不是一定要排在第一個,具體可以詳見GAUC,而且還有一種新奇的比較模型之間提升關係的方法,我很喜歡,如公式2:

第四,算是對自己的一個提醒,這玩意CaseStudy十分重要啊,字數不夠CaseStudy來湊,還能加上幾個好看的圖片和TSNE降維(顏色越深代表預測的點擊概率越大)。

代碼實現

僅貼上論文主體框架的代碼,本人加了幾個註釋,方便大家理解一下數據傳輸的流程。原代碼真的是晦澀難懂。。。。命名習慣極差,推薦大家參照PEP8命名規範

import tensorflow as tf

from ..feature_column import SparseFeat, VarLenSparseFeat, DenseFeat, build_input_features
from ..inputs import create_embedding_matrix, embedding_lookup, get_dense_input, varlen_embedding_lookup, \
    get_varlen_pooling_list
from ..layers.core import DNN, PredictionLayer
from ..layers.sequence import AttentionSequencePoolingLayer
from ..layers.utils import concat_func, NoMask, combined_dnn_input


def DIN(dnn_feature_columns, history_feature_list, dnn_use_bn=False,
        dnn_hidden_units=(200, 80), dnn_activation='relu', att_hidden_size=(80, 40), att_activation="dice",
        att_weight_normalization=False, l2_reg_dnn=0, l2_reg_embedding=1e-6, dnn_dropout=0, seed=1024,
        task='binary'):

    features = build_input_features(dnn_feature_columns)

    sparse_feature_columns = list(
        filter(lambda x: isinstance(x, SparseFeat), dnn_feature_columns)) if dnn_feature_columns else []
    dense_feature_columns = list(
        filter(lambda x: isinstance(x, DenseFeat), dnn_feature_columns)) if dnn_feature_columns else []
    varlen_sparse_feature_columns = list(
        filter(lambda x: isinstance(x, VarLenSparseFeat), dnn_feature_columns)) if dnn_feature_columns else []  # 特徵分類

    history_feature_columns = []
    sparse_varlen_feature_columns = []
    history_fc_names = list(map(lambda x: "hist_" + x, history_feature_list))
    for fc in varlen_sparse_feature_columns:
        feature_name = fc.name
        if feature_name in history_fc_names:
            history_feature_columns.append(fc)
        else:
            sparse_varlen_feature_columns.append(fc)

    inputs_list = list(features.values())

    embedding_dict = create_embedding_matrix(dnn_feature_columns, l2_reg_embedding, seed, prefix="") # 爲每個特徵做embedding

    query_emb_list = embedding_lookup(embedding_dict, features, sparse_feature_columns, history_feature_list,
                                      history_feature_list, to_list=True) # 根據列表查找特徵id的embedding
    keys_emb_list = embedding_lookup(embedding_dict, features, history_feature_columns, history_fc_names,
                                     history_fc_names, to_list=True)
    dnn_input_emb_list = embedding_lookup(embedding_dict, features, sparse_feature_columns,
                                          mask_feat_list=history_feature_list, to_list=True)
    dense_value_list = get_dense_input(features, dense_feature_columns)

    sequence_embed_dict = varlen_embedding_lookup(embedding_dict, features, sparse_varlen_feature_columns)
    sequence_embed_list = get_varlen_pooling_list(sequence_embed_dict, features, sparse_varlen_feature_columns,
                                                  to_list=True) # 變長變爲定長

    dnn_input_emb_list += sequence_embed_list  # 合併列表

    keys_emb = concat_func(keys_emb_list, mask=True) # Goods對應的列表都concat起來,
    deep_input_emb = concat_func(dnn_input_emb_list)
    query_emb = concat_func(query_emb_list, mask=True)
    hist = AttentionSequencePoolingLayer(att_hidden_size, att_activation,
                                         weight_normalization=att_weight_normalization, supports_masking=True)([
        query_emb, keys_emb]) # 根據Ads和Goods的embedding計算Attention

    deep_input_emb = tf.keras.layers.Concatenate()([NoMask()(deep_input_emb), hist])
    deep_input_emb = tf.keras.layers.Flatten()(deep_input_emb)
    dnn_input = combined_dnn_input([deep_input_emb], dense_value_list)
    output = DNN(dnn_hidden_units, dnn_activation, l2_reg_dnn, dnn_dropout, dnn_use_bn, seed=seed)(dnn_input) # 喂入一個Dnn
    final_logit = tf.keras.layers.Dense(1, use_bias=False,
                                        kernel_initializer=tf.keras.initializers.glorot_normal(seed))(output)

    output = PredictionLayer(task)(final_logit)

    model = tf.keras.models.Model(inputs=inputs_list, outputs=output) # 這裏寫法真不一樣,這裏直接用Model而不是層返回,有可能爲了封裝方便。
    return model

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章