FM 模型的引入
逻辑回归模型及其缺点
FM 模型其实是一种思路,具体的应用稍少。一般来说做推荐 CTR 预估时最简单的思路就是将特征做线性组合(逻辑回归 LR),传入 sigmoid 中得到一个概率值,本质上这就是一个线性模型,因为 sigmoid 是单调增函数不会改变里面的线性模型的 CTR 预测顺序,因此逻辑回归模型效果会比较差。也就是 LR 的缺点有:
- 是一个线性模型
- 每个特征对最终输出结果独立,需要手动特征交叉(
),比较麻烦
二阶交叉项的考虑及改进
由于 LR 模型的上述缺陷(主要是手动做特征交叉比较麻烦),干脆就考虑所有的二阶交叉项,也就是将目标函数由原来的
变为
但这个式子有一个问题,只有当
FM 模型使用了如下的优化函数:
事实上做的唯一改动就是把
FM 公式的理解
从公式来看,模型前半部分就是普通的 LR 线性组合,后半部分的交叉项:特征组合。首先,单从模型表达能力上来看,FM 是要强于 LR 的,至少它不会比 LR 弱,当交叉项参数
定理:任意一个实对称矩阵(正定矩阵)
都存在一个矩阵 ,使得 成立。
类似地,所有二次项参数
需要估计的参数有
上面的公式中:
为全局偏置; 是模型第 个变量的权重; 特征 和 的交叉权重;- $v_{i}
i$ 维特征的隐向量; 代表向量点积; 为隐向量的长度,包含 个描述特征的因子。
FM 模型中二次项的参数数量减少为 $kn $ 个,远少于多项式模型的参数数量。另外,参数因子化使得
显而易见,FM 的公式是一个通用的拟合方程,可以采用不同的损失函数用于解决 regression、classification 等问题,比如可以采用 MSE(Mean Square Error)loss function 来求解回归问题,也可以采用 Hinge/Cross-Entropy loss 来求解分类问题。当然,在进行二元分类时,FM 的输出需要使用 sigmoid 函数进行变换,该原理与 LR 是一样的。直观上看,FM 的复杂度是
证明:
解释:
是一个具体的值;- 第 1 个等号:对称矩阵
对角线上半部分; - 第 2 个等号:把向量内积
, 展开成累加和的形式; - 第 3 个等号:提出公共部分;
- 第 4 个等号:
和 相当于是一样的,表示成平方过程。
FM 优缺点
优点
- 通过向量内积作为交叉特征的权重,可以在数据非常稀疏的情况下还能有效的训练处交叉特征的权重(因为不需要两个特征同时不为零)
- 可以通过公式上的优化,得到 O (nk) 的计算复杂度,k 一般比较小,所以基本上和 n 是正相关的,计算效率非常高
- 尽管推荐场景下的总体特征空间非常大,但是 FM 的训练和预测只需要处理样本中的非零特征,这也提升了模型训练和线上预测的速度
- 由于模型的计算效率高,并且在稀疏场景下可以自动挖掘长尾低频物料。所以在召回、粗排和精排三个阶段都可以使用。应用在不同阶段时,样本构造、拟合目标及线上服务都有所不同(注意 FM 用于召回时对于 user 和 item 相似度的优化)
- 其他优点及工程经验参考石塔西的文章
缺点
- 只能显示的做特征的二阶交叉,对于更高阶的交叉无能为力。对于此类问题,后续就提出了各类显示、隐式交叉的模型,来充分挖掘特征之间的关系
代码实现
class FM(Layer):
"""显示特征交叉,直接按照优化后的公式实现即可
注意:
1. 传入进来的参数看起来是一个Embedding权重,没有像公式中出现的特征,那是因
为,输入的id特征本质上都是onehot编码,取出对应的embedding就等价于特征乘以
权重。所以后续的操作直接就是对特征进行操作
2. 在实现过程中,对于公式中的平方的和与和的平方两部分,需要留意是在哪个维度
上计算,这样就可以轻松实现FM特征交叉模块
"""
def __init__(self, **kwargs):
super(FM, self).__init__(**kwargs)
def build(self, input_shape):
if not isinstance(input_shape, list) or len(input_shape) < 2:
raise ValueError('`FM` layer should be called \
on a list of at least 2 inputs')
super(FM, self).build(input_shape) # Be sure to call this somewhere!
def call(self, inputs, **kwargs):
"""
inputs: 是一个列表,列表中每个元素的维度为:(None, 1, emb_dim), 列表长度
为field_num
"""
concated_embeds_value = Concatenate(axis=1)(inputs) #(None,field_num,emb_dim)
# 根据最终优化的公式计算即可,需要注意的是计算过程中是沿着哪个维度计算的,将代码和公式结合起来看会更清晰
square_of_sum = tf.square(tf.reduce_sum(
concated_embeds_value, axis=1, keepdims=True)) # (None, 1, emb_dim)
sum_of_square = tf.reduce_sum(
concated_embeds_value * concated_embeds_value,
axis=1, keepdims=True) # (None, 1, emb_dim)
cross_term = square_of_sum - sum_of_square
cross_term = 0.5 * tf.reduce_sum(cross_term, axis=2, keepdims=False)#(None,1)
return cross_term
def compute_output_shape(self, input_shape):
return (None, 1)
def get_config(self):
return super().get_config()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
参考资料