9月7日-8日,相约 2023 腾讯全球数字生态大会!聚焦产业未来发展新趋势! 了解详情
写点什么

结合代码带你理解 DeepFM

  • 2019-05-15
  • 本文字数:7565 字

    阅读完需:约 25 分钟

结合代码带你理解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)
def preprocess(df): cols = [c for c in df.columns if c not in ["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"] return df
dfTrain = preprocess(dfTrain) dfTest = preprocess(dfTest)
cols = [c for c in dfTrain.columns if c not in ["id", "target"]] cols = [c for c in cols if (not c in config.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 = [i for i,c in enumerate(cols) if c in config.CATEGORICAL_COLS]
return dfTrain, 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 的特征有多少个取值,就占多少位。


class FeatureDictionary(object):    def __init__(self, trainfile=None, testfile=None,                 dfTrain=None, dfTest=None, numeric_cols=[], ignore_cols=[]):        assert not ((trainfile is None) and (dfTrain is None)), "trainfile or dfTrain at least one is set"        assert not ((trainfile is not None) and (dfTrain is not None)), "only one can be set"        assert not ((testfile is None) and (dfTest is None)), "testfile or dfTest at least one is set"        assert not ((testfile is not None) and (dfTest is not None)), "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()
def gen_feat_dict(self): if self.dfTrain is None: dfTrain = pd.read_csv(self.trainfile) else: dfTrain = self.dfTrain if self.dfTest is None: dfTest = pd.read_csv(self.testfile) else: dfTest = self.dfTest df = pd.concat([dfTrain, dfTest]) self.feat_dict = {} tc = 0 #通过下面的循环 计算输入到模型中特征的总的长度 for col in df.columns: if col in self.ignore_cols: continue if col in self.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): assert not ((infile is None) and (df is None)), "infile or df at least one is set" assert not ((infile is not None) and (df is not None)), "only one can be set" if infile is None: dfi = df.copy() else: dfi = pd.read_csv(infile) if has_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) # dfi for feature index # dfv for feature value which can be either binary (1/0) or float (e.g., 10.24) # dfi 记录的是特征所对应的索引 也就是输入样本在输入维度的第几个地方不等于0 dfv记录的是特征的具体的值 dfv = dfi.copy() for col in dfi.columns: if col in self.feat_dict.ignore_cols: dfi.drop(col, axis=1, inplace=True) dfv.drop(col, axis=1, inplace=True) continue if col in self.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 of feature indices of each sample in the dataset Xi = dfi.values.tolist() # list of list of feature values of each sample in the dataset Xv = dfv.values.tolist() if has_label: return Xi, Xv, y else: return Xi, 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 i in l] 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) in enumerate(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() 方法。

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


self.feat_index = tf.placeholder(tf.int32, shape=[None, None],name="feat_index") # None * F batch_size * field_sizeself.feat_value = tf.placeholder(tf.float32, shape=[None, None],name="feat_value") # None * F batch_size * field_sizeself.label = tf.placeholder(tf.float32, shape=[None, 1], name="label") # None * 1self.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] for i in range(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 if self.use_fm and self.use_deep: input_size = self.field_size + self.embedding_size + self.deep_layers[-1] elif self.use_fm: input_size = self.field_size + self.embedding_size elif self.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)
return weights
复制代码


  1. 根据每次输入的特征的索引,从隐特征向量中取出其对应的隐向量。将每一个特征对应的具体的值,和自己对应的隐向量相乘。如果是 numerical 的,就直接用对应的 value 乘以隐向量。如果是 categories 的特征,其对应的特征值是 1,相乘完还是原来的隐向量。最后,self.embeddings 存放的就是输入的样本的特征值和隐向量的乘积。大小为 batch_sizefield_sizeembedding_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_sizefeat_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_sizeself.field_size * self.embedding_size) reshape 成 batch_size(self.field_size * self.embedding_size) 的大小,然后输入到网络里面进行计算。

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

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


# loss 损失函数            if self.loss_type == "logloss":                self.out = tf.nn.sigmoid(self.out)                self.loss = tf.losses.log_loss(self.label, self.out)            elif self.loss_type == "mse":                self.loss = tf.nn.l2_loss(tf.subtract(self.label, self.out))            # l2 regularization on weights 正则化项目            if self.l2_reg > 0:                self.loss += tf.contrib.layers.l2_regularizer(                    self.l2_reg)(self.weights["concat_projection"])                if self.use_deep:                    for i in range(len(self.deep_layers)):                        self.loss += tf.contrib.layers.l2_regularizer(                            self.l2_reg)(self.weights["layer_%d"%i])
# optimizer if self.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) elif self.optimizer_type == "adagrad": self.optimizer = tf.train.AdagradOptimizer(learning_rate=self.learning_rate, initial_accumulator_value=1e-8).minimize(self.loss) elif self.optimizer_type == "gd": self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.learning_rate).minimize(self.loss) elif self.optimizer_type == "momentum": self.optimizer = tf.train.MomentumOptimizer(learning_rate=self.learning_rate, momentum=0.95).minimize( self.loss) elif self.optimizer_type == "yellowfin": self.optimizer = YFOptimizer(learning_rate=self.learning_rate, momentum=0.0).minimize( self.loss)
复制代码

作者介绍:

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


本文来自 王腾龙 在 DataFun 社区的演讲,由 DataFun 编辑整理。


活动推荐:

2023年9月3-5日,「QCon全球软件开发大会·北京站」 将在北京•富力万丽酒店举办。此次大会以「启航·AIGC软件工程变革」为主题,策划了大前端融合提效、大模型应用落地、面向 AI 的存储、AIGC 浪潮下的研发效能提升、LLMOps、异构算力、微服务架构治理、业务安全技术、构建未来软件的编程语言、FinOps 等近30个精彩专题。咨询购票可联系票务经理 18514549229(微信同手机号)。

2019-05-15 08:006584

评论 3 条评论

发布
用户头像
可以给下数据集吗
2019-08-23 11:15
回复
用户头像
可以给下数据集吗
2019-08-23 11:15
回复
用户头像
请问,能给个原数据吗。谢谢
2019-08-01 19:39
回复
没有更多了
发现更多内容

模块七

Geek_2ce415

不吹不黑JAVA Stream的collect用法与原理,远比你想象的更强大

程序员小毕

Java 程序员 程序人生 stream collect

TPC藏宝计划质押系统开发(Dapp)

薇電13242772558

智能合约 dapp

面试官:你确定Redis是单线程的进程吗?

Java永远的神

Java redis 程序员 架构 面试

易周金融 | 邮惠万家银行开业;微信公众号叫停四类金融营销宣传

易观分析

金融

不是我说,Nacos和Apollo中的长轮询定时机制,真的太好用了

Java全栈架构师

Java 程序员 面试 微服务 nacos

Vue3:显示markdown文档

空城机

markdown Vue3 7月月更

JAVA编程规范之控制语句

源字节1号

后端开发

Verilog HDL

贾献华

7月月更

快速构建企业级应用的开发平台

力软低代码开发平台

想进大厂拿高薪?掌握Redis的Sentinel哨兵原理将是至关重要的突破口

了不起的程序猿

Java java程序员 Redis 数据结构

全新出品!阿里P5工程师~P8架构师晋升路线揭秘

程序员小毕

Java 程序员 面试 架构师 学习路线

软件研发落地实践,要从设计就开始

华为云开发者联盟

云计算 后端 开发

万物皆可Cassandra:HUAWEI Tag背后的神仙数据库

华为云开发者联盟

数据库 后端

算法题每日一练---第1天:猴子分香蕉

知心宝贝

算法 前端 后端 云开发 7月月更

Google上网神器Ghelper

源字节1号

软件开发 小程序开发

短视频直播系统源码——如何优化满足用户需求?

开源直播系统源码

直播系统源码 开源源码 短视频直播系统源码

易观千帆银行用户体验中心成立,助力银行业用户体验升级

易观分析

银行 用户体验

uni-app进阶之内嵌应用【day14】

恒山其若陋兮

7月月更

云计算和大数据的关系以及区别详细讲解

行云管家

云计算 大大数据

李宏毅《机器学习》丨2. Regression(回归)

AXYZdong

机器学习 7月月更

值得一看的智能运维AIOps关键核心技术概览!

云智慧AIOps社区

人工智能 机器学习 运维 智能运维 自动化运维

动手实践丨手把手教你用STM32做一个智能鱼缸

华为云开发者联盟

物联网 IoT

2022年5月视频用户洞察:用户使用时长增长 平台降本增效初见成效

易观分析

视频行业

Redis 持久化 - RDB 源码详细分析|保姆级别分析!全网最全

领创集团Advance Intelligence Group

持久化 rdb redis 底层原理

Mria+RLOG新架构下的EMQX 5.0如何实现1亿MQTT连接

EMQ映云科技

物联网 IoT mnesia emqx 7月月更

模块七作业 - 王者荣耀商城异地多活架构设计

Elvis FAN

如何使用IDEA数据库工具连接TDengine?

TDengine

数据库 tdengine 时序数据库

vueCli3.x版本中如何修改打包好的css、js、img文件名

木叶🐱

7月月更

怎么学自动化测试

和牛

测试

2022年浙江省等保备案流程指南

行云管家

等保 等保备案

  • 扫码添加小助手
    领取最新资料包
结合代码带你理解DeepFM_AI_DataFunTalk_InfoQ精选文章