推荐系统系列博客:
这篇文章是中科大、北邮和微软合作发表在KDD’18上的文章,从论文写作手法上来看充满了浓浓的学院风。言归正传,乍一看论文标题xDeepFM还以为是对DeepFM的改进,实际上不是,xDeepFM是对上一篇博客中介绍的DCN(deep&cross network)的改进,这也是为什么这篇博客会介绍xDeepFM的原因。本篇博客将会从一下几个方面来介绍xDeepFM:
这篇文章diss的一个重点就是DCN网络中cross network部分,因此,个人强烈建议在看xDeepFM之前,请先移步去本人的上一篇博客看看DCN。我们先来看看xDeepFM给出的论据:
上面两个点,第二个点没啥好说的,重点来看看第一个点。xDeepFM这篇文章中给出了公式证明,先来看看DCN中cross部分的交叉公式:
x k = x 0 x k − 1 T w k + b k + x k − 1 (1) x_k = x_0x^T_{k-1}w_k + b_k + x_{k-1} \tag{1} xk=x0xk−1Twk+bk+xk−1(1)
再来看看xDeepFM中的证明:
x 1 = x 0 ( x 0 T w 1 ) + x 0 = x 0 ( x 0 T w 1 + 1 ) = α 1 x 0 (2) \begin{aligned} x_1 &= x_0(x_0^Tw_1) + x_0 \\ &= x_0(x_0^Tw_1 + 1) \\ &=\alpha^1x_0 \tag{2} \end{aligned} x1=x0(x0Tw1)+x0=x0(x0Tw1+1)=α1x0(2)
其中, α 1 = x 0 T w 1 + 1 \alpha^1=x_0^Tw_1 + 1 α1=x0Tw1+1。
以此类推,
x i + 1 = α i + 1 x 0 = α i ( x 0 T w i + 1 + 1 ) x 0 = α i − 1 ( x 0 T w i + 1 ) ( x 0 T w i + 1 + 1 ) x 0 = α i − 2 ( x 0 T w i − 1 + 1 ) ( x 0 T w i + 1 ) ( x 0 T w i + 1 + 1 ) x 0 = . . . = ( x 0 T w 1 + 1 ) . . . ( x 0 T w i − 1 + 1 ) ( x 0 T w i + 1 ) ( x 0 T w i + 1 + 1 ) x 0 (3) \begin{aligned} x_{i+1} &= \alpha^{i+1}x_0 \\ &= \alpha^{i}(x_0^Tw_{i+1} + 1)x_0 \\ &= \alpha^{i-1}(x_0^Tw_{i} + 1)(x_0^Tw_{i+1} + 1)x_0 \\ &= \alpha^{i-2}(x_0^Tw_{i-1} + 1)(x_0^Tw_{i} + 1)(x_0^Tw_{i+1} + 1)x_0 \\ &=... \\ &= (x_0^Tw_1 + 1)...(x_0^Tw_{i-1} + 1)(x_0^Tw_{i} + 1)(x_0^Tw_{i+1}+ 1)x_0 \tag{3} \end{aligned} xi+1=αi+1x0=αi(x0Twi+1+1)x0=αi−1(x0Twi+1)(x0Twi+1+1)x0=αi−2(x0Twi−1+1)(x0Twi+1)(x0Twi+1+1)x0=...=(x0Tw1+1)...(x0Twi−1+1)(x0Twi+1)(x0Twi+1+1)x0(3)
xDeepFM认为这种交叉是一种对 x 0 x_0 x0特殊的交叉,但其实吧,我个人认为DCN中这种交叉是没什么问题的,直接拿我在DCN(deep&cross network)这篇博客里的例子来看:
假设样本只有两个特征,为了简单,假设每个特征的embedding维度为1,偏置 b b b直接忽略,设 x 0 = [ x 0 , 1 x 0 , 2 ] x_0=\begin{bmatrix} x_{0,1} \\ x_{0,2} \\ \end{bmatrix} x0=[x0,1x0,2],那么,则有:
x 1 = x 0 ⊙ ( w 1 x 1 ) + x 0 = [ x 0 , 1 x 0 , 2 ] ⊙ [ w 1 , 1 x 0 , 1 + w 1 , 2 x 0 , 2 w 1 , 1 x 0 , 1 + w 1 , 2 x 0 , 2 ] + [ x 0 , 1 x 0 , 2 ] = [ x 0 , 1 ( w 1 , 1 x 0 , 1 + w 1 , 2 x 0 , 2 ) + x 0 , 1 x 0 , 2 ( w 1 , 1 x 0 , 1 + w 1 , 2 x 0 , 2 ) + x 0 , 2 ] = [ w 1 , 1 x 0 , 1 2 + w 1 , 2 x 0 , 2 x 0 , 1 + x 0 , 1 w 1 , 1 x 0 , 1 x 0 , 2 + w 1 , 2 x 0 , 2 2 + x 0 , 2 ] (4) \begin{aligned} x_1&=x_0\odot(w_1x_1) + x_0 \\ &=\begin{bmatrix} x_{0,1} \\ x_{0,2} \\ \end{bmatrix}\odot\begin{bmatrix} w_{1,1}x_{0,1} +w_{1,2}x_{0,2} \\ w_{1,1}x_{0,1} +w_{1,2}x_{0,2} \\ \end{bmatrix} + \begin{bmatrix} x_{0,1} \\ x_{0,2} \\ \end{bmatrix} \\\\ &=\begin{bmatrix} x_{0,1}(w_{1,1}x_{0,1} +w_{1,2}x_{0,2}) + x_{0,1} \\ x_{0,2}(w_{1,1}x_{0,1} +w_{1,2}x_{0,2}) + x_{0,2}\\ \end{bmatrix} \\\\ &=\begin{bmatrix} w_{1,1}x_{0,1}^2 +w_{1,2}x_{0,2}x_{0,1} + x_{0,1} \\ w_{1,1}x_{0,1} x_{0,2} +w_{1,2}x_{0,2}^2 + x_{0,2}\\ \end{bmatrix} \end{aligned} \tag{4} x1=x0⊙(w1x1)+x0=[x0,1x0,2]⊙[w1,1x0,1+w1,2x0,2w1,1x0,1+w1,2x0,2]+[x0,1x0,2]=[x0,1(w1,1x0,1+w1,2x0,2)+x0,1x0,2(w1,1x0,1+w1,2x0,2)+x0,2]=[w1,1x0,12+w1,2x0,2x0,1+x0,1w1,1x0,1x0,2+w1,2x0,22+x0,2](4)
从上面的例子能够看出,DCN中cross部分交叉项包含了 x 0 , 1 2 , x 0 , 2 x 0 , 1 , . . . x_{0,1}^2,x_{0,2}x_{0,1},... x0,12,x0,2x0,1,...等交叉项,所以DCN中这种cross的设计是没什么问题的。所以xDeepFM是否在真实的场景中效果比DCN好,这一点我是持怀疑态度的,但有一点可以肯定的是xDeepFM网络结构相对复杂,参数量相对大的多,计算复杂度高,在工业界大规模数据场景下,能否顺利落地是个问题。
关于xDeepFM的整天网络结构,如下图所示(图片直接摘自原论文):
从上图可以看出,xDeepFM依然延续了wide&deep,DCN这种混合网络结构模式,左手画个xxx网络用于构造高阶交叉特征,右手画个普通DNN用于学习隐式的交叉特征,最终一起联合训练。xDeepFM这里包含三部分:普通的线性部分、CIN部分用于构造高阶交叉特征、普通的DNN部分用于学习隐式特征。
这里核心部分显然在于CIN结果,本篇博客会重点讲解这个部分。
CIN全称Compressed Interaction Network,我们照着论文中给出的图逐一讲解,先上图:
x 0 x^0 x0的维度为 m ∗ D m * D m∗D,m为特征个数,D为特征embedding向量维度。 x k x^k xk的维度为 H k ∗ D H_k*D Hk∗D, H k H_k Hk表示第 k k k层特征向量个数。它这里的做法是:用 x k x^k xk的第 i i i列向量即 x ∗ i k x^k_{*i} x∗ik与 x 0 x^0 x0的第 i i i列向量的转置即 ( x ∗ i 0 ) T (x^0_{*i})^T (x∗i0)T做外积,因此得到一个 H k ∗ m H_k*m Hk∗m大小的矩阵,因为总共有D个这些向量,所以最终结果也就是 Z k + 1 Z^{k+1} Zk+1为一个 H k ∗ m ∗ D H_k*m*D Hk∗m∗D的三维矩阵。
用个例子来说明上图,假设 x ∗ 1 k = [ a 1 , a 2 , a 3 ] x^k_{*1}=[a_1,a_2,a_3] x∗1k=[a1,a2,a3], x ∗ 1 0 = [ b 1 , b 2 , b 3 , b 4 ] x^0_{*1}=[b_1,b_2,b_3,b_4] x∗10=[b1,b2,b3,b4],则有:
x ∗ 1 k ⊗ ( x ∗ 1 0 ) T = [ a 1 a 2 a 3 ] ⊗ [ b 1 b 2 b 3 b 4 ] = [ a 1 b 1 a 1 b 2 a 1 b 3 a 1 b 4 a 2 b 1 a 2 b 2 a 2 b 3 a 2 b 4 a 3 b 1 a 3 b 2 a 3 b 3 a 3 b 4 ] \begin{aligned} x^k_{*1} \otimes (x^0_{*1})^T=\begin{bmatrix} a_1& a_2& a_3 \\ \end{bmatrix} \otimes \begin{bmatrix} b_1 \\ b_2 \\ b_3 \\ b_4 \end{bmatrix}=\begin{bmatrix} a_1b_1& a_1b_2& a_1b_3&a_1b_4 \\ a_2b_1& a_2b_2& a_2b_3&a_2b_4 \\ a_3b_1& a_3b_2& a_3b_3&a_3b_4 \\ \end{bmatrix} \end{aligned} x∗1k⊗(x∗10)T=[a1a2a3]⊗⎣⎢⎢⎡b1b2b3b4⎦⎥⎥⎤=⎣⎡a1b1a2b1a3b1a1b2a2b2a3b2a1b3a2b3a3b3a1b4a2b4a3b4⎦⎤
以此类推, x ∗ 2 k ⊗ x ∗ 2 0 x^k_{*2} \otimes x^0_{*2} x∗2k⊗x∗20, x ∗ 3 k ⊗ x ∗ 3 0 x^k_{*3} \otimes x^0_{*3} x∗3k⊗x∗30,…, x ∗ D k ⊗ x ∗ D 0 x^k_{*D} \otimes x^0_{*D} x∗Dk⊗x∗D0均会得到一个 H k ∗ m H_k*m Hk∗m的矩阵,合在一起变为一个 H k ∗ m ∗ D H_k*m*D Hk∗m∗D的三维矩阵,也就是上图中的 Z k + 1 Z^{k+1} Zk+1。
经过图(a)中的外积操作,我们得到一个 H k ∗ m ∗ D H_k*m*D Hk∗m∗D的三维矩阵 Z k + 1 Z^{k+1} Zk+1,图(b)要做的事情就是把一个 H k ∗ m ∗ D H_k*m*D Hk∗m∗D的矩阵压缩成一个 H k ∗ D H_k*D Hk∗D的矩阵,这也是CIN中C的来源,Compressed。
说到矩阵维度的压缩,自然就想到了CNN中的卷积操作,论文也借鉴了卷积的操作思想,我们来看看是怎么操作的:它这里使用了 H k + 1 H_{k+1} Hk+1个大小为 H k ∗ m H_k*m Hk∗m的向量(矩阵)对 Z k + 1 Z^{k+1} Zk+1做了一波点乘内积,此时得到是 H k + 1 H_{k+1} Hk+1个向量的第一个元素,因为 Z k + 1 Z^{k+1} Zk+1的channel为D,因此最终得到的 x k + 1 x^{k+1} xk+1为 H k + 1 H_{k+1} Hk+1个D维向量,因此 x k + 1 x^{k+1} xk+1的维度为 H k ∗ D H_k*D Hk∗D。这里需要理解的是 H k ∗ m H_k*m Hk∗m向量(filter)对 Z k + 1 Z^{k+1} Zk+1做点乘内积,得到的是一个值,即向量的某个元素。在原论文里对应 H k H_k Hk个 W k , h W^{k,h} Wk,h。
图(c)这里把整个CIN部分做了个概览,原论文中把这个步骤往RNN的思想上靠了一波。 x k x^k xk的产生由它的上一层 x k − 1 x^{k-1} xk−1和初始的 x 0 x^0 x0共同决定,论文中给出了一个形式化的公式:
X h , ∗ k = ∑ i = 1 H k − 1 ∑ j = 1 m W i j k , h ( X i , ∗ k − 1 ∘ X j , ∗ 0 ) X_{h,*}^k=\sum_{i=1}^{H_{k-1}}\sum_{j=1}^{m}W_{ij}^{k,h}(X_{i,*}^{k-1} \circ X_{j,*}^0) Xh,∗k=i=1∑Hk−1j=1∑mWijk,h(Xi,∗k−1∘Xj,∗0)
拿 x 2 x^2 x2为例来看下全过程, x 0 x^0 x0与 x 1 x^1 x1先经过图(a)中的外积操作得到一个 H 1 ∗ m ∗ D H_1*m*D H1∗m∗D的矩阵 Z 2 Z^{2} Z2,然后 Z 2 Z^{2} Z2经过图(b)中的压缩(卷积)操作,得到维度为 H 2 ∗ D H_2*D H2∗D的矩阵 x 2 x^2 x2,该矩阵包含 H 2 H_2 H2个D维的向量。然后对这 H 2 H_2 H2个D维向量分别做sum pooling操作,如图(c)所示,因此, x 1 x_1 x1最终产生一个 H 1 ∗ 1 H_1*1 H1∗1维向量, x 2 x_2 x2产生一个 H 2 ∗ 1 H_2*1 H2∗1维向量,…, x k x_k xk产生一个 H k ∗ 1 H_k*1 Hk∗1维向量,最后合并成一个维度为 ∑ i = 1 k H i \sum_{i=1}^kH_i ∑i=1kHi维度的向量,与DNN向量再做联合训练。论文中形式化的定义为,假设 X k X^k Xk经过sum pooling后得到的向量为 p k p^k pk,则 p k = [ p 1 k , p 2 k , . . . , p H k k ] p^k=[p_1^k,p_2^k,...,p_{H_k}^k] pk=[p1k,p2k,...,pHkk],则整个CIN最终产生的向量为 p + = [ p 1 , p 2 , . . . . , p K ] p^+=[p^1,p^2,....,p^K] p+=[p1,p2,....,pK], p + p^+ p+的维度为 ∑ i = 1 K H i \sum_{i=1}^KH_i ∑i=1KHi。
paddle官方给出的cin部分的实现代码为(我增加了tensor的维度注释,方便大家理解):
def forward(self, feat_embeddings):
# [shape=[5, 39, 9]]
Xs = [feat_embeddings]
last_s = self.num_field
# layer_sizes_cin=[128, 32]
# cnn_layers= [Conv2D(1521, 128, kernel_size=[1, 1], data_format=NCHW),
# Conv2D(4992, 32, kernel_size=[1, 1], data_format=NCHW)]
for s, _conv in zip(self.layer_sizes_cin, self.cnn_layers):
# 做外积,对应图(a)
# Tensor(shape=[5, 9, 39, 1])
X_0 = paddle.reshape(
# shape=[5,9,39]
x=paddle.transpose(Xs[0], [0, 2, 1]),
shape=[-1, self.sparse_feature_dim, self.num_field,
1]) # None, embedding_size, num_field, 1
# shape=[5, 9, 1, 39]
X_k = paddle.reshape(
x=paddle.transpose(Xs[-1], [0, 2, 1]),
shape=[-1, self.sparse_feature_dim, 1,
last_s]) # None, embedding_size, 1, last_s
# shape=[5, 9, 39, 39]
Z_k_1 = paddle.matmul(
x=X_0, y=X_k) # None, embedding_size, num_field, last_s
# 卷积操作,对应图(b)
# compresses Z^(k+1) to X^(k+1)
# shape=[5, 9, 1521]
Z_k_1 = paddle.reshape(
x=Z_k_1,
shape=[-1, self.sparse_feature_dim, last_s * self.num_field
]) # None, embedding_size, last_s*num_field
# shape=[5, 1521, 9]
Z_k_1 = paddle.transpose(
Z_k_1, [0, 2, 1]) # None, s*num_field, embedding_size
# shape=[5, 1521, 1, 9]
Z_k_1 = paddle.reshape(
x=Z_k_1,
shape=[
-1, last_s * self.num_field, 1, self.sparse_feature_dim
]
) # None, last_s*num_field, 1, embedding_size (None, channal_in, h, w)
# shape=[5, 128, 1, 9]
X_k_1 = _conv(Z_k_1)
# shape=[5, 128, 9]
X_k_1 = paddle.reshape(
x=X_k_1,
shape=[-1, s,
self.sparse_feature_dim]) # None, s, embedding_size
#X_k_1 = m(X_k_1)
Xs.append(X_k_1)
last_s = s
# 池化,图(c)
# sum pooling
# Xs-->Tensor(shape=[5, 39, 9]), Tensor(shape=[5, 128, 9]), Tensor(shape=[5, 32, 9]]
# shape=[5, 160, 9]
y_cin = paddle.concat(
x=Xs[1:], axis=1) # None, (num_field++), embedding_size
# shape=[5, 160]
y_cin = paddle.sum(x=y_cin, axis=-1) # None, (num_field++)i
tmp_sum = sum(self.layer_sizes_cin)
# shape=[5, 1]
y_cin = self.cin_linear(y_cin)
# shape=[5, 1]
return y_cin
xDeepFM整体包含三部分:线性部分、CIN部分、DNN部分。论文给出了联合训练的交叉熵损失函数:
L = − 1 N ∑ i = 1 N y i l o g y i ^ + ( 1 − y i ) l o g ( 1 − y i ^ ) L=-\frac{1}{N}\sum_{i=1}^Ny_ilog\hat{y_i} + (1-y_i)log(1-\hat{y_i}) L=−N1i=1∑Nyilogyi^+(1−yi)log(1−yi^)
其中,预测输出 y i ^ = σ ( W l i n e a r T a + W d n n T x d n n k + W c i n T p + + b ) \hat{y_i}=\sigma(W^T_{linear}a + W^T_{dnn}x^k_{dnn} + W^T_{cin}p^+ +b) yi^=σ(WlinearTa+WdnnTxdnnk+WcinTp++b),即把三部分想加然后做sigmoid。
个人认为xDeepFM参数量较大,计算复杂度应该相对较高,在真实的场景下性能可能会成为一个问题。至于效果,在不同的数据集下与其他模型可能会呈现出不同的结果,例如DCN V2论文里给出的实验结果,xDeepFM在AUC指标上在一些数据集上效果还不如DCN。有兴趣的可以直接去看DCN V2这篇论文的实验比较部分,DCN V2论文地址:DCN V2。