本文主要讲述FM的推导,DeepFM结构与DeepFM的tensorflow代码解读
FM算法
以往我们在预测点击时,常用的技术是GBDT+LR(逻辑回归),但这种方法并没有考虑特征交叉项对目标的影响。
假如我们认为点击时符合户线性归回模型时,当我们加入交叉项时,多元线性回归变为以下形式:
(注,在计量中,上面形式仍为线性回归,最小二乘依然是有效的)
这样做的好处考虑的特征之间的相互效应,模型的精度会上升。但代价是模型参数量大量增加(算法由O(n)变为O(n^2)。对此,可尝试对nn的二次项矩阵系数W,进行分解,变为NK的矩阵T与其转置相乘。有:
此时无需在估计nn的二次项矩阵系数W,转为估计nk的矩阵T,通常k<<n。进一步的对公式二次项进行拆开有:
于是可以提出FM的结构如下:
注:由于经过计算训练好的embedding层的系数矩阵刚好等于向量Vi,因此可用embedding层计算Vi,并且embedding的输出刚好为ViXi。图中输入层为one-hot编码方式吗,每个Field中红色点代表该位置为1,蓝色点代表该位置为0,是非常稀疏的。经过embedding计算后,embedding层输出是稠密的。由于embedding的输出刚好为ViXi,由FM公式可知,需将不同ViXi两两组合。图中embedding层到FM层两两组合的红线即代表这一过程。图中一次项为输入层直接到FM层的黑线。
由于特征有很多是类别变量,需要进行one-hot处理,这导致输入是稀疏的。因此提出了FFM概念,FFM与FM的主要区别是FFM提出了类别Field的概念。同一类别下的特征(比如男:[1,0],女[0,1]都属于性别类)属于一个类别,FFM中每一维特征 xi,针对其它特征的每一种field fj,都会学习一个隐向量 vi,fj。因此,隐向量不仅与特征相关,也与field相关。
FM中,每个特征i对其他特征学习一个隐向量vi,j。FFM中,每个特征i对不同的Field学习不同的隐向量vi,fj。
假设样本的 n个特征属于 f个field,那么FFM的二次项有 nf个隐向量,而在FM模型中,每一维特征的隐向量只有一个。FM可以看作FFM的特例,是把所有特征都归属到一个field时的FFM模型。FFM中需要求解的隐向量更多,求解更加困难。
DeepFM算法
DeepFM是FM结构和DNN结构并连组成的结构。上面FM结构中embedding的输出可直接输入到DNN中。因此可设计如下DNN结构
将FM结构和DNN结构并联,遍得到了DeepFM:
DeepFM代码解读
本文代码提取自:DeepFM.git
#声明placeholder
self.feat_index = tf.placeholder(tf.int32,shape=[None,None],name='feat_index')
self.feat_value = tf.placeholder(tf.float32,shape=[None,None],name='feat_value')
self.label = tf.placeholder(tf.float32,shape=[None,1],name='label')
self.dropout_keep_fm = tf.placeholder(tf.float32,shape=[None],name='dropout_keep_fm') #存放FM层的dropout概率
self.dropout_keep_deep = tf.placeholder(tf.float32,shape=[None],name='dropout_deep_deep') #存放FM层的dropout概率
#embeddings构建权重
weights['feature_embeddings'] = tf.Variable( tf.random_normal([self.feature_size,self.embedding_size],0.0,0.01),name='feature_embeddings') #权重项
weights['feature_bias'] =tf.Variable(tf.random_normal([self.feature_size,1],0.0,1.0),name='feature_bias') #偏置项
#构建底层field到embedding层
# model
self.embeddings = tf.nn.embedding_lookup(self.weights['feature_embeddings'],self.feat_index) # N * F * K,embedding层
feat_value = tf.reshape(self.feat_value,shape=[-1,self.field_size,1]) #对输入向量展开
self.embeddings = tf.multiply(self.embeddings,feat_value) #输入向量与embedding矩阵相乘,得到embedding向量,即(Vi*Xi)
# 构建FM层
# FM公式中一次项的构建
self.y_first_order = tf.nn.embedding_lookup(self.weights['feature_bias'],self.feat_index) #一次项Xi前面的次数,与输入特征维度一致
self.y_first_order = tf.reduce_sum(tf.multiply(self.y_first_order,feat_value),2) #计算一次向的输出,W0*x
self.y_first_order = tf.nn.dropout(self.y_first_order,self.dropout_keep_fm[0]) #对一次向dropout,防止过拟合
#FM公式中二次项的构建
# 二次项中和的平方的构建,即(sum(Vi*Xi))^2
self.summed_features_emb = tf.reduce_sum(self.embeddings,1) # None * k 构建FM模型中的Vi*Xi,即对embedding求和
self.summed_features_emb_square = tf.square(self.summed_features_emb) # None * K ,求平方
# 二次项中平方和项的构建。即sum(Vi^2*Xi^2)
self.squared_features_emb = tf.square(self.embeddings) #求平方
self.squared_sum_features_emb = tf.reduce_sum(self.squared_features_emb, 1) # None * K,求和
#二次项的构建,(sum(Vi*Xi))^2-sum(Vi^2*Xi^2),即summed_features_emb_square -squared_sum_features_emb
self.y_second_order = 0.5 * tf.subtract(self.summed_features_emb_square,self.squared_sum_features_emb) #相减,构建FM二次项层
self.y_second_order = tf.nn.dropout(self.y_second_order,self.dropout_keep_fm[1]) #FM二次项层的dropout层
#DNN结构编写
#首先利用embedding层的输入当多DNN的输入
self.y_deep = tf.reshape(self.embeddings,shape=[-1,self.field_size * self.embedding_size])
self.y_deep =tf.nn.dropout(self.y_deep,self.dropout_keep_deep[0]) #dropout层
#都用循环构建多层DNN网络
for i in range(0,len(self.deep_layers)):
self.y_deep = tf.add(tf.matmul(self.y_deep,self.weights["layer_%d" %i]), self.weights["bias_%d"%I])
self.y_deep = self.deep_layers_activation(self.y_deep)
self.y_deep = tf.nn.dropout(self.y_deep,self.dropout_keep_deep[i+1])
#将FM的一次向,二次项输出和DNN输出,用concat并行起来,构成最终输出
concat_input = tf.concat([self.y_first_order, self.y_second_order, self.y_deep], axis=1)
# 定义损失函数
#二分类损失用logloss
if self.loss_type == "logloss":
self.out = tf.nn.sigmoid(self.out)
self.loss = tf.losses.log_loss(self.label, self.out)
#连续值用MSE
elif self.loss_type == "mse":
self.loss = tf.nn.l2_loss(tf.subtract(self.label, self.out))
# l2 regularization on weights添加l2正则项
if self.l2_reg > 0:
self.loss += tf.contrib.layers.l2_regularizer(
self.l2_reg)(self.weights["concat_projection"])
if self.use_deep:
for i in range(len(self.deep_layers)):
self.loss += tf.contrib.layers.l2_regularizer(
self.l2_reg)(self.weights["layer_%d" % I])
#定义不同的优化器
if self.optimizer_type == "adam":
self.optimizer = tf.train.AdamOptimizer(learning_rate=self.learning_rate, beta1=0.9, beta2=0.999,epsilon=1e-8).minimize(self.loss)
elif self.optimizer_type == "adagrad":
self.optimizer = tf.train.AdagradOptimizer(learning_rate=self.learning_rate,
initial_accumulator_value=1e-8).minimize(self.loss)
elif self.optimizer_type == "gd":
self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.learning_rate).minimize(self.loss)
elif self.optimizer_type == "momentum":
self.optimizer = tf.train.MomentumOptimizer(learning_rate=self.learning_rate, momentum=0.95).minimize(self.loss)