FM的paper地址如下:https://www.csie.ntu.edu.tw/~b97053/paper/Rendle2010FM.pdf
1. FM背景
在计算广告和推荐系统中,CTR预估(click-through rate)是非常重要的一个环节,判断一个物品是否进行推荐需要根据CTR预估的点击率排序决定。业界常用的方法有人工特征工程 + LR(Logistic Regression)、GBDT(Gradient Boosting Decision Tree) + LR、FM(Factorization Machine)和FFM(Field-aware Factorization Machine)模型。
最近几年出现了很多基于FM改进的方法,如deepFM,FNN,PNN,DCN,xDeepFM等,今后的内容将会分别介绍。
2. FM原理
FM主要是解决稀疏数据下的特征组合问题,并且其预测的复杂度是线性的,对于连续和离散特征有较好的通用性。
假设一个电影评分系统,根据用户和电影的特征,预测用户对电影的评分。
系统记录了用户(U)在特定时间(t)对电影(i)的评分(r),评分为1,2,3,4,5。
给定一个例子:
评分是label,用户id、电影id、评分时间是特征。由于用户id和电影id是categorical类型的,需要经过独热编码(One-Hot Encoding)转换成数值型特征。因为是categorical特征,所以经过one-hot编码以后,导致样本数据变得很稀疏。
下图显示了从S构建的例子:
每行表示目标 与其对应的特征向量 ,蓝色区域表示了用户变量,红色区域表示了电影变量,黄色区域表示了其他隐含的变量,进行了归一化,绿色区域表示一个月内的投票时间,棕色区域表示了用户上一个评分的电影,最右边的区域是评分。
2.1 特征交叉
普通的线性模型,我们都是将各个特征独立考虑的,并没有考虑到特征与特征之间的相互关系。但实际上,特征之间可能具有一定的关联。以新闻推荐为例,一般男性用户看军事新闻多,而女性用户喜欢情感类新闻,那么可以看出性别与新闻的频道有一定的关联性,如果能找出这类的特征,是非常有意义的。
为了简单起见,我们只考虑二阶交叉的情况,具体的模型如下:
其中, 代表样本的特征数量, 是第i个特征的值, 、 、 是模型参数,只有当 与 都不为0时,交叉才有意义。
在数据稀疏的情况下,满足交叉项不为0的样本将非常少,当训练样本不足时,很容易导致参数 训练不充分而不准确,最终影响模型的效果。
那么,交叉项参数的训练问题可以用矩阵分解来近似解决,有下面的公式。
其中模型需要估计的参数是:
是两个 维向量的内积:
对任意正定矩阵 ,只要 足够大,就存在矩阵 ,使得 。然而在数据稀疏的情况下,应该选择较小的k,因为没有足够的数据来估计 。限制k的大小提高了模型更好的泛化能力。
为什么说可以提高模型的泛化能力呢?
以上述电影评分系统为例,假设我们要计算用户 与电影 的交叉项,很显然,训练数据里没有这种情况,这样会导致 ,但是我们可以近似计算出 。首先,用户 和 有相似的向量 和 ,因为他们对 的预测评分比较相似, 所以 和 是相似的。用户 和 有不同的向量,因为对 和 的预测评分完全不同。接下来, 和 的向量可能相似,因为用户 对这两部电影的评分也相似。最后可以看出, 与 是相似的。
直接计算公式(2)的时间复杂度是 ,因为所有的交叉特征都需要计算。但是通过公式变换,可以减少到线性复杂度,方法如下:
可以看到这时的时间复杂度为 。
2.2 预测
FM算法可以应用在多种的预测任务中,包括:
- Regression: 可以直接用作预测,并且最小平方误差来优化。
- Binary classification: 作为目标函数并且使用hinge loss或者logit loss来优化。
- Ranking:向量 通过 的分数排序,并且通过pairwise的分类损失来优化成对的样本
对以上的任务中,正则化项参数一般加入目标函数中以防止过拟合。
2.3 参数学习
从上面的描述可以知道FM可以在线性的时间内进行预测。因此模型的参数可以通过梯度下降的方法(例如随机梯度下降)来学习,对于各种的损失函数。FM模型的梯度是:
由于 只与 有关,与 是独立的,可以提前计算出来,并且每次梯度更新可以在常数时间复杂度内完成,因此FM参数训练的复杂度也是 。综上可知,FM可以在线性时间训练和预测,是一种非常高效的模型。
2.4 多阶FM
2阶FM可以很容易泛化到高阶:
其中对第 个交互参数是由PARAFAC模型的参数因子分解得到:
直接计算公式 的时间复杂度是 。通过调整也可以在线性时间内运行。
2.5 Factorization Machines With FTRL
FTRL是Google在2013年放出这个优化方法,该方法有较好的稀疏性和收敛性。FTRL是一个在线学习的框架,论文中用于求解LR,具体求解方法如下图:
我们只需要把论文中的伪代码进行修改,即可用于FM的参数求解。伪代码如下:
2.6 总结
FM模型有两个优势:
- 在高度稀疏的情况下特征之间的交叉仍然能够估计,而且可以泛化到未被观察的交叉
- 参数的学习和模型的预测的时间复杂度是线性的
FM模型的优化点:
1.特征为全交叉,耗费资源,通常user与user,item与item内部的交叉的作用要小于user与item的交叉
2.使用矩阵计算,而不是for循环计算
3.高阶交叉特征的构造
3. FM实践
代码是用python3.5写的,tensorflow的版本为1.10.1,其他低版本可能不兼容。完整代码可参考我的github地址:
本文使用的数据是movielens-100k,数据包括u.item,u.user,ua.base及ua.test,u.item包括的数据格式为:
movie id | movie title | release date | video release date |
IMDb URL | unknown | Action | Adventure | Animation |
Children's | Comedy | Crime | Documentary | Drama | Fantasy |
Film-Noir | Horror | Musical | Mystery | Romance | Sci-Fi |
Thriller | War | Western |
u.user包括的数据格式为:
user id | age | gender | occupation | zip code
ua.base和ua.test的数据格式为:
user id | item id | rating | timestamp
本文将评分等于5分的评分数据作为用户的点击数据,评分小于5分的数据作为用户的未点击数据,构造为一个二分类问题。
数据输入
要使用FM模型,首先要将数据处理成一个矩阵,本文使用了pandas对数据进行处理,生成输入的矩阵,并且对label做onehot编码处理。
def onehot_encoder(labels, NUM_CLASSES):
enc = LabelEncoder()
labels = enc.fit_transform(labels)
labels = labels.astype(np.int32)
batch_size = tf.size(labels)
labels = tf.expand_dims(labels, 1)
indices = tf.expand_dims(tf.range(0, batch_size,1), 1)
concated = tf.concat([indices, labels] , 1)
onehot_labels = tf.sparse_to_dense(concated, tf.stack([batch_size, NUM_CLASSES]), 1.0, 0.0)
with tf.Session() as sess:
return sess.run(onehot_labels)
def load_dataset():
header = ['user_id', 'age', 'gender', 'occupation', 'zip_code']
df_user = pd.read_csv('data/u.user', sep='|', names=header)
header = ['item_id', 'title', 'release_date', 'video_release_date', 'IMDb_URL', 'unknown', 'Action', 'Adventure', 'Animation', 'Children',
'Comedy', 'Crime', 'Documentary', 'Drama', 'Fantasy', 'Film-Noir', 'Horror', 'Musical', 'Mystery', 'Romance', 'Sci-Fi',
'Thriller', 'War', 'Western']
df_item = pd.read_csv('data/u.item', sep='|', names=header, encoding = "ISO-8859-1")
df_item = df_item.drop(columns=['title', 'release_date', 'video_release_date', 'IMDb_URL', 'unknown'])
df_user['age'] = pd.cut(df_user['age'], [0,10,20,30,40,50,60,70,80,90,100], labels=['0-10','10-20','20-30','30-40','40-50','50-60','60-70','70-80','80-90','90-100'])
df_user = pd.get_dummies(df_user, columns=['gender', 'occupation', 'age'])
df_user = df_user.drop(columns=['zip_code'])
user_features = df_user.columns.values.tolist()
movie_features = df_item.columns.values.tolist()
cols = user_features + movie_features
header = ['user_id', 'item_id', 'rating', 'timestamp']
df_train = pd.read_csv('data/ua.base', sep='\t', names=header)
df_train['rating'] = df_train.rating.apply(lambda x: 1 if int(x) == 5 else 0)
df_train = df_train.merge(df_user, on='user_id', how='left')
df_train = df_train.merge(df_item, on='item_id', how='left')
df_test = pd.read_csv('data/ua.test', sep='\t', names=header)
df_test['rating'] = df_test.rating.apply(lambda x: 1 if int(x) == 5 else 0)
df_test = df_test.merge(df_user, on='user_id', how='left')
df_test = df_test.merge(df_item, on='item_id', how='left')
train_labels = onehot_encoder(df_train['rating'].astype(np.int32), 2)
test_labels = onehot_encoder(df_test['rating'].astype(np.int32), 2)
return df_train[cols].values, train_labels, df_test[cols].values, test_labels
模型设计
得到输入之后,我们使用tensorflow来设计我们的模型,目标函数包括两部分,线性以及交叉特征的部分,交叉特征直接使用我们最后推导的形式即可。
#输入
def add_input(self):
self.X = tf.placeholder('float32', [None, self.p])
self.y = tf.placeholder('float32', [None, self.num_classes])
self.keep_prob = tf.placeholder('float32')
#forward过程
def inference(self):
with tf.variable_scope('linear_layer'):
w0 = tf.get_variable('w0', shape=[self.num_classes],
initializer=tf.zeros_initializer())
self.w = tf.get_variable('w', shape=[self.p, num_classes],
initializer=tf.truncated_normal_initializer(mean=0,stddev=0.01))
self.linear_terms = tf.add(tf.matmul(self.X, self.w), w0)
with tf.variable_scope('interaction_layer'):
self.v = tf.get_variable('v', shape=[self.p, self.k],
initializer=tf.truncated_normal_initializer(mean=0, stddev=0.01))
self.interaction_terms = tf.multiply(0.5,
tf.reduce_sum(
tf.subtract(
tf.pow(tf.matmul(self.X, self.v), 2),
tf.matmul(self.X, tf.pow(self.v, 2))),
1, keep_dims=True))
self.y_out = tf.add(self.linear_terms, self.interaction_terms)
if self.num_classes == 2:
self.y_out_prob = tf.nn.sigmoid(self.y_out)
elif self.num_classes > 2:
self.y_out_prob = tf.nn.softmax(self.y_out)
#loss
def add_loss(self):
if self.num_classes == 2:
cross_entropy = tf.nn.sigmoid_cross_entropy_with_logits(labels=self.y, logits=self.y_out)
elif self.num_classes > 2:
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(labels=self.y, logits=self.y_out)
mean_loss = tf.reduce_mean(cross_entropy)
self.loss = mean_loss
tf.summary.scalar('loss', self.loss)
#计算accuracy
def add_accuracy(self):
# accuracy
self.correct_prediction = tf.equal(tf.cast(tf.argmax(self.y_out,1), tf.float32), tf.cast(tf.argmax(self.y,1), tf.float32))
self.accuracy = tf.reduce_mean(tf.cast(self.correct_prediction, tf.float32))
# add summary to accuracy
tf.summary.scalar('accuracy', self.accuracy)
#训练
def train(self):
self.global_step = tf.Variable(0, trainable=False)
optimizer = tf.train.FtrlOptimizer(self.lr, l1_regularization_strength=self.reg_l1,
l2_regularization_strength=self.reg_l2)
extra_update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
with tf.control_dependencies(extra_update_ops):
self.train_op = optimizer.minimize(self.loss, global_step=self.global_step)
#构建图
def build_graph(self):
self.add_input()
self.inference()
self.add_loss()
self.add_accuracy()
self.train()
本文如有错误的地方,请私信或者留言指出。
欢迎关注微信公众号数据挖掘杂货铺!
参考资料:
https://www.csie.ntu.edu.tw/~b97053/paper/Rendle2010FM.pdf
https://static.googleusercontent.com/media/research.google.com/zh-CN//pubs/archive/41159.pdf
http://wnzhang.net/share/rtb-papers/fm-ftrl.pdf
推荐系统遇上深度学习(一)--FM模型理论和实践 - 云+社区 - 腾讯云