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