文章目录
原文链接:
https://link.zhihu.com/?target=https%3A//arxiv.org/pdf/1904.04447.pdf
摘要
点击率预测是推荐系统中的一项重要任务,旨在估计用户点击给定项目的概率。最近,已经提出了许多深度模型来从原始特征中学习低阶和高阶特征交互。然而,由于有用的交互总是稀疏的,因此 DNN 很难在大量参数下有效地学习它们。在实际场景中,人工特征能够提高深度模型的性能(例如 Wide & Deep Learning),但特征工程成本高昂且需要领域知识,使其在不同场景中不切实际。因此,有必要自动扩充特征空间。在本文中,我们提出了一种新颖的卷积神经网络特征生成 (FGCNN) 模型,该模型具有两个组件:特征生成和深度分类器。特征生成利用 CNN 的优势来生成局部模式并将它们重新组合以生成新特征。深度分类器采用 IPNN 的结构从增强的特征空间中学习交互。三个大规模数据集的实验结果表明,FGCNN 显着优于九个最先进的模型。此外,当应用一些最先进的模型作为深度分类器时,总是可以获得更好的性能,这表明我们的 FGCNN 模型具有很好的兼容性。这项工作探索了 CTR 预测的新方向:通过自动识别重要特征来降低 DNN 的学习难度非常有用。
Figure 1,与WDL比较图如下:
1. 介绍
点击率(CTR)是推荐系统的一项关键任务,它估计用户点击给定项目的概率。在一个价值十亿美元的在线广告应用程序中,候选广告的排名策略是 CTR×bid,其中“bid”是广告被点击后系统获得的利润。在此类应用中,CTR 预测模型的性能是决定系统收入的核心因素之一。
CTR 预测任务的关键挑战是有效地对特征交互进行建模。广义线性模型,例如 FTRL,在实践中表现良好,但这些模型缺乏学习特征交互的能力。为了克服这一限制,因子分解机及其变体被提出将成对特征交互建模为潜在向量的内积,并显示出有希望的结果。最近,深度神经网络 (DNN) 在计算机视觉和自然语言处理方面取得了显着进展。并且已经提出了一些用于 CTR 预测的深度学习模型,例如PIN、xDeepFM等。这些模型将原始特征提供给深度神经网络,以显式或隐式地学习特征交互。从理论上讲,DNN 能够从原始特征中学习任意特征交互。然而,由于与原始特征的组合空间相比,有用的交互通常是稀疏的,因此从大量参数中有效地学习它们是非常困难的。
观察到这些困难,WDL利用wide部分中的特征工程来帮助deep部分的学习。在人工特征的帮助下,deep部分的性能得到显着提升(离线 AUC 提升 0.6%,在线 CTR 提升 1%)。然而,特征工程可能很昂贵并且需要领域知识。如果我们可以通过机器学习模型自动生成复杂的特征交互,它将更加实用和健壮。
因此,如 Figure 1 所示,我们提出了一个自动特征生成的通用框架。将原始特征输入到机器学习模型(由图 1 中的红色框表示)以识别和生成新特征。之后,将原始特征和生成的新特征结合起来并输入到深度神经网络中。生成的特征能够通过提前捕获稀疏但重要的特征交互来降低深度模型的学习难度。
自动特征生成最直接的方法是执行多层感知器(MLP)并使用隐藏的神经元作为生成的特征。然而,如上所述,由于有用的特征交互通常是稀疏的,MLP 很难从巨大的参数空间中学习这种交互。例如,假设我们有四个用户特征:姓名、年龄、身高、性别来预测用户是否会下载在线游戏。假设年龄和性别之间的特征交互是唯一重要的信号,因此最佳模型应该识别这个并且只有这个特征交互。当只使用一个隐藏层执行 MLP 时,与 Name 和 Height 的嵌入相关的最佳权重应该全为 0,这是相当难以实现的。
作为一种先进的神经网络结构,卷积神经网络(CNN)在计算机视觉和自然语言处理领域取得了巨大的成功。在 CNN 中,共享权重和池化机制的设计大大减少了寻找重要局部模式所需的参数数量,这将缓解后期 MLP 结构的优化困难。因此,CNN 提供了一个潜在的好解决方案来实现我们的想法(识别稀疏但重要的特征交互)。但是,直接应用 CNN 可能会导致性能不理想。在CTR预测中,原始特征的不同排列顺序没有不同的含义。例如,特征的排列顺序是(姓名、年龄、身高、性别)还是(年龄、姓名、身高、性别)对于描述样本的语义没有任何区别,这与图像和句子的情况完全不同。.如果我们只使用 CNN 提取的邻居模式,许多有用的全局特征交互将会丢失。这也是 CNN 模型在 CTR 预测任务中表现不佳的原因。为了克服这个限制,我们执行相互补充的 CNN 和 MLP,以学习用于特征生成的全局-局部特征交互。
在本文中,我们提出了一种新的 CTR 预测任务模型,即卷积神经网络的特征生成(FGCNN),它由两部分组成:特征生成(Feature Generation)和深度分类器(Deep Classifier)。在特征生成Feature Generation
中,CNN+MLP 结构旨在从原始特征中识别和生成新的重要特征。更具体地说,CNN 用于学习邻居特征交互,而 MLP 用于重新组合它们以提取全局特征交互。在特征生成之后,可以通过结合原始特征和新特征来扩大特征空间。在深度分类器Deep Classifier
中,几乎所有最先进的网络结构(如 PIN、xDeepFM、DeepFM)都可以采用。所以,我们的模型与推荐系统中最先进的模型具有良好的兼容性。为了便于说明,我们将采用 IPNN 模型 作为 FGCNN 中的深度分类器,因为它在模型复杂性和准确性之间取得了良好的折衷。三个大规模数据集的实验结果表明,FGCNN 显着优于九个最先进的模型,证明了 FGCNN 的有效性。在 Deep Classifier 中采用其他模型时,总是能获得更好的性能,这显示了生成特征的有用性。逐步分析表明,FGCNN 中的每个组件都有助于最终性能。与传统的 CNN 结构相比,我们的 CNN+MLP 结构在原始特征顺序发生变化时表现更好、更稳定,这证明了 FGCNN 的鲁棒性。
总而言之,本文的主要贡献可以突出如下:
- 确定了 CTR 预测的一个重要方向:通过提前自动生成重要特征来降低深度学习模型的优化难度,既必要又有用。
- 我们提出了一种用于自动特征生成和分类的新模型——FGCNN,它由两个部分组成:特征生成
Feature Generation
和深度分类器Deep Classifier
。特征生成利用相互补充的 CNN 和 MLP 来识别重要但稀疏的特征。此外,几乎所有其他 CTR 模型都可以应用于深度分类器中,以基于生成的和原始特征进行学习和预测。 - 在三个大规模数据集上的实验证明了 FGCNN 模型的整体有效性。当生成的特征用于其他模型时,总是能获得更好的性能,这表明我们的 FGCNN 模型具有很好的兼容性和鲁棒性。
本文的其余部分组织如下:第 2 节详细介绍了所提出的 FGCNN 模型。实验结果将在第 3 节中展示和讨论。相关工作将在第 4 节中介绍。第 5 节总结了本文。
2. 卷积神经网络模型的特征生成
2.1 概貌
在本节中,我们将详细描述通过卷积神经网络(FGCNN)模型提出的特征生成模型。表1总结了使用的符号。
如 Figure 2 所示,FGCNN 模型由两部分组成:特征生成和深度分类器。更具体地说,特征生成侧重于识别有用的局部和全局模式以生成新特征作为原始特征的补充,而深度分类器通过深度学习模型基于增强的特征空间进行学习和预测。除了这两个组件,我们还将形式化我们模型的特征嵌入。这些组件的详细信息在以下小节中介绍。
2.2 Feature Embedding
在大多数 CTR 预测任务中,数据以多字段分类形式收集,因此每个数据实例通常通过 one-hot 编码转换为高维稀疏向量。例如,(Gender=Male, Height=175, Age=18, Name=Bob) 可以表示为:
如第 1 节所述,从原始特征生成新特征有助于提高深度学习模型的性能(如 Wide & Deep Learning 所证明的)。为了实现这个目标,特征生成设计了一个合适的神经网络结构来识别有用的特征交互,然后自动生成新的特征。如第 1 节所述,仅使用 MLP 或 CNN 无法从原始特征生成有效的特征交互,原因如下:
- 首先,有用的特征交互在原始特征的组合空间中总是稀疏的。因此,MLP 很难从大量参数中学习它们。
- 其次,虽然 CNN 可以缓解 MLP 通过减少参数的数量,它只生成相邻的特征交互,这可能会丢失许多有用的全局特征交互。
为了克服单独应用 MLP 或 CNN 的弱点,如 Figure 2 的上半部分所示,我们执行 CNN 和 MLP 作为相互补充的特征生成。
Figure 3 展示了一个 CNN+重组结构以捕获全局特征交互的示例。可以观察到,CNN 使用有限数量的参数学习有用的邻居特征模式,而重组层(这是一个完全连接的层)基于 CNN 提供的邻居模式生成全局特征交互。因此,通过这种神经网络结构可以有效地生成重要特征,与直接应用 MLP 进行特征生成相比,它的参数更少。
在接下来的部分中,我们详细介绍了特征生成的CNN+重组结构,即卷积层、池化层和重组层。
2.3.1 Convolutional Layer
2.3.2 Pooling Layer
2.3.3 Recombination Layer
2.3.4 Concatenation
2.4 Deep Classifier
2.4.1 Network Structure
IPNN 模型结合了 FM 和 MLP 的学习能力。它利用 FM 层通过内积运算从嵌入向量中提取成对特征交互。之后,将输入特征的嵌入和 FM 层的结果连接起来并馈送到 MLP 进行学习。IPNN 的性能略低于 PIN,但 IPNN 的效率要高得多。我们将说明 IPNN 模型的网络结构。
PNN的乘积方式为内积即为IPNN
2.4.2 Batch Normalization
2.4.3 Objective Function.
3. paper结果
在本文中,我们提出了一种用于 CTR 预测的 FGCNN 模型,旨在通过提前识别重要特征来降低 DNN 模型的学习难度。该模型由两个组件组成:特征生成和深度分类器。特征生成利用 CNN 的优势来识别有用的局部模式,并通过引入重组层从局部模式的重组中生成全局新特征来缓解 CNN 的弱点。在深度分类器中,大多数现有的深度模型都可以应用于增强特征空间。在三个大规模数据集上进行了广泛的实验,结果表明 FGCNN 优于九个最先进的模型。此外,当在深度分类器中应用其他模型时,与没有特征生成的原始模型相比,总是可以获得更好的性能,这证明了生成特征的有效性。一步一步的实验表明,FGCNN 中的每个组件都有助于最终的性能。此外,与传统的CNN结构相比,我们在特征生成中的CNN+重组结构在打乱原始特征的排列顺序时总是表现得更好、更稳定。这项工作探索了 CTR 预测的新方向,即首先自动生成重要特征而不是直接将原始嵌入提供给深度学习模型是有效的。
tf2 复现
# coding:utf-8
import time
import numpy as np, pandas as pd
import tensorflow as tf
from tensorflow.python.layers import utils
from tensorflow.keras import layers
from tensorflow.keras import Model
from tensorflow.keras import optimizers
from tensorflow.keras import metrics
from tensorflow.keras import regularizers
from keras import backend as K
from sklearn.model_selection import train_test_split
from sklearn.metrics import precision_score, recall_score, accuracy_score, f1_score, roc_auc_score
from sklearn.utils import shuffle
from tools import *
from settings import *
class DNN_layers(layers.Layer):
def __init__(self, hidden_units, activation, droup_out):
super(DNN_layers, self).__init__()
self.DNN = tf.keras.Sequential()
for hidden in hidden_units:
# self.DNN.add(layers.Dense(hidden, kernel_regularizer=regularizers.l2()))
self.DNN.add(layers.Dense(hidden))
self.DNN.add(layers.BatchNormalization())
self.DNN.add(layers.Activation(activation))
self.DNN.add(layers.Dropout(droup_out))
def call(self, inputs, **kwargs):
return self.DNN(inputs)
class FGCNN_layers(layers.Layer):
def __init__(self, filters=[14, 16], kernel_with=[7, 7], dnn_maps=[3, 3], pooling_width=[2, 2]):
super(FGCNN_layers, self).__init__()
self.filters = filters
self.kernel_with = kernel_with
self.dnn_maps = dnn_maps
self.pooling_width = pooling_width
def build(self, input_shape):
embedding_size = int(input_shape[-1])
pooling_shape = input_shape.as_list() + [1, ] # [None, n, k, 1]
self.conv_layers = []
self.pooling_layers = []
self.dense_layers = []
for i in range(len(self.filters)):
conv_output_shape = self._conv_output_shape(pooling_shape, (self.kernel_with[i], 1))
pooling_shape = self._pooling_output_shape(conv_output_shape, (self.pooling_width[i], 1))
self.conv_layers.append(
layers.Conv2D(filters=self.filters[i], kernel_size=(self.kernel_with[i], 1), strides=(1, 1), padding='same', activation='tanh')
)
self.pooling_layers.append(
layers.MaxPooling2D(pool_size=(self.pooling_width[i], 1))
)
self.dense_layers.append(layers.Dense(pooling_shape[1] * embedding_size * self.dnn_maps[i], activation='tanh', use_bias=True))
self.flatten_layer = layers.Flatten()
def call(self, inputs, **kwargs):
k = inputs.shape[-1]
new_feature_list = []
x = tf.expand_dims(inputs, axis=-1) # [None, n, k, 1] 最后一维为通道
for i in range(len(self.filters)):
x = self.conv_layers[i](x) # [None, n, k, filters[i]]
x = self.pooling_layers[i](x) # [None, n/poolwidth[i], k, filters[i]]
out = self.flatten_layer(x) # [None, n/poolwidth[i] * k * filters[i]]
out = self.dense_layers[i](out)
out = tf.reshape(out, shape=(-1, out.shape[1] // k, k))
new_feature_list.append(out)
output = tf.concat(new_feature_list, axis=1)
return output
def _conv_output_shape(self, input_shape, kernel_size):
space = input_shape[1:-1] # [n, k]
new_space = []
for i in range(len(space)):
new_dim = utils.conv_output_length(space[i], kernel_size[i], padding='same', stride=1, dilation=1)
new_space.append(new_dim)
return ([input_shape[0]] + new_space + [self.filters])
def _pooling_output_shape(self, input_shape, pool_size):
rows = input_shape[1]
cols = input_shape[2]
rows = utils.conv_output_length(rows, pool_size[0], 'valid', pool_size[0])
cols = utils.conv_output_length(cols, pool_size[1], 'valid', pool_size[1])
return [input_shape[0], rows, cols, input_shape[3]]
class InnerProductLayer(layers.Layer):
def __init__(self):
super().__init__()
def call(self, inputs, **kwargs): # [None, field, k]
if K.ndim(inputs) != 3:
raise ValueError(
"Unexpected inputs dimensions %d, expect to be 3 dimensions" % (K.ndim(inputs)))
field_num = inputs.shape[1]
# for循环计算点乘,复杂度高
# InnerProduct = []
# for i in range(field_num - 1):
# for j in range(i + 1, field_num):
# Inner = inputs[:, i, :] * inputs[:, j, :] #[None, k]
# Inner = tf.reduce_sum(Inner, axis=1, keepdims=True) #[None, 1]
# InnerProduct.append(Inner)
# InnerProduct = tf.concat(InnerProduct, axis=1) #[None, field*(field-1)/2]
# 复杂度更低,先将要相乘的emb找出,存为两个矩阵,然后点乘即可
row, col = [], []
for i in range(field_num - 1):
for j in range(i + 1, field_num):
row.append(i)
col.append(j)
p = tf.transpose(tf.gather(tf.transpose(inputs, [1, 0, 2]), row), [1, 0, 2]) # [None, pair_num, k]
q = tf.transpose(tf.gather(tf.transpose(inputs, [1, 0, 2]), col), [1, 0, 2]) # [None, pair_num, k]
InnerProduct = tf.reduce_sum(p * q, axis=-1) # [None, pair_num]
return InnerProduct
class OuterProductLayer(layers.Layer):
def __init__(self):
super().__init__()
def build(self, input_shape):
self.field_num = input_shape[1]
self.k = input_shape[2]
self.pair_num = self.field_num * (self.field_num - 1) // 2
# 该形状方便计算,每个外积矩阵对应一个,共pair个w矩阵
self.w = self.add_weight(name='W', shape=(self.k, self.pair_num, self.k),
initializer=tf.random_normal_initializer(),
regularizer=tf.keras.regularizers.l2(1e-4),
trainable=True)
def call(self, inputs, **kwargs): # [None, field, k]
if K.ndim(inputs) != 3:
raise ValueError(
"Unexpected inputs dimensions %d, expect to be 3 dimensions" % (K.ndim(inputs)))
row, col = [], []
for i in range(self.field_num - 1):
for j in range(i + 1, self.field_num):
row.append(i)
col.append(j)
p = tf.transpose(tf.gather(tf.transpose(inputs, [1, 0, 2]), row), [1, 0, 2]) # [None, pair_num, k]
q = tf.transpose(tf.gather(tf.transpose(inputs, [1, 0, 2]), col), [1, 0, 2]) # [None, pair_num, k]
p = tf.expand_dims(p, axis=1) # [None, 1, pair_num, k] 忽略掉第一维,需要两维与w一致才能进行点乘
tmp = tf.multiply(p, self.w) # [None, 1, pair_num, k] * [k, pair_num, k] = [None, k, pair_num, k]
tmp = tf.reduce_sum(tmp, axis=-1) # [None, k, pair_num]
tmp = tf.multiply(tf.transpose(tmp, [0, 2, 1]), q) # [None, pair_num, k]
OuterProduct = tf.reduce_sum(tmp, axis=-1) # [None, pair_num]
return OuterProduct
class FGCNN(Model):
def __init__(self, dense_feature_columns, sparse_feature_columns, hidden_units, activation, dropout,
mode='inner', use_fgcnn=False, filters=[14, 16], kernel_with=[7, 7], dnn_maps=[3, 3], pooling_width=[2, 2]):
super(FGCNN, self).__init__()
self.mode = mode
self.sparse_feature_columns = sparse_feature_columns
self.dense_feature_columns = dense_feature_columns
self.emb_layers = [layers.Embedding(feat['vocabulary_size'], feat['embed_dim']) for i, feat in
enumerate(self.sparse_feature_columns)]
self.use_fgcnn = use_fgcnn
if use_fgcnn:
self.fgcnn_layer = FGCNN_layers(filters=filters, kernel_with=kernel_with, dnn_maps=dnn_maps,
pooling_width=pooling_width)
self.inner_product_layer = InnerProductLayer()
self.outer_product_layer = OuterProductLayer()
self.dnn_layers = DNN_layers(hidden_units, activation, dropout)
self.out_layers = layers.Dense(1)
def call(self, inputs, training=None, mask=None):
dense_inputs, sparse_inputs = inputs[:, :13], inputs[:, 13:]
sparse_embed = [layer(sparse_inputs[:, i]) for i, layer in enumerate(self.emb_layers)] # 26 * [None, k], field=26
sparse_embed = tf.transpose(tf.convert_to_tensor(sparse_embed), [1, 0, 2]) # [None, field, k]
# product之前加入fgcnn层
if self.use_fgcnn:
fgcnn_out = self.fgcnn_layer(sparse_embed)
sparse_embed = tf.concat([sparse_embed, fgcnn_out], axis=1)
z = sparse_embed # [None, field, k]
embed = tf.reshape(sparse_embed, shape=(-1, sparse_embed.shape[1] * sparse_embed.shape[2])) # [None, field*k]
# inner product
if self.mode == 'inner':
inner_product = self.inner_product_layer(z) # [None, field*(field-1)/2]
inputs = tf.concat([embed, inner_product], axis=1)
# outer product
elif self.mode == 'outer':
outer_product = self.outer_product_layer(z) # [None, field*(field-1)/2]
inputs = tf.concat([embed, outer_product], axis=1)
# inner and outer product
elif self.mode == 'both':
inner_product = self.inner_product_layer(z) # [None, field*(field-1)/2]
outer_product = self.outer_product_layer(z) # [None, field*(field-1)/2]
inputs = tf.concat([embed, inner_product, outer_product], axis=1)
# Wrong Input
else:
raise ValueError("Please choice mode's value in 'inner' 'outer' 'both'.")
inputs = tf.concat([dense_inputs, inputs], axis=1)
out = self.dnn_layers(inputs)
return tf.nn.sigmoid(self.out_layers(out))
if __name__ == '__main__':
data = pd.read_csv(criteo_sampled_data_path)
data = shuffle(data, random_state=42)
data_X = data.iloc[:, 1:]
data_y = data['label'].values
# I1-I13:总共 13 列数值型特征
# C1-C26:共有 26 列类别型特征
dense_features = ['I' + str(i) for i in range(1, 14)]
sparse_features = ['C' + str(i) for i in range(1, 27)]
dense_feature_columns = [denseFeature(feat) for feat in dense_features]
sparse_feature_columns = [sparseFeature(feat, data_X[feat].nunique(), 8) for feat in sparse_features]
feature_columns = [dense_feature_columns + sparse_feature_columns]
tmp_X, test_X, tmp_y, test_y = train_test_split(data_X, data_y, test_size=0.05, random_state=42, stratify=data_y)
train_X, val_X, train_y, val_y = train_test_split(tmp_X, tmp_y, test_size=0.05, random_state=42, stratify=tmp_y)
print(len(train_y))
print(len(val_y))
print(len(test_y))
model = FGCNN(dense_feature_columns=dense_feature_columns, sparse_feature_columns=sparse_feature_columns,
hidden_units=(64, 128, 256), activation='relu', dropout=0.0)
callbacks = [tf.keras.callbacks.EarlyStopping(monitor='val_loss', min_delta=1e-8, patience=3, verbose=1)]
# adam = optimizers.Adam(lr=config['train']['adam_lr'], beta_1=0.95, beta_2=0.96, decay=config['train']['adam_lr'] / config['train']['epochs'])
adam = optimizers.Adam(lr=1e-4, beta_1=0.95, beta_2=0.96)
model.compile(
optimizer=adam,
loss='binary_crossentropy',
metrics=[metrics.AUC(), metrics.Precision(), metrics.Recall(), 'accuracy'],
# metrics=[metrics.AUC(), 'accuracy'],
)
model.fit(
train_X.values, train_y,
validation_data=(val_X.values, val_y),
batch_size=2000,
epochs=30,
verbose=2,
shuffle=True,
callbacks=callbacks,
)
scores = model.evaluate(test_X.values, test_y, verbose=2)
print(' %s: %.4f' % (model.metrics_names[0], scores[0]))
print(' %s: %.4f' % (model.metrics_names[1], scores[1]))
print(' %s: %.4f' % (model.metrics_names[2], scores[2]))
print(' %s: %.4f' % (model.metrics_names[3], scores[3]))
print(' %s: %.4f' % ('F1', (2 * scores[2] * scores[3]) / (scores[2] + scores[3])))
print(' %s: %.4f' % (model.metrics_names[4], scores[4]))
# y_pre_sc = model.predict(test_X.values, batch_size=256)
print('==============================')
print()
print()
print()
其它文章:
https://zhuanlan.zhihu.com/p/70087762
https://zhuanlan.zhihu.com/p/273274811
https://www.cnblogs.com/Jesee/p/11129251.html