1 引言
Wide & Deep的提出,使推荐模型同时具备记忆和泛化能力。通过融合低阶和高阶特征交叉,开启了推荐算法异构模型的风潮。后续越来越多的模型,在其基础上进一步优化,并取得了不错的效果。DeepFM就是其中一个很经典的模型,它主要对Wide侧进行优化。
DeepFM全称“ DeepFM: A Factorization-Machine based Neural Network for CTR Prediction”[7]。由哈尔滨工业大学和华为于2017年联合提出。它对Wide & Deep模型的优化主要有:
- 将Wide侧从LR升级为FM,从而增加了二阶特征自动交叉能力。Wide & Deep模型的Wide侧为逻辑回归,本身并不具备二阶特征交叉能力,需要手工构造交叉特征。DeepFM通过因子分解机的二阶特征自动交叉能力,减少对特征工程的依赖,从而降低了人力成本。
- 让Wide和Deep两侧共享原始输入特征和Embedding特征向量,从而加快模型收敛速度,并进一步提升模型表达能力。
2 DeepFM模型结构
DeepFM模型包括两部分,Wide侧采用因子分解机(FM),Deep侧采用深度神经网络(DNN)。通过一层线性连接融合两部分,并经过sigmoid激活函数最终输出,如公式4-17所示。模型为点击率(CTR)预估任务,目标函数仍然采用LogLoss。利用联合训练使两部分同时达到最优。两部分的优化器均采用Adam算法。
模型整体结构如图4-8所示。其中左边为FM,右边为DNN,二者共享原始输入特征和Embedding向量。FM负责低阶特征交叉(一阶和二阶),拥有较强的记忆能力。DNN负责高阶特征交叉,拥有较强的泛化能力。
先来看FM部分。它主要包括两部分:一阶特征交叉和二阶特征交叉。一阶部分对输入特征进行线性求和(Addition),表达特征间“或”的关系。二阶部分对输入特征的隐向量进行内积(Inner Product)计算,表达特征间“且”的关系。FM计算如公式4-18所示。
在推荐场景下,特征间“且”的关系显然更为重要。这一点是PNN模型提出的核心出发点,在4.4.3章节中已经详细阐述过。另外对于训练数据中没有出现过的特征组合,一阶特征交叉无法表达特征间相关关系,而二阶特征交叉,则可以通过隐向量实现。因此,二阶特征交叉相对来说更重要。
DNN部分模型结构和Wide & Deep模型比较类似,仍然采用经典的“Embedding + MLP”结构,不再赘述。主要区别为,DNN与FM两部分,共享了原始输入特征和Embedding向量。这样做有两大好处:
- 相当于加入了某种意义上的正则,使得Embedding向量需要同时在低阶和高阶特征交叉中表现良好,有利于提升模型表达能力和鲁棒性
- 降低了模型参数量和计算复杂度,使得模型训练速度更快。利用FM隐向量进行二阶特征交叉,代替手工构造的二阶交叉特征,对这一点帮助也很大。
同时需要指出的是,DeepFM中FM和DNN两部分共享的Embedding向量参数,可以通过端到端训练学习。这一点与FNN模型有很大差别。FNN需要先通过FM预训练Embedding向量,然后作为DNN模型的初始化参数。这样一方面使得模型整体性能高度依赖于FM预训练质量,另一方面FM和DNN两部分是脱节的,不能端到端训练。同时由于需要提前得到预训练模型,加大了整体计算复杂度和训练时长。DeepFM的这一优化同样十分重要,千万不要把DeepFM简单理解为,只是将LR替换为了FM。
3 DeepFM实现代码
DeepFM由FM和DNN两部分组成,FM又包括一阶和二阶特征交叉。下面分模块分别实现。代码基于Keras深度学习库实现,其中Keras版本为2.10。先定义FM一阶特征交叉函数。一阶特征交叉,也就是LR,与Embedding计算类似。故可利用Embedding来实现。注意Embedding的输入维度可根据实际场景具体值来确定,输出维度为1。最后将所有Embedding求和即可。
from tensorflow.keras.layers import Layer, Dense, Embedding, Concatenate, Reshape, Add, Subtract, Lambda
from tensorflow.keras import backend as K
def build_fm_first_order(inputs):
"""
构建FM一阶部分,也就是LR逻辑回归
@param inputs: 输入特征,包括类别型和连续型特征。连续型特征会经过分桶离散化
@return: 返回一阶特征交叉结果
"""
embeddings = []
for x in inputs:
# 遍历每个特征,进行Embedding,相当于做 wx + b计算
# 下面代码中,假设了输入特征5000维,具体场景根据实际值来确定。对于逻辑回归,输出为1维
embedding = Embedding(input_dim=5000, output_dim=1)(x)
embedding = Reshape(target_shape=(1,))(embedding)
embeddings.append(embedding)
# 加权求和即得到一阶部分的结果
return Add()(embeddings)
二阶特征交叉为内积计算,实现相对复杂。需要先化简一下,推导如公式4-19所示。
特别需要注意的是,FM和DNN的Embedding是共享的。先把各特征的Embedding向量的相同位置相加,相当于做求和池化(sum pooling),再对池化后的向量逐点取平方。然后对每个Embedding先逐点平方,再把他们加起来。最后两部分向量逐点相减,并将相减后得到的向量各元素累加起来,得到一个标量,即为二阶交叉的结果。实现代码如下。
# 定义求和层
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])
def build_fm_second_order(embeddings):
"""
构建FM二阶部分
@param embeddings: 输入特征embedding拼接后的高维向量,与DNN部分是共享的
@return: 返回二阶特征交叉结果
"""
# 先相加后平方
sum_square_result = Lambda(lambda x: x ** 2)(Add()(embeddings))
# 先平方后相加
square_sum_result = Add()([Lambda(lambda x: x ** 2)(emb) for emb in embeddings])
# 两部分相减
substract_result = Lambda(lambda x: x * 0.5)(Subtract()([sum_square_result, square_sum_result]))
# 最后通过求和层输出
return SumLayer()(substract_result)
DNN层的实现则比较简单,下面例子构建了三层全连接神经网络,神经元个数分别为1024、512、256。激活函数为ReLU。可根据实际情况修改DNN层数和每层神经元个数。
def build_dnn(embeddings):
"""
构建DNN部分
@param embeddings: 输入特征embedding拼接后的高维向量,与FM部分是共享的
@return: 返回DNN结果
"""
# 此处为三层全连接神经网络,神经元个数分别为1024、512、256。可根据实际情况修改
x_deep = Dense(units=1024, activation="relu")(embeddings)
x_deep = Dense(units=512, activation="relu")(x_deep)
deep_out = Dense(units=256, activation="relu")(x_deep)
return deep_out
各模块实现好之后,开始构建DeepFM整体结构。流程分为4步:
- 先构建FM一阶部分,调用对应模块函数即可
- 再构建FM二阶部分。由于FM与DNN共享Embedding,故先构建特征向量,每个特征向量长度相同。代码中假设输入特征为5000维,可根据具体场景实际值来修改。
- 再构建DNN部分,调用对应模块函数即可
- 最后是融合层。先融合FM的一阶部分和二阶部分,再将FM和DNN通过一层线性连接融合起来,最后通过Sigmoid函数归一化为0到1之间。
def build_deep_fm(inputs):
"""
构建FM模型
@param inputs: 输入特征,包括类别型和连续型特征。连续型特征会经过分桶离散化
@return: 返回FM模型输出结果
"""
# 1 构建fm一阶部分
fm_first_order = build_fm_first_order(inputs)
# 2 构建fm二阶部分
# 2.1 先构建隐向量,后面DNN也会用到。二者是共享的
emb_size = 32
embeddings = []
for x in inputs:
# 遍历每个特征,进行Embedding
# 下面代码中,假设了输入特征5000维,具体场景根据实际值来修改。
embedding = Embedding(input_dim=5000, output_dim=emb_size)(x)
embedding = Reshape(target_shape=(emb_size,))(embedding)
embeddings.append(embedding)
fm_second_order = build_fm_second_order(embeddings)
# 3 构建dnn,注意此处embedding与FM是共享的
dnn_output = build_dnn(embeddings)
# 4 融合得到最终结果
# 4.1 先融合FM的一阶部分和二阶部分
fm_output = Add()([fm_first_order, fm_second_order])
# 4.2 再融合FM和DNN两部分
output = Dense(units=1, activation="sigmoid")([fm_output, dnn_output])
return output
4 DeepFM总结
DeepFM通过对Wide侧优化,提升了异构模型表达能力,是推荐算法中的经典模型。虽然早在2017年就提出,距今已经很多年,但仍然广泛应用于各大推荐场景。与Deep Crossing、FNN、PNN、Wide & Deep等模型相比,它的优势十分明显,主要有:
- 不需要手工构造交叉特征,降低了对特征工程的依赖。相比之下,Wide & Deep仍然需要。
- 模型可同时进行低阶和高阶特征交叉,兼顾了记忆和泛化两大能力。相比之下,Deep Crossing、FNN、PNN等模型则缺失了低阶特征交叉能力。
可以端到端联合训练FM和DNN两部分模型参数,从而降低计算复杂度并提升模型表达能力。相比之下,FNN需要先预训练FM,然后再训练DNN,无法端到端学习。
5 作者新书推荐
历经两年多,花费不少心血,终于撰写完成了这部新书。本文在4.6节中重点阐述了。
源代码:扫描图书封底二维码,进入读者群,群公告中有代码下载方式
微信群:图书封底有读者微信群,作者也在群里,任何技术、offer选择和职业规划的问题,都可以咨询。
详细介绍和全书目录,详见
精通推荐算法,限时半价,半日达https://u.jd.com/VbCJsCz