FM利用交叉特征的思路如果引申到二阶以上会因为组合爆炸问题导致权重数量和训练复杂度过高。理论上神经网络可以逼近任意复杂的函数,如果利用深度学习将二项交叉的部分替换成f(x),则可以解决这个局限性。
NFM的具体结构:
这个结构中最为特殊的部分就是Bi-Interaction Pooling layer特征交叉池化层,用数学表达式来表达这层网络结构:
f
B
I
(
V
x
)
=
∑
i
=
1
n
∑
j
=
i
+
1
n
x
i
v
i
⊙
x
j
v
j
f_{B I}\left(\mathcal{V}_{x}\right)=\sum_{i=1}^{n} \sum_{j=i+1}^{n} x_{i} \mathbf{v}_{i} \odot x_{j} \mathbf{v}_{j}
fBI(Vx)=i=1∑nj=i+1∑nxivi⊙xjvj
其中
V
x
\mathcal{V}_{x}
Vx是所有特征embedding的集合
Bi-Interaction层不需要额外的模型学习参数,更重要的是它在一个线性的时间内完成计算,和FM一致的,即时间复杂度为
O
(
k
N
x
)
O\left(k N_{x}\right)
O(kNx),
N
x
N_x
Nx为embedding向量的数量。参考FM,可以将上式转化为:
f
B
I
(
V
x
)
=
1
2
[
(
∑
i
=
1
n
x
i
v
i
)
2
−
∑
i
=
1
n
(
x
i
v
i
)
2
]
f_{B I}\left(\mathcal{V}_{x}\right)=\frac{1}{2}\left[\left(\sum_{i=1}^{n} x_{i} \mathbf{v}_{i}\right)^{2}-\sum_{i=1}^{n}\left(x_{i} \mathbf{v}_{i}\right)^{2}\right]
fBI(Vx)=21⎣⎡(i=1∑nxivi)2−i=1∑n(xivi)2⎦⎤
后面代码复现NFM就是用的这个公式直接计算,比较简便且清晰。.
代码实现:
pooling_output = get_bi_interaction_pooling_output(sparse_input_dict, dnn_feature_columns, embedding_layers)
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
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
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])
问题:这里为什么叫池化层?