簡介
本文要介紹的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)圖,如下:模型的輸入是一系列的獨立特征哪痰,模型總共包含4層遂赠,分別是Embedding層、Stacking層晌杰、Residual Unit層跷睦、Scoring層,模型的輸出是用戶點擊率預(yù)測值肋演。
注意上圖中紅色方框部分抑诸,輸入特征沒有經(jīng)過Embedding層就直接連接到了Stacking層了。這是因為輸入特征可能是稠密的也可能是稀疏的爹殊,論文中指出蜕乡,對于維度小于256的特征直接連接到Stacking層。
損失函數(shù)
論文中使用的是交叉熵損失函數(shù)边灭,但是也可以使用Softmax或者其他損失函數(shù)异希。定義如下:
其中
Embedding層
Embedding層的主要作用是對輸入特征進行特征轉(zhuǎn)換父虑,Embedding層包含了一個單層神經(jīng)網(wǎng)絡(luò)该酗,網(wǎng)絡(luò)定義如下:
其中
值得注意的是戚啥,Embedding層的大小會對整個模型的整體大小產(chǎn)生很重要的影響。即便輸入是稀疏特征锉试,大小為
Stacking層
當(dāng)?shù)玫搅怂休斎胩卣鞯腅mbedding表示之后(特征維度小于256的除外)。Stacking層所做的事情只是簡單把這些特征聚合起來呆盖,形成一個向量炫彩。表示如下:
其中
Residual層
殘差層的是由下圖所示的殘差單元構(gòu)建成的丁频。殘差單元如下所示:
Deep Crossing模型中使用的殘差單元與ResNet中使用的不太一樣杉允,它不包含卷積操作。殘差單元的特有屬性就是將輸入加到了隱層的輸出上席里,上圖所示的殘差單元的計算可由下式來表示:
其中
關(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é)果:完整代碼見https://github.com/HeartbreakSurvivor/RsAlgorithms/blob/main/Test/deepcrossing_test.py