飞道的博客

TensorFlow-Keras - FM、WideAndDeep、DeepFM、DeepFwFM、DeepFmFM 理论与实战

5284人阅读  评论(0)

目录

一.引言

二.浅层模型概述

1.LR

2.FM

3.FMM

4.FwFM

5.FmFM

三.常用推荐算法实现

Pre.数据准备

1.FM

2.WideAndDeep

3.DeepFM

4.DeepFwFM

5.DeepFmFM

四.总结

1.函数测试

2.函数效果与复杂度对比[来自FmFM论文]

3.More


一.引言

推荐系统中常见的 CTR 模型从最初的一阶 LR 演变至二阶的 FM,二阶的 FM 演变至高阶的 DNN,最终通过低阶与高阶的结合实现了热门的 DeepFM。至此之后,推荐系统的优化方向大致分两个方向,一个是在二阶交叉部分继续增加域 Filed 的信息,获得更细力度的交叉;另一个是在 DNN 侧寻求更复杂的高阶特征交叉从而获得更多地高阶交叉信息。本文主要针对前者介绍一些常用推荐算法的基本原理与实现。

二.浅层模型概述

1.LR

LR 是最广泛的 CTR 预测模型,其具备参数少、计算快、可解释性强等特点。但由于模型均为一阶特征,未引入特征组合或者需要人工组合特征,因此对 CTR 场景下的特征组合表征不强。

引言中的插图 wide 部分即为 LR,通过加权求和并通过一个 Sigmoid 层即可获得 CTR 的预测值。

2.FM

FM 为每个特征引入一个隐向量 Vector 进行学习,解决了 Poly2 场景下参数量过大且特征组合稀疏导致很多参数无法学习的问题。

- Poly2

- FM

FM 由于其参数矩阵为对称矩阵,所以后面的二阶计算可以优化,从而将二阶项的复杂度优化至 O(kn),k 为向量长度、n 为特征数。快速记忆的话:和平方-平方和。

3.FMM

FM 为每一个特征学习一个隐向量,这基于特征与其他 Field 特征交叉时权重相同或者等重要性,但在实际场景下,引入域的信息可以很好细化特征学习,以如下样本为例:

 在 FM 中:

FM 中每个特征只有一个潜在的矢量需要学习具有任何其他特征的潜在效果,将 ESPN 作为wESPN 用于学习 Nike(wESPN·wNike) 和 Male(wESPN·wMale)。然而因为 Nike 和 Male 属于不同的领域 (EPSN, Nike) 和 (EPSN, Male) 的效果可能不同。在FFM中,每个特征都有几个潜在向量。根据其他功能的领域,使用该域对应的向量。

针对不同域选择不同表征向量计算提高了模型的表征能力,但随之而来的是大量的训练参数,相比 FM 的 O(kn),FFM 的复杂度为 O(kn^2) 。

4.FwFM

FM 提出后又提出了针对域信息的 FFM,但由于实际场景下计算成本消耗过大,工业场景下 FFM 使用不多,所以在保证引入域信息又不希望计算消耗太大的折中考虑下,引入了 FwFM 与 FmFM,并结合 DNN 得到了后来的 DeepFwFM 与 DeepFmFM。

FwFM 是 FM 的扩展,因为我们使用了额外的权重 rFiFj 明确捕捉不同的交互强度。通过与 DNN 组合即可得到 DeepFwFM

5.FmFM

FwFM 仅仅使用一个标量来表示特征域的交叉权重,相对来说表征能力不足,为了解决 FwFM 表征能力不足的情况,FmFM 针对每一对交叉域 i、j 提供一个可训练的参数矩阵,提高其表征能力。

rFiFj 由 MFiFj 替代,其维度为 (emb_dim x emb_dim),相对于 FFM 而言参数量也大大减少。

Field i 分别与特征 j、k... 交叉,如果 n 个特征则共有 (n * (n-1)) / 2 次交叉计算。

A.LookUp 获取 Field i 对应 Embedding [None x K]

B.获取 i、j 对应的参数矩阵 Wij 并执行矩阵乘法 [None x K] * [K x K] => [None x K]

C.MatMul 得到的结果继续与 Field j  LookUp 得到的向量进行 dot 获取 [None x 1]

Tips:

- 由于 FwFM 与 FmFM 不具备与 FM 相同的对称参数情况,所以计算复杂度与 FM 的优化形式不同

- 当 Matrix 为 E 单位矩阵时,FmFM 退化为 FM

 - 当 Matrix 为常量矩阵时,FmFM 退化为 FwFM

- 可变嵌入维度

传统 FM 变种特征的维度 K 都是固定的,由于 FmFM 引入了特征矩阵,所以根据特征重要性不同我们可以调整 Matrix 的维度,其不再局限于为方阵,从而影响不同特征的信息携带量进而影响其重要性。

- 与 FwFM 类似,将 FmFM 与 DNN 组合即可得到 DeepFM 的改良版 DeepFmFM。

- 参考 FiBiNET,FmFM 的矩阵也可以做适当改变,针对 i-j 的矩阵 Mij 可以退化为每个特征域 F 有一个矩阵 Mi,还可以继续退化至所有特征域公用一个矩阵 M

- 几种浅层模型的参数复杂度

m 为线性部分、K 为 emb 维度、mK 为隐向量参数矩阵的参数个数、n(n-1)/2 为特征组合数,FwFM 时每个组合一个参数,FmFM 时每个组合一个 [emb,emb] 矩阵,所以多了 K^2、对于FFM,参数数量为 𝑚 +𝑚(𝑛 − 1)𝐾 ,因为每个特征具有𝑛 − 1嵌入向量,通常情况下 𝑛 ≪ 𝑚。

三.常用推荐算法实现

Pre.数据准备


  
  1. import numpy as np
  2. def genSamples( numSamples=60000, seed=0):
  3. np.random.seed(seed)
  4. # 原始特征输入
  5. categoryA = np.random.randint( 0, 100, (numSamples, 1))
  6. categoryB = np.random.randint( 100, 200, (numSamples, 1))
  7. categoryC = np.random.randint( 200, 300, (numSamples, 1))
  8. categoryD = np.random.randint( 300, 400, (numSamples, 1))
  9. labels = np.random.randint( 0, 2, size=numSamples)
  10. labels = np.asarray(labels)
  11. return np.concatenate([categoryA, categoryB, categoryC, categoryD], axis=- 1).astype( 'int32'), labels

这里模拟简单随机样本,其中每个特征有100种取值,每次命中一个特征,共4个Field最后将4个特征 Concat 作为样本输出。实际场景下,大家可以自己构建分享实现特征划分并获取特征 id,如果是 Sparse 特征或者 Multi 特征,可以使用 lookup_sparse 将 embding 聚合。


  
  1. train, labels = genSamples()
  2. print( "训练数据样例与Size:")
  3. print(train[ 0: 5])
  4. print(train.shape)
  5. print( "样本labels:")
  6. print(labels[ 0: 5])

由于是 CTR 场景,所以 Label 的取值为 0 或者 1。genSampls 支持传入样本数量 num 与随机 seed,seed 主要为了保持后续算法比较时样本一致。这里取5条样本和 Label 展示,后面的算法都基于该随机样本进行测试与训练:


  
  1. 训练数据样例与Size:
  2. [[ 44 113 226 315]
  3. [ 47 155 298 394]
  4. [ 64 191 210 384]
  5. [ 67 175 284 329]
  6. [ 67 186 246 308]]
  7. (60000, 4)
  8. 样本labels:
  9. [0 1 0 0 1]

1.FM

这里实现参考 DeepFM 的 FM Layer,根据多个 Field 首先通过 Dense Embedding 层获取 id 对应 Embedding,随后分别实现 LR 和 FM 二级交叉项。

- 一阶项 LR


  
  1. from tensorflow.keras import backend as K
  2. from tensorflow.keras import layers
  3. import tensorflow as tf
  4. def get_first_order( featIndex, args):
  5. # None x Feat
  6. embedding = tf.nn.embedding_lookup(args, featIndex)[:, :, - 1]
  7. linear = tf.reduce_sum(embedding, axis=- 1)
  8. sum_embedding = K.expand_dims(linear, axis= 1)
  9. return sum_embedding

首先 lookup 获取 id 对应参数即 (None, Feat),随后 reduce_sum 得到 (None, ) 最后通过 expand_dims 得到 (None, 1) ,从而实现  LR 的累加过程。

- 二阶项交叉


  
  1. def get_second_order( featIndex, args):
  2. # None x Feat x 8
  3. embedding = tf.nn.embedding_lookup(args, featIndex)[:, :, :args.shape[- 1]]
  4. # 先求和再平方
  5. sum_embedding = tf.reduce_sum(embedding, axis= 1)
  6. sum_square = K.square(sum_embedding)
  7. # 先平方在求和
  8. suqared = K.square(embedding)
  9. square_sum = tf.reduce_sum(suqared, axis= 1)
  10. # 二阶交叉项
  11. second_order = 0.5 * tf.subtract(sum_square, square_sum)
  12. return second_order

同理,首先 Lookup 获取 Embedding,对于 Field = 4 则得到 None x 4 x K,K 为 emb_dim,套用公式 和平方 ➖ 平方和,记得加 1/2,最终输出 (None, 8)。

- 完整 Layer


  
  1. from tensorflow.keras import layers, Model
  2. from tensorflow.keras.layers import Layer
  3. from GetTrainAndTestData import genSamples
  4. import tensorflow as tf
  5. from Handel import get_first_order, get_second_order
  6. class FM( Layer):
  7. def __init__( self, feature_num, output_dim, **kwargs):
  8. self.feature_num = feature_num
  9. self.output_dim = output_dim
  10. super().__init__(**kwargs)
  11. # 定义模型初始化 根据特征数目
  12. def build( self, input_shape):
  13. # create a trainable weight variable for this layer
  14. self.kernel = self.add_weight(name= 'lr_layer',
  15. shape=(self.feature_num, 1),
  16. initializer= 'glorot_normal',
  17. trainable= True)
  18. self.embedding = self.add_weight(name= 'fm_layer',
  19. shape=(self.feature_num, self.output_dim),
  20. initializer= 'glorot_normal',
  21. trainable= True)
  22. # Be sure to call this at the end
  23. super(FM, self).build(input_shape)
  24. def call( self, inputs, **kwargs):
  25. # input 为多个样本的稀疏特征表示
  26. first_order = get_first_order(inputs, self.kernel)
  27. seconder_order = get_second_order(inputs, self.embedding)
  28. concat_order = tf.concat([first_order, seconder_order], axis=- 1)
  29. return concat_order

Keras 自定义 Layer 主要实现如下四个函数:


  
  1. init: 初始化参数
  2. build: 定义权重
  3. call: 层的功能与逻辑
  4. compute_output_shape: 推断输出模型维度

每次调用最终都会调用至 call 方法,并执行内部的计算逻辑,这里主要注意输入输出维度,避免出现维度不匹配的问题。

FM 的系数量为 m + mK,这里 K = 8,m = 400,所以 FM Layer 的参数量为 3600。

2.WideAndDeep

LR + DNN 的组合,LR 可以参考上面 FM 的  get_first_order 的部分,DNN 则直接通过add_weight 添加 dense 和 bias 层即可。这里添加两层 DNN 网络,激活函数采用 relu 且未添加正则化参数。

- Deep Layer


  
  1. def get_deep_order( feat_index, args):
  2. embedding = tf.nn.embedding_lookup(args, feat_index)[:, :, :]
  3. embedding_flatten = layers.Flatten()(embedding)
  4. return embedding_flatten

lookup 获取对应 id 的全部 Embedding 并 flatten 打平连入后续的 fully_connected 层:


  
  1. # DNN None x Dim2
  2. deep_order = get_deep_order(inputs, self.embedding) # None x 32
  3. deep_order = self.activation(tf.matmul(deep_order, self.dense1) + self.bias1)
  4. deep_order = self.activation(tf.matmul(deep_order, self.dense2) + self.bias2)

经过两次全连接将原始 None x (Field x K) 维度转换为 None x 32,32 为第二个全连接层的输出维度,最后与 LR 的 None x 1 连接送入 Sigmoid 计算即可得到最终预测 CTR。

- 完整 Layer


  
  1. from tensorflow.keras import layers, Model, regularizers
  2. from tensorflow.keras.layers import Layer, Activation
  3. import tensorflow as tf
  4. from tensorflow.python.keras.backend import relu
  5. from GetTrainAndTestData import genSamples
  6. from Handel import get_first_order, get_deep_order
  7. class WideAndDeep( Layer):
  8. """
  9. init: 初始化参数
  10. build: 定义权重
  11. call: 层的功能与逻辑
  12. compute_output_shape: 推断输出模型维度
  13. """
  14. def __init__( self, feature_num, embedding_dim, dense1_dim=128, dense2_dim=64, **kwargs):
  15. self.feature_num = feature_num
  16. self.embedding_dim = embedding_dim
  17. self.dense1_dim = dense1_dim
  18. self.dense2_dim = dense2_dim
  19. self.kernel_regularize = regularizers.l2( 0.1)
  20. self.activation = Activation(relu)
  21. super().__init__(**kwargs)
  22. # 定义模型初始化 根据特征数目
  23. def build( self, input_shape):
  24. # create a trainable weight variable for this layer
  25. self.kernel = self.add_weight(name= 'lr_layer',
  26. shape=(self.feature_num, 1),
  27. initializer= 'glorot_normal',
  28. trainable= True)
  29. self.embedding = self.add_weight(name= "embedding",
  30. shape=(self.feature_num, self.embedding_dim),
  31. initializer= 'he_normal',
  32. trainable= True)
  33. # DNN Dense1
  34. self.dense1 = self.add_weight(name= 'dense1',
  35. shape=(input_shape[ 1] * self.embedding_dim, self.dense1_dim),
  36. initializer= 'he_normal',
  37. trainable= True)
  38. # DNN Bias1
  39. self.bias1 = self.add_weight(name= 'bias1',
  40. shape=(self.dense1_dim,),
  41. initializer= 'he_normal',
  42. trainable= True)
  43. # DNN Dense2
  44. self.dense2 = self.add_weight(name= 'dense2',
  45. shape=(self.dense1_dim, self.dense2_dim),
  46. initializer= 'he_normal',
  47. trainable= True)
  48. # DNN Bias1
  49. self.bias2 = self.add_weight(name= 'bias2',
  50. shape=(self.dense2_dim,),
  51. initializer= 'he_normal',
  52. trainable= True)
  53. # Be sure to call this at the end
  54. super(WideAndDeep, self).build(input_shape)
  55. def call( self, inputs, **kwargs):
  56. # LR None x 1
  57. first_order = get_first_order(inputs, self.kernel)
  58. # DNN None x Dim2
  59. deep_order = get_deep_order(inputs, self.embedding) # None x 32
  60. deep_order = self.activation(tf.matmul(deep_order, self.dense1) + self.bias1)
  61. deep_order = self.activation(tf.matmul(deep_order, self.dense2) + self.bias2)
  62. # Concat LR + DNN
  63. concat_order = tf.concat([first_order, deep_order], axis=- 1)
  64. return concat_order

DNN + LR 层参数计算:

400x1 [LR参数] + 400x8 [Embedding层参数] + 32x128 [Dense1] + 128 [Bias1] + 128x64 [Dense2] + 64 [Bias2] = 16080

3.DeepFM

DeepFM = FM + DNN ,所以基于上面 FM 和 WideAndDeep 的 DNN 侧我们可以快速实现 DeepFM 的结构。

- 完整 Layer


  
  1. from tensorflow.keras import layers, Model, regularizers
  2. from tensorflow.keras.layers import Layer, Activation
  3. import tensorflow as tf
  4. from tensorflow.python.keras.backend import relu
  5. from GetTrainAndTestData import genSamples
  6. from Handel import get_first_order, get_second_order, get_deep_order
  7. class DeepFM( Layer):
  8. def __init__( self, feature_num, embedding_dim, dense1_dim=128, dense2_dim=64, **kwargs):
  9. self.feature_num = feature_num
  10. self.embedding_dim = embedding_dim
  11. self.dense1_dim = dense1_dim
  12. self.dense2_dim = dense2_dim
  13. self.kernel_regularize = regularizers.l2( 0.1)
  14. self.activation = Activation(relu)
  15. super().__init__(**kwargs)
  16. # 定义模型初始化 根据特征数目
  17. def build( self, input_shape):
  18. # create a trainable weight variable for this layer
  19. self.kernel = self.add_weight(name= 'lr_layer',
  20. shape=(self.feature_num, 1),
  21. initializer= 'glorot_normal',
  22. trainable= True)
  23. self.embedding = self.add_weight(name= "embedding",
  24. shape=(self.feature_num, self.embedding_dim),
  25. initializer= 'he_normal',
  26. trainable= True)
  27. # DNN Dense1
  28. self.dense1 = self.add_weight(name= 'dense1',
  29. shape=(input_shape[ 1] * self.embedding_dim, self.dense1_dim),
  30. initializer= 'he_normal',
  31. trainable= True)
  32. # DNN Bias1
  33. self.bias1 = self.add_weight(name= 'bias1',
  34. shape=(self.dense1_dim,),
  35. initializer= 'he_normal',
  36. trainable= True)
  37. # DNN Dense2
  38. self.dense2 = self.add_weight(name= 'dense2',
  39. shape=(self.dense1_dim, self.dense2_dim),
  40. initializer= 'he_normal',
  41. trainable= True)
  42. # DNN Bias1
  43. self.bias2 = self.add_weight(name= 'bias2',
  44. shape=(self.dense2_dim,),
  45. initializer= 'he_normal',
  46. trainable= True)
  47. # Be sure to call this at the end
  48. super(DeepFM, self).build(input_shape)
  49. def call( self, inputs, **kwargs):
  50. # LR None x 1 FM None x 8
  51. first_order = get_first_order(inputs, self.kernel)
  52. seconder_order = get_second_order(inputs, self.embedding)
  53. # DNN None x Dim2
  54. deep_order = get_deep_order(inputs, self.embedding) # None x 32
  55. deep_order = self.activation(tf.matmul(deep_order, self.dense1) + self.bias1)
  56. deep_order = self.activation(tf.matmul(deep_order, self.dense2) + self.bias2)
  57. # Concat LR + DNN
  58. concat_order = tf.concat([first_order, seconder_order, deep_order], axis=- 1)
  59. return concat_order

由于 Embedding 层在 FM 侧和 Deep 侧是共享的,所以与 WideAndDeep 的参数量是一致的。实际场景下,也可以选择 FM 预训练的 Embedding 当做初始化向量。 

4.DeepFwFM

相比于 FM 使用和平方和平方和的计算方式,这里由于引入 Field 之间的权重,所以不能基于公式优化计算方式。这里我们严格按照公式的计算流程 for 循环计算,当然也可以使用空间换时间的方法,以及做缓存,提高整体推理速度。

相比于 FM 这里只需新增参数 rFiFj 即可:


  
  1. self.field_matrix = self.add_weight(name= 'field_pair_matrix',
  2. shape=(self.num_fields, self.num_fields),
  3. initializer= 'truncated_normal')

  
  1. def get_Fw_FM( featIndex, args, field_matrix, num_fields, mode="part"):
  2. """
  3. Input: 3D Tensor [batch_size, field_size, embedding_size]
  4. Output: 2D [batch_size, 1 / (n*(n-1))/2]
  5. num_fields: fileds 数量
  6. """
  7. # None x Feat x 8
  8. inputs = tf.nn.embedding_lookup(args, featIndex)[:, :, :args.shape[- 1] - 1]
  9. if K.ndim(inputs) != 3:
  10. raise ValueError(
  11. "Unexpected inputs dimensions %d, expect to be 3 dimensions"
  12. % (K.ndim(inputs)))
  13. if inputs.shape[ 1] != num_fields:
  14. raise ValueError( "Mismatch in number of fields {} and \
  15. concatenated embeddings dims {}". format(num_fields, inputs.shape[ 1]))
  16. # 成对内积
  17. pair_inner_prods = []
  18. # 快速全排列
  19. for fi, fj in itertools.combinations( range(num_fields), 2):
  20. r_ij = field_matrix[fi, fj]
  21. # 获取不同域的 Embedding
  22. feat_embedding_i = tf.squeeze(inputs[:, fi:fi + 1, :], axis= 1) # None x 1 x 8 => None x 8
  23. feat_embedding_j = tf.squeeze(inputs[:, fj:fj + 1, :], axis= 1) # None x 1 x 8 => None x 8
  24. f = tf.scalar_mul(r_ij, K.batch_dot(feat_embedding_i, feat_embedding_j, axes= 1))
  25. pair_inner_prods.append(f)
  26. if mode == "part":
  27. fwFm_output = tf.concat(pair_inner_prods, axis= 1)
  28. else:
  29. fwFm_output = tf.add_n(pair_inner_prods)
  30. return fwFm_output

首先通过 itertools.combinations API 获取当前所有特征交叉的组合,随后从 inputs 中获取对应特征的 Embedding,首先将 Embedding dot 随后乘以对应的 rFiFj,添加到 pair_inner_prods 数组中。实际场景中,如果 FwFM 作为最终结果输出,可以使用 add_n,此时得到的输出是 None x 1,与 LR 结合再 sigmod 就能获得最终的预测 CTR 了,如果在 DeepFwFM 场景下,可以 batch_dot 再 concat,此时得到的输出时 (None x (n*(n-1)/2)),可以后续与 DNN concat 继续增加隐层训练。当然也可以不执行 batch_dot,执行 multiply,此时得到的输出向量维度更高为 (None x (k*n*(n-1)/2)),同理可以自己连接全连接或者与 DNN 结合都可以,这个大家可以根据实际场景自己尝试表现最好的方法。

- 完整 Layer


  
  1. from tensorflow.keras import backend as K
  2. from tensorflow.keras import layers, Model
  3. from tensorflow.keras.layers import Layer
  4. from GetTrainAndTestData import genSamples
  5. import tensorflow as tf
  6. from tensorflow.python.keras.backend import relu
  7. from tensorflow.keras.layers import Layer, Lambda, Dense, Input, Activation
  8. from Handel import get_first_order, get_second_order, get_deep_order
  9. class FwFM( Layer):
  10. def __init__( self, feature_num, embedding_dim, mode="part", num_fields=4, dense1_dim=128, dense2_dim=64, **kwargs):
  11. self.feature_num = feature_num
  12. self.embedding_dim = embedding_dim
  13. self.num_fields = num_fields
  14. self.dense1_dim = dense1_dim
  15. self.dense2_dim = dense2_dim
  16. self.activation = Activation(relu)
  17. if mode == "part":
  18. self.fwFm_out = (num_fields * (num_fields - 1)) / 2
  19. else:
  20. self.fwFm_out = 1
  21. super().__init__(**kwargs)
  22. # 定义模型初始化 根据特征数目
  23. def build( self, input_shape):
  24. self.field_matrix = self.add_weight(name= 'field_pair_matrix',
  25. shape=(self.num_fields, self.num_fields),
  26. initializer= 'truncated_normal')
  27. self.kernel = self.add_weight(name= 'lr_layer',
  28. shape=(self.feature_num, 1),
  29. initializer= 'glorot_normal',
  30. trainable= True)
  31. self.embedding = self.add_weight(name= "embedding",
  32. shape=(self.feature_num, self.embedding_dim),
  33. initializer= 'he_normal',
  34. trainable= True)
  35. # DNN Dense1
  36. self.dense1 = self.add_weight(name= 'dense1',
  37. shape=(input_shape[ 1] * self.embedding_dim, self.dense1_dim),
  38. initializer= 'he_normal',
  39. trainable= True)
  40. # DNN Bias1
  41. self.bias1 = self.add_weight(name= 'bias1',
  42. shape=(self.dense1_dim,),
  43. initializer= 'he_normal',
  44. trainable= True)
  45. # DNN Dense2
  46. self.dense2 = self.add_weight(name= 'dense2',
  47. shape=(self.dense1_dim, self.dense2_dim),
  48. initializer= 'he_normal',
  49. trainable= True)
  50. # DNN Bias1
  51. self.bias2 = self.add_weight(name= 'bias2',
  52. shape=(self.dense2_dim,),
  53. initializer= 'he_normal',
  54. trainable= True)
  55. # Be sure to call this at the end
  56. super(FwFM, self).build(input_shape)
  57. def call( self, inputs, **kwargs):
  58. # input 为多个样本的稀疏特征表示
  59. first_order = get_first_order(inputs, self.kernel)
  60. # FwFM 部分
  61. fwfm_order = get_Fw_FM(inputs, self.embedding, self.field_matrix, self.num_fields)
  62. # DNN None x Dim2
  63. deep_order = get_deep_order(inputs, self.embedding) # None x 32
  64. deep_order = self.activation(tf.matmul(deep_order, self.dense1) + self.bias1)
  65. deep_order = self.activation(tf.matmul(deep_order, self.dense2) + self.bias2)
  66. concat_order = tf.concat([first_order, fwfm_order, deep_order], axis=- 1)
  67. return concat_order

FwFM 比 FM 层多了 4x4 个 Rij 参数,所以在前面 16080 参数的基础上多了 16个参数即 16096。 

5.DeepFmFM

只需要把 FwFM 的 rFiFj 参数修改为 Matrix 形式即可实现 FmFM,再增加 DNN 就得到 DeepFmFM:


  
  1. def get_Fm_FM( featIndex, args, field_matrix, num_fields, mode="part"):
  2. # None x Feat x 8
  3. inputs = tf.nn.embedding_lookup(args, featIndex)[:, :, :args.shape[- 1]]
  4. if K.ndim(inputs) != 3:
  5. raise ValueError(
  6. "Unexpected inputs dimensions %d, expect to be 3 dimensions"
  7. % (K.ndim(inputs)))
  8. if inputs.shape[ 1] != num_fields:
  9. raise ValueError( "Mismatch in number of fields {} and \
  10. concatenated embeddings dims {}". format(num_fields, inputs.shape[ 1]))
  11. # 成对内积
  12. pair_inner_prods = []
  13. # 快速全排列
  14. for fi, fj in itertools.combinations( range(num_fields), 2):
  15. r_ij = field_matrix[ str(fi) + "_" + str(fj)]
  16. # r_ij = public_matrix
  17. # 获取不同域的 Embedding
  18. feat_embedding_i = tf.squeeze(inputs[:, fi:fi + 1, :], axis= 1) # None x 1 x 8 => None x 8
  19. feat_embedding_j = tf.squeeze(inputs[:, fj:fj + 1, :], axis= 1) # None x 1 x 8 => None x 8
  20. f = tf.multiply(tf.matmul(feat_embedding_i, r_ij), feat_embedding_j)
  21. pair_inner_prods.append(f)
  22. if mode == "part":
  23. fmFm_output = tf.concat(pair_inner_prods, axis= 1)
  24. else:
  25. fmFm_output = tf.add_n(pair_inner_prods)
  26. return fmFm_output

这里没有采用 FwFM 的 BatchDot 而是采用 multiply,前者会返回 None x (n*(n-1)/2) 后者则返回 None x (k*n*(n-1)/2)。得到长向量后一般可以选择接一个全连接再与 Deep 侧 concat,减少二阶项对全局的影响。

- 完整 Layer


  
  1. class FmFM( Layer):
  2. def __init__( self, feature_num, embedding_dim, mode="part", num_fields=4, dense1_dim=128, dense2_dim=64, **kwargs):
  3. self.feature_num = feature_num
  4. self.embedding_dim = embedding_dim
  5. self.num_fields = num_fields
  6. self.dense1_dim = dense1_dim
  7. self.dense2_dim = dense2_dim
  8. self.activation = Activation(relu)
  9. if mode == "part":
  10. self.fwFm_out = int((num_fields * (num_fields - 1)) / 2)
  11. else:
  12. self.fwFm_out = 1
  13. super().__init__(**kwargs)
  14. # 定义模型初始化 根据特征数目
  15. def build( self, input_shape):
  16. self.kernel = self.add_weight(name= 'lr_layer',
  17. shape=(self.feature_num, 1),
  18. initializer= 'he_normal',
  19. trainable= True)
  20. self.embedding = self.add_weight(name= "embedding",
  21. shape=(self.feature_num, self.embedding_dim),
  22. initializer= 'he_normal',
  23. trainable= True)
  24. # DNN Dense1
  25. self.dense1 = self.add_weight(name= 'dense1',
  26. shape=(input_shape[ 1] * self.embedding_dim, self.dense1_dim),
  27. initializer= 'he_normal',
  28. trainable= True)
  29. # DNN Bias1
  30. self.bias1 = self.add_weight(name= 'bias1',
  31. shape=(self.dense1_dim,),
  32. initializer= 'he_normal',
  33. trainable= True)
  34. # DNN Dense2
  35. self.dense2 = self.add_weight(name= 'dense2',
  36. shape=(self.dense1_dim, self.dense2_dim),
  37. initializer= 'he_normal',
  38. trainable= True)
  39. # DNN Bias1
  40. self.bias2 = self.add_weight(name= 'bias2',
  41. shape=(self.dense2_dim,),
  42. initializer= 'he_normal',
  43. trainable= True)
  44. self.matrix_dict = {}
  45. for fi, fj in itertools.combinations( range(self.num_fields), 2):
  46. self.matrix_dict[ str(fi) + "_" + str(fj)] = self.add_weight(name= "matrix_weight_%d_%d" % (fi, fj),
  47. shape=(self.embedding_dim, self.embedding_dim),
  48. initializer= 'he_normal',
  49. trainable= True)
  50. # DNN Dense2
  51. self.dense4FmFM = self.add_weight(name= 'dense4FmFM',
  52. shape=(self.fwFm_out * self.embedding_dim, self.embedding_dim),
  53. initializer= 'he_normal',
  54. trainable= True)
  55. # DNN Bias1
  56. self.bias4FmFM = self.add_weight(name= 'bias4FmFM',
  57. shape=(self.embedding_dim,),
  58. initializer= 'he_normal',
  59. trainable= True)
  60. # Be sure to call this at the end
  61. super(FmFM, self).build(input_shape)
  62. def call( self, inputs, **kwargs):
  63. # input 为多个样本的稀疏特征表示
  64. first_order = get_first_order(inputs, self.kernel)
  65. # FmFM 部分
  66. fmfm_order = get_Fm_FM(inputs, self.embedding, self.matrix_dict, self.num_fields)
  67. fmfm_order = self.activation(tf.matmul(fmfm_order, self.dense4FmFM) + self.bias4FmFM)
  68. # DNN None x Dim2
  69. deep_order = get_deep_order(inputs, self.embedding) # None x 32
  70. deep_order = self.activation(tf.matmul(deep_order, self.dense1) + self.bias1)
  71. deep_order = self.activation(tf.matmul(deep_order, self.dense2) + self.bias2)
  72. concat_order = tf.concat([first_order, fmfm_order, deep_order], axis=- 1)
  73. return concat_order

在 DeepFM 16080 基础上增加了 6*8*8 的参数矩阵以及 6*8*8 的 dense 和 8 的 bias,所以最终参数量为 16856。

四.总结

1.函数测试

针对上述自定义 Layer,可以调用不同的模型 Layer 进行模型训练与预测。


  
  1. if __name__ == '__main__':
  2. train, labels = genSamples()
  3. # 构建模型
  4. input = layers.Input(shape= 4, name= 'input', dtype= 'int32')
  5. # FM、WideAndDeep、DeepFM、FwFM、FmFM
  6. model_layer = FmFM( 400, 8)( input)
  7. output = layers.Dense( 1, activation= 'sigmoid')(model_layer)
  8. model = Model( input, output)
  9. # 模型编译
  10. model. compile(optimizer= 'adam',
  11. loss= 'binary_crossentropy',
  12. metrics= 'accuracy')
  13. model.summary()
  14. # 模型训练
  15. model.fit(train, labels, epochs= 10, batch_size= 128)
  16. # 模型预测
  17. print( "模型预测结果:")
  18. test_sample, test_label = genSamples( 100, 99)
  19. print(model.predict(test_sample))

以 DeepFmFM 为例:


  
  1. Epoch 1/10
  2. 469/469 [==============================] - 1s 1ms/step - loss: 0.6946 - accuracy: 0.5009
  3. Epoch 2/10
  4. 469/469 [==============================] - 1s 1ms/step - loss: 0.6923 - accuracy: 0.5174
  5. Epoch 3/10
  6. 469/469 [==============================] - 1s 1ms/step - loss: 0.6899 - accuracy: 0.5322
  7. Epoch 4/10
  8. 469/469 [==============================] - 0s 1ms/step - loss: 0.6866 - accuracy: 0.5475
  9. Epoch 5/10
  10. 469/469 [==============================] - 0s 1ms/step - loss: 0.6799 - accuracy: 0.5682
  11. Epoch 6/10
  12. 469/469 [==============================] - 0s 1ms/step - loss: 0.6737 - accuracy: 0.5793
  13. Epoch 7/10
  14. 469/469 [==============================] - 0s 1ms/step - loss: 0.6665 - accuracy: 0.5956
  15. Epoch 8/10
  16. 469/469 [==============================] - 0s 1ms/step - loss: 0.6605 - accuracy: 0.6049
  17. Epoch 9/10
  18. 469/469 [==============================] - 0s 1ms/step - loss: 0.6551 - accuracy: 0.6113
  19. Epoch 10/10
  20. 469/469 [==============================] - 0s 1ms/step - loss: 0.6489 - accuracy: 0.6214

2.函数效果与复杂度对比[来自FmFM论文]

下图为 Criteo 数据集上 FmFM 与其他低阶模型的训练 AUC 与 LogLoss 对比:

 下图为多种模型 AUC 与 ELOPs 对比:

ELOPS: Floating Point Operations Per Second 意为每秒浮点运算次数,视为运算速度。

ELOPs: Floating Point Operations 意为浮点运算数,视为计算量,上图 FLOP 为该指标。

3.More

DeepFM: A Factorization-Machine based Neural Network for CTR Prediction

FwFM [WWW 2018]Field-weighted Factorization Machines for Click-Through Rate Prediction in Display Advertising

FiBiNET [RecSys 2019]FiBiNET: Combining Feature Importance and Bilinear feature Interaction for Click-Through Rate Prediction

FM2: Field-matrixed Factorization Machines for Recommender Systems


转载:https://blog.csdn.net/BIT_666/article/details/129195152
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场