模型介绍
由于FM模型只能以线性的方式学习两两特征之间的交互关系,无法捕捉现实数据的复杂结果。因此尽管DeepFM将FM和DNN并行设计,也无法很好地捕捉低阶特征。因此这篇文章提出了一种将FM融合进DNN的模型NFM(Neural Factorization Machines for Sparse Predictive Analytics)。
FM模型中目标值的预测公式为:
NFM模型的目标值预测公式为:
可以看到,NFM用f(x)替代了FM模型中二阶隐向量内积的部分,即让f(x)更复杂,表达更强。作者在这篇文章中将f(x)建模为一个神经网络。NFM的模型结构为
在Embedding层后增加了Bi-Interaction层。
下面逐层介绍NFM结构:
输入层:输入层包括各个类别特征的one-hot表示以及连续特征。
Embedding层:将每个特征映射到一个向量,得到embedding向量集合
Bi-Interaction层:将embedding向量集合转换为单一向量。这一操作可用公式表示:
其中表示两个向量的元素积(两个向量对应维度相乘得到的元素积向量),因此得到的向量和embedding向量维度相同。这一层实现了对embedding向量的二阶交叉特征提取,同时没有引入新的模型参数。
隐藏层:这些全连接层用来在学习到的二阶特征的基础上,进行多阶、非线性的特征学习。另外,由于Bi-Interaction层学习到了更多的组合特征,只需要很少隐藏层就能学习到高阶特征信息。
预测层:已知最后一层隐藏层的输出,得到最终的预测结果。如果是CTR预估问题,预测层这里需要再加一个sigmoid函数。
代码实现
在代码实现时将输入分为linear input和dnn input两部分,两部分都包含离散特征和连续特征。由于NFM模型除了Bi-Interaction之外,还包括线性部分。两个输入将分别作为网络中线性部分和神经网络部分的输入,可以由自行构建。
输入层构建:
def build_input_layers(feature_columns):
# 构建Input层字典,并以dense和sparse两类字典的形式返回
dense_input_dict, sparse_input_dict = {}, {}
for fc in feature_columns:
if isinstance(fc, SparseFeat):
sparse_input_dict[fc.name] = Input(shape=(1, ), name=fc.name)
elif isinstance(fc, DenseFeat):
dense_input_dict[fc.name] = Input(shape=(fc.dimension, ), name=fc.name)
return dense_input_dict, sparse_input_dict
Embedding层构建:
def build_embedding_layers(feature_columns, input_layers_dict, is_linear):
# 定义一个embedding层对应的字典
embedding_layers_dict = dict()
# 将特征中的sparse特征筛选出来
sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), feature_columns)) if feature_columns else []
# 如果是用于线性部分的embedding层,其维度为1,否则维度就是自己定义的embedding维度
if is_linear:
for fc in sparse_feature_columns:
embedding_layers_dict[fc.name] = Embedding(fc.vocabulary_size, 1, name='1d_emb_' + fc.name)
else:
for fc in sparse_feature_columns:
embedding_layers_dict[fc.name] = Embedding(fc.vocabulary_size, fc.embedding_dim, name='kd_emb_' + fc.name)
return embedding_layers_dict
计算FM中线性部分的结果:
def get_linear_logits(dense_input_dict, sparse_input_dict, sparse_feature_columns):
# 将所有的dense特征的Input层,然后经过一个全连接层得到dense特征的logits
concat_dense_inputs = Concatenate(axis=1)(list(dense_input_dict.values()))
dense_logits_output = Dense(1)(concat_dense_inputs)
# 获取linear部分sparse特征的embedding层,这里使用embedding的原因是:
# 对于linear部分直接将特征进行onehot然后通过一个全连接层,当维度特别大的时候,计算比较慢
# 使用embedding层的好处就是可以通过查表的方式获取到哪些非零的元素对应的权重,然后在将这些权重相加,效率比较高
linear_embedding_layers = build_embedding_layers(sparse_feature_columns, sparse_input_dict, is_linear=True)
# 将一维的embedding拼接,注意这里需要使用一个Flatten层,使维度对应
sparse_1d_embed = []
for fc in sparse_feature_columns:
feat_input = sparse_input_dict[fc.name]
embed = Flatten()(linear_embedding_layers[fc.name](feat_input))
sparse_1d_embed.append(embed)
# embedding中查询得到的权重就是对应onehot向量中一个位置的权重,所以后面不用再接一个全连接了,本身一维的embedding就相当于全连接
# 只不过是这里的输入特征只有0和1,所以直接向非零元素对应的权重相加就等同于进行了全连接操作(非零元素部分乘的是1)
sparse_logits_output = Add()(sparse_1d_embed)
# 最终将dense特征和sparse特征对应的logits相加,得到最终linear的logits
linear_part = Add()([dense_logits_output, sparse_logits_output])
return linear_part
计算Bi-Interaction和后面隐藏层部分:
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])
def get_dnn_logits(pooling_out):
# dnn层,这里的Dropout参数,Dense中的参数都可以自己设定, 论文中还说使用了BN, 但是个人觉得BN和dropout同时使用
# 可能会出现一些问题,感兴趣的可以尝试一些,这里就先不加上了
dnn_out = Dropout(0.5)(Dense(1024, activation='relu')(pooling_out))
dnn_out = Dropout(0.3)(Dense(512, activation='relu')(dnn_out))
dnn_out = Dropout(0.1)(Dense(256, activation='relu')(dnn_out))
dnn_logits = Dense(1)(dnn_out)
return dnn_logits
最后将linear,dnn的输出相加,再经过sigmoid函数即可。
参考
https://github.com/datawhalechina/team-learning-rs/blob/master/DeepRecommendationModel/NFM.md