序言:
DeepCrossing模型是由微软提出的应用于Bing搜索广告推荐的模型,作为datawhale深度推荐模型的开篇,DeepCrossing模型结构并不复杂,原理也很简单,可谓是相当新手友好了。
1 模型原理
DeepCrossing模型的结构设计是明确以问题为导向的:
问题1:推荐系统中存在大量的类别特征,例如广告ID等,这些特征编码之后会形成大型稀疏矩阵,不利于神经网络学习。
解决办法:采用Embedding将稀疏向量稠密化。
问题二:如何让特征自动交叉组合。
解决办法:采用多层残差网络。神经网络的一大特点就是可以将输入特征进行层层的交叉组合,网络越深则特征组合越复杂,能够学习到的就越多。然而网络层数太深就会存在梯度消失的问题,导致网络的学习能力下降。残差网络能够跳过中间层,因此相较于一般的神经网络可以有更大的深度。
问题三:如何在输出层达成问题设定的优化目标。
解决办法:在最终输出之前加入Scoring层,用来拟合优化目标,在CTR预估中通常采用逻辑回归来输出概率值。
将以上方法串联起来,就得到了DeepCrossing模型的结构:将输入特征划分为数值型特征和类别型特征,对类别型特征做Embedding处理,然后将处理后的类别型特征与数值型特征拼接起来,一起放进残差网络中进行学习,学习的结果放入Scoring层拟合设定的目标,最终就得到了预期的输出结果。
2 模型构建
2.1 构建数据集
采用已经处理过的criteo部分数据,数据集包括39个特征,其中I1 ~ I13为数值型特征,C1 ~ C26为类别特征,目标是预测CTR。以下是训练集的部分数据:
因为DeepCrossing模型要对类别型特征做Embedding处理,而数值型特征不做处理,因此在构建数据集时要分离开数值型特征和类别特征:
# 类别型特征
cate_feas = [col for col in train_df.columns if col[0]=='C']
# 数值型特征
num_feas = [col for col in train_df.columns if col[0]=='I']
# 构造训练数据
X_train = [train_df[num_feas].values.astype('float32'), train_df[cate_feas].values.astype('float32')]
y_train = train_df['Label'].values.astype('int32')
X_valid = [valid_df[num_feas].values.astype('float32'), valid_df[cate_feas].values.astype('float32')]
y_valid = valid_df['Label'].values.astype('int32')
模型中要对每一个类别型特征都做Embedding处理,其作用是把稀疏的类别编码矩阵稠密化。假设一个类别特征中包含1000个类别,经过one-hot编码后就得到一个长度为1000的稀疏向量[0, …, 0, 1, 0, …, 0],我们将Embedding的输出维度设置为8,那么Embedding就能将这个稀疏向量转化为长度为8的稠密向量。因此,Embedding层有三个重要参数:
- Input_dim:输入维度。就是类别特征中所包含的类别个数,在上述假设中就是1000。
- Input_length:输入长度。这取决于类别特征的输入形式,在上述假设中采用one-hot编码,那么这个类别特征的每一行数据就是长度为1000的向量,所以输入长度是1000。在这个问题中,预处理criteo数据集时采用的是LabelEncoder编码,每一行数据就只是一个表示类别的数值,所以输入长度是1。
- output_dim:输出维度。这个值是自己设定的,表示你希望将输入数据压缩到多少维,在上述假设中输出维度就是8。
为了方便后续构建模型,采用字典来保存数值型特征和类别型特征的输入维度及输出维度:
# 构建数值型(稠密)特征字典
def denseFeature(feat):
return {'feat': feat}
# 构建类别型(稀疏)特征字典,其中feat_num表示类别特征的输入维度,embed_dim表示输出维度
def sparseFeature(feat, feat_num, embed_dim=4):
return {'feat': feat, 'feat_num': feat_num, 'embed_dim': embed_dim}
# 将数值型特征和类别型特征共同保存在列表中
feature_columns = [[denseFeature(feat) for feat in num_feas]] + \
[[sparseFeature(feat, len(data_df[feat].unique()), embed_dim=embed_dim)
for feat in cate_feas]]
2.2 构建残差模块
DeepCrossing采用残差网络来进行特征之间的交叉组合,一个残差网络是由多个残差模块堆叠而成的。
残差模块的结构很简单,一个常规的残差网络由两个Dense层组成。对于输入数据x,在进入残差模块后就兵分两路,一路直达终点,另一路经过Dense-ReLU-Dense得到x1,将x和x1相加后输入ReLU,就构成了一个残差模块。注意x要与x1相加,因此要保证x1的维度与x的维度相同,也即是第二个Dense层的节点个数要与x的维度相同。
在tf.keras中可以通过继承Layer类来自定义网络层:
#构建残差模块
class ResidualLayer(Layer):
def __init__(self, hidden_unit, dim_stack):
super(ResidualLayer, self).__init__()
self.layer1 = Dense(units=hidden_unit, activation='relu')
self.layer2 = Dense(units=dim_stack, activation=None)
self.relu = ReLU()
def call(self, inputs, **kwargs):
x = inputs
x = self.layer1(x)
x = self.layer2(x)
outputs = self.relu(x + inputs)
return outputs
2.3 构建DeepCrossing模型
同样的,通过继承Model类来自定义模型:
#构建Deep Crossing模型
class DeepCrossing(keras.Model):
def __init__(self, feature_columns, hidden_units, dropout_rate=0, embed_reg=1e-4):
super(DeepCrossing, self).__init__()
#分别取出数值型特征和类别特征
self.dense_feas, self.sparse_feas = feature_columns
#对类别型特征中的每个特征都定义一个Embedding层
self.embed_layers = {
'embed_' + str(i): Embedding(input_dim=feat['feat_num'],
input_length=1,
output_dim=feat['embed_dim'],
embeddings_initializer='random_uniform',
embeddings_regularizer=l2(embed_reg))
for i, feat in enumerate(self.sparse_feas)
}
# 所有类别型特征经过Embedding后拼接起来得到总的维度,也就是所有类别特征Embedding层的输出维度之和
embed_dim = sum([feat['embed_dim'] for feat in self.sparse_feas])
# 将数值型特征和Embedding后的类别型特征拼接起来即是残差网络的输入,于是残差网络的输入维度就是数值型特征长度和embed_dim之和
dim_stack = len(self.dense_feas) + embed_dim
# 设置残差网络
self.resnet = [ResidualLayer(unit, dim_stack) for unit in hidden_units]
# 设置Dropout层
self.res_dropout = Dropout(dropout_rate)
self.dense = Dense(1)
def call(self, inputs):
# 分别取出数值型输入数据和类别型输入数据
dense_inputs, sparse_inputs = inputs
# 将类别型输入数据输入Embedding层
sparse_embed = tf.concat([self.embed_layers['embed_{}'.format(i)](sparse_inputs[:, i]) for i in range(sparse_inputs.shape[1])], axis=-1)
# 拼接数值型输入数据和Embedding处理后的类别型数据
stack = tf.concat([sparse_embed, dense_inputs], axis=-1)
r = stack
# 将所有数据输入残差网络
for res in self.resnet:
r = res(r)
r = self.res_dropout(r)
# 采用逻辑回归作为Scoring层,输出点击率值
outputs = tf.nn.sigmoid(self.dense(r))
return outputs
def summary(self):
dense_inputs = Input(shape=(len(self.dense_feas), ), dtype=tf.float32)
sparse_inputs = Input(shape=(len(self.sparse_feas), ), dtype=tf.float32)
keras.Model(inputs=[dense_inputs, sparse_inputs], outputs=self.call([dense_inputs, sparse_inputs])).summary()
2.4 模型训练
这里就是熟悉的常规步骤了:创建模型-编译-训练。
# 设置模型参数
embed_dim = 8
dropout_rate = 0.5
hidden_units = [256, 128, 64]
learning_rate = 0.001
batch_size = 1024
epochs = 10
# 创建并编译模型
mirrored_strategy = tf.distribute.MirroredStrategy()
with mirrored_strategy.scope():
model = DeepCrossing(feature_columns, hidden_units)
model.summary()
model.compile(loss=binary_crossentropy, optimizer=Adam(learning_rate=learning_rate), metrics=['AUC'])
# 训练模型
history = model.fit(X_train, y_train, epochs=epochs, callbacks=[EarlyStopping(monitor='val_auc', patience=2, restore_best_weights=True)],
batch_size=batch_size, validation_data=(X_valid, y_valid), verbose=2)
最终得到训练集和验证集曲线如下:
3 总结
总的来说,DeepCrossing的模型虽然结构简单,但却基本上完整地解决了深度学习在推荐系统中的应用问题,特别是采用残差网络自动完成特征的深度交叉与组合,无需人工构造特征,能够方便地完成端到端的模型训练。