总结
- 在DeepFM中,FM算法负责对一阶特征以及由一阶特征两两组合而成的二阶特征进行特征的提取;DNN算法负责对由输入的一阶特征进行全连接等操作形成的高阶特征进行特征的提取。
- 结合了广度和深度模型的优点,联合训练FM模型和DNN模型,同时学习低阶特征组合和高阶特征组合。
- 没有特征工程的端到端模型。
- DeepFM 共享相同的输入和 embedding vector,训练更高效。
- 评估模型时,用到了一个新的指标“Gini Normalization”
零、从LR到SVM再到FM模型
直线型:
逻辑回归LR:
优点:简单、可解释、易于扩展、易于并行化;
缺点:难以捕捉特征组合。
CTR早期用的LR最多,采用【线性模型】+【人工特征组合引入非线性】模式。后面为了解决LR需要人工特征工程的缺陷,大佬们想办法把特征组合能力体现在模型中,如下式子,最后一项是两两特征组合(类似多项式核SVM),这里的组合特征的权重在训练阶段获得。但是这样组合特征泛化能力较弱——尤其是在大规模稀疏特征存在的场景,如CTR预估和推荐排序时:
为了解决刚才说的组合特征泛化能力弱的问题,FM的式子出现(如下),前两项即LR部分,后两项是Dense化的两两特征组合,特征组合的权重计算方式是亮点:为每个特征学习一个大小为k的一维向量,如为两个特征和分别学习到和向量,那么这两个特征组合的权重值,就是两个向量的内积,在2010年时FM这么做,其实和今天的对特征进行embedding化表征是差不多的意思:
一、FM如何处理特征交叉
机器学习模型因子分解机模型(Factorization Machine)即FM的结构如下图:
(1)类别型特征转为one-hot向量
(2)将one-hot向量通过embedding层转为稠密的embedding层
(3)独特的是这里,使用一个单独的FM层处理特征之间的交叉问题:这里是多个内积操作单元对不同特征向量两两组合
(4)将(3)的内积操作结果输入到输出神经元,完成预测
这样回到刚才的栗子,用户喜欢的电影风格和电影本身的风格,通过FM层的两两特征的内积操作,使得特征进行充分的组合,不至于像embedding + MLP一样的MLP内部像黑盒子一样低效地交叉。
FM和DeepFM的特征交叉
1.1 FM提出的原因:
逻辑回归模型表达能力不强,容易造成有效信息的损失。辛普森悖论就说明了多维度特征交叉的重要性:分组实验相当于使用【性别】+【视频id】的组合特征计算点击率,而汇总实验则使用【视频id】这一单一特征计算点击率。这里汇总实验对高维特征进行合并,损失了大量有效信息,即逻辑回归只是对单一特征做简单加权,不具备特征交叉生成高维组合特征的能力,从而无法正确刻画数据模式。
1.2 POLY2模型——特征交叉的开始
算法工程师经验和精力有限,难以找到最优的特征组合,提出POLY2模型进行特征的暴力组合:上面模型中对所有特征都进行两两交叉(特征和),并且所有特征组合有权重W。其本质上还是线性模型。
缺点:
- 互联网数据常用one-hot编码处理类别型数据(所以特征向量很稀疏),POLY2无选择的特征交叉,使得特征向量更加稀疏,导致大部分交叉特征的权重却反有效数据进行训练(无法收敛)。
- 权重参数量从n上升到n平方,训练更加复杂。
1.3 FM模型的隐向量特征交叉
- 为了解决POLY2模型的缺陷,2010年提出FM模型。如下式子是FM(Factor Machine,因子分解机)二阶部分的数学表达式
- FM和POLY2的区别是,FM用两个向量的内积取代了单一的权重系数。
- FM权重参数量少了,降低训练开销;
- FM虽然丢失了某些具体特征组合的精确记忆能力,但泛化能力大大提高。
- FM的二阶以上交叉没有很强的意义,因为没法加速。
- FM为每个特征学习了一个隐权重向量
(和矩阵分解,用隐向量代表 user 和 item 思想一样,
从单纯的 user、item隐向量扩展到了所有特征上)。在特征交叉时,使用两个特征隐向量的内积作为交叉特征的权重
. - FM引入隐向量,更好解决数据稀疏性问题,ex:
- 商品推荐场景,样本有两个特征:频道(channel)和品牌(brand),某训练样本的特征组合是(ESPN, Adidas)。
- 在POLY2中,只有当ESPN和Adidas同时出现在一个训练样本时,模型才能学习到这个组合特征对应的权重;
- 在FM中,ESPN的隐向量也可以通过(ESPN, Gucci)样本进行更新,Adidas的隐向量也可以通过(NBC, Adidas)样本进行更新,即大幅降低模型,对数据稀疏性的要求。
二、DeepFM模型
哈工大和华为一起提出DeepFM就是基于wide&deep组合模型的思想,我们可以将以往的FM和其他深度学习模型结合,成为一个全新的强特征组合能力的模型,并且也具有强拟合能力。
注意下图的FM层有个加操作:加操作是不进行特征交叉,直接把原先的特征接入输出层,相当于wide&deep模型中的wide层。
由上图的DeepFM架构图看出:
(1)用FM层替换了wide&deep左边你的wide部分;
——增强浅层网络的特征组合能力。
(2)右边保持和wide&deep一毛一样,利用多层神经元(如MLP)进行所有特征的深层处理
(3)最后输出层将FM的output和deep的output组合起来,产生预估结果
三、代码流程
3.1 导入数据集
通过get_dataset
导入movielen数据集。
import tensorflow as tf
"""
Diff with DeepFM:
1. separate categorical features from dense features when processing first order features
and second order features
2. modify original fm part with a fully crossed fm part
"""
# load sample as tf dataset
def get_dataset(file_path):
dataset = tf.data.experimental.make_csv_dataset(
file_path,
batch_size=12,
label_name='label',
na_value="0",
num_epochs=1,
ignore_errors=True)
return dataset
# split as test dataset and training dataset
train_dataset = get_dataset('D:/wide&deep/trainingSamples.csv')
test_dataset = get_dataset('D:/wide&deep/testSamples.csv')
3.2 特征处理
(1)类别型特征:利用 One-hot 编码处理
- 第一类是类别、ID 型特征(以下简称类别型特征)。
拿电影推荐来说,电影的风格、ID、标签、导演演员等信息,用户看过的电影 ID、用户的性别、地理位置信息、当前的季节、时间(上午,下午,晚上)、天气等等,这些无法用数字表示的信息全都可以被看作是类别、ID 类特征。——利用one hot编码。
# define input for keras model
inputs = {
'movieAvgRating': tf.keras.layers.Input(name='movieAvgRating', shape=(), dtype='float32'),
'movieRatingStddev': tf.keras.layers.Input(name='movieRatingStddev', shape=(), dtype='float32'),
'movieRatingCount': tf.keras.layers.Input(name='movieRatingCount', shape=(), dtype='int32'),
'userAvgRating': tf.keras.layers.Input(name='userAvgRating', shape=(), dtype='float32'),
'userRatingStddev': tf.keras.layers.Input(name='userRatingStddev', shape=(), dtype='float32'),
'userRatingCount': tf.keras.layers.Input(name='userRatingCount', shape=(), dtype='int32'),
'releaseYear': tf.keras.layers.Input(name='releaseYear', shape=(), dtype='int32'),
'movieId': tf.keras.layers.Input(name='movieId', shape=(), dtype='int32'),
'userId': tf.keras.layers.Input(name='userId', shape=(), dtype='int32'),
'userRatedMovie1': tf.keras.layers.Input(name='userRatedMovie1', shape=(), dtype='int32'),
'userGenre1': tf.keras.layers.Input(name='userGenre1', shape=(), dtype='string'),
'userGenre2': tf.keras.layers.Input(name='userGenre2', shape=(), dtype='string'),
'userGenre3': tf.keras.layers.Input(name='userGenre3', shape=(), dtype='string'),
'userGenre4': tf.keras.layers.Input(name='userGenre4', shape=(), dtype='string'),
'userGenre5': tf.keras.layers.Input(name='userGenre5', shape=(), dtype='string'),
'movieGenre1': tf.keras.layers.Input(name='movieGenre1', shape=(), dtype='string'),
'movieGenre2': tf.keras.layers.Input(name='movieGenre2', shape=(), dtype='string'),
'movieGenre3': tf.keras.layers.Input(name='movieGenre3', shape=(), dtype='string'),
}
# movie id embedding feature
movie_col = tf.feature_column.categorical_column_with_identity(key='movieId', num_buckets=1001)
movie_emb_col = tf.feature_column.embedding_column(movie_col, 10)
movie_ind_col = tf.feature_column.indicator_column(movie_col) # movid id indicator columns
# user id embedding feature
user_col = tf.feature_column.categorical_column_with_identity(key='userId', num_buckets=30001)
user_emb_col = tf.feature_column.embedding_column(user_col, 10)
user_ind_col = tf.feature_column.indicator_column(user_col) # user id indicator columns
# genre features vocabulary
genre_vocab = ['Film-Noir', 'Action', 'Adventure', 'Horror', 'Romance', 'War', 'Comedy', 'Western', 'Documentary',
'Sci-Fi', 'Drama', 'Thriller',
'Crime', 'Fantasy', 'Animation', 'IMAX', 'Mystery', 'Children', 'Musical']
# user genre embedding feature
user_genre_col = tf.feature_column.categorical_column_with_vocabulary_list(key="userGenre1",
vocabulary_list=genre_vocab)
user_genre_ind_col = tf.feature_column.indicator_column(user_genre_col)
user_genre_emb_col = tf.feature_column.embedding_column(user_genre_col, 10)
# item genre embedding feature
item_genre_col = tf.feature_column.categorical_column_with_vocabulary_list(key="movieGenre1",
vocabulary_list=genre_vocab)
item_genre_ind_col = tf.feature_column.indicator_column(item_genre_col)
item_genre_emb_col = tf.feature_column.embedding_column(item_genre_col, 10)
(2)一阶特征
在tensorflow中,tf.keras.layers.Dense
和tf.keras.layers.DenseFeatures
意思不同,前者即常规的全连接层,后者如字面意思,根据feature_columns
生成稠密的张量。
# fm first-order categorical items
cat_columns = [movie_ind_col, user_ind_col, user_genre_ind_col, item_genre_ind_col]
deep_columns = [tf.feature_column.numeric_column('releaseYear'),
tf.feature_column.numeric_column('movieRatingCount'),
tf.feature_column.numeric_column('movieAvgRating'),
tf.feature_column.numeric_column('movieRatingStddev'),
tf.feature_column.numeric_column('userRatingCount'),
tf.feature_column.numeric_column('userAvgRating'),
tf.feature_column.numeric_column('userRatingStddev')]
first_order_cat_feature = tf.keras.layers.DenseFeatures(cat_columns)(inputs)
first_order_cat_feature = tf.keras.layers.Dense(1, activation=None)(first_order_cat_feature)
first_order_deep_feature = tf.keras.layers.DenseFeatures(deep_columns)(inputs)
first_order_deep_feature = tf.keras.layers.Dense(1, activation=None)(first_order_deep_feature)
## first order feature
first_order_feature = tf.keras.layers.Add()([first_order_cat_feature, first_order_deep_feature])
(3)二阶特征
tf.keras.layers.multiply
是向量之间的element-wise乘积运算。
second_order_cat_columns_emb = [tf.keras.layers.DenseFeatures([item_genre_emb_col])(inputs),
tf.keras.layers.DenseFeatures([movie_emb_col])(inputs),
tf.keras.layers.DenseFeatures([user_genre_emb_col])(inputs),
tf.keras.layers.DenseFeatures([user_emb_col])(inputs)
]
second_order_cat_columns = []
for feature_emb in second_order_cat_columns_emb:
feature = tf.keras.layers.Dense(64, activation=None)(feature_emb)
feature = tf.keras.layers.Reshape((-1, 64))(feature)
second_order_cat_columns.append(feature)
second_order_deep_columns = tf.keras.layers.DenseFeatures(deep_columns)(inputs)
second_order_deep_columns = tf.keras.layers.Dense(64, activation=None)(second_order_deep_columns)
second_order_deep_columns = tf.keras.layers.Reshape((-1, 64))(second_order_deep_columns)
second_order_fm_feature = tf.keras.layers.Concatenate(axis=1)(second_order_cat_columns + [second_order_deep_columns])
## second_order_deep_feature
deep_feature = tf.keras.layers.Flatten()(second_order_fm_feature)
deep_feature = tf.keras.layers.Dense(32, activation='relu')(deep_feature)
deep_feature = tf.keras.layers.Dense(16, activation='relu')(deep_feature)
在tensorflow2.0中,如果需要自定义一个layer类,则需要理解清楚__init__()
、bulid()
、call()
函数,似乎__init__()
和build()
函数都在对Layer进行初始化,都初始化了一些成员函数,而call()
函数则是在该layer被调用时执行。
【tensorflow官方推荐】tf.keras.layers.Layer
的所有派生类都必须实现这三个方法:__init__()
、build()
、call()
:
- __init__():参数初始化,如初始化卷积的一些参数;
- build():在 call() 函数第一次执行时会被调用一次,这时候可以知道输入数据的shape。返回去看一看,果然是 __init__() 函数中只初始化了输出数据的shape,
而输入数据的shape需要在 build() 函数中动态获取,这也解释了为什么在有 __init__() 函数时还需要使用 build() 函数 - call(): 当其被调用时会被执行。
class ReduceLayer(tf.keras.layers.Layer):
def __init__(self, axis, op='sum', **kwargs):
super().__init__()
self.axis = axis
self.op = op
assert self.op in ['sum', 'mean']
def build(self, input_shape):
pass
def call(self, input, **kwargs):
if self.op == 'sum':
return tf.reduce_sum(input, axis=self.axis)
elif self.op == 'mean':
return tf.reduce_mean(input, axis=self.axis)
return tf.reduce_sum(input, axis=self.axis)
second_order_sum_feature = ReduceLayer(1)(second_order_fm_feature)
second_order_sum_square_feature = tf.keras.layers.multiply([second_order_sum_feature, second_order_sum_feature])
second_order_square_feature = tf.keras.layers.multiply([second_order_fm_feature, second_order_fm_feature])
second_order_square_sum_feature = ReduceLayer(1)(second_order_square_feature)
## second_order_fm_feature
second_order_fm_feature = tf.keras.layers.subtract([second_order_sum_square_feature, second_order_square_sum_feature])
3.3 模型训练
拼接三个模块:first_order_feature
、second_order_fm_feature
、deep_feature
,将拼接后的向量送到最终的sigmoid
函数进行预测:
concatenated_outputs = tf.keras.layers.Concatenate(axis=1)([first_order_feature, second_order_fm_feature, deep_feature])
output_layer = tf.keras.layers.Dense(1, activation='sigmoid')(concatenated_outputs)
model = tf.keras.Model(inputs, output_layer)
# compile the model, set loss function, optimizer and evaluation metrics
model.compile(
loss='binary_crossentropy',
optimizer='adam',
metrics=['accuracy', tf.keras.metrics.AUC(curve='ROC'), tf.keras.metrics.AUC(curve='PR')])
# train the model
model.fit(train_dataset, epochs=5)
# evaluate the model
test_loss, test_accuracy, test_roc_auc, test_pr_auc = model.evaluate(test_dataset)
print('\n\nTest Loss {}, Test Accuracy {}, Test ROC AUC {}, Test PR AUC {}'.format(test_loss, test_accuracy,
test_roc_auc, test_pr_auc))
# print some predict results
predictions = model.predict(test_dataset)
for prediction, goodRating in zip(predictions[:12], list(test_dataset)[0][1][:12]):
print("Predicted good rating: {:.2%}".format(prediction[0]),
" | Actual rating label: ",
("Good Rating" if bool(goodRating) else "Bad Rating"))
四、代码结果
2022-03-27 20:34:51.550267: I tensorflow/core/platform/cpu_feature_guard.cc:142] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN)to use the following CPU instructions in performance-critical operations: AVX AVX2
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
Epoch 1/5
D:\anaconda1\envs\tensorflow\lib\site-packages\tensorflow\python\keras\engine\functional.py:540: UserWarning: Input dict contained keys ['rating', 'timestamp', 'userRatedMovie2', 'userRatedMovie3', 'userRatedMovie4', 'userRatedMovie5', 'userAvgReleaseYear', 'userReleaseYearStddev'] which did not match any model input. They will be ignored by the model.
warnings.warn(
7403/7403 [==============================] - 50s 7ms/step - loss: 7.5576 - accuracy: 0.5619 - auc: 0.5690 - auc_1: 0.6180
Epoch 2/5
7403/7403 [==============================] - 45s 6ms/step - loss: 0.7450 - accuracy: 0.6289 - auc: 0.6625 - auc_1: 0.7007/7403 [======>.......................] - ETA: 34s - loss: 0.9516 - accuracy: 0.5969 - auc: 0.6269 - auc_1: 0.6854
Epoch 3/5
7403/7403 [==============================] - 46s 6ms/step - loss: 0.6031 - accuracy: 0.6795 - auc: 0.7342 - auc_1: 0.7610
Epoch 4/5
7403/7403 [==============================] - 46s 6ms/step - loss: 0.5639 - accuracy: 0.7133 - auc: 0.7753 - auc_1: 0.8006
Epoch 5/5
7403/7403 [==============================] - 45s 6ms/step - loss: 0.5255 - accuracy: 0.7410 - auc: 0.8112 - auc_1: 0.8356
1870/1870 [==============================] - 5s 3ms/step - loss: 0.6117 - accuracy: 0.6695 - auc: 0.7239 - auc_1: 0.7510
Test Loss 0.611655592918396, Test Accuracy 0.6694741249084473, Test ROC AUC 0.7239346504211426, Test PR AUC 0.7510392069816589
Predicted good rating: 75.24% | Actual rating label: Good Rating
Predicted good rating: 60.70% | Actual rating label: Bad Rating
Predicted good rating: 89.65% | Actual rating label: Bad Rating
Predicted good rating: 90.05% | Actual rating label: Bad Rating
Predicted good rating: 46.65% | Actual rating label: Bad Rating
Predicted good rating: 57.64% | Actual rating label: Good Rating
Predicted good rating: 52.52% | Actual rating label: Good Rating
Predicted good rating: 24.43% | Actual rating label: Bad Rating
Predicted good rating: 79.91% | Actual rating label: Good Rating
Predicted good rating: 25.34% | Actual rating label: Bad Rating
Predicted good rating: 71.32% | Actual rating label: Good Rating
Predicted good rating: 67.41% | Actual rating label: Good Rating
5.其他特征相交方法
除了点积和元素积这两个操作外,还有没有其他的方法能处理两个 Embedding 向量间的特征交叉?
- 是否可以把这两个embedding向量组合之后再做一次embedding。
- 对于两个Embedding向量做一次pooling层,采用average/max pooling。
- 元素减,外积等交叉操作,除此之外还有一些自定义的复杂交叉操作,比如google cross&deep模型中自定义的一些cross操作。
- 对特征embedding做concat、average pooling、sum pooling 确实都可以,但针对性不强,还是一些专门针对两个embedding特征交叉设计的操作效果好一些。比如我们提到的dot product, element-wise product 和element-wise minus. outer product等等。
六、机型注意事项
6.1 数值型特征不参与特征交叉
(1)DeepFM的图示中,输入均是类别型特征的one-hot或embedding,请问是因为特征交叉仅适用于类别型特征的交叉吗?数值型特征之间,数值型与类别型特征之间能否进行交叉呢?另外,在DeepFM的wide部分中一阶交叉项是否可以包含未参与特征交叉的数值型特征呢?
【答】按照DeepFM原论文,数值型特征是不参与特征交叉的,因为特征交叉的操作是在两个embedding向量间进行的。但是如果可以把通过分桶操作把连续型特征处理成离散型特征,然后再加Embedding层,就可以让数值型特征也参与特征交叉。
因为内积只能变成同一个维度,从
categorical feature embedding
到embedding_dim
维度,数值类型需要映射到embedding_dim
维度。数值映射可以通过合并或乘以embedding_dim
向量来完成。
6.2 特征交叉
(2)原FM中二阶交叉项中隐向量的内积仅作为权重,但从sparrow代码看,其中的内积直接作为了交叉项的结果,而没有了初始特征的交叉。
问题:这样做是因为教程里所选的特征是one-hot格式,所以维度可能不一致,从而无法进行初始特征的交叉吗?
【答】原FM中内积作为权重,然后还要乘以特征本身的值。但在DeepFM中,所有的参与交叉的特征都先转换成了embedding,而且由于是one-hot,所以特征的值就是1,参不参与交叉都无所谓。所以直接使用embedding的内积作为交叉后的值就可以了。
6.3 embedding维度相差很大时咋办
(3)按FM的交叉方式,不同特征的embedding 向量维度要相同,但实际不同离散特征的维度可能相差很大,如果想用不同的embedding 维度,那应该怎样做交叉,业界有没有这样的处理方式?
【答】几乎不可以。如果一定要做的话,也要在不同embedding层上再加上一层fc layer或者embedding layer,把他们变成一致的,然后交叉。
Reference
[1]DeepFM算法解析及Python实现
[2]推荐算法DeepFM原理介绍及tensorflow代码实现(重点)
[3]CTR预估经典论文详解(二)——DeepFM模型
[4] deepFM论文地址:https://www.ijcai.org/proceedings/2017/0239.pdf
[5] (读论文)推荐系统之ctr预估-DeepFM模型解析
[6]推荐系统召回四模型之:全能的FM模型(张俊林)
[7] https://blog.csdn.net/qq_32806793/article/details/114003470
[8]tensorflow2.0中Layer的__init__(),build(), call()函数
文章出处登录后可见!