DeepMCP 网络介绍与源码浅析

DeepMCP 网络介绍与源码浅析

前言 (与正文无关, 请忽略~)

又有一段时间没写博客了, DIEN 写了一部分, 在草稿箱内躺着, DMT 看完了代码, 在想啥时候写… 一直拖着是因为早上真的不愿起床了 😂

广而告之

可以在微信中搜索 “珍妮的算法之路” 或者 “world4458” 关注我的微信公众号;另外可以看看知乎专栏 PoorMemory-机器学习, 以后文章也会发在知乎专栏中;

文章信息

核心观点

像 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, 定义为:

loss p = − 1 N ∑ y [ y log ⁡ y ^ + ( 1 − y ) log ⁡ ( 1 − y ^ ) ] \text{loss}_p = -\frac{1}{N}\sum_{y}\left[y\log\hat{y} + (1 - y)\log(1 - \hat{y})\right] lossp=N1y[ylogy^+(1y)log(1y^)]

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

s ( v u , v a ) = 1 1 + exp ⁡ ( − v u T v a ) s\left(\mathbf{v}_u, \mathbf{v}_a\right) = \frac{1}{1 + \exp{\left(-\mathbf{v}_u^T\mathbf{v}_a\right)}} s(vu,va)=1+exp(vuTva)1

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

l o s s m = − 1 N ∑ y [ y ( u , a ) log ⁡ s ( v u , v a ) + ( 1 − y ( u , a ) ) ( 1 − log ⁡ s ( v u , v a ) ) ] loss_m = -\frac{1}{N}\sum_{y}\left[y(u, a)\log s\left(\mathbf{v}_u, \mathbf{v}_a\right) + \left(1 - y(u, a)\right)\left(1 - \log s\left(\mathbf{v}_u, \mathbf{v}_a\right)\right)\right] lossm=N1y[y(u,a)logs(vu,va)+(1y(u,a))(1logs(vu,va))]

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

l l = 1 L ∑ i = 1 L ∑ − C ≤ j ≤ C 1 ≤ i + j ≤ L , j ≠ 0 log ⁡ p ( a i + j ∣ a i ) ll = \frac{1}{L}\sum_{i=1}^{L}\sum_{-C \leq j \leq C}^{1\leq i+j\leq L,j\neq 0}\log p(a_{i+j} | a_{i}) ll=L1i=1LCjC1i+jL,j=0logp(ai+jai)

其中 L L L 为历史行为序列长度, C C C 表示上下文窗口大小. 之后采用 Negative Sampling 的方式来定义 p ( a i + j ∣ a i ) p(a_{i+j} | a_{i}) p(ai+jai):

p ( a i + j ∣ a i ) = σ ( h a i + j T h a i ) ∏ q = 1 Q σ ( − h a q T h a i ) p(a_{i+j} | a_{i}) = \sigma(\mathbf{h}_{a_{i+j}}^T\mathbf{h}_{a_i})\prod_{q=1}^{Q}\sigma(-\mathbf{h}_{a_q}^T\mathbf{h}_{a_i}) p(ai+jai)=σ(hai+jThai)q=1Qσ(haqThai)

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

loss c = − 1 L ∑ i = 1 L ∑ − C ≤ j ≤ C 1 ≤ i + j ≤ L , j ≠ 0 [ log ⁡ [ σ ( h a i + j T h a i ) ] + ∑ q = 1 Q log ⁡ [ σ ( − h a q T h a i ) ] ] \text{loss}_c = -\frac{1}{L}\sum_{i=1}^{L}\sum_{-C \leq j \leq C}^{1\leq i+j\leq L,j\neq 0}\left[\log\left[\sigma(\mathbf{h}_{a_{i+j}}^T\mathbf{h}_{a_i})\right] + \sum_{q=1}^{Q}\log\left[\sigma(-\mathbf{h}_{a_q}^T\mathbf{h}_{a_i})\right]\right] lossc=L1i=1LCjC1i+jL,j=0[log[σ(hai+jThai)]+q=1Qlog[σ(haqThai)]]

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

loss = loss p + α loss m + β loss c \text{loss} = \text{loss}_p + \alpha\text{loss}_m + \beta\text{loss}_c loss=lossp+αlossm+βlossc

其中 α \alpha α β \beta β 为超参数, 用于调节每个任务的重要程度.

在线预估

在线进行预估时, 只需要使用 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 个数分别是 SM, embedding 的大小为 K, Batch 大小为 B, 那么上面函数的输出结果大小为 [B, S + M, K].

其中 get_masked_one_hotget_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 网络的效果产生正向的影响.

第一次一本正经的总结… 😂😂😂

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值