结合代码带你理解DeepFM 原创: 王腾龙 DataFunTalk 1周前

本文深入剖析DeepFM模型,一种结合因子分解机(FM)与深度神经网络(DNN)的优势,用于解决大规模推荐系统中稀疏特征的高效学习问题。文章详细介绍了DeepFM模型的构建过程,包括特征处理、模型初始化、权重设置、一阶和二阶特征交互的计算,以及深度部分的搭建。此外,还探讨了模型的训练流程和优化策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 https://mp.weixin.qq.com/s/KFUtS4Cd9-XSBlS4u7HZLA

# https://mp.weixin.qq.com/s/KFUtS4Cd9-XSBlS4u7HZLA

'''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

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_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]
        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
'''3. 根据每次输入的特征的索引,从隐特征向量中取出其对应的隐向量。将每一个特征对应的具体的值,和自己对应的隐向量相乘。
如果是 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) # 两个矩阵中对应元素各自相乘 这个就是就是每个值和自己的隐向量的乘积
'''4.计算一阶项,从 self.weights["feature_bias"] 取出对应的 w ,得到一阶项,大小为 batch_size*field_size。

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


5. 计算 deep 的项。将 self.embeddings(大小为batch_size*self.field_size * self.embedding_size)  reshape 成 batch_size*(self.field_size * self.embedding_size) 的大小,然后输入到网络里面进行计算。

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

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

# 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)

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值