前言在前面一篇文章完成了 Criteo 数据集的清洗与特征处理得到了DataLoader 类型的数据批接下来就可以选择合适的神经网络进行 CTR 预估了博主打算检验一下自己学的经典推荐模型因此后面会使用多个模型进行训练同时也横向对各个模型做个对比这次先使用 DeepFM模型DeepFM 模型FM部分FM 的公式如下公式的前两项是偏置项 w0以及一阶项FM 的精髓在于二阶交叉特征项也就是最后一项xi与 xj表示特征 i 与特征 j 的值前面的 vi, vj 是这两个特征对应的权重显而易见的是把 vi与 vj存储在一个 n x n 的矩阵 W 中n 为特征数量但特征数量很多时会导致矩阵占用空间巨大FM 的做法是利用 W VTVV的形状为 n x kk 是超参数可人为选择这样模型仅需存储一个矩阵 V从而降低了模型的空间复杂度在我的模型中没有使用偏置以及一阶项因为二阶特征交叉已经包括了足够多的信息从深度学习的角度理解V 矩阵实际就是 Embedding 层的权重矩阵在实际计算中二阶特征交互项通常使用下列等价公式来优化时间复杂度具体推导大家如果有兴趣可以去看看我就不写出来了Deep 部分Deep 部分从上面的结构图中看就可以看出来是一个 MLP即多层感知机由多个全连接层图中画了 3 层仅为示例和激活函数组成大家应该都十分熟悉也就不多介绍了模型实现classDeepFM(nn.Module):def__init__(self,emb_size_of_every_col,feature_cols,dim):super(DeepFM,self).__init__()# 每一列一个 Embedding 列的先后顺序与 all_cat_cols 中一致self.embeddingsnn.ModuleList([nn.Embedding(size,dim,max_norm1)forsizeinemb_size_of_every_col])self.mlpself._mlp(feature_cols*dim)def_mlp(self,dim):returnnn.Sequential(nn.Linear(dim,dim//2),nn.Tanh(),nn.Linear(dim//2,dim//4),nn.Tanh(),nn.Linear(dim//4,1)# 不需要 sigmoid)defDeep(self,features_embs):# features_embs: [batch_size, feature_cols, dim]features_embsfeatures_embs.reshape((features_embs.shape[0],-1))# [batch_size, feature_cols * dim]returnself.mlp(features_embs)defFMCross(self,features_embs):# features_embs: [batch_size, feature_cols, dim]square_of_sumtorch.sum(features_embs,dim1)**2# [batch_size, dim]sum_of_squaretorch.sum(features_embs**2,dim1)# [batch_size, dim]return0.5*torch.sum(square_of_sum-sum_of_square,dim1)# [batch_size]defforward(self,X):# X: [batch_size, feature_cols]num_colsX.shape[1]features[]# 取 X 的每一列特征送入该列的 Embeddingforiinrange(num_cols):featureself.embeddings[i](X[:,i])# [batch_size, 1, dim] 第 i 列特征的 Embedding 向量features.append(feature)featurestorch.stack(features,dim1)# [batch_size, feature_cols, dim]# Deep 部分uv_deepself.Deep(features)# [batch_size, 1]uv_deeptorch.squeeze(uv_deep,dim1)# [batch_size]# FM 部分uv_fmself.FMCross(features)# [batch_size]returnuv_deepuv_fm# 由于损失函数采用 BCEWithLogitsLoss 所以不需要取 sigmoid上面是 DeepFM 模型的 PyTorch 实现前面的特征处理中我们得到了39列类别特征包括原始的26列类别特征以及对整数特征分桶得到的13列类别特征这些列的含义各不相同因此模型内维护了一个 nn.ModuleList为每一个特征列都提供 Embedding模型初始化时要传入每列的词表大小 emb_size_of_every_col 用于初始化各列的 Embedding其余代码则按照模型结构图实现训练循环模型编写好就可以来写训练循环了首先加载数据这些在上一篇博客里已经写了train_df,valid_df,test_df,all_cat_cols,encodersprocess(data_path)train_iter,valid_iter,test_iterload_data(train_df,valid_df,test_df,all_cat_cols,batch_size,num_workers)emb_size_of_every_col[len(encoders[col])1forcolinall_cat_cols]# 每个特征列特征值的最大值这一列 Embedding 的大小接着定义模型、损失函数以及优化器# 网络定义modelDeepFM(emb_size_of_every_col,len(all_cat_cols),dimdim)criterionnn.BCEWithLogitsLoss()optimizertorch.optim.AdamW(model.parameters(),lrlr,weight_decayweight_decay)损失函数采用二元交叉熵损失函数 BCEWithLogitsLoss() 该函数在计算损失时会自动对输入的张量求 sigmoid这也是模型 MLP 的输出以及总输出没有取 sigmoid 的原因优化器采用 AdamW包含权重衰减参数能缓解过拟合接下来开始编写训练循环训练基本策略如下训练集每个 epoch 遍历 train_iter进行前向传播、算 loss、反向传播、优化器更新计算模型在训练集上的损失验证集每个 epoch 训练完后跑一次验证集 valid_iter注意使用上下文管理器 torch.no_grad() 不更新参数计算验证集的损失、AUC根据验证集 AUC 保存最佳模型测试集训练全部结束后加载验证集上最好模型仅跑一次 test_iter得到最终 AUC模型的训练以及参数更新foreinrange(epochs):# 模型训练model.train()total_train_loss0.0train_samples0forX,yintrain_iter:optimizer.zero_grad()losscriterion(model(X),y)loss.backward()optimizer.step()total_train_lossloss.item()*len(y)train_sampleslen(y)train_losstotal_train_loss/train_samples每一个 epoch 结束后在验证集上计算 avg_loss 以及 AUCvalid_loss,aucevaluate(model,valid_iter,criterion)evaluate 函数的实现defevaluate(model,valid_iter,criterion):model.eval()all_y_pred[]all_y_label[]total_loss0.0num_samples0withtorch.no_grad():forX,y_labelinvalid_iter:logitsmodel(X)losscriterion(logits,y_label)y_predtorch.sigmoid(logits)# 模型输出的是 logits未经过 sigmoid# 计算总损失与样本数后面会计算 avg_losstotal_lossloss.item()*len(y_label)# BCEWithLogitsLoss 返回的是误差均值别忘了乘该批次样本数量num_sampleslen(y_label)# 将 y_pred 与 y_label 转为 numpy 数组并加入列表后面方便计算 aucall_y_pred.append(y_pred.cpu().numpy())all_y_label.append(y_label.cpu().numpy())# 计算损失avg_losstotal_loss/num_samples# 计算 AUCy_prednp.concatenate(all_y_pred).reshape(-1)y_labelnp.concatenate(all_y_label).reshape(-1)aucroc_auc_score(y_label,y_pred)returnavg_loss,auc计算 AUC 使用的是 sklearn 的 roc_auc_score 函数传入预测值和真实值的 numpy 数组即可根据计算的 auc 更新最佳模型以及 best_auc同时加入早停策略ifaucbest_auc:# 更新最佳模型best_aucauc torch.save(model.state_dict(),save/best_deepfm.pt)bad_epochs0else:# 如果 AUC 没有上涨则早停计数加一bad_epochs1ifbad_epochsearly_stop_patience:print(fTraining early stopped at epoch{e1})break训练所有 epoch 结束后加载最佳模型在测试集上计算 AUCmodel.load_state_dict(torch.load(save/best_deepfm.pt))_,aucevaluate(model,test_iter,criterion)print(fTest AUC:{auc:.4f})完整的训练代码如下importnumpyasnpimporttorchimporttorch.nnasnnfromsklearn.metricsimportroc_auc_scorefromdata_process.processimportprocessfromdata_process.loadimportload_datafrommodels.DeepFMimportDeepFM# 采用 Kaggle 上的 Criteo_1M_with_nans.csv 数据集data_pathdata/Criteo_1M_with_nans.csvbatch_size256num_workers4# 使用子进程数dim16# Embedding 层的维度lr1e-3weight_decay1e-4defevaluate(model,valid_iter,criterion):model.eval()all_y_pred[]all_y_label[]total_loss0.0num_samples0withtorch.no_grad():forX,y_labelinvalid_iter:logitsmodel(X)losscriterion(logits,y_label)y_predtorch.sigmoid(logits)# 模型输出的是 logits未经过 sigmoid# 计算总损失与样本数后面会计算 avg_losstotal_lossloss.item()*len(y_label)# BCEWithLogitsLoss 返回的是误差均值别忘了乘该批次样本数量num_sampleslen(y_label)# 将 y_pred 与 y_label 转为 numpy 数组并加入列表后面方便计算 aucall_y_pred.append(y_pred.cpu().numpy())all_y_label.append(y_label.cpu().numpy())# 计算损失avg_losstotal_loss/num_samples# 计算 AUCy_prednp.concatenate(all_y_pred).reshape(-1)y_labelnp.concatenate(all_y_label).reshape(-1)aucroc_auc_score(y_label,y_pred)returnavg_loss,aucdefmain(epochs,early_stop_patience,best_auc,bad_epochs):train_df,valid_df,test_df,all_cat_cols,encodersprocess(data_path)train_iter,valid_iter,test_iterload_data(train_df,valid_df,test_df,all_cat_cols,batch_size,num_workers)emb_size_of_every_col[len(encoders[col])1forcolinall_cat_cols]# 每个特征列特征值的最大值这一列 Embedding 的大小# 网络定义modelDeepFM(emb_size_of_every_col,len(all_cat_cols),dimdim)criterionnn.BCEWithLogitsLoss()optimizertorch.optim.AdamW(model.parameters(),lrlr,weight_decayweight_decay)foreinrange(epochs):# 模型训练model.train()total_train_loss0.0train_samples0forX,yintrain_iter:optimizer.zero_grad()losscriterion(model(X),y)loss.backward()optimizer.step()total_train_lossloss.item()*len(y)train_sampleslen(y)train_losstotal_train_loss/train_samples# 每一个 epoch 结束后在验证集上计算 avg_loss 以及 AUCvalid_loss,aucevaluate(model,valid_iter,criterion)print(fEpoch{e1}: \ntrain loss:{train_loss:.4f}\nvalid loss:{valid_loss:.4f}\nvalid AUC:{auc:.4f})ifaucbest_auc:# 更新最佳模型best_aucauc torch.save(model.state_dict(),save/best_deepfm.pt)bad_epochs0else:# 如果 AUC 没有上涨则早停计数加一bad_epochs1ifbad_epochsearly_stop_patience:print(fTraining early stopped at epoch{e1})break# 训练结束加载最佳模型在测试集上计算 AUCmodel.load_state_dict(torch.load(save/best_deepfm.pt))_,aucevaluate(model,test_iter,criterion)print(fTest AUC:{auc:.4f})if__name____main__:main(epochs10,early_stop_patience3,best_auc0.0,bad_epochs0)模型参数以及训练参数的设定非最优后面可能还会继续调整参数名称含义值batch_size数据批次的大小X的第一维大小256dimEmbedding 层维度16lr学习率0.001weight_decay权重衰减系数0.0001epochs训练轮数10early_stop_patience早停次数3