推荐系统之NFM原理与实现学习

发布时间:2024-08-12 08:01

目录

  • 特征交叉
  • NFM模型简介
    • 目标函数
    • 模型结构
  • 代码学习

上一次学习了DeepFM模型,DeepFM是在经典的Wide&Deep模型的基础上将wide部分替换成了FM模型,然后将FM模型的输出结果与deep部分的输出结果并行连接,最后结合两边的输出得到最终的预测结果。而这次学习的NFM模型与DeepFM模型的不同之处在于,NFM是将FM模型和Deep部分串行连接在了一起,FM部分的输出结果经过一个特征交叉池化层后就直接作为下一层deep模型的输入。

特征交叉

在开始这次的学习之前,先简单了解婴喜爱特征交叉。所谓的特征交叉,我的理解就是将两个或者多个特征使用相乘或者平方的方式生成新的特征,新产生的特征可能具有比原始特征更好的效果。
推荐系统之NFM原理与实现学习_第1张图片
比如我们要对上图的蓝色和橙色的点进行分类,如果我们只使用x1和x2这两个特征的话,得到的永远都是一条直线,是无法将这两类点分开的。因此我们需要重新组合特征。
推荐系统之NFM原理与实现学习_第2张图片
可以看到,增加了x1x2这个组合特征后有所变化,但是仍然无法完成分类,那么我们继续组合。
推荐系统之NFM原理与实现学习_第3张图片
这次我们使用了x12和x22这两个特征组合,可以看到,分类器终于实现了比较精确的分类。
从这个小实验可以看出,对特征进行交叉组合并且选出合适的特征组合对于模型有着十分重要的影响。

NFM模型简介

目标函数

从上面的小案例可以看出,有时候特征组合会对模型效果有着很大的改善。但是传统的FM模型只能进行特征的二阶组合,无法组合高阶的特征,而DNN具有组合高阶特征的能力和非线性能力,因此就考虑将FM模型与DNN结合起来,于是就产生了我们今天学习的模型NFM。模型的目标函数如下:
在这里插入图片描述
目标函数前半部分就是典型的FM模型部分,而后面圈起来的地方,就是后面的DNN部分要实现的。具体来说,就是对稀疏特征进行了embedding,然后再经过一个特征交叉池化层,然后是一个隐藏层,最后经过预测层得到最终结果。

模型结构

模型具体结构如下图所示:
推荐系统之NFM原理与实现学习_第4张图片
最下面的embedding是常规做法,这个模型比较新颖的地方在于特征交叉池化层,这一层是将FM模型和DNN连接起来的关键。可以看一下特征交叉池化层的操作:
在这里插入图片描述
需要注意的是,这里的圈乘得到的不是一个数,而是一个k维的矩阵,与点乘的操作不一样。后面为了代码实现的方便,可以将这个式子进行转换:
在这里插入图片描述
也就是平方的和减去和的平方。
再后面的隐藏层和预测层就不用多介绍了,都是平常的做法。

代码学习

代码实现如下:

def NFM(linear_feature_columns, dnn_feature_columns):
    """
    搭建NFM模型,上面已经把所有组块都写好了,这里拼起来就好
    :param linear_feature_columns: A list. 里面的每个元素是namedtuple(元组的一种扩展类型,同时支持序号和属性名访问组件)类型,表示的是linear数据的特征封装版
    :param dnn_feature_columns: A list. 里面的每个元素是namedtuple(元组的一种扩展类型,同时支持序号和属性名访问组件)类型,表示的是DNN数据的特征封装版
    """
    # 构建输入层,即所有特征对应的Input()层, 这里使用字典的形式返回, 方便后续构建模型
    # 构建模型的输入层,模型的输入层不能是字典的形式,应该将字典的形式转换成列表的形式
    # 注意:这里实际的输入与Input()层的对应,是通过模型输入时候的字典数据的key与对应name的Input层
    dense_input_dict, sparse_input_dict = build_input_layers(linear_feature_columns+dnn_feature_columns)
    input_layers = list(dense_input_dict.values()) + list(sparse_input_dict.values())
    
    # 线性部分的计算 w1x1 + w2x2 + ..wnxn + b部分,dense特征和sparse两部分的计算结果组成,具体看上面细节
    linear_logits = get_linear_logits(dense_input_dict, sparse_input_dict, linear_feature_columns)
    
    # DNN部分的计算
    # 首先,在这里构建DNN部分的embedding层,之所以写在这里,是为了灵活的迁移到其他网络上,这里用字典的形式返回
    # embedding层用于构建FM交叉部分以及DNN的输入部分
    embedding_layers = build_embedding_layers(dnn_feature_columns, sparse_input_dict, is_linear=False)
    
    # 过特征交叉池化层
    pooling_output = get_bi_interaction_pooling_output(sparse_input_dict, dnn_feature_columns, embedding_layers)
    
    # 加个BatchNormalization
    pooling_output = BatchNormalization()(pooling_output)
    
    # dnn部分的计算
    dnn_logits = get_dnn_logits(pooling_output)
    
    # 线性部分和dnn部分的结果相加,最后再过个sigmoid
    output_logits = Add()([linear_logits, dnn_logits])
    output_layers = Activation("sigmoid")(output_logits)
    
    model = Model(inputs=input_layers, outputs=output_layers)
    
    return model

线性部分的输出结果与前面学习的模型一样,就不过多介绍。主要看一下特征交叉池化层做了些什么:

class BiInteractionPooling(Layer):
    def __init__(self):
        super(BiInteractionPooling, self).__init__()

    def call(self, inputs):
        # 优化后的公式为: 0.5 * (和的平方-平方的和)  =>> B x k
        concated_embeds_value = inputs # B x n x k
        #tf.square求平方,
        square_of_sum = tf.square(tf.reduce_sum(concated_embeds_value, axis=1, keepdims=False)) # B x k
        sum_of_square = tf.reduce_sum(concated_embeds_value * concated_embeds_value, axis=1, keepdims=False) # B x k
        cross_term = 0.5 * (square_of_sum - sum_of_square) # B x k

        return cross_term

    def compute_output_shape(self, input_shape):
        return (None, input_shape[2])


def get_bi_interaction_pooling_output(sparse_input_dict, sparse_feature_columns, dnn_embedding_layers):
    # 只考虑sparse的二阶交叉,将所有的embedding拼接到一起
    # 这里在实际运行的时候,其实只会将那些非零元素对应的embedding拼接到一起
    # 并且将非零元素对应的embedding拼接到一起本质上相当于已经乘了x, 因为x中的值是1(公式中的x)
    sparse_kd_embed = []
    for fc in sparse_feature_columns:
        feat_input = sparse_input_dict[fc.name]
        _embed = dnn_embedding_layers[fc.name](feat_input) # B x 1 x k
        sparse_kd_embed.append(_embed)

    # 将所有sparse的embedding拼接起来,得到 (n, k)的矩阵,其中n为特征数,k为embedding大小
    concat_sparse_kd_embed = Concatenate(axis=1)(sparse_kd_embed) # B x n x k
    
    pooling_out = BiInteractionPooling()(concat_sparse_kd_embed)

    return pooling_out

get_bi_interaction_pooling_output函数前面是先生成一个有k维embedding向量组成的列表,然后将列表连接起来,得到一个nk维的矩阵。将这个nk维的矩阵代入函数BiInteractionPooling就得到了最终的结果。在BiInteractionPooling函数中就是进行了我们前面的那个使用平方的和减去和的平方的公式。
最后将线性部分和FM加DNN得到的结果相加,再通过激活函数就得到最终的预测结果。

参考资料:
1、https://github.com/datawhalechina/team-learning-rs/blob/master/DeepRecommendationModel/NFM.md
2、https://developers.google.cn/machine-learning/crash-course/feature-crosses/playground-exercises

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号