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

簡介

本文要介紹的是Google于2016年提出的Wide&Deep模型,此模型的提出對業(yè)界產(chǎn)生了非常大的影響,不僅其本身成功地應(yīng)用在多家一線互聯(lián)網(wǎng)公司,而且其后續(xù)的改進工作也一直延續(xù)至今。Wide&Deep模型正如其名,分別包含了Wide部分和Deep部分。其中Wide部分的作用是讓模型具有較強的“記憶能力”(memorization);而Deep部分的作用是讓模型具有“泛化能力”(generalization)。正是這樣的設(shè)計,使得模型兼具了邏輯回歸和深度神經(jīng)網(wǎng)絡(luò)的優(yōu)點——能夠快速處理并記憶大量歷史行為特征,并具有強大的表達能力。原論文在這里。

背景知識

推薦系統(tǒng)可以被認為是一個搜索排名系統(tǒng),它的輸入是一組用戶以及上下文信息,輸出是物品的排序列表。對于一次查詢,推薦任務(wù)是從數(shù)據(jù)庫中找到相關(guān)的物品,然后根據(jù)具體的任務(wù)(比如點擊率預(yù)估或者購買預(yù)測)對這些物品進行排名,最后把結(jié)果呈現(xiàn)給用戶。與一般的搜索排名問題類似,推薦系統(tǒng)中的一項挑戰(zhàn)是同時實現(xiàn)記憶(memorization)和泛化(generalization),Wide&Deep模型正是為了解決這項挑戰(zhàn)而提出的。那么我們首先來理解下這兩個概念,記憶和泛化。

Memoriization(記憶能力)

下面是原論文中的描述:

Memorization can be loosely defined as learning the frequent co-occurrence of items or features and exploiting the correlation available in the historical data.

“記憶能力”可以被理解成模型直接學(xué)習(xí)并利用歷史數(shù)據(jù)中物品或者特征的"共現(xiàn)頻率"的能力。一般來說,協(xié)同過濾、邏輯回歸等簡單模型具有較強的“記憶能力”。由于這類模型結(jié)構(gòu)簡單,原始數(shù)據(jù)往往可以直接影響推薦結(jié)果,產(chǎn)生類似于"如果曾經(jīng)點擊過A,就推薦B"這類規(guī)則式的推薦,這相當(dāng)于模型直接記住了歷史數(shù)據(jù)的分布,并根據(jù)這些特點進行推薦。

Generalization(泛化能力)

下面是原論文中的描述:

Generalization, on the other hand, is based on transitivity of correlation and explores new feature combinations that have never or rarely occurred in the past.

“泛化能力”可以被理解為模型傳遞特征的相關(guān)性,以及發(fā)掘稀疏甚至從未出現(xiàn)過的稀有特征與最終標(biāo)簽相關(guān)性的能力。矩陣分解比協(xié)同過濾的泛化能力強,因為矩陣分解引入了隱向量這樣的結(jié)構(gòu),使得數(shù)據(jù)稀少的用戶或者物品也能生成隱向量,從而獲得有數(shù)據(jù)支撐的推薦得分,這就是非常典型的將全局?jǐn)?shù)據(jù)傳遞到稀疏物品上,從而提高泛化能力的例子。

下面以人為例來總結(jié)一下記憶和泛化能力,我們?nèi)祟惪梢杂^察日常事件并且記在腦子中,比如我們觀察到了“麻雀會飛”和“鴿子會飛”等自然事件。除此之外,我們還可以根據(jù)已有記憶進行總結(jié)并制定出相應(yīng)規(guī)則(比如“有翅膀的動物會飛”),并應(yīng)用到之前未見過的事物。當(dāng)然也有例外,比如"企鵝不會飛",因此我們也需要記住一些異常情況,來進一步完善之前制定出的規(guī)則。

Wide&Deep模型如何工作

假設(shè)我們現(xiàn)在要開發(fā)一款點餐app,用戶只需要輸入它想要的某種食物(query),點餐app就可以預(yù)測出用戶最喜歡的食物(item),并且呈現(xiàn)出來。如果用戶下單了app推薦的食物,那么得分為1,否則為0。
我們首先使用Wide模型來處理這個問題,Wide模型希望能夠記住對于一次query,究竟哪一個item與之最為匹配。這個模型預(yù)測一個消費概率P(consumption|query,item),即對于一個特定的query和推薦的item,它被消費的概率有多大?舉個例子,這個模型學(xué)到了組合特征"AND(query='fried chicken', item='chicken and waffles')"成功率很高,即如果用戶搜索炸雞,app推薦炸雞和華夫餅,那么用戶消費的概率就很大。組合特征"AND(query='fried chicken', item='chicken fried rice')"卻并沒有得到用戶的青睞,盡管從名字上看,炸雞和雞肉炒飯相似度很高,但實際上這兩者完全是不同的口味。因此Wide模型是要記住用戶之前喜歡什么樣的item。下圖展示了Wide模型的“記憶過程”。

Wide模型

對于組合特征"AND(query='fried chicken', item='chicken fried rice')",Wide模型會降低此組合特征的權(quán)重,而增大組合特征"AND(query='fried chicken', item='chicken and waffles')"的權(quán)重。

過了一段時間,用戶對app的推薦內(nèi)容感到疲倦,他們希望app能夠推薦一些符合他們口味,但同時又能帶來新鮮感的食物。因此我們選擇Deep模型來解決這個問題,Deep模型會對每個query和item都生成低維的稠密embedding向量,并且在embedding空間中來查找彼此比較接近的ietm。舉個例子,你會發(fā)現(xiàn)搜索炸雞的用戶一般也不會介意再吃個漢堡。下圖展示了Deep模型示意圖,可以看到在Embedding空間中,炸雞和漢堡彼此距離比較近。
Deep模型

但是Deep模型也有它自身的問題,就是泛化過度,即給用戶推薦了不太相關(guān)的物品。通過查詢歷史數(shù)據(jù),我們發(fā)現(xiàn)實際上存在兩種不同的query-item關(guān)系。
第一種是精準(zhǔn)查詢,用戶輸入了非常精準(zhǔn)的食物描述,比如“冰脫脂牛奶拿鐵咖啡”,我們不能因為它與“熱全脂拉鐵咖啡”在Embedding空間中比較相近就推薦給用戶。
第二種是寬泛查詢,比如用戶輸入了類似"海鮮"或者“意大利食物”這樣的關(guān)鍵字,根據(jù)這種具有寬泛意義的關(guān)鍵詞可以找到非常多相關(guān)的item。

了解到了這些問題之后,一個很自然的想法就是將Wide和Deep模型結(jié)合起來使用,如下圖:
Wide&Deep模型

如上圖所示,對于兩個稀疏特征query="fried chicken" 以及 item="chicken fried rice",我們同時丟入Wide模型(左邊)和Deep模型(右邊)進行訓(xùn)練。這樣模型就兼具了記憶和泛化的能力,從而可以達到更好的推薦效果。

Wide&Deep模型

因此在這里引入本文的主角,Wide&Deep模型,如下圖:
Wide&Deep模型

上圖左邊是Wide模型,右邊是Deep模型,中間便是Wide&Deep模型了,下面分別來介紹一下:

Wide部分

Wide模型其實就是一個簡單的廣義線性模型,公式定義如下:


其中x代表原始特征,\phi(x)代表的是叉乘特征,w是權(quán)重矩陣,b是偏置。
這里需要介紹一下叉乘特征,叉乘特征是通過特定的變換函數(shù)對特征進行組合得來的,其中論文使用的是交叉積變換函數(shù),其定義如下:

其中c_{ki}是一個布爾變量,當(dāng)?shù)?img class="math-inline" src="https://math.jianshu.com/math?formula=i" alt="i" mathimg="1">個特征值屬于第k個組合特征時,c_{ki}=1,否則為0;x_{i}是第i個特征的值。例如,對于組合特征"AND(query='fried chicken', item='chicken fried rice')"來說,只有當(dāng)query中uery='fried chicken'和item='chicken fried rice'這兩個特征都為1的時候,組合特征才為1,否則為0。

Deep部分

Deep模型其實就是一個前饋神經(jīng)網(wǎng)絡(luò),網(wǎng)絡(luò)會對一些稀疏特征(如ID類特征)學(xué)習(xí)一個低維的稠密Embedding向量,維度通常在O(10)~O(100)之間,然后與一些原始稠密特征一起作為網(wǎng)絡(luò)的輸入,依次通過若干隱層進行前向傳播,每一個隱層都執(zhí)行以下計算:


其中f是激活函數(shù),這里選用的是ReLU,a^{(l)},b^{(l)},w^{(l)}分別是第l層的激活值,偏置,以及權(quán)重矩陣。

Wide&Deep聯(lián)合訓(xùn)練

論文特意強調(diào)了Wide模型和Deep模型是聯(lián)合(Joint)訓(xùn)練的,與集成(Ensemble)是不同的,集成訓(xùn)練是每個模型單獨訓(xùn)練,再將模型結(jié)果匯總。因此每個模型都會學(xué)的足夠好的時候才會進行匯總,故每個模型相對較大。而對于Wide&Deep的聯(lián)合訓(xùn)練而言,Wide部分只是為了補償Deep部分缺失的記憶能力,它只需要使用一小部分的叉乘特征,故相對較小。
Wide&Deep模型采用的Logistic Loss函數(shù),模型的預(yù)測值定義如下:


其中Y是二值化的類別標(biāo)簽,\sigma (\cdot)是Sigmoid激活函數(shù),\phi(x)是對原始特征x進行交叉積轉(zhuǎn)換后的叉乘特征,b是偏置值。w_{wide}是所有的wide模型的權(quán)重,w_{deep}是應(yīng)用在最后一層神經(jīng)元的激活值上的權(quán)重矩陣。

關(guān)于模型訓(xùn)練,論文對Wide部分使用了FTRL算法并且加上了L1正則化,對于Deep部分使用了AdaGrad算法。

實驗

作者將Wide&Deep模型運用到了Google Play商店中,當(dāng)一個用戶訪問Google Play,會生成一個包含用戶和上下文信息的query,推薦系統(tǒng)的精排模型會對于候選池中召回的一系列apps(即item)進行打分,按打分生成app的排序列表返回給用戶。

app推薦系統(tǒng)的pipeline包含3個部分,分別是數(shù)據(jù)生成、模型訓(xùn)練、模型服務(wù)。Google Play的app推薦系統(tǒng)管道示意圖如下:

論文使用的Wide&Deep模型結(jié)構(gòu)如下:
Wide&Deep模型應(yīng)用于推薦系統(tǒng)中

實驗細節(jié)如下:

  • 訓(xùn)練樣本約5000億
  • Categorical 特征(sparse)會有一個過濾閾值,即至少在訓(xùn)練集中出現(xiàn)m次才會被加入
  • Continuous 特征(dense)通過CDF被歸一化到 [0,1] 之間
  • Categorical 特征映射到32維embeddings,和原始Continuous特征共1200維作為神經(jīng)網(wǎng)絡(luò)的輸入
  • Wide部分只用了一組特征叉乘,即已安裝的應(yīng)用和曝光應(yīng)用
  • 線上模型更新時,通過“熱啟動”重訓(xùn)練,即使用上次的embeddings和模型參數(shù)初始化

Wide部分的輸入僅僅是已安裝應(yīng)用和曝光應(yīng)用兩類特征,其中已安裝應(yīng)用代表用戶的歷史行為,而曝光應(yīng)用代表當(dāng)前的待推薦應(yīng)用。選擇這兩類特征的原因是充分發(fā)揮Wide部分“記憶能力”強的優(yōu)勢。

通過3周的線上A/B實驗,實驗結(jié)果如下,其中Acquisition表示下載。
實驗結(jié)果

可以看到,經(jīng)過3周的實驗之后,Wide&Deep模型使Google Play應(yīng)用商店主頁上的app下載量提升了3.9%。

代碼實踐

網(wǎng)絡(luò)模型部分分別實現(xiàn)了Wide和Deep模型,然后拼接起來實現(xiàn)了Wide&Deep模型,模型部分代碼如下:

import torch
import torch.nn as nn

class Wide(nn.Module):
    def __init__(self, input_dim):
        super(Wide, self).__init__()
        # hand-crafted cross-product features
        self.linear = nn.Linear(in_features=input_dim, out_features=1)

    def forward(self, x):
        return self.linear(x)

class Deep(nn.Module):
    def __init__(self, config, hidden_layers):
        super(Deep, self).__init__()
        self.dnn = nn.ModuleList([nn.Linear(layer[0], layer[1]) for layer in list(zip(hidden_layers[:-1], hidden_layers[1:]))])
        self.dropout = nn.Dropout(p=config['deep_dropout'])

    def forward(self, x):

        for layer in self.dnn:
            x = layer(x)
            # 如果輸出層大小是1的話,這里再使用了個ReLU激活函數(shù),可能導(dǎo)致輸出全變成0,即造成了梯度消失,導(dǎo)致Loss不收斂
            x = torch.relu(x)
        x = self.dropout(x)
        return x

class WideDeep(nn.Module):
    def __init__(self, config, dense_features_cols, sparse_features_cols):
        super(WideDeep, self).__init__()
        self._config = config
        # 稠密特征的數(shù)量
        self._num_of_dense_feature = dense_features_cols.__len__()
        # 稠密特征
        self.sparse_features_cols = sparse_features_cols

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

        # Deep hidden layers
        self._deep_hidden_layers = config['hidden_layers']
        self._deep_hidden_layers.insert(0, self._num_of_dense_feature + config['embed_dim'] * len(self.sparse_features_cols))

        self._wide = Wide(self._num_of_dense_feature)
        self._deep = Deep(config, self._deep_hidden_layers)
        # 之前直接將這個final_layer加入到了Deep模塊里面,想著反正輸出都是1,結(jié)果沒注意到Deep沒經(jīng)過一個Linear層都會經(jīng)過Relu激活函數(shù),如果
        # 最后輸出層大小是1的話,再經(jīng)過ReLU之后,很可能變?yōu)榱?,造成梯度消失問題,導(dǎo)致Loss怎么樣都降不下來。
        self._final_linear = nn.Linear(self._deep_hidden_layers[-1], 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[i](sparse_inputs[:, i]) for i in range(sparse_inputs.shape[1])]
        sparse_embeds = torch.cat(sparse_embeds, axis=-1)
        # Deep模塊的輸入是稠密特征和稀疏特征經(jīng)過Embedding產(chǎn)生的稠密特征的
        deep_input = torch.cat([sparse_embeds, dense_input], axis=-1)

        wide_out = self._wide(dense_input)
        deep_out = self._deep(deep_input)
        deep_out = self._final_linear(deep_out)

        assert (wide_out.shape == deep_out.shape)

        outputs = torch.sigmoid(0.5 * (wide_out + deep_out))
        return outputs

    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)

數(shù)據(jù)集方面采用的是criteo數(shù)據(jù)集的一個很小的子集,僅僅是為了測試模型功能。測試數(shù)據(jù)并沒有標(biāo)簽,因此模型訓(xùn)練好了之后,直接對測試集進行點擊率預(yù)估,輸出的結(jié)果是由0,1組成的向量,代表點擊與否,部分結(jié)果如下:

后記

此網(wǎng)絡(luò)模型代碼也是借鑒網(wǎng)上的,具體鏈接記不得了。自己在調(diào)試的時候,遇到一個很棘手的問題,就是模型代碼設(shè)計好了之后,在訓(xùn)練時,無論怎樣調(diào)整學(xué)習(xí)率等超參數(shù),損失就是降不下來。自己也搜了很多相關(guān)的資料,有很多說的是數(shù)據(jù)集本身有問題,學(xué)習(xí)率過小,損失函數(shù)不對等等。結(jié)果折騰了半天依然沒找到原因。最后通過一步一步對比自己的代碼,發(fā)現(xiàn)WideDeep模型最后是會將Wide和Deep模型各自的兩個輸出Tensor相加的,這兩個Tensor大小都是1。而自己當(dāng)時覺得為了方便,將輸出為1的Linear層直接放到了Deep層里面,其實這樣做并沒有什么問題,但是由于Deep模型進行前向計算時,每次都會經(jīng)過ReLU激活函數(shù),當(dāng)Deep模型最后的輸出大小為1的時候,再經(jīng)過ReLU,很可能導(dǎo)致輸出直接變?yōu)?,即造成了梯度消失的問題。因此這樣無論怎么訓(xùn)練,梯度都無法傳遞到網(wǎng)絡(luò)的淺層部分,導(dǎo)致模型參數(shù)無法更新,Loss無法降低。通過這次代碼調(diào)試,自己也算是學(xué)到一些技巧了,以后在編寫代碼的時候要盡可能注意不要犯這樣的錯誤。
完整代碼見https://github.com/HeartbreakSurvivor/RsAlgorithms/blob/main/Test/widedeep_test.py.

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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