在CTR/CVR预估任务中,FFM(Field-aware Factorization Machine)与FM模型表现非常亮眼。FFM可以看作是FM的升级版,由Yuchi Juan于2016年提出。这里简单一下FFM的原理以及如何将理论转化为实践。
1. FFM公式定义
相较于FM模型,FFM模型引入了域(Field)的概念可看做是对特征进行分组。例如,对于性别特征而言,通常有male,female两种取值,对值进行one-hot编码之后性别特征会拆分为两个独立的特征X_male,X_female。显然,这两个特征具有共同的性质:都属于性别。所以可以将这两个特征归在同一个Field下,即有相同的Field编号。不同域的特征之间,往往具有明显的差异性。对比FM中的做法,每个特征有且仅有一个隐向量,在对特征 xi,与其他特征进行交叉时,始终使用同一个隐向量Vi。
这种无差别式交叉方式,并没有考虑到不同特征之间的共性(同域)与差异性(异域)。
FFM公式化定义如下:
对比FM可以发现,二者之间的差异仅在于二阶交叉对应的隐向量。设数据集中Field的数目为F,那么对于每个特征xi拥有F个对应的隐向量,分别应用于与不同域特征进行交叉时。设隐向量维度为k,FFM二阶交叉项参数为nkF。
2. 求解
由于引入了Field,公式(1)不能像FM那样进行改写,所以FFM模型进行 推断 时的时间复杂度为O(kn*n)。
3. 性能评估
FFM 训练/推断 时间复杂度都为O(kn*n)。
4. 优缺点
优点:
缺点:
为了使用FFM方法,所有的特征必须转换成“field_id:feat_id:value”格式,field_id代表特征所属field的编号,feat_id是特征编号,value是特征的值。
1)含有类别特征的数据集,且需要对特征进行二值化处理。
2)越是稀疏的数据集表现效果相较于其他模型更优。
3)FFM比较难应用于纯数值类型的数据集。
数据预处理 :
与FM一样,最好先进行特征归一化,再进行样本归一化。
超参数对于模型的影响 :
1)FFM的隐向量维度远小于FM的隐向量维度,k对于模型的影响不大。
2)正则化系数
如果lambda太大,容易导致模型欠拟合,反之容易过拟合。
3)在论文中,使用的是Adagrad优化器,全局学习率eta也是超参数。
如果eta在一个较小的水平,则可以表现最佳。过大容易导致过拟合。过小容易欠拟合。
模型训练加速:
1)梯度分布计算;2)自适应学习率;3)多核并行计算;4)SSE3指令并行编程;
核心代码如下:
class FFM(object):
def __init__(self, vec_dim, feat_num, field_num, lr, lamda):
self.vec_dim = vec_dim
self.feat_num = feat_num
self.field_num = field_num
self.lr = lr
self.lamda = lamda
self._build_graph()
def _build_graph(self):
self.add_input()
self.inference()
def add_input(self):
self.x = tf.placeholder(tf.float32, shape=[None, self.feat_num], name='input_x')
self.y = tf.placeholder(tf.float32, shape=[None], name='input_y')
def inference(self):
with tf.variable_scope('linear_part'):
w0 = tf.get_variable(name='bias', shape=[1], dtype=tf.float32)
self.W = tf.get_variable(name='linear_w', shape=[self.feat_num], dtype=tf.float32)
self.linear_part = w0 + tf.reduce_sum(tf.multiply(self.x, self.W), axis=1)
with tf.variable_scope('interaction_part'):
self.V = tf.get_variable(name='interaction_w', shape=[self.feat_num, self.field_num, self.vec_dim], dtype=tf.float32)
self.interaction_part = tf.constant(0, dtype=tf.float32)
for i in range(self.feat_num):
for j in range(i+1, self.feat_num):
self.interaction_part += \
tf.reduce_sum(tf.multiply(self.V[i, field_map[j]], self.V[j, field_map[i]])) * \
tf.multiply(self.x[:, i], self.x[:, j])
self.y_logits = self.linear_part + self.interaction_part
self.y_hat = tf.nn.sigmoid(self.y_logits)
self.pred_label = tf.cast(self.y_hat > 0.5, tf.int32)
self.loss = -tf.reduce_mean(self.y*tf.log(self.y_hat+1e-8) + (1-self.y)*tf.log(1-self.y_hat+1e-8))
self.reg_loss = self.lamda*(tf.reduce_mean(tf.nn.l2_loss(self.W)) + tf.reduce_mean(tf.nn.l2_loss(self.V)))
self.total_loss = self.loss + self.reg_loss
self.train_op = tf.train.AdamOptimizer(self.lr).minimize(self.total_loss)
FFM 训练速度确实很慢=^~^=。