推薦系統遇上深度學習(十八)--探祕阿里之深度興趣網路(DIN)淺析及實現
阿里近幾年公開的推薦領域演算法有許多,既有傳統領域的探索如MLR演算法,還有深度學習領域的探索如entire -space multi-task model,Deep Interest Network等,同時跟清華大學合作展開了強化學習領域的探索,提出了MARDPG演算法。
上一篇,我們介紹了MLR演算法,通過分而治之的思想改進了傳統的LR演算法,使其能夠擬合更復雜的線性關係。這一篇,我們來簡單理解和實現一下阿里在去年提出的另一個重要的推薦系統模型-深度興趣網路(DIN,Deep Interest Network). 該方法由蓋坤大神領導的阿里媽媽的精準定向檢索及基礎演算法團隊提出,充分利用/挖掘使用者歷史行為資料中的資訊來提高CTR預估的效能。
1、背景
深度學習在CTR預估領域已經有了廣泛的應用,常見的演算法比如Wide&Deep,DeepFM等。這些方法一般的思路是:通過Embedding層,將高維離散特徵轉換為固定長度的連續特徵,然後通過多個全聯接層,最後通過一個sigmoid函式轉化為0-1值,代表點選的概率。即Sparse Features -> Embedding Vector -> MLPs -> Sigmoid -> Output.
這種方法的優點在於:通過神經網路可以擬合高階的非線性關係,同時減少了人工特徵的工作量。
不過,阿里的研究者們通過觀察收集到的線上資料,發現了使用者行為資料中有兩個很重要的特性:
Diversity:使用者在瀏覽電商網站的過程中顯示出的興趣是十分多樣性的。
Local activation: 由於使用者興趣的多樣性,只有部分歷史資料會影響到當次推薦的物品是否被點選,而不是所有的歷史記錄。
這兩種特性是密不可分的。
舉個簡單的例子,觀察下面的表格:
Diversity體現在年輕的母親的歷史記錄中體現的興趣十分廣泛,涵蓋羊毛衫、手提袋、耳環、童裝、運動裝等等。而愛好游泳的人同樣興趣廣泛,歷史記錄涉及浴裝、旅遊手冊、踏水板、馬鈴薯、冰激凌、堅果等等。
Local activation體現在,當我們給愛好游泳的人推薦goggle(護目鏡)時,跟他之前是否購買過薯片、書籍、冰激凌的關係就不大了,而跟他游泳相關的歷史記錄如游泳帽的關係就比較密切。
針對上面提到的使用者行為中存在的兩種特性,阿里將其運用於自身的推薦系統中,推出了深度興趣網路DIN,接下來,我們就一起來看一下模型的一些實現細節,然後我們會給出一個簡化版的tensorflow實現。
2、模型設計
整體框架
我們先來看一下推薦系統的整體框架:
整個流程可以描述為:
1.檢查使用者歷史行為資料
2.使用matching module產生候選ads。
3.通過ranking module做point-wise的排序,即得到每個候選ads的點選概率,並根據概率排序得到推薦列表。
4.記錄下使用者在當前展示廣告下的反應(點選與否),作為label。
特徵設計
本文將所涉及到的特徵分為四個部分:使用者特徵、使用者行為特徵、廣告特徵、上下文特徵,具體如下:
其中,使用者行為特徵是multi-hot的,即多值離散特徵。針對這種特徵,由於每個涉及到的非0值個數是不一樣的,常見的做法就是將id轉換成embedding之後,加一層pooling層,比如average-pooling,sum-pooling,max-pooling。DIN中使用的是weighted-sum,其實就是加權的sum-pooling,權重經過一個activation unit計算得到。這裡我們後面還會再介紹到。
BaseModel
在介紹DIN之前,我們先來看一下一個基準模型,結構如下:
這裡element-wise的意思其實就是元素級別的加減,同時,可不要忽略廣播的存在喲。一個元素和一個向量相乘,也可以看作element-wise的,因為這個元素會廣播成和向量一樣的長度嘛,嘻嘻。
可以看到,Base Model首先吧one-hot或multi-hot特徵轉換為特定長度的embedding,作為模型的輸入,然後經過一個DNN的part,得到最終的預估值。特別地,針對multi-hot的特徵,做了一次element-wise+的操作,這裡其實就是sum-pooling,這樣,不管特徵中有多少個非0值,經過轉換之後的長度都是一樣的!
Deep Interest Network
Base Model有一個很大的問題,它對使用者的歷史行為是同等對待的,沒有做任何處理,這顯然是不合理的。一個很顯然的例子,離現在越近的行為,越能反映你當前的興趣。因此,對使用者歷史行為基於Attention機制進行一個加權,阿里提出了深度興趣網路(Deep Interest Network),先來看一下模型結構:
Attention機制簡單的理解就是,針對不同的廣告,使用者歷史行為與該廣告的權重是不同的。假設使用者有ABC三個歷史行為,對於廣告D,那麼ABC的權重可能是0.8、0.1、0.1;對於廣告E,那麼ABC的權重可能是0.3、0.6、0.1。這裡的權重,就是Attention機制即上圖中的Activation Unit所需要學習的。
為什麼要引入這一個機制呢?難道僅僅是通過觀察歷史資料拍腦袋決定的麼?當然不是,如果不用Local activation的話,將會出現下面的情況:假設使用者的興趣的Embedding是Vu,候選廣告的Embedding是Va,使用者興趣和候選的廣告的相關性可以寫作F(U,A) = Va * Vu。如果沒有Local activation機制的話,那麼同一個使用者對於不同的廣告,Vu都是相同的。舉例來說,如果有兩個廣告A和B,使用者興趣和A,B的相似性都很高,那麼在Va和Vb連線上的廣告都會有很高的相似性。這樣的限制使得模型非常難學習到有效的使用者和廣告的embedidng表示。
在加入Activation Unit之後,使用者的興趣表示計算如下:
其中,Vi表示behavior id i的嵌入向量,比如good_id,shop_id等。Vu是所有behavior ids的加權和,表示的是使用者興趣;Va是候選廣告的嵌入向量;wi是候選廣告影響著每個behavior id的權重,也就是Local Activation。wi通過Activation Unit計算得出,這一塊用函式去擬合,表示為g(Vi,Va)。
3、模型細節
3.1 評價指標GAUC
模型使用的評價指標是GAUC,我們先來看一下GAUC的計算公式:
我們首先要肯定的是,AUC是要分使用者看的,我們的模型的預測結果,只要能夠保證對每個使用者來說,他想要的結果排在前面就好了。
假設有兩個使用者A和B,每個使用者都有10個商品,10個商品中有5個是正樣本,我們分別用TA,TB,FA,FB來表示兩個使用者的正樣本和負樣本。也就是說,20個商品中有10個是正樣本。假設模型預測的結果大小排序依次為TA,FA,TB,FB。如果把兩個使用者的結果混起來看,AUC並不是很高,因為有5個正樣本排在了後面,但是分開看的話,每個使用者的正樣本都排在了負樣本之前,AUC應該是1。顯然,分開看更容易體現模型的效果,這樣消除了使用者本身的差異。
但是上文中所說的差異是在使用者點選數即樣本數相同的情況下說的。還有一種差異是使用者的展示次數或者點選數,如果一個使用者有1個正樣本,10個負樣本,另一個使用者有5個正樣本,50個負樣本,這種差異同樣需要消除。那麼GAUC的計算,不僅將每個使用者的AUC分開計算,同時根據使用者的展示數或者點選數來對每個使用者的AUC進行加權處理。進一步消除了使用者偏差對模型的影響。通過實驗證明,GAUC確實是一個更加合理的評價指標。
3.2 Dice啟用函式
從Relu到PRelu
Relu啟用函式形式如下:
Relu啟用函式在值大於0時原樣輸出,小於0時輸出為0。這樣的話導致了許多網路節點的更新緩慢。因此又了PRelu,也叫Leaky Relu,形式如下:
這樣,及時值小於0,網路的引數也得以更新,加快了收斂速度。
從PReLU到Dice
儘管對Relu進行了修正得到了PRelu,但是仍然有一個問題,即我們認為分割點都是0,但實際上,分割點應該由資料決定,因此文中提出了Dice啟用函式
Dice啟用函式的全稱是Data Dependent Activation Function,形式如下:
其中,期望和方差的計算如下:
可也看到,每一個yi對應了一個概率值pi。pi的計算主要分為兩步:將yi進行標準化和進行sigmoid變換。
3.3 自適應正則 Adaptive Regularization
CTR中輸入稀疏而且維度高,通常的做法是加入L1、L2、Dropout等防止過擬合。但是論文中嘗試後效果都不是很好。使用者資料符合長尾定律long-tail law,也就是說很多的feature id只出現了幾次,而一小部分feature id出現很多次。這在訓練過程中增加了很多噪聲,並且加重了過擬合。
對於這個問題一個簡單的處理辦法就是:直接去掉出現次數比較少的feature id。但是這樣就人為的丟掉了一些資訊,導致模型更加容易過擬合,同時閾值的設定作為一個新的超引數,也是需要大量的實驗來選擇的。
因此,阿里提出了自適應正則的做法,即:
1.針對feature id出現的頻率,來自適應的調整他們正則化的強度;
2.對於出現頻率高的,給與較小的正則化強度;
3.對於出現頻率低的,給予較大的正則化強度。
計算公式如下:
4、效果展示
下圖是對Local Activation效果的一個展示,可以看到,對於候選的廣告是一件衣服的時候,使用者歷史行為中跟衣服相關的權重較高,而非衣服的部分,權重較低。
下圖是對使用不同正則項的結果進行的展示,可以發現,使用自適應正則的情況下,模型的驗證集誤差和驗證集GAUC均是最好的。
下圖對比了Base Model和DIN的實驗結果,可以看到,DIN模型在加入Dice啟用函式以及自適應正則之後,模型的效果有了一定的提升:
5、實戰DIN
這裡我們只給出一些模型細節的實現,具體的資料處理以及其他方面的內容大家可以根據上面兩個地址進行學習:
資料準備
按照下面的方法下載資料:
Dice啟用函式
這裡實現的Dice啟用函式沒有根據上一步的均值方差來計算這一步的均值方差,而是直接計算了這個batch的均值方差。我們可以根據計算出的均值方差對x進行標準化(程式碼中被註釋掉了),也可以直接呼叫batch_normalization來對輸入進行標準化。
注意的一點是,alpha也是需要訓練的一個引數。
import tensorflow as tf
def dice(_x,axis=-1,epsilon=0.0000001,name=''):
alphas = tf.get_variable('alpha'+name,_x.get_shape()[-1],
initializer = tf.constant_initializer(0.0),
dtype=tf.float32)
input_shape = list(_x.get_shape())
reduction_axes = list(range(len(input_shape)))
del reduction_axes[axis] # [0]
broadcast_shape = [1] * len(input_shape) #[1,1]
broadcast_shape[axis] = input_shape[axis] # [1 * hidden_unit_size]
# case: train mode (uses stats of the current batch)
mean = tf.reduce_mean(_x, axis=reduction_axes) # [1 * hidden_unit_size]
brodcast_mean = tf.reshape(mean, broadcast_shape)
std = tf.reduce_mean(tf.square(_x - brodcast_mean) + epsilon, axis=reduction_axes)
std = tf.sqrt(std)
brodcast_std = tf.reshape(std, broadcast_shape) #[1 * hidden_unit_size]
# x_normed = (_x - brodcast_mean) / (brodcast_std + epsilon)
x_normed = tf.layers.batch_normalization(_x, center=False, scale=False) # a simple way to use BN to calculate x_p
x_p = tf.sigmoid(x_normed)
return alphas * (1.0 - x_p) * _x + x_p * _x
Activation Unit
這裡的輸入有三個,候選廣告queries,使用者歷史行為keys,以及Batch中每個行為的長度。這裡為什麼要輸入一個keys_length呢,因為每個使用者發生過的歷史行為是不一樣多的,但是輸入的keys維度是固定的(都是歷史行為最大的長度),因此我們需要這個長度來計算一個mask,告訴模型哪些行為是沒用的,哪些是用來計算使用者興趣分佈的。
經過以下幾個步驟得到使用者的興趣分佈:
- 將queries變為和keys同樣的形狀B * T * H(B指batch的大小,T指使用者歷史行為的最大長度,H指embedding的長度)
- 通過三層神經網路得到queries和keys中每個key的權重,並經過softmax進行標準化
- 通過weighted sum得到終端使用者的歷史行為分佈
def attention(queries,keys,keys_length):
'''
queries: [B, H]
keys: [B, T, H]
keys_length: [B]
'''
queries_hidden_units = queries.get_shape().as_list()[-1]
queries = tf.tile(queries,[1,tf.shape(keys)[1]])
queries = tf.reshape(queries,[-1,tf.shape(keys)[1],queries_hidden_units])
din_all = tf.concat([queries,keys,queries-keys,queries * keys],axis=-1) # B*T*4H
# 三層全連結
d_layer_1_all = tf.layers.dense(din_all, 80, activation=tf.nn.sigmoid, name='f1_att')
d_layer_2_all = tf.layers.dense(d_layer_1_all, 40, activation=tf.nn.sigmoid, name='f2_att')
d_layer_3_all = tf.layers.dense(d_layer_2_all, 1, activation=None, name='f3_att') #B*T*1
outputs = tf.reshape(d_layer_3_all,[-1,1,tf.shape(keys)[1]]) #B*1*T
# Mask
key_masks = tf.sequence_mask(keys_length,tf.shape(keys)[1])
key_masks = tf.expand_dims(key_masks,1) # B*1*T
paddings = tf.ones_like(outputs) * (-2 ** 32 + 1) # 在補足的地方附上一個很小的值,而不是0
outputs = tf.where(key_masks,outputs,paddings) # B * 1 * T
# Scale
outputs = outputs / (keys.get_shape().as_list()[-1] ** 0.5)
# Activation
outputs = tf.nn.softmax(outputs) # B * 1 * T
# Weighted Sum
outputs = tf.matmul(outputs,keys) # B * 1 * H 三維矩陣相乘,相乘發生在後兩維,即 B * (( 1 * T ) * ( T * H ))
return outputs