推荐算法-DeepFM

推荐算法-DeepFM

一、DeepFM出现的原因

  在FM中,采用了一阶和二阶的特征组合,相比与只使用一阶线性组合效果要好很多。但是特征组合的能力还是有限的。即特征之间组合的力度,挖掘特征之间的关联性还是较差的。在图像处理的一些方法中,模型的深度都比较深经过了很多层的非线性变换,主要的目的是为了让模型充分的学习数据的分布以及更加抽象的表示,也就是希望利用高阶的特征。在模型的深层部分得到的特征就是数据的高阶表示,因此DeepFM就是在FM的基础上加上深度模型。让模型进行更多的非线性变换,得到更高阶的特征组合。

二、 FM与深度模型的组合方式

  FM与深度模型的组合有两种,一种是二者并行,另一种是二者串行。DeepFM就是并行的一种结构。并行就是FM将输入部分计算完之后单独拿出来,得到一组特征表示,然后再利用深度模型(多层全连接)对输入部分进行告阶的特征组合。最后把二者的特征进行concact,得到一组特征,最后对这组特征进行分类或者回归。其实这只是特征的一种组合方式,目的就是为了得到特征的高阶表示。
在这里插入图片描述
上图就是DeepFM的结构图,从图中也可以看出模型是比较简单的。这样模型的输出其实就是 y = s i g m o i d ( y F M + y D N N ) y=sigmoid(y_{FM}+y_{DNN}) y=sigmoid(yFM+yDNN)这里的加号是特征级联的意思。FM部分如同我们上一章的做法,DNN部分是三层全连接。

三、数据处理

  刚刚接触推荐算法,也就是数据挖掘之类的问题。第一次处理感觉还是有些困难的,尤其是特征处理部分。不像对图像这些问题的处理那样方便,但是处理特征是这类工作的重中之重,所以还是要重视起来的。我也参考了大神的文章和代码,大神的文章地址在这里推荐系统遇上深度学习(三)–DeepFM模型理论和实践。我现在还是处于比着葫芦画瓢的阶段,完全是学习别人的东西。
  数据分为训练集和测试集两个csv文件。数据量也比较小,特征有数值类型的也有需要进行one-hot处理的,然后还有一部分特征直接没有处理直接从文件中去除。下面讲一下处理的思路:
1、首先是对于缺失的数据,每一行代表一个用户的数据。会有一些数据存在缺失,全部用-1填充。然后又生成一列新的特征’missing_feat’,这一列就是记录每一个用户缺失值的个数。
df["missing_feat"] = np.sum((df[cols] == -1).values, axis=1)
这个做法很简单,就是先找到每一行等于-1的位置,然后把统计每一行的个数。
在这里插入图片描述
2、接下来考虑对特征进行编码。因为特征有很多类型,有的是数值类型的比如年龄,有很多的取值,这种可以保留原始数值。有的是类别性的,比如性别,这种需要进行one-hot编码。

        self.feat_dict = {}
        tc = 0
        for col in df.columns:
            if col in self.ignore_cols:  # 去掉不需要的列
                continue
            if col in self.numeric_cols:  # 需要的列标记出来,变为数值,存入feat_dict
                self.feat_dict[col] = tc
                tc += 1                   # tc用于编码 此处是当前列全部编为tc
            else:                         # 指定类别的列 当前列的数值集中有几个数值,对这些数值进行编码 
                us = df[col].unique()     # 当前列去重
                # print(us)
                self.feat_dict[col] = dict(zip(us, range(tc, len(us)+tc)))  # us[0]:tc, us[1]:tc+1, us[2]:tc+2 对每一个数值标号
                # print(self.feat_dict[col])
                tc += len(us)             # 更新tc
        self.feat_dim = tc                # 所有数值特征的个数?

上面的做法很好理解,对于一些需要ignore的特征,直接忽略即可。对于数值类型的,直接把这一列特征记为tc,也就是这一列全部编码为tc。对于类别性的特征,首先是对这一列特曾进行去重,然后对每一种取值进行编码。编码的目的是为了我们输入时做Embedding来用的,tc的最大值就是Embedding层的featureSize,假如我们设置Embedding的大小为k。那么embedding部分的权重为featureSize*k的大小,我们在对输入进行编码时,根据每一个特征的取值索引去取对应位置的权重即可。接下来就是生成dfi和dfv两个表格,dfi记录每一个特征的每一个取值的tc编码,dfv对于one-hot类型的特征标为1,数值类型的特征保持原始数据不变。

    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()  # 找出label
            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)
        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]  # 需要变为数值的列利用上一步的的tc值替换 tc代表
            else:                                         # 需要变为类别的列利用上一步的特征标号替换 tc, tc+1, tc+2..
                dfi[col] = dfi[col].map(self.feat_dict.feat_dict[col])
                dfv[col] = 1.
        xi = dfi.values.tolist()   # 每一个特征的记号或者索引 全部变为类别索引
        xv = dfv.values.tolist()   # 原始数据 除了需要变为类别的列之外全部为原始数据
        if has_label:
            return xi, xv, y
        else:
            return xi, xv, ids

这个地方其实很好理解,
在这里插入图片描述
xi中就是对特征的编码,xv中是原始的数据,在embedding的过程中,根据xi中的索引去获得对应的权重,然后再让权重与xv中的值对应相乘即可。

三、模型部分

  模型的处理部分,首先就是构建权重,然后就是模型的计算图,最后是各种数据的读取与评价指标等。
1、权重的构建。在上面我们对特征进行了编码,其实就相当于one-hot编码,只是在形式上没有体现出来,而是xi和xv的维度是一致的。如果显性的体现处one-hot,xi的宽度是要更大的。举个例子,数据的col共有filesSize=39个,但是编码之后的特征的编码最大值为featureSize=157,embedding的大小16。我们在embedding层需要构建featureSize*embedding大小的矩阵,然后可以根据每一个特征的索引去获得对应的embedding表示。首先看embedding部分的权重构建:

        weights = dict()
        # embedding
        weights['featureEmbedding'] = tf.Variable(tf.truncated_normal(shape=[self.featureSize, self.embeddingSize], mean=0.0,  
        stddev=0.001), dtype=tf.float32, name='featureEmbedding')  # f * k f=tc
        
        weights['featureBias'] = tf.Variable(tf.truncated_normal(shape=[self.featureSize, 1], mean=0.0, stddev=0.001),
                                              dtype=tf.float32, name='featureBias')  # f

weights[‘featureEmbeddings’] 存放的每一个值其实就是FM中的vik,所以它是F * K的。其中,F代表feture的大小(将离散特征转换成one-hot之后的特征总量),K代表dense vector的大小。
weights[‘feature_bias’]是FM中的一次项的权重。
然后就是DNN部分的权重:

        # deep
        numLayer = len(self.deepLayers)
        inputSize = self.fieldSize * self.embeddingSize  # (k*embedding)
        weights['layer0'] = tf.Variable(tf.truncated_normal(shape=[inputSize, self.deepLayers[0]], mean=0.0,
                                                            stddev=0.001), dtype=tf.float32)  # 全连接输入层
        weights['bias0'] = tf.Variable(tf.random_normal(shape=[1, self.deepLayers[0]], mean=0.0, stddev=0.001),
                                       dtype=tf.float32)

        for i in range(1, numLayer):
            weights['layer{}'.format(i)] = tf.Variable(tf.truncated_normal(shape=[self.deepLayers[i-1], self.deepLayers[i]],
                                                                            mean=0.0, stddev=0.001), dtype=tf.float32)
            weights['bias{}'.format(i)] = tf.Variable(tf.truncated_normal(shape=[1, self.deepLayers[i]], mean=0.0,
                                                                          stddev=0.001), dtype=tf.float32)

        if self.useFM and self.useDeep:
            inputSize = self.fieldSize + self.embeddingSize + self.deepLayers[-1]
        elif self.useFM:
            inputSize = self.fieldSize + self.embeddingSize
        elif self.useDeep:
            inputSize = self.deepLayers[-1]

        weights['concat_projection'] = tf.Variable(tf.truncated_normal(shape=[inputSize, 1], mean=0.0, stddev=0.001), dtype=tf.float32)
        weights['concat_bias'] = tf.Variable(tf.constant(0.01), dtype=np.float32)

        return weights

DNN部分的权重都是全连接部分的,比较简单。在最后将FM部分与DNN的部分的特征进行合并的时候,要注意feature的大小,一次项的长度为fieldSIze,FM部分的长度为embedding的大小,deep部分的长度为人为设置的大小。

**2、计算图的构建。**计算图的构建就是编写整个模型的结构。首先是输入部分,因为我们要输入特征的索引,以及特征的数值,还有label信息,因此需要三个输入。

self.feat_index = tf.placeholder(shape=[None, None], dtype=tf.int32, name='feat_index')
self.feat_value = tf.placeholder(shape=[None, None], dtype=tf.float32, name='feat_value')
self.label = tf.placeholder(shape=[None, 1], dtype=tf.float32, name='label')
# 设置dropout
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_deep_deep')
self.train_phase = tf.placeholder(tf.bool, name='train_phase')

获得embedding的表示:

self.weights = self._initialWeights()
# 获得输入特征的embedding表示  f field_size   k embedding
self.embedding = tf.nn.embedding_lookup(self.weights['featureEmbedding'], self.feat_index)  # batch * f * k
feat_value = tf.reshape(self.feat_value, shape=[-1, self.fieldSize, 1])  # batch * f * 1
self.embedding = tf.multiply(self.embedding, feat_value)  # batch * f * k

lookup就是根据索引获得对应embedding权重。然后将输入数据调整shape,最后将对应的数值与权重部分相乘,也就获得了每一个输入部分的embedding部分的表示。
接下来计算一次项

# 一次项  w1*x1 , w2*x2 , w3*x3.. 长度为f
self.y_first_order = tf.nn.embedding_lookup(self.weights['featureBias'], self.feat_index)  # batch * 1
self.y_first_order = tf.reduce_sum(tf.multiply(self.y_first_order, feat_value), 2)  # (batch*1)x(batch*f*1)
self.y_first_order = tf.nn.dropout(self.y_first_order, self.dropout_keep_fm[0])  # batch * f

一次项的计算就是根据索引值获得每一个value对应的权重w,然后将value与w相乘即可。权重的shape=batch * 1,而feat_value的shape=batch * f * 1,二者相乘是对应位置相乘,得到的shape=batch * f * 1,reduce_sum是按照axis=2进行求和的,其实还是原来的数据,只是维度变成了shape=batch * 1。这样就得到了所有的特征一次项 w 1 ∗ x 1 , w 2 ∗ x 2... w n ∗ x n w1*x1, w2 * x2...wn * xn w1x1,w2x2...wnxn

接下来就是二次项的构建:

# sum(vx)^2
self.sum_feature_emb = tf.reduce_sum(self.embedding, 1)   # batch * k
self.sum_feature_emb_square = tf.square(self.sum_feature_emb)

# sum((vx)^2)
self.square_feature_emb = tf.square(self.embedding)      # batch*f*k
self.square_feature_emb_sum = tf.reduce_sum(self.square_feature_emb, 1)  # batch*k

# 二次项
self.y_second_order = 0.5 * tf.subtract(self.sum_feature_emb_square, self.square_feature_emb_sum)  # batch*k
self.y_second_order = tf.nn.dropout(self.y_second_order, self.dropout_keep_fm[1])

在这里插入图片描述
在这里插入图片描述
FM的公式展开如上式所示,分别计算减号左右两边的,然后再做差就可完成。
接下来就是DNN部分的构建,将特征的embedding表示展开成一个全连接向量进行几层全连接的计算就可以。

            # 深度部分
            self.y_deep = tf.reshape(self.embedding, shape=[-1, self.fieldSize * self.embeddingSize])
            self.y_deep = tf.nn.dropout(self.y_deep, self.dropDeep[0])

            for i in range(len(self.deepLayers)):
                print(i)
                self.y_deep = tf.add(tf.matmul(self.y_deep, self.weights['layer{}'.format(i)]), self.weights['bias{}'.format(i)])
                self.y_deep = self.acti(self.y_deep)
                self.y_deep = tf.nn.dropout(self.y_deep, self.dropout_keep_deep[i+1])

            if self.useFM and self.useDeep:
                concat_input = tf.concat([self.y_first_order, self.y_second_order, self.y_deep], axis=1)
            elif self.useFM:
                concat_input = tf.concat([self.y_first_order, self.y_second_order], axis=1)
            elif self.useDeep:
                concat_input = self.y_deep

            self.out = tf.add(tf.matmul(concat_input, self.weights['concat_projection']), self.weights['concat_bias'])

这样整个DeepFM的计算图就构建完成了。

最后就是对模型的训练了。
在重新写(其实是抄)这个代码的过程中,一直有一个疑问,就是我一直一为FM部分得到的是一个数值,而不是一段向量。后来发现是一次项和二次项都是用向量表示的,然后再和deep部分的特征concact,最后进行分类。
其实感觉理解的还不是很透彻,欢迎各位多多指教。

参考

https://www.jianshu.com/p/6f1c2643d31b
https://blog.csdn.net/qq_18293213/article/details/89647215
https://arxiv.org/pdf/1703.04247.pdf

  • 3
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值