tensorflow2.0实现xDeepFM


本文基于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} Xk1 ,然后经过以下的公式计算:
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=1Hk1j=1mWijk,h(Xi,k1Xj,0)R1D, where 1hHk
得到输出: X k ∈ R H k ∗ D \mathbf X^k \in \mathbb R^{H_k * D} XkRHkD

其中 W k , h ∈ R H k − 1 ∗ m W^{k, h} \in \mathbb R^{H_{k-1} * m} Wk,hRHk1m,表示要得到第 k 层第 h 个向量所需要的权重矩阵, H k − 1 H_{k-1} Hk1 表示第 k − 1 k-1 k1 层的输出矩阵 X k − 1 X^{k-1} Xk1 H k − 1 H_{k-1} Hk1 个维度为 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,k1Xj,0 是表示取出 X k − 1 X^{k-1} Xk1 的第 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

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值