目录
1.FM背景与简介
FM目的主要是为了解决稀疏数据下的特征组合问题。2010年,日本大阪大学(Osaka University)的 Steffen Rendle 在矩阵分解(MF)、SVD++[2]、PITF[3]、FPMC[4] 等基础之上,归纳出针对高维稀疏数据的因子机(Factorization Machine, FM)模型[13]。因子机模型可以将上述模型全部纳入一个统一的框架进行分析[1]。目前,FM被广泛的应用于广告预估模型中,相比LR而言,效果强了不少。
1.1 稀疏数据
高维的稀疏矩阵是工程中常见的问题,比如下图:
图中每一个样本对应多个属性,分别是User,Movie,Other Movies rated,Time,Last Movie rated,由于这些属性都是categorical类型,所以一般进行One-hot编码转换为数值类型,但是由于每个属性都有多个离散的取值,所以One-hot编码之后样本空间相比原来变大了许多,而特征矩阵也会变得非常稀疏。假设有10000部电影,有10000个用户,单看前两条,每个样本的特征维度就是两万维,但是每个特征中只有两维取值不为0,非常稀疏。在CTR/CVR预测时,One-Hot编码常会导致样本的稀疏性,样本的稀疏性是实际问题中不可避免的挑战。
1.2为什么进行特征组合
下面以一个示例引入FM模型。假设一个广告分类的问题,根据用户和广告位相关的特征,预测用户是否点击了广告。源数据如下:
![]() |
“Clicked?”是label,Country、Day、Ad_type是特征。由于三种特征都是categorical类型的,需要经过独热编码(One-Hot Encoding)转换成数值型特征。
![]() |
由上表可以看出,经过One-Hot编码之后,大部分样本数据特征是比较稀疏的。上面的样例中,每个样本有7维特征,但平均仅有3维特征具有非零值。
同时通过观察大量的样本数据可以发现,某些特征经过关联之后,与label之间的相关性就会提高。例如,“USA”与“Thanksgiving”、“China”与“Chinese New Year”这样的关联特征,对用户的点击有着正向的影响。换句话说:来自“China”的用户很可能会在“Chinese New Year”有大量的浏览、购买行为;而在“Thanksgiving”却不会有特别的消费行为。这种关联特征与label的正向相关性在实际问题中是普遍存在的,如<“化妆品”类商品,“女”性>,<“球类运动配件”的商品,“男”性>,<“电影票”的商品,“电影”>品类偏好等。因此,引入两个特征的组合是非常有意义的。
1.3如何组合特征
SVM就曾通过多项式核函数来实现特征的交叉。实际上,多项式是构建组合特征的一种非常直观的模型。我们先看一下二阶多项式的建模,只有两个特征都是非零的时候组合特征才有意义:
![]() |
从公式来看,模型前半部分就是普通的LR线性组合,后半部分的交叉项即:特征的组合。单从模型表达能力上来看,FM的表达能力是强于LR的,至少不会比LR弱,当交叉项参数全为0时退化为普通的LR模型。
从公式(1)可以看出,组合特征的参数一共 个,任意两个参数都是独立的。然而,在数据稀疏性普遍存在的实际应用场景中,二次项参数的训练是很困难的。其原因是:每个参数 wij 的训练需要大量xi 和 xj都非零的样本;由于样本数据本来就比较稀疏,满足xi 和 xj都非零”的样本将会非常少。训练样本的不足,很容易导致参数 wij 不准确,最终将严重影响模型的性能。
1.4二次项参数求解方法
矩阵分解提供了一种解决思路。在model-based的协同过滤中,一个rating矩阵可以分解为user矩阵和item矩阵,每个user和item都可以采用一个隐向量表示。比如在下图中的例子中,我们把每个user表示成一个二维向量,同时把每个item表示成一个二维向量,两个向量的点积就是矩阵中user对item的打分。
任意的 N×N 实对称矩阵都有 N 个线性无关的特征向量。并且这些特征向量都可以正交单位化而得到一组正交且模为 1 的向量。故实对称矩阵 A 可被分解成:
1.5公式推导
引入辅助向量V之前,计算复杂度为O(n2),引入V之后,为O(kn),通过交叉项求解,我们知道,计算复杂度为O(kn)。一般来说k远小于n。
之后就可以用梯度下降法求解了,FM模型的梯度为:
1.6关于隐向量
这里的 vi 是 xi 特征的低纬稠密表达,实际中隐向量的长度通常远小于特征维度N,在实际的CTR场景中,数据都是很稀疏的category特征,通常表示成离散的one-hot形式,这种编码方式,使得one-hot vector非常长,而且很稀疏,同时特征总量也骤然增加,达到千万级甚至亿级别都是有可能的,而实际上的category特征数目可能只有几百维。FM学到的隐向量可以看做是特征的一种embedding表示,把离散特征转化为Dense Feature,这种Dense Feature还可以后续和DNN来结合,作为DNN的输入,事实上FNN的CTR也是这个思路来做的。
2:FM代码实现
def model():
x_train, x_test, y_train, y_test = load_data()
n, onehot_dim = x_train.shape
embeding_size = 8
w0 = tf.Variable(tf.zeros([1]))
w = tf.Variable(tf.zeros([onehot_dim]))
X = tf.placeholder(tf.float32, [None, onehot_dim], name="liner_para")
Y = tf.placeholder(tf.float32, [None, 1], name='output')
V = tf.Variable(tf.random_normal([onehot_dim, embeding_size], mean=0, stddev=0.01, seed=1), name="para_martix")
#线性和
liner_sum = tf.add(w0, tf.reduce_sum(tf.multiply(w, X), keep_dims=True, axis=1))
pair_sum = 0.5*tf.reduce_sum(tf.subtract(tf.pow(tf.matmul(X, V), 2), tf.matmul(tf.pow(X, 2), tf.pow(V, 2))),axis=1,keep_dims=True)
#交叉和
y_hat=tf.add(liner_sum,pair_sum)
#正则化部分
lambda_w = tf.constant(0.001, name='lambda_w')
lambda_v = tf.constant(0.001, name='lambda_v')
l2_norm = tf.reduce_sum(
tf.add(
tf.multiply(lambda_w, tf.pow(w, 2)),
tf.multiply(lambda_v, tf.pow(tf.transpose(V), 2))
)
)
#损失函数
error = tf.reduce_mean(tf.square(Y - y_hat))
loss = tf.add(error, l2_norm)
train_op = tf.train.GradientDescentOptimizer(learning_rate=0.01).minimize(loss)
epochs = 10
batch_size = 1000
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
for epoch in tqdm(range(epochs), unit='epoch'):
perm = np.random.permutation(x_train.shape[0])
# iterate over batches
for bX, bY in batcher(x_train[perm], y_train[perm], batch_size):
_, t = sess.run([train_op, loss], feed_dict={X: bX.reshape(-1, onehot_dim), Y: bY.reshape(-1, 1)})
print(t)
errors = []
for bX, bY in batcher(x_test, y_test):
errors.append(sess.run(error, feed_dict={X: bX.reshape(-1, onehot_dim), Y: bY.reshape(-1, 1)}))
print(errors)
RMSE = np.sqrt(np.array(errors).mean())
print(RMSE)
3:xlearn流程以及抽取embeding向量
3.1:数据处理
将数据转换为xlearn所需要的格式,对于 LR 和 FM 算法而言,我们的输入数据格式必须是 CSV
或者 libsvm
. 对于 FFM 算法而言,我们的输入数据必须是 libffm
格式
libsvm format: y index_1:value_1 index_2:value_2 ... index_n:value_n 0 0:0.1 1:0.5 3:0.2 ... 0 0:0.2 2:0.3 4:0.1 ... 1 0:0.2 2:0.3 5:0.1 ... CSV format: y value_1 value_2 .. value_n 0 0.1 0.2 0.2 ... 1 0.2 0.3 0.1 ... 0 0.1 0.2 0.4 ... libffm format: y field_1:index_1:value_1 field_2:index_2:value_2 ... 0 0:0:0.1 1:1:0.5 2:3:0.2 ... 0 0:0:0.2 1:2:0.3 2:4:0.1 ... 1 0:0:0.2 1:2:0.3 2:4:0.1 ...
3.2:训练模型
# Training task
fm_model = xl.create_fm()
fm_model.setTrain("./small_train.txt") # Set the path of training data
# parameter:
# 0. task: binary classification
# 1. learning rate : 0.2
# 2. regular lambda : 0.002
param = {'task':'binary', 'lr':0.2, 'lambda':0.002}
# Train model
fm_model.setTXTModel("./model.txt")
fm_model.fit(param, "./model.out")
模型输出二进制文件,还可以通过
setTXTModel()
API 将模型输出成人类可读的TXT
格式,例如:对于线性模型来说,TXT 格式的模型输出将每一个模型参数存储在一行。对于 FM 和 FFM,模型将每一个 latent vector 存储在一行。
FM:
bias: 0 i_0: 0 i_1: 0 i_2: 0 i_3: 0 v_0: 5.61937e-06 0.0212581 0.150338 0.222903 v_1: 0.241989 0.0474224 0.128744 0.0995021 v_2: 0.0657265 0.185878 0.0223869 0.140097 v_3: 0.145557 0.202392 0.14798 0.127928
3.3抽取向量
根据编码的index对3.2中的向量进行抽取,一般来说,离散字段每个value都会对应一个向量,每个连续字段会对应一个向量,所以在很多情况下,我们会连续值进行分桶,学习到的信息量会更多。
tips:
在xlearn官方文档中,说到支持在线学习,即加载之前的模型对新加进来数据进行增量训练,然而有一个重大缺陷就是我们无法增加新的特征,需要使用之前的编码对新数据进行编码,每条样本因为onehot之后对权重矩阵的命中率很低,因为大部分数值都是0,所以加载之前的模型进行增量训练其实是有一定道理的,能够加快收敛速度。
参考:
https://blog.csdn.net/xxiaobaib/article/details/92801244
https://zhuanlan.zhihu.com/p/37963267
https://xlearn-doc-cn.readthedocs.io/en/latest/python_api/index.html#id1