用Keras实现一个DeepFM
本文仅提供一个思路,有问题欢迎指出,转载请注明,谢谢。
一、数据格式
在设计模型之间,首先要明确数据的格式应该是怎样的。我们假设现在要解决的问题是一个CTR预估问题,数据集是 (X,y) ( X , y ) ,每一个样本都是高度稀疏的高维向量。假设我们有两种 field 的特征,连续型和离散型,连续型 field 一般不做处理沿用原值,离散型一般会做One-hot编码。离散型又能进一步分为单值型和多值型,单值型在Onehot后的稀疏向量中,只有一个特征为1,其余都是0,而多值型在Onehot后,有多于1个特征为1,其余是0。
下面给出一个两个样本的例子,其中shop_score
是连续型field,gender
是单值离散型field,interest
是多值离散型field。可以看到shop_score
的取值是实数,gender
的取值是离散值,interest
的取值是离散值序列。
label | shop_score | gender | interest |
---|---|---|---|
0 | 0.2 | male | football, cooking |
1 | 0.8 | female | cooking |
对各field进行Onehot后,可见单值离散field对应的独热向量只有一位取1,而多值离散field对应的独热向量有多于一位取1,表示该field可以同时取多个特征值。
label | shop_score | gender=m | gender=f | interest=f | interest=c |
---|---|---|---|---|---|
0 | 0.2 | 1 | 0 | 1 | 1 |
1 | 0.8 | 0 | 1 | 0 | 1 |
进一步,我们对每个field中的特征取值分别单独编码或联合编码,则确定了特征的index,这在libsvm和libffm数据格式中是需要的。
field | feature | encoding separate | encoding union |
---|---|---|---|
shop_score (1) | 1 | 1 | |
gender (2) | male | 1 | 2 |
gender (2) | female | 2 | 3 |
interest (3) | football | 1 | 4 |
interest (3) | cooking | 2 | 5 |
libsvm格式:
libffm格式:
可见,连续field和单值field对样本长度的贡献恒定为1,但多值离散型field可能会导致样本长度不一样。对不定长样本的处理方法自然是padding补零了,但我选择对每个多值field分别进行padding,原因有二。首先,若对样本整体进行padding,万一想要进行截断,可能会截掉某些连续field和单值field,分别padding则可以分别截断,而不影响其他的field。第二,对每个field的不同特征单独编码互不影响,不需要维护一个全局的字典,每次只需要处理一个field的特征,甚至可以实现并行处理以及节省内存的特征Encoding方案。
FM所需的数据格式正是libsvm格式,既需要数值本身(Value),也需要特征取值在字典中的index(ID)。假如我们采用对每个field的不同特征取值单独编码的方式,则可以实现一些简便性优化。首先,数值型field的ID永远是1,因此可以省略ID;第二,单值离散型field的Value永远是1,因此可以省略Value;第三,多值离散型field可以用padding+masking的方式省略ID。
给每个field分配ID和Value时,为了用0做padding,ID编码需要从1开始。如下所示,shop_score
作为连续型特征,每个样本的ID和Value列表长度都是1,所有样本共用同一ID,而所有样本的Value保持原值;gender
作为单值离散型field,每个样本的ID和Value列表长度都是1,ID是编码后的特征编号,由于是离散型,Value全是1;interest
作为多值离散型field,ID和Value列表的长度应该取该field的最长长度,第一个样本的interest
field长度是2,因此两个样本的ID和Value列表长度都应padding补零到定长2,每个样本的ID列表是各特征取值的编码值,而Value在ID的非零位置上取1。
ID_shop_score = [[1], [1]] # 多余,可省略
Value_shop_score = [[0.2], [0.8]]
ID_gender = [[1], [2]]
Value_gender = [[1], [1]] # 多余,可省略
ID_interest = [[1,2], [2,0]]
Value_gender = [[1,1], [1,0]] # 多余,可省略
根据上面给出的规则,我对各种field提取ID和Value提供参考方法如下:
- 连续型field
- ID:
np.ones()
或舍弃 - Value : 沿用原 ndarray
- ID:
- 单值离散型field
- ID:
sklearn.preprocessing.LabelEncoder()
- Value:
np.ones()
或舍弃
- ID:
- 多值离散型field
- ID:
sklearn.preprocessing.LabelEncoder()
+ padding + 加一 - Value:
np.ones()
+ padding 或舍弃
- ID:
二、一个DeepFM需要哪些模块
在动手写代码之前,先要对模型结构做一个宏观地观察,看看具体要实现哪些模块。上图是DeepFM论文中给出的整体网络结构图,可见要实现一个DeepFM,实现两个部分即可:FM部分和DNN部分,FM又可以进一步分为一次项和二次项。
从根源上,DeepFM的各模块共享同一输入,输入是由各个field的Onehot编码横向拼接而成的高维稀疏向量。首先,原始输入的各个field经过加权(实际上是Embedding为1维)后,求和可得一次项;其次,原始输入的各个field(不同长度)的Embedding(等长, k k 维latent vector),一方面两两内积,然后求和可得二次项,另一方面作为输入全连接到DNN。