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

网友投稿 1368 2022-11-17

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

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

文章目录

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

​​6.1 特征工程​​​​6.2 模型部分​​

​​七、几个问题​​​​Reference​​

零、特征组合的发展史

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

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

一、deepFM原理

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

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函数中。

FM部分的输出:

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

二、FM部分的数学优化

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

如果把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 nncategorial_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 torchimport torch.nn as nnimport numpy as npclass 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 torchimport torch.nn as nnimport torch.optim as optimimport numpy as npimport pandas as pdimport argparsefrom torch.utils.data import DataLoaderfrom torch.utils.data import samplerfrom data.dataset import build_datasetfrom model.DeepFM import DeepFMdef 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.8125epoch 0, batch_idx 200, loss 35.660301208496094epoch 0, batch_idx 400, loss 39.15966033935547epoch 0, batch_idx 600, loss 33.86609649658203epoch 0, test loss 23.580318277532403, test acc 74.97727272727273epoch 1, batch_idx 0, loss 32.055198669433594epoch 1, batch_idx 200, loss 28.279659271240234epoch 1, batch_idx 400, loss 21.818199157714844epoch 1, batch_idx 600, loss 8.688355445861816epoch 1, test loss 18.055269842798058, test acc 78.07954545454545epoch 2, batch_idx 0, loss 19.79637908935547epoch 2, batch_idx 200, loss 13.639955520629883epoch 2, batch_idx 400, loss 10.169021606445312epoch 2, batch_idx 600, loss 9.186278343200684epoch 2, test loss 6.011827245354652, test acc 78.7215909090909。。。。。。。。。epoch 13, batch_idx 0, loss 0.4752316176891327epoch 13, batch_idx 200, loss 0.42497631907463074epoch 13, batch_idx 400, loss 0.5759319067001343epoch 13, batch_idx 600, loss 0.6097909212112427epoch 13, test loss 0.6569343952631409, test acc 86.55681818181819epoch 14, batch_idx 0, loss 0.36898481845855713epoch 14, batch_idx 200, loss 0.42660871148109436epoch 14, batch_idx 400, loss 0.5741548538208008epoch 14, batch_idx 600, loss 0.5197790861129761epoch 14, test loss 0.7259972247887742, test acc 86.67613636363636

六、使用rec hub包实现deepFM

6.1 特征工程

Dense特征:又称数值型特征,例如薪资、年龄。 这里对Dense特征进行两种操作:

MinMaxScaler归一化,使其取值在[0,1]之间将其离散化成新的Sparse特征

Sparse特征:又称类别型特征,例如性别、学历。这里对Sparse特征直接进行LabelEncoder编码操作,将原始的类别字符串映射为数值,在模型中将为每一种取值生成Embedding向量。

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

6.2 模型部分

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

对于这一块的计算,我们用了一个get_linear_logits函数实现,后面再说,总之通过这个函数,我们就可以实现上面这个公式的计算过程,得到linear的输出,这部分特征由数值特征和类别特征的onehot编码组成的一维向量组成;实际应用中根据自己的业务放置不同的一阶特征(这里的dense特征并不是必须的,有可能会将数值特征进行分桶,然后在当做类别特征来处理)

​​fm_logits​​: 这一块主要是针对离散的特征,首先过embedding,然后使用FM特征交叉的方式,两两特征进行交叉,得到新的特征向量,最后计算交叉特征的logits​​dnn_logits​​: 这一块主要是针对离散的特征,首先过embedding,然后将得到的embedding拼接成一个向量(具体参考下面的模型结构图),通过dnn学习类别特征之间的隐式特征交叉并输出logits值

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的部分即下图中的黄色圆圈部分。

Reference

[1] [2] deepFM论文地址:[3] ​​​PyTorch手把手自定义Dataset读取数据​​​ [4] [5] ​​Migrating feature_columns to TF2’s Keras Preprocessing Layers​​ [6] ​​对于DeepFM参数共享的理解​​ [7] ​​torch.autograd.detect_anomaly()​​ [8] deepFM算法代码:[9] datawhale funRec项目:[10] 推荐算法(四)——经典模型 DeepFM 原理详解及代码实践 [11] ​​[12] ​​https://github.com/datawhalechina/torch-rechub/tree/main/tutorials​​

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:各种知识收集1(持续更新)
下一篇:kubernetes之多容器pod以及通信
相关文章

 发表评论

暂时没有评论,来抢沙发吧~