结合代码带你理解 DeepFM

闲谈

 

众所周知,自从人工智能火了以后,大家现在全民 AI,连小学生中学生都在搞所谓的 AI。AI 的实现应该靠算法与硬件的结合,但是国内貌似搞算法的远超搞硬件的。现阶段来看,算法层面上,主要靠深度网络。我理解所谓的深度网络,就是用一系列的线性函数模拟复杂的非线性函数。举个简单例子,一个正弦函数,我们可以将他的作用域划分成一系列的小区间,将每个区间端点的函数值用直线连接起来。如果这些区间足够小,就可以用一系列的一次线性函数拟合这个正弦函数。神经网络中,每个神经元就可以模拟一个区间。所以理论上,一个两层的神经网络,就可以拟合任意一个复杂的函数。

 

最近工作中遇到了类似 ctr 点击的问题,所以看了一些这方面深度学习的方法。 ctr 最开始用逻辑回归算法,后来发展使用了 FM 算法。FM 算法中,解决了特征之间的 interaction 的问题。随后研究人员提出使用深度学习的方法来做 ctr 问题,神经网络可以解决的是高层特征的 interaction 问题,不仅仅解决了两阶 interaction 的问题。言归正传,下面我们开始介绍 DeepFM 这篇文章:

 

https://arxiv.org/pdf/1703.04247.pdf

 

DeepFM

 

相当于 Google 的 Wide&Deep ,这篇文章 wide 的部分是需要预先训练的 FM 算法,也就是说它不是一个 end-to-end 的方法。DeepFM 算法将 FM 算法作为一个训练的参数放到网络中,直接使用原始的特征输入,只需要告诉网络你的特征哪个是 numerical ,哪个是 categories 的特征。DeepFM 在 Wide&Deep 的基础上进行改进,成功解决了这两个问题,并做了一些改进,其优势 / 优点如下:

不需要预训练 FM 得到隐向量;
不需要人工特征工程;
能同时学习低阶和高阶的组合特征;
FM 模块和 Deep 模块共享 Feature Embedding 部分,可以更快的训练,以及更精确的训练学习。

下面我们结合 kaggle 比赛的一个数据和代码进行分析。

 

1. 输入处理

 

首先,利用 pandas 读取数据,然后获得对应的特征和 target ,保存到对应的变量中。并且将 categories 的变量保存下来。

 

 

# 加载数据
def_load_data():
# 读取 csv 文件
dfTrain = pd.read_csv(config.TRAIN_FILE)
dfTest = pd.read_csv(config.TEST_FILE)
defpreprocess(df):
cols = [cforcindf.columnsifcnotin["id","target"]]
df["missing_feat"] = np.sum((df[cols] == -1).values, axis=1)
df["ps_car_13_x_ps_reg_03"] = df["ps_car_13"] * df["ps_reg_03"]
returndf
dfTrain = preprocess(dfTrain)
dfTest = preprocess(dfTest)
cols = [cforcindfTrain.columnsifcnotin["id","target"]]
cols = [cforcincolsif(notcinconfig.IGNORE_COLS)]# 只保留我们需要的列
X_train = dfTrain[cols].values
y_train = dfTrain["target"].values
X_test = dfTest[cols].values
ids_test = dfTest["id"].values
cat_features_indices = [ifori,cinenumerate(cols)ifcinconfig.CATEGORICAL_COLS]
returndfTrain, dfTest, X_train, y_train, X_test, ids_test, cat_features_indices

 

2. 创建模型

 

 

fd = FeatureDictionary(dfTrain=dfTrain,dfTest=dfTest,
numeric_cols=config.NUMERIC_COLS,
ignore_cols=config.IGNORE_COLS)

 

2.1 创建一个特征处理的字典

 

首先,创建一个特征处理的字典。在初始化方法中,传入第一步读取得到的训练集和测试集。然后生成字典,在生成字典中,循环遍历特征的每一列,如果当前的特征是数值型的,直接将特征作为键值,和目前对应的索引作为 value 存到字典中。如果当前的特征是 categories ,统计当前的特征总共有多少个不同的取值,这时候当前特征在字典的 value 就不是一个简单的索引了,value 也是一个字典,特征的每个取值作为 key,对应的索引作为 value,组成新的字典。总而言之,这里面主要是计算了特征的的维度,numerical 的特征只占一位,categories 的特征有多少个取值,就占多少位。

 

 

classFeatureDictionary(object):
def__init__(self, trainfile=None, testfile=None,
dfTrain=None, dfTest=None, numeric_cols=[], ignore_cols=[]):
assertnot((trainfileisNone)and(dfTrainisNone)),"trainfile or dfTrain at least one is set"
assertnot((trainfileisnotNone)and(dfTrainisnotNone)),"only one can be set"
assertnot((testfileisNone)and(dfTestisNone)),"testfile or dfTest at least one is set"
assertnot((testfileisnotNone)and(dfTestisnotNone)),"only one can be set"
self.trainfile = trainfile
self.testfile = testfile
self.dfTrain = dfTrain
self.dfTest = dfTest
self.numeric_cols = numeric_cols
self.ignore_cols = ignore_cols
# 根据特征的种类是 numerical 还是 categories 的类别 计算输入到网络里面的特征的长度
self.gen_feat_dict()
defgen_feat_dict(self):
ifself.dfTrainisNone:
dfTrain = pd.read_csv(self.trainfile)
else:
dfTrain = self.dfTrain
ifself.dfTestisNone:
dfTest = pd.read_csv(self.testfile)
else:
dfTest = self.dfTest
df = pd.concat([dfTrain, dfTest])
self.feat_dict = {}
tc =0
# 通过下面的循环 计算输入到模型中特征的总的长度
forcolindf.columns:
ifcolinself.ignore_cols:
continue
ifcolinself.numeric_cols:
# map to a single index
self.feat_dict[col] = tc
tc +=1
else:
us = df[col].unique()# 查看当前 categories 种类的特征有多少个唯一的值
self.feat_dict[col] = dict(zip(us, range(tc, len(us)+tc)))
tc += len(us)
self.feat_dim = tc

 

2.2 数据解析

 

 

data_parser = DataParser(feat_dict=fd)
# 解析数据 Xi_train 存放的是特征对应的索引 Xv_train 存放的是特征的具体的值
Xi_train, Xv_train, y_train = data_parser.parse(df=dfTrain,has_label=True)
Xi_test, Xv_test, ids_test = data_parser.parse(df=dfTest)
在解析数据中,逐行处理每一条数据,dfi 记录了当前的特征在总的输入的特征中的索引。dfv 中记录的是具体的值,如果是 numerical 特征,存的是原始的值,如果是 categories 类型的,就存放 1。这个相当于进行了 one-hot 编码,在 dfi 存储了特征所在的索引。输入到网络中的特征的长度是 numerical 特征的个数 +categories 特征 one-hot 编码的长度。最终,Xi 和 Xv 是一个二维的 list,里面的每一个 list 是一行数据,Xi 存放的是特征所在的索引,Xv 存放的是具体的特征值。

 

 

# 解析数据
class DataParser(object):
def __init__(self, feat_dict):
self.feat_dict = feat_dict
def parse(self, infile=None, df=None, has_label=False):
assertnot((infileisNone)and(dfisNone)),"infile or df at least one is set"
assertnot((infileisnotNone)and(dfisnotNone)),"only one can be set"
ifinfileisNone:
dfi = df.copy()
else:
dfi = pd.read_csv(infile)
ifhas_label:
y = dfi["target"].values.tolist()
dfi.drop(["id","target"], axis=1, inplace=True)
else:
ids = dfi["id"].values.tolist()
dfi.drop(["id"], axis=1, inplace=True)
# dfiforfeatureindex
# dfvforfeaturevalue which can be either binary (1/0)orfloat(e.g.,10.24)
# dfi 记录的是特征所对应的索引 也就是输入样本在输入维度的第几个地方不等于0dfv 记录的是特征的具体的值
dfv = dfi.copy()
forcolindfi.columns:
ifcolinself.feat_dict.ignore_cols:
dfi.drop(col, axis=1, inplace=True)
dfv.drop(col, axis=1, inplace=True)
continue
ifcolinself.feat_dict.numeric_cols:
dfi[col] = self.feat_dict.feat_dict[col]
else:
dfi[col] = dfi[col].map(self.feat_dict.feat_dict[col])
dfv[col] =1.
# list of list offeatureindicesof each sampleinthe dataset
Xi = dfi.values.tolist()
# list of list offeaturevaluesof each sampleinthe dataset
Xv = dfv.values.tolist()
ifhas_label:
returnXi, Xv, y
else:
returnXi, Xv, ids

 

2.3 准备 batch

 

 

# 解析数据 Xi_train 存放的是特征对应的索引 Xv_train 存放的是特征的具体的值
Xi_train, Xv_train,y_train= data_parser.parse(df=dfTrain,has_label=True)
Xi_test, Xv_test,ids_test= data_parser.parse(df=dfTest)
#feature_size 记录特征的维度 field_size 记录了特征的个数
dfm_params["feature_size"] = fd.feat_dim
dfm_params["field_size"] = len(Xi_train[0])
y_train_meta= np.zeros((dfTrain.shape[0],1),dtype=float)
y_test_meta= np.zeros((dfTest.shape[0],1),dtype=float)
_get= lambda x, l: [x[i] for iinl]
gini_results_cv= np.zeros(len(folds),dtype=float)
gini_results_epoch_train= np.zeros((len(folds), dfm_params["epoch"]),dtype=float)
gini_results_epoch_valid= np.zeros((len(folds), dfm_params["epoch"]),dtype=float)
for i, (train_idx, valid_idx)inenumerate(folds):
Xi_train_, Xv_train_,y_train_= _get(Xi_train, train_idx), _get(Xv_train, train_idx), _get(y_train, train_idx)
Xi_valid_, Xv_valid_,y_valid_= _get(Xi_train, valid_idx), _get(Xv_train, valid_idx), _get(y_train, valid_idx)

 

2.4 构造 DeepFM 模型

 

 

    1. 在初始化方法中,先设置一些初始化的参数,比如特征的长度,总共有多少个原始的字段等。然后最重要的就是初始化图 self._init_graph() 方法。

 

    1. 在构造图的方法中,先定义了 6 个 placeholder ,每个大小的 None 代表的是 batch_size 的大小。

 

 

 

self.feat_index = tf.placeholder(tf.int32, shape=[None,None],
name="feat_index") #None* F batch_size * field_size
self.feat_value = tf.placeholder(tf.float32, shape=[None,None],
name="feat_value") #None* F batch_size * field_size
self.label = tf.placeholder(tf.float32, shape=[None,1],name="label") #None*1
self.dropout_keep_fm = tf.placeholder(tf.float32, shape=[None],name="dropout_keep_fm")
self.dropout_keep_deep = tf.placeholder(tf.float32, shape=[None],name="dropout_keep_deep")
self.train_phase = tf.placeholder(tf.bool,name="train_phase")

 

在这之后,调用权重的初始化方法。将所有的权重放到一个字典中。 feature_embeddings 本质上就是 FM 中的 latent vector 。对于每一个特征都建立一个隐特征向量。feature_bias 代表了 FM 中的 w 的权重 。然后就是搭建深度图,输入到深度网络的大小为:特征的个数 * 每个隐特征向量的长度。根据每层的配置文件,生产相应的权重。对于输出层,根据不同的配置,生成不同的输出的大小。如果只是使用 FM 算法,那幺

 

 

# todo 初始化权重
def_initialize_weights(self):
weights = dict()
# embeddings
weights["feature_embeddings"] = tf.Variable(
tf.random_normal([self.feature_size,self.embedding_size],0.0,0.01),
name="feature_embeddings")# feature_size * K
weights["feature_bias"] = tf.Variable(
tf.random_uniform([self.feature_size,1],0.0,1.0), name="feature_bias")# feature_size * 1
# deep layers
num_layer = len(self.deep_layers)
# 计算输入的大小
input_size =self.field_size *self.embedding_size
#todo ======================== 第一层的网络结构 =============================
glorot = np.sqrt(2.0/ (input_size +self.deep_layers[0]))
weights["layer_0"] = tf.Variable(
np.random.normal(loc=0, scale=glorot, size=(input_size,self.deep_layers[0])), dtype=np.float32)
weights["bias_0"] = tf.Variable(np.random.normal(loc=0, scale=glorot, size=(1,self.deep_layers[0])),
dtype=np.float32)# 1 * layers[0]
foriinrange(1, num_layer):
glorot = np.sqrt(2.0/ (self.deep_layers[i-1] +self.deep_layers[i]))
weights["layer_%d"% i] = tf.Variable(
np.random.normal(loc=0, scale=glorot, size=(self.deep_layers[i-1],self.deep_layers[i])),
dtype=np.float32)# layers[i-1] * layers[i]
weights["bias_%d"% i] = tf.Variable(
np.random.normal(loc=0, scale=glorot, size=(1,self.deep_layers[i])),
dtype=np.float32)# 1 * layer[i]
# final concat projection layer
ifself.use_fmandself.use_deep:
input_size =self.field_size +self.embedding_size +self.deep_layers[-1]
elifself.use_fm:
input_size =self.field_size +self.embedding_size
elifself.use_deep:
input_size =self.deep_layers[-1]
glorot = np.sqrt(2.0/ (input_size +1))
weights["concat_projection"] = tf.Variable(
np.random.normal(loc=0, scale=glorot, size=(input_size,1)),
dtype=np.float32)# layers[i-1]*layers[i]
weights["concat_bias"] = tf.Variable(tf.constant(0.01), dtype=np.float32)
returnweights

 

 

    1. 根据每次输入的特征的索引,从隐特征向量中取出其对应的隐向量。将每一个特征对应的具体的值,和自己对应的隐向量相乘。如果是 numerical 的,就直接用对应的 value 乘以隐向量。如果是 categories 的特征,其对应的特征值是 1,相乘完还是原来的隐向量。最后,self.embeddings 存放的就是输入的样本的特征值和隐向量的乘积。大小为 batch_size

field_size

    1. embedding_size

 

 

 

self.embeddings = tf.nn.embedding_lookup(self.weights["feature_embeddings"],self.feat_index)# None * F * K 由于 one-hot 是 01 编码 所以取出的大小为 batch_size*field_size*embedding_size
feat_value = tf.reshape(self.feat_value, shape=[-1,self.field_size,1])
self.embeddings = tf.multiply(self.embeddings, feat_value)# 两个矩阵中对应元素各自相乘 这个就是就是每个值和自己的隐向量的乘积

 

 

    1. 计算一阶项,从 self.weights[“feature_bias”] 取出对应的 w ,得到一阶项,大小为 batch_size*field_size。

 

 

二阶项的计算,也就是 FM 的计算,利用了的技巧。先将 embeddings 在 filed_size 的维度上求和,最后得到红框里面的项。

 

 

 

    1. 计算 deep 的项。将 self.embeddings(大小为 batch_size

self.field_size * self.embedding_size) reshape 成 batch_size

    1. (self.field_size * self.embedding_size) 的大小,然后输入到网络里面进行计算。

 

    1. 最后将所有项 concat 起来,投影到一个值。如果是只要 FM ,不要 deep 的部分,则投影的大小为 filed_size+embedding_size 的大小。如果需要 deep 的部分,则大小再加上 deep 的部分。利用最后的全连接层,将特征映射到一个 scalar 。

 

    1. 最后一项就是定义损失和优化器。

 

 

 

# loss 损失函数
ifself.loss_type =="logloss":
self.out= tf.nn.sigmoid(self.out)
self.loss = tf.losses.log_loss(self.label,self.out)
elifself.loss_type =="mse":
self.loss = tf.nn.l2_loss(tf.subtract(self.label,self.out))
# l2 regularization on weights 正则化项目
ifself.l2_reg >0:
self.loss += tf.contrib.layers.l2_regularizer(
self.l2_reg)(self.weights["concat_projection"])
ifself.use_deep:
foriinrange(len(self.deep_layers)):
self.loss += tf.contrib.layers.l2_regularizer(
self.l2_reg)(self.weights["layer_%d"%i])
# optimizer
ifself.optimizer_type =="adam":
self.optimizer = tf.train.AdamOptimizer(learning_rate=self.learning_rate, beta1=0.9, beta2=0.999,
epsilon=1e-8).minimize(self.loss)
elifself.optimizer_type =="adagrad":
self.optimizer = tf.train.AdagradOptimizer(learning_rate=self.learning_rate,
initial_accumulator_value=1e-8).minimize(self.loss)
elifself.optimizer_type =="gd":
self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.learning_rate).minimize(self.loss)
elifself.optimizer_type =="momentum":
self.optimizer = tf.train.MomentumOptimizer(learning_rate=self.learning_rate, momentum=0.95).minimize(
self.loss)
elifself.optimizer_type =="yellowfin":
self.optimizer = YFOptimizer(learning_rate=self.learning_rate, momentum=0.0).minimize(
self.loss)

 

作者介绍:

 

王腾龙,滴滴算法工程师,中科院硕士,主要研究方向为机器学习与 Deepctr 。

发表评论

电子邮件地址不会被公开。 必填项已用*标注