共计 3775 个字符,预计需要花费 10 分钟才能阅读完成。
前言
在之前的一篇文章中介绍了 Hinton 的胶囊网络,MIND 中也是使用了胶囊网络不过做了一些修改。
MIND整体架构图如下所示:
对着这网络结构一步一步解析。
抽象网络结构逻辑
输入:
- 用户的基础属性特征 Other Features 例如性别、年龄等属性特征
- 用户的序列特征,用户实时的行为列表,可以是点击列表或者购买列表等等
- 用户的target ,如果是 ctr 任务那么就是点击的样本
计算:
- 序列特征每个 id 需要经过 Embedding Lookup ,这里是包含 Item 多个特征的Embedding 拼接(ps:分类,颜色等相关的特征)
- 序列特征经过 Pooling
- 序列特征进入兴趣网络,提取用户的兴趣特征
- 基础属性特征与兴趣特征合并喂入 FC
- 4 的输出作为 Attention(Label-aware Attention Layer) 的Q K ,然后与 Target(V) 计算
- Sample Softmax Loss 优化
输出:
- 物料的 Embedding 用于线上召回,实时获取用户的表征 Embedding 实现 U2I召回
关键环节介绍
兴趣提取层
这块的核心计算逻辑是与前一篇文章描述的动态路由算法大部分都是一致的,但是这篇文章还是整了一个新的名字叫做B2I Dynamic Routing。
区别点主要在以下三个方面:
- 共享映射矩阵 –在上一篇文章中举例说明minists手写体构建 low-level capsule 到 high-level capsule 时每一个之间的映射矩阵都是相互独立的。但是 MIND 这篇文章使用固定的映射矩阵?给出的理由有两个 (1) 用户的行为数量是不等,有的只有几个,有的甚至上百个 (2)保持相同的向量空间,如果是不同的映射矩阵可能将兴趣映射到不同的向量空间,所以最终的路由算法是:
b _{ij}=u_j*S*e_i - 随机初始话映射矩阵 — 上一篇文章都是初始化默认 0 ,但是此处我们是共享矩阵,如果都是0 那么默认兴趣都是一样的,完全不符合我们的意图。所以使用高斯分布N(0, σ2)来初始化参数。
-
动态兴趣个数 — 顾名思义就是每个用户的兴趣个数都是不一样的,之前那篇文章都是固定的个数。
K_u = max(1, min(K, log2(|I_u |)))
Attention 网络层
下面的公式中 \vec e_i 标识 target 物料的向量表示,\vec v_u表示生成的用户向量表示,这里展示的是训练的时候的计算。
假设你在线上走实时U2I 召回的时候, target 物料都是从内容库里去匹配,可以理解为全量库可推荐的物料向量,使用ANN 方法实时计算最相似的Top N。
\vec v_u = Attention( \vec e_i , V_u, V_u)= Vu*softmax(pow(V^T_u * pow(V^T_u * \vec e_i ,p ))
代码解析
代码解析主要是在胶囊网络,参考代码来自deepMatch,我在这里做了详细的解释,结合论文帮助理解,Attention 那块也是可以去看代码比较好理解。
class CapsuleLayer(Layer):
def __init__(self, input_units, out_units, max_len, k_max, iteration_times=3,
init_std=1.0, **kwargs):
self.input_units = input_units
self.out_units = out_units
self.max_len = max_len # 序列的最大长度
self.k_max = k_max # 兴趣的个数
self.iteration_times = iteration_times # 动态路由计算的次数,一般情况下需要计算三次
self.init_std = init_std
super(CapsuleLayer, self).__init__(**kwargs)
def build(self, input_shape):
# 不可训练 注意 trainable 参数
self.routing_logits = self.add_weight(shape=[1, self.k_max, self.max_len],
initializer=RandomNormal(stddev=self.init_std),
trainable=False, name="B", dtype=tf.float32)
self.bilinear_mapping_matrix = self.add_weight(shape=[self.input_units, self.out_units],
initializer=RandomNormal(stddev=self.init_std),
name="S", dtype=tf.float32)
super(CapsuleLayer, self).build(input_shape)
def call(self, inputs, **kwargs):
behavior_embddings, seq_len = inputs # [B,max_len,input_units] , [B,1] seq_len 记录序列的真实长度,后续用于 Mask
batch_size = tf.shape(behavior_embddings)[0]
seq_len_tile = tf.tile(seq_len, [1, self.k_max]) # [B,k_max] 对每个兴趣都要做 Mask 处理
for i in range(self.iteration_times): # 动态路由的循环迭代
mask = tf.sequence_mask(seq_len_tile, self.max_len) # 生成 Mask 矩阵 [B,k_max,max_len]
pad = tf.ones_like(mask, dtype=tf.float32) * (-2 ** 32 + 1) # 构建 pad 矩阵 [B,k_max,max_len] .使用 32位最小值可以在 sigmoid的时候输出接近于 0
routing_logits_with_padding = tf.where(mask, tf.tile(self.routing_logits, [batch_size, 1, 1]), pad) # 对路由进行 Mask 处理 [B,k_max,max_len]
weight = tf.nn.softmax(routing_logits_with_padding) # 操作 softmax 得到 w_ij 可以对比原论文 [B,k_max,max_len]
# 原文得到High-cat 需要经过 w_ij* S_ij*C_i 此步骤只是计算后面两个参数的点积 [B,max_len,input_units] dot axis=1 [input_units,out_units](Broadcast) ---> [B,max_len,out_units]
behavior_embdding_mapping = tf.tensordot(behavior_embddings, self.bilinear_mapping_matrix, axes=1)
Z = tf.matmul(weight, behavior_embdding_mapping) # 接上一步完成 High-cat 输出计算 [B,k_max,out_units]
interest_capsules = squash(Z) # [B,k_max,out_units]
# [B,k_max,out_units] matual [B,out_units,max_len] ----> [B,k_max,max_len] ---reduce_sum--> [1,k_max,max_len]
delta_routing_logits = reduce_sum(
tf.matmul(interest_capsules, tf.transpose(behavior_embdding_mapping, perm=[0, 2, 1])),
axis=0, keep_dims=True
)
self.routing_logits.assign_add(delta_routing_logits)
interest_capsules = tf.reshape(interest_capsules, [-1, self.k_max, self.out_units]) # 输出兴趣胶囊 [B,k_max,out_units]
return interest_capsules
def compute_output_shape(self, input_shape):
return (None, self.k_max, self.out_units)
def get_config(self, ):
config = {'input_units': self.input_units, 'out_units': self.out_units, 'max_len': self.max_len,
'k_max': self.k_max, 'iteration_times': self.iteration_times, "init_std": self.init_std}
base_config = super(CapsuleLayer, self).get_config()
return dict(list(base_config.items()) + list(config.items()))