【CTR模型】TensorFlow2.0 的 xDeepFM 实现与实战(附代码+数据)
CTR 系列文章:
本篇文章講解 xDeepFM 的 tensorflow2.0 實現,并使用 Criteo 數據集的子集加以實踐。如果在看本文時有所困惑,可以看看 xDeepFM 的相關理論:CTR 模型之 Deep & Cross (DCN) 與 xDeepFM 解讀。
本文使用的數據下載地址于代碼獲取地址在文末獲取。
首先了解一下 Criteo數據集,它由有39個特征,1個label列,其中以I開頭的為數值型特征,以C開頭的為類別特征:
可以看到數據中有缺失值需要填充,并且類別變量需要進行類別編碼(onehot 編碼的任務交給模型),這部分預處理的代碼不詳細講了。
為了方便后面建立模型,先將特征劃分為 dense 特征與 sparse 特征兩個類別:
# 數值型 dense_feats = [f for f in cols if f[0] == "I"] # 類別型 sparse_feats = [f for f in cols if f[0] == "C"]xDeepFM 網絡結構如下:
構造模型輸入
對于 dense 特征,按下面的代碼構造輸入:
# 構造每個 dense 特征的輸入 dense_inputs = [] for f in dense_feats:_input = Input([1], name=f)dense_inputs.append(_input)# 將輸入拼接到一起 concat_dense_inputs = Concatenate(axis=1)(dense_inputs) # ?, 13# 對dense特征加權求和 fst_order_dense_layer = Dense(1)(concat_dense_inputs)上面代碼的注釋中 ? 表示輸入數據的 batch_size。
對于每一個 sparse 特征,一般都是進行one-hot以后再轉化為embedding特征,但實際上由于稀疏性的存在,很多位置的 xix_ixi? 取0時,對應的 wixiw_i x_iwi?xi? 也為0。因此,可以將 sparse 特征 embedding 到 1維,然后通過 embedding lookup 的方式來找到對應的 wiw_iwi? 。
這里舉個例子:假設我們的性別特征取值有-1,0,1三種,某個樣本的取值為1,則其one-hot以后為[0, 0, 1]向量,我們進行線性回歸時會得到 w1×0+w2×0+w3×1w_1 \times 0 + w_2 \times 0 +w_3 \times 1w1?×0+w2?×0+w3?×1 ,僅僅只有 w3w_3w3? 被保留下來。因此,可以對性別構造一個 3*1 的 embedding 向量,然后通過 embedding lookup 得到系數。
對于 sparse 特征,按下面的代碼構造輸入:
# 這里單獨對每一個 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)# 對sparse特征加權求和 fst_order_sparse_layer = Add()(sparse_1d_embed)然后將 dense 特征與 sparse 特征加權求和結果相加,完成模型最左側的 Linear 部分:
linear_part = Add()([fst_order_dense_layer, fst_order_sparse_layer])Compressed Interaction Network
終于來到最核心的 CIN 部分,此部分的網絡結構如下圖所示:
CIN 的輸入來自 embedding 層,假設有 m 個 field,每個 field 的 embedding 維度為 D ,則輸入可以表示為矩陣 X0∈Rm?DX^0 \in \mathbb R^{m * D}X0∈Rm?D。為了得到 X0X^0X0,在進入 CIN 網絡之前,需要先將 sparse 特征進行 embedding 并構建 X0X^0X0:
# embedding size D = 8sparse_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)CIN 內部有 k 層,每一層都會輸出一個矩陣 Xk∈RHk?DX^k \in \mathbb R^{H_k * D}Xk∈RHk??D ,k 表示第 k 層的輸出, HkH_kHk? 表示第 k 層有 HkH_kHk? 個維度為 D 的向量。要得到 XkX^{k}Xk ,需要接收兩個矩陣作為輸入,一個是 Xk?1X^{k-1}Xk?1 ,另一個是 X0X^0X0 ,具體的計算公式如下:
Xh,?k=∑i=1Hk?1∑j=1mWijk,h(Xi,?k?1°Xj,?0)∈R1?D,where?1≤h≤Hk\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?1??j=1∑m?Wijk,h?(Xi,?k?1?°Xj,?0?)∈R1?D,?where?1≤h≤Hk?
其中 Wk,h∈RHk?1?mW^{k, h} \in \mathbb R^{H_{k-1} * m}Wk,h∈RHk?1??m,表示要得到第 k 層第 h 個向量所需要的權重矩陣, Hk?1H_{k-1}Hk?1? 表示第 k?1k-1k?1 層的輸出矩陣 Xk?1X^{k-1}Xk?1 由 Hk?1H_{k-1}Hk?1? 個維度為 D 的向量組成。°\circ° 表示Hadamard乘積。式子中 Xi,?k?1°Xj,?0\boldsymbol{X}_{i, *}^{k-1} \circ \boldsymbol{X}_{j, *}^{0}Xi,?k?1?°Xj,?0? 是表示取出 Xk?1X^{k-1}Xk?1 的第 iii 個向量與輸入層 X0X^{0}X0 中第 jjj 個向量進行 Hadamard 乘積運算。這個部分的計算過程如下圖所示:
此公式的實現代碼如下:
def compressed_interaction_net(x0, xl, D, n_filters):"""@param x0: 原始輸入@param xl: 第l層的輸入@param D: embedding dim@param n_filters: 壓縮網絡filter的數量"""# 這里設x0中共有m個特征,xl中共有h個特征# 1.將x0與xl按照k所在的維度(-1)進行拆分,每個都可以拆成D列x0_cols = tf.split(x0, D, axis=-1) # ?, m, Dxl_cols = tf.split(xl, D, axis=-1) # ?, h, Dassert len(x0_cols)==len(xl_cols), print("error shape!")# 2.遍歷D列,對于x0與xl所在的第i列進行外積計算,存在feature_maps中feature_maps = []for i in range(D):# transpose_b=True 將 x0_cols[i] 轉置feature_map = tf.matmul(xl_cols[i], x0_cols[i], transpose_b=True) # 外積 ?, h, mfeature_map = tf.expand_dims(feature_map, axis=-1) # ?, h, m, 1feature_maps.append(feature_map)# 3.得到 h × m × D 的三維tensorfeature_maps = Concatenate(axis=-1)(feature_maps) # ?, h, m, D# 3.壓縮網絡x0_n_feats = x0.get_shape()[1] # mxl_n_feats = xl.get_shape()[1] # hreshaped_feature_maps = Reshape(target_shape=(x0_n_feats * xl_n_feats, D))(feature_maps) # ?, h*m, Dtransposed_feature_maps = tf.transpose(reshaped_feature_maps, [0, 2, 1]) # ?, D, h*m# Conv1D:使用 n_filters 個形狀為 1 * (h*m) 的卷積核以 1 為步長,# 按嵌入維度 D 的方向進行卷積,最終得到形狀為 ?, D, n_filters 的輸出new_feature_maps = Conv1D(n_filters, kernel_size=1, strides=1)(transposed_feature_maps) # ?, D, n_filters# 為了保持輸出結果最后一維 為嵌入維度 D ,需要進行轉置操作new_feature_maps = tf.transpose(new_feature_maps, [0, 2, 1]) # ?, n_filters, Dreturn new_feature_maps代碼中分為三步:
- 第一步將輸入的兩個需要交互的 feature maps 分成 D 列
- 第二步計算交互的三維 tensor,即在每個維度上進行向外積操作
- 第三步使用一維的卷積操作得到當前層的輸出
這里 n_filters 是經過該 CIN 層后,輸出的 feature map 的個數,也就是說最終生成了由 n_filters 個 D 維向量組成的輸出矩陣。
有了單層的 CIN 實現,我們緊接著可以實現多層的CIN網絡,這里要注意的是 CIN 網絡的輸出是將每一層的 feature maps 進行 sum pooling,然后 concat 起來。相關代碼實現如下:
def build_cin(x0, D=8, n_layers=3, n_filters=12):"""構建多層CIN網絡@param x0: 原始輸入的feature maps: ?, m, D@param D: 特征embedding的維度@param n_layers: CIN網絡層數@param n_filters: 每層CIN網絡輸出的feature_maps的個數"""# 存儲每一層cin sum pooling的結果pooling_layers = []xl = x0for layer in range(n_layers):xl = compressed_interaction_net(x0, xl, D, n_filters)# sum poolingpooling = 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)經過 3 層的 CIN 網絡,每一層的 filter 是12,意味著會產出 3 個 12*D 的 feature maps,再經過 sum-pooling 和 concat 操作后,就得到 3*12=36 維的向量。
DNN 部分
這部分好理解,直接上代碼吧:
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))輸出部分
代碼如下:
concat_layer = Concatenate()([linear_part, cin_layer, fc_layer_output]) output_layer = Dense(1, activation='sigmoid')(concat_layer) model.compile(optimizer="adam", loss="binary_crossentropy", metrics=["binary_crossentropy", tf.keras.metrics.AUC(name='auc')])完善模型
model = Model(dense_inputs+sparse_inputs, output_layer) model.compile(optimizer="adam", loss="binary_crossentropy", metrics=["binary_crossentropy", tf.keras.metrics.AUC(name='auc')])訓練模型
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=128,validation_data=(val_dense_x+val_sparse_x, val_label),)最后,本文的代碼鏈接在:https://github.com/zxxwin/tf2_xDeepFM 。
數據下載地址為:鏈接:https://pan.baidu.com/s/1Qy3yemu1LYVtj0Wn47myHQ 提取碼:pv7u
參考文章:
CTR預估模型:DeepFM/Deep&Cross/xDeepFM/AutoInt代碼實戰與講解
NELSONZHAO/zhihu/ctr_models/xDeepFM
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的【CTR模型】TensorFlow2.0 的 xDeepFM 实现与实战(附代码+数据)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于Bean
- 下一篇: 数据库设计-简化字典表[通俗易懂](常用