简介
传统上,我们采用大量特征工程的LR模型来应对大规模稀疏数据的分类和回归任务。在本论文里,将宽度模型与深度神经网络进行联合训练,结合了记忆与泛化能力。深宽模型要比单独的深模型和宽模型的效果要好。
推荐系统与一般的搜索和排序问题的共同点在于,要同时满足记忆和泛化要求。记忆可大致定义为学习物品和特征的共现率,以及利用历史数据中的相关性。泛化可基于相关性,探索之前很少出现或从未出现的交叉特征。基于记忆的推荐系统更加局部化,会将物品和产生对应动作的用户直接相关,而泛化则更趋向于推荐多样化的物品。
工业界的大规模数据的推荐排序系统,可以使用广义线性模型。例如:逻辑回归简单、可解释,因此被广泛使用。对于类别特征,模型输入的是One-Hot后的编码,例如(user_installed_app=netflix)用户安装netflix,则值为1。在稀疏数据上进行特征交叉,可以实现一定的记忆功能,例如(user_installed_app=netflix, impression_app=pandora), label=1如果用户安装netflix,然后展示了pandora,这解释了特征共现如何影响标签label结果。通过进行交叉特征来增加模型的泛化能力。例如:(user_installed_category = video, impression_category=music),但这种方法的局限性在于,无法推广到训练数据中未出现的一些特征对。(比如,用户专业和文章类别)基于嵌入向量的模型,例如FM或深度神经网络可以通过学习低纬稠密向量来推广到以前未出现的特征对。但是,当物品特征很稀疏时(比如用户有特殊偏好或很少出现的物品),特征对很难进行稠密低纬表示。在这种情况下多数特征是没有交叉的,但稠密embedding向量会为所有特征对生成非零的预测。这样过于泛化,会生成很多不相关的物品。
总结,使用特征交叉的线性特征可以学到特征的共现规则,但泛化能力不足,无法推广到未出现过的特征对。低纬稠密向量模型具备泛化能力,但对于稀疏数据,可能生成很多不相关的物品。所以,论文提出了深宽模型,以同时实现记忆和泛化。
论文主要贡献包括:
1) 深宽模型框架联合训练带有embedding的前馈神经网络和交叉特征的线性模型,可作为稀疏数据输入的推荐系统。
2) 实现和评估深宽模型在Google Play上,一个活跃了一亿用户和超一亿apps的手机app商店。
3) 在Tensorflow1的高阶API中开源了代码。
Wide & Deep原理
The Wide Component
wide组件是一个广义线性模型,y = wTx + b,其中y是预测值,x=[x1, x2, ...,xd],w=[w1, w2,...,wd],模型参数b是偏差。输入特征包括原始特征和交叉特征。
交叉特征不好的地方在于,对于训练集中没有出现的样本,它不能进行泛化。wide模型可以对一些特例进行“记忆”,比如AND(query=”fried chicken”, item=”chicken fried rice”)虽然字符上看很相似,但二者是不同的东西。模型会记住这是一个不好的特例。下次用户查询鸡肉时,模型不会推荐鸡肉炒米饭了。
The Deep Component
deep组件是一个前馈神经网络。高纬稀疏类别特征转化为低纬稠密实数值的向量(embedding向量),向量维度通常为O(10)到O(100)之间,embedding向量刚开始随机赋值,在最小化损失函数的过程中得到训练。然后这些低纬稠密向量fed into神经网络的隐藏层。隐藏层的计算公式:
l是层数,f是一个激活函数,通常使用Relu(修正线性单元),a是激活函数,b是偏差,W是模型权重。
Joint Training of Wide & Deep Model
联合训练指深度和宽模型通过一个LR组合在一起。联合训练和集成训练有明显的区别。集成训练是每个模型相互独立,只有在预测时才会组合在一起。而联合训练会同时优化所有参数,比如wide与deep的输出结果,通过加权求和的方式进行。对于集成训练,由于模型的训练是不联合的,所以每个模型都需要全部特征。而对于联合训练,wde模型只需要补充deep模型的不足,使用一小部分交叉特征即可,而无需使用一个全连接的wide模型。
在实验中,我们使用FTRL算法作为宽模型的优化器,AdaGrad作为深模型的优化器。
模型包括三个部分:数据生成、模型训练和模型服务上线。
特征工程部分包括:连续特征归一化。对离散特征处理时去掉频次较少的特征,以减少计算的复杂度。
Wide & Deep的网络结构
联合训练一个线性模型组件和一个神经网络组件。
对于宽度部分包括用户安装apps和用户曝光apps的交叉特征。对于深度部分包括,每个分类特征学习出一个32维的embedding向量。我们连接所有的embedding,得到一个1200维度的稠密向量。然后输入进3个Relu层,最终使用sigmoid作为输出神经元。
当新的训练数据来临的时候,我们用的是热启动的方式,也就是从之前的模型读取Embeddings以及线性模型的权重初始化一个新模型,而不是全部推倒重新训练。
代码实现
import os
import numpy as np
import pandas as pd
from collections import namedtuple
import tensorflow as tf
from tensorflow.keras.layers import *
from tensorflow.keras.models import *
from sklearn.preprocessing import MinMaxScaler, LabelEncoder
##### 数据预处理
data = pd.read_csv('./data/criteo_sample.txt')
data.head()
def data_processing(df, dense_features, sparse_features):
df[dense_features] = df[dense_features].fillna(0.0)
for f in dense_features:
df[f] = df[f].apply(lambda x: np.log(x+1) if x > -1 else -1)
df[sparse_features] = df[sparse_features].fillna("-1")
for f in sparse_features:
lbe = LabelEncoder()
df[f] = lbe.fit_transform(df[f])
return df[dense_features + sparse_features]
dense_features = [i for i in data.columns.values if 'I' in i]
sparse_features = [i for i in data.columns.values if 'C' in i]
df = data_processing(data, dense_features, sparse_features)
df['label'] = data['label']
##### 模型构建
# 使用具名元组定义特征标记
SparseFeature = namedtuple('SparseFeature', ['name', 'vocabulary_size', 'embedding_size'])
DenseFeature = namedtuple('DenseFeature', ['name', 'dimension'])
VarLenSparseFeature = namedtuple('VarLenSparseFeature', ['name', 'vocabulary_size', 'embedding_size', 'maxlen'])
def build_input_layers(feature_columns):
""" 构建输入层 """
dense_input_dict, sparse_input_dict = {}, {}
for f in feature_columns:
if isinstance(f, DenseFeature):
dense_input_dict[f.name] = Input(shape=(f.dimension, ), name=f.name)
elif isinstance(f, SparseFeature):
sparse_input_dict[f.name] = Input(shape=(1, ), name=f.name)
return dense_input_dict, sparse_input_dict
def build_embedding_layers(feature_columns, is_linear):
embedding_layers_dict = {}
# 筛选出sparse特征列
sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeature), feature_columns)) if feature_columns else []
if is_linear:
for f in sparse_feature_columns:
embedding_layers_dict[f.name] = Embedding(f.vocabulary_size + 1, 1, name='1d_emb_' + f.name)
else:
for f in sparse_feature_columns:
embedding_layers_dict[f.name] = Embedding(f.vocabulary_size + 1, f.embedding_size, name='kd_emb_' + f.name)
return embedding_layers_dict
def concat_embedding_list(feature_columns, input_layer_dict, embedding_layer_dict, flatten=False):
""" 拼接embedding特征 """
sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeature), feature_columns)) if feature_columns else []
embedding_list = []
for f in sparse_feature_columns:
_input_layer = input_layer_dict[f.name]
_embed = embedding_layer_dict[f.name]
embed_layer = _embed(_input_layer)
if flatten:
embed_layer = Flatten()(embed_layer)
embedding_list.append(embed_layer)
return embedding_list
def get_linear_logits(dense_input_dict, sparse_input_dict, sparse_feature_columns):
concat_dense_inputs = Concatenate(axis=1)(list(dense_input_dict.values()))
dense_logits_output = Dense(1)(concat_dense_inputs)
linear_embedding_layer = build_embedding_layers(sparse_feature_columns, is_linear=True)
sparse_1d_embed_list = []
for f in sparse_feature_columns:
temp_input = sparse_input_dict[f.name]
temp_embed = Flatten()(linear_embedding_layer[f.name](temp_input))
sparse_1d_embed_list.append(temp_embed)
sparse_logits_output = Add()(sparse_1d_embed_list)
linear_logits = Add()([dense_logits_output, sparse_logits_output])
return linear_logits
def get_dnn_logits(dense_input_dict, sparse_input_dict, sparse_feature_columns, dnn_embedding_layers):
concat_dense_inputs = Concatenate(axis=1)(list(dense_input_dict.values()))
sparse_kd_embed = concat_embedding_list(sparse_feature_columns, sparse_input_dict, dnn_embedding_layers, flatten=True)
concat_sparse_kd_embed = Concatenate(axis=1)(sparse_kd_embed)
dnn_input = Concatenate(axis=1)([concat_dense_inputs, concat_sparse_kd_embed])
# DNN层
dnn_out = Dropout(0.5)(Dense(1024, activation='relu')(dnn_input))
dnn_out = Dropout(0.5)(Dense(512, activation='relu')(dnn_input))
dnn_out = Dropout(0.5)(Dense(256, activation='relu')(dnn_input))
dnn_logits = Dense(1)(dnn_out)
return dnn_logits
def WideDeep(linear_feature_columns, dnn_feature_columns):
dense_input_dict, sparse_input_dict = build_input_layers(linear_feature_columns + dnn_feature_columns)
# linear
linear_sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeature), linear_feature_columns))
input_layers_list = list(dense_input_dict.values()) + list(sparse_input_dict.values())
linear_logits = get_linear_logits(dense_input_dict, sparse_input_dict, linear_sparse_feature_columns)
# dnn
dnn_embedding_layers = build_embedding_layers(dnn_feature_columns, is_linear=False)
dnn_sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeature), dnn_feature_columns))
dnn_logits = get_dnn_logits(dense_input_dict, sparse_input_dict, dnn_sparse_feature_columns, dnn_embedding_layers)
output_logits = Add()([linear_logits, dnn_logits])
output_layer = Activation("sigmoid")(output_logits)
model = Model(input_layers_list, output_layer)
return model
# 定义特征列
linear_feature_columns = [SparseFeature(f, vocabulary_size=df[f].nunique(), embedding_size=4) for f in sparse_features] + \
[DenseFeature(f, 1,) for f in dense_features]
dnn_feature_columns = [SparseFeature(f, vocabulary_size=df[f].nunique(), embedding_size=4) for f in sparse_features] + \
[DenseFeature(f, 1,) for f in dense_features]
model = WideDeep(linear_feature_columns, dnn_feature_columns)
model.summary()