1 介绍
FM(Factorization Machines,因子分解机)早在2010年提出,作为逻辑回归模型的改进版,拟解决在稀疏数据的场景下模型参数难以训练的问题。并且考虑了特征的二阶交叉,弥补了逻辑回归表达能力差的缺陷。
FM 作为推荐算法广泛应用于推荐系统及计算广告领域,通常用于预测点击率 CTR(click-through rate)和转化率 CVR(conversion rate)。
2 原理
逻辑回归为普通的线性模型,优点是复杂度低、方便求解,但缺点也很明显,没有考虑特征之间的交叉,表达能力有限。
FM在线性模型的基础上添加了一个多项式,用于描述特征之间的二阶交叉。
- n n n 表示一个样本的特征的个数(类别特征 onehot 之后的维度),两两交互可得到 n ( n − 1 ) / 2 n(n-1)/2 n(n−1)/2 个交叉项;
- w i j w_{ij} wij 是组合对应的权重,用于表征这对组合的重要性,多项式要学习的参数即为 n ( n − 1 ) / 2 n(n-1)/2 n(n−1)/2 个 w w w 系数。
2.1 存在的问题:
参数 w i j w_{ij} wij 学习困难,因为对 w i j w_{ij} wij 进行更新时,求得的梯度对应为 x i x j x_{i}x_{j} xixj,当且仅当 x i x_{i} xi 与 x j x_{j} xj 都非0时参数才会得到更新。
但是经过 onehot 处理的数据非常稀疏,能够保证两者都非0的组合较少,导致大部分参数 w w w 难以得到充分训练。
2.2 解决方法:
作者对每个特征分量
x
i
x_{i}
xi 引入
k
k
k 维(k<<n) 辅助向量
v
i
=
(
v
i
1
,
v
i
2
,
.
.
.
,
v
i
k
)
v_{i}=(v_{i1},v_{i2},...,v_{ik})
vi=(vi1,vi2,...,vik),每个特征对应一个总共
n
n
n 个向量,然后利用向量内积的结果
v
i
v
j
T
v_{i}v_{j}^{T}
vivjT 来表示原来的组合参数
w
i
j
w_{ij}
wij.
于是,原式变成了如下形式:(尖括号表示内积)
这样要学习的参数从 n ( n − 1 ) / 2 n(n-1)/2 n(n−1)/2 个 w i j w_{ij} wij 系数变成了元素个数为 n ∗ k n*k n∗k 的 V V V 矩阵,因为 k < < n k<<n k<<n, 所以该做法降低了训练复杂度。
此外,引入辅助向量削弱了参数间的独立性,因为对于 x i x_{i} xi 的隐向量 v i v_{i} vi,任何包含 x i x_{i} xi 的特征组合,只要 x i x_{i} xi 本身不为0,都可对 v i v_{i} vi 进行更新,同理每个隐向量都能得到充分的学习,这样就解决了数据稀疏带来的难以训练问题。
2.3 复杂度分析
为了进一步降低算法复杂度,对多次项进行如下化简:
对需要训练的参数
θ
\theta
θ 求梯度得:
重点关注
v
i
f
v_{if}
vif 的梯度,
v
i
f
v_{if}
vif 表示
x
i
x_{i}
xi 的隐向量,因为梯度项
∑
j
=
1
n
v
j
,
f
x
j
\sum_{j=1}^{n} v_{j, f} x_{j}
∑j=1nvj,fxj 中不包含
i
i
i,只与
f
f
f 有关,因此只要一次性求出所有的
f
f
f 的
∑
j
=
1
n
v
j
,
f
x
j
\sum_{j=1}^{n} v_{j, f} x_{j}
∑j=1nvj,fxj 的值(复杂度
O
(
n
k
)
O(nk)
O(nk)),在求每个参数的梯度时都可复用该值。
当已知 ∑ j = 1 n v j , f x j \sum_{j=1}^{n} v_{j, f} x_{j} ∑j=1nvj,fxj 时计算每个参数梯度的复杂度是 O ( 1 ) O(1) O(1),因此训练FM模型的复杂度也是 O ( n k ) O(nk) O(nk)。
化简之后,FM的复杂度从 O ( n 2 k ) O(n^{2}k) O(n2k) 降到线性的 O ( n k ) O(nk) O(nk),更利于上线使用。
2.4 扩展到多维 FM:
将二阶交叉项转换成多阶交叉项,进一步提升模型的表达能力。跟二阶交叉项相同,多阶交叉项也可从
O
(
n
l
k
)
O(n^{l}k)
O(nlk) 复杂度降到线性的
O
(
n
k
)
O(nk)
O(nk),具有非常好的性质。
3 代码
理论结合代码食用更佳, 代码中会加入充分注释,以易理解。(整体代码可参考Github仓库)
FM 层代码:
(将 FM 封装成 Layer,随后在搭建 Model 时直接调用即可)
import tensorflow as tf
import tensorflow.keras.backend as K
class FM_layer(tf.keras.layers.Layer):
def __init__(self, k, w_reg, v_reg):
super(FM_layer, self).__init__()
self.k = k # 隐向量vi的维度
self.w_reg = w_reg # 权重w的正则项系数
self.v_reg = v_reg # 权重v的正则项系数
def build(self, input_shape): # 需要根据input来定义shape的变量,可在build里定义)
self.w0 = self.add_weight(name='w0', shape=(1,), # shape:(1,)
initializer=tf.zeros_initializer(),
trainable=True,)
self.w = self.add_weight(name='w', shape=(input_shape[-1], 1), # shape:(n, 1)
initializer=tf.random_normal_initializer(), # 初始化方法
trainable=True, # 参数可训练
regularizer=tf.keras.regularizers.l2(self.w_reg)) # 正则化方法
self.v = self.add_weight(name='v', shape=(input_shape[-1], self.k), # shape:(n, k)
initializer=tf.random_normal_initializer(),
trainable=True,
regularizer=tf.keras.regularizers.l2(self.v_reg))
def call(self, inputs, **kwargs):
# inputs维度判断,不符合则抛出异常
if K.ndim(inputs) != 2:
raise ValueError("Unexpected inputs dimensions %d, expect to be 2 dimensions" % (K.ndim(inputs)))
# 线性部分,相当于逻辑回归
linear_part = tf.matmul(inputs, self.w) + self.w0 #shape:(batchsize, 1)
# 交叉部分——第一项
inter_part1 = tf.pow(tf.matmul(inputs, self.v), 2) #shape:(batchsize, self.k)
# 交叉部分——第二项
inter_part2 = tf.matmul(tf.pow(inputs, 2), tf.pow(self.v, 2)) #shape:(batchsize, k)
# 交叉结果
inter_part = 0.5*tf.reduce_sum(inter_part1 - inter_part2, axis=-1, keepdims=True) #shape:(batchsize, 1)
# 最终结果
output = linear_part + inter_part
return tf.nn.sigmoid(output) #shape:(batchsize, 1)
定义好了 FM 层,模型搭建就简单了,Model 代码如下:
class FM(tf.keras.Model):
def __init__(self, k, w_reg=1e-4, v_reg=1e-4):
super(FM, self).__init__()
self.fm = FM_layer(k, w_reg, v_reg) # 调用写好的FM_layer
def call(self, inputs, training=None, mask=None):
output = self.fm(inputs) # 输入FM_layer得到输出
return output
数据处理代码: 实验数据点我
import pandas as pd
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split
def create_criteo_dataset(file_path, test_size=0.3):
data = pd.read_csv(file_path)
dense_features = ['I' + str(i) for i in range(1, 14)] # 数值特征
sparse_features = ['C' + str(i) for i in range(1, 27)] # 类别特征
# 缺失值填充
data[dense_features] = data[dense_features].fillna(0)
data[sparse_features] = data[sparse_features].fillna('-1')
# 归一化(数值特征)
data[dense_features] = MinMaxScaler().fit_transform(data[dense_features])
# onehot编码(类别特征)
data = pd.get_dummies(data)
#数据集划分
X = data.drop(['label'], axis=1).values
y = data['label']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size)
return (X_train, y_train), (X_test, y_test)
模型训练代码:
from model import FM
from utils import create_criteo_dataset
import tensorflow as tf
from tensorflow.keras import optimizers, losses, metrics
from sklearn.metrics import accuracy_score
if __name__ == '__main__':
file_path = 'data\train.txt' # 修改为自己的路径
(X_train, y_train), (X_test, y_test) = create_criteo_dataset(file_path, test_size=0.2)
k = 8
w_reg = 1e-5
v_reg = 1e-5
model = FM(k, w_reg, v_reg)
optimizer = optimizers.SGD(0.01)
summary_writer = tf.summary.create_file_writer('.\tensorboard') # tensorboard可视化文件路径
for epoch in range(100):
with tf.GradientTape() as tape:
y_pre = model(X_train) # 前馈得到预测值
loss = tf.reduce_mean(losses.binary_crossentropy(y_true=y_train, y_pred=y_pre)) # 与真实值计算loss值
print('epoch: {} loss: {}'.format(epoch, loss.numpy()))
grad = tape.gradient(loss, model.variables) # 根据loss计算模型参数的梯度
optimizer.apply_gradients(grads_and_vars=zip(grad, model.variables)) # 将梯度应用到对应参数上进行更新
# 需要tensorboard记录的变量(不需要可视化可将该模块注释掉)
with summary_writer.as_default():
tf.summary.scalar("loss", loss, step=epoch)
#评估
pre = model(X_test)
pre = [1 if x>0.5 else 0 for x in pre]
print("AUC: ", accuracy_score(y_test, pre)) # AUC: 0.772
tensorboard的可视化结果如下: (可继续加大 epoch 数)
4 总结
看到这,相信你对 FM 也有了基本的了解。下面是对优缺点的总结:
优点:
- 将二阶交叉特征考虑进来,提高模型的表达能力;
- 引入隐向量,缓解了数据稀疏带来的参数难训练问题;
- 模型复杂度保持为线性,并且改进为高阶特征组合时,仍为线性复杂度,有利于上线应用。
缺点:
- 虽然考虑了特征的交叉,但是表达能力仍然有限,不及深度模型;
- 同一特征 x i x_{i} xi 与不同特征组合使用的都是同一隐向量 v i v_{i} vi,违反了特征与不同特征组合可发挥不同重要性的事实。
下篇文章将会是 FM 算法的改进版本 FFM,敬请期待…
写在最后
一直都想开一个推荐系统专栏,记录一些经典推算法的原理介绍及代码实践。但忙于学业,一直拖到了现在才产出专栏的第一篇文章,之后陆续发表其他算法的文章.
如果你想系统的学习推荐算法,原理可参考我的知乎专栏,代码的实现可参考Github仓库。
码字不易,读完此文的你,如果感觉有收获,就请点个赞吧~