之前讲到过常用的 CTR 模型类似 WideAndDeep,其中包含了 LR 与 DNN,LR主要着重于挖掘一阶特征,DNN主要挖掘特征高阶交叉,FM模型则主要针对与模型的二阶交叉,下面看下 FM 的原理以及如何通过 Keras 自定义层实现 FM Layer,有了 FM,后续 DeepFM 也更好的引入。
下面是经典的 FM 实现流程图,Filed i,..,m 代表m个特征域,FM 会对每个特征域内的特征生成一个 K 维的隐向量,随后通过 Addition 相加操作实现 LR 的一阶交叉,通过 Inner Product 内积操作实现特征之间的二阶交叉,这里 bias 偏置可以根据自己的情况添加,最后将 Addtion 与 InnerProduct 得到结果进行合并输出到最后的 Dense 层,这里激活函数采用 Sigmoid,因为 FM 模型的强项是处理 CTR 相关问题,所以我们需要一个 0-1 之间的概率。
有了上面的简介,这个公式也就很好理解了,前面两个部分 w0 + ∑ wi·xi 代表线性部分即 LR,后面 ∑∑(vi · vj)xi·xj 代表二阶交叉部分,与传统的 POLY2 模型相比,FM 通过引入隐向量 vi,vj 代替了 w_ij ,从而把模型参数由 n^2 优化至 nk (k为隐向量维度,n >> k),在使用梯度下降训练 FM 的复杂度也降低到 nk 级别。
(1) 为何引入隐向量?
除了上面提到的可以优化参数个数与算法复杂度,还有一个原因是隐向量的引入解决了稀疏数据特征的表示问题,vi vj 并不需要 i-j 特征才统一样本内出现才能训练,甚至对于之前没有见过或者样本中没有交叉过的特征,也可以通过隐向量内积算出其交叉得分。
(2) 为何算法复杂度由 n^2 降低为 nk?
传统 POLY2 模型 n个特征二阶交叉,需要对每个特征学习其他特征的交叉权重,所以 W_ij 共需要n^2 个参数,但是对于 特征i 与 特征j 而言,w_ij 与 wi_ji 是相同的,所以特征矩阵 W 是对称矩阵,通过对对称阵的优化,参数的复杂度可以优化到 nk。网上对复杂度简化的内容比较多,这里不再展开证明,这里只给出最终优化的结果。
其中 K 代表隐向量的维度,i,j代表特征,f代表隐向量中具体index的值,复杂的双求和乘积操作转换为 v·x 矩阵的 1/2 * (∑vx)^2 - ∑(vx)^2 即先求和再平方 减去 先平方再求和。具体的操作步骤可以看下述图示:
如果看了流程图,看了公式,看了矩阵图示,看了公式证明还是没搞懂什么是FM的话,你就只需要知道 FM 为每个特征训练得到一个 K 维隐向量,两个特征的二阶交叉系数可以通过其对应隐向量内积结果得到,这样就解决了二阶交叉的问题。
假设有4个特征A、B、C、D,其特征域范围都为100,则原始特征空间共4x100个特征,预测问题为 CTR,所以 label 为 0/1。
# 原始特征输入
num_samples = 60000
categoryA = np.random.randint(0, 100, (num_samples, 1))
categoryB = np.random.randint(100, 200, (num_samples, 1))
categoryC = np.random.randint(200, 300, (num_samples, 1))
categoryD = np.random.randint(300, 400, (num_samples, 1))
train = np.concatenate([categoryA, categoryB, categoryC, categoryD], axis=-1).astype('int32')
print("训练数据样例与Size:")
print(train[0:5])
print(train.shape)
labels = np.random.randint(0, 2, size=num_samples)
labels = np.asarray(labels)
print("样本labels:")
print(labels[0:10])
num_samples 为训练样本数量,train 为训练数据,维度为 (None x 4),labels 为标签,取值为0/1,数据均为 random 随机构造。
训练数据样例与Size:
[[ 93 153 238 306]
[ 46 118 249 366]
[ 20 135 293 376]
[ 25 132 243 318]
[ 67 108 201 343]]
(60000, 4)
样本labels:
[0 0 1 1 1 1 0 0 0 1]
根据上面的公式拆解,FM层实现主要实现 LR + 二阶交叉,这里每个特征对应 LR 有1个权重,对于二阶交叉,其隐向量维度为K,所以不考虑截距项的情况下,每个特征对应 1+K 个权重,基于这个前提构造的 FM 层可以通过 num_samples x K+1 维的参数矩阵同时训练 LR 与 隐向量权重,优化了算法设计逻辑与效率。
(1) FM层整体构造
这里自定义 Layer 需要类继承 from tensorflow.keras.layers import Layer 类。
init : 定义了模型的特征维度 feature_num 以及输出的隐向量的维度 output_dim
build : 定义可训练的权重矩阵,本例 feature_num = 400,隐向量维度 k=8,则生成参数矩阵维度为 400 x 9
call : call函数内层的具体实现逻辑,first_order 代表一阶段LR部分,second_order 代表二阶交叉部分,最终通过 concat 输出到下一层,first_order,second_order 实现随后给出。
compute_output_shape: 计算输出维度,供后续 keras 层推断,这里输出维度为: (输入样本数 * 9)
class Fm_Layer(Layer):
"""
init: 初始化参数
build: 定义权重
call: 层的功能与逻辑
compute_output_shape: 推断输出模型维度
"""
def __init__(self, feature_num, output_dim, **kwargs):
self.feature_num = feature_num
self.output_dim = output_dim
super().__init__(**kwargs)
# 定义模型初始化 根据特征数目
def build(self, input_shape):
# create a trainable weight variable for this layer
self.kernel = self.add_weight(name='kernel',
shape=(self.feature_num, self.output_dim + 1),
initializer='glorot_normal',
trainable=True)
super(Fm_Layer, self).build(input_shape) # Be sure to call this at the end
def call(self, inputs, **kwargs):
# input 为多个样本的稀疏特征表示
first_order = get_first_order(inputs, self.kernel)
seconder_order = get_second_order(inputs, self.kernel)
concat_order = tf.concat([first_order, seconder_order], axis=-1)
return concat_order
def compute_output_shape(self, input_shape):
return (input_shape(0), self.output_dim+1)
(2) LR部分实现
featIndex为特征索引,args 为参数矩阵,即 layer 的可训练权重,这里 lookup 得到每个权重的最后一位作为其线性部分权重,并求和得到 LR 部分。
def get_first_order(featIndex, args):
embedding = tf.nn.embedding_lookup(args, featIndex)[:, :, -1]
print('embedding', embedding)
linear = tf.reduce_sum(embedding, axis=-1)
sum_embedding = K.expand_dims(linear, axis=1)
return sum_embedding
(3) 二阶交叉部分
featIndex为特征索引,args为参数矩阵,这里权重矩阵去掉了最后一维,剩下 K 维代表隐向量权重,先求和再平方,先平方再求和的图示逻辑可以参考上面的流程图,这里不再赘述代码逻辑,最后套公式相减后乘 1/2 即可得到二阶交叉结果。
def get_second_order(featIndex, args):
embedding = tf.nn.embedding_lookup(args, featIndex)[:, :, :args.shape[-1]-1]
# 先求和再平方
sum_embedding = tf.reduce_sum(embedding, axis=1)
sum_square = K.square(sum_embedding)
# 先平方在求和
squared = K.square(embedding)
square_sum = tf.reduce_sum(squared, axis=1)
# 二阶交叉项
second_order = 0.5 * tf.subtract(sum_square, square_sum)
return second_order
输入样本维度为 60000 x 4,所以 Input 层的shape为4,fm层特征数为400,隐向量维度为8,最终concat LR与二阶交叉部分结果并连接 Dense 层通过 sigmoid 归一化到 0-1 的浮点数,整个模型构建完成。
# 构建模型
deep_input = layers.Input(shape=4, name='deep', dtype='int32')
fm_layer = Fm_Layer(400, 8)(deep_input)
re = layers.Dense(1, activation='sigmoid')(fm_layer)
fm_model = Model(deep_input, re)
损失函数选择交叉熵,也有同学使用 MSE,其他超参也可以自己调整。
# 模型编译
fm_model.compile(optimizer='rmsprop',
loss='binary_crossentropy',
metrics='accuracy')
fm_model.summary()
# 模型训练
fm_model.fit(train, labels , epochs=10, batch_size=128)
Epoch 1/10
469/469 [==============================] - 0s 459us/step - loss: 0.6932 - accuracy: 0.4997
Epoch 2/10
469/469 [==============================] - 0s 450us/step - loss: 0.6922 - accuracy: 0.5253
......
Epoch 9/10
469/469 [==============================] - 0s 425us/step - loss: 0.6657 - accuracy: 0.5937
Epoch 10/10
469/469 [==============================] - 0s 430us/step - loss: 0.6640 - accuracy: 0.5992
由于是随机数据,训练相关指标仅供参考。
构造4维的原始特征,调用 model.predict 即可,结果是 0-1 之间的浮点数,代表CTR的高低。
# 模型预测
print("模型预测结果:")
test_sample = [[1, 101, 201, 301], [7, 110, 222, 323], [81, 114, 270, 342], [63, 139, 204, 323], [33, 173, 201, 396]]
print(fm_model.predict(test_sample))
模型预测结果:
[[0.5398645 ]
[0.43967327]
[0.5679844 ]
[0.45938677]
[0.69800216]]
隐向量维度为8,lR权重维度为1,通过 getWeigths() 方法即可获得对应 index 特征的隐向量与权重。
# 获取线性权重与隐向量
index = 100
weigths = np.array(fm_model.get_layer(index=1).get_weights()).reshape(400, 9)
vector = weigths[index]
print("V-隐向量")
v = vector[:8]
print(v)
print("W-线性:")
w = vector[-1]
print(w)
V-隐向量
[-0.04662997 -0.11286838 0.23590335 -0.09384488 0.03299862 0.23256397
0.10323359 0.05801315]
W-线性:
0.16635393
fm_layer = Fm_Layer(400, 64)(deep_input)
隐向量维度刻画了特征的画像,理论上维度越高描述越精确,但随之而来的过拟合和性能问题也会凸显,只需修改 fm_layer 的第二个参数即可修改隐向量维度,修改为 64 维表征后,模型的指标均得到提升。
Epoch 1/10
469/469 [==============================] - 1s 726us/step - loss: 0.6933 - accuracy: 0.4970
Epoch 2/10
469/469 [==============================] - 0s 691us/step - loss: 0.6910 - accuracy: 0.5614
......
Epoch 9/10
469/469 [==============================] - 0s 707us/step - loss: 0.5414 - accuracy: 0.7333
Epoch 10/10
469/469 [==============================] - 0s 701us/step - loss: 0.5300 - accuracy: 0.7408
1.FM 优化了特征组合交叉部分,交叉维度介于 LR 与 DNN ,LR与DNN可以参考 WideAndDeep
2.自定义 Layer 必须实现的3个函数为: build,call,compute_output_shape
3.自定义回调可以参考: 自定义回调函数,你可以定期保存模型ckpt或者提前结束模型训练
4.调整样本权重可以参考: 训练样本加权,调整样本权重类似于修改采样策略,使模型偏好于某一类样本
5.自定义训练指标可以参考: 自定义Loss,Metrics,这里可以修改自己的梯度更新方式,指标计算方法
6.修改参数初始化方法可以参考:keras常见参数初始化方法,不同的参数初始化对模型的训练也有较大关系
7.模型性能的优化以及大规模特征输入可以参考: sparseTensor 与 lookup_embedding_sparse
8.训练得到的隐向量也可以作为特征表征作为其他模型的输入特征,所以并不限于计算内积