wide&deep

1、動機
在CTR預估任務中利用手工構(gòu)造的交叉組合特征來使線性模型具有“記憶性”,使模型記住共線頻率較高的特征組合,往往也能達到一個不錯的baseline,且可解釋性強。但是這種方式有著較為明顯的缺點:
1.特征工程需要耗費很大的精力
2.模型是強行記住這些特征組合,對于未曾出現(xiàn)過的特征組合,權(quán)重系數(shù)為0,無法進行泛化

為了加強模型的泛化能力,研究者引入DNN結(jié)構(gòu),將高維稀疏特征編碼為低維稠密的Embedding vector,這種基于Embedding的方式能夠有效提高泛化能力。但是,基于Embedding的方式可能因為數(shù)據(jù)長尾分布,導致長尾的一些特征無法被充分學習,其對應的Embedding vector是不準確的,這便會造成模型泛化過度。

wide&deep模型就是圍繞記憶性和泛化性進行討論的,模型能夠從歷史數(shù)據(jù)中學習高頻共振的特征組合能力,稱為模型的Memorization。能夠利用特征之間的傳遞性去探索歷史數(shù)據(jù)中未出現(xiàn)過的特征組合,稱為模型的Generalization。wide&deep兼顧Memorization與Generalization并在Google Play store的場景中成功落地。

2、模型結(jié)構(gòu)及原理


1

如何理解Wide部分有利于增強模型的“記憶能力”,Deep部分有利于增強模型的“泛化能力”

  • wide部分是一個廣義的線性模型,輸入的特征主要有兩部分組成,一部分是原始的部分特征,另一部分是原始特征的交叉特征(cross-product transformation),對于交互特征可以定義為: \phi_{k}(x)=\prod_{i=1}^d x_i^{c_{ki}}, c_{ki}\in {0,1} c_{ki}是一個布爾變量,當?shù)趇個特征屬于第k個特征組合時,c_{ki}的值為1,否則為0,x_i是第i個特征的值,大體意思就是兩個特征都同時為1這個新的特征才能為1,否則就是0,說白了就是一個特征組合。用原論文的例子舉例:

AND(user_installed_app=QQ, impression_app=WeChat),當特征user_installed_app=QQ,和特征impression_app=WeChat取值都為1的時候,組合特征AND(user_installed_app=QQ, impression_app=WeChat)的取值才為1,否則為0。

對于wide部分訓練時候使用的優(yōu)化器是帶L_1正則的FTRL算法(Follow-the-regularized-leader),而L1 FTLR是非常注重模型稀疏性質(zhì)的,也就是說W&D模型采用L1 FTRL是想讓Wide部分變得更加的稀疏,即Wide部分的大部分參數(shù)都為0,這就大大壓縮了模型權(quán)重及特征向量的維度。Wide部分模型訓練完之后留下來的特征都是非常重要的,那么模型的“記憶能力”就可以理解為發(fā)現(xiàn)"直接的",“暴力的”,“顯然的”關(guān)聯(lián)規(guī)則的能力。例如Google W&D期望wide部分發(fā)現(xiàn)這樣的規(guī)則:用戶安裝了應用A,此時曝光應用B,用戶安裝應用B的概率大。

Wide部分與Deep部分的結(jié)合

W&D模型是將兩部分輸出的結(jié)果結(jié)合起來聯(lián)合訓練,將deep和wide部分的輸出重新使用一個邏輯回歸模型做最終的預測,輸出概率值。聯(lián)合訓練的數(shù)學形式如下:需要注意的是,因為Wide側(cè)的數(shù)據(jù)是高維稀疏的,所以作者使用了FTRL算法優(yōu)化,而Deep側(cè)使用的是 Adagrad。 P(Y=1|x)=\delta(w_{wide}^T[x,\phi(x)] + w_{deep}^T a^{(lf)} + b)

3.代碼實現(xiàn)
Wide側(cè)記住的是歷史數(shù)據(jù)中那些常見、高頻的模式,是推薦系統(tǒng)中的“紅?!薄嶋H上,Wide側(cè)沒有發(fā)現(xiàn)新的模式,只是學習到這些模式之間的權(quán)重,做一些模式的篩選。正因為Wide側(cè)不能發(fā)現(xiàn)新模式,因此我們需要根據(jù)人工經(jīng)驗、業(yè)務背景,將我們認為有價值的、顯而易見的特征及特征組合,喂入Wide側(cè)

Deep側(cè)就是DNN,通過embedding的方式將categorical/id特征映射成稠密向量,讓DNN學習到這些特征之間的深層交叉,以增強擴展能力。

模型的實現(xiàn)與模型結(jié)構(gòu)類似由deep和wide兩部分組成,這兩部分結(jié)構(gòu)所需要的特征在上面已經(jīng)說過了,針對當前數(shù)據(jù)集實現(xiàn),我們在wide部分加入了所有可能的一階特征,包括數(shù)值特征和類別特征的onehot都加進去了,其實也可以加入一些與wide&deep原論文中類似交叉特征。只要能夠發(fā)現(xiàn)高頻、常見模式的特征都可以放在wide側(cè),對于Deep部分,在本數(shù)據(jù)中放入了數(shù)值特征和類別特征的embedding特征,實際應用也需要根據(jù)需求進行選擇。

# Wide&Deep 模型的wide部分及Deep部分的特征選擇,應該根據(jù)實際的業(yè)務場景去確定哪些特征應該放在Wide部分,哪些特征應該放在Deep部分
def WideNDeep(linear_feature_columns, dnn_feature_columns):
    # 構(gòu)建輸入層,即所有特征對應的Input()層,這里使用字典的形式返回,方便后續(xù)構(gòu)建模型
    dense_input_dict, sparse_input_dict = build_input_layers(linear_feature_columns + dnn_feature_columns)

    # 將linear部分的特征中sparse特征篩選出來,后面用來做1維的embedding
    linear_sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), linear_feature_columns))

    # 構(gòu)建模型的輸入層,模型的輸入層不能是字典的形式,應該將字典的形式轉(zhuǎn)換成列表的形式
    # 注意:這里實際的輸入與Input()層的對應,是通過模型輸入時候的字典數(shù)據(jù)的key與對應name的Input層
    input_layers = list(dense_input_dict.values()) + list(sparse_input_dict.values())

    # Wide&Deep模型論文中Wide部分使用的特征比較簡單,并且得到的特征非常的稀疏,所以使用了FTRL優(yōu)化Wide部分(這里沒有實現(xiàn)FTRL)
    # 但是是根據(jù)他們業(yè)務進行選擇的,我們這里將所有可能用到的特征都輸入到Wide部分,具體的細節(jié)可以根據(jù)需求進行修改
    linear_logits = get_linear_logits(dense_input_dict, sparse_input_dict, linear_sparse_feature_columns)
    
    # 構(gòu)建維度為k的embedding層,這里使用字典的形式返回,方便后面搭建模型
    embedding_layers = build_embedding_layers(dnn_feature_columns, sparse_input_dict, is_linear=False)

    dnn_sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), dnn_feature_columns))

    # 在Wide&Deep模型中,deep部分的輸入是將dense特征和embedding特征拼在一起輸入到dnn中
    dnn_logits = get_dnn_logits(dense_input_dict, sparse_input_dict, dnn_sparse_feature_columns, embedding_layers)
    
    # 將linear,dnn的logits相加作為最終的logits
    output_logits = Add()([linear_logits, dnn_logits])

    # 這里的激活函數(shù)使用sigmoid
    output_layer = Activation("sigmoid")(output_logits)

    model = Model(input_layers, output_layer)
    return model
2
3

詳細代碼

import warnings 
warnings.filterwarnings("ignore")
import itertools
import pandas as pd
import numpy as np
from tqdm import tqdm
from collections import nametuple

import tensorflow as tf
from tensorflow.keras.layers import *
from tensorflow.keras.model import *

from sklearn.model_selection import train_test_split
from skleran.preprocessing import MinMaxScaler,LabelEncoder

from utils import SparseFeat , DenseFeat , VarLenSparseFeat

#簡單處理特征,包括缺失值,數(shù)值處理,類別編碼
def data_process(data_df , dense_features , sparse_features):
    data_df[dense_features] = data_df[dense_features].fillna(0.0)
    for f in dense_features:
        lbe = LabelEncoder()
        data_df[f] = lbe.fit_transform(data_df[f])

    return data_df[dense_features + sparse_features]

def build_input_layers(feature_columns):
    #構(gòu)建Input層字典,并以dense和sparse兩類字典的形式返回
    dense_input_dict, sparse_input_dict = {} , {}

    for fc in feature_columns:
        if isinstance(fc, SparseFeat):
            sparse_input_dict[fc.name] = Input(shape=(1,),name = fc.name)
        elif isinstance(fc,DenseFeat):
            dense_input_dict[fc.name] = Input(shape = (fc.dimension , ) , name = fc.name)

    return dense_input_dict , sparse_input_dict


def build_embedding_layers(feature_columns , input_layers_dict , is_linear):

    #定義一個embedding層對應的字典
    embedding_layers_dict = dict()
    
    #將特征中的sparse特征篩選出來
    sparse_feature_columns = list(filter(lambda x: isinstance(x , SparseFeat) , feature_columns)) if feature_columns else []

    #如果是用于線性部分的embedding層,其維度為1,否則維度就是自己定義的embedding維度
    if is_linear:
        for fc in sparse_feature_columns:
            embedding_layers_dict[fc.name] = Embedding(fc.vocabulary_size , 1, name = 'ld_emb_' + fc.name)

    else:
        for fc in sparse_feature_columns:
            embedding_layers_dict[fc.name] = Embedding(fc.vocabulary_size , fc.embedding_dim , name = 'kd_emb_' + fc.name)

    return embedding_layers_dict

def get_linear_logits(dense_input_dict, sparse_input_dict, sparse_feature_columns):
    # 將所有的dense特征的Input層,然后經(jīng)過一個全連接層得到dense特征的logits
    concat_dense_inputs = Concatenate(axis=1)(list(dense_input_dict.values()))
    dense_logits_output = Dense(1)(concat_dense_inputs)
    
    # 獲取linear部分sparse特征的embedding層,這里使用embedding的原因是:
    # 對于linear部分直接將特征進行onehot然后通過一個全連接層,當維度特別大的時候,計算比較慢
    # 使用embedding層的好處就是可以通過查表的方式獲取到哪些非零的元素對應的權(quán)重,然后在將這些權(quán)重相加,效率比較高
    linear_embedding_layers = build_embedding_layers(sparse_feature_columns, sparse_input_dict, is_linear=True)
    
    # 將一維的embedding拼接,注意這里需要使用一個Flatten層,使維度對應
    sparse_1d_embed = []
    for fc in sparse_feature_columns:
        feat_input = sparse_input_dict[fc.name]
        embed = Flatten()(linear_embedding_layers[fc.name](feat_input)) # B x 1
        sparse_1d_embed.append(embed)

    # embedding中查詢得到的權(quán)重就是對應onehot向量中一個位置的權(quán)重,所以后面不用再接一個全連接了,本身一維的embedding就相當于全連接
    # 只不過是這里的輸入特征只有0和1,所以直接向非零元素對應的權(quán)重相加就等同于進行了全連接操作(非零元素部分乘的是1)
    sparse_logits_output = Add()(sparse_1d_embed)

    # 最終將dense特征和sparse特征對應的logits相加,得到最終linear的logits
    linear_logits = Add()([dense_logits_output, sparse_logits_output])
    return linear_logits


# 將所有的sparse特征embedding拼接
def concat_embedding_list(feature_columns, input_layer_dict, embedding_layer_dict, flatten=False):
    # 將sparse特征篩選出來
    sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), feature_columns))

    embedding_list = []
    for fc in sparse_feature_columns:
        _input = input_layer_dict[fc.name] # 獲取輸入層 
        _embed = embedding_layer_dict[fc.name] # B x 1 x dim  獲取對應的embedding層
        embed = _embed(_input) # B x dim  將input層輸入到embedding層中

        # 是否需要flatten, 如果embedding列表最終是直接輸入到Dense層中,需要進行Flatten,否則不需要
        if flatten:
            embed = Flatten()(embed)
        
        embedding_list.append(embed)
    
    return embedding_list 


def get_dnn_logits(dense_input_dict, sparse_input_dict, sparse_feature_columns, dnn_embedding_layers):
    concat_dense_inputs = Concatenate(axis=1)(list(dense_input_dict.values())) # B x n1 (n表示的是dense特征的維度) 

    sparse_kd_embed = concat_embedding_list(sparse_feature_columns, sparse_input_dict, dnn_embedding_layers, flatten=True)

    concat_sparse_kd_embed = Concatenate(axis=1)(sparse_kd_embed) # B x n2k  (n2表示的是Sparse特征的維度)

    dnn_input = Concatenate(axis=1)([concat_dense_inputs, concat_sparse_kd_embed]) # B x (n2k + n1)

    # dnn層,這里的Dropout參數(shù),Dense中的參數(shù)及Dense的層數(shù)都可以自己設定
    dnn_out = Dropout(0.5)(Dense(1024, activation='relu')(dnn_input))  
    dnn_out = Dropout(0.3)(Dense(512, activation='relu')(dnn_out))
    dnn_out = Dropout(0.1)(Dense(256, activation='relu')(dnn_out))

    dnn_logits = Dense(1)(dnn_out)

    return dnn_logits

# Wide&Deep 模型的wide部分及Deep部分的特征選擇,應該根據(jù)實際的業(yè)務場景去確定哪些特征應該放在Wide部分,哪些特征應該放在Deep部分
def WideNDeep(linear_feature_columns, dnn_feature_columns):
    # 構(gòu)建輸入層,即所有特征對應的Input()層,這里使用字典的形式返回,方便后續(xù)構(gòu)建模型
    dense_input_dict, sparse_input_dict = build_input_layers(linear_feature_columns + dnn_feature_columns)

    # 將linear部分的特征中sparse特征篩選出來,后面用來做1維的embedding
    linear_sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), linear_feature_columns))

    # 構(gòu)建模型的輸入層,模型的輸入層不能是字典的形式,應該將字典的形式轉(zhuǎn)換成列表的形式
    # 注意:這里實際的輸入與Input()層的對應,是通過模型輸入時候的字典數(shù)據(jù)的key與對應name的Input層
    input_layers = list(dense_input_dict.values()) + list(sparse_input_dict.values())

    # Wide&Deep模型論文中Wide部分使用的特征比較簡單,并且得到的特征非常的稀疏,所以使用了FTRL優(yōu)化Wide部分(這里沒有實現(xiàn)FTRL)
    # 但是是根據(jù)他們業(yè)務進行選擇的,我們這里將所有可能用到的特征都輸入到Wide部分,具體的細節(jié)可以根據(jù)需求進行修改
    linear_logits = get_linear_logits(dense_input_dict, sparse_input_dict, linear_sparse_feature_columns)
    
    # 構(gòu)建維度為k的embedding層,這里使用字典的形式返回,方便后面搭建模型
    embedding_layers = build_embedding_layers(dnn_feature_columns, sparse_input_dict, is_linear=False)

    dnn_sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), dnn_feature_columns))

    # 在Wide&Deep模型中,deep部分的輸入是將dense特征和embedding特征拼在一起輸入到dnn中
    dnn_logits = get_dnn_logits(dense_input_dict, sparse_input_dict, dnn_sparse_feature_columns, embedding_layers)
    
    # 將linear,dnn的logits相加作為最終的logits
    output_logits = Add()([linear_logits, dnn_logits])

    # 這里的激活函數(shù)使用sigmoid
    output_layer = Activation("sigmoid")(output_logits)

    model = Model(input_layers, output_layer)
    return model


if __name__ == "__main__":
    # 讀取數(shù)據(jù)
    data = pd.read_csv('./data/criteo_sample.txt')

    # 劃分dense和sparse特征
    columns = data.columns.values
    dense_features = [feat for feat in columns if 'I' in feat]
    sparse_features = [feat for feat in columns if 'C' in feat]

    # 簡單的數(shù)據(jù)預處理
    train_data = data_process(data, dense_features, sparse_features)
    train_data['label'] = data['label']

    # 將特征分組,分成linear部分和dnn部分(根據(jù)實際場景進行選擇),并將分組之后的特征做標記(使用DenseFeat, SparseFeat)
    linear_feature_columns = [SparseFeat(feat, vocabulary_size=data[feat].nunique(),embedding_dim=4)
                            for i,feat in enumerate(sparse_features)] + [DenseFeat(feat, 1,)
                            for feat in dense_features]

    dnn_feature_columns = [SparseFeat(feat, vocabulary_size=data[feat].nunique(),embedding_dim=4)
                            for i,feat in enumerate(sparse_features)] + [DenseFeat(feat, 1,)
                            for feat in dense_features]

    # 構(gòu)建WideNDeep模型
    history = WideNDeep(linear_feature_columns, dnn_feature_columns)
    history.summary()
    history.compile(optimizer="adam", 
                loss="binary_crossentropy", 
                metrics=["binary_crossentropy", tf.keras.metrics.AUC(name='auc')])

    # 將輸入數(shù)據(jù)轉(zhuǎn)化成字典的形式輸入
    train_model_input = {name: data[name] for name in dense_features + sparse_features}
    # 模型訓練
    history.fit(train_model_input, train_data['label'].values,
            batch_size=64, epochs=5, validation_split=0.2, )  
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容