DIN原理和代碼實現(xiàn)

注意力機制

DIN模型的創(chuàng)新點在于增加了注意力機制球昨,用于生成用戶的embedding向量运怖。具體公式如下

Vu = f(Va) = \sum_{i=1}^{N}w_{i}·V_{i}=\sum_{i=1}^{N}g(V_{i}, V_{a})·V_{i}

其中幢踏,Vu是用戶的embedding向量斩萌,V_{a}是候選商品的Embedding向量,V_{i}是用戶u的第i次行為的embedding向量件缸。

用戶embedding = Sum(候選商品與歷史行為商品的注意力分*用戶歷史行為商品的embedding向量)

注意力分g(Vi, Va)函數(shù)是如何計算的呢铜靶?

對候選商品embedding和歷史行為商品的embedding的元素減、候選商品embedding他炊、歷史行為商品的embedding進行concat争剿,然后輸入fc層,最后神經(jīng)元輸出層得到注意力得分痊末。


image.png
代碼

tf.tile()函數(shù)是對張量進行擴展的蚕苇。對張量內(nèi)的數(shù)據(jù)按一定規(guī)則復制。

tf.tile(
    input, # 待擴展的張量
    multiples, # 擴展方式凿叠,假如input是一個3維的張量涩笤,multiples必須是一個1x3的張量,且這3個值表示input的1盒件、2蹬碧、3維度擴展幾倍
    name=None
)

tf.squeeze()函數(shù)是從張量中移除大小為1的維度。

tf.squeeze(
    input,
    axis=None, # 指定的維度列表
)

tf.where()函數(shù)炒刁。

tf.where(input, name=None)
用法一:返回輸入矩陣中true對應(yīng)的位置恩沽。

tf.where(input, a, b)
用法二:將a中input對應(yīng)位置為true的元素值不變,其余元素替換為b中對應(yīng)位置的元素翔始。
import os
import numpy as np
import pandas as pd
from collections import namedtuple

import tensorflow as tf
from tensorflow.keras.layers import *
from tensorflow.keras.models import *
from sklearn.preprocessing import  MinMaxScaler, LabelEncoder

# 使用具名元組定義特征標記
SparseFeature = namedtuple('SparseFeature', ['name', 'vocabulary_size', 'embedding_size'])
DenseFeature = namedtuple('DenseFeature', ['name', 'dimension'])
VarLenSparseFeature = namedtuple('VarLenSparseFeature', ['name', 'vocabulary_size', 'embedding_size', 'maxlen'])

##### 數(shù)據(jù)預處理
data = pd.read_csv('./data/movie_sample.txt', sep="\t", header=None)
data.columns = ["user_id", "gender", "age", "hist_movie_id", "hist_len", "movie_id", "movie_type_id", "label"]
data.head()

# 輸入數(shù)據(jù)
X = data[['user_id', 'gender', 'age', 'hist_movie_id', 'hist_len',
       'movie_id', 'movie_type_id']]
y = data['label']
X_train = {
    'user_id': np.array(X['user_id']),
    'gender': np.array(X['gender']),
    'age': np.array(X['age']),
    'hist_movie_id': np.array([[int(i) for i in s.split(',')] for s in X['hist_movie_id']]),
    'hist_len': np.array(X['hist_len']),
    'movie_id': np.array(X['movie_id']),
    'movie_type_id': np.array(X['movie_type_id'])
}
y_train = np.array(y)

def build_input_layers(feature_columns):
    """ 構(gòu)建輸入層 """
    input_layer_dict = {}
    for f in feature_columns:
        if isinstance(f, DenseFeature):
            input_layer_dict[f.name] = Input(shape=(f.dimension, ), name=f.name)
        elif isinstance(f, SparseFeature):
            input_layer_dict[f.name] = Input(shape=(1, ), name=f.name)
        elif isinstance(f, VarLenSparseFeature):
            input_layer_dict[f.name] = Input(shape=(f.maxlen, ), name=f.name)
    return input_layer_dict

def build_embedding_layers(feature_columns):
    embedding_layers_dict = {}
    for f in feature_columns:
        if isinstance(f, SparseFeature):
            embedding_layers_dict[f.name] = Embedding(f.vocabulary_size + 1, f.embedding_size, name='emb_' + f.name)
        elif isinstance(f, VarLenSparseFeature):
            embedding_layers_dict[f.name] = Embedding(f.vocabulary_size + 1, f.embedding_size, name='var_emb_' + f.name, mask_zero=True)
    return embedding_layers_dict

def concat_embedding_list(feature_columns, input_layer_dict, embedding_layer_dict, flatten=False):
    """ 拼接embedding特征 """
    sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeature), feature_columns)) if feature_columns else []
    embedding_list = []
    for f in sparse_feature_columns:
        _input_layer = input_layer_dict[f.name] 
        _embed = embedding_layer_dict[f.name]
        embed_layer = _embed(_input_layer)
        if flatten:
            embed_layer = Flatten()(embed_layer)
        
        embedding_list.append(embed_layer)
    return embedding_list

def embedding_lookup(feature_columns, input_layer_dict, embedding_layer_dict):
    embedding_list = []
    for f in feature_columns:
        _input_layer = input_layer_dict[f] 
        _embed = embedding_layer_dict[f]
        embed_layer = _embed(_input_layer)
        embedding_list.append(embed_layer)
    return embedding_list

def concat_input_list(input_list):
    """ 合并input列表 """
    _nums = len(input_list)
    if _nums > 1:
        return Concatenate(axis=1)(input_list)
    elif len(input_list) == 1:
        return input_list[0]
    else:
        return None

class LocalActivationUnit(Layer):
    
    def __init__(self, hidden_units=(256, 128, 56), activation='prelu'):
        super(LocalActivationUnit, self).__init__()
        self.hidden_units = hidden_units
        self.linear = Dense(1)
        self.dnn = [Dense(unit, activation=PReLU() if activation == 'prelu' else Dice()) for unit in hidden_units]
        
    def call(self, inputs):
        query, keys = inputs
        
        # 序列長度
        keys_len = keys.get_shape()[1]
        queries = tf.tile(query, multiples=[1, keys_len, 1]) #  B x keys_len x emb_size
        
        # 對特征進行拼接(原始向量罗心、向量差、外積結(jié)果)
        attention_input = tf.concat([queries, keys, queries-keys, queries*keys], axis=-1) # B x keys_len x 4*emb_size
        
        # 將原始向量和
        attention_out = attention_input
        for fc in self.dnn:
            attention_out = fc(attention_out) # B x keys_len x 56
        
        attention_out = self.linear(attention_out) # B x keys_len x 1
        attention_out = tf.squeeze(attention_out, -1) # B x keys_len
        return attention_out
    
class AttentionPoolingLayer(Layer):
    def __init__(self, attention_hidden_units=(256, 128, 56)):
        super(AttentionPoolingLayer, self).__init__()
        self.attention_hidden_units = attention_hidden_units
        # 對輸入做了一層轉(zhuǎn)化
        self.local_attention = LocalActivationUnit(self.attention_hidden_units)
        
    def call(self, inputs):
        # queries: B x 1 x emb_size keys: B x keys_len x emb_size
        queries, keys = inputs 
        
        # pad在計算atten_score也有得分城瞎,這四步的作用是借助padding的embedding為0渤闷,將atten_score中padding部分的興趣分轉(zhuǎn)化為0。
        
        # 獲取行為序列中每個商品對應(yīng)的注意力權(quán)重
        attention_score = self.local_attention([queries, keys])
        
        # 獲取行為序列embedding的mask矩陣全谤,將embedding矩陣非零元素設(shè)置為True
        key_masks = tf.not_equal(keys[:, :, 0], 0) # B x keys_len
        
        # 標記行為序列embedding中無效的位置
        paddings = tf.zeros_like(attention_score) # B x keys_len
        
        outputs = tf.where(key_masks, attention_score, paddings) # B x 1 x keys_len
        outputs = tf.expand_dims(outputs, axis=1) # B x 1 x keys_len
        outputs = tf.matmul(outputs, keys) # B x 1 x emb_size
        outputs = tf.squeeze(outputs, axis=1) # B x emb_size
        return outputs
    
def get_dnn_logits(dnn_input, hidden_units=(200, 80), activation='prelu'):
    dnn_list = [Dense(unit, activation=PReLU() if activation=='prelu' else Dice()) for unit in hidden_units]
    dnn_out = dnn_input
    for dnn in dnn_list:
        dnn_out = dnn(dnn_out)
    
    dnn_logits = Dense(1, activation='sigmoid')(dnn_out)
    return dnn_logits

def DIN(feature_columns, behavior_feature_list, behavior_seq_feature_list):
    """ Deep Interest Network肤晓,采用注意力機制對用戶的興趣動態(tài)模擬 """
    input_layer_dict = build_input_layers(feature_columns)
    input_layers = list(input_layer_dict.values())
    
    # 特征中的sparse和dense特征
    sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeature), feature_columns))
    dense_feature_columns = list(filter(lambda x: isinstance(x, DenseFeature), feature_columns))
    
    # dense
    dnn_dense_input_list = []
    for f in dense_feature_columns:
        dnn_dense_input_list.append(input_layer_dict[f.name])
    
    # dense特征拼接
    concat_dnn_dense_input = concat_input_list(dnn_dense_input_list)
        
    # embedding
    embedding_layer_dict = build_embedding_layers(feature_columns)
    
    dnn_sparse_embed_input = concat_embedding_list(sparse_feature_columns, input_layer_dict, embedding_layer_dict, flatten=True)
    print('dnn_sparse_embed_input: ', dnn_sparse_embed_input)
    concat_dnn_sparse_embed_input = concat_input_list(dnn_sparse_embed_input)
    
    #print('input_layer_dict: {}, embedding_layer_dict: {}'.format(input_layer_dict, embedding_layer_dict))
    
    # 獲取當前物品movie的embedding
    query_embed_list = embedding_lookup(behavior_feature_list, input_layer_dict, embedding_layer_dict)
    # 獲取行為序列的embedding
    key_embed_list = embedding_lookup(behavior_seq_feature_list, input_layer_dict, embedding_layer_dict)
    
    # 使用注意力機制將歷史movie_id進行池化
    dnn_seq_input_list = []
    for i in range(len(key_embed_list)):
        # # B x emb_size
        seq_emb = AttentionPoolingLayer()([query_embed_list[i], key_embed_list[i]])
        dnn_seq_input_list.append(seq_emb)
    
    # 將多個行為序列的attention pooling(當前物品和行為序列物品的互操作求注意力權(quán)重爷贫,興趣分 = 權(quán)重*歷史行為物品的embedding)的embedding進行拼接
    concat_dnn_seq_input_list = concat_input_list(dnn_seq_input_list)
    
    # 將dense特征认然、sparse特征及注意力加權(quán)的序列特征拼接
    print('concat_dnn_dense_input: ', concat_dnn_dense_input)
    print('concat_dnn_sparse_embed_input: ', concat_dnn_sparse_embed_input)
    print('concat_dnn_seq_input_list: ', concat_dnn_seq_input_list)

    dnn_input = Concatenate(axis=1)([concat_dnn_dense_input, concat_dnn_sparse_embed_input, concat_dnn_seq_input_list])
    dnn_logits = get_dnn_logits(dnn_input, activation='prelu')
    model = Model(input_layers, dnn_logits)
    return model

# 特征列
feature_columns = [
    SparseFeature('user_id', data.user_id.max()+1, embedding_size=8),
    SparseFeature('gender', data.gender.max()+1, embedding_size=8),
    SparseFeature('age', data.age.max()+1, embedding_size=8),
    SparseFeature('movie_id', data.movie_id.max()+1, embedding_size=8),
    SparseFeature('movie_type_id', data.movie_type_id.max()+1, embedding_size=8),
    DenseFeature('hist_len', 1),
]

feature_columns += [VarLenSparseFeature('hist_movie_id', 
                                        vocabulary_size=data.movie_id.max()+1, 
                                        embedding_size=8,
                                        maxlen=50)]

#print('feature_columns: ', feature_columns)

# 行為特征列表 行為序列特征列表
behavior_feature_list = ['movie_id']
behavior_seq_feature_list = ['hist_movie_id']
model = DIN(feature_columns, behavior_feature_list, behavior_seq_feature_list)
model.summary()

model.compile(optimizer="adam",
             loss="binary_crossentropy",
             metrics=["binary_crossentropy", tf.keras.metrics.AUC(name='auc')])

model.fit(X_train, y_train,
         batch_size=64, epochs=5, validation_split=0.2)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末补憾,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子卷员,更是在濱河造成了極大的恐慌盈匾,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,378評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毕骡,死亡現(xiàn)場離奇詭異削饵,居然都是意外死亡,警方通過查閱死者的電腦和手機未巫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評論 3 399
  • 文/潘曉璐 我一進店門窿撬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人叙凡,你說我怎么就攤上這事劈伴。” “怎么了握爷?”我有些...
    開封第一講書人閱讀 168,983評論 0 362
  • 文/不壞的土叔 我叫張陵跛璧,是天一觀的道長。 經(jīng)常有香客問我新啼,道長追城,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,938評論 1 299
  • 正文 為了忘掉前任燥撞,我火速辦了婚禮座柱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘叨吮。我一直安慰自己辆布,他們只是感情好,可當我...
    茶點故事閱讀 68,955評論 6 398
  • 文/花漫 我一把揭開白布茶鉴。 她就那樣靜靜地躺著锋玲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪涵叮。 梳的紋絲不亂的頭發(fā)上惭蹂,一...
    開封第一講書人閱讀 52,549評論 1 312
  • 那天,我揣著相機與錄音割粮,去河邊找鬼盾碗。 笑死,一個胖子當著我的面吹牛舀瓢,可吹牛的內(nèi)容都是我干的廷雅。 我是一名探鬼主播,決...
    沈念sama閱讀 41,063評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼航缀!你這毒婦竟也來了商架?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,991評論 0 277
  • 序言:老撾萬榮一對情侶失蹤芥玉,失蹤者是張志新(化名)和其女友劉穎蛇摸,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體灿巧,經(jīng)...
    沈念sama閱讀 46,522評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡赶袄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,604評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了抠藕。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饿肺。...
    茶點故事閱讀 40,742評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖盾似,靈堂內(nèi)的尸體忽然破棺而出唬格,到底是詐尸還是另有隱情,我是刑警寧澤颜说,帶...
    沈念sama閱讀 36,413評論 5 351
  • 正文 年R本政府宣布购岗,位于F島的核電站,受9級特大地震影響门粪,放射性物質(zhì)發(fā)生泄漏喊积。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,094評論 3 335
  • 文/蒙蒙 一玄妈、第九天 我趴在偏房一處隱蔽的房頂上張望乾吻。 院中可真熱鬧,春花似錦拟蜻、人聲如沸绎签。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽诡必。三九已至,卻和暖如春搔扁,著一層夾襖步出監(jiān)牢的瞬間爸舒,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評論 1 274
  • 我被黑心中介騙來泰國打工稿蹲, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留扭勉,地道東北人。 一個月前我還...
    沈念sama閱讀 49,159評論 3 378
  • 正文 我出身青樓苛聘,卻偏偏與公主長得像涂炎,于是被迫代替她去往敵國和親忠聚。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,747評論 2 361

推薦閱讀更多精彩內(nèi)容