DeepFM是一个集成了FM和DNN的神经网络框架,思路和Google的Wide&Deep相似,都包括wide和deep两部分。W&D模型的wide部分是广义线性模型,DeepFM的wide部分则是FM模型,两者的deep部分都是深度神经网络。DeepFM神经网络部分,隐含层的激活函数用ReLu和Tanh做信号非线性映射,Sigmoid函数做CTR预估的输出函数。
上图是原论文中的网络结构图,在大多数人的实现中,都或多或少的忽略了两个问题:
1. DeepFM的原始特征是非常稀疏的,所以代码实现需要考虑特征的稀疏化运算;
2. 生产环境中,每一个Field的输入可能是多值,有的实现中,将每一个one-hot特征都看作一个独立的field,这样虽然简单实现DeepFM模型,但是会造成模型的参数爆炸,训练效率和inference效率低下,而更多的实现中则是忽略了这种情况。
本文的实现方案解决了以上两个问题,使DeepFM可以真正应用于生产环境中。
如图所示,每一种颜色代表不同Field的特征,我们假设输入是稀疏的维度为N,并且同一个Field的特征可以不相邻,Field的大小为F,FM的embedding维度为D。最终进入神经网络的是F*D维的向量,并且Field多值的情况下,我们进行field-avg-pooling。
代码地址:https://github.com/ck8275411/deep_rec
Field-Avg-Pooling最麻烦的地方在于:如何在稀疏化的样本tensor中,找出属于同一个Field的特征,并将这些特征进行avg-pooling。
我这里设计了一组名为Field-Selector的0-1矩阵,每一个矩阵中仅有属于同一个Field的特征所属的向量值为1,其它特征的向量值为0。具体方法如下:
1. 将一个Field-Selector与FM embedding矩阵进行element-wise运算,可以得仅与当前Field相关的所有特征的embedding:fm_field_embeddings;
2. 将Field-Selector与样本的SparseTensor进行点积,可以得到每条样本中该Field的特征个数;
3. 将fm_field_embeddings与样本的SparseTensor进行点积,可以得到每条样本中该Field的sum-pooling;
4. sum-pooling值除以特征个数,即得到了avg-pooling。
示意图如下:
1. 生成Field-Selector矩阵
Field-Selector矩阵主要是从一个Field-特征id的映射字典里得到,字典格式为:第一列为Field_id,第二列为特征id。具体实现如下:
def get_feature_field(self, feature_field_file):
feature_fields = []
field_names = {}
_fields = []
for line in open(feature_field_file, "r"):
tokens = line.strip('\r').strip('\n').split(' ')
#特征id从0开始编码,所以feature_fields数组中的下表就可以表示特征id,值表示特征所属的field
feature_fields.append(tokens[0])
#保存去重后的field id
field_names[tokens[0]] = 1
for field_name in field_names.keys():
#遍历所有field
field = []
for j in range(self.feature_size):
'''
遍历所有特征id
如果j大于len(feature_fields),则直接置0
如果第j个特征的field等于当前field,则置1.0
如果第j个特征的field不等于当前field,则置0.0
'''
if j >= len(feature_fields):
field.append([0.0])
elif feature_fields[j] == field_name:
field.append([1.0])
else:
field.append([0.0])
_fields.append(field)
return _fields, len(field_names)
2. Avg-Pooling实现
输入是样本SparseTensor,假设样本数为K,则输出为[K,F*D]的样本DenseTensor以及维度K*D。
def get_field_embeddings(self, sparse_features):
input_layers = []
k = 0
with tf.variable_scope("fm_layer", reuse=tf.AUTO_REUSE):
fm_layer_embeddings = tf.get_variable("weights",
self.fm_weights_shape)
for i in range(self.fields_num):
fm_field_embeddings = tf.multiply(fm_layer_embeddings, self.field_embedding_masks[i])
# 计算每个field的feature_cnt
sparse_x_feature_cnt = tf.maximum(tf.sparse_tensor_dense_matmul(sparse_features, self.field_embedding_masks[i]), 1.0)
sparse_x_field_embedded = tf.sparse_tensor_dense_matmul(sparse_features, fm_field_embeddings)
# 计算embedding计算的均值
sparse_x_field_embedded = tf.divide(sparse_x_field_embedded, sparse_x_feature_cnt)
input_layers.append(sparse_x_field_embedded)
k += self.embedding_dim
nn_inputs = tf.concat(input_layers, 1)
return nn_inputs, k
跑了一个实际数据集,验证两种deepfm的实现在训练效率以及效果的差异:
1. 将one-hot特征每一维看做一个独立的field的deepfm;
2. 增加field-avg-pooling层的field-deepfm;
batch_size = 1024
sample_num = 1700w
feature_size = 10560
field_size = 29
nn_layer_shape = [128, 64, 8]
learning_rate = 0.01
optimizer = adam
效果如下:
可以看到,deepfm的auc比field-deepfm好了0.006,但是训练耗时是field-deepfm的9.2倍,考虑到生产环境的训练效率和Inference效率,显然field-deepfm的实现是好于普通的deepfm的。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。