在CTR和CVR预估任务中,可能有大量的ID类特征(Categorical Feature),一般来说并不适合直接送入树模型(xgboost完全不支持,lightgbm只根据取值不同),一种常用的做法是通过Label Encoding变成离散的稀疏的高维向量(最具代表的是Onehot独热编码),然后送入LR训练。在独热编码的作用下,产生了多项式模型参数学习困难的问题。
因子分解机 Factorization Machine(FM)解决了稀疏特征下的参数学习问题,实现了树模型很难做到的特征交叉(树模型需要手动提取交叉特征),而且可以不考虑0特征以加速训练,对稀疏严重的数据有着较好的相性,因此是LR以外的一个不错的选择。常见的使用方法是Onehot特征直接送入FM,或者经过其他模型输出的离散高维特征(比如提升树模型输出的叶子序号的序列)送入FM做stacking。本文参考了其他的文献,对FM、FFM以及相应的实现包括libffm和xlearn做出基本的介绍。文章以后仍会继续完善,尤其是libffm和xlearn还需要进一步follow代码。
考虑一个模型,它的输出由单特征( d d 维)与组合特征的线性组合构成,如果不看二次项,这就是一个线性回归模型,现在引入了交叉项。
现在假设目标函数是 L(y,f(x)) L ( y , f ( x ) ) ,为了使用梯度下降法训练交叉项参数,需要求导:
将矩阵 W={wi,j} W = { w i , j } 矩阵(这是一个对称方阵)分解成 W=VTV W = V T V 的形式,其中 V=(v1,v2,⋯,vd) V = ( v 1 , v 2 , ⋯ , v d ) 是一个 k×d k × d 矩阵,且 k≪d k ≪ d ,于是 W W 矩阵的每一个元素都可以用 V V 矩阵对应的两列做内积得到: wij=vi⋅vj w i j = v i ⋅ v j ,同时多项式模型可以重写,这就是因子分解机模型。
经过因子化之后,组合特征 xixj x i x j 和 xjxk x j x k 的系数 (vi⋅vj) ( v i ⋅ v j ) 与 (vj⋅vk) ( v j ⋅ v k ) 不再独立,他们共有了 vj v j ,因此所有包含 xj x j 特征的非零组合特征的样本都能拿来训练。这是什么意思呢?现在,如果只看交叉项(不管用什么loss,根据链式法则我们总需要乘上 ∂f(x)∂wij ∂ f ( x ) ∂ w i j ):
FM模型方程似乎是通用的,根据任务不同,使用不同的loss。比如,回归问题用MSE,分类问题先取sigmoid或者softmax,然后用cross-entropy,比较灵活。
在FM的基础上,进一步提出 field 的概念。一般来说,同一个ID类特征进行Onehot而产生的所有特征都可以归为同一个 field。在FFM中,对每一个特征 xi x i ,每一个field fj f j ,学习一个隐向量 vi,fj v i , f j ,不同的特征跟同一个 field 进行关联时使用不同的隐向量。假设总共有 d d 个特征,属于 f f 个field,那么每个特征都用 f f 个隐向量来描述,所以总共有 d×f d × f 个隐向量。而FM中,一个特征只有一个隐向量,所以FM可以看成FFM中所有特征都属于同一个 field 的特例。
为了更直观地理解FFM的计算,下面给出一个例子。
Clicked | User(U) | Advertizer(A) | Gender(G) |
---|---|---|---|
0 | Arthur | Lancelot | Male |
1 | Arthur | Guinevere | Male |
One-Hot编码转换,其中等于其他情况的列都是0,省略掉了。
Clicked | U=Arthur | A=Lancelot | A=Guinevere | G=Male |
---|---|---|---|---|
0 | 1 | 1 | 0 | 1 |
1 | 1 | 0 | 1 | 1 |
这么看不够直观,将特征和对应的field映射成整数编号。
field name | field index | feature name | feature index |
---|---|---|---|
User | 1 | U=Arthur | 1 |
Advertizer | 2 | A=Lancelot | 2 |
A=Guinevere | 3 | ||
Gender | 3 | G=Male | 4 |
第一个instance的FFM的组合项有6项,注意 vi,fj v i , f j 下标的含义,就很好懂了。由于存在部分0,最后实际上只有3项。
下面这个算法流程摘自美团-深入FFM原理与实践,实际上正是libffm的实现,具体的介绍可以在ffm论文里找到。首先对数据逐列归一化,然后进行迭代,每次迭代计算梯度并更新参数。
libffm模型省略了常数和一次项,将FFM目标简化为下面这个形式。
如果用SGD进行更新,具体在计算梯度的时候有一点trick,由于链式法则:
根据论文的描述,学习率更新使用了AdaGrad(但据我所知libffm默认是用sgd的,xlearn默认是用AdaGrad的),学习率分母用历次迭代梯度和代替,前期加速,后期缓和,且保证每个参数的学习率都不同,每个参数既能较快收敛,又不容易震荡。
libffm 数据格式如下:
Clicked | User(U) | Advertizer(A) | Price(P) |
---|---|---|---|
0 | Arthur | Lancelot | 9.99 |
1 | Arthur | Guinevere | 9.99 |
那么这两条样本应该处理成
然后我们要对feature进行编码,编码的方式多种多样,可以直接根据feature取值构造字典(vocabulary),也可以使用哈希函数转换字符串。
例如直接构造字典如下。值得注意的是,只有category类特征需要对不同的特征取值进行编码,数值型特征共享同一编码即可。
vocabulary = {"U=Arthur":1,
"A=Lancelot":2,
"A=Guinevere":3,
"price":4} # 数值型特征共享同一编码
下面列出编码后的结果:
field name | field index | feature name | feature index |
---|---|---|---|
User | 1 | U=Arthur | 1 |
Advertizer | 2 | A=Lancelot | 2 |
A=Guinevere | 3 | ||
Price | 3 | Price | 4 |
根据以上编码结果,我们的样本变成这样:
另外提供一个hash转换函数
def hashstr(str, nr_bins=1e+6):
return int(hashlib.md5(str.encode('utf8')).hexdigest(), 16)%(nr_bins-1)+1
libffm有个参数是pa.norm,默认对样本归一化,如果不这样做可能会导致计算的梯度太大而出现inf的溢出。
当特征中同时含有数值类和ID类时,例如某数值特征a取值为10000,ID类特征b取值是1,做样本归一化后,a=0.9999,b=9.999E-5,就会导致ID类特征没有贡献。因此要对每一列特征先做归一化。
上面的输入其实应该是这样的:
xlearn其实不止支持ffm,还支持LR和FM。
pip安装即可
sudo pip install xlearn
git clone后自己编译
git clone https://github.com/aksnzhy/xlearn.git
cd xlearn
./build.sh
import xlearn as xl
具体使用可参考 ./demo
build/xlearn_train
./xlearn_train train_set -m model
build/xlearn_predict
./xlearn_predict test_set model -o output
-m
-t
# 默认输出模型是 train_set + ".model" 文件
./xlearn_train train_set
# 指定输出模型,就会输出一个 xlearn_model 文件
./xlearn_train train_set -m xlearn_model
# 指定输出模型到txt
./xlearn_train train_set -t xlearn_model.txt
支持GLM(LR),FM和FFM,三种算法对数据有所要求。LR和FM支持libsvm和csv输入格式,libffm格式会被处理成libsvm格式;FFM只支持libffm格式。
:
label index_1:value_1 index_2:value_2 ... index_n:value_n
:
value_1 value_2 .. value_n label
:
label field_1:index_1:value_1 field_2:index_2:value_2 ...
-s 0/1/2
./xlearn_train train_set -s 0 # Linear model
./xlearn_train train_set -s 1 # Factorization machine (FM)
./xlearn_train train_set -s 2 # Field-awre factorization machine (FFM)
-v
./xlearn_train train_set -v test_set
分类问题支持accuracy、precision、F1和AUC
-x acc/prec/f1/auc
./xlearn_train train_set -v test_set -x acc
./xlearn_train train_set -v test_set -x prec
./xlearn_train train_set -v test_set -x f1
./xlearn_train train_set -v test_set -x auc
回归问题支持mae、mape和rmsd(rmse)
./xlearn_train train_set -v test_set -x mae
./xlearn_train train_set -v test_set -x rmsd
默认是5折,可以用-f改变折数
-f
–cv
./xlearn_train train_set -f 3 --cv # 3折交叉验证
支持sgd、adagrad和FTRL
-p
./xlearn_train train_set -p sgd
./xlearn_train train_set -p adagrad
./xlearn_train train_set -p ftrl
-r
./xlearn_train train_set -r 0.01
./xlearn_train train_set -b 0.01
-alpha
-beta
-lambda_1
-lambda_2
./xlearn_train train_set -p ftrl -alpha 0.002 -beta 0.8 -lambda_1 0.001 -lambda_2 1.0
-k
./xlearn_train train_set -s 2 -k 4
-u
./xlearn_train train_set -s 2 -u 0.1
-e
./xlearn_train train_set -e 10 # 训练10个epoch
EarlyStopping是默认选项,但是可以设置不用它
–dis-es
./xlearn_train train_set -e 10 --dis-es
归一化是默认选项,但是可以设置不用它
–no-norm
./xlearn_train train_set --no-norm
训练时不会计算指标,可以加速
–quiet
./xlearn_train train_set --quiet
–sigmoid
–sign
$ 输出概率
./xlearn_predict test_set model --sigmoid
$ 输出0和1
./xlearn_predict test_set model --sigmoid
-o
$ 指定输出文件
./xlearn_predict test_set model --sigmoid -o submission.txt
xlearn支持三种算法LR、FM和FFM,同时支持三种输入数据格式CSV、libsvm和libffm。其中LR和FM支持CSV和libsvm格式,但是FFM只支持libffm格式。LR和FM当然也支持libffm,只不过它会被当成libsvm格式,即field字段无效。
值得注意的是,一般测试集test是没有label的,但仍然需要添加一列label做占位符(全部设-1或0都可以),否则parser会把第一列数据当成Label的。
libsvm format:
CSV format:
libffm format:
我最喜欢xlearn的一点是,方便python调用,不得不佩服这些造轮子的人。具体的使用参考这里。
支持分类和回归,更像是libffm的改进版,在mushroom数据集上比libffm更快,准确率更高(训练集和测试集都是),libffm不支持直接python调用,xlearn则支持。
根据github上的描述,xlearn支持外存训练(out-of-core),可以并行化。
输入是稀疏的高维特征,黑色线是带权值的,将每一维输入直接连接到黄色的带“+”号的结点,这部分是FM中的常数和一次项,相当于LR。另一方面,FM中的二次项相当于首先对输入做了某种Embedding,变成稠密的向量,然后这些向量进行内积,即图中褐色结点,红色线上权值恒为1,不可训练。Embedding涉及到因子 vi v i ,它们是可训练的。最终一次项和二次项被加起来,送给sigmoid输出概率。
举个例子,field i 是某个原始特征(比如性别=男)onehot后的稀疏特征: [1,0,0] [ 1 , 0 , 0 ] ,让 xi=1 x i = 1 ,我们知道这个field中其他特征都完全没用,因为它们都是0。一方面, xi x i 参与了一次项的计算:
因此,对于 m m 个field,共 d d 维特征,实际上参与训练的参数包括如下几方面:
那么,FM输入格式应该怎么对应到这个网络图上呢?先回顾一下FM格式,以及上面给出的一个实例(省略0)。
Clicked | U=Arthur | A=Lancelot | A=Guinevere | G=Male |
---|---|---|---|---|
0 | 1 | 1 | 0 | 1 |
首先设定隐向量长度 k k ,然后为每个 index 初始化向量 vi v i ,这里 index 最大为4,因此 v v 矩阵的大小为 4×k 4 × k 。接着,对于每个特征 xi x i 选取 v v 矩阵对应的 indexi i n d e x i 行向量 vi v i ,做Embedding得到 xivi x i v i 。接下来的步骤不再赘述。
训练的时候,根据样本中每一个 index 取出 v v 矩阵中的某一行,经过FM前向计算得到loss,然后梯度下降更新参数。从这里可以看出,只要存在 xi≠0 x i ≠ 0 的样本, vi v i 就能够得到训练。
有时候我们会遇到一些特殊的离散特征,在一个样本中该特征会取多个值,例如下面这个例子,我们的劳模Arthur喜欢的水果有四种,如果做onehot的话,Like特征就不像上面介绍的那些特征那样只有一个位置上有1,而是有4个位置有1,其余是0。
Clicked | User | Like |
---|---|---|
0 | Arthur | Apple,Banana,Orange,Grapes |
如果我们按照libsvm格式处理样本,让“User=Arthur”特征编码为1,“Like=Apple”到“Like=Grapes”分别编码为2到5,注意虽然2到5特征编码不同,但它们是属于同一个field的(在FFM的概念里)。我们能得到样本:
可以发现其实没有任何区别,只不过因为FM里面没有field的概念,所以看起来好像是把Like这个特征分成了4个不同的Onehot field,每个field只有一个位置取值为1.