DeepMCP 网络介绍与源码浅析

文章信息

  • 论文标题: Representation Learning-Assisted Click-Through Rate Prediction

  • 论文地址: https://www.ijcai.org/Proceedings/2019/0634.pdf

  • 代码地址: https://github.com/oywtece/deepmcp

  • 发表时间: IJCAI, 2019

  • 论文作者: Ouyang, Wentao and Zhang, Xiuwu and Ren, Shukui and Qi, Chao and Liu, Zhaojie and Du, Yanlong

  • 作者单位: Alibaba Group

核心观点

像 W&D, FM, DeepFM, NFM 等模型主要是刻画输入特征和CTR之间的关系, 但它们并没有考虑到特征与特征之间关联. 本文提出的 DeepMCP 网络由 3 个子网络构建而成, 其中 Matching 子网络用于构建用户和广告之间的关系, Correlation 子网络用于刻画广告和广告之间的关系, 而 Prediction 子网络则用于刻画 feature 和 CTR 之间的关系, 三个子网络联合训练, 以学习出更具表达力的特征,从而提升对 pCTR 的预估能力.

在网络结构上, Matching 网络采用双塔结构, 而 Correlation 和 Prediction 网络则是典型的 DNN 网络. 此外值得注意的是, Correlation 网络中的目标函数在设计时借鉴了 word2vec 中的 negative sampling.

核心观点解读

DeepMCP 网络结构如下:

图片

主要由三部分构成:

  • Prediction Subnet: 用于建模特征-CTR 之间的关系, 输入为 User, Query, Ad 以及 Other 特征对应的 embedding, 输出为 pCTR. loss 采用 Cross Entropy Loss, 定义为:

 

 

  • Matching Subnet: 采用双塔结构, 用于建模用户和广告之间的关系(比如广告是否符合用户的兴趣), 目标是学习更为准确的用户以及广告 embedding. 使用 DNN 分别获取到用户的高阶表达  以及广告的高阶表达  后, 使用下式计算二者的匹配分数:

 

 

Matching Subnet 的 loss 定义如下, 其中 label 和 Prediction Subnet 网络中的 label 一致:  表示用户  点击了广告 , 等于  则表示未发生点击.

 

 

  • Correlation Subnet: 用于建模广告-广告之间的关系, 其借鉴 Word2Vec 中的 Skip-Gram 模型来学习广告特征的表达. 图中 Ad features 表示目标商品的特征, 而 Context ad features 表示用户历史点击过的样本特征, 即正样本特征, 而 Negative ad features 表示负样本特征. 假设用户的历史点击商品序列为 , Correlation Subnet 的目标函数为最大化 log 似然:

 

 

其中  为历史行为序列长度,  表示上下文窗口大小. 之后采用 Negative Sampling 的方式来定义  :

 

 

因此 Correlation Subnet 的 loss 函数定义为:

 

 

三个子网络进行联合训练, DeepMCP 的联合训练 Loss 定义为:

 

 

其中  和  为超参数, 用于调节每个任务的重要程度.

在线预估

在线进行预估时, 只需要使用 Prediction Subnet 进行 Inference 即可.

源码分析

代码地址为: https://github.com/oywtece/deepmcp 直接分析其中 deepmcp.py 中的代码, 捡其中的重点介绍. 发现前一篇博客 DSIN 深度 Session 兴趣网络介绍及源码剖析 中对代码的分析太过专注细节, 事无巨细的感觉, 反而把重点给掩盖了, 文章全篇看下来太累, 因此下面代码分析希望能更简洁一些, 突出重点.

网络总览

DeepMCP 各个子网络的输入输出分别定义如下:

data_embed_concat = get_concate_embed(x_input_one_hot, x_input_mul_hot)
## Prediction Subnet 的输出
y_hat = get_pred_output(data_embed_concat) 
## Matching Subnet 的输出
y_hat_match = get_match_output(data_embed_concat) 
## Correlation Subnet 的输出
inner_prod_dict_corr = get_corr_output(x_input_corr)

其中输入特征被划分为单值特征 x_input_one_hot 和多值特征 x_input_mul_hot, 通过 get_concate_embed() 函数的处理映射为低维稠密向量并进行拼接, 然后分别输入到 Prediction 和 Matching 网络中.

Correlation 网络的输入 x_input_corr 应该要包含正负样本以及 target item, 具体后面详细看 get_corr_output() 函数时介绍.

下一步则是构建 Loss:

## Prediction Subnet 和 Matching Subnet 对应的 Loss
loss_ctr = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=y_hat, labels=y_target))
loss_match = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=y_hat_match, labels=y_target))

## Correlation Subnet 对应的 Loss
## 假设负样本个数为 Q, 那么 inner_prod_dict_corr 的大小为 Q + 1
## inner_prod_dict_corr[0] 表示对正样本的预估结果
# logloss
y_corr_cast_1 = tf.ones_like(inner_prod_dict_corr[0])
y_corr_cast_0 = tf.zeros_like(inner_prod_dict_corr[0])
# pos
loss_corr = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=inner_prod_dict_corr[0], \
    labels=y_corr_cast_1))
# neg
for i in range(n_neg_used_corr):
    loss_corr += tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=inner_prod_dict_corr[i+1], \
                 labels=y_corr_cast_0))

loss = loss_ctr + alpha*loss_match + beta*loss_corr

注意到 Prediction Subnet 和 Matching Subnet 对应的 Loss 中, 使用的是相同的 Label.

网络框架大致如上, 下面再简单看看实现细节.

输入 Embedding

主要使用 get_concate_embed 函数实现:

# output: (B, n_one_hot_slot + n_mul_hot_slot, k)
def get_concate_embed(x_input_one_hot, x_input_mul_hot):
    data_embed_one_hot = get_masked_one_hot(x_input_one_hot)
    data_embed_mul_hot = get_masked_mul_hot(x_input_mul_hot)
    data_embed_concat = tf.concat([data_embed_one_hot, data_embed_mul_hot], 1)
    return data_embed_concat

假设单值特征和多值特征对应的 Field 个数分别是 S 和 M, embedding 的大小为 K, Batch 大小为 B, 那么上面函数的输出结果大小为 [B, S + M, K].

其中 get_masked_one_hot 和 get_masked_mul_hot 实现如下, 采用 Mask 的原因是, 令等于0 的位置对应的 embedding 应该是 0 向量. 另外 get_masked_mul_hot 中采用的 sum pooling.

# add mask
def get_masked_one_hot(x_input_one_hot):
    data_mask = tf.cast(tf.greater(x_input_one_hot, 0), tf.float32)
    data_mask = tf.expand_dims(data_mask, axis = 2)
    data_mask = tf.tile(data_mask, (1,1,k))
    # output: (?, n_one_hot_slot, k)
    data_embed_one_hot = tf.nn.embedding_lookup(emb_mat, x_input_one_hot)
    data_embed_one_hot_masked = tf.multiply(data_embed_one_hot, data_mask)
    return data_embed_one_hot_masked

def get_masked_mul_hot(x_input_mul_hot):
    data_mask = tf.cast(tf.greater(x_input_mul_hot, 0), tf.float32)
    data_mask = tf.expand_dims(data_mask, axis = 3)
    data_mask = tf.tile(data_mask, (1,1,1,k))
    # output: (?, n_mul_hot_slot, max_len_per_slot, k)
    data_embed_mul_hot = tf.nn.embedding_lookup(emb_mat, x_input_mul_hot)
    data_embed_mul_hot_masked = tf.multiply(data_embed_mul_hot, data_mask)
    # output: (?, n_mul_hot_slot, k)
    data_embed_mul_hot_masked = tf.reduce_sum(data_embed_mul_hot_masked, 2)
    return data_embed_mul_hot_masked

Prediction Subnet

网络的输入为大小等于 [B, S + M, K] 的 Embedding, 首先 reshape 成 [B, (S + M) * K] 的 Tensor, 再输入到 DNN 中. 其中 i = n_layer - 1 时表示进行到最后一层, 不需要加激活函数, 因为前面介绍 Loss 时使用的是 tf.nn.sigmoid_cross_entropy_with_logits.

def get_pred_output(data_embed_concat):
    # include output layer
    n_layer = len(layer_dim)
    data_embed_dnn = tf.reshape(data_embed_concat, [-1, (n_one_hot_slot + n_mul_hot_slot)*k])
    cur_layer = data_embed_dnn
    # loop to create DNN struct
    for i in range(0, n_layer):
        # output layer, linear activation
        if i == n_layer - 1:
            cur_layer = tf.matmul(cur_layer, weight_dict[i]) + bias_dict[i]
        else:
            cur_layer = tf.nn.relu(tf.matmul(cur_layer, weight_dict[i]) + bias_dict[i])
            cur_layer = tf.nn.dropout(cur_layer, keep_prob)
    
    y_hat = cur_layer
    return y_hat

Matching Subnet

Matching 子网络采用双塔结构, 构建用户和广告特征之间的匹配关系:

# matching loss input
def get_match_output(data_embed_concat):

 """
 输入为 [B, S+M, K] 的 embedding, S+M 是所有特征的总个数, 其中 user_ft_idx (ft 是 feature 的缩写) 这个列表
 保存所有用户特征对应的索引, 下面这段代码就是从 data_embed_concat 中取出
 所有用户特征对应的 embedding, 假设用户特征个数为 U, 那么
 user_ft_cols 的大小为 [B, U, K]
 """
    cur_idx = user_ft_idx[0]
    user_ft_cols = data_embed_concat[:, cur_idx:cur_idx+1, :]
    for i in range(1, len(user_ft_idx)):
        cur_idx = user_ft_idx[i]
        cur_x = data_embed_concat[:, cur_idx:cur_idx+1, :]
        user_ft_cols = tf.concat([user_ft_cols, cur_x], 1)
        
    """
    同理, 假设广告特征的个数为 A, 那么 
    ad_ft_cols 的大小为 [B, A, K]
 """
    cur_idx = ad_ft_idx[0]
    ad_ft_cols = data_embed_concat[:, cur_idx:cur_idx+1, :]
    for i in range(1, len(ad_ft_idx)):
        cur_idx = ad_ft_idx[i]
        cur_x = data_embed_concat[:, cur_idx:cur_idx+1, :]
        ad_ft_cols = tf.concat([ad_ft_cols, cur_x], 1)
    
    """
 user_ft_vec: [B, U*K]
 ad_ft_vec: [B, A*K]
 """
    user_ft_vec = tf.reshape(user_ft_cols, [-1, n_user_ft*k])
    ad_ft_vec = tf.reshape(ad_ft_cols, [-1, n_ad_ft*k])
    
    n_layer_match = len(layer_dim_match)
 
 """
 用户特征 user_ft_vec 经过 DNN, 得到高阶特征表达 user_rep,
 假设大小为 [B, R], DNN 最后一层采用 tanh 激活函数, 论文中专门提到过.
 """
    cur_layer = user_ft_vec
    for i in range(0, n_layer_match):
        if i == n_layer_match - 1:
         ## 最后一层 tanh 激活函数
            cur_layer = tf.nn.tanh(tf.matmul(cur_layer, weight_dict_user[i]) + bias_dict_user[i])
        else:
            cur_layer = tf.nn.relu(tf.matmul(cur_layer, weight_dict_user[i]) + bias_dict_user[i])
    user_rep = cur_layer
    
    """
    广告特征 user_ft_vec 经过 DNN, 得到高阶特征表达 ad_rep
    假设大小为 [B, R], DNN 最后一层采用 tanh 激活函数
    """
    cur_layer = ad_ft_vec
    for i in range(0, n_layer_match):
        if i == n_layer_match - 1:
            cur_layer = tf.nn.tanh(tf.matmul(cur_layer, weight_dict_ad[i]) + bias_dict_ad[i])
        else:
            cur_layer = tf.nn.relu(tf.matmul(cur_layer, weight_dict_ad[i]) + bias_dict_ad[i])
    ad_rep = cur_layer
    
    """
    user_rep 和 ad_rep 进行内积, 结果为 [B, 1]
 """
    inner_prod = tf.reduce_sum(tf.multiply(user_rep, ad_rep), 1, keep_dims=True)
    return inner_prod

Correlation Subnet

用于广告特征之间关系的建模.

def get_corr_output(x_input_corr):

 """
 不纠结 partition_input_corr 的实现, 其中带 x_tar_ 前缀的表示 target item 对应的特征, 而带 x_input_ 前缀的表示正负样本对应的特征, 其大小为 Q + 1.
 """
    x_tar_one_hot_corr, x_tar_mul_hot_corr, x_input_one_hot_dict_corr, x_input_mul_hot_dict_corr = \
        partition_input_corr(x_input_corr)
    
    """
    获取 target item 对应的 embedding, 假设为 [B, C+D, K], 经 reshape 后为
    [B, (C+D)*K]
 """
    data_embed_tar = get_concate_embed(x_tar_one_hot_corr, x_tar_mul_hot_corr)
    data_vec_tar = tf.reshape(data_embed_tar, [-1, (n_one_hot_slot_corr + n_mul_hot_slot_corr)*k])
    
    n_layer_corr = len(layer_dim_corr) ## 网络层数
    """
    target item 的 embedding 经过 DNN, 得到高阶表达: data_rep_tar,
    大小为 [B, E]
 """
    cur_layer = data_vec_tar
    for i in range(0, n_layer_corr):
        if i == n_layer_corr - 1:
            cur_layer = tf.nn.tanh(tf.matmul(cur_layer, weight_dict_corr[i]) + bias_dict_corr[i])
        else:
            cur_layer = tf.nn.relu(tf.matmul(cur_layer, weight_dict_corr[i]) + bias_dict_corr[i])
    data_rep_tar = cur_layer
    
    # idx 0 - pos, idx 1 -- neg
    """
    获取正负样本对应的 embedding, 并分别输入到 DNN 中. 其中 n_neg_used_corr
    表示负样本个数 Q, 因此正负样本总数为 Q + 1, idx=0 表示正样本, 其余为负样本.
  经过 DNN 后, 分别和 target item 的 embedding 做内积
 """
    inner_prod_dict = {}
    for mm in range(n_neg_used_corr + 1):
     """
     获取样本对应的 embedding
     """
        cur_data_embed = get_concate_embed(x_input_one_hot_dict_corr[mm], \
                                           x_input_mul_hot_dict_corr[mm])
        cur_data_vec = tf.reshape(cur_data_embed, [-1, (n_one_hot_slot_corr + n_mul_hot_slot_corr)*k])
        """
        经过 DNN 处理
  """
        cur_layer = cur_data_vec
        for i in range(0, n_layer_corr):
            if i == n_layer_corr - 1:
                cur_layer = tf.nn.tanh(tf.matmul(cur_layer, weight_dict_corr[i]) + bias_dict_corr[i])
            else:
                cur_layer = tf.nn.relu(tf.matmul(cur_layer, weight_dict_corr[i]) + bias_dict_corr[i])
        cur_data_rep = cur_layer
        # each ele - None*1
        """
        和 target item 对应的 embedding 做内积, 保存到字典 inner_prod_dict,
        字典大小为 Q+1, 其中 key = 0, 1, ..., Q, 每个 value 的大小为 [B, 1],
        其中 key=0 的 value 表示正样本和 target item 进行内积的结果.
  """
        inner_prod_dict[mm] = tf.reduce_sum(tf.multiply(data_rep_tar, cur_data_rep), 1, \
                            keep_dims=True)
    
    return inner_prod_dict

总结

我认为 DeepMCP 有效的原因是: 首先我们的共识是交叉特征对网络的预估效果应该是起正向作用的. Prediction 网络是一个 DNN, 其进行特征交叉的方式是隐式的, 而 DeepFM, xDeepFM, DCN 等工作证明了对特征交叉进行显式的建模是有收益的, 本文介绍的 Matching 子网络以及 Correlation 子网络其实可以认为是建模用户特征和广告特征, 以及广告特征之间的交叉关系. 和前面 DeepFM 等工作的区别是, DeepMCP 的交叉特征并没有直接输入到 Prediction Subnet 中, 而是通过联合建模共享 Embedding 的方式对 Prediction 网络进行影响. 如果用户和广告特征之间的匹配关系学习的越好, 用户和广告特征能学习到更准确的表达, 将有助于 Prediction 网络预估效果的提升, 而 Prediction 网络效果提升, 也会同时对 Matching 网络的效果产生正向的影响

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值