文章目录
写在前面:
FM算法被用来解决解决稀疏数据下的特征组合问题.它的改进有不少.[1]中提出了基于FM的PITF(pairwise interaction tensor factorization)方法做个性化标签推荐.在2012年KDD杯中,PITF的通用版本factor model(论文称为FFMs)[2]被提了出来.而作者认为FFMs只考虑了"用户","物品"和"标签"三个特征等一系列问题,而提出了更加通用的FFMs版本FFM(Field-aware Factorization Machines)[3].
FFM论文:https://arxiv.org/abs/1701.04099
FFMppt:https://www.csie.ntu.edu.tw/~r01922136/slides/ffm.pdf
FFM第一作者分享的c++开源工具:https://github.com/ycjuan/libffm
1.FFM基本原理
推荐中的数据one-hot编码后得到的稀疏数据一般如上图所示.每一行是一个样本,每一列就是一个特征,一个样本有多少维,就有多少个特征;维度分为几类(User, Movie, Time…),就有几个field(当然field可以自己定义,不按照样本特征原先的类来).首先回顾一下FM中对特征交叉的处理:
而FFM做了如下改进:
f2就是j2所属的那个field,就是one-hot编码前这个特征所在的类.看出来了吧,就是将原先的wj1更加细化,原先的xj1只对应一个隐向量wj1,现在隐向量要看xj2所在的field类进行选择,参数量增加了.
当然了,当xj1和xj2同时不为0时的特征交叉才有意义.代码中也可以根据这个进行优化.
我们结合ppt看个例子:
一目了然.红字代表所在的field,蓝字代表特征,绿字代表特征的值,这里第三个field中两个特征同时存在,因此均为1.而FM对这些特征的交叉处理就是:
C是xxj,xj2均不为0的二阶组合,<>是两个向量点击后求和.每个特征i都一个对应的隐向量wi.总共有w1,w2,w3,w4,w5五个隐向量,FM全用到了.
FFM的处理如下:
很明显,
隐向量w1–>w11,w12,w13,w14,
隐向量w2–>w21,w22,w23,w24
…
向量原先有5个,现在共有5x4=20个.这个样本只用到了15个.
FFM第一作者分享的开源工具[6]只实现了之前展示的特征交叉项,本文的实验部分会把常数项和一次项加上(下面这个公式剪自[7],xi,xj同上面的xj1,xj2,都是指一个样本的不同特征的值):
2.注意事项
2.1 省略零值特征
零值特征相乘对结果无影响,实现过程中可以直接省略.
2.2 样本归一化
[8] 中指出,FFM默认是进行样本数据的归一化,否则很容易造成数据inf溢出.因此,样本层面的数据是推荐进行归一化的.
2.3 特征归一化
category类的特征one-hot编码中只取0或1,而continuous类的特征如果不作处理(比如10000),两者量值差距过大,很有可能因为量值差异造成不同特征对模型影响差距很大.
实际上,对于离散特征xi,其相关的隐向量wifj由其他所有非零的xj共同影响,当不同的xj之间量值差异很大,xifj很难同时照顾到"离群点xj"和其他xj.可能因此增加continuous类特征对模型影响,这是不合理的.
2.4 隐向量维度
由于和每个特征xi有关的隐向量通过field进一步细化,因此隐向量的维度相比FM的维度少了一些.论文中FFM取40和100维,FFM只用取4维:
论文中还指出,隐向量维度k值对结果影响不是很大,这也说明了FFM的k值不需要过大:
2.5 参数量
假设样本特征有n维,共有f个field,每个隐向量有k维.那么交叉项的参数量就是nfk.FM中交叉项参数量是nk.但是如2.4所说,FFM的隐向量维度更低.比如FM可能是几十或几百维,FFM就几维或几十维.但当field过多时,还是FFM参数多(一般是这样),而[3]也证明其效果更好.
2.6 时间复杂度
在FM那篇文章中我们分析过,可以使用多重求和的一些公式将交叉项进行优化,将时间复杂度由O(kn2})降到O(kn).这里显然无法做类似优化.因此特征交叉这一部分的时间复杂度就是O(kn2).所以,同样使用SGD,FFM训练时间一般更长(原代码中还有个FM,两者一比,FFM确实慢了不少).
2.7 SGD寻优小技巧
[8] 中讲了几个优化SGD寻优的小技巧,包括梯度分步计算,自适应学习率,OpenMP多核并行计算,SSE3指令并行编程.先保存下来,回头慢慢看(并不会).
3. tensorflow实现
首先看看这个类怎么实现的,里面有三个方法.不到60行代码.
数据的field_num=6,试验中使用了6类特征,5类离散特征,1类连续特征.离散特征进行one-hot编码,离散特征直接归一化.共有
24个特征,所以feature_nums=24,隐向量维度embedding_size=3.预测函数分为线性项(常数项和一次项)和交叉项:
class FFM(object):
# field_nums: F = 6
# feature_nums: N = 24
# embedding_size: k= 3
def __init__(self, hparams, df_i, df_v):
self.hparams = hparams
tf.set_random_seed(self.hparams.seed)
self.line_result = self.line_section(df_i, df_v)
self.fm_result = self.fm_section(df_i, df_v)
print(self.line_result, self.fm_result)
self.logits = self.line_result+self.fm_result
线性项中使用到了df_i,shape为(?, 6), 以及df_v,shape为(?, 6).
样本的六个属性有24个不同的值,5个离散属性23个,1个连续属性1个,所以one-hot编码时共有24个特征.df_i记录了每个样本的六个属性中在这个排列中的id,取值区间为[0,24).对于离散属性,df_v取值为1,否则取值为特征归一化后的值.所以df_v是什么样的呢?就是类似这样的:
样本有6个属性,都只取一个值,所以每个样本有6个非零值.正如文中所说,计算时可只取非零特征值计算进行优化,所以我们看到的df_i和df_v都只有6维,而不是24维.
def line_section(self, df_i, df_v):
'''
# df_i records the positon of i_th feature in 24
# df_v records the value of i_th feature
'''
with tf.variable_scope('line'):
weights = tf.get_variable('weights',
shape=[self.hparams.feature_nums, 1],
dtype=tf.float32,
initializer=tf.initializers.glorot_uniform())
batch_weights = tf.nn.embedding_lookup(weights, df_i) # (*, 6, 1)
batch_weights = tf.squeeze(batch_weights, axis=2) # remove dimensions of size 1 ==> (*, 6)
line_result = tf.multiply(df_v, batch_weights, name='line_w_x')
biase = tf.get_variable('biase',
shape=[1,1],
dtype=tf.float32,
initializer=tf.initializers.zeros())
line_result = tf.add(tf.reduce_mean(line_result, axis=1, keepdims=True), biase) # (*, 1)
return line_result
看完了常数项和一次项,再看看交叉项,懂了原理后,代码一看就清楚了:
def fm_section(self, df_i, df_v):
with tf.variable_scope('fm'):
embedding = tf.get_variable('embedding',
shape=[self.hparams.field_nums,
self.hparams.feature_nums,
self.hparams.embedding_size],
dtype=tf.float32,
initializer=tf.initializers.random_normal())
fm_result = None
for i in range(self.hparams.field_nums):
for j in range(i+1, self.hparams.field_nums):
vi_fj = tf.nn.embedding_lookup(embedding[j], df_i[:, i]) # (*, k)
vj_fi = tf.nn.embedding_lookup(embedding[i], df_i[:, j]) # (*, k)
wij = tf.reduce_sum(tf.multiply(vi_fj, vj_fi), axis=1, keepdims=True) # (*, 1)
x_i = tf.expand_dims(df_v[:, i], 1) # (*, 1)
x_j = tf.expand_dims(df_v[:, j], 1) # (*, 1)
xij = tf.multiply(x_i, x_j) # (*, 1)
if(fm_result == None):
fm_result = tf.multiply(wij, xij) # (*, 1)
else:
fm_result += tf.multiply(wij, xij)
return fm_result
感兴趣可以跑一下完整代码,有数据集:
https://github.com/wyl6/Recommender-Systems-Samples/tree/master/RecSys%20Traditional/MF/FFM
参考
[1] Rendle, S., & Schmidt-Thieme, L. (2010, February). Pairwise interaction tensor factorization for personalized tag recommendation. In Proceedings of the third ACM international conference on Web search and data mining (pp. 81-90). ACM.
[2] Jahrer, M., Toscher, A., Lee, J. Y., Deng, J., Zhang, H., & Spoelstra, J. (2012, August). Ensemble of collaborative filtering and feature engineered models for click through rate prediction. In KDDCup Workshop.
[3] Juan, Y., Lefortier, D., & Chapelle, O. (2017, April). Field-aware factorization machines in a real-world online advertising system. In Proceedings of the 26th International Conference on World Wide Web Companion (pp. 680-688). International World Wide Web Conferences Steering Committee.
[4] Rendle, S. (2010, December). Factorization machines. In 2010 IEEE International Conference on Data Mining (pp. 995-1000). IEEE.
[5] https://www.csie.ntu.edu.tw/~r01922136/slides/ffm.pdf
[6] https://github.com/ycjuan/libffm
[7] https://www.cnblogs.com/zhangchaoyang/articles/8157893.html
[8] https://tech.meituan.com/2016/03/03/deep-understanding-of-ffm-principles-and-practices.html
[9] https://www.cnblogs.com/zhangchaoyang/articles/8157893.html
公众号
更多精彩内容请移步公众号:推荐算法工程师
