特征建模之FiBiNet

FiBiNet: Combine Feature importance and Bilinear feature Interaction for Click-Through Rate Prediction https://arxiv.org/abs/1905.0943

一、特征建模的重要性

推荐领域的深度CTR模型中的参数主要由两部分构成:特征Embedding参数和MLP层参数,假设模型中有1亿个特征,Embedding的维度是10维,MLP包括三层FC,神经元个数是1024/512/256,那么我们可以算出两部分的参数量分别是:

  • 特征Embedding参数: 1亿*10 = 10亿
  • MLP的参数: 1024*512*256 = 1.3亿

可以看出特征Embeding参数占比达到90%+,巨大的特征参数量是导致CTR模型容易过拟合的主要原因。同时值得考虑的是,这10亿个参数都是有用的么?不同的特征Embedding的重要程度一样么?如果答案是否定的,那应该如何入筛选对模型更有益的特征,如何度量每个特征的重要程度?

微博提出的FiBiNet系列模型就给出了解决上述问题的一种可行方案。

二、FiBiNet理论

为了解决业界现有模型存在的两个问题(一是没有度量CTR模型中特征的重要性,二是简单的使用inner product 或者 Hadamard product 无法很好的进行特征交叉),FiBiNet中设计了两个子网络,分别是建模特征重要性的SENet模块和建模特征交叉的Bilinear-Interaction Layer。

(一)、SENet

SENet: Squeeze-and-Excitation Networks ,https://arxiv.org/abs/1709.01507

SENet最初是在CV领域被提出,因其轻量化且具备显著的有效性被广泛应用。该模块重点关注了不同channel之间的关系,学习不同channel对最终预测的重要程度,对于CNN网络过程中的特征图U,SENet模块主要进行以下三步操作:

  1. Squeeze: 输入通道数为C的U,对每个cnannel 进行max pooling,选取最大值这一统计特征表征一个channel, 最终输出1 X 1 X C的张量。
  2. Excitation: 类似门控机制,使用两个FC层学习通道级别的权重,每个weight的含义可以理解维对应通道在最终预测时的重要度。
  3. Re-scale: 将求解的权重与最初的输入U相乘,得到加权后的输出。

借鉴该模块的设计思想,在推荐任务中考虑引入一个类似的门控系统,能够实现"系统预测不重要的特征的权重趋近于0,预测重要特征的权重越大越好",通过这种方式对特征重要性建模,使模型可以忽略掉低频特征、不重要特征的负面影响,对高频特征进行更好的建模。

下图是FiBiNet中的SENet模块,模块输入是所有的特征embedding [e1 e2 ... ef],输出是乘以特征权重后的weighted embeding [v1 v2 ... vf],具体过程为:

  1. Squeeze: 这一步的目的是得到每个特征embedding的统计值特征。采用mean pooling的方式选取每个特征的统计值特征zi,假设输入有f个slot,那么这一步输出shape为1 X f的张量。
  2. Excitation:这一步的目的是学习每个特征的权重。通过双层MLP网络对第一步输出的张量进行变换,将其映射到最终的权重特征空间,为了避免过拟合,往往会通过超参数r将第一个FC层设计为窄网络,将第二个FC设计为宽网络。同时采用ReLU激活函数打压不重要的特征,最终输出权重张量 [w1 w2 ... wf] 。 

      A=F_{ex}(z)=\sigma_{2}(W_{2}(\sigma_{1}(W_{1}))) \newline ~~~~~~~~~~~~~~~W_{1}\in R^{f\times \frac{f}{r}} \newline ~~~~~~~~~~~~~~~W_{2}\in R^{\frac{f}{r} \times f}

     3. Re-weight: 这一步的目的是将求解的权重张量与原始的特征embedding进行field-wise的乘法,实现对低频不重要特征的打压,对高频重要特征的boost。

根据上述原理可以看出,推荐系统中SENet的本质是对离散特征做field-wise加权

(二)、Bilinear-Interaction Layer

为了更好的建模特征交叉,FiBiNet在任意两个特征进行交互时引入一个新的参数矩阵W,通过这个参数矩阵更精细地表征交互过程。具体来说,先计算特征vi和W的内积得到中间结果z,然后计算z与vj的哈达玛乘积(逐元素相乘)得到双线性交叉的结果。

假设有f个slot/field, 特征embedding的维度是k, 实现Bilinear-Interaction Layer时有三种不同形式。

 1.Field-All Type

  • P_{ij}=V_i\cdot W\odot V_j
  • 任意两个field进行交互时,共享同一个参数矩阵W
  • 参数量为k x k

  2.Field-Each Type

  • P_{ij}=V_i \cdot W_i \odot Vj
  • 进行inner product时,每个field对应一个参数矩阵Wi
  • 参数量为 f x k x k

 3. Field-Interaction Type

  • P_{ij}=V_i \cdot W_{ij} \odot V_j
  • 任意两个field进行交互时,都有一个自己的参数矩阵Wi

(三)、FiBiNet流程总结

借用王树森老师的图总结一下FiBiNet的设计:

1. RecSys中,首先将离散特征embed化,每个离散特征对应一个K维的embedding向量,得到embeding矩阵M;

2. 在原有DNN结构基础上,FiBiNet新增了红框中的子网络结构:

  a. 直接把M所有的特征Embeding拼接,产出张量A;

  b. 对M中所有特征进行Bilinear运算,产出张量B;

  c. 先将M中所有特征进行SENet运算,然后再通过Bilinear运算,产出张量C。

3. 将A、B、C与连续特征拼接到一起,作为上层网络的输入。

三、FiBiNet实践

基于paddle框架实现了FiBiNet,这里主要给出SENet和Bilinear interaction Layer两个模块的的实现代码。

(一)、SENet

def _senet(self, all_emb, reduction_ratio=3):
    """
    Func:
        implementation of senet
    Args:
        all_emb: a lod tensor, shape is (-1 slot_nums, embed_dim)
        reduction_ratio: integer, the ratio of fc
    Output:
        a lod tensor, shape is (-1 slot_nums, embed_dim)
    """
    slot_nums = all_emb.shape[1] # 获取特征个数
    fc_unit = max(1, slot_nums // reduction_ratio) # 计算FC层的神经元个数
    ################## squeeze ##################
    squeeze_emb = layers.reduce_mean(all_emb, dim=-1) # (-1, slot_nums, 1)
    falten_emb = layers.flatten(squeeze_emb) # (-1, slot_nums)   
    print('feature nums is ' + str(slot_nums))
    print('falten_emb shape is:' + str(falten_emb.shape))
    ################## excitation ##################
    weight = layers.fc(input=falten_emb, size=fc_unit, act='relu',
            param_attr =
                fluid.ParamAttr(learning_rate=1.0,
                initializer=fluid.initializer.NormalInitializer(loc=0.0, scale=self._init_range / (slot_nums ** 0.5)),
                    name="se_w_1"),
            bias_attr =
                fluid.ParamAttr(learning_rate=1.0,
                initializer=fluid.initializer.NormalInitializer(loc=0.0, scale=self._init_range / (slot_nums ** 0.5)),
                name="se_b_1"))  # (-1, fc_unit)
    weight = layers.fc(input=weight, size=slot_nums, act='relu',
            param_attr =
                fluid.ParamAttr(learning_rate=1.0,
                initializer=fluid.initializer.NormalInitializer(loc=0.0, scale=self._init_range / (fc_unit ** 0.5)),
                    name="se_w_2"),
            bias_attr =
                fluid.ParamAttr(learning_rate=1.0,
                initializer=fluid.initializer.NormalInitializer(loc=0.0, scale=self._init_range / (fc_unit ** 0.5)),
                name="se_b_2")) # (-1, slot_nums)
    ################## re_weight ##################
    out = layers.elementwise_mul(all_emb, layers.unsqueeze(weight, axes=[2])) # (-1, slot_nums, embed_dim) * (-1, slot_nums, 1)
    print('senet out shape is: ' + str(out.shape)) # (-1 slot_nums embed_dim)
    return out

(二)、Bilinear Interaction Layer

def _bilinear_interaction_layer(self, all_emb, mode):
    """
    Func:
        implementation of bilinear interaction layer
    Args:
        all_emb: an embedding which has concated all embed , shape is (-1, slot_nums, embed_dim)

    """
    slot_nums = all_emb.shape[1]
    embed_dim = all_emb.shape[2]
    emb_list = layers.split(all_emb, num_or_sections=slot_nums, dim=1)
    emb_list = [layers.squeeze(emb, axes=[1]) for emb in emb_list] # list, ele shape is (-1 embed_dim)
    if mode == "field_all":
        # 构建一个共享的参数矩阵
        W = layers.create_parameter(
            shape=[embed_dim, embed_dim], dtype='float32')
        # 先计算点积
        vidots = [layers.matmul(emb, W) for emb in emb_list]  # (-1 embed_dim)
        # 计算Hadamard Product
        p_ij = [
            fluid.layers.elementwise_mul(vidots[i], emb_list[j])
                for i, j in itertools.combinations(range(slot_nums), 2)
        ] # (-1 embed_dim)
        output = layers.concat(p_ij, axis=-1) # (-1 embed_dim * slot_nums)
        return output
        
    elif mode == "field_each":
        # 构建参数矩阵,数量与slot_nums保持一致
        W_list = [
            layers.create_parameter(shape=[embed_dim, embed_dim], dtype='float32') for _ in range(slot_nums)
        ]
        # 计算点积
        vidots = [layers.matmul(emb_list[1], W_list[i]) for i in range(slot_nums)]
        # 计算 Hadamard product
        p_ij = [layers.elementwise_mul(vidots[i], emb_list[j])
                for i, j in itertools.combinations(range(slot_nums), 2)] # (-1 embed_dim)
        output = layers.concat(p_ij, axis=-1) # (-1 embed_dim * slot_nums)
        return output

    elif mode == "field_interaction":
        W_list = [layers.create_parameter(shape=[embed_dim, embed_dim], dtype='float32') 
                    for _, _ in itertools.combinations(range(slot_nums), 2)]
        p_ij = [layers.elementwise_mul(layers.matmul(v[0], w), v[1])
            for v, w in zip(itertools.combinations(emb_list, 2), self.W_list)
        ]
    else:
        raise NotImplementedError

三、FiBiNet存在的问题

       原文中把所有的特征embedding都进行双线性特征交叉,这一部分会带来巨大的参数量,也导致线上推理时长和内存存储的增加,因此在实现时,可以根据具体业务,选择出必要的特征进行交叉。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值