推荐算法(六)—— xDeepFM 通俗理解及代码实战

1 介绍

本文为推荐系统专栏的第六篇文章,内容围绕 xDeepFM 的原理及代码展开。

xDeepFM 是由中科大、北大、微软联合发表在 KDD’18 上的文章,颇为经典。
在这里插入图片描述
论文传送门:xDeepFM: Combining Explicit and Implicit Feature Interactionsfor Recommender Systems

代码传送门:xDeepFM

xDeepFM 是 Wide & Deep 的改进版,在此基础上添加了 CIN 层显式的构造有限阶特征组合。xDeepFM 虽然名字跟 DeepFM 类似,但是两者相关性不大,DCN 才是它的近亲。

Tips:阅读此文之前,建议先了解 DCN 的原理。

2 原理

在这里插入图片描述
上图为 xDeepFM 的总体结构,有三个分支:Linear(稀疏的01向量作为输入)、DNN(经过embedding的稠密向量作为输入)、CIN(压缩感知层)。

xDeepFM 如果去掉 CIN 分支,就等同于 Wide & Deep, Wide & Deep 原理不做介绍,本文主要围绕 CIN 层进行展开。

CIN:Compressed Interaction Network

介绍 CIN 之前,先简单介绍一下它的近亲 DCN 中的 cross layer。

在这里插入图片描述在这里插入图片描述
cross layer 是将所有 field 的向量横向 concat,作为一个输入向量 x 0 x_{0} x0,然后每层特征都会与 x 0 x_{0} x0 做内积,得到更高一阶的特征相互,很明显,是在元素级(bit-wise)的特征交互。

这样的缺陷也很明显,模型意识不到域的概念了,同属一个 field 的元素,应该被同等对待,与其他特征交互时应该使用同一权重,所以这是 bit-wise 方式的缺陷所在。

CIN 引入 vector-wise 的方式,特征交互时同属一个 field 的元素整体考虑 ,避免了这种缺陷。

下面进入正题…

Tips:为了容易理解,不会上来就放大量公式。
在这里插入图片描述
上图为 CIN 层的计算方式,具体含义如下:

  1. CIN 的输入是所有 field 列向量横向拼接得到的矩阵 X 0 ∈ R m ∗ D \boldsymbol{X}^{0} \in \mathbb{R}^{m * D} X0RmD (m 个维度为 D 的 field 向量)。并且后续的每层特征组合都是形状都是 ( H i , D ) (H_{i} , D) (Hi,D),每层的特征个数 H i H_{i} Hi 不再相同,维度 D D D 始终保持不变;

  2. 每次第 i 层的特征矩阵 ( H i , D ) (H_{i} , D) (Hi,D) 都会与原始输入 ( m , D ) (m, D) (m,D) 做交互,得到第 i+1 层的特征矩阵 ( H i + 1 , D ) (H_{i+1} , D) (Hi+1,D)具体交互方式在下文介绍

  3. 因为每一层只包含某一阶的特征组合,所以每一层的特征矩阵都会被使用来对最终结果作预测,将每层特征矩阵的 H i H_{i} Hi 个特征在 D 维度做 sum pooling,总共会得到 H 1 + H 2 + . . . + H k H_{1} + H_{2} + ... + H_{k} H1+H2+...+Hk 个标量,concat 拼接在一起得到的一维向量作为CIN层的输出,后续与 linear 和 dnn 部分的输出拼接做最终的预测。

这就是 CIN 一层一层的特征交互方式,以及做最终结果预测的过程。


下面详细介绍两个特征矩阵交互的计算过程:

矩阵 ( H k , D ) (H_{k} , D) (Hk,D) 与原始输入 ( m , D ) (m, D) (m,D) 做交互,怎样得到形状为 ( H k + 1 , D ) (H_{k+1} , D) (Hk+1,D) 的特征矩阵呢?

论文中的图晦涩难懂,暂时先不放图。先简述一下计算过程,然后再看图就比较容易了。

计算过程:

  1. 矩阵 ( H k , D ) (H_{k} , D) (Hk,D) ( m , D ) (m, D) (m,D) 分别是含有 H k H_{k} Hk m m m D D D 维特征的矩阵;
  2. 两个矩阵中的特征两两做对应元素乘积,会得到 H k ∗ m H_{k} * m Hkm D D D 维特征,这样就实现了特征交互(图中画出的框框是内积计算方式,不好理解);
  3. 这些交互特征不是直接拼接成形状为 ( H k ∗ m , D ) (H_{k}*m, D) (Hkm,D) 的特征矩阵,而是拼接成三维矩阵 ( H k , m , D ) (H_{k},m, D) (Hk,m,D)

在这里插入图片描述
如上图所示,两两交互后得到的是一个三维矩阵,那么该矩阵如何转换成下一层的矩阵 ( H i + 1 , D ) (H_{i+1} , D) (Hi+1,D) 呢?

  1. 使用一个形状为 ( H k , m ) (H_{k} , m) (Hk,m) 权重矩阵 W W W,分别与三维矩阵中 D 个同形状的矩阵做元素乘积,每次乘积之后求和得到一个标量,然后将每个标量 concat 到一起,最终得到一个维度为 D 的向量 F e a t u r e Feature Feature m a p map map 1 1 1
  2. 使用 H k + 1 H_{k+1} Hk+1 个不同的权重矩阵 W W W,即可得到 H k + 1 H_{k+1} Hk+1 不同的 D 维向量,拼接在一起即为下一层的特征矩阵 ( H k + 1 , D ) (H_{k+1} , D) (Hk+1,D)

在这里插入图片描述
了解计算过程再看图就容易多了,下面简单看一下计算公式。

在这里插入图片描述

  1. 两个遍历符号用于遍历两个特征矩阵的特征个数 H k − 1 H_{k-1} Hk1 m m m
  2. X i , ∗ k − 1 \mathrm{X}_{i, *}^{k-1} Xi,k1 表示第 k-1 个特征矩阵 ( H k − 1 , m ) (H_{k-1} , m) (Hk1,m) 中的第 i 个特征,* 表示特征的每个取值,括号内即为两个特征的元素积;
  3. 矩阵 W i j k , h \mathbf{W}_{i j}^{k,h} Wijk,h 是形状为 ( H k − 1 , m ) (H_{k-1} , m) (Hk1,m) 的矩阵, 经过层层映射会将三维矩阵压缩至一维,得到下一层特征矩阵中的一个特征 X h , ∗ k \mathrm{X}_{h, *}^{k} Xh,k
  4. 使用 H k H_{k} Hk 个不同的 W 即可得到下一层特征矩阵 ( H k , D ) (H_{k} , D) (Hk,D)

以上就是 CIN 的具体计算过程,其实并不算难,只是比较繁琐。

3 总结

优点:

使用 vector-wise 的方式,通过特征的元素积来进行特征交互,将一个特征域的元素整体考虑,比 bit-wise 方式更 make sence 一些;

缺点:

CIN 层的复杂度通常比较大,它并不具有像 DCN 的 cross layer 那样线性复杂度,它的复杂度通常是平方级的,因为需要计算两个特征矩阵中特征的两两交互,这就给模型上线带来压力。

为什么 CIN 叫压缩感知层?

因为每次矩阵 W 都会将特征两两交互得到的三维矩阵压缩成一维,所以叫做压缩感知。

为什么模型名称中有 DeepFM?

因为特征两两组合得到的三维矩阵 ( H k , m , D ) (H_{k},m, D) (Hk,m,D),经过矩阵 W 压缩得到一维向量的过程,就相当于将 H k ∗ m H_{k}*m Hkm 个向量进行了加权累加,得到一个 D 维向量,如果权重为1的话,那么此时就相当于 FM 中二阶交叉项的计算,所以题目中包含了 DeepFM。

4 实验

下表显示了 xDeepFM 在各个数据集上都具有比较 SOTA 的效果。
在这里插入图片描述文中没有对复杂度的对比实验,说明了 xDeepFM 的复杂度是其比较大的一个短板。

5 代码实践

Layer 搭建:

import tensorflow as tf
from tensorflow.keras.layers import Layer, Dense, Dropout

class Linear(Layer):
    def __init__(self):
        super(Linear, self).__init__()
        self.out_layer = Dense(1, activation=None)

    def call(self, inputs, **kwargs):
        output = self.out_layer(inputs)
        return output

class Dense_layer(Layer):
    def __init__(self, hidden_units, out_dim=1, activation='relu', dropout=0.0):
        super(Dense_layer, self).__init__()
        self.hidden_layers = [Dense(i, activation=activation) for i in hidden_units]
        self.out_layer = Dense(out_dim, activation=None)
        self.dropout = Dropout(dropout)

    def call(self, inputs, **kwargs):
        # inputs: [None, n*k]
        x = inputs
        for layer in self.hidden_layers:
            x = layer(x)
        x = self.dropout(x)
        output = self.out_layer(x)
        return output

class CIN(Layer):
    def __init__(self, cin_size):
        super(CIN, self).__init__()
        self.cin_size = cin_size  # 每层的矩阵个数

    def build(self, input_shape):
        # input_shape: [None, n, k]
        self.field_num = [input_shape[1]] + self.cin_size # 每层的矩阵个数(包括第0层)

        self.cin_W = [self.add_weight(
                         name='w'+str(i),
                         shape=(1, self.field_num[0]*self.field_num[i], self.field_num[i+1]),
                         initializer=tf.initializers.glorot_uniform(),
                         regularizer=tf.keras.regularizers.l1_l2(1e-5),
                         trainable=True)
                      for i in range(len(self.field_num)-1)]

    def call(self, inputs, **kwargs):
        # inputs: [None, n, k]
        k = inputs.shape[-1]
        res_list = [inputs]
        X0 = tf.split(inputs, k, axis=-1)           # 最后维切成k份,list: k * [None, field_num[0], 1]
        for i, size in enumerate(self.field_num[1:]):
            Xi = tf.split(res_list[-1], k, axis=-1) # list: k * [None, field_num[i], 1]
            x = tf.matmul(X0, Xi, transpose_b=True) # list: k * [None, field_num[0], field_num[i]]
            x = tf.reshape(x, shape=[k, -1, self.field_num[0]*self.field_num[i]])
                                                    # [k, None, field_num[0]*field_num[i]]
            x = tf.transpose(x, [1, 0, 2])          # [None, k, field_num[0]*field_num[i]]
            x = tf.nn.conv1d(input=x, filters=self.cin_W[i], stride=1, padding='VALID')
                                                    # (None, k, field_num[i+1])
            x = tf.transpose(x, [0, 2, 1])          # (None, field_num[i+1], k)
            res_list.append(x)

        res_list = res_list[1:]   # 去掉 X0
        res = tf.concat(res_list, axis=1)     # (None, field_num[1]+...+field_num[n], k)
        output = tf.reduce_sum(res, axis=-1)  # (None, field_num[1]+...+field_num[n])
        return output

Model 搭建:

from layer import Linear, Dense_layer, CIN

import pandas as pd
import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Dense, Embedding

class xDeepFM(Model):
    def __init__(self, feature_columns, cin_size, hidden_units, out_dim=1, activation='relu', dropout=0.0):
        super(xDeepFM, self).__init__()
        self.dense_feature_columns, self.sparse_feature_columns = feature_columns
        self.embed_layers = [Embedding(feat['feat_onehot_dim'], feat['embed_dim'])
                                    for feat in self.sparse_feature_columns]
        self.linear = Linear()
        self.dense_layer = Dense_layer(hidden_units, out_dim, activation, dropout)
        self.cin_layer = CIN(cin_size)
        self.out_layer = Dense(1, activation=None)

    def call(self, inputs, training=None, mask=None):
        dense_inputs, sparse_inputs = inputs[:, :13], inputs[:, 13:]

        # linear
        linear_out = self.linear(inputs)

        emb = [self.embed_layers[i](sparse_inputs[:, i]) for i in range(sparse_inputs.shape[1])] # [n, None, k]
        emb = tf.transpose(tf.convert_to_tensor(emb), [1, 0, 2]) # [None, n, k]

        # CIN
        cin_out = self.cin_layer(emb)

        # dense
        emb = tf.reshape(emb, shape=(-1, emb.shape[1]*emb.shape[2]))
        emb = tf.concat([dense_inputs, emb], axis=1)
        dense_out = self.dense_layer(emb)

        output = self.out_layer(linear_out + cin_out + dense_out)
        return tf.nn.sigmoid(output)

完整可运行的代码可在文末 Github 仓库中查看。

写在最后

下一篇预告:推荐算法(七)——FM 与 DNN 的另一种结合产物 FNN


各种推荐算法的模型复现,可参照以下 Github 仓库:

Recommend-System-TF2.0

希望看完此文的你,能够有所收获!

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值