DeepFM的一些理解和代码示例(Pytorch)
之前因为项目的需要,我开始接触深度学习的推荐系统。网上一搜索,啪!很快啊,上来就是协同过滤、FM、FFM、DeepFM,我看他们都是有备而来。点进去发现这样公式,那样公式,也不推导,反正我看不懂,但我大受震撼。他们可能大都是乱“打”的,他们也承认,标注“转载”。苦苦思索后,我把自己理解的一些DeepFM关键点以及相应的Pytorch代码写在下面(至于DeepFM详解,大家可以看看最后的参考),希望跟我一样的小白能够不要再犯迷糊。也求求算法大佬们讲讲武德,以后把细节说得清楚友好一些,orz。
先回顾一下FM(Factorization Machine),由于某些特征是类别型数据,需要进行one-hot转换处理。这样后果是产生高维稀疏矩阵,特别是CTR/CVR任务中,
有大量的类别型数据。FM解决办法是对特征进行两两组合,产生二阶特征:
(1)
其中,n代表样本的特征数量,就是第i个特征值,都是模型参数。大家可以对比一下这个公式和多项式线性回归,其实两者是差不多的。我们知道,要训练需要非零,但实际情况是样本存在大量的零值。那FM是怎么解决这个问题的呢?很简单,将W矩阵分解,用隐向量去表示某一维特征,用隐向量的内积来替代两个维度的交叉项系数,即是。
这里要留意,实际用到的是W矩阵的上三角元素。
那么公式1可以改写为: (2)
公式2中的最后一项可以进一步改写为:
(3)
有了这个公式,我们就可以很好地理解DeepFM了。
DeepFM的结构如下图所示:
图1 DeepFM论文中展示的结构
具体的数据输入格式,是下面这个图:
图2 DeepFM数据传输形式
DeepFM论文中提到原始数据的处理方式:
Each categorical field is represented as a vector of one-hot encoding, and each continuous field is represented as the value itself, or a vector of one-hot encoding after discretization.
什么意思呢?请师爷给大家翻译翻译,什么叫TMD惊喜!不好意思,串场了。。
就是说类别型的呢,咱就one-hot,连续数值型的呢,咱就直接用它。当然,连续型的也可以作离散化处理,再转one-hot,但这样比较麻烦,体现不了end-to-end的优势,咱就不做。
与FFM相同的是,DeepDM里仍然按照field转换数据。目光再次回到图1,我们看到field数据经过Dense Embedding后,再分别传入FM和DNN中。如何理解这个dense embedding呢,隐向量v在哪里呢?熟悉NLP word2vec的朋友应该很容易理解权矩阵跟隐向量v的关系,其实隐向量就是embedding的权重矩阵,只是因为原数据是one-hot的原因,有人会误以为隐向量v就是embedding vector,这其实是错误的。
下面用代码来演示一些示例,这里我用Pytorch。
import torch
import torch.nn as nn
# 单个field,10000个可能取值
feature_size = 10**4
embedding_size = 30
embed_layer = nn.Embedding(feature_size, embedding_size)
# 多个field
features = ['job', 'country', 'hobby']
feature_sizes = [10**3, 10**2*3, 100]
embed_layer = nn.ModuleDict({features[i]:\
nn.Embedding(feature_sizes[i], embedding_size, sparse=False) for i in range(len(features)))
# 最后所有field拼接起来传入DNN
dense_dim = n_sparse_fields * embedding_size + n_dense_fields
hidden_units = [dense_dim, 256, 128, 32, 16, 1]
dnn = nn.ModuleLists([
nn.Linear(hidden_units[i], hidden_units[i+1]) for i in range(len(hidden_units) - 1)])
想必,少数人可能对上面的dense_dim有点疑问,接下来我就讲一讲如何在实际中处理sparse和dense的数据,也就是one-hot和连续型数值数据。
回到公式2,也就是FM里,我们还是要处理数据的一阶形式。通过图1,我们可以看到一阶计算也是通过不同的field乘以权重相加。在论文中第二页的注脚也提到"a Normal Connection in black refers to a connection with weight to be learned"。好了,那我们就可以把不同field的one-hot vectors先embedding成1维,和dense vectors对应。
# 这里代码只是示意,方便理解,具体可实施的请看参考3
sparse_embedding_dict = nn.ModuleDict(
feat: nn.Emebedding(feat_sizes[feat], 1, sparse=False)
for feat in sparse_features)
sparse_embd_lists = [sparse_embedding_dict[feat](data[:, feature_index[feat]].long())
for feat in sparse_features]
weights_dense = nn.Parameter(torch.Tensor(len(dense_features), 1))
linear_dense = torch.cat(dense_values, dim=-1).matmul(weights_dense)
linear_sparse = torch.sum(torch.cat(sparse_embd_lists, dim=-1), dim=-1, keepdim=False)
linear_sum = linear_dense + linear_sparse
为什么dense要乘weights,而sparse不用呢?因为我们已经把sparse的weights放在了embedding里去学习啦~
二阶计算怎么做呢?把公式3写成代码就好啦!只是用到的embedding vector不再是一维的,而是我们设定的超参数embedding_size。我就直接搬用参考3的代码了:
# 注意这里的sparse_embedding_list是二阶的embedding vector
# 也是我们后面DNN的输入
fm_input = torch.cat(sparse_embedding_list, dim=1) # shape: (batch_size,field_size,embedding_size)
square_of_sum = torch.pow(torch.sum(fm_input, dim=1, keepdim=True), 2) # shape: (batch_size,1,embedding_size)
sum_of_square = torch.sum(torch.pow(fm_input, 2), dim=1, keepdim=True) # shape: (batch_size,1,embedding_size)
cross_term = square_of_sum - sum_of_square
cross_term = 0.5 * torch.sum(cross_term, dim=2, keepdim=False) # shape: (batch_size,1)
FM =linear_sum + cross_term
好了,FM的计算就完成了。DNN的计算就更简单了,只要你清楚网络的输入由什么组成,把输入直接丢给网络就好啦!
# sparse_embedding_list、 dense_value
dnn_sparse_input = torch.cat(sparse_embedding_list, dim=1)
batch_size = dnn_sparse_input.shape[0]
dnn_sparse_input=dnn_sparse_input.reshape(batch_size,-1)
dnn_dense_input = torch.cat(dense_value, dim=-1)
dnn_total_input = torch.cat([dnn_sparse_input, dnn_dense_input], dim=-1)
dnn_input = dnn_total_input
dnn_output = dnn_model(deep_input)
最后DeepFM的结果就是FM+DNN,再做sigmoid:
pred = torch.sigmoid(FM+dnn_output)
结语:
希望本文,能帮助一些朋友更好地理解DeepFM。欢迎提问,共同进步~
[参考]
知乎-FM算法解析
原创 [深度学习] DeepFM 介绍与Pytorch代码解释
原创 DeepFM Pytorch实现(Criteo数据集验证)