deepFM
deepfm包含两个部分:因子分解机FM和神经网络DNN,分别负责低阶特征和高阶特征的提取。可以处理全是分类特征的数据,或者分类与数值型结合的数据。
FM部分 是对一阶特征和二阶特征(一阶特征之间的交互)的处理。
DNN部分 是对高阶特征进行交叉处理。
思考🤔:为什么要做特征与特征之间的交互呢?
文献中写到男性青少年喜欢射击游戏和RPG游戏,这意味着app类别、用户性别和年龄的之间是存在相互影响的。一般来说,用户点击行为背后的这种特征交互可以是高度复杂的,其中低阶和高阶特征交互都应该起重要作用。
所以FM考虑低阶特征交互,DNN考虑高阶特征交互。
(一)数据处理
将数据的特征主要分为类别型特征和数值型特征两种,还有其他类(label、userid等)标记为igore feature。
类别类特征 :离散、稀疏的,标记为sparse feature;
数值型特征:连续、稠密的,标记为dense feature。
数据预处理代码,需要对sparse feature进行编码处理,对dense feature进行归一化化处理,对于自己的数据集,需要自行分好sparse feature和dense feature。
python
#CATEGORICAL_COLS=['性别','职业']等类别特征名称
#NUMERIC_COLS=['时间','次数']等连续性数类特征名称
#IGNORE_COLS=['','']#不参与训练的特征名称
import pandas as pd
data=pd.read_cav("datasets.scv")
sparse_data=data[CATEGORICAL_COLS]
dense_data=train[NUMERIC_COLS]
ignore_data=train[IGNORE_COLS]
对dense fea进行编码处理
python
from sklearn.preprocessing import LabelEncoder
from tqdm import tqdm
#训练数据集
## 类别特征labelencoder
for feat in tqdm(CATEGORICAL_COLS):
lbe = LabelEncoder()
sparse_data[feat] = lbe.fit_transform(sparse_data[feat])
对sparse fea进行标准化处理
python
for feat in tqdm(NUMERIC_COLS): #将每一个特征下的数据进行标准化
mean = dense_data[feat].mean()
std = dense_data[feat].std()
dense_data[feat] = (dense_data[feat] - mean) / (std + 1e-12) # 防止除零
将数据转为模型可处理的tensor类型,划分训练测试集
python
from sklearn.model_selection import train_test_split
import torch
import torch.utils.data as Data
data=pd.concat([sparse_data, dense_data,data['label']], axis=1)
train, valid = train_test_split(data, test_size=0.2, random_state=42)
train_dataset = Data.TensorDataset(torch.LongTensor(train[CATEGORICAL_COLS].values),
torch.FloatTensor(train[NUMERIC_COLS].values),
torch.FloatTensor(train['target'].values), )
train_loader = Data.DataLoader(dataset=train_dataset, batch_size=2048, shuffle=True)
valid_dataset = Data.TensorDataset(torch.LongTensor(valid[CATEGORICAL_COLS].values),
torch.FloatTensor(valid[NUMERIC_COLS].values),
torch.FloatTensor(valid['target'].values), )
valid_loader = Data.DataLoader(dataset=valid_dataset, batch_size=4096, shuffle=False)
(二)模型搭建
python
# 模型结构
import torch
import torch.nn as nn
class DeepFM(nn.Module):
def __init__(self, cate_fea_nuniqs, nume_fea_size=0, emb_size=128,
hid_dims=[256, 128], num_classes=1, dropout=[0.2, 0.2]):
"""
cate_fea_nuniqs: 类别特征的唯一值个数列表,也就是每个类别特征的vocab_size所组成的列表
nume_fea_size: 数值特征的个数,该模型会考虑到输入全为类别型,即没有数值特征的情况
"""
super().__init__()
self.cate_fea_size = len(cate_fea_nuniqs) #分类特征的数量
self.nume_fea_size = nume_fea_size
"""FM部分"""
# 一阶 数值型的利用线性层进行计算得到一个值
#类别型的数据 先用连续的数值表达类别,例如(s/m/l/xl)用1/2/3/4表达 分别对其embedding,得到一个值
if self.nume_fea_size != 0:
self.fm_1st_order_dense = nn.Linear(self.nume_fea_size, 1) # 数值特征的一阶表示
self.fm_1st_order_sparse_emb = nn.ModuleList([ #将类别特征分别进行embedding 有几个类别特征,就有几个embedding层
nn.Embedding(voc_size, 1) for voc_size in cate_fea_nuniqs]) # 类别特征的一阶表示
# 二阶
self.fm_2nd_order_sparse_emb = nn.ModuleList([
nn.Embedding(voc_size, emb_size) for voc_size in cate_fea_nuniqs]) # 类别特征的二阶表示 #将类别特征embedding,输出隐藏层的维度
"""DNN部分"""
self.all_dims = [self.cate_fea_size * emb_size] + hid_dims #二阶类别特征的维度加上隐藏层
self.dense_linear = nn.Linear(self.nume_fea_size, self.cate_fea_size * emb_size) # 数值特征的维度变换到FM输出维度一致
self.relu = nn.ReLU()
# for DNN
for i in range(1, len(self.all_dims)): #all_dims 3维数组 [self.cate_fea_size * emb_size,256,128]
setattr(self, 'linear_' + str(i), nn.Linear(self.all_dims[i - 1], self.all_dims[i]))
#使用setattr函数动态创建线性层。nn.Linear是PyTorch中的线性层(全连接层),
# self.all_dims[i - 1]表示前一层的输出节点数,self.all_dims[i]表示当前层的输出节点数。
# 这样,可以创建一个从前一层到当前层的线性连接。
setattr(self, 'batchNorm_' + str(i), nn.BatchNorm1d(self.all_dims[i]))
setattr(self, 'activation_' + str(i), nn.ReLU())
setattr(self, 'dropout_' + str(i), nn.Dropout(dropout[i - 1]))
# for output
self.dnn_linear = nn.Linear(hid_dims[-1], num_classes)
self.sigmoid = nn.Sigmoid()
def forward(self, X_sparse, X_dense=None):
"""
X_sparse: 类别型特征输入 [bs, cate_fea_size]
X_dense: 数值型特征输入(可能没有) [bs, dense_fea_size]
"""
"""FM 一阶部分"""
fm_1st_sparse_res = [emb(X_sparse[:, i].unsqueeze(1)).view(-1, 1) #将一维张量转换为二维列向量
for i, emb in enumerate(self.fm_1st_order_sparse_emb)] #对分类特征进行embedding
fm_1st_sparse_res = torch.cat(fm_1st_sparse_res, dim=1) # [bs, cate_fea_size]
fm_1st_sparse_res = torch.sum(fm_1st_sparse_res, 1, keepdim=True) # [bs, 1] #最后只有一个值,求和
if X_dense is not None:
fm_1st_dense_res = self.fm_1st_order_dense(X_dense)
fm_1st_part = fm_1st_sparse_res + fm_1st_dense_res
else:
fm_1st_part = fm_1st_sparse_res # [bs, 1] #一阶得到一个值?
"""FM 二阶部分"""
fm_2nd_order_res = [emb(X_sparse[:, i].unsqueeze(1)) for i, emb in enumerate(self.fm_2nd_order_sparse_emb)]
#torch.cat可以沿指定的维度将两个或多个张量连接在一起。
#dim=1 竖向堆叠 dim=0 横向拼接
fm_2nd_concat_1d = torch.cat(fm_2nd_order_res, dim=1) # [bs, n, emb_size] n为类别型特征个数(cate_fea_size)
# 先求和再平方 #求和时不同分类特征 同一维度的值相加。
sum_embed = torch.sum(fm_2nd_concat_1d, 1) # [bs, emb_size]
square_sum_embed = sum_embed * sum_embed # [bs, emb_size]
# 先平方再求和
square_embed = fm_2nd_concat_1d * fm_2nd_concat_1d # [bs, n, emb_size]
sum_square_embed = torch.sum(square_embed, 1) # [bs, emb_size]
# 相减除以2
sub = square_sum_embed - sum_square_embed
sub = sub * 0.5 # [bs, emb_size]
fm_2nd_part = torch.sum(sub, 1, keepdim=True) # [bs, 1] 最后得到一个值?
"""DNN部分"""
dnn_out = torch.flatten(fm_2nd_concat_1d, 1) # [bs, n * emb_size]
if X_dense is not None:
dense_out = self.relu(self.dense_linear(X_dense)) # [bs, n * emb_size]
dnn_out = dnn_out + dense_out # [bs, n * emb_size]
for i in range(1, len(self.all_dims)):
dnn_out = getattr(self, 'linear_' + str(i))(dnn_out)
dnn_out = getattr(self, 'batchNorm_' + str(i))(dnn_out)
dnn_out = getattr(self, 'activation_' + str(i))(dnn_out)
dnn_out = getattr(self, 'dropout_' + str(i))(dnn_out)
dnn_out = self.dnn_linear(dnn_out) # [bs, 1]
out = fm_1st_part + fm_2nd_part + dnn_out # [bs, 1]
out = self.sigmoid(out)
return out
其中
关注公式中的后一项
只用计算xi与其他xj之间的乘积之和,所以只需要矩阵x与自身相乘,然后只需要对成矩阵除对角线外的右上角部分。
因为是对成的,所以2A+对角线上的值就等于整个矩阵的内积。
可以算出来A=(x矩阵内积-对角线的乘积)/2
同时