推薦系統(tǒng)之Deep Crossing模型原理以及代碼實踐

簡介

本文要介紹的Deep Crossing模型是由微軟研究院在論文《Deep Crossing: Web-Scale Modeling without Manually Crafted Combinatorial Features》中提出的浊洞,它主要是用來解決大規(guī)模特征自動組合問題,從而減輕或者避免手工進行特征組合的開銷。Deep Crossing可以說是深度學(xué)習(xí)CTR模型的最典型和基礎(chǔ)性的模型。

背景知識

傳統(tǒng)機器學(xué)習(xí)算法充分利用所有的輸入特征來對新實例進行預(yù)測和分類。但是琅关,僅僅使用原始特征很難獲得最佳結(jié)果。因此無論是在工業(yè)界還是學(xué)術(shù)界,都進行著大量的工作來對原始特征進行轉(zhuǎn)換孕讳。一種有效的特征轉(zhuǎn)換方式是進行多種特征的組合,然后將融合后的特征輸入到學(xué)習(xí)器中去巍膘。
組合特征在很多領(lǐng)域已經(jīng)被證實能發(fā)揮強大的功能厂财。在Kaggle社區(qū)里,頂級的數(shù)據(jù)科學(xué)家往往都十分擅長特征融合峡懈,甚至能橫跨3~5個維度進行特征組合璃饱,直覺和創(chuàng)造有效組合特征的能力是他們贏得比賽的制勝法寶。同理肪康,在圖像識別等領(lǐng)域荚恶,類似SIFT的特征提取也是某些算法能夠在ImageNet等數(shù)據(jù)集上取得最佳結(jié)果的關(guān)鍵因素。然而磷支,進行高效的特征融合卻需要高昂的成本代價谒撼。隨著特征數(shù)量的增加,管理雾狈,維護變得充滿挑戰(zhàn)廓潜,尤其是在大規(guī)模的網(wǎng)絡(luò)應(yīng)用程序中。龐大的搜索空間和樣本數(shù)量,導(dǎo)致訓(xùn)練和評估變得異常緩慢茉帅,因此尋找額外的組合特征來改進現(xiàn)有模型是一項艱巨的任務(wù)叨叙。
深度學(xué)習(xí)模型天然就可以從獨立特征中進行學(xué)習(xí),并且無需人工干預(yù)堪澎。在計算機視覺以及自然語言處理等領(lǐng)域已經(jīng)發(fā)揮出了它強大的功能擂错,比如基于CNN的模型在圖像識別比賽中取得的成績就已經(jīng)超過了基于傳統(tǒng)手工特征SIFT的相關(guān)方法取得的最好成績。
Deep Crossing模型將深度學(xué)習(xí)從圖像和自然語言處理等領(lǐng)域擴展到了更加廣泛的環(huán)境中樱蛤,比如每個輸入特征都具有不同的性質(zhì)钮呀。更具體地說,它可以輸入諸如文本昨凡、類別爽醋、ID以及數(shù)值信息等特征,并且根據(jù)特定任務(wù)要求便脊,自動搜索最佳的特征組合蚂四。

模型介紹

首先給出Deep Crossing的整體模型架構(gòu)圖,如下:
Deep Crossing模型架構(gòu)圖

模型的輸入是一系列的獨立特征哪痰,模型總共包含4層遂赠,分別是Embedding層、Stacking層晌杰、Residual Unit層跷睦、Scoring層,模型的輸出是用戶點擊率預(yù)測值肋演。
注意上圖中紅色方框部分抑诸,輸入特征沒有經(jīng)過Embedding層就直接連接到了Stacking層了。這是因為輸入特征可能是稠密的也可能是稀疏的爹殊,論文中指出蜕乡,對于維度小于256的特征直接連接到Stacking層。

損失函數(shù)

論文中使用的是交叉熵損失函數(shù)边灭,但是也可以使用Softmax或者其他損失函數(shù)异希。定義如下:

損失函數(shù)

其中i代表的是訓(xùn)練樣本的下標(biāo),N是訓(xùn)練樣本的總數(shù)绒瘦,y_i是每個樣本的標(biāo)簽,在用戶點擊率預(yù)估問題中就是用戶點擊率扣癣,p_i是模型的預(yù)估值惰帽,在Deep Crossing中是Sigmoid函數(shù)的輸出。

Embedding層

Embedding層的主要作用是對輸入特征進行特征轉(zhuǎn)換父虑,Embedding層包含了一個單層神經(jīng)網(wǎng)絡(luò)该酗,網(wǎng)絡(luò)定義如下:


其中j代表輸入特征的索引,X_j^I \in \mathbb R^{n_j}代表輸入特征,W_j是一個m_j \times n_j的矩陣呜魄,b \in \mathbb R^{n_j}X_j^O是輸出的Embedding特征爵嗅。當(dāng)m_j < n_j時娇澎,Embedding層的作用就是對輸入特征進行降維。max操作在神經(jīng)網(wǎng)絡(luò)中代表的就是ReLU激活函數(shù)睹晒。故Embedding層的主要作用是趟庄,首先對輸入特征進行一個線性變化,其次通過ReLU激活函數(shù)得到最后的Embedding特征伪很。
值得注意的是戚啥,Embedding層的大小會對整個模型的整體大小產(chǎn)生很重要的影響。即便輸入是稀疏特征锉试,大小為m_j \times n_j的權(quán)重矩陣也是稠密的猫十。

Stacking層

當(dāng)?shù)玫搅怂休斎胩卣鞯腅mbedding表示之后(特征維度小于256的除外)。Stacking層所做的事情只是簡單把這些特征聚合起來呆盖,形成一個向量炫彩。表示如下:


其中K是輸入特征的數(shù)量。注意到這里W_jb_j都屬于模型的參數(shù)絮短,都是需要經(jīng)過優(yōu)化的江兢,這一點也是模型中Embedding的重要特點之一。

Residual層

殘差層的是由下圖所示的殘差單元構(gòu)建成的丁频。殘差單元如下所示:

殘差單元

Deep Crossing模型中使用的殘差單元與ResNet中使用的不太一樣杉允,它不包含卷積操作。殘差單元的特有屬性就是將輸入加到了隱層的輸出上席里,上圖所示的殘差單元的計算可由下式來表示:

其中W_{\{0,1\}}b_{\{0,1\}}分別代表兩個全連接層的參數(shù)叔磷,F代表的是兩個全連接層的映射函數(shù),若將X^I移動到上式的左邊之后奖磁,可得F(\cdot) = X^O - X^I改基,即F函數(shù)學(xué)習(xí)到的是目標(biāo)輸出和輸入之間的殘差。

關(guān)于ResNet更詳細的介紹可以參考我的另外一篇博客PyTorch實現(xiàn)經(jīng)典網(wǎng)絡(luò)之ResNet咖为。

Scoring層

Residual層的輸出首先連接到全連接層秕狰,其次再經(jīng)過Sigmoid激活函數(shù),最后輸出的是一個廣告的預(yù)測點擊率躁染。

代碼實踐

論文中給出了基于CNTK的偽代碼實現(xiàn)鸣哀,如下:

模型訓(xùn)練和測試使用的是內(nèi)部的一個數(shù)據(jù)集。
我使用pytorch對代碼進行了改寫吞彤,并且基于criteo數(shù)據(jù)集進行訓(xùn)練和測試我衬。模型部分代碼如下:

import torch
import torch.nn as nn

class ResidualBlock(nn.Module):
    def __init__(self, input_dim, hidden_dim):
        super(ResidualBlock, self).__init__()
        self.linear1 = nn.Linear(in_features=input_dim, out_features=hidden_dim, bias=True)
        self.linear2 = nn.Linear(in_features=hidden_dim, out_features=input_dim, bias=True)

    def forward(self, x):
        out = self.linear2(torch.relu(self.linear1(x)))
        out += x
        out = torch.relu(out)
        return out

class DeepCrossing(nn.Module):
    def __init__(self, config, dense_features_cols, sparse_features_cols):
        super(DeepCrossing, self).__init__()
        self._config = config
        # 稠密特征的數(shù)量
        self._num_of_dense_feature = dense_features_cols.__len__()
        # 稠密特征
        self.sparse_features_cols = sparse_features_cols
        self.sparse_indexes = [idx for idx, num_feat in enumerate(self.sparse_features_cols) if num_feat > config['min_dim']]
        self.dense_indexes = [idx for idx in range(len(self.sparse_features_cols)) if idx not in self.sparse_indexes]

        # 對特征類別大于config['min_dim']的創(chuàng)建Embedding層叹放,其余的直接加入Stack層
        self.embedding_layers = nn.ModuleList([
            # 根據(jù)稀疏特征的個數(shù)創(chuàng)建對應(yīng)個數(shù)的Embedding層,Embedding輸入大小是稀疏特征的類別總數(shù)挠羔,輸出稠密向量的維度由config文件配置
            nn.Embedding(num_embeddings = self.sparse_features_cols[idx], embedding_dim=config['embed_dim'])
                for idx  in self.sparse_indexes
        ])

        self.dim_stack = self.sparse_indexes.__len__()*config['embed_dim'] + self.dense_indexes.__len__() + self._num_of_dense_feature

        self.residual_layers = nn.ModuleList([
            # 根據(jù)稀疏特征的個數(shù)創(chuàng)建對應(yīng)個數(shù)的Embedding層井仰,Embedding輸入大小是稀疏特征的類別總數(shù),輸出稠密向量的維度由config文件配置
            ResidualBlock(self.dim_stack, layer)
            for layer in config['hidden_layers']
        ])

        self._final_linear = nn.Linear(self.dim_stack, 1)

    def forward(self, x):
        # 先區(qū)分出稀疏特征和稠密特征破加,這里是按照列來劃分的俱恶,即所有的行都要進行篩選
        dense_input, sparse_inputs = x[:, :self._num_of_dense_feature], x[:, self._num_of_dense_feature:]
        sparse_inputs = sparse_inputs.long()

        sparse_embeds = [self.embedding_layers[idx](sparse_inputs[:, i]) for idx, i in enumerate(self.sparse_indexes)]
        sparse_embeds = torch.cat(sparse_embeds, axis=-1)

        # 取出sparse中維度小于config['min_dim']的Tensor
        indices = torch.LongTensor(self.dense_indexes)
        sparse_dense = torch.index_select(sparse_inputs, 1, indices)

        output = torch.cat([sparse_embeds, dense_input, sparse_dense], axis=-1)

        for residual in self.residual_layers:
            output = residual(output)

        output = self._final_linear(output)
        output = torch.sigmoid(output)
        return output

    def saveModel(self):
        torch.save(self.state_dict(), self._config['model_name'])

    def loadModel(self, map_location):
        state_dict = torch.load(self._config['model_name'], map_location=map_location)
        self.load_state_dict(state_dict, strict=False)

測試部分代碼:

import torch
from DeepCrossing.trainer import Trainer
from DeepCrossing.network import DeepCrossing
from Utils.criteo_loader import getTestData, getTrainData
import torch.utils.data as Data

deepcrossing_config = \
{
    'embed_dim': 8, # 用于控制稀疏特征經(jīng)過Embedding層后的稠密特征大小
    'min_dim': 256, # 稀疏特征維度小于min_dim的直接進入stack layer,不用經(jīng)過embedding層
    'hidden_layers': [512,256,128,64,32],
    'num_epoch': 30,
    'batch_size': 32,
    'lr': 1e-3,
    'l2_regularization': 1e-4,
    'device_id': 0,
    'use_cuda': False,
    'train_file': '../Data/criteo/processed_data/train_set.csv',
    'fea_file': '../Data/criteo/processed_data/fea_col.npy',
    'validate_file': '../Data/criteo/processed_data/val_set.csv',
    'test_file': '../Data/criteo/processed_data/test_set.csv',
    'model_name': '../TrainedModels/DeepCrossing.model'
}

if __name__ == "__main__":
    ####################################################################################
    # DeepCrossing 模型
    ####################################################################################
    training_data, training_label, dense_features_col, sparse_features_col = getTrainData(deepcrossing_config['train_file'], deepcrossing_config['fea_file'])
    train_dataset = Data.TensorDataset(torch.tensor(training_data).float(), torch.tensor(training_label).float())
    test_data = getTestData(deepcrossing_config['test_file'])
    test_dataset = Data.TensorDataset(torch.tensor(test_data).float())

    deepCrossing = DeepCrossing(deepcrossing_config, dense_features_cols=dense_features_col, sparse_features_cols=sparse_features_col)

    ####################################################################################
    # 模型訓(xùn)練階段
    ####################################################################################
    # # 實例化模型訓(xùn)練器
    trainer = Trainer(model=deepCrossing, config=deepcrossing_config)
    # 訓(xùn)練
    trainer.train(train_dataset)
    # 保存模型
    trainer.save()

    ####################################################################################
    # 模型測試階段
    ####################################################################################
    deepCrossing.eval()
    if deepcrossing_config['use_cuda']:
        deepCrossing.loadModel(map_location=lambda storage, loc: storage.cuda(deepcrossing_config['device_id']))
        deepCrossing = deepCrossing.cuda()
    else:
        deepCrossing.loadModel(map_location=torch.device('cpu'))

    y_pred_probs = deepCrossing(torch.tensor(test_data).float())
    y_pred = torch.where(y_pred_probs>0.5, torch.ones_like(y_pred_probs), torch.zeros_like(y_pred_probs))
    print("Test Data CTR Predict...\n ", y_pred.view(-1))


點擊率預(yù)估部分測試結(jié)果:
測試集預(yù)估結(jié)果

完整代碼見https://github.com/HeartbreakSurvivor/RsAlgorithms/blob/main/Test/deepcrossing_test.py

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市三痰,隨后出現(xiàn)的幾起案子赖条,更是在濱河造成了極大的恐慌彼城,老刑警劉巖代嗤,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件渠鸽,死亡現(xiàn)場離奇詭異叫乌,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進店門厕九,熙熙樓的掌柜王于貴愁眉苦臉地迎上來灾螃,“玉大人,你說我怎么就攤上這事排宰。” “怎么了红省?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵额各,是天一觀的道長。 經(jīng)常有香客問我吧恃,道長虾啦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任痕寓,我火速辦了婚禮傲醉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘呻率。我一直安慰自己硬毕,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布礼仗。 她就那樣靜靜地躺著吐咳,像睡著了一般逻悠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上韭脊,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天童谒,我揣著相機與錄音,去河邊找鬼沪羔。 笑死饥伊,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蔫饰。 我是一名探鬼主播琅豆,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼篓吁!你這毒婦竟也來了茫因?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤越除,失蹤者是張志新(化名)和其女友劉穎节腐,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體摘盆,經(jīng)...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡翼雀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了孩擂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片狼渊。...
    茶點故事閱讀 39,773評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖类垦,靈堂內(nèi)的尸體忽然破棺而出狈邑,到底是詐尸還是另有隱情,我是刑警寧澤蚤认,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布米苹,位于F島的核電站,受9級特大地震影響砰琢,放射性物質(zhì)發(fā)生泄漏蘸嘶。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一陪汽、第九天 我趴在偏房一處隱蔽的房頂上張望训唱。 院中可真熱鬧,春花似錦挚冤、人聲如沸况增。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽澳骤。三九已至歧强,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間宴凉,已是汗流浹背誊锭。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工表悬, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留弥锄,地道東北人。 一個月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓蟆沫,卻偏偏與公主長得像籽暇,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子饭庞,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,689評論 2 354

推薦閱讀更多精彩內(nèi)容