0
点赞
收藏
分享

微信扫一扫

【Pytorch基础教程27】DeepFM推荐算法


文章目录

  • ​​零、特征组合的发展史​​
  • ​​一、deepFM原理​​
  • ​​二、FM部分的数学优化​​
  • ​​三、改进FM后的模型代码​​
  • ​​四、训练和测试部分​​
  • ​​五、训练结果​​
  • ​​六、使用rec hub包实现deepFM​​
  • ​​6.1 特征工程​​
  • ​​6.2 模型部分​​
  • ​​七、几个问题​​
  • ​​Reference​​

零、特征组合的发展史

提升CTR的有效策略有特征组合,但是随着二阶特征、三阶特征的阶数提高,时间复杂度就升高很多了,在推荐系统中就难以满足实时性的要求。

  • DNN:网络参数过大(对离散特征进行one hot编码,input维度大)
  • 【Pytorch基础教程27】DeepFM推荐算法_全连接

  • DNN改进:加入Field思想,将One hot特征转换为Dense vector,基本思想:避免全连接,分而治之。

【Pytorch基础教程27】DeepFM推荐算法_ide_02


【Pytorch基础教程27】DeepFM推荐算法_deepFM_03

  • FNN和PNN:FNN使用预训练好的FM得到隐向量,把隐向量作为DNN的输入;后面提出的PNN,在embedding layer和hidden layer1之间增加一个product层(替代FM预训练层)。
  • 【Pytorch基础教程27】DeepFM推荐算法_推荐算法_04

  • wide&deep:FNN和PNN模型中因为是FM和DNN串行方式,导致学到较少低阶组合特征(DNN的全连接结构导致低阶特征并不能在DNN的输出端较好的表现),所以将串行方式改为并行方式即为wide&deep模型,但这里需要一些特征工程。

一、deepFM原理

上次在​​【推荐算法实战】DeepFM模型(tensorflow版)​​已经过了一遍模型的大体原理和tensorflow实现,本文侧重FM公式的优化改机和deepFM代码的pytorch版本的实现。

【Pytorch基础教程27】DeepFM推荐算法_deepFM_05

DeepFM模型架构图 (出自论文 DeepFM: A Factorization-Machine based Neural Network for CTR Prediction)


由上图的DeepFM架构图看出:
(1)用FM层替换了wide&deep左边你的wide部分;
——加强浅层网络的特征组合能力。
(2)右边保持和wide&deep一毛一样,利用多层神经元(如MLP)进行所有特征的深层处理
(3)最后输出层将FM的output和deep的output组合起来,产生预估结果

模型模块的具体分析:

(1)FM和deep模块共享 feature embedding部分

(2)Sparse Feature 到 Dense Embedding 的 Embedding 矩阵,中间也是全连接的,要训练的是中间的权重矩阵,这个权重矩阵也就是隐向量 Vi。

(3)最后的一层:将FM部分(​​fm_1st_part​​​、二阶特征交叉​​fm_2nd_part​​​)和DNN部分(​​dnn_output​​)拼接得到的向量送到最后的sigmoid函数中。

【Pytorch基础教程27】DeepFM推荐算法_deepFM_05


FM部分的输出:

而在特征处理(维度)上看:
(1)deepFM的输入可以由连续型变量和类别型变量共同组成(类别型变量进行one-hot独热编码,但是这样会高维且稀疏,可以通过word2vec进行词嵌入映射到低维稠密向量中)
(2)这里的即deepFM中embedding vector的权矩阵
(3)下图中使用用全连接的方式将Dense Embedding输入到Hidden Layer,这里面Dense Embeddings就是为了解决DNN中的参数爆炸问题。dense embedding层的神经元个数 = embedding vector * field_size,其中​​​field_size​​​即原始特征的数量。
(4)DNN的输入是embedding vector,权值共享。

【Pytorch基础教程27】DeepFM推荐算法_deepFM_09


从上图中:FM Layer是由一阶特征和二阶特征Concatenate到一起在经过一个Sigmoid得到logits。

二、FM部分的数学优化

优化后的FM部分能够将原来的时间复杂度降低为,其中n是特征数量,k是embedding size。具体的优化公式如下,其中第一步比较好理解(多算了一次所以要除二,减去多余的),第二步就是将都是k维的向量的点积公式展开。第三步是难点(见下图),第四步前项因为两个不同的下标 i 和 j ,都是全和,所以可以合并为一个下标。

其中第三步(如下)是关键,

【Pytorch基础教程27】DeepFM推荐算法_git_15

如果把k维特征向量内积求和公式抽到最外边后,公式就转成了上图这个公式了(不考虑最外边k维求和过程的情况下)。它有两层循环,内循环其实就是指定某个特征的第f位(这个f是由最外层那个k指定的)后,和其它任意特征对应向量的第f位值相乘求和;而外循环则是遍历每个的第f位做循环求和。这样就完成了指定某个特征位f后的特征组合计算过程。最外层的k维循环则依此轮循第f位,于是就算完了步骤三的特征组合。

三、改进FM后的模型代码

同样是将​​fm_1st_part​​​、二阶特征交叉​​fm_2nd_part​​​、​​dnn_output​​,拼接得到的向量送到最后的sigmoid函数中,进行预测。

其中构造一阶特征的embedding ModuleList:

import torch.nn as nn
categorial_feature_vocabsize = [20] * 8 + [30001] + [1001]
# 列表[20, 20, 20, 20, 20, 20, 20, 20, 30001, 1001]
fm_1st_order_sparse_emb = nn.ModuleList([nn.Embedding(vocab_size, 1)
for vocab_size
in categorial_feature_vocabsize])
print(fm_1st_order_sparse_emb)
"""
ModuleList(
(0): Embedding(20, 1)
(1): Embedding(20, 1)
(2): Embedding(20, 1)
(3): Embedding(20, 1)
(4): Embedding(20, 1)
(5): Embedding(20, 1)
(6): Embedding(20, 1)
(7): Embedding(20, 1)
(8): Embedding(30001, 1)
(9): Embedding(1001, 1)
)
"""

数值型特征​​xi​​​大小为​​[128, 7]​​​、类别型特征​​xv​​​大小为​​[128, 10]​​​、​​y​​​大小为​​[128]​​,这里的128是设定了batch_size=128。

完整的模型部分:

import torch
import torch.nn as nn
import numpy as np

class DeepFM(nn.Module):
def __init__(self, categorial_feature_vocabsize, continous_feature_names, categorial_feature_names,
embed_dim=10, hidden_dim=[128, 128]):
super().__init__()
assert len(categorial_feature_vocabsize) == len(categorial_feature_names)
self.continous_feature_names = continous_feature_names
self.categorial_feature_names = categorial_feature_names
# FM part
# first-order part
if continous_feature_names:
self.fm_1st_order_dense = nn.Linear(len(continous_feature_names), 1)
self.fm_1st_order_sparse_emb = nn.ModuleList([nn.Embedding(vocab_size, 1) for vocab_size in categorial_feature_vocabsize])
# senond-order part
self.fm_2nd_order_sparse_emb = nn.ModuleList([nn.Embedding(vocab_size, embed_dim) for vocab_size in categorial_feature_vocabsize])

# deep part
self.dense_embed = nn.Sequential(nn.Linear(len(continous_feature_names), len(categorial_feature_names)*embed_dim),
nn.BatchNorm1d(len(categorial_feature_names)*embed_dim),
nn.ReLU(inplace=True))
self.dnn_part = nn.Sequential(nn.Linear(len(categorial_feature_vocabsize)*embed_dim, hidden_dim[0]),
nn.BatchNorm1d(hidden_dim[0]),
nn.ReLU(inplace=True),
nn.Linear(hidden_dim[0], hidden_dim[1]),
nn.BatchNorm1d(hidden_dim[1]),
nn.ReLU(inplace=True),
nn.Linear(hidden_dim[1], 1))
# output act
self.act = nn.Sigmoid()

def forward(self, xi, xv):
# FM first-order part
fm_1st_sparse_res = []
for i, embed_layer in enumerate(self.fm_1st_order_sparse_emb):
fm_1st_sparse_res.append(embed_layer(xv[:, i].long()))
fm_1st_sparse_res = torch.cat(fm_1st_sparse_res, dim=1)
fm_1st_sparse_res = torch.sum(fm_1st_sparse_res, 1, keepdim=True)
if xi is not None:
fm_1st_dense_res = self.fm_1st_order_dense(xi)
fm_1st_part = fm_1st_dense_res + fm_1st_sparse_res
else:
fm_1st_part = fm_1st_sparse_res


# FM second-order part
fm_2nd_order_res = []
for i, embed_layer in enumerate(self.fm_2nd_order_sparse_emb):
fm_2nd_order_res.append(embed_layer(xv[:, i].long()))
fm_2nd_concat_1d = torch.stack(fm_2nd_order_res, dim=1) # [bs, n, emb_dim]
# sum -> square
square_sum_embed = torch.pow(torch.sum(fm_2nd_concat_1d, dim=1), 2)
# square -> sum
sum_square_embed = torch.sum(torch.pow(fm_2nd_concat_1d, 2), dim=1)
# minus and half,(和平方-平方和)
sub = 0.5 * (square_sum_embed - sum_square_embed)
fm_2nd_part = torch.sum(sub, 1, keepdim=True)


# Dnn part
dnn_input = torch.flatten(fm_2nd_concat_1d, 1)
if xi is not None:
dense_out = self.dense_embed(xi)
dnn_input = dnn_input + dense_out
dnn_output = self.dnn_part(dnn_input)

out = self.act(fm_1st_part + fm_2nd_part + dnn_output)
return

四、训练和测试部分

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import pandas as pd
import argparse

from torch.utils.data import DataLoader
from torch.utils.data import sampler
from data.dataset import build_dataset
from model.DeepFM import DeepFM

def train(epoch):
model.train()
for batch_idx, (xi, xv, y) in enumerate(loader_train):
xi, xv, y = torch.squeeze(xi).to(torch.float32), \
torch.squeeze(xv), \
torch.squeeze(y).to(torch.float32)
#print("xi的大小:\n", xi.shape, "\n") # torch.Size([128, 7])
#print("xv的大小:\n", xv.shape, "\n") # torch.Size([128, 10])
#print("y的大小:\n", y.shape, "\n") # torch.Size([128])

if args.gpu:
# 迁移到GPU中,注意迁移的device要和模型的device相同
xi, xv, y = xi.to(device), xv.to(device), y.to(device)
# 梯度清零
optimizer.zero_grad()
# 向前传递,和计算loss值
out = model(xi, xv)
loss = nn.BCELoss()(torch.squeeze(out, dim=1), y)
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
if batch_idx % 200 == 0:
print("epoch {}, batch_idx {}, loss {}".format(epoch, batch_idx, loss))

def test(epoch, best_acc=0):
model.eval()
test_loss = 0.0 # cost function error
correct = 0.0
for batch_idx, (xi, xv, y) in enumerate(loader_test):
xi, xv, y = torch.squeeze(xi).to(torch.float32), \
torch.squeeze(xv), \
torch.squeeze(y).to(torch.float32)

if args.gpu:
xi, xv, y = xi.to(device), \
xv.to(device), \
y.to(device)
out = model(xi, xv)
test_loss += nn.BCELoss()(torch.squeeze(out, dim=1), y).item()
correct += ((torch.squeeze(out, dim=1) > 0.5) == y).sum().item()
if correct/len(loader_test) > best_acc:
best_acc = correct/len(loader_test)
torch.save(model, args.save_path)
print("epoch {}, test loss {}, test acc {}".format(epoch,
test_loss/len(loader_test),
correct/len(loader_test)))
return

主程序:

if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('-gpu', action='store_true', default=True, help='use gpu or not ')
parser.add_argument('-bs', type=int, default=128, help='batch size for dataloader')
parser.add_argument('-epoches', type=int, default=15, help='batch size for dataloader')
parser.add_argument('-warm', type=int, default=1, help='warm up training phase')
parser.add_argument('-lr', type=float, default=1e-3, help='initial learning rate')
parser.add_argument('-resume', action='store_true', default=False, help='resume training')
parser.add_argument('-train_path', action='store_true', default='data/raw/trainingSamples.csv',
help='train data path')
parser.add_argument('-test_path', action='store_true', default='data/raw/testSamples.csv',
help='test data path')
parser.add_argument('-save_path', action='store_true', default='checkpoint/DeepFM/DeepFm_best.pth',
help='save model path')

args = parser.parse_args()

# 连续型特征(7个)
continous_feature_names = ['releaseYear', 'movieRatingCount', 'movieAvgRating', 'movieRatingStddev',
'userRatingCount', 'userAvgRating', 'userRatingStddev']
# 类别型特征,注意id类的特征也是属于类别型特征,有10个特征(8个genre,2个id)
categorial_feature_names = ['userGenre1', 'userGenre2', 'userGenre3', 'userGenre4', 'userGenre5',
'movieGenre1', 'movieGenre2', 'movieGenre3', 'userId', 'movieId']

categorial_feature_vocabsize = [20] * 8 + [30001] + [1001]
# [20, 20, 20, 20, 20, 20, 20, 20, 30001, 1001] ,最后两个分别是userId 和 movieId

# build dataset for train and test
batch_size = args.bs
train_data = build_dataset(args.train_path)
# 用dataloader读取数据
loader_train = DataLoader(train_data,
batch_size=batch_size,
num_workers=8,
shuffle=True,
pin_memory=True)
test_data = build_dataset(args.test_path)
loader_test = DataLoader(test_data,
batch_size=batch_size,
num_workers=8)

# 正向传播时:开启自动求导的异常侦测
torch.autograd.set_detect_anomaly(True)
device = torch.device("cuda" if args.gpu else "cpu")

# train model
model = DeepFM(categorial_feature_vocabsize,
continous_feature_names,
categorial_feature_names,
embed_dim=64)
model = model.to(device)
optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-3)
best_acc = 0
for ep in range(args.epoches):
# ep为训练的轮次epoch
train(ep)
best_acc = test(ep, best_acc)

五、训练结果

最后训练得到的模型在测试集上的准确度为86.67%,效果还是阔以的!

epoch 0, batch_idx 0, loss 57.8125
epoch 0, batch_idx 200, loss 35.660301208496094
epoch 0, batch_idx 400, loss 39.15966033935547
epoch 0, batch_idx 600, loss 33.86609649658203
epoch 0, test loss 23.580318277532403, test acc 74.97727272727273
epoch 1, batch_idx 0, loss 32.055198669433594
epoch 1, batch_idx 200, loss 28.279659271240234
epoch 1, batch_idx 400, loss 21.818199157714844
epoch 1, batch_idx 600, loss 8.688355445861816
epoch 1, test loss 18.055269842798058, test acc 78.07954545454545
epoch 2, batch_idx 0, loss 19.79637908935547
epoch 2, batch_idx 200, loss 13.639955520629883
epoch 2, batch_idx 400, loss 10.169021606445312
epoch 2, batch_idx 600, loss 9.186278343200684
epoch 2, test loss 6.011827245354652, test acc 78.7215909090909
。。。。。。。。。
epoch 13, batch_idx 0, loss 0.4752316176891327
epoch 13, batch_idx 200, loss 0.42497631907463074
epoch 13, batch_idx 400, loss 0.5759319067001343
epoch 13, batch_idx 600, loss 0.6097909212112427
epoch 13, test loss 0.6569343952631409, test acc 86.55681818181819
epoch 14, batch_idx 0, loss 0.36898481845855713
epoch 14, batch_idx 200, loss 0.42660871148109436
epoch 14, batch_idx 400, loss 0.5741548538208008
epoch 14, batch_idx 600, loss 0.5197790861129761
epoch 14, test loss 0.7259972247887742, test acc 86.67613636363636

六、使用rec hub包实现deepFM

这里使用的是经典的criteo广告数据集,原数据集 包括对百万个展示广告的点击反馈记录,有40个特征(其中13个dense特征和26个sparse类别特征)

6.1 特征工程

  • Dense特征:又称数值型特征,例如薪资、年龄。 这里对Dense特征进行两种操作:
  • MinMaxScaler归一化,使其取值在[0,1]之间
  • 将其离散化成新的Sparse特征
  • Sparse特征:又称类别型特征,例如性别、学历。这里对Sparse特征直接进行LabelEncoder编码操作,将原始的类别字符串映射为数值,在模型中将为每一种取值生成Embedding向量。

注:这里留意一个有效的离散化trick,对数值型特征进行取​​log​​运算后离散化操作。

6.2 模型部分

整个模型拆成三部分,分别是一阶特征处理linear部分,二阶特征交叉FM以及DNN的高阶特征交叉。

  • ​linear_logits​​​: 这部分是有关于线性计算,也就是FM的前半部分一阶特征交叉的计算。
  • 对于这一块的计算,我们用了一个get_linear_logits函数实现,后面再说,总之通过这个函数,我们就可以实现上面这个公式的计算过程,得到linear的输出,这部分特征由数值特征和类别特征的onehot编码组成的一维向量组成
  • 实际应用中根据自己的业务放置不同的一阶特征(这里的dense特征并不是必须的,有可能会将数值特征进行分桶,然后在当做类别特征来处理)
  • ​fm_logits​​: 这一块主要是针对离散的特征,首先过embedding,然后使用FM特征交叉的方式,两两特征进行交叉,得到新的特征向量,最后计算交叉特征的logits
  • ​dnn_logits​​: 这一块主要是针对离散的特征,首先过embedding,然后将得到的embedding拼接成一个向量(具体参考下面的模型结构图),通过dnn学习类别特征之间的隐式特征交叉并输出logits值

【Pytorch基础教程27】DeepFM推荐算法_全连接_17

  • deep_features指用deep模块训练的特征(兼容dense和sparse),使用全连接的方式将dense embedding输入到hidden layer中,这里的dense embeddings就是为了解决DNN的参数爆炸问题,也是推荐模型中的常见处理方法。
  • fm_features指用fm模块训练的特征,只能传入sparse类型

class MyDeepFM(torch.nn.Module):
# Deep和FM为两部分,分别处理不同的特征,因此传入的参数要有两种特征,由此我们得到参数deep_features,fm_features
# 此外神经网络类的模型中,基本组成原件为MLP多层感知机,多层感知机的参数也需要传进来,即为mlp_params
def __init__(self, deep_features, fm_features, mlp_params):
super().__init__()
self.deep_features = deep_features
self.fm_features = fm_features
self.deep_dims = sum([fea.embed_dim for fea in deep_features])
self.fm_dims = sum([fea.embed_dim for fea in fm_features])
# LR建模一阶特征交互
self.linear = LR(self.fm_dims)
# FM建模二阶特征交互
self.fm = FM(reduce_sum=True)
# 对特征做嵌入表征
self.embedding = EmbeddingLayer(deep_features + fm_features)
self.mlp = MLP(self.deep_dims, **mlp_params)

def forward(self, x):
input_deep = self.embedding(x, self.deep_features, squeeze_dim=True) #[batch_size, deep_dims]
input_fm = self.embedding(x, self.fm_features, squeeze_dim=False) #[batch_size, num_fields, embed_dim]

y_linear = self.linear(input_fm.flatten(start_dim=1))
y_fm = self.fm(input_fm)
y_deep = self.mlp(input_deep) #[batch_size, 1]
# 最终的预测值为一阶特征交互,二阶特征交互,以及深层模型的组合
y = y_linear + y_fm + y_deep
# 利用sigmoid来将预测得分规整到0,1区间内
return torch.sigmoid(y.squeeze(1))

  • 看项目源码可知这里的​​fm​​​的返回值是二阶特征交叉部分,​​y_linear​​​是一阶特征交叉部分(用​​LR​​进行建模了)
  • 这里的​​self.embedding​​​层也很常见,和​​torch.nn.embedding​​​功能类似,传入对应的​​feature_name​​​列表,然后返回对应特征的embedding字典,这里也可以参考​​【Pytorch基础教程28】浅谈torch.nn.embedding​​,通过embedding层将高维向量转为低维稠密向量。

七、几个问题

  • 如果对于FM采用随机梯度下降SGD训练模型参数,请写出模型各个参数的梯度和FM参数训练的复杂度
  • 对于下图所示,Sparse Feature中的不同颜色节点分别表示什么意思:对sparse features进行one hot后的1的部分即下图中的黄色圆圈部分。

【Pytorch基础教程27】DeepFM推荐算法_全连接_18

Reference

[1] https://zhuanlan.zhihu.com/p/58160982
[2] deepFM论文地址:https://www.ijcai.org/proceedings/2017/0239.pdf
[3] ​​​PyTorch手把手自定义Dataset读取数据​​​ [4] https://pytorch.org/docs/stable/data.html?highlight=dataloader#torch.utils.data.DataLoader
[5] ​​Migrating feature_columns to TF2’s Keras Preprocessing Layers​​ [6] ​​对于DeepFM参数共享的理解​​ [7] ​​torch.autograd.detect_anomaly()​​ [8] deepFM算法代码:http://blog.sina.com.cn/s/blog_628cc2b70102yooc.html
[9] datawhale funRec项目:https://datawhalechina.github.io/fun-rec/#/ch02/ch2.2/ch2.2.3/DeepFM
[10] 推荐算法(四)——经典模型 DeepFM 原理详解及代码实践
[11] ​​https://github.com/datawhalechina/fun-rec​​ [12] ​​https://github.com/datawhalechina/torch-rechub/tree/main/tutorials​​


举报

相关推荐

0 条评论