闲谈
众所周知,自从人工智能火了以后,大家现在全民 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 模型
- 在初始化方法中,先设置一些初始化的参数,比如特征的长度,总共有多少个原始的字段等。然后最重要的就是初始化图 self._init_graph() 方法。
- 在构造图的方法中,先定义了 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
- 根据每次输入的特征的索引,从隐特征向量中取出其对应的隐向量。将每一个特征对应的具体的值,和自己对应的隐向量相乘。如果是 numerical 的,就直接用对应的 value 乘以隐向量。如果是 categories 的特征,其对应的特征值是 1,相乘完还是原来的隐向量。最后,self.embeddings 存放的就是输入的样本的特征值和隐向量的乘积。大小为 batch_size
field_size
- 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)# 两个矩阵中对应元素各自相乘 这个就是就是每个值和自己的隐向量的乘积
- 计算一阶项,从 self.weights[“feature_bias”] 取出对应的 w ,得到一阶项,大小为 batch_size*field_size。
二阶项的计算,也就是 FM 的计算,利用了的技巧。先将 embeddings 在 filed_size 的维度上求和,最后得到红框里面的项。
- 计算 deep 的项。将 self.embeddings(大小为 batch_size
self.field_size * self.embedding_size) reshape 成 batch_size
- (self.field_size * self.embedding_size) 的大小,然后输入到网络里面进行计算。
- 最后将所有项 concat 起来,投影到一个值。如果是只要 FM ,不要 deep 的部分,则投影的大小为 filed_size+embedding_size 的大小。如果需要 deep 的部分,则大小再加上 deep 的部分。利用最后的全连接层,将特征映射到一个 scalar 。
- 最后一项就是定义损失和优化器。
# 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 。
Be First to Comment