推薦系統(tǒng)遇上深度學(xué)習(xí)系列:
推薦系統(tǒng)遇上深度學(xué)習(xí)(一)--FM模型理論和實踐:http://www.reibang.com/p/152ae633fb00
推薦系統(tǒng)遇上深度學(xué)習(xí)(二)--FFM模型理論和實踐:http://www.reibang.com/p/781cde3d5f3d
1、背景
特征組合的挑戰(zhàn)
對于一個基于CTR預(yù)估的推薦系統(tǒng)瞬铸,最重要的是學(xué)習(xí)到用戶點擊行為背后隱含的特征組合辟汰。在不同的推薦場景中,低階組合特征或者高階組合特征可能都會對最終的CTR產(chǎn)生影響丰榴。
之前介紹的因子分解機(Factorization Machines, FM)通過對于每一維特征的隱變量內(nèi)積來提取特征組合。最終的結(jié)果也非常好。但是类腮,雖然理論上來講FM可以對高階特征組合進(jìn)行建模做入,但實際上因為計算復(fù)雜度的原因一般都只用到了二階特征組合谴垫。
那么對于高階的特征組合來說,我們很自然的想法母蛛,通過多層的神經(jīng)網(wǎng)絡(luò)即DNN去解決翩剪。
DNN的局限
下面的圖片來自于張俊林教授在AI大會上所使用的PPT。
我們之前也介紹過了彩郊,對于離散特征的處理前弯,我們使用的是將特征轉(zhuǎn)換成為one-hot的形式,但是將One-hot類型的特征輸入到DNN中秫逝,會導(dǎo)致網(wǎng)絡(luò)參數(shù)太多:
如何解決這個問題呢恕出,類似于FFM中的思想,將特征分為不同的field:
再加兩層的全鏈接層违帆,讓Dense Vector進(jìn)行組合浙巫,那么高階特征的組合就出來了
但是低階和高階特征組合隱含地體現(xiàn)在隱藏層中,如果我們希望把低階特征組合單獨建模刷后,然后融合高階特征組合的畴。
即將DNN與FM進(jìn)行一個合理的融合:
二者的融合總的來說有兩種形式,一是串行結(jié)構(gòu)尝胆,二是并行結(jié)構(gòu)
而我們今天要講到的DeepFM丧裁,就是并行結(jié)構(gòu)中的一種典型代表。
2含衔、DeepFM模型
我們先來看一下DeepFM的模型結(jié)構(gòu):
DeepFM包含兩部分:神經(jīng)網(wǎng)絡(luò)部分與因子分解機部分煎娇,分別負(fù)責(zé)低階特征的提取和高階特征的提取。這兩部分共享同樣的輸入贪染。DeepFM的預(yù)測結(jié)果可以寫為:
FM部分
FM部分的詳細(xì)結(jié)構(gòu)如下:
FM部分是一個因子分解機缓呛。關(guān)于因子分解機可以參閱文章[Rendle, 2010] Steffen Rendle. Factorization machines. In ICDM, 2010.。因為引入了隱變量的原因杭隙,對于幾乎不出現(xiàn)或者很少出現(xiàn)的隱變量哟绊,F(xiàn)M也可以很好的學(xué)習(xí)。
FM的輸出公式為:
深度部分
深度部分是一個前饋神經(jīng)網(wǎng)絡(luò)寺渗。與圖像或者語音這類輸入不同匿情,圖像語音的輸入一般是連續(xù)而且密集的兰迫,然而用于CTR的輸入一般是及其稀疏的。因此需要重新設(shè)計網(wǎng)絡(luò)結(jié)構(gòu)炬称。具體實現(xiàn)中為汁果,在第一層隱含層之前,引入一個嵌入層來完成將輸入向量壓縮到低維稠密向量玲躯。
嵌入層(embedding layer)的結(jié)構(gòu)如上圖所示据德。當(dāng)前網(wǎng)絡(luò)結(jié)構(gòu)有兩個有趣的特性,1)盡管不同field的輸入長度不同跷车,但是embedding之后向量的長度均為K棘利。2)在FM里得到的隱變量Vik現(xiàn)在作為了嵌入層網(wǎng)絡(luò)的權(quán)重。
這里的第二點如何理解呢朽缴,假設(shè)我們的k=5善玫,首先,對于輸入的一條記錄密强,同一個field 只有一個位置是1茅郎,那么在由輸入得到dense vector的過程中,輸入層只有一個神經(jīng)元起作用或渤,得到的dense vector其實就是輸入層到embedding層該神經(jīng)元相連的五條線的權(quán)重系冗,即vi1,vi2薪鹦,vi3掌敬,vi4绷蹲,vi5赌厅。這五個值組合起來就是我們在FM中所提到的Vi汇恤。在FM部分和DNN部分月帝,這一塊是共享權(quán)重的,對同一個特征來說洼裤,得到的Vi是相同的蓬坡。
有關(guān)模型具體如何操作,我們可以通過代碼來進(jìn)一步加深認(rèn)識离斩。
3、相關(guān)知識
我們先來講兩個代碼中會用到的相關(guān)知識吧瘪匿,代碼是參考的github上星數(shù)最多的DeepFM實現(xiàn)代碼跛梗。
Gini Normalization
代碼中將CTR預(yù)估問題設(shè)定為一個二分類問題,繪制了Gini Normalization來評價不同模型的效果棋弥。這個是什么東西核偿,不太懂,百度了很多顽染,發(fā)現(xiàn)了一個比較通俗易懂的介紹漾岳。
假設(shè)我們有下面兩組結(jié)果轰绵,分別表示預(yù)測值和實際值:
predictions = [0.9, 0.3, 0.8, 0.75, 0.65, 0.6, 0.78, 0.7, 0.05, 0.4, 0.4, 0.05, 0.5, 0.1, 0.1]
actual = [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
然后我們將預(yù)測值按照從小到大排列,并根據(jù)索引序?qū)嶋H值進(jìn)行排序:
SortedActualValues[0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1]
然后尼荆,我們可以畫出如下的圖片:
接下來我們將數(shù)據(jù)Normalization到0左腔,1之間。并畫出45度線捅儒。
橙色區(qū)域的面積液样,就是我們得到的Normalization的Gini系數(shù)。
這里巧还,由于我們是將預(yù)測概率從小到大排的鞭莽,所以我們希望實際值中的0盡可能出現(xiàn)在前面,因此Normalization的Gini系數(shù)越大麸祷,分類效果越好澎怒。
embedding_lookup
在tensorflow中有個embedding_lookup函數(shù),我們可以直接根據(jù)一個序號來得到一個詞或者一個特征的embedding值阶牍,那么他內(nèi)部其實是包含一個網(wǎng)絡(luò)結(jié)構(gòu)的丹拯,如下圖所示:
假設(shè)我們想要找到2的embedding值,這個值其實是輸入層第二個神經(jīng)元與embedding層連線的權(quán)重值荸恕。
之前有大佬跟我探討word2vec輸入的問題乖酬,現(xiàn)在也算是有個比較明確的答案,輸入其實就是one-hot Embedding融求,而word2vec要學(xué)習(xí)的是new Embedding咬像。
4、代碼解析
好生宛,一貫的風(fēng)格县昂,先來介紹幾個地址:
原代碼地址:https://github.com/ChenglongChen/tensorflow-DeepFM
本文代碼地址:https://github.com/princewen/tensorflow_practice/tree/master/Basic-DeepFM-model
數(shù)據(jù)下載地址:https://www.kaggle.com/c/porto-seguro-safe-driver-prediction
好了,話不多說陷舅,我們來看看代碼目錄吧倒彰,接下來,我們將主要對網(wǎng)絡(luò)的構(gòu)建進(jìn)行介紹莱睁,而對數(shù)據(jù)的處理待讳,流程的控制部分,相信大家根據(jù)代碼就可以看懂仰剿。
項目結(jié)構(gòu)
項目結(jié)構(gòu)如下:
其實還應(yīng)該有一個存放data的路徑创淡。config.py保存了我們模型的一些配置。DataReader對數(shù)據(jù)進(jìn)行處理南吮,得到模型可以使用的輸入琳彩。DeepFM是我們構(gòu)建的模型。main是項目的入口。metrics是計算normalized gini系數(shù)的代碼露乏。
模型輸入
模型的輸入主要有下面幾個部分:
self.feat_index = tf.placeholder(tf.int32,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? shape=[None,None],? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? name='feat_index')self.feat_value = tf.placeholder(tf.float32,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? shape=[None,None],? ? ? ? ? ? ? ? ? ? ? ? ? ? ? name='feat_value')self.label = tf.placeholder(tf.float32,shape=[None,1],name='label')self.dropout_keep_fm = tf.placeholder(tf.float32,shape=[None],name='dropout_keep_fm')self.dropout_keep_deep = tf.placeholder(tf.float32,shape=[None],name='dropout_deep_deep')
feat_index是特征的一個序號碧浊,主要用于通過embedding_lookup選擇我們的embedding。feat_value是對應(yīng)的特征值瘟仿,如果是離散特征的話辉词,就是1,如果不是離散特征的話猾骡,就保留原來的特征值瑞躺。label是實際值。還定義了兩個dropout來防止過擬合兴想。
權(quán)重構(gòu)建
權(quán)重的設(shè)定主要有兩部分幢哨,第一部分是從輸入到embedding中的權(quán)重,其實也就是我們的dense vector嫂便。另一部分就是深度神經(jīng)網(wǎng)絡(luò)每一層的權(quán)重捞镰。第二部分很好理解,我們主要來看看第一部分:
#embeddingsweights['feature_embeddings'] = tf.Variable(? ? tf.random_normal([self.feature_size,self.embedding_size],0.0,0.01),? ? name='feature_embeddings')weights['feature_bias'] = tf.Variable(tf.random_normal([self.feature_size,1],0.0,1.0),name='feature_bias')
weights['feature_embeddings'] 存放的每一個值其實就是FM中的vik毙替,所以它是N * F * K的岸售。其中N代表數(shù)據(jù)量的大小,F(xiàn)代表feture的大小(將離散特征轉(zhuǎn)換成one-hot之后的特征總量),K代表dense vector的大小厂画。
weights['feature_bias']是FM中的一次項的權(quán)重凸丸。
Embedding part
這個部分很簡單啦,是根據(jù)feat_index選擇對應(yīng)的weights['feature_embeddings']中的embedding值袱院,然后再與對應(yīng)的feat_value相乘就可以了:
# modelself.embeddings = tf.nn.embedding_lookup(self.weights['feature_embeddings'],self.feat_index)# N * F * Kfeat_value = tf.reshape(self.feat_value,shape=[-1,self.field_size,1])self.embeddings = tf.multiply(self.embeddings,feat_value)
FM part
首先來回顧一下我們之前對FM的化簡公式屎慢,之前去今日頭條面試還問到過公式的推導(dǎo)。
所以我們的二次項可以根據(jù)化簡公式輕松的得到忽洛,再加上我們的一次項腻惠,F(xiàn)M的part就算完了。同時更為方便的是欲虚,由于權(quán)重共享集灌,我們這里可以直接用Embedding part計算出的embeddings來得到我們的二次項:
# first order termself.y_first_order = tf.nn.embedding_lookup(self.weights['feature_bias'],self.feat_index)self.y_first_order = tf.reduce_sum(tf.multiply(self.y_first_order,feat_value),2)self.y_first_order = tf.nn.dropout(self.y_first_order,self.dropout_keep_fm[0])# second order term# sum-square-partself.summed_features_emb = tf.reduce_sum(self.embeddings,1)# None * kself.summed_features_emb_square = tf.square(self.summed_features_emb)# None * K# squre-sum-partself.squared_features_emb = tf.square(self.embeddings)self.squared_sum_features_emb = tf.reduce_sum(self.squared_features_emb,1)# None * K#second orderself.y_second_order =0.5* tf.subtract(self.summed_features_emb_square,self.squared_sum_features_emb)self.y_second_order = tf.nn.dropout(self.y_second_order,self.dropout_keep_fm[1])
DNN part
DNNpart的話,就是將Embedding part的輸出再經(jīng)過幾層全鏈接層:
# Deep componentself.y_deep = tf.reshape(self.embeddings,shape=[-1,self.field_size *self.embedding_size])self.y_deep = tf.nn.dropout(self.y_deep,self.dropout_keep_deep[0])foriinrange(0,len(self.deep_layers)):self.y_deep = tf.add(tf.matmul(self.y_deep,self.weights["layer_%d"%i]),self.weights["bias_%d"%I])self.y_deep =self.deep_layers_activation(self.y_deep)self.y_deep = tf.nn.dropout(self.y_deep,self.dropout_keep_deep[i+1])
最后复哆,我們要將DNN和FM兩部分的輸出進(jìn)行結(jié)合:
concat_input = tf.concat([self.y_first_order,self.y_second_order,self.y_deep], axis=1)
損失及優(yōu)化器
我們可以使用logloss(如果定義為分類問題)欣喧,或者mse(如果定義為預(yù)測問題),以及多種的優(yōu)化器去進(jìn)行嘗試寂恬,這些根據(jù)不同的參數(shù)設(shè)定得到:
# lossifself.loss_type =="logloss":self.out = tf.nn.sigmoid(self.out)self.loss = tf.losses.log_loss(self.label,self.out)elifself.loss_type =="mse":self.loss = tf.nn.l2_loss(tf.subtract(self.label,self.out))# l2 regularization on weightsifself.l2_reg >0:self.loss += tf.contrib.layers.l2_regularizer(self.l2_reg)(self.weights["concat_projection"])ifself.use_deep:foriinrange(len(self.deep_layers)):self.loss += tf.contrib.layers.l2_regularizer(self.l2_reg)(self.weights["layer_%d"% I])ifself.optimizer_type =="adam":self.optimizer = tf.train.AdamOptimizer(learning_rate=self.learning_rate, beta1=0.9, beta2=0.999,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? epsilon=1e-8).minimize(self.loss)elifself.optimizer_type =="adagrad":self.optimizer = tf.train.AdagradOptimizer(learning_rate=self.learning_rate,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? initial_accumulator_value=1e-8).minimize(self.loss)elifself.optimizer_type =="gd":self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.learning_rate).minimize(self.loss)elifself.optimizer_type =="momentum":self.optimizer = tf.train.MomentumOptimizer(learning_rate=self.learning_rate, momentum=0.95).minimize(self.loss)
模型效果
前面提到了续誉,我們用logloss作為損失函數(shù)去進(jìn)行模型的參數(shù)更新,但是代碼中輸出了模型的 Normalization 的 Gini值來進(jìn)行模型評價初肉,我們可以對比一下(記住,Gini值越大越好呦):
參考資料
1饰躲、http://www.360doc.com/content/17/0315/10/10408243_637001469.shtml
2牙咏、https://blog.csdn.net/u010665216/article/details/78528261
3:http://www.reibang.com/p/6f1c2643d31