xDeepFM 网络介绍与源码浅析

xDeepFM 网络介绍与源码浅析

前言 (与主题无关, 可以忽略)

哈哈哈, 十月第一篇博客, 希望这个季度能更奋进一些~~~ 不想当咸鱼了… 🤣🤣🤣

广而告之

可以在微信中搜索 “珍妮的算法之路” 或者 “world4458” 关注我的微信公众号;另外可以看看知乎专栏 PoorMemory-机器学习, 以后文章也会发在知乎专栏中;

xDeepFM

文章信息

核心观点

  • xDeepFM (eXtreme Deep Factorization Machine) 的目的是为了处理特征交叉的问题, 可以视为是对 DCN 进行改进. 文章对 DCN 的特征交叉公式进行了观察和推导, 发现 Cross 在做特征交叉其形式受限于一个特殊的形式, 其交叉特征表示为一个 scalar α \alpha α 和输入特征 x 0 \bm{x}_0 x0 的乘积. 当然, 这里的 α \alpha α 是有 x 0 \bm{x}_0 x0 有关的, 此外注意到 Cross 层做特征交叉是在 bit-wise level 上进行的;
  • xDeepFM 的改进是: 首先特征交叉是在 vector-wise 的层级上做的, 并且和 DCN 的 Cross 层一样, 本文介绍的 CIN (Compressed Interaction Network) 层对交叉特征进行显式地学习,这样就可以知晓特征的阶数;
  • CIN 进行特征交叉的具体过程是: 对于第 k k k 层 CIN, 其使用第 k − 1 k - 1 k1 层的 H k − 1 H_{k-1} Hk1 个特征和原始的输入 X 0 X^0 X0 中的 m m m 个特征进行特征交叉, 生成 m × H k − 1 m\times H_{k-1} m×Hk1 个交叉特征, 然后使用对这些特征进行加权求和 (每个交叉特征对应一个权重参数, 因此权重参数的个数为 m × H k − 1 m\times H_{k-1} m×Hk1), 这样就得到了一个输出的交叉特征; 由于第 k k k 层使用 H k H_k Hk 个权重矩阵, 那么最后就可以得到 H k H_k Hk 个输出的交叉特征;
  • xDeepFM 的输入到输出层的结果包含三部分, 分别对应着线性层, CIN 层以及 Deep 层的输出结果,其中线性层包含着低阶特征, CIN 层包含显式学习的高阶特征, 而 Deep 层包含隐式学习的高阶特征.

核心观点介绍

首先看 xDeepFM 的网络结构, 如下图所示:

可以看到, 其结构类似于 Wide & Deep 与 DCN 结构, 不过其主要包含三个部分, 分别为线性层, CIN 层以及 Deep 层, 设 a \bm{a} a 为 raw features, x d n n k \bm{x}_{dnn}^k xdnnk 为 DNN 的输出结果, p + \bm{p}^{+} p+ 为 CIN 层的输出结果, 那么 xDeepFM 的输出为:

y ^ = σ ( w linear T a + w d n n T x d n n k + w c i n T p + + b ) \hat{y}=\sigma\left(\mathbf{w}_{\text {linear}}^{T} \mathbf{a}+\mathbf{w}_{d n n}^{T} \mathbf{x}_{d n n}^{k}+\mathbf{w}_{c i n}^{T} \mathbf{p}^{+}+b\right) y^=σ(wlinearTa+wdnnTxdnnk+wcinTp++b)

可以认为 xDeepFM 是对 DCN 的改进, 其介绍了 CIN 层来替换 DCN 中的 Cross 层, 用于显式学习交叉特征.

高阶特征交叉 (High-order Interactions)

下面在介绍 CIN 层的原理之前, 先说明一下 Cross 层存在的问题, 这个是 xDeepFM 这篇 paper 讨论的一个重点. 为了方便讨论, 先引入一些必要的符号. 设原始的高维稀疏特征经 Embedding Layer 处理后, 映射为低维的稠密向量:

e = [ e 1 , e 2 , … , e m ] \mathbf{e}=\left[\mathbf{e}_{1}, \mathbf{e}_{2}, \ldots, \mathbf{e}_{m}\right] e=[e1,e2,,em]

其中 m m m 表示 Field 的个数, e i ∈ R D \mathbf{e}_{i}\in\mathbb{R}^D eiRD 表示第 i i i 个 Field 所对应的 embedding. 因此对于一个样本来说, 它对应的 embedding 大小为 m × D m\times D m×D.

在讨论 DCN 中的 Cross 层之前, 先介绍两个概念:

  • 隐式高阶特征交叉 (Implicit High-order Interactions): DNN 学习特征交叉的方式是隐式的, 因为DNN 最后表示的函数是任意的, 而且目前没有理论论证了 DNN 能学习的特征交叉的最大阶数.
    (此处再插入对 bit-wisevector-wise 的讨论: 此外, DNN 学习特征交叉是在 bit-wise level, 而 FM 学习特征交叉是在 vector-wise level. 所谓 bit, 在文章的 Introduction 这一节中有具体的描述, 即用来表示一个向量中的某个元素, 比如向量 v = [ a , b , c ] \bm{v} = \left[a, b, c\right] v=[a,b,c] 中, a a a 就可以称为 bit. DNN 对高阶特征交叉建模是以 bit-wise 的形式进行的, 这意味着在同一个 field embedding 中的各个元素之间也会相互影响, 而 vertor-wise 则不会出现这种情况, 其是对整个向量进行处理.)
  • 显式高阶特征交叉 (Explicit High-order Interactions): 即学习到的交叉特征其阶数是知晓的, 比如 FM 学习到的是二阶特征交叉. DCN 中 Cross 层的目的也是希望显式地对高阶特征交叉进行建模.

DCN 中 Cross 层通过下式对高阶特征交叉进行建模:

x k = x 0 x k − 1 T w k + b k + x k − 1 \mathbf{x}_{k}=\mathbf{x}_{0} \mathbf{x}_{k-1}^{T} \mathbf{w}_{k}+\mathbf{b}_{k}+\mathbf{x}_{k-1} xk=x0xk1Twk+bk+xk1

其中 w k , b k , x k ∈ R m D \mathbf{w}_{k}, \mathbf{b}_{k}, \mathbf{x}_{k} \in \mathbb{R}^{m D} wk,bk,xkRmD 分别为第 k k k 层的权重, bias 以及输出. 作者认为 Cross 层学习出来的是一类特殊的高阶特征交叉, 其表示为一个 scalar α \alpha α 和输入特征 x 0 \mathbf{x}_{0} x0 的乘积. 当然, 这里的 α \alpha α 是有 x 0 \mathbf{x}_{0} x0 有关的, 此外注意到 Cross 层做特征交叉是在 bit-wise level 上进行的; 具体推导如下:

k = 1 k=1 k=1 时, 可以得到:

x 1 = x 0 ( x 0 T w 1 ) + x 0 = x 0 ( x 0 T w 1 + 1 ) = α 1 x 0 \begin{aligned} \mathbf{x}_{1} &=\mathbf{x}_{0}\left(\mathbf{x}_{0}^{T} \mathbf{w}_{1}\right)+\mathbf{x}_{0} \\ &=\mathbf{x}_{0}\left(\mathbf{x}_{0}^{T} \mathbf{w}_{1}+1\right) \\ &=\alpha^{1} \mathbf{x}_{0} \end{aligned} x1=x0(x0Tw1)+x0=x0(x0Tw1+1)=α1x0

其中 α 1 = x 0 T w 1 + 1 \alpha^{1} = \mathbf{x}_{0}^{T} \mathbf{w}_{1}+1 α1=x0Tw1+1 为一个 scalar, 因此 x 1 \mathbf{x}_{1} x1 可以表示为一个 scalar 和 x 0 \mathbf{x}_{0} x0 的乘积. 使用数学归纳法, 假设这一点对 k = i k = i k=i 时仍然成立, 那么当 k = i + 1 k = i + 1 k=i+1 时, 有:

x i + 1 = x 0 x i T w i + 1 + x i = x 0 ( ( α i x 0 ) T w i + 1 ) + α i x 0 = α i + 1 x 0 \begin{aligned} \mathbf{x}_{i+1} &=\mathbf{x}_{0} \mathbf{x}_{i}^{T} \mathbf{w}_{i+1}+\mathbf{x}_{i} \\ &=\mathbf{x}_{0}\left(\left(\alpha^{i} \mathbf{x}_{0}\right)^{T} \mathbf{w}_{i+1}\right)+\alpha^{i} \mathbf{x}_{0} \\ &=\alpha^{i+1} \mathbf{x}_{0} \end{aligned} xi+1=x0xiTwi+1+xi=x0((αix0)Twi+1)+αix0=αi+1x0

其中 α i + 1 = α i ( x 0 T w i + 1 + 1 ) \alpha^{i+1}=\alpha^{i}\left(\mathrm{x}_{0}^{T} \mathbf{w}_{i+1}+1\right) αi+1=αi(x0Twi+1+1) 是一个 scalar. 因此 x i + 1 \mathbf{x}_{i+1} xi+1 仍然是一个 scalar 和 x 0 \mathbf{x}_{0} x0 的乘积. 因此可以证明, Cross 层的输出确实满足一种特殊的形式, 即 x k \mathbf{x}_{k} xk 是一个 scalar 和 x 0 \mathbf{x}_{0} x0 的乘积.

Cross 层可以高效的对高阶交叉特征进行显式地学习, 但问题是其结果受限于一个特殊的形式, 并且特征交叉是在 bit-wise level 而不是 vector-wise level 上. (另外注意, 虽然 x k \mathbf{x}_{k} xk 可以表示为一个 scalar 和 x 0 \mathbf{x}_{0} x0 的乘积, 但并不意味着 x k \mathbf{x}_{k} xk x 0 \mathbf{x}_{0} x0 是线性关系, 因为系数 α i \alpha^{i} αi 是跟 x 0 \mathbf{x}_{0} x0 有关的).

基于以上考虑, 本文提出 CIN 模块, 一种新的对特征进行显式交叉的方法, 并且特征交叉是在 vector-wise level 上进行的.

CIN (Compressed Interaction Network)

假设 Embedding Layer 的输出为 X 0 ∈ R m × D \mathbf{X}^0\in\mathbb{R}^{m\times D} X0Rm×D, 其中 m m m 表示 field 的个数, D D D 表示每个 field 所对应的 embedding 的大小, 第 i i i 行表示第 i i i 个 field 所对应的 embedding, 即 X i , ∗ 0 = e i \mathbf{X}^0_{i,\ast} = \mathbf{e}_i Xi,0=ei. CIN 的第 k k k 层输出也是一个矩阵 X k ∈ R H k × D \mathbf{X}^k\in\mathbb{R}^{H_k\times D} XkRHk×D, 其中 H k H_k Hk 表示第 k k k 层的特征数量, 此外, 我们设置 H 0 = m H_0 = m H0=m. CIN 的计算过程可以用下图表示:

对于 CIN 中的每一层来说, 其输出 X k \mathbf{X}^k Xk 的计算方式如下:

X h , ∗ k = ∑ i = 1 H k − 1 ∑ j = 1 m W i j k , h ( X i , ∗ k − 1 ∘ X j , ∗ 0 ) \mathbf{X}_{h, *}^{k}=\sum_{i=1}^{H_{k-1}} \sum_{j=1}^{m} \mathbf{W}_{i j}^{k, h}\left(\mathbf{X}_{i, *}^{k-1} \circ \mathbf{X}_{j, *}^{0}\right) Xh,k=i=1Hk1j=1mWijk,h(Xi,k1Xj,0)

其中 1 ≤ h ≤ H k 1 \leq h \leq H_k 1hHk, W k , h ∈ R H k − 1 × D \mathbf{W}^{k, h}\in\mathbb{R}^{H_{k-1}\times D} Wk,hRHk1×D 表示第 h h h 个特征所对应的参数矩阵. ∘ \circ 表示哈达玛积 (Hadamard product), 比如对于向量 ⟨ a 1 , a 2 , a 3 ⟩ ∘ ⟨ b 1 , b 2 , b 3 ⟩ = ⟨ a 1 b 1 , a 2 b 2 , a 3 b 3 ⟩ \langle a_1, a_2, a_3\rangle\circ\langle b_1, b_2, b_3\rangle = \langle a_1b_1, a_2b_2, a_3b_3\rangle a1,a2,a3b1,b2,b3=a1b1,a2b2,a3b3.
注意到 X k \mathbf{X}^{k} Xk 是通过 X k − 1 \mathbf{X}^{k - 1} Xk1 X 0 \mathbf{X}^{0} X0 进行交叉得到的, 而且交叉特征的学习是显式的, 其阶数会随着层数的增加而增大. 上式可以用上面图示中的 (a) 和 (b) 来形象地描述.

上图中的 ( c c c) 给出了 CIN 整体的结构, 假设 CIN 总共有 T T T 层, 在得到每层的输出结果 X k \mathbf{X}^k Xk ( k ∈ [ 1 , T ] k\in[1, T] k[1,T]) 后, 对结果中的每个 feature map 进行 sum pooling, 即:

p i k = ∑ j = 1 D X i , j k p_{i}^{k}=\sum_{j=1}^{D} \mathrm{X}_{i, j}^{k} pik=j=1DXi,jk

其中 i ∈ [ 1 , H k ] i\in[1, H_k] i[1,Hk]. 因此我们可以得到经过 pooling 后的 vector: p k = [ p 1 k , p 2 k , … , p H k k ] \mathbf{p}^{k}=\left[p_{1}^{k}, p_{2}^{k}, \ldots, p_{H_{k}}^{k}\right] pk=[p1k,p2k,,pHkk], 其中 H k H_k Hk 表示第 k k k 层的特征个数. 之后将 CIN 中所有层的 pooling vector 进行 concatenation, 得到:

p + = [ p 1 , p 2 , … , p T ] ∈ R ∑ i = 1 T H i \mathbf{p}^{+}=\left[\mathbf{p}^{1}, \mathbf{p}^{2}, \ldots, \mathbf{p}^{T}\right] \in \mathbb{R}^{\sum_{i=1}^{T} H_{i}} p+=[p1,p2,,pT]Ri=1THi

p + \mathbf{p}^{+} p+ 为 CIN 层的输出结果.

最后再讨论下 CIN 层的参数个数, 对于第 k k k 层来说, 其权重参数个数为 H k × H k − 1 × m H_k\times H_{k - 1}\times m Hk×Hk1×m, 那么总共有 ∑ k = 1 T H k × H k − 1 × m \sum\limits_{k=1}^{T} H_k\times H_{k - 1}\times m k=1THk×Hk1×m 个参数 (论文中考虑了输出层的 ∑ k = 1 T H k \sum\limits_{k=1}^{T} H_k k=1THk 个参数, 所以总共为 ∑ k = 1 T H k × ( 1 + H k − 1 × m ) \sum\limits_{k=1}^{T} H_k\times \left(1 + H_{k - 1}\times m\right) k=1THk×(1+Hk1×m) 个, 咱不考虑😁)

CIN 源码浅析

原作者在 https://github.com/Leavingseason/xDeepFM/blob/master/exdeepfm/src/CIN.py 中实现了 CIN, 由于代码有点多, 不太想看, 这里分析 DeepCTR 对于 CIN 的实现, 了解个大概就行, 要用到的时候再说 🤣 🤣 🤣
DeepCTR 在 https://github.com/shenweichen/DeepCTR/blob/master/deepctr/layers/interaction.py 中实现了 CIN 模块.

详细注释写在了代码中, 其中不太直观的地方有两处, 我写了很简单的测试用例, 可以用于后续的参考:

  • dot_result_m = tf.matmul(split_tensor0, split_tensor, transpose_b=True)
import tensorflow as tf

B = 2
D = 3
m = 2
H = 2 ## 理解为 H_{k-1}
a = tf.reshape(tf.range(B * D * m, dtype=tf.float32),
              (B, m, D))
b = tf.split(a, D * [1], 2)
c = tf.matmul(b, b, transpose_b=True)

with tf.Session() as sess:
    print(sess.run(tf.shape(c))) ## shape 为 [D, B, m, H_{k-1}]
  • curr_out = tf.nn.conv1d(dot_result, filters=self.filters[idx], stride=1, padding='VALID')
import tensorflow as tf

B = 2
D = 3
E = 4  ## 代表 m * H_{k-1}
F = 5  ## 代表 H_{k}
a = tf.reshape(tf.range(B * D * E, dtype=tf.float32),
              (B, D, E))
b = tf.reshape(tf.range(1 * E * F, dtype=tf.float32),
              (1, E, F))
curr_out = tf.nn.conv1d(
    a, filters=b, stride=1, padding='VALID')

with tf.Session() as sess:
    print(sess.run(tf.shape(curr_out))) ## 结果为 [B, D, H_{k}]

CIN 模块的代码如下:

class CIN(Layer):
    """Compressed Interaction Network used in xDeepFM.This implemention is
    adapted from code that the author of the paper published on https://github.com/Leavingseason/xDeepFM.
      Input shape
        - 3D tensor with shape: ``(batch_size,field_size,embedding_size)``.
      Output shape
        - 2D tensor with shape: ``(batch_size, featuremap_num)`` ``featuremap_num =  sum(self.layer_size[:-1]) // 2 + self.layer_size[-1]`` if ``split_half=True``,else  ``sum(layer_size)`` .
      Arguments
        - **layer_size** : list of int.Feature maps in each layer.
        - **activation** : activation function used on feature maps.
        - **split_half** : bool.if set to False, half of the feature maps in each hidden will connect to output unit.
        - **seed** : A Python integer to use as random seed.
      References
        - [Lian J, Zhou X, Zhang F, et al. xDeepFM: Combining Explicit and Implicit Feature Interactions for Recommender Systems[J]. arXiv preprint arXiv:1803.05170, 2018.] (https://arxiv.org/pdf/1803.05170.pdf)
    """

    def __init__(self, layer_size=(128, 128), activation='relu', split_half=True, l2_reg=1e-5, seed=1024, **kwargs):
        if len(layer_size) == 0:
            raise ValueError(
                "layer_size must be a list(tuple) of length greater than 1")
        self.layer_size = layer_size
        self.split_half = split_half
        self.activation = activation
        self.l2_reg = l2_reg
        self.seed = seed
        super(CIN, self).__init__(**kwargs)

    def build(self, input_shape):
        if len(input_shape) != 3:
            raise ValueError(
                "Unexpected inputs dimensions %d, expect to be 3 dimensions" % (len(input_shape)))

        self.field_nums = [int(input_shape[1])]
        self.filters = []
        self.bias = []
        for i, size in enumerate(self.layer_size):
			
			## layer_size 对应着论文中的 H_{k}, 表示 CIN 每层中 feature map 的个数
			## self.filters[i] 的 shape 为 [1, m * H_{k-1}, H_{k}]
            self.filters.append(
            	self.add_weight(name='filter' + str(i),
                                shape=[1, self.field_nums[-1] * self.field_nums[0], size],
								dtype=tf.float32, initializer=glorot_uniform(seed=self.seed + i),
                                regularizer=l2(self.l2_reg)))
			## self.bias[i] 的 shape 为 [H_{k}]
            self.bias.append(
            	self.add_weight(name='bias' + str(i), 
            					shape=[size], dtype=tf.float32,
                                initializer=tf.keras.initializers.Zeros()))

            if self.split_half:
                if i != len(self.layer_size) - 1 and size % 2 > 0:
                    raise ValueError(
                        "layer_size must be even number except for the last layer when split_half=True")

                self.field_nums.append(size // 2)
            else:
                self.field_nums.append(size)

        self.activation_layers = [activation_layer(
            self.activation) for _ in self.layer_size]

        super(CIN, self).build(input_shape)  # Be sure to call this somewhere!

    def call(self, inputs, **kwargs):
		## inputs 的 shape 为 [B, m, D], 其中 m 为 Field 的数量,
		## D 为 embedding size, 我注释的符号尽量和论文中的一样
        if K.ndim(inputs) != 3:
            raise ValueError(
                "Unexpected inputs dimensions %d, expect to be 3 dimensions" % (K.ndim(inputs)))

        dim = int(inputs.get_shape()[-1]) # D
        hidden_nn_layers = [inputs]
        final_result = []
		
		## split_tensor0 表示 list: [x1, x2, ..., xD], 其中 xi 的 shape 为 [B, m, 1]
        split_tensor0 = tf.split(hidden_nn_layers[0], dim * [1], 2)
        for idx, layer_size in enumerate(self.layer_size):
        	## split_tensor 表示 list: [t1, t2, ..., tH_{k-1}], 即有 H_{k-1} 个向量;
        	## 其中 ti 的 shape 为 [B, H_{k-1}, 1]
            split_tensor = tf.split(hidden_nn_layers[-1], dim * [1], 2)
			
			## dot_result_m 为一个 tensor, 其 shape 为 [D, B, m, H_{k-1}]
            dot_result_m = tf.matmul(
                split_tensor0, split_tensor, transpose_b=True)

			## dot_result_o 的 shape 为 [D, B, m * H_{k-1}]
            dot_result_o = tf.reshape(
                dot_result_m, shape=[dim, -1, self.field_nums[0] * self.field_nums[idx]])
			
			## dot_result 的 shape 为 [B, D, m * H_{k-1}]
            dot_result = tf.transpose(dot_result_o, perm=[1, 0, 2])
			
			## 牛掰啊, 还可以这样写, 精彩!
			## self.filters[idx] 的 shape 为 [1, m * H_{k-1}, H_{k}]
			## 因此 curr_out 的 shape 为 [B, D, H_{k}]
            curr_out = tf.nn.conv1d(
                dot_result, filters=self.filters[idx], stride=1, padding='VALID')
			
			## self.bias[idx] 的 shape 为 [H_{k}]
			## 因此 curr_out 的 shape 为 [B, D, H_{k}]
            curr_out = tf.nn.bias_add(curr_out, self.bias[idx])
			
			## curr_out 的 shape 为 [B, D, H_{k}]
            curr_out = self.activation_layers[idx](curr_out)
			
			## curr_out 的 shape 为 [B, H_{k}, D]
            curr_out = tf.transpose(curr_out, perm=[0, 2, 1])
			
            if self.split_half:
                if idx != len(self.layer_size) - 1:
                    next_hidden, direct_connect = tf.split(
                        curr_out, 2 * [layer_size // 2], 1)
                else:
                    direct_connect = curr_out
                    next_hidden = 0
            else:
                direct_connect = curr_out
                next_hidden = curr_out

            final_result.append(direct_connect)
            hidden_nn_layers.append(next_hidden)
		
		## 先假设不走 self.split_half 的逻辑, 此时 result 的
		## shape 为 [B, sum(H_{k}), D] (k=1 -> T, T 为 CIN 的总层数)
        result = tf.concat(final_result, axis=1)
        ## result 最终的 shape 为 [B, sum(H_{k})]
        result = reduce_sum(result, -1, keep_dims=False)

        return result

总结

这篇文章有点麻烦, 写博客花的时间有点久啊, 上午开始写, 左磨右磨终于写完了. 必须说一下, 我昨天把手机 B 站 APP 给卸载了, 感觉生活多出了很多时间… 🤣 🤣 🤣

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值