机器@深度学习

DeepFM的一些理解和代码示例(Pytorch)

2021-01-19  本文已影响0人  烟花如雨旧故里

之前因为项目的需要,我开始接触深度学习的推荐系统。网上一搜索,啪!很快啊,上来就是协同过滤、FM、FFM、DeepFM,我看他们都是有备而来。点进去发现这样公式,那样公式,也不推导,反正我看不懂,但我大受震撼。他们可能大都是乱“打”的,他们也承认,标注“转载”。苦苦思索后,我把自己理解的一些DeepFM关键点以及相应的Pytorch代码写在下面(至于DeepFM详解,大家可以看看最后的参考),希望跟我一样的小白能够不要再犯迷糊。也求求算法大佬们讲讲武德,以后把细节说得清楚友好一些,orz。

先回顾一下FM(Factorization Machine),由于某些特征是类别型数据,需要进行one-hot转换处理。这样后果是产生高维稀疏矩阵,特别是CTR/CVR任务中,
有大量的类别型数据。FM解决办法是对特征进行两两组合,产生二阶特征:
y(X) = w_0+\sum^n_{i=1}w_ix_i+\sum^{n-1}_{i=1}\sum^n_{j=i+1}w_{ij}x_ix_j (1)
其中,n代表样本的特征数量,x_i就是第i个特征值,w_0,w_i, w_{ij}都是模型参数。大家可以对比一下这个公式和多项式线性回归,其实两者是差不多的。我们知道,要训练w_{ij}需要x_ix_j非零,但实际情况是样本存在大量的零值。那FM是怎么解决这个问题的呢?很简单,将W矩阵分解,用隐向量去表示某一维特征,用隐向量的内积来替代两个维度的交叉项系数,即是w_{ij}
W=\begin{bmatrix}w_{11} &w_{12} &...& w_{1n}\\ w_{21} & w_{22} &...& w_{2n} \\ ... & ... & ...& ... \\ w_{n1} & w_{n2} &..& w_{nn} \end{bmatrix} =V^TV = \begin{bmatrix}V_1 \\ V_2 \\...\\V_n\end{bmatrix} \begin{bmatrix}V_1 & V_2 &...&V_n\end{bmatrix}=\begin{bmatrix}v_{11} &v_{12} &...& v_{1k}\\ v_{21} & v_{22} &...& v_{2k} \\ ... & ... & ...& ... \\ v_{n1} & v_{n2} &..& v_{nk} \end{bmatrix} \begin{bmatrix}v_{11} &v_{21} &...& v_{k1}\\ v_{12} & v_{22} &...& v_{k2} \\ ... & ... & ...& ... \\ v_{1k} & v_{2k} &..& v_{nk} \end{bmatrix}
这里要留意,实际用到的w_{ij}是W矩阵的上三角元素。
那么公式1可以改写为:y(X) = w_0+\sum^n_{i=1}w_ix_i+\sum^{n-1}_{i=1}\sum^n_{j=i+1}<v_i,v_j>x_ix_j (2)
<v_i,v_j>=\sum^k_{f=1}v_{i,f} \cdot v_{j,f}
公式2中的最后一项可以进一步改写为:
\begin{equation}\begin{split}\sum^{n-1}_{i=1}\sum^n_{j=i+1}<v_i,v_j>x_ix_j&=\cfrac12\sum^n_{i=1}\sum^n_{j=1}<v_i,v_j>x_ix_j-\cfrac12\sum^n_{i=1}<v_i,v_i>x_ix_i\\&=\cfrac12\sum^n_{i=1}\sum^n_{j=1}\sum^k_{f=1}v_{i,f}v_{j,f}x_ix_j-\cfrac12\sum^n_{i=1}\sum^k_{f=1}v_{i,f}v_{i,f}x_ix_i\\&=\cfrac12\sum^k_{f=1}[(\sum^n_{i=1}v_{i,f}x_i)\cdot(\sum^n_{j=1}v_{j,f}x_j)-\sum^n_{i=1}v^2_{i,f}x^2_i]\\&=\cfrac12\sum^k_{f=1}[(\sum^n_{i=1}v_{i,f}x_i)^2-\sum^n_{i=1}v^2_{i,f}x^2_i]\end{split}\end{equation} (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数据集验证)

上一篇下一篇

猜你喜欢

热点阅读