一些前面说明
DeepFM 简述
deepFM 的发展史我们也不多介绍,目前我们也主要用于做 ctr cvr 预测。
deepFM 说起来结构还是比较简单,包含了左边的 FM 和右边的 deep 部分,每个神经元进行了什么操作也在图中表示得很清楚。需要注意的是,图中的连线有红线和黑线的区别,红线表示权重为 1,黑线表示有需要训练的权重连线。
FM(因式分解机) 简述
这里不着重描述 FM 是什么,FM 由如下公式表示(只讨论二阶组合的情况)
同样是线性公式,和 LR 的唯一区别,就在于后面的二次项,该二次项表示各个特征交叉相乘,即相当于我们在机器学习中的组合特征。
FM 的这部分能力,解决了 LR 只能对一阶特征做学习的局限性。
LR 如果要使用组合特征,必须手动做特征组合,这一步需要经验。FM 的二次项可以自动对特征做组合。
同时 FM 的公式可以化为如下,v 表示的就是对应的特征 x 的隐向量。
上面的公式还能进一步转换成
这个公式的优点在于,上一个公式要训练组合权重 w,需要两个组合特征的样本值同时有值才能使 w 得到训练,但是组合特征原本样本就较少,这样的训练方式很难使权重 w 得到充分训练。
通过因式分解机,可以使用一个长度为 k 的隐向量来表达每一个输入的特征值 x,标记为 v,并且通过两个特征的 v 值求内积,其结果可以等同于特征交叉项的权重 w。
通过隐向量 v 表示特征的方式好处是,交叉项不需要保证两个特征均有值才能使 v 得到训练,每一个包含有值特征 x 的样本,都能使之对应的隐向量 v 得到训练。
这里圈一下重点:
样本格式
每个训练样本都会有自己的保存格式,libsvm 或者 tfrecord 或者其他什么形式。
我们的样本格式为:
最后得到的样本形如 1,5,10,3,6,0.5,0.4,100,[5,9,11]。这样的话,线上的 TFserving 除了最后的 [5,9,11] 部分因为是变长,还是必须转换成 one-hot 形式。其余部分线上交给 embedding 层处理,就无需拼接 one-hot 向量输入,节省输入样本长度。
当然如果保存后的样本是上面些的 1,5,10,3,6,0.5,0.4,100,[5,9,11],我们还需要知道每个值是什么特征,维度是多少,以及训练时如何转换成可以使用的样本。
所以需要有一行特征索引和每一条样本的每个值一一对应。假设可以用以下形式存储索引。
1-age-100, 1-gender-3, 2-ads_weight-1, 3-game-50...
对应的每个表示是类型-特征名-维度
左滑查看完整代码,下同
总之只要有一个对应方式,通过查询索引找到特征的信息即可,我们后面的输入样本就需要根据这些信息来转换,并且喂给模型做训练。
后续对于模型的输入,我们根据不同特征定义了对应不同的 Input。所以最后输入的训练格式要注意。训练输入应该长相如下,
train_x = [np.array([...]), np.array([...]), np.array([...])]label = np.array([0, 1, 0 ...])
实现 FM 部分
谈到具体如何实现模型。下图是 deepFM 网络的 FM 部分。
我们看到上图有红色的连线和黑色的连线
按步骤实现,就是需要实现一次项和二次项两部分,然后相加得到 FM 这部分的输出。
FM 的一次项部分
这一部分思路很简单
上述过程可以简单通过代码表达为
continuous = Input(shape=(1, ), name='single_continuous')single_discrete = Input(shape=(1, ), name='single_discrete')multi_discrete = Input(shape=(8, ), name='multi_discrete')continuous_dense = Dense(1)(continuous)single_embedding = Reshape([1])(Embedding(10, 1)(single_discrete))multi_dense = Dense(1)(multi_discrete)first_order_sum = Add()([continuous_dense, single_embedding, multi_dense])
FM 的二次项部分
等价于
这一部分主要做的事情,就是需要得到一个表示各 field 的隐向量,而且不管每个特征 field 长度是多少,最后得到的隐向量长度都为 k(这里 k 由开发者自己指定)。我们来分析一下如何处理这部分。
承接上面的代码,这一部分的代码表示为
continuous_k = Dense(3)(continuous)single_k = Reshape([3])(Embedding(10, 3)(single_discrete))multi_k = Dense(3)(multi_discrete)
最后得到如下图的输出,我们这里假设 k=3,下列每一种类型的 3 位输出,最后 concate 在一起,要作为后面说到的 deep 层的输入。
再看一次上面二次项部分的公式,我们使用
其实相当于两部分内容
这一部分是先相加后平方
这一部分是先平方后相加
从上一张图我们看到一个信息,每个 k=3 的时候,每个输出节点,其实就相当于 Xi*Vil。
多值离散特征的 k=3 的每个输出其实等于 XiVil+XjVjl,因为他还是同一个 field 的多个特征值,为了简化,我们认为这个结果近似等于
所以不管是要先相加后平方,或者先平方后相加,最后等同于上面的 3 个 3维神经元相加起来,对应得到的数就等于求和部分。如下图。
先相加后平方
所以这里每个 k=3 的输出,都是一个 Xi*Vil。先相加后平方的一项,利用 Lambda 层对每个元素做一次平方处理,接上面的代码得到
sum_square_layer = Lambda(lambda x: x**2)(Add()([continuous_k, single_k, multi_k]))
先平方后相加
跟上一步类似,我们得到
continuous_square = Lambda(lambda x:x**2)(continuous_k)single_square = Lambda(lambda x:x**2)(single_k)multi_square = Lambda(lambda x:x**2)(multi_k)square_sum_layer = Add()([continuous_square, single_square, multi_square])
二次项的最后输出
最后结合上面两部分,得到二次项的最后输出为上面两项相减,乘以二分之一后,再对 k=3 的三个值相加。
substract_layer = Lambda(lambda x:x*0.5)(Subtract()([sum_square_layer, square_sum_layer]))# 要实现单层的各个值相加,目前 Keras 似乎没有这样的操作# 可以通过自定义一个简单层来简单实现我们需要的功能class SumLayer(Layer): def __init__(self, **kwargs): super(SumLayer, self).__init__(**kwargs) def call(self, inputs): inputs = K.expand_dims(inputs) return K.sum(inputs, axis=1) def compute_output_shape(self, input_shape): return tuple([input_shape[0], 1])# 最后相加 k 维输出,结果等于second_order_sum = SumLayer()(substract_layer)
我们再回顾一下要注意到最开始的deepFM论文中的原图,FM 部分最后连接到outpu units的 FM 部分,是红色的线,weight-1 connection。
也就是说,FM 部分最后相当于需要把一次项和二次项的输出值相加得到一个单值输出,然后再跟 deep 部分的输出相加,进入 sigmoid 激活函数。所以 FM 部分我们最后的输出为
fm_output = Add()([first_order_sum, second_order_sum])
实现 deep 部分
[ deep部分 ]
deep 部分全是黑色连线,所以实现很简单,只需要把上面 FM 部分的二次项 k 维输出 concate 后作为输入,然后进入几层全连接层,最后得到的单值输出和 FM 部分的单值输出 concate,再经过一次 Dense(1),进入 sigmoid 函数即可。
可以直接看代码如何实现这部分。
deep_input = Concatenate()([continuous_k, single_k, multi_k])deep_layer_0 = Dropout(0.5)(Dense(64, activation='relu')(deep_input))deep_layer_1 = Dropout(0.5)(Dense(64, activation='relu')(deep_layer_0))deep_layer_2 = Dropout(0.5)(Dense(64, activation='relu')(deep_layer_1))deep_output = Dropout(0.5)(Dense(1, activation='relu')(deep_layer_2))
最后输出部分
concat_layer = Concatenate()([fm_output, deep_output])y = Dense(1, activation='sigmoid')(concat_layer)
到此模型的代码就完成了,剩余的就是样本的处理,以及各自如何把样本喂入模型的代码。这些代码应该是根据各自业务,各自样本格式需要做对应处理。
deepFM 结果对比
根据上面的方法实现了模型之后,我们用自己的业务的几份数据集做了离线测试。
在目前我们个业务的几份不同日期的数据集合上测试,得到的AUC 结果如下。
数据集 | deepFM | FM | deep |
---|---|---|---|
A1 | 0.86661 | 0.86581 | 0.85166 |
A2 | 0.86125 | 0.86121 | 0.84789 |
A3 | 0.84842 | 0.84841 | 0.80581 |
A4 | 0.84170 | 0.84083 | 0.82848 |
有一个大致经验,deepFM 在有效特征更多,特征工程处理更好,数据更干净,数据更有区分度的数据集上,得到的对比结果差异会更大。
反之,如果数据特征和 label 本身的关联性不高,数据本身无法很好的区分样本时,对比结果差异会很小。
FM 有时候表现已经和 deepFM 几乎无差别。猜测的原因是数据本身并不需要复杂规则就能得到很好的模型区分,所以比 FM 多出来的这部分 deep 能力显得并不太重要。
同时因为 deep 部分明显效果都不如前两者,所以可能可以验证上一步猜测。
预计需要的是更多在特征工程上做优化,以及挖掘更多有效特征。
最后附上代码demo
以上面的代码为例,附上完整的实现代码。这个demo是直接可运行的。
import numpy as npfrom keras.layers import *from keras.models import Modelfrom keras import backend as Kfrom keras import optimizersfrom keras.engine.topology import Layer# 样本和标签,这里需要对应自己的样本做处理train_x = [ np.array([0.5, 0.7, 0.9]), np.array([2, 4, 6]), np.array([[0, 1, 0, 0, 0, 1, 0, 1], [0, 1, 0, 0, 0, 1, 0, 1], [0, 1, 0, 0, 0, 1, 0, 1]])]label = np.array([0, 1, 0])# 输入定义continuous = Input(shape=(1, ), name='single_continuous')single_discrete = Input(shape=(1, ), name='single_discrete')multi_discrete = Input(shape=(8, ), name='multi_discrete')# FM 一次项部分continuous_dense = Dense(1)(continuous)single_embedding = Reshape([1])(Embedding(10, 1)(single_discrete))multi_dense = Dense(1)(multi_discrete)# 一次项求和first_order_sum = Add()([continuous_dense, single_embedding, multi_dense])# FM 二次项部分 k=3continuous_k = Dense(3)(continuous)single_k = Reshape([3])(Embedding(10, 3)(single_discrete))multi_k = Dense(3)(multi_discrete)# 先相加后平方sum_square_layer = Lambda(lambda x: x**2)( Add()([continuous_k, single_k, multi_k]))# 先平方后相加continuous_square = Lambda(lambda x: x**2)(continuous_k)single_square = Lambda(lambda x: x**2)(single_k)multi_square = Lambda(lambda x: x**2)(multi_k)square_sum_layer = Add()([continuous_square, single_square, multi_square])substract_layer = Lambda(lambda x: x * 0.5)( Subtract()([sum_square_layer, square_sum_layer]))# 定义求和层class SumLayer(Layer): def __init__(self, **kwargs): super(SumLayer, self).__init__(**kwargs) def call(self, inputs): inputs = K.expand_dims(inputs) return K.sum(inputs, axis=1) def compute_output_shape(self, input_shape): return tuple([input_shape[0], 1])# 二次项求和second_order_sum = SumLayer()(substract_layer)# FM 部分输出fm_output = Add()([first_order_sum, second_order_sum])# deep 部分deep_input = Concatenate()([continuous_k, single_k, multi_k])deep_layer_0 = Dropout(0.5)(Dense(64, activation='relu')(deep_input))deep_layer_1 = Dropout(0.5)(Dense(64, activation='relu')(deep_layer_0))deep_layer_2 = Dropout(0.5)(Dense(64, activation='relu')(deep_layer_1))deep_output = Dropout(0.5)(Dense(1, activation='relu')(deep_layer_2))concat_layer = Concatenate()([fm_output, deep_output])y = Dense(1, activation='sigmoid')(concat_layer)model = Model(inputs=[continuous, single_discrete, multi_discrete], outputs=y)Opt = optimizers.Adam( lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=None, decay=0.0, amsgrad=False)model.compile( loss='binary_crossentropy', optimizer=Opt, metrics=['acc'])model.fit( train_x, label, shuffle=True, epochs=1, verbose=1, batch_size=1024, validation_split=None)
参考内容