赞
踩
在2016年, 随着微软的Deep Crossing, 谷歌的Wide&Deep以及FNN、PNN等一大批优秀的深度学习模型被提出, 推荐系统全面进入了深度学习时代, 时至今日, 依然是主流。 推荐模型主要有下面两个进展:
与传统的机器学习模型相比, 深度学习模型的表达能力更强, 能够挖掘更多数据中隐藏的模式
深度学习模型结构非常灵活, 能够根据业务场景和数据特点, 灵活调整模型结构, 使模型与应用场景完美契合
深度学习推荐模型,以多层感知机(MLP)为核心, 通过改变神经网络结构进行演化。
2015年由澳大利亚国立大学提出AutoRec单隐层神经网络模型,由于比较简单, 表达能力不足, 并没有真正的被应用。
在2016年,微软基于ResNet的经典DNN结构, 提出了Deep Crossing模型, 该模型完整的解决了从特征工程、稀疏向量稠密化, 多层神经网络进行优化目标拟合
等一系列深度学习在推荐系统中的应用问题。
为了完成端到端的训练, DeepCrossing模型要在内部网络结构中解决如下问题:
离散类特征编码后过于稀疏, 不利于直接输入神经网络训练, 需要解决稀疏特征向量稠密化的问题
如何解决特征自动交叉组合的问题
如何在输出层中达成问题设定的优化目标
DeepCrossing分别设置了不同神经网络层解决上述问题。 模型结构如下:
Embedding层:
将稀疏的类别型特征转成稠密的Embedding向量,Embedding的维度会远小于原始的系数特征向量
。
Stacking层:
这个层是把不同的Embedding特征和数值型特征拼接在一起, 形成新的包含全部特征的特征向量, 该层通常也称为连接层。
Multiple Residual Units层:
该层的主要结构是多层感知机, 但DeepCrossing采用了残差网络进行的连接。通过多层残差网络对特征向量各个维度充分的交叉组合, 使得模型能够抓取更多的非线性特征和组合特征信息, 增加模型的表达能力
。
Scoring层:
这个作为输出层, 为了拟合优化目标存在。 对于CTR预估二分类问题, Scoring往往采用逻辑回归, 对于多分类, 往往采用Softmax模型
总结一下:
DeepCrossing的结构比较清晰和简单, 没有引入特殊的模型结构, 只是常规的Embedding+多层神经网络。但这个网络模型的出现, 有革命意义。
DeepCrossing模型中没有任何人工特征工程的参与, 只需要清洗一下, 原始特征经Embedding后输入神经网络层, 自主交叉和学习。
相比于FM, FFM只具备二阶特征交叉能力的模型, DeepCrossing可以通过调整神经网络的深度
进行特征之间的“深度交叉”, 这也是Deep Crossing名称的由来。
通过代码,我们更能理解这个模型的细节。
首先, 会有embedding层。这个由于不同的类别特征会通过不同的embedding, 且每个类别的取值个数不一样, 所以有多少个类别特征就需要多少次embedding
。
里面的每个值都是embedding层
。然后就是残差层, 因为Stacking不需要特殊的层, 只需要把特征拼接即可
。 残差层其实就是实现了神经网络的运算过程, 只不过稍微有点不同的是残差网络加了跳远链接
ModuleList
,然后加了一层Dropout层缓解过拟合, 最后一个线性层加sigmoid完成scoring层的实现。import torch.nn as nn import torch.nn.functional as F import torch # 首先, 自定义一个残差块 class Residual_block(nn.Module): """ Define Residual_block 注意:残差块的输入输出需要一致 """ def __init__(self, hidden_unit, dim_stack): super(Residual_block, self).__init__() # 两个线性层 注意维度, 输出的时候和输入的那个维度一致, 这样才能保证后面的相加 self.linear1 = nn.Linear(dim_stack, hidden_unit) self.linear2 = nn.Linear(hidden_unit, dim_stack) self.relu = nn.ReLU() def forward(self, x): orig_x = x.clone() x = self.relu(self.linear1(x)) x = self.linear2(x) outputs = self.relu(x + orig_x) return outputs # 定义deep Crossing 网络 class DeepCrossing(nn.Module): def __init__(self, feature_info, hidden_units, dropout=0., embed_dim=10, output_dim=1): """ DeepCrossing: feature_info: 特征信息(数值特征, 类别特征, 类别特征embedding映射) hidden_units: 列表, 隐藏单元的个数(多层残差那里的) dropout: Dropout层的失活比例 embed_dim: embedding维度 """ super(DeepCrossing, self).__init__() self.dense_features, self.sparse_features, self.sparse_features_map = feature_info # embedding层, 这里需要一个列表的形式, 因为每个类别特征都需要embedding self.embed_layers = nn.ModuleDict( { 'embed_' + str(key): nn.Embedding(num_embeddings=val, embedding_dim=embed_dim) for key, val in self.sparse_features_map.items() } ) # 统计embedding_dim的总维度 # 一个离散型(类别型)变量 通过embedding层变为10纬 embed_dim_sum = sum([embed_dim] * len(self.sparse_features)) # stack layers的总维度 = 数值型特征的纬度 + 离散型变量经过embedding后的纬度 dim_stack = len(self.dense_features) + embed_dim_sum # 残差层 self.res_layers = nn.ModuleList([ Residual_block(unit, dim_stack) for unit in hidden_units ]) # dropout层 self.res_dropout = nn.Dropout(dropout) # 线性层 self.linear = nn.Linear(dim_stack, output_dim) def forward(self, x): # 1、首先得先把输入向量x分成两部分处理、因为数值型和类别型的处理方式不一样, 类别型经过embedding, 数值型是直接进入stacking。 dense_inputs, sparse_inputs = x[:, :len(self.dense_features)], x[:, len(self.dense_features):] # 需要转成长张量,这个是embedding的输入要求格式 sparse_inputs = sparse_inputs.long() # 2、不同的类别特征分别embedding sparse_embeds = [ self.embed_layers['embed_' + key](sparse_inputs[:, i]) for key, i in zip(self.sparse_features_map.keys(), range(sparse_inputs.shape[1])) ] # 3、把类别型特征进行拼接,即emdedding后,转换为一行 sparse_embed = torch.cat(sparse_embeds, axis=-1) # 4、数值型和类别型拼接, 这就是stacking层的任务 stack = torch.cat([sparse_embed, dense_inputs], axis=-1) r = stack # 5、经过残差网络 for res in self.res_layers: r = res(r) # 6、dropout减轻过拟合、sigmoid激活输出 r = self.res_dropout(r) outputs = torch.sigmoid(self.linear(r)) return outputs if __name__ == '__main__': x = torch.rand(size=(1, 5), dtype=torch.float32) feature_info = [ ['I1','I2'], # 连续性特征 ['C1','C2','C3'],# 离散型特征 { 'C1': 20, 'C2': 20, 'C3': 20 } ] hidden_units = [256, 128, 64, 32] net = DeepCrossing(feature_info, hidden_units) print(net) print(net(x))
DeepCrossing( (embed_layers): ModuleDict( (embed_C1): Embedding(20, 10) (embed_C2): Embedding(20, 10) (embed_C3): Embedding(20, 10) ) (res_layers): ModuleList( (0): Residual_block( (linear1): Linear(in_features=32, out_features=256, bias=True) (linear2): Linear(in_features=256, out_features=32, bias=True) (relu): ReLU() ) (1): Residual_block( (linear1): Linear(in_features=32, out_features=128, bias=True) (linear2): Linear(in_features=128, out_features=32, bias=True) (relu): ReLU() ) (2): Residual_block( (linear1): Linear(in_features=32, out_features=64, bias=True) (linear2): Linear(in_features=64, out_features=32, bias=True) (relu): ReLU() ) (3): Residual_block( (linear1): Linear(in_features=32, out_features=32, bias=True) (linear2): Linear(in_features=32, out_features=32, bias=True) (relu): ReLU() ) ) (res_dropout): Dropout(p=0.0, inplace=False) (linear): Linear(in_features=32, out_features=1, bias=True) ) tensor([[0.5532]], grad_fn=<SigmoidBackward0>)
前向传播部分的逻辑:
首先得先把输入向量X分成两部分处理, 因为数值型和类别型的处理方式不一样, 类别型经过embedding, 数值型是直接进入stacking。
而由于Pytorch中, embedding层输入要求是long类型, 需要转一下。
第三行代码就是不同的类别特征分别embedding。
第四行代码是把类别型特征进行拼接, 第五行代码是数值型和类别型拼接, 这就是stacking层的任务。
后面就是经过残差网络, sigmoid激活输出。
这里直接引用王喆老师《深度学习推荐系统》一书page63:
从目前的时间节点上看,Deep Crossing模型是平淡无奇的,因为它没有引人任何诸如注意力机制、序列模型等特殊的模型结构,只是采用了常规的“Embedding+多层神经网络”的经典深度学习结构。
但从历史的尺度看,DeepCrossing模型的出现是有革命意义的。Deep Crossing模型中没有任何人工特征工程的参与,原始特征经Embedding后输入神经网络层,将全部特征交叉的任务交给模型。相比之前介绍的 FM、FFM 模型只具备二阶特征交叉的能力,DeepCrossing模型可以通过调整神经网络的深度进行特征之间的“深度交叉”,这也是 Deep Crossing名称的由来。
Criteo数据集是非常经典的点击率预估比赛。训练集4千万行,特征连续型的有13个,类别型的26个,没有提供特征名称,样本按时间排序。测试集6百万行。
数据集下载地址:https://www.kaggle.com/datasets/mrkmakr/criteo-dataset
由于数据量太大, 为了在单机上能够运行,因此做了采样,取了很少的一部分进行实验。
import pandas as pd
from sklearn.preprocessing import MinMaxScaler, LabelEncoder
from sklearn.model_selection import train_test_split
train_df = pd.read_csv('./data/train.csv')
test_df = pd.read_csv('./data/test.csv')
print(train_df.shape)
print(test_df.shape) # 少了Label这一列
train_df.head()
# 将训练集和测试集进行合并,方便进行特征的预处理工作
label = train_df['Label']
del train_df['Label']
data_df = pd.concat((train_df, test_df))
del data_df['Id']
data_df.columns
Index(['I1', 'I2', 'I3', 'I4', 'I5', 'I6', 'I7', 'I8', 'I9', 'I10', 'I11',
'I12', 'I13', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9',
'C10', 'C11', 'C12', 'C13', 'C14', 'C15', 'C16', 'C17', 'C18', 'C19',
'C20', 'C21', 'C22', 'C23', 'C24', 'C25', 'C26'],
dtype='object')
# C开头是为类别特征
sparse_feas = [col for col in data_df.columns if col[0] == 'C']
# I开头是为连续型特征
dense_feas = [col for col in data_df.columns if col[0] == 'I']
# 1、填充缺失值
# 类别特征填充为-1
data_df[sparse_feas] = data_df[sparse_feas].fillna('-1')
# 连续特征填充为0
data_df[dense_feas] = data_df[dense_feas].fillna(0)
# 2、对类别特征进行LabelEncoder编码(而非one-hot编码)
for feat in sparse_feas:
le = LabelEncoder()
data_df[feat] = le.fit_transform(data_df[feat])
data_df[sparse_feas].head()
# 3、对于连续性变量进行归一化
mms = MinMaxScaler()
data_df[dense_feas] = mms.fit_transform(data_df[dense_feas])
data_df[dense_feas].head()
# 处理后,再分为训练集和测试集
train = data_df[:train_df.shape[0]]
test = data_df[train_df.shape[0]:]
train['Label'] = label
# 对于训练数据集,划分为训练集及验证集
train_set, val_set = train_test_split(train, test_size = 0.2, random_state=2023)
# 统计
train_set['Label'].value_counts()
val_set['Label'].value_counts()
# 保存预处理后的文件,方便以后其他的推荐模型直接使用
train_set.reset_index(drop=True, inplace=True)
val_set.reset_index(drop=True, inplace=True)
train_set.to_csv('preprocessed_data/train_set.csv', index=0)
val_set.to_csv('preprocessed_data/val_set.csv', index=0)
test.to_csv('preprocessed_data/test.csv', index=0)
import datetime import pandas as pd import torch from torch.utils.data import TensorDataset, Dataset, DataLoader import torch.nn as nn import torch.nn.functional as F # pip install torchkeras from torchkeras import summary from sklearn.metrics import auc, roc_auc_score, roc_curve import warnings warnings.filterwarnings('ignore')
# 封装为函数 def prepared_data(file_path): # 读入训练集,验证集和测试集 train_set = pd.read_csv(file_path + 'train_set.csv') val_set = pd.read_csv(file_path + 'val_set.csv') test_set = pd.read_csv(file_path + 'test.csv') # 这里需要把特征分成数值型和离散型 # 因为后面的模型里面离散型的特征需要embedding, 而数值型的特征直接进入了stacking层, 处理方式会不一样 data_df = pd.concat((train_set, val_set, test_set)) # 数值型特征直接放入stacking层 dense_features = ['I' + str(i) for i in range(1, 14)] # 离散型特征需要需要进行embedding处理 sparse_features = ['C' + str(i) for i in range(1, 27)] # 定义一个稀疏特征的embedding映射, 字典{key: value}, # key表示每个稀疏特征, value表示数据集data_df对应列的不同取值个数, 作为embedding输入维度 sparse_feas_map = {} for key in sparse_feas: sparse_feas_map[key] = data_df[key].nunique() feature_info = [dense_features, sparse_features, sparse_feas_map] # 这里把特征信息进行封装, 建立模型的时候作为参数传入 # 把数据构建成数据管道 dl_train_dataset = TensorDataset( # 特征信息 torch.tensor(train_set.drop(columns='Label').values).float(), # 标签信息 torch.tensor(train_set['Label'].values).float() ) dl_val_dataset = TensorDataset( # 特征信息 torch.tensor(val_set.drop(columns='Label').values).float(), # 标签信息 torch.tensor(val_set['Label'].values).float() ) dl_train = DataLoader(dl_train_dataset, shuffle=True, batch_size=16) dl_vaild = DataLoader(dl_val_dataset, shuffle=True, batch_size=16) return feature_info,dl_train,dl_vaild,test_set
# 保存的数据
file_path = './preprocessed_data/'
feature_info,dl_train,dl_vaild,test_set = prepared_data(file_path)
from deep_crossing import DeepCrossing
# 隐藏层
hidden_units = [256, 128, 64, 32]
# 创建DeepCrossing模型
net = DeepCrossing(feature_info, hidden_units)
summary(net, input_shape=(train_set.shape[1],))
-------------------------------------------------------------------------- Layer (type) Output Shape Param # ========================================================================== Embedding-1 [-1, 10] 790 Embedding-2 [-1, 10] 2,520 Embedding-3 [-1, 10] 12,930 Embedding-4 [-1, 10] 10,430 Embedding-5 [-1, 10] 300 Embedding-6 [-1, 10] 70 Embedding-7 [-1, 10] 11,640 Embedding-8 [-1, 10] 390 Embedding-9 [-1, 10] 20 Embedding-10 [-1, 10] 9,080 Embedding-11 [-1, 10] 9,260 Embedding-12 [-1, 10] 12,390 Embedding-13 [-1, 10] 8,240 Embedding-14 [-1, 10] 200 Embedding-15 [-1, 10] 8,190 Embedding-16 [-1, 10] 11,590 Embedding-17 [-1, 10] 90 Embedding-18 [-1, 10] 5,340 Embedding-19 [-1, 10] 2,010 Embedding-20 [-1, 10] 40 Embedding-21 [-1, 10] 12,040 Embedding-22 [-1, 10] 70 Embedding-23 [-1, 10] 120 Embedding-24 [-1, 10] 7,290 Embedding-25 [-1, 10] 330 Embedding-26 [-1, 10] 5,540 Linear-27 [-1, 256] 70,144 ReLU-28 [-1, 256] 0 Linear-29 [-1, 273] 70,161 ReLU-30 [-1, 273] 0 Linear-31 [-1, 128] 35,072 ReLU-32 [-1, 128] 0 Linear-33 [-1, 273] 35,217 ReLU-34 [-1, 273] 0 Linear-35 [-1, 64] 17,536 ReLU-36 [-1, 64] 0 Linear-37 [-1, 273] 17,745 ReLU-38 [-1, 273] 0 Linear-39 [-1, 32] 8,768 ReLU-40 [-1, 32] 0 Linear-41 [-1, 273] 9,009 ReLU-42 [-1, 273] 0 Dropout-43 [-1, 273] 0 Linear-44 [-1, 1] 274 ========================================================================== Total params: 394,836 Trainable params: 394,836 Non-trainable params: 0 -------------------------------------------------------------------------- Input size (MB): 0.000153 Forward/backward pass size (MB): 0.028061 Params size (MB): 1.506180 Estimated Total Size (MB): 1.534393 --------------------------------------------------------------------------
# 测试一下模型
for feature, label in iter(dl_train):
out = net(feature)
print(out)
break
# 导入的这两个类可以参考 # https://blog.csdn.net/qq_44665283/article/details/130598697?spm=1001.2014.3001.5502 from AnimatorClass import Animator from TimerClass import Timer # 模型的相关设置 def metric_func(y_pred, y_true): pred = y_pred.data y = y_true.data return roc_auc_score(y, pred) def try_gpu(i=0): if torch.cuda.device_count() >= i + 1: return torch.device(f'cuda:{i}') return torch.device('cpu') # 封装为函数,方便其他推荐模型进行复用 def train_ch(net, dl_train, dl_vaild, num_epochs, lr, device): """⽤GPU训练模型""" print('training on', device) net.to(device) # 二值交叉熵损失 loss_func = nn.BCELoss() optimizer = torch.optim.Adam(params=net.parameters(), lr=lr) animator = Animator(xlabel='epoch', xlim=[1, num_epochs],legend=['train loss', 'train auc', 'test loss', 'test auc'] ,figsize=(8.0, 6.0)) timer, num_batches = Timer(), len(dl_train) log_step_freq = 10 for epoch in range(1, num_epochs + 1): # 训练阶段 net.train() loss_sum = 0.0 metric_sum = 0.0 step = 1 for step, (features, labels) in enumerate(dl_train, 1): timer.start() # 梯度清零 optimizer.zero_grad() # 正向传播 predictions = net(features) loss = loss_func(predictions, labels.unsqueeze(1) ) try: # 这里就是如果当前批次里面的y只有一个类别, 跳过去 metric = metric_func(predictions, labels) except ValueError: pass # 反向传播求梯度 loss.backward() optimizer.step() timer.stop() # 打印batch级别日志 loss_sum += loss.item() metric_sum += metric.item() if step % log_step_freq == 0: animator.add(epoch + step / num_batches,(loss_sum/step, metric_sum/step, None, None)) # 验证阶段 net.eval() val_loss_sum = 0.0 val_metric_sum = 0.0 val_step = 1 for val_step, (features, labels) in enumerate(dl_vaild, 1): with torch.no_grad(): predictions = net(features) val_loss = loss_func(predictions, labels.unsqueeze(1)) try: val_metric = metric_func(predictions, labels) except ValueError: pass val_loss_sum += val_loss.item() val_metric_sum += val_metric.item() if val_step % log_step_freq == 0: animator.add(epoch + val_step / num_batches, (None,None,val_loss_sum / val_step , val_metric_sum / val_step)) print(f'loss {loss_sum/len(dl_train):.3f}, auc {metric_sum/len(dl_train):.3f},' f' val loss {val_loss_sum/len(dl_vaild):.3f}, val auc {val_metric_sum/len(dl_vaild):.3f}') print(f'{num_batches * num_epochs / timer.sum():.1f} examples/sec on {str(device)}')
lr, num_epochs = 0.001, 4
train_ch(net, dl_train, dl_vaild, num_epochs, lr, try_gpu())
y_pred_probs = net(torch.tensor(test_set.values).float())
y_pred = torch.where(
y_pred_probs>0.5,
torch.ones_like(y_pred_probs),
torch.zeros_like(y_pred_probs)
)
y_pred.data[:10]
# 模型的保存与使用 torch.save(net.state_dict(), './model/net_parameter.pkl') # 创建模型 net_clone = DeepCrossing(feature_info, hidden_units) # 加载模型 net_clone.load_state_dict(torch.load('./model/net_parameter.pkl')) # 进行预测 y_pred_probs = net_clone(torch.tensor(test_set.values).float()) y_pred = torch.where( y_pred_probs>0.5, torch.ones_like(y_pred_probs), torch.zeros_like(y_pred_probs) ) y_pred.data[:10]
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。