簡(jiǎn)介
本文要介紹的是由斯坦福大學(xué)聯(lián)合Google的研究人員發(fā)表的論文《Deep & Cross Network for Ad Click Predictions》中提出的Deep&Cross模型,簡(jiǎn)稱DCN遣铝。DCN模型是Wide&Deep的改進(jìn)版本佑刷,其中Deep部分的設(shè)計(jì)思路與Wide&Deep沒有發(fā)生本質(zhì)的變化,DCN主要是重新設(shè)計(jì)了Cross部分以增加特征之間的交互力度酿炸,使用了多層特征交叉層對(duì)輸入向量進(jìn)行特征交叉瘫絮,以獲得更加強(qiáng)壯的特征表達(dá)能力。
關(guān)于Wide&Deep模型介紹可以參考推薦系統(tǒng)之Wide&Deep模型原理以及代碼實(shí)踐填硕。
摘要
點(diǎn)擊率(CTR)預(yù)估任務(wù)是一個(gè)大規(guī)模的問題麦萤,尤其是對(duì)于價(jià)值數(shù)百億美元的在線廣告業(yè)務(wù)。在廣告界扁眯,廣告商向發(fā)布商付款壮莹,以便在發(fā)布商的網(wǎng)站上展示其廣告。較為主流的付款方式是根據(jù)每次點(diǎn)擊付款(cost-per-click)姻檀,即當(dāng)用戶點(diǎn)擊了一次廣告之后命满,發(fā)布商就可以向廣告商索取費(fèi)用。因此绣版,對(duì)于發(fā)布商而言胶台,其預(yù)測(cè)廣告點(diǎn)擊率的能力直接決定了其營(yíng)收。
做出較為準(zhǔn)確的點(diǎn)擊率預(yù)測(cè)的關(guān)鍵是要識(shí)別出經(jīng)常預(yù)測(cè)的特征杂抽,并且同時(shí)挖掘出不常見的交叉特征信息诈唬。然而,用于Web級(jí)推薦系統(tǒng)的數(shù)據(jù)通常是離散的缩麸,且大多是類別數(shù)據(jù)铸磅,這就導(dǎo)致了一個(gè)非常龐大且稀疏的特征空間,對(duì)特征探索提出了巨大的挑戰(zhàn)。這也使得大多數(shù)大型系統(tǒng)受限于使用簡(jiǎn)單的線性模型愚屁,比如邏輯回歸济竹。線性模型很簡(jiǎn)單,解釋性很強(qiáng)霎槐,并且計(jì)算快速。然而梦谜,這也限制了它的表達(dá)能力丘跌。交叉特征已經(jīng)被證明對(duì)提高模型的表達(dá)能力很有效。然而它一般需要手工特征工程或者紛繁復(fù)雜的搜索才能找到唁桩;此外闭树,它也很難推廣到那些看不見的特征交互上。
作者在這篇論文中引入了一個(gè)新型的神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)荒澡,即DCN报辱。它避免了傳統(tǒng)的針對(duì)特定任務(wù)的特征工程,依靠神經(jīng)網(wǎng)絡(luò)強(qiáng)大的學(xué)習(xí)能力单山,在一定程度上實(shí)現(xiàn)自動(dòng)學(xué)習(xí)交叉特征組合碍现。
Deep&Cross 網(wǎng)絡(luò)模型
Deep&Cross模型,簡(jiǎn)稱DCN米奸。DCN模型的輸入是稠密和稀疏向量昼接,可以自動(dòng)進(jìn)行特征學(xué)習(xí),有效捕捉有限度的特征交互悴晰,學(xué)習(xí)高度非線性的特征之間的交互慢睡,無需復(fù)雜繁瑣的特征工程和詳細(xì)的特征搜索,并且具備較低的計(jì)算成本铡溪。模型的整體結(jié)構(gòu)圖如下:DCN模型整體結(jié)構(gòu)比較簡(jiǎn)單漂辐,下面分別進(jìn)行描述。
Embedding和Stack層
輸入數(shù)據(jù)包含稠密和稀疏向量棕硫。在Web級(jí)的推薦系統(tǒng)任務(wù)中髓涯,比如點(diǎn)擊率預(yù)估,其輸入大都是類別向量饲帅,比如“國(guó)家=中國(guó)”复凳。這樣的特征通常被編碼成one-hot向量,比如這樣“[0,1,0]”灶泵;但是如果稀疏向量很多育八,且類別較大,那么這將導(dǎo)致一個(gè)超高維的特征空間赦邻,同時(shí)造成空間浪費(fèi)以及計(jì)算復(fù)雜度高髓棋。為了減少維度,通常采用embedding處理(即上圖紅色矩形框部分),將這些二值化特征轉(zhuǎn)化為包含實(shí)數(shù)的稠密向量按声,也叫做嵌入向量(embedding vector)膳犹。轉(zhuǎn)換公式如下:其中就是embedding向量,
是第
個(gè)類別向量的二值輸入签则,并且
是對(duì)應(yīng)的embedding矩陣须床,這個(gè)矩陣也會(huì)隨著網(wǎng)絡(luò)的其他參數(shù)一起更新。隨后渐裂,將embedding向量和經(jīng)過歸一化處理的稠密向量
聚合成一個(gè)輸入向量
豺旬,定義如下:
Cross網(wǎng)絡(luò)
Cross網(wǎng)絡(luò)是DCN模型中最關(guān)鍵的部分,它以一種高效的方式來進(jìn)行特征交叉柒凉。Cross網(wǎng)絡(luò)包含若干個(gè)cross層族阅,每一層都通過以下公式計(jì)算:
其中
cross層的工作示意圖如下:
其中
這種結(jié)構(gòu)的設(shè)計(jì)使得特征交叉的力度隨著cross層深度的加深而逐漸加大。對(duì)于第
Cross Layer 設(shè)計(jì)的巧妙之處全部體現(xiàn)在上面的計(jì)算公式中,我們先看一些明顯的細(xì)節(jié):1) 每層的神經(jīng)元個(gè)數(shù)都相同咕村,都等于輸入
的維度
,也即每層的輸入輸出維度都是相等的场钉;2) 受殘差網(wǎng)絡(luò)(Residual Network)結(jié)構(gòu)啟發(fā),每層的函數(shù)擬合
的是
?
的殘差懈涛,殘差網(wǎng)絡(luò)有很多優(yōu)點(diǎn)逛万,其中一點(diǎn)是處理梯度消失的問題,使網(wǎng)絡(luò)可以“更深”批钠。
那為什么這樣設(shè)計(jì)呢宇植?對(duì)此論文中給出了定理3.1以及相關(guān)證明,但定理與證明過程都比較晦澀埋心,為了直觀清晰地講解清楚指郁,我們直接看一個(gè)具體的例子。加入Cross有2層拷呆,設(shè)置x_0 =闲坎,為了便于討論疫粥,令
,則有:
最后得到參與到最后的loss計(jì)算腰懂」4可以看到
包含了原始特征
從一階到二階的所有可能的叉乘組合,而
則包含了其從一階到三階的所有可能叉乘組合⌒辶铮現(xiàn)在大家應(yīng)該可以理解cross layer計(jì)算公式的設(shè)計(jì)意圖了慷彤。這個(gè)例子可以幫助我們更加深入理解cross層的設(shè)計(jì):
- 有限高階:叉乘階數(shù)由網(wǎng)絡(luò)深度決定,深度
對(duì)應(yīng)最高的
階的叉乘
- 自動(dòng)叉乘: Cross輸出包含了原始特征從一階到
階的所有叉乘組合怖喻,而模型的參數(shù)量?jī)H僅隨著輸入維度成線性增加瞬欧。
- 參數(shù)共享:不同叉乘項(xiàng)對(duì)應(yīng)的權(quán)重不同,單并非每個(gè)叉乘組合對(duì)應(yīng)獨(dú)立的權(quán)重罢防,通過參數(shù)共享,cross有效降低了參數(shù)量唉侄。此外咒吐,參數(shù)共享還使得模型具有更強(qiáng)的泛化性和魯棒性。例如属划,如果獨(dú)立訓(xùn)練權(quán)重恬叹,當(dāng)訓(xùn)練集中
這個(gè)叉乘特征沒有出現(xiàn),對(duì)應(yīng)的權(quán)重肯定為0同眯,而參數(shù)共享不會(huì)绽昼。類似地,數(shù)據(jù)集中的一些噪聲可以由大部分正常樣本來糾正權(quán)重參數(shù)的學(xué)習(xí)须蜗。
復(fù)雜度計(jì)算
令為cross層的數(shù)量硅确,
代表輸入向量的維度,那么cross層的參數(shù)量是:
即cross層的時(shí)間和空間復(fù)雜度是輸入向量的維度的線性函數(shù)明肮。相比deep層菱农,cross層引入的復(fù)雜度微不足道,這就保證了DCN的復(fù)雜度跟DNN是差不多的柿估。
論文表示循未,cross網(wǎng)絡(luò)之所以能夠高效地學(xué)習(xí)組合特征,就是因?yàn)?img class="math-inline" src="https://math.jianshu.com/math?formula=x_0%20%5Ctimes%20x_l%5ET" alt="x_0 \times x_l^T" mathimg="1">的秩為1秫舌,使得我們不用計(jì)算并存儲(chǔ)整個(gè)矩陣就可以得到所有的cross items的妖。
我們?cè)賮碛^察一下cross層的計(jì)算迭代公式:
假如輸入向量維度為
實(shí)際代碼調(diào)試的時(shí)候,兩種方法都實(shí)驗(yàn)了寝受,在我的電腦上測(cè)試發(fā)現(xiàn)坷牛,使用優(yōu)化過的計(jì)算方式大概可以將速度提升6倍左右。
Deep網(wǎng)絡(luò)
Deep層比較簡(jiǎn)單很澄,就是一個(gè)全連接前向神經(jīng)網(wǎng)絡(luò)京闰,每一層的計(jì)算方式如下:
其中
復(fù)雜度計(jì)算
出于簡(jiǎn)化目的讯蒲,我們假設(shè)所有的Deep層都是通用的大小痊土。令代表Deep層的數(shù)量,
代表Deep層的大小墨林,那么Deep網(wǎng)絡(luò)的參數(shù)量為:
其中 是第一層的參數(shù)量赁酝,第二層到最后一層的參數(shù)量為
,因?yàn)閺牡诙娱_始萌丈,輸入向量維度就變成了
維赞哗。
聚合層
聚合的作用是將Deep和Cross網(wǎng)絡(luò)的輸出聚合到一個(gè)向量中,并且通過一個(gè)標(biāo)準(zhǔn)的邏輯層辆雾。計(jì)算公式如下:
其中
主要貢獻(xiàn)
DCN的主要貢獻(xiàn)主要包含以下幾點(diǎn):
- 提出一種新型的交叉網(wǎng)絡(luò)結(jié)構(gòu)藤乙,可以用來提取交叉組合特征,并不需要人為設(shè)計(jì)的特征工程
- 這種網(wǎng)絡(luò)結(jié)構(gòu)足夠簡(jiǎn)單同時(shí)也很有效惭墓,可以獲得隨網(wǎng)絡(luò)層數(shù)增加而增加的多項(xiàng)式階(polynomial degree)交叉特征
- 十分節(jié)約內(nèi)存(依賴于正確地實(shí)現(xiàn))坛梁,并且易于使用
- 實(shí)驗(yàn)結(jié)果表明,DCN相比于其他模型有更出色的效果腊凶,與DNN模型相比划咐,較少的參數(shù)卻取得了較好的效果
代碼實(shí)踐
模型部分代碼拴念,主要包含了Deep和Cross,以及DeepCross模型實(shí)現(xiàn)褐缠,特別要注意的就是Cross模型中的矩陣計(jì)算的時(shí)候政鼠,有兩種方式,默認(rèn)用的是優(yōu)化過后的方法队魏。
import torch
import torch.nn as nn
from BaseModel.basemodel import BaseModel
class Deep(nn.Module):
def __init__(self, input_dim, deep_layers):
super(Deep, self).__init__()
deep_layers.insert(0, input_dim)
deep_ayer_list = []
for layer in list(zip(deep_layers[:-1], deep_layers[1:])):
deep_ayer_list.append(nn.Linear(layer[0], layer[1]))
deep_ayer_list.append(nn.BatchNorm1d(layer[1], affine=False))
deep_ayer_list.append(nn.ReLU(inplace=True))
self._deep = nn.Sequential(*deep_ayer_list)
def forward(self, x):
out = self._deep(x)
return out
class Cross(nn.Module):
"""
the operation is this module is x_0 * x_l^T * w_l + x_l + b_l for each layer, and x_0 is the init input
"""
def __init__(self, input_dim, num_cross_layers):
super(Cross, self).__init__()
self.num_cross_layers = num_cross_layers
weight_w = []
weight_b = []
batchnorm = []
for i in range(num_cross_layers):
weight_w.append(nn.Parameter(torch.nn.init.normal_(torch.empty(input_dim))))
weight_b.append(nn.Parameter(torch.nn.init.normal_(torch.empty(input_dim))))
batchnorm.append(nn.BatchNorm1d(input_dim, affine=False))
self.weight_w = nn.ParameterList(weight_w)
self.weight_b = nn.ParameterList(weight_b)
self.bn = nn.ModuleList(batchnorm)
def forward(self, x):
out = x
x = x.reshape(x.shape[0], -1, 1)
for i in range(self.num_cross_layers):
# this compute mode is time-consuming.
# out = torch.matmul(torch.bmm(x, torch.transpose(out.reshape(out.shape[0], -1, 1), 1, 2)), self.weight_w[i]) + self.weight_b[i] + out
# use this compute mode to speed up calculation
xxTw = torch.matmul(x, torch.matmul(torch.transpose(out.reshape(out.shape[0], -1, 1), 1, 2), self.weight_w[i].reshape(1, -1, 1)))
xxTw = xxTw.reshape(xxTw.shape[0], -1)
out = xxTw + self.weight_b[i] + out
out = self.bn[i](out)
return out
class DeepCross(BaseModel):
def __init__(self, config, dense_features_cols, sparse_features_cols):
super(DeepCross, self).__init__(config)
self._config = config
# 稠密和稀疏特征的數(shù)量
self._num_of_dense_feature = dense_features_cols.__len__()
self._num_of_sparse_feature = sparse_features_cols.__len__()
# For categorical features, we embed the features in dense vectors of dimension of 6 * category cardinality^1/4
# calculate all the embedding dimension of all the sparse features
self.embedding_dims = list(map(lambda x: int(6 * pow(x, 0.25)), sparse_features_cols))
# create embedding layers for all the sparse features
self.embedding_layers = nn.ModuleList([
nn.Embedding(num_embeddings=e[0], embedding_dim=e[1], scale_grad_by_freq=True) for e in list(zip(sparse_features_cols, self.embedding_dims))
])
self._input_dim = self._num_of_dense_feature + sum(self.embedding_dims)
self._deepNet = Deep(self._input_dim, self._config['deep_layers'])
self._crossNet = Cross(self._input_dim, self._config['num_cross_layers'])
self._final_dim = self._input_dim + self._config['deep_layers'][-1]
self._final_linear = nn.Linear(self._final_dim, 1)
def forward(self, x):
# 先區(qū)分出稀疏特征和稠密特征公般,這里是按照列來劃分的,即所有的行都要進(jìn)行篩選
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)
# 將dense特征和sparse特征聚合起來
input = torch.cat([sparse_embeds, dense_input], axis=-1)
deep_out = self._deepNet(input)
cross_out = self._crossNet(input)
final_input = torch.cat([deep_out, cross_out], dim=1)
output = self._final_linear(final_input)
output = torch.sigmoid(output)
return output
測(cè)試數(shù)據(jù)是criteo數(shù)據(jù)集的一個(gè)很小的子集胡桨,測(cè)試代碼如下:
import torch
from DeepCross.trainer import Trainer
from DeepCross.network import DeepCross
from Utils.criteo_loader import getTestData, getTrainData
import torch.utils.data as Data
deepcross_config = \
{
'deep_layers': [256,128,64,32], # 設(shè)置Deep模塊的隱層大小
'num_cross_layers': 4, # cross模塊的層數(shù)
'num_epoch': 2,
'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/DeepCross.model'
}
if __name__ == "__main__":
####################################################################################
# DeepCross 模型
####################################################################################
training_data, training_label, dense_features_col, sparse_features_col = getTrainData(deepcross_config['train_file'], deepcross_config['fea_file'])
train_dataset = Data.TensorDataset(torch.tensor(training_data).float(), torch.tensor(training_label).float())
test_data = getTestData(deepcross_config['test_file'])
test_dataset = Data.TensorDataset(torch.tensor(test_data).float())
deepCross = DeepCross(deepcross_config, dense_features_cols=dense_features_col, sparse_features_cols=sparse_features_col)
####################################################################################
# 模型訓(xùn)練階段
####################################################################################
# # 實(shí)例化模型訓(xùn)練器
trainer = Trainer(model=deepCross, config=deepcross_config)
# 訓(xùn)練
trainer.train(train_dataset)
# 保存模型
trainer.save()
####################################################################################
# 模型測(cè)試階段
####################################################################################
deepCross.eval()
if deepcross_config['use_cuda']:
deepCross.loadModel(map_location=lambda storage, loc: storage.cuda(deepcross_config['device_id']))
deepCross = deepCross.cuda()
else:
deepCross.loadModel(map_location=torch.device('cpu'))
y_pred_probs = deepCross(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))
測(cè)試代碼就是對(duì)criteo的測(cè)試集中的每一個(gè)樣本數(shù)據(jù)輸出對(duì)應(yīng)的點(diǎn)擊率預(yù)測(cè)官帘,0或者1。以下是部分測(cè)試結(jié)果:完整代碼見https://github.com/HeartbreakSurvivor/RsAlgorithms/tree/main/DeepCross昧谊。