本文基于tensorflow2.0实现的 xDeepFM 结构。数据集: Criteo 的500000条数据子集。
必要的库
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow.keras.layers import *
from tensorflow.keras.models import Model
import tensorflow.keras.backend as K
from sklearn.preprocessing import LabelEncoder
数据预处理
Criteo 数据集一共39个特征,1个label特征,如下图所示:以 l 开头的是数值型特征,以 C 开头的是类别型特征。
train = pd.read_csv('./criteo_sampled_data.csv')
cols = data.columns.values
train.head()
首先,我们对数据进行简单的处理。
定义特征组
dense_feats = [f for f in cols if f[0] == 'I']
sparse_feats = [f for f in cols if f[0] == 'C']
对于数值型特征,用 0 填充,然后进行log变换。
def process_dense_feats(data,feats):
d = data.copy()
d = d[feats].fillna(0.0)
for f in feats:
d[f] = d[f].apply(lambda x: np.log(x+1) if x>-1 else -1)
return d
data_dense = process_dense_feats(train, dense_feats)
对于类别型特征,用’-1’填充,然后进行类别编码。
def process_spares_feats(data,feats):
d = data.copy()
d = d[feats].fillna('-1')
for f in feats:
d[f] = LabelEncoder().fit_transform(d[f])
return d
data_sparse = process_spares_feats(train,sparse_feats)
合并处理后的特征,加入label
total_data = pd.concat([data_dense,data_sparse],axis=1)
total_data['label'] = train['label']
模型的构建
xDeepFM的模型结构如下:
模型由主要Linear层,CIN层,DNN,输出层Output组成。
part1—Linear层
对于每一个sparse Features 特征,在FM的原理中,一般都是进行one-hot后在进行交叉特征,但是由于稀疏的存在,很多位置的 x i = 0 x_i=0 xi=0,对应的 w i x i = 0 w_i x_i = 0 wixi=0。因此,可以将sparse 特征embedding到1维,对应的值也就是 w i x i w_i x_i wixi的值。
举例说明:假设某品牌有三种取值,进行one-hot后,得到商品为[0,0,1],我们进行线性回归时会得到 w 1 × 0 + w 2 × 0 + w 3 × 1 w_1 \times 0 + w_2 \times 0 + w_3 \times 1 w1×0+w2×0+w3×1 ,只有 w 3 w_3 w3被保留。所以,可以对商品构造一个3*1的embedding向量,向量的值就是对应的系数。
对sparse_feat 具体代码如下:
# 单独对每一个 sparse 特征构造输入
sparse_inputs = []
for f in sparse_feats:
_input = Input([1], name=f)
sparse_inputs.append(_input)
sparse_1d_embed = []
for i, _input in enumerate(sparse_inputs):
f = sparse_feats[i]
voc_size = total_data[f].nunique()
# 使用 l2 正则化防止过拟合
reg = tf.keras.regularizers.l2(0.5)
_embed = Embedding(voc_size, 1, embeddings_regularizer=reg)(_input)
# 由于 Embedding 的结果是二维的,
# 因此如果需要在 Embedding 之后加入 Dense 层,则需要先连接上 Flatten 层
_embed = Flatten()(_embed)
sparse_1d_embed.append(_embed)
# 对每个 embedding lookup 的结果 wi 求和
fst_order_sparse_layer = Add()(sparse_1d_embed)
图中只有sparse_feat的一阶特征。我们额外加入dense_feat的一阶特征。
# 构造每个 dense 特征的输入
dense_inputs = []
for f in dense_feats:
_input = Input([1], name=f)
dense_inputs.append(_input)
# 将输入拼接到一起,方便连接 Dense 层
concat_dense_inputs = Concatenate(axis=1)(dense_inputs) # ?, 13
# 然后连上输出为1个单元的全连接层,表示对 dense 变量的加权求和
fst_order_dense_layer = Dense(1)(concat_dense_inputs) # ?, 1
到这里,分别完成了对Dense特征和Sparse特征的加权求和。最后将二者的结果再求和:
linear_part = Add()([fst_order_dense_layer, fst_order_sparse_layer])
最终结果是:linear_part
Part2—CIN
CIN(Compressed Interaction Network)这是xDeepFM模型最复杂的部分,也是最核心的部分,
这一部分只考虑sparse feature。此部分的网络结构如下图所示:
结合xDeepFM 的结构图可知:CIN的输入来自于Embedding层,上图中的m表示一共有m个域也就是m个sparse feature,将每个特征嵌入到 D 维,这样就得到了CIN的输入矩阵 X 0 \mathbf X^0 X0,接下来的代码用于生成输入矩阵 X 0 \mathbf X^0 X0:
# embedding size
D = 8
# 只考虑sparse的交叉
sparse_kd_embed = []
for i, _input in enumerate(sparse_inputs):
f = sparse_feats[i]
voc_size = data[f].nunique()
reg = tf.keras.regularizers.l2(0.7)
_embed = Embedding(voc_size+1, D, embeddings_regularizer=reg)(_input)
sparse_kd_embed.append(_embed)
# 构建feature mmap X0
input_feature_map = Concatenate(axis=1)(sparse_kd_embed)
print(input_feature_map)
设CIN的内部有k层,k 表示第 k 层的输出,
H
k
H_k
Hk 表示第 k 层有
H
k
H_k
Hk 个维度为 D 的向量。每一层接收两个矩阵:一个是初始输入
X
0
\mathbf X^0
X0,一个是上一层的输出
X
k
−
1
\mathbf X^{k-1}
Xk−1 ,然后经过以下的公式计算:
X
h
,
∗
k
=
∑
i
=
1
H
k
−
1
∑
j
=
1
m
W
i
j
k
,
h
(
X
i
,
∗
k
−
1
∘
X
j
,
∗
0
)
∈
R
1
∗
D
,
where
1
≤
h
≤
H
k
\boldsymbol{X}_{h, *}^{k}=\sum_{i=1}^{H_{k-1}} \sum_{j=1}^{m} \boldsymbol{W}_{i j}^{k, h}\left(\boldsymbol{X}_{i, *}^{k-1} \circ \boldsymbol{X}_{j, *}^{0}\right) \in \mathbb{R}^{1 * D}, \quad \text { where } 1 \leq h \leq H_{k}
Xh,∗k=i=1∑Hk−1j=1∑mWijk,h(Xi,∗k−1∘Xj,∗0)∈R1∗D, where 1≤h≤Hk
得到输出:
X
k
∈
R
H
k
∗
D
\mathbf X^k \in \mathbb R^{H_k * D}
Xk∈RHk∗D 。
其中
W
k
,
h
∈
R
H
k
−
1
∗
m
W^{k, h} \in \mathbb R^{H_{k-1} * m}
Wk,h∈RHk−1∗m,表示要得到第 k 层第 h 个向量所需要的权重矩阵,
H
k
−
1
H_{k-1}
Hk−1 表示第
k
−
1
k-1
k−1 层的输出矩阵
X
k
−
1
X^{k-1}
Xk−1 由
H
k
−
1
H_{k-1}
Hk−1 个维度为 D 的向量组成。
∘
\circ
∘ 表示Hadamard乘积,即逐元素乘,例如:
<
a
1
,
b
1
,
c
1
>
∘
<
a
2
,
b
2
,
c
2
>
=
<
a
1
a
2
,
b
1
b
2
,
c
1
c
2
>
<a_1, b_1, c_1> \circ <a_2, b_2, c_2> = <a_1a_2, b_1b_2, c_1c_2>
<a1,b1,c1>∘<a2,b2,c2>=<a1a2,b1b2,c1c2>
式子中
X
i
,
∗
k
−
1
∘
X
j
,
∗
0
\boldsymbol{X}_{i, *}^{k-1} \circ \boldsymbol{X}_{j, *}^{0}
Xi,∗k−1∘Xj,∗0 是表示取出
X
k
−
1
X^{k-1}
Xk−1 的第
i
i
i 个向量与输入层
X
0
X^{0}
X0 中第
j
j
j 个向量进行 Hadamard 乘积运算。整个公式的计算过程可以用下图表示:
函数:compressed_interaction_net 实现了图中的(1)~(4)
def compressed_interaction_net(x0, xl, k, n_filters):
"""
@param x0: 原始输入
@param xl: 第l层的输入
@param k: embedding dim
@param n_filters: 压缩网络filter的数量
"""
# 这里设x0中共有m个特征,xl中共有h个特征
# 1.将x0与xl按照k所在的维度(-1)进行拆分,每个都可以拆成k列
x0_cols = tf.split(x0, k, axis=-1) # ?, m, k
xl_cols = tf.split(xl, k, axis=-1) # ?, h, k
assert len(x0_cols)==len(xl_cols), print("error shape!")
# 2.遍历k列,对于x0与xl所在的第i列进行外积计算,存在feature_maps中
feature_maps = []
for i in range(k):
feature_map = tf.matmul(xl_cols[i], x0_cols[i], transpose_b=True) # 外积 ?, h, m
feature_map = tf.expand_dims(feature_map, axis=-1) # ?, h, m, 1
feature_maps.append(feature_map)
# 得到 h × m × k 的三维tensor
feature_maps = Concatenate(axis=-1)(feature_maps) # ?, h, m, k
# 3.压缩网络
x0_n_feats = x0.get_shape()[1] # m
xl_n_feats = xl.get_shape()[1] # h
reshaped_feature_maps = Reshape(target_shape=(x0_n_feats * xl_n_feats, k))(feature_maps) # ?, h*m, k
transposed_feature_maps = tf.transpose(reshaped_feature_maps, [0, 2, 1]) # ?, k, h*m
# 4.Conv1D:使用 n_filters 个形状为 1 * (h*m) 的卷积核以 1 为步长,
# 按嵌入维度 D 的方向进行卷积,最终得到形状为 ?, D, n_filters 的输出
new_feature_maps = Conv1D(n_filters, 1, 1)(transposed_feature_maps) # ?, k, n_filters
new_feature_maps = tf.transpose(new_feature_maps, [0, 2, 1]) # ?, n_filters, k
return new_feature_maps
注:这里 n_filters 是经过该 CIN 层后,输出的 feature map 的个数,也就是说最终生成了由 n_filters 个 D 维向量组成的输出矩阵,即 H k = n _ f i l t e r s H_k=n\_filters Hk=n_filters 。
除了第(5)部的加权求和,就基本实现了单层的 CIN 实现,我们通过 build_cin 实现加权求和和多层CIN网络,并且将结果组合输出。
def build_cin(x0, k=8, n_layers=3, n_filters=12):
"""
构建多层CIN网络
@param x0: 原始输入的feature maps: ?, m, k
@param k: 特征embedding的维度
@param n_layers: CIN网络层数
@param n_filters: 每层CIN网络输出的feature_maps的个数
"""
# cin layers
cin_layers = []
# 存储每一层cin sum pooling的结果
pooling_layers = []
xl = x0
for layer in range(n_layers):
xl = compressed_interaction_net(x0, xl, k, n_filters)
cin_layers.append(xl)
#5. sum pooling
pooling = Lambda(lambda x: K.sum(x, axis=-1))(xl)
pooling_layers.append(pooling)
# 将所有层的pooling结果concat起来
output = Concatenate(axis=-1)(pooling_layers)
return output
# 生成 CIN
cin_layer = build_cin(input_feature_map)
此处,我们设定CIN的网络层数维3层,每一层的filter为12,即CIN层会生成3个12*D的feature maps。在经过sum-pooling和concat操作后得到的CIN层的输出维度3*12=36维的向量。
最终结果是:cin_layer
part3—DNN部分
这一部分就是对Embedding的结果简单的全连接,具体代码如下
embed_inputs = Flatten()(Concatenate(axis=-1)(sparse_kd_embed))
fc_layer = Dropout(0.5)(Dense(128, activation='relu')(embed_inputs))
fc_layer = Dropout(0.3)(Dense(128, activation='relu')(fc_layer))
fc_layer_output = Dropout(0.1)(Dense(128, activation='relu')(fc_layer))
最终结果是:fc_layer_output
part4—输出部分
组合part1~part3,然后经过一个全连接层,得到最终结果。
concat_layer = Concatenate()([linear_part, cin_layer, fc_layer_output])
output_layer = Dense(1, activation='sigmoid')(concat_layer)
模型的编译与运行
编译
model = Model(dense_inputs+sparse_inputs, output_layer)
model.compile(optimizer="adam",
loss="binary_crossentropy",
metrics=["binary_crossentropy",
tf.keras.metrics.AUC(name='auc')])
切分数据。训练集:数据集中的前500000条,验证集数据集的最后100000条:
train_data = total_data.loc[:500000-1]
valid_data = total_data.loc[500000:]
train_dense_x = [train_data[f].values for f in dense_feats]
train_sparse_x = [train_data[f].values for f in sparse_feats]
train_label = [train_data['label'].values]
val_dense_x = [valid_data[f].values for f in dense_feats]
val_sparse_x = [valid_data[f].values for f in sparse_feats]
val_label = [valid_data['label'].values]
模型的训练
model.fit(train_dense_x+train_sparse_x,
train_label, epochs=5, batch_size=256,
validation_data=(val_dense_x+val_sparse_x, val_label),
)
训练输出如下:
数据下载地址为:数据下载地址为:链接:https://pan.baidu.com/s/1Qy3yemu1LYVtj0Wn47myHQ 提取码:pv7u