天天看點

DSIN 深度 Session 興趣網絡介紹及源碼剖析

DSIN 深度 Session 興趣網絡介紹及源碼剖析

前言(可以忽略~)

本文介紹 DSIN 網絡的基本原理,并對源碼進行詳細分析,從資料預處理,訓練資料生成,模型建構等方面對 DSIN 的完整實作進行詳細介紹。

(PS:好久好久沒有寫文章了,罪過罪過,這段時間發生了太多的事情,似夢如幻,2020 年結尾鐘聲快要敲響之際,平靜終于回歸了我的内心,過去的事情不再留戀,2021 年開啟新的征程。“星空”我望過了,還差的就是腳踏實地。祝新的一年身體健康,萬事如意!)

廣而告之

可以在微信中搜尋 “珍妮的算法之路” 或者 “world4458” 關注我的微信公衆号;另外可以看看知乎專欄 ​​PoorMemory-機器學習​​, 以後文章也會發在知乎專欄中;

文章資訊

  • 論文标題:Deep Session Interest Network for Click-Through Rate Prediction
  • 論文位址:​​https://www.ijcai.org/Proceedings/2019/0319.pdf​​
  • 代碼位址:​​https://github.com/shenweichen/DSIN​​
  • 發表時間: 2019
  • 論文作者: Yufei Feng, Fuyu Lv, Weichen Shen, Menghan Wang, Fei Sun, Yu Zhu, Keping Yang
  • 作者機關: Alibaba

核心觀點

對使用者曆史行為序列進行模組化,阿裡有幾個非常重要的工作,如 DIN (深度興趣網絡,詳見部落格 ​​DIN 深度興趣網絡介紹以及源碼淺析​​) 以及 DIEN (深度興趣進化網絡, 部落格之後完成, Flag~????),然而這些模型并沒有考慮到使用者曆史行為序列中的内在結構,即行為序列可以被劃分為多個 Sessions,Session 之間可以反映出使用者興趣的變化。下圖是将使用者行為進行 Session 劃分的一個例子:

DSIN 深度 Session 興趣網絡介紹及源碼剖析

圖檔表示使用者點選過的商品,圖檔下面的數字為點選時間,Session 的劃分規則是:如果兩個行為之間的間隔超過了 30 分鐘,就劃分一個 Session;(該規則出自 Airbnb 的一篇論文:​​Real-time personalization using embeddings for

search ranking at airbnb​​) 從圖中可以清楚的看到,同一個 Session 内的行為非常相似,而 Session 間的興趣則是多樣的。是以,考慮對 Session 進行模組化,可以更好的捕獲使用者動态變化的興趣。

本文提出的 DSIN 網絡主要包含三個核心組成部分:

  • Session 興趣提取層 (Session Interest Extractor Layer): 将使用者的序列行為劃分為多個 session 後,使用 self-attention 以及 bias-encoding 對每個 session 進行模組化;self-attention 可以捕獲 session 内各個行為之間的内在聯系,進而提取出每個 session 的使用者興趣;
  • Session 興趣互動層 (Session Interest Interacting Layer):提取出使用者的 session 興趣後,session 興趣之間也是存在聯系的,采用 Bi-LSTM(雙向 LSTM) 來捕獲 session 興趣間的變化和演進。
  • Session 興趣激活層 (Session Interest Activating Layer):每個 session 興趣對目标商品的影響不同,采用 Attention 機制刻畫目标商品和每個 session 興趣之間的相關性。

以上是對 DSIN 的概括總結, 下面進行詳細的分析.

核心觀點解讀

DSIN 的網絡結構如下圖所示:

DSIN 深度 Session 興趣網絡介紹及源碼剖析

網絡結構圖稍顯複雜, 我們按照從易到難的順序進行介紹, 大緻可以分為 3 個部分:

  • 左下角部分: 将使用者側和目标商品側的稀疏特征分别轉換為 embedding, 設為和(其中表示使用者側稀疏特征的個數,表示商品側稀疏特征的個數,
  • 右下角部分: DSIN 的核心子產品, 對使用者曆史行為進行 session 劃分, 采用 self-attention 與 Bi-LSTM 來分别捕獲 session 興趣序列和, 并通過 Attention 機制來學習目标商品 (Target Item) 和 session 興趣間的相關性程度, 再對兩個 session 興趣序列分别進行權重求和, 得到聚合後的 session 興趣(使用淺黃色表示) 以及
  • 上半部分: 為經典的 MLP 結構, 輸入為使用者側和商品側的 embedding,以及自适應學習的 session 興趣,

下面對右下角的部分進行拆解分析, 該部分為 DSIN 的核心子產品, 主要由:

  • Session Divsion Layer(Session 劃分層)
  • Session Interest Extractor Layer(Session 興趣提取層)
  • Session Interest Interacting Layer(Session 興趣互動層)
  • Session Interest Activating Layer(Session 興趣激活層)

四個部分構成. 下面按順序依次介紹.

Session 劃分層 (Session Divsion Layer)

對使用者曆史行為序列按照時間順序排序, 如果兩個行為之間的間隔超過了 30 分鐘,就劃分一個 Session; 按該規則将使用者行為序列 劃分為多個 Sessions , 其中第 個 Session 定義為 , 其中 為該 Session 中的行為個數, 表示該 Session 中使用者的第

Session 興趣提取層 (Session Interest Extractor Layer)

劃分完 Session 後, 為了捕獲 Session 内各個行為的内在關系, 在每個 Session 内應用 multi-head self-attention 機制. 為了刻畫 Session 内行為的順序關系, 類似于 Transorformer 中的 positional encoding, DSIN 介紹了 Bias Encoding (其中 為使用者 Session 的個數, 為一個 Session 内的行為個數,

其中

注意 是一個元素值, 表示第 個 Session 内的第 個行為對應的 embedding, 其第 個元素所對應的偏置值 (bias). 另外注意

這裡補充一點, 可能一開始看到這裡的時候會有些疑惑, 比如 這個 Tensor 是如何實作的, 畢竟 是三個次元不相等的向量, 它們無法直接相加. 在代碼實作時, 為大小等于 ​​

​(K, 1, 1)​

​​ 的數組, 為大小等于 ​​

​(1, T, 1)​

​​ 的數組, 而 為大小等于 ​​

​(1, 1, C)​

​​ 的數組, 利用 Broadcast 操作可以将三者給相加, 最終得到大小為

使用者 Session 與 Bias Encoding 相加後, 得到新的表達 輸入到 Multi-Head Self-Attention 子產品中. 設 Head 的個數為 , 那麼對于第 個 Session , 其将被劃分為 份, 即 , 其中 為 的第 個 Head, 其中 .

其中

其中 也為線性矩陣, . 之後對該 Session 中的

此時 .

Session 興趣互動層 (Session Interest Interacting Layer)

為了進一步捕獲 Session 興趣間的變化和演進, 作者采用了 Bi-LSTM 模型. 其公式化如下:

其中 為 sigmoid 函數, 分别為 input gate, forget gate, output gate 以及 cell vectors, 它們的大小和 一緻. Bi-LSTM 的隐層狀态

其中 表示前向 LSTM 的隐層狀态而

Session 興趣激活層 (Session Interest Activating Layer)

在得到 Session 興趣序列後, 由于每個 session 興趣對目标商品的影響不同,這裡采用 Attention 機制來刻畫目标商品和每個 session 興趣之間的相關性。

前面介紹了使用 Multi-Head Self-Attention 與 Bi-LSTM 分别捕獲了 session 興趣序列 和 , 目标商品分别與兩個 Session 興趣序列進行 Attention 的結果為:

其中 為目标商品對應的 embedding; 最後, 将 Attention 得到的結果 , , 目标商品 embedding 以及使用者側 embedding

源碼分析

下面對 DSIN 的源碼進行分析,了解其資料處理,模型建構、訓練等實作細節,進而加深對論文核心觀點的了解。要解讀的源碼位址為:​​https://github.com/shenweichen/DSIN​​

資料集介紹

DSIN 代碼處理的資料集 ​​Ad Display/Click Data on Taobao.com​​ 是阿裡巴巴提供的一個淘寶展示廣告點選率預估資料集。其主要内容如下:

DSIN 深度 Session 興趣網絡介紹及源碼剖析

說明:

  • ​raw_sample​

    ​:原始的樣本骨架,應該是從展現點選日志中擷取的資料,描述了使用者與曝光商品(廣告)之間的關系,比如是否發生了點選等;
  • ​ad_feature​

    ​: 描述了廣告的基本資訊,比如廣告 ID,廣告計劃 ID,品牌等;
  • ​user_profile​

    ​: 描述了使用者的基本資訊,如使用者 ID, 年齡,性别等;
  • ​raw_behavior_log​

    ​: 使用者的行為日志,描述了使用者的曆史行為,行為類型主要包含浏覽、購買、加購、喜歡(收藏);

資料預處理 – 采樣使用者

代碼位址:​​https://github.com/shenweichen/DSIN/blob/master/code/0_gen_sampled_data.py​​

作者貼心地在給代碼檔案命名時加上了 ​

​0_​

​​, ​

​1_​

​​ 之類的字首,表明了代碼的執行順序,首先我們需要執行 ​

​0_gen_sampled_data.py​

​ 對使用者進行采樣。代碼中很多内容是檔案的讀取,下面我隻截取出比較核心的部分:

  1. 采樣使用者 (詳情見注釋)

基本資訊讀取,并對使用者進行下采樣,曆史行為隻考慮浏覽行為。

## 讀取使用者資訊表和原始樣本骨架
user = pd.read_csv('../raw_data/user_profile.csv')
sample = pd.read_csv('../raw_data/raw_sample.csv')

## FRAC 為采樣率,對使用者進行下采樣;原始樣本骨架隻考慮采樣後的使用者
if FRAC < 1.0:
    user_sub = user.sample(frac=FRAC, random_state=1024)
else:
    user_sub = user
sample_sub = sample.loc[sample.user.isin(user_sub.userid.unique())]

## 讀取使用者曆史行為,這裡隻考慮浏覽行為(log['btag'] == 'pv', 其中 'pv' 表示浏覽)
## 曆史行為也隻考慮采樣後的使用者
log = pd.read_csv('../raw_data/behavior_log.csv')
log = log.loc[log['btag'] == 'pv']
userset = user_sub.userid.unique()
log = log.loc[log.user.isin(userset)]

## 讀取廣告基本資訊
ad = pd.read_csv('../raw_data/ad_feature.csv')
ad['brand'] = ad['brand'].fillna(-1)      
  1. 使用者行為編碼

分别對 ​

​cate_id​

​​ 和 ​

​brand​

​​ 進行編碼,注意編碼從 ​

​1​

​​ 開始計數,這是因為後續會劃分 Session,每個 Session 内的行為個數是不同的,為了讓 Session 内的行為數為一固定值,那麼行為數不足的 Session 就需要補 ​

​0​

​​,這些 ​

​0​

​​ 可以表示無行為;是以為了友善區分,可以從 ​

​1​

​ 開始計數。

from sklearn.preprocessing import LabelEncoder

## 分别對 cate_id 和 brand 進行編碼
lbe = LabelEncoder()
unique_cate_id = np.concatenate(
    (ad['cate_id'].unique(), log['cate'].unique()))
lbe.fit(unique_cate_id)
ad['cate_id'] = lbe.transform(ad['cate_id']) + 1
log['cate'] = lbe.transform(log['cate']) + 1

lbe = LabelEncoder()
unique_brand = np.concatenate(
    (ad['brand'].unique(), log['brand'].unique()))
lbe.fit(unique_brand)
ad['brand'] = lbe.transform(ad['brand']) + 1
log['brand'] = lbe.transform(log['brand']) + 1      

檔案剩餘的内容就是将處理後的資料進行儲存。OK,現在總結一下該檔案的作用:

  • 資料處理, 對使用者資訊表(​

    ​user_profile​

    ​​)中的使用者進行采樣, 并從原始樣本骨架(​

    ​raw_sample​

    ​)中擷取這部分使用者對廣告的回報(是否點選等);
  • 之後結合廣告基本資訊表 (​

    ​ad_feature​

    ​​) 擷取廣告的基本資訊, 主要是​

    ​brand​

    ​​(品牌) 以及​

    ​cate_id​

    ​(商品類目) 資訊;
  • 接下來結合使用者行為日志 (​

    ​raw_behavior_log​

    ​​), 擷取使用者的曆史浏覽行為(代碼隻考慮浏覽行為, 其他三種行為, 如: 加購、購買、喜歡 暫不考慮), 并對曆史行為進行編碼,浏覽行為用​

    ​(cate_id, brand, time_stamp)​

    ​ 來表示.

資料預處理 – Session 劃分

代碼位置:​​https://github.com/shenweichen/DSIN/blob/master/code/1_gen_sessions.py​​

在 ​

​0_gen_sample_data.py​

​​ 中完成了對使用者的采樣以及曆史行為的預處理,​

​1_gen_sessions.py​

​ 檔案采用多程序的方式對這部分使用者的曆史行為進行 Session 劃分;

gen_user_hist_sessions 運作入口

函數中我會删去不需要特意分析的代碼;

def gen_user_hist_sessions(model, FRAC=0.25):
    """
    在 0_gen_sample_data.py 中完成了對使用者的采樣, 這會采用多程序的方式對這部分使用者的曆史行為進行 session 的劃分; session 的劃分标準是: 如果一個使用者的前後兩次行為的時間差 > 30min, 那麼就劃分一個 session; 此外如果一個 session 内的行為數不超過 2 個, 該 session 就不保留了.
    """
    
    ## 讀取使用者曆史行為,隻保留 0503 ~ 0513 這段時間範圍内的資料
    print("gen " + model + " hist sess", FRAC)
    name = '../sampled_data/behavior_log_pv_user_filter_enc_' + str(FRAC) + '.pkl'
    data = pd.read_pickle(name)
    data = data.loc[data.time_stamp >= 1493769600]  # 0503-0513
    # 0504~1493856000
    # 0503 1493769600
    
    ## 讀取采樣後的使用者資訊
    user = pd.read_pickle('../sampled_data/user_profile_' + str(FRAC) + '.pkl')

    ## 使用者數量較多,采用多程序的方式來進行處理,
    n_samples = user.shape[0]
    batch_size = 150000
    iters = (n_samples - 1) // batch_size + 1

    for i in range(0, iters):
        target_user = user['userid'].values[i * batch_size:(i + 1) * batch_size]
        sub_data = data.loc[data.user.isin(target_user)]
        
        ## 對使用者進行分組聚合,将同一個使用者的所有行為聚合在一起,然後再對這個
        ## 行為清單進行排序以及 Session 劃分,這部分邏輯在 gen_session_list_dsin
        ## 函數中完成,調用 applyParallel 函數實作多程序處理
        df_grouped = sub_data.groupby('user')
        user_hist_session = applyParallel(
            df_grouped, gen_session_list_dsin, n_jobs=20, backend='multiprocessing')


    print("1_gen " + model + " hist sess done")      

gen_session_list_dsin 實作劃分 Session 的邏輯

根據使用者的曆史行為來劃分 session, 按照前後兩次行為的時間間隔是否超過 30min 來進行 session 的劃分, 另外隻保留行為數超過 2 個的 sessions.

def gen_session_list_dsin(uid, t):
    """
    根據使用者的曆史行為來劃分 session, 按照前後兩次行為的時間間隔是否超過 30min 來進行 session 的劃分, 另外隻保留行為數超過 2 個的 sessions
    """
    ## 對使用者行為序列 t 按時間從小到大排序,也就是說,近期的行為排在後面,而較老的行為排在前面
    t.sort_values('time_stamp', inplace=True, ascending=True)
    last_time = 1483574401  # pd.to_datetime("2017-01-05 00:00:01")
    session_list = []
    session = []
    ## row[0] 為樣本在表格中的序号, row[1] 為 pandas.Series, 裡面的内容包括
    ## (user, time_stamp, cate, brand)
    ## 下面的邏輯是計算前後兩個行為之間的時間誤差,如果 delta > 30min, 那麼就劃分一個 Session;此外,如果該 Session 中的行為數不多于 2 個,那麼就丢棄該 Session.
    for row in t.iterrows():
        time_stamp = row[1]['time_stamp']
        delta = time_stamp - last_time  ## 計算和上一個行為的時間誤差
        cate_id = row[1]['cate']
        brand = row[1]['brand']
        if delta > 30 * 60:  # Session begin when current behavior and the last behavior are separated by more than 30 minutes.
            if len(session) > 2:  # Only use sessions that have >2 behaviors
                session_list.append(session[:])
            session = []

        session.append((cate_id, brand, time_stamp))
        last_time = time_stamp
    if len(session) > 2:
        session_list.append(session[:])
    return uid,      

函數最後傳回 ​

​user_id​

​​ 以及 ​

​session_list​

​​, 行為使用 ​

​(cate_id, brand, timestamp)​

​​ 來表示,那麼 ​

​session_list​

​ 可以表示為:

session_list = [
    [(c11, b11, t11), (c12, b12, t12), ...],
    [(c21, b21, t21), (c22, b22, t22), ...],
    .......
]      

産生模型訓練資料

代碼位置:​​https://github.com/shenweichen/DSIN/blob/master/code/2_gen_dsin_input.py​​

之後運作 ​

​2_gen_dsin_input.py​

​ 來産生模型所需要的訓練資料。這部分代碼相對比較複雜,下面将代碼拆分,對各個部分進行分析。

  1. 讀取​

    ​1_gen_session.py​

    ​​ 産生的使用者 session 檔案,并統一儲存到​

    ​user_hist_session​

    ​ 字典中。
user_hist_session = {}
FILE_NUM = len(
    list(filter(lambda x: x.startswith('user_hist_session_' + str(FRAC) + '_dsin_'),
                os.listdir('../sampled_data/'))))

print('total', FILE_NUM, 'files')

for i in range(FILE_NUM):
    """
    在 1_gen_sessions.py 中, 劃分完 session 後, 最終使用字典來保留 {user_id: session_list}, 即每個使用者對應的 session 序列;
    這裡将所有使用者的 session 序列統一儲存到 user_hist_session 中
    """
    user_hist_session_ = pd.read_pickle(
        '../sampled_data/user_hist_session_' + str(FRAC) + '_dsin_' + str(i) + '.pkl')  # 19,34
    user_hist_session.update(user_hist_session_)
    del      
  1. 擷取采樣後的使用者, 儲存到​

    ​sample_sub​

    ​ 中
sample_sub = pd.read_pickle(
        '../sampled_data/raw_sample_' + str(FRAC) + '.pkl')      
  1. 将​

    ​sample_sub​

    ​​ 中的所有使用者的 session 彙總,儲存到​

    ​sess_input_dict​

    ​​ 中,每個使用者隻保留​

    ​DSIN_SESS_COUNT = 5​

    ​​ (定義在​

    ​config.py​

    ​​ 檔案中)個 Session. 另外使用​

    ​sess_input_length_dict​

    ​​ 儲存每個 session 的真實行為個數,因為在後面的處理過程中,會存在将 session 截斷或補 0 的操作,使所有 Session 的長度統一為​

    ​DSIN_SESS_MAX_LEN = 10​

    ​.
sess_input_dict = {}
sess_input_length_dict = {}
for i in range(SESS_COUNT):
    sess_input_dict['sess_' + str(i)] = {'cate_id': [], 'brand': []}
    sess_input_length_dict['sess_' + str(i)] = []

sess_length_list = []
for row in tqdm(sample_sub[['user', 'time_stamp']].iterrows()):
    sess_input_dict_, sess_input_length_dict_, sess_length = gen_sess_feature_dsin(
        row)
    for i in range(SESS_COUNT):
        sess_name = 'sess_' + str(i)
        sess_input_dict[sess_name]['cate_id'].append(
            sess_input_dict_[sess_name]['cate_id'])
        sess_input_dict[sess_name]['brand'].append(
            sess_input_dict_[sess_name]['brand'])
        sess_input_length_dict[sess_name].append(
            sess_input_length_dict_[sess_name])
    sess_length_list.append(sess_length)      

其中 ​

​gen_sess_feature_dsin​

​​ 函數得到每個使用者的 ​

​SESS_COUNT = 5​

​​ 個 Sessions,并儲存在 ​

​sess_input_dict_​

​​ 中,而 ​

​sess_input_dict​

​​ 用于彙總所有使用者的 Sessions。最後得到的 ​

​sess_input_dict​

​ 形式如下:

## c: cate_id, b: brand
## 假設使用者數為 k 個, 每個使用者保留 5 個 Session。
## 由于 Session 内的行為數不固定,是以 m,n,q 不一定相等
sess_input_dict = {
    'sess_0': {
        'cate_id': [[c11, c12, ..., c1m], ## sess_0 中使用者1 有 m 個行為
                    [c21, c22, ..., c2n], ## 使用者 2 有 n 個行為
                    ...,
                    [ck1, ck2, ..., ckq]], ## 使用者 k 有 q 個行為
        'brand'  : [[b11, b12, ..., b1m],
                    [b21, b22, ..., b2n],
                    ...,
                    [bk1, bk2, ..., bkq]],
    },
    'sess_1': {
        ......
    },
    ....., 
    'sess_5': {
        .....
    },
}      

下面再詳細介紹 ​

​gen_sess_feature_dsin​

​ 函數:

其内容主要是從一個使用者的 ​

​session_list​

​ 中保留最近的 5 個 Session,同時要保證 Session 内的行為時間不超過使用者和廣告發生互動的時間 (否則就不叫曆史行為了…)

def gen_sess_feature_dsin(row):
    """
    row 中儲存着一個使用者的 ID 以及對廣告進行回報的時間 time_stamp,
    是以在下面的進行中,主要目的是将 time_stamp 之前的行為保留,
    同時對每個使用者隻保留最近的 5 (DSIN_SESS_COUNT)個 Session
    """
    sess_count = DSIN_SESS_COUNT  ## 5
    sess_max_len = DSIN_SESS_MAX_LEN  ## 10,該函數沒有用到這個變量
    sess_input_dict = {}
    sess_input_length_dict = {}
    for i in range(sess_count):
        sess_input_dict['sess_' + str(i)] = {'cate_id': [], 'brand': []}
        sess_input_length_dict['sess_' + str(i)] = 0
    sess_length = 0
    user, time_stamp = row[1]['user'], row[1]['time_stamp'] ## time_stamp 是使用者對廣告的回報時間, 曆史行為的時間應該要小于這個時間
    # 邊界情況處理
    if user not in user_hist_session:
        for i in range(sess_count):
            sess_input_dict['sess_' + str(i)]['cate_id'] = [0]
            sess_input_dict['sess_' + str(i)]['brand'] = [0]
            sess_input_length_dict['sess_' + str(i)] = 0
        sess_length = 0
    else: # 核心邏輯
        ## 先确定 sess_0 的結果,再确定 sess_1 ~ sess_4 的結果
        valid_sess_count = 0
        last_sess_idx = len(user_hist_session[user]) - 1
        for i in reversed(range(len(user_hist_session[user]))): ## 從最新的 session 開始處理
            cur_sess = user_hist_session[user][i]  ## 使用者 user 的第 i 個 session, [(cate_id, brand, timestamp), ....]
            if cur_sess[0][2] < time_stamp: ## cur_sess[0][2] 表示第一個行為的行為時間, 需要小于 time_stamp
                in_sess_count = 1
                for j in range(1, len(cur_sess)): ## 這個session中其他行為的時間也應該小于 time_stamp, in_sess_count 統計這個 session 内小于 time_stamp 的行為數
                    if cur_sess[j][2] < time_stamp:
                        in_sess_count += 1
                if in_sess_count > 2:
                    ## 取該 session 中最近的 sess_max_len(10個) 個行為, 如果 session 内的行為個數(此外還要滿足時間<time_stamp這個條件)少于 10 個,
                    ## 那麼 index 範圍為 [0, in_sess_count];
                    ## 如果 session 中行為數較多, 那麼取時間最新的, 行為用 (cate_id, brand, timestamp) 表示, e[0] 表示 cate_id, e[1] 表示 brand
                    sess_input_dict['sess_0']['cate_id'] = [e[0] for e in cur_sess[max(0,
                                                                                       in_sess_count - sess_max_len):in_sess_count]]
                    sess_input_dict['sess_0']['brand'] = [e[1] for e in
                                                          cur_sess[max(0, in_sess_count - sess_max_len):in_sess_count]]
                    sess_input_length_dict['sess_0'] = min(
                        sess_max_len, in_sess_count)
                    last_sess_idx = i
                    valid_sess_count += 1
                    break
        ## 上一段代碼得到最新的 session 作為 sess_0, 下面依次擷取 sess_1 ~ sess_4
        for i in range(1, sess_count):
            if last_sess_idx - i >= 0: ## 一個 session 内至少有兩個行為, 在 1_gen_sessions.py 中有這樣的設定
                cur_sess = user_hist_session[user][last_sess_idx - i]
                ## 這裡擷取 session 内行為的代碼使用簡便的 cur_sess[-sess_max_len:], 而上一段代碼使用複雜的 
                ## cur_sess[max(0, in_sess_count - sess_max_len):in_sess_count], 是因為第一個 session 要考慮
                ## 行為的時間不能超過 time_stamp, 但這裡的 session 中所有行為的時間都小于 time_stamp, 是以直接用 cur_sess[-sess_max_len:]
                ## 處理即可
                sess_input_dict['sess_' + str(i)]['cate_id'] = [e[0]
                                                                for e in cur_sess[-sess_max_len:]]
                sess_input_dict['sess_' + str(i)]['brand'] = [e[1]
                                                              for e in cur_sess[-sess_max_len:]]
                sess_input_length_dict['sess_' +
                                       str(i)] = min(sess_max_len, len(cur_sess))
                valid_sess_count += 1
            else:  ## 如果使用者的 session 個數比較少, 預設用 0 表示
                sess_input_dict['sess_' + str(i)]['cate_id'] = [0]
                sess_input_dict['sess_' + str(i)]['brand'] = [0]
                sess_input_length_dict['sess_' + str(i)] = 0

        sess_length = valid_sess_count  ## valid_sess_count 記錄有效的 session 長度
    ## sess_input_length_dict 用于記錄每個 session 真實的行為長度
    return sess_input_dict, sess_input_length_dict,      

由于 ​

​gen_sess_feature_dsin​

​​ 函數隻處理一個使用者的 ​

​session_list​

​​, 是以其傳回結果 ​

​sess_input_dict​

​ 形如:

sess_input_dict = {
    'sess_0': {
        'cate_id': [c1, c2, ..., cm],
        'brand'  : [b1, b2, ..., bm],
    },
    'sess_1': {
        ......
    },
    ....., 
    'sess_5': {
        .....
    },
}      
  1. 繼續介紹主邏輯:下一步讀取使用者資訊以及廣告資訊,并和原始樣本骨架表(raw_sample) 三表進行關聯:
user = pd.read_pickle('../sampled_data/user_profile_' + str(FRAC) + '.pkl')
ad = pd.read_pickle('../sampled_data/ad_feature_enc_' + str(FRAC) + '.pkl')
user = user.fillna(-1)
user.rename(
    columns={'new_user_class_level ': 'new_user_class_level'}, inplace=True)

sample_sub = pd.read_pickle(
    '../sampled_data/raw_sample_' + str(FRAC) + '.pkl')
sample_sub.rename(columns={'user': 'userid'}, inplace=True)

## sample_sub 描述了使用者和廣告的關聯, 要得到 user_id 本身的資訊以及 ad 的資訊, 需要用
## sample_sub 去關聯 user 和 ad 兩個表
data = pd.merge(sample_sub, user, how='left', on='userid', )
data = pd.merge(data, ad, how='left', on='adgroup_id')      

關聯後的結果儲存到 ​

​data​

​ 變量中.

  1. 将稀疏特征和稠密特征都轉化為 ID,并記錄各個特征空間的大小:
## 這裡 sparse_features 大小為 13
sparse_features = ['userid', 'adgroup_id', 'pid', 'cms_segid', 'cms_group_id', 'final_gender_code', 'age_level',
                       'pvalue_level', 'shopping_level', 'occupation', 'new_user_class_level', 'campaign_id',
                       'customer']

dense_features = ['price']

## 轉換為 id; 
for feat in tqdm(sparse_features):
    lbe = LabelEncoder()  # or Hash
    data[feat] = lbe.fit_transform(data[feat])
mms = StandardScaler()
data[dense_features] = mms.fit_transform(data[dense_features])

## 記錄特征空間的大小; SingleFeat 就是一個 namedtuple, 用于記錄特征的基本資訊
## sparse_feature_list 大小為 13 + 2 = 15
sparse_feature_list = [SingleFeat(feat, data[feat].nunique(
) + 1) for feat in sparse_features + ['cate_id', 'brand']]
dense_feature_list = [SingleFeat(feat, 1) for feat in dense_features]      

加上 ​

​cate_id​

​​ 和 ​

​brand​

​​ 的話,總共 15 個稀疏特征,以及 1 個稠密特征。注意代碼中使用 ​

​sparse_feature_list​

​​ 和 ​

​dense_feature_list​

​ 記錄了各個稀疏和稠密特征的個數,用于後續建構 Embedding Layer (設定 Embedding Layer 的大小)以及 Hash 等。

  1. 将所有使用者各個 Session 中的行為個數統一限制為​

    ​DSIN_SESS_MAX_LEN​

    ​ (10個),如果行為個數不足 10 個,那麼進行補零操作:
from tensorflow.python.keras.preprocessing.sequence import pad_sequences

sess_feature = ['cate_id', 'brand']
sess_input = []
sess_input_length = []
## 使用 pad_sequences 對所有 session 進行補 0 操作, 使得所有 session 的長度均為 DSIN_SESS_MAX_LEN
## sess_input 為一個大小為 SESS_COUNT * len(sess_feature) 的 list, 裡面的元素為 shape 為 (number_of_user, DSIN_SESS_MAX_LEN) 的 numpy 數組. 
## SESS_COUNT=5, sess_feature = [cateid, brand]
for i in tqdm(range(SESS_COUNT)):
    sess_name = 'sess_' + str(i)
    for feat in sess_feature:
        sess_input.append(pad_sequences(
            sess_input_dict[sess_name][feat], maxlen=DSIN_SESS_MAX_LEN, padding='post'))
    sess_input_length.append(sess_input_length_dict[sess_name])      
  1. 構造 DSIN 的輸入資料:
## model_input 中每個元素為 shape=(number_of_user,) 的 numpy 數組, model_input 大小為 len(sparse_feature_list) + len(dense_feature_list);
    ## sess_input 為一個大小為 SESS_COUNT * len(sess_feature) 的 list, 裡面的元素為 shape 為 (number_of_user, DSIN_SESS_MAX_LEN) 的 numpy 數組
    ## np.array(sess_length_list) 為 shape=(number_of_user,) 的 numpy 數組 
model_input = [data[feat.name].values for feat in sparse_feature_list] + \
              [data[feat.name].values for feat in dense_feature_list]
sess_lists = sess_input + [np.array(sess_length_list)]
model_input +=      

​model_input​

​​ 是一個 list,裡面儲存着各種 numpy 數組:(注: 圖中的 應該等于 ​​

​sample_sub​

​ 的行數)

DSIN 深度 Session 興趣網絡介紹及源碼剖析

​model_input​

​​ 的大小為 27,其中包括 15 個稀疏特征,1 個稠密特征,5 個 Session,每個 Session 内用 ​

​cate_id​

​​ 和 ​

​brand​

​ 表示行為,是以有 5 x 2 個元素,最後再加上 1 個數組表示每個 Session 内真實的行為長度,是以總大小是 15 + 1 + 5 x 2 + 1 = 27.

最後将輸入資料,label 以及特征空間大小儲存下來,用于後續訓練模型。

if not os.path.exists('../model_input/'):
        os.mkdir('../model_input/')

pd.to_pickle(model_input, '../model_input/dsin_input_' +
             str(FRAC) + '_' + str(SESS_COUNT) + '.pkl')
pd.to_pickle(data['clk'].values, '../model_input/dsin_label_' +
             str(FRAC) + '_' + str(SESS_COUNT) + '.pkl')
## 将稀疏特征和稠密特征的空間大小使用字典儲存
pd.to_pickle({'sparse': sparse_feature_list, 'dense': dense_feature_list},
             '../model_input/dsin_fd_' + str(FRAC) + '_' + str(SESS_COUNT) + '.pkl')
print("gen dsin input done")      

介紹完資料預處理的部分,下面介紹模型訓練代碼。

模型訓練代碼介紹

代碼位于:​​https://github.com/shenweichen/DSIN/blob/master/code/train_dsin.py​​

資料預進行中得到的 ​

​model_input​

​ 包含了所有的資料,是以首先需要劃分訓練集和測試集:

SESS_COUNT = DSIN_SESS_COUNT  ## 5, 每個使用者的 Session 個數
SESS_MAX_LEN = DSIN_SESS_MAX_LEN  ## 10,每個 Session 中的行為個數

fd = pd.read_pickle('../model_input/dsin_fd_' +
                    str(FRAC) + '_' + str(SESS_COUNT) + '.pkl')
model_input = pd.read_pickle(
    '../model_input/dsin_input_' + str(FRAC) + '_' + str(SESS_COUNT) + '.pkl')
label = pd.read_pickle('../model_input/dsin_label_' +
                       str(FRAC) + '_' + str(SESS_COUNT) + '.pkl')

sample_sub = pd.read_pickle(
    '../sampled_data/raw_sample_' + str(FRAC) + '.pkl')
## 增加 idx 列,表示樣本的索引,用于後續訓練集和測試集的劃分
sample_sub['idx'] = list(range(sample_sub.shape[0]))

## 按時間,将使用者與廣告發生互動的時間 < 1494633600 的樣本當作訓練集,
## 其他作為測試集,首先擷取到這些樣本的索引
train_idx = sample_sub.loc[sample_sub.time_stamp <
                           1494633600, 'idx'].values
test_idx = sample_sub.loc[sample_sub.time_stamp >=
                          1494633600, 'idx'].values      

得到索引後,再劃分模型的輸入資料和 label 等:

## 輸入也進行劃分, 下面兩行代碼中的 i 均為 numpy 數組
train_input = [i[train_idx] for i in model_input]
test_input = [i[test_idx] for i in model_input]

train_label = label[train_idx]
test_label = label[test_idx]      

模型訓練和預估, ​

​loss​

​​ 采用 ​

​binary_crossentropy​

​​, 優化方法選擇 ​

​adagrad​

​​. 重點在 ​

​DSIN​

​ 模型。

sess_count = SESS_COUNT
sess_len_max = SESS_MAX_LEN
BATCH_SIZE = 4096

sess_feature = ['cate_id', 'brand']
TEST_BATCH_SIZE = 2 ** 14

## DSIN 的建構依賴 deepctr 庫
model = DSIN(fd, sess_feature, embedding_size=4, sess_max_count=sess_count,
             sess_len_max=sess_len_max, dnn_hidden_units=(200, 80), att_head_num=8,
             att_embedding_size=1, bias_encoding=False)

model.compile('adagrad', 'binary_crossentropy',
              metrics=['binary_crossentropy', ])

hist_ = model.fit(train_input, train_label, batch_size=BATCH_SIZE,
                  epochs=1, initial_epoch=0, verbose=1, )

pred_ans = model.predict(test_input, TEST_BATCH_SIZE)

print()
print("test LogLoss", round(log_loss(test_label, pred_ans), 4), "test AUC",
      round(roc_auc_score(test_label, pred_ans), 4))      

下面詳細介紹 ​

​DSIN​

​ 模型。

DSIN 模型代碼介紹

代碼位置:​​https://github.com/shenweichen/DSIN/blob/master/code/models/dsin.py​​

​DSIN​

​​ 的建構依賴 ​

​deepctr​

​​ 庫,注意一開始運作代碼之前,使用 ​

​pip install -r requirements.txt​

​​ 安裝必要的依賴項,​

​deepctr​

​​ 庫最新版本很多接口發生了變化,是以最好是安裝 ​

​requirements.txt​

​ 中指定的版本。

作者給輸入參數添加了詳細的說明,我再介紹一下:

def DSIN(feature_dim_dict, sess_feature_list, embedding_size=8, sess_max_count=5, sess_len_max=10, bias_encoding=False,
         att_embedding_size=1, att_head_num=8, dnn_hidden_units=(200, 80), dnn_activation='sigmoid', dnn_dropout=0,
         dnn_use_bn=False, l2_reg_dnn=0, l2_reg_embedding=1e-6, init_std=0.0001, seed=1024, task='binary',
         ):
    """Instantiates the Deep Session Interest Network architecture.

    :param feature_dim_dict: 類似 {'sparse':{'field_1':4,'field_2':3,'field_3':2},'dense':[]} 這樣的字典,指明了每個特征空間的大小
    :param sess_feature_list: 傳入的是 ['cate_id', 'brand'], 用這些表示使用者行為
    :param embedding_size: 稀疏特征 embedding size 的大小
    :param sess_max_count: 每個使用者最大的 Session 個數,為 5
    :param sess_len_max: 每個 Session 中的行為個數,為 10
    :param bias_encoding: bool 值,是否使用 bias encoding
    :param att_embedding_size: the embedding size of each attention head
    :param att_head_num: attention head 的個數
    :param dnn_hidden_units: list, dnn 網絡每一隐層的節點個數
    :param dnn_activation: dnn 中每一層的激活函數
    :param dnn_dropout: float in [0,1), the probability we will drop out a given DNN coordinate.
    :param dnn_use_bn: bool. Whether use BatchNormalization before activation or not in deep net
    :param l2_reg_dnn: float. L2 regularizer strength applied to DNN
    :param l2_reg_embedding: float. L2 regularizer strength applied to embedding vector
    :param init_std: float,to use as the initialize std of embedding vector
    :param seed: 随機種子
    :param task: str, ``"binary"`` for  binary logloss or  ``"regression"`` for regression loss
    :return: A Keras model instance.
    """      

在 ​

​train_dsin.py​

​ 檔案中傳入到該函數的參數中,需要注意的有:

sess_feature=['cate_id', 'brand'] 
embedding_size=4
sess_max_count=sess_count  ## 5
sess_len_max=sess_len_max  ## 10
att_head_num=8
att_embedding_size=1      

注意特征的 embedding size 為 ​

​4​

​.

代碼首先進行數值判斷,要保證 Multi-Head Self-Attention 的輸出結果 embedding 的次元和輸入是相同的。首先,每個稀疏特征都會被映射為大小等于 ​

​embedding_size​

​​ 的向量,而一個行為使用 ​

​(cate_id, brand)​

​​ 來表示,該行為對應的 embedding 大小是這兩個稀疏特征對應的 embedding 向量進行 concatenation 得到的,即行為的 embedding 大小為 ​

​2 * embedding_size​

​​ (​

​len(sess_feature_list) == 2​

​)

if (att_embedding_size * att_head_num != len(sess_feature_list) * embedding_size):
        raise ValueError(
            "len(session_feature_lsit) * embedding_size must equal to att_embedding_size * att_head_num ,got %d * %d != %d *%d" % (
            len(sess_feature_list), embedding_size, att_embedding_size, att_head_num))      

擷取模型的輸入:

## sparse_input: sparse_input 是大小為 15 的字典, key 有 userid, group_id 等特征, 而元素均為 shape=[B, 1] 的 tensor,表示每個特征的 id
## user_behavior_input_dict 為大小為 sess_max_count 的字典,key 為 sess_0 ~ sess_1, 每個 session 包含 brand/cid 兩種類型的序列
## user_sess_length 表示真實 Session 的長度,雖然每個使用者最後都劃分了 5 個 Session,
## 但并不是每個使用者都有 5 個 Session
sparse_input, dense_input, user_behavior_input_dict, _, user_sess_length = get_input(
    feature_dim_dict, sess_feature_list, sess_max_count, sess_len_max)

"""
-- 中途插入 --
其中 get_input 定義如下,我把代碼直接貼在這裡,友善閱讀
"""
from tensorflow.python.keras.layers import Input
def get_input(feature_dim_dict, seq_feature_list, sess_max_count, seq_max_len):
    sparse_input, dense_input = create_singlefeat_inputdict(feature_dim_dict)
    user_behavior_input = {}
    for idx in range(sess_max_count):
        sess_input = OrderedDict()
        for i, feat in enumerate(seq_feature_list):
            sess_input[feat] = Input(
                shape=(seq_max_len,), name='seq_' + str(idx) + str(i) + '-' + feat)

        user_behavior_input["sess_" + str(idx)] = sess_input

    user_behavior_length = {"sess_" + str(idx): Input(shape=(1,), name='seq_length' + str(idx)) for idx in
                            range(sess_max_count)}
    user_sess_length = Input(shape=(1,), name='sess_length')

    return sparse_input, dense_input, user_behavior_input, user_behavior_length,      

上面代碼擷取的輸入(​

​Input​

​​) 有 ​

​sparse_input, dense_input, user_behavior_input_dict, user_sess_length​

​​, 為了保證我們思維的連續性,我們先暫時略過 ​

​DSIN​

​​ 函數中間部分的細節,直接跳到該函數的末尾部分,看看模型完整的輸入是如何建構的,這樣可以和前面講過的​

​model_input​

​ (資料預處理那一節)結合起來:

"""
我們直接跳到 DSIN 函數最後的部分,看看模型完整的輸入

get_inputs_list 函數大概的作用是将 dict/OrderedDict 轉換成 list 輸出
"""
sess_input_list = []
for i in range(sess_max_count):
    sess_name = "sess_" + str(i)
    sess_input_list.extend(get_inputs_list(
        [user_behavior_input_dict[sess_name]]))

model_input_list = get_inputs_list([sparse_input, dense_input]) + sess_input_list + [
    user_sess_length]

model = Model(inputs=model_input_list, outputs=output)

return      

我們可以看到 ​

​model_input_list​

​​ 這個變量,它包含的内容剛好是 ​

​[sparse_input, dense_input] + sess_input_list + [user_sess_length]​

​, 而如果将它們都展開來看的話,正是:

## sparse_input 大小為 15
sparse_input = [
    Input(shape=(1,), name='user_id', dtype),
    Input(shape=(1,), name='adgroup_id', dtype),
    ......,
    Input(shape=(1,), name='cate_id', dtype),
    Input(shape=(1,), name='brand', dtype),
]
## dense_input 大小為 1
dense_input = [
    Input(shape=(1,), name='price', dtype),
]
## sess_input_list 大小為 len(sess_feature_list) * SESS_COUNT = 2 * 5 = 10
## seq_max_len == 10
sess_input_list = [
    Input(shape=(seq_max_len,), name='seq_00-cate_id'),
    Input(shape=(seq_max_len,), name='seq_01-brand'),
    ......,
    Input(shape=(seq_max_len,), name='seq_40-cate_id'),
    Input(shape=(seq_max_len,), name='seq_41-brand'),
]
## user_sess_length 大小為 1,表示真實 Session 的長度
user_sess_length = [
    Input(shape=(1,), name='sess_length')
]      

如果把上面的所有 ​

​Input​

​ 按順序來看,正好可以和下圖對應!

DSIN 深度 Session 興趣網絡介紹及源碼剖析

​model_input​

​​ 中的資料就通過 ​

​Input​

​ 輸入到模型中了。

下面我們回到模型代碼。擷取輸入資料後,由于輸入的是各種特征對應的 ID 值,首先要将它們轉換為稠密的 embedding 向量:

from tensorflow.python.keras.layers import (Concatenate, Dense, Embedding,
                                            Flatten, Input)
## sparse_embedding_dict 儲存每個稀疏特征對應的 Embedding Layer
## 後面可以使用 embedding_lookup 查找每個特征對應的 embedding
sparse_embedding_dict = {feat.name: Embedding(feat.dimension, embedding_size,
                                              embeddings_initializer=RandomNormal(
                                                      mean=0.0, stddev=init_std, seed=seed),
                                              embeddings_regularizer=l2(
                                                  l2_reg_embedding),
                                              name='sparse_emb_' +
                                                   str(i) + '-' + feat.name,
                                              mask_zero=(feat.name in sess_feature_list)) for i, feat in
                         enumerate(feature_dim_dict["sparse"])}      

擷取目标商品對應的 embedding:

"""
+ query_emb_list 為大小為 2 (等于 len(sess_feature_list)) 的 list, 儲存兩個 tensor, 
+ target item 對應的 cate_id 和 brand
+ feature_dim_dict['sparse'] 記錄了每個 field 的空間大小,
                   用于對這個 field 下的特征值進行 Hash, 
+ sparse_embedding_dict 存儲了每個 field 對應的 embedding layer, 
                   用于将稀疏特征映射為稠密向量;
+ sparse_input 則儲存了每個特征的取值 
"""
query_emb_list = get_embedding_vec_list(sparse_embedding_dict, sparse_input, feature_dim_dict["sparse"],
                                        sess_feature_list, sess_feature_list)

"""
-- 中途插入 --
get_embedding_vec_list 定義如下,用于擷取特征對應的 embedding, 相當于在 Embedding Layer
做 embedding_lookup, 如果指定了 return_feat_list,那麼将隻會擷取 return_feat_list 中特征對應的 embedding
"""
def get_embedding_vec_list(embedding_dict, input_dict, sparse_fg_list,return_feat_list=(),mask_feat_list=()):
    embedding_vec_list = []
    for fg in sparse_fg_list:
        feat_name = fg.name
        if len(return_feat_list) == 0  or feat_name in return_feat_list:
            if fg.hash_flag:
                ## 做 hash 的時候,if mask_zero = True,0 or 0.0 will be set to 0,other value will be set in range[1,num_buckets)
                lookup_idx = Hash(fg.dimension,mask_zero=(feat_name in mask_feat_list))(input_dict[feat_name])
            else:
                lookup_idx = input_dict[feat_name]

            embedding_vec_list.append(embedding_dict[feat_name](lookup_idx))

    return      

​query_emb_list​

​​ 是一個 list,儲存着兩個大小為 ​

​[B, 1, 4]​

​​ 的 Tensor (​

​embedding_size = 4​

​), 之後将特征進行 concatenation

query_emb = concat_fun(query_emb_list)  ## [B, 1, 8]      

之後再擷取輸入到 DNN 中的特征, 仍然是調用 ​

​get_embedding_vec_list​

​ 實作:

"""
sparse_embedding_dict 儲存 embedding layer, sparse_input 儲存特征, 
feature_dim_dict["sparse"] 儲存特征空間大小
傳入 mask_feat_list 是想将這些特征中的 0 值直接映射為 0 向量;
"""
deep_input_emb_list = get_embedding_vec_list(sparse_embedding_dict, sparse_input, feature_dim_dict["sparse"],
                                             mask_feat_list=sess_feature_list)
deep_input_emb = concat_fun(deep_input_emb_list)  ## [B, 1, 60], 15 個稀疏特征, emb_dim=4
deep_input_emb = Flatten()(NoMask()(deep_input_emb)) ## [B, 60]      

擷取到 ​

​deep_input_emb​

​ 後,相當于完成了模型示意圖中的左下角部分:

DSIN 深度 Session 興趣網絡介紹及源碼剖析

下一步實作 Session Division Layer, 用于将使用者行為劃分為 Session, 并加上 Bias-Encoding. 但由于在制作資料集的時候已經對行為劃分了 Session, 是以在這一步主要内容是将行為轉換為 embedding:

"""
tr_input: list, 長度等于 sess_max_count, 每個元素為 [B, 10, 8] 大小的 Tensor, 
10 表示 max_session_len, tr_input 的字首 tr_ 表示 transformer,說明該變量是 Transformer 的輸入
"""
tr_input = sess_interest_division(sparse_embedding_dict, user_behavior_input_dict, feature_dim_dict['sparse'],
                                  sess_feature_list, sess_max_count, bias_encoding=bias_encoding)

""""
-- 中途插入 --
sess_interest_division 函數的定義如下:
"""
def sess_interest_division(sparse_embedding_dict, user_behavior_input_dict, sparse_fg_list, sess_feture_list,
                           sess_max_count,
                           bias_encoding=True):
    tr_input = []
    """
    使用 get_embedding_vec_list 擷取每個行為對應的 embedding,
    行為使用 (cate_id, brand) 表示,而 cate_id 和 brand 對應的 Tensor 大小
    均為 [B, 10], 是以 get_embedding_vec_list 得到兩個大小為 [B, 10, 4] 的 Tensor,
    使用 concat_fun 進行 concat,得到 keys_emb shape=[B, 10, 8]
    tr_input 的長度為 sess_max_count=5
    """
    for i in range(sess_max_count):
        sess_name = "sess_" + str(i)
        keys_emb_list = get_embedding_vec_list(sparse_embedding_dict, user_behavior_input_dict[sess_name],
                                               sparse_fg_list, sess_feture_list, sess_feture_list)
        keys_emb = concat_fun(keys_emb_list)  ## [B, 10, 8]
        tr_input.append(keys_emb)
    ## 加上 BiasEncoding
    if bias_encoding:
        tr_input = BiasEncoding(sess_max_count)(tr_input)
    return      

上面代碼中用到了論文中介紹的 BiasEncoding,單獨介紹一下, 其核心代碼段如下:

class BiasEncoding(Layer):
    def __init__(self, sess_max_count, seed=1024, **kwargs):
        self.sess_max_count = sess_max_count
        self.seed = seed
        super(BiasEncoding, self).__init__(**kwargs)

    def build(self, input_shape):
        # Create a trainable weight variable for this layer.

        if self.sess_max_count == 1:
            embed_size = input_shape[2].value
            seq_len_max = input_shape[1].value
        else:
            embed_size = input_shape[0][2].value
            seq_len_max = input_shape[0][1].value
        
        """
        sess_bias_embedding: [sess_max_count, 1, 1]
        seq_bias_embedding: [1, seq_len_max, 1]
        item_bias_embedding: [1, 1, embed_size]
        注意它們的 shape
        """
        self.sess_bias_embedding = self.add_weight('sess_bias_embedding', shape=(self.sess_max_count, 1, 1),
                                                   initializer=TruncatedNormal(
                                                       mean=0.0, stddev=0.0001, seed=self.seed))
        self.seq_bias_embedding = self.add_weight('seq_bias_embedding', shape=(1, seq_len_max, 1),
                                                  initializer=TruncatedNormal(
                                                      mean=0.0, stddev=0.0001, seed=self.seed))
        self.item_bias_embedding = self.add_weight('item_bias_embedding', shape=(1, 1, embed_size),
                                                   initializer=TruncatedNormal(
                                                       mean=0.0, stddev=0.0001, seed=self.seed))

    def call(self, inputs, mask=None):
        """
        :param concated_embeds_value: None * field_size * embedding_size
        :return: None*1
        sess_bias_embedding + seq_bias_embedding + item_bias_embedding 
        三者通過 Broadcast 進行相加
        """
        transformer_out = []
        for i in range(self.sess_max_count):
            transformer_out.append(
                inputs[i] + self.item_bias_embedding + self.seq_bias_embedding + self.sess_bias_embedding[i])
        return      

得到經過 BiasEncoding 處理後的 Session 輸入後,相當于實作了模型示意圖中的如下部分:

DSIN 深度 Session 興趣網絡介紹及源碼剖析

下一步要将其輸入到 Multi-Head Self-Attention 中,以學習 Session 内各行為的内在關系, 并學習出對應的 Session 興趣,這一步相當于實作了 Session Interest Extractor Layer:

Self_Attention = Transformer(att_embedding_size, att_head_num, dropout_rate=0, use_layer_norm=False,
                                 use_positional_encoding=(not bias_encoding), seed=seed, supports_masking=True,
                                 blinding=True)
"""
tr_input 為 list,大小為 5,裡面的元素為大小等于 [B, 10, 8] 的 Tensor
Self-Attention 的輸出 sess_fea 大小為 [B, 5, 8], 5 為 sess_max_count

提前做個說明,Transformer 中完成每個 Session 的 Multi-Head Self-Attention 後,結果大小
應該是 out=[B, 10, 8], 但最後輸出時會做 reduce_mean(out, axis=1, keep_dims=True), 
用于生成 Session 興趣對應的 embedding,大小為 [B, 1, 8], 由于總共有 sess_max_count=5 個 Session,是以最終的輸出 sess_fea 大小為 [B, 5, 8]
"""
sess_fea = sess_interest_extractor(
    tr_input, sess_max_count, Self_Attention)  ## [B, 5, 8]

"""
-- 中途插入 --
sess_interest_extractor 函數的定義如下
"""
def sess_interest_extractor(tr_input, sess_max_count, TR):
    """
    tr_input 為 list, 大小為 5,裡面的元素大小為 [B, 10, 8]
    sess_max_count=5
    TR 即 Transformer,實作 Multi-Head Self-Attention
    """
    tr_out = []
    for i in range(sess_max_count):
        tr_out.append(TR(
            [tr_input[i], tr_input[i]]))
    sess_fea = concat_fun(tr_out, axis=1)
    return      

​Transformer​

​ 的核心代碼就不貼了,太長了,知道它輸入輸出大小即可。上述代碼完成了模型結構圖中如下部分:

DSIN 深度 Session 興趣網絡介紹及源碼剖析

下一步使用 Bi-LSTM 學習 Session 興趣之間的演進,完成了 Session Interest Interacting Layer 的實作:

"""
輸入 sess_fea 大小為 [B, 5, 8]
輸出 lstm_outputs 大小也為 [B, 5, 8]
"""
lstm_outputs = BiLSTM(len(sess_feature_list) * embedding_size,
                      layers=2, res_layers=0, dropout_rate=0.2, )(sess_fea)      

相當于實作了模型示意圖中的:

DSIN 深度 Session 興趣網絡介紹及源碼剖析

在得到 Session 興趣序列後, 由于每個 session 興趣對目标商品的影響不同,這裡采用 Attention 機制來刻畫目标商品和每個 session 興趣之間的相關性,即實作 Session Interest Activating Layer 層:

"""
注意輸入為 [query_emb, sess_fea, user_sess_length], 其中 query_emb 為目标商品對應的 embedding,大小為 [B, 8], 而 sess_fea 表示使用者興趣,大小為 [B, 5, 8], user_sess_length 大小為 [B, 1], 表示使用者真實的 Session 的長度,在做 Attention 時,用作 Mask,以計算權重系數。

AttentionSequencePoolingLayer 即 DIN 網絡中目标商品和曆史行為的 Attention,關于 DIN 網絡的介紹可以檢視: https://zhuanlan.zhihu.com/p/338050940
輸出結果 interest_attention_layer 的 shape=[B, 1, 8]
"""
interest_attention_layer: [B, 1, 8], user_sess_length: [B, 1]
interest_attention_layer = AttentionSequencePoolingLayer(att_hidden_units=(64, 16), weight_normalization=True,
                                                         supports_masking=False)(
                                    [query_emb, sess_fea, user_sess_length])

"""
同理,這裡時 query_emb 和 lstm_outputs 繼續 Attention,
lstm_outputs 大小為 [B, 5, 8]
輸出結果 lstm_attention_layer 大小為 [B, 1, 8]
"""
lstm_attention_layer = AttentionSequencePoolingLayer(att_hidden_units=(64, 16), weight_normalization=True)(
                               [query_emb, lstm_outputs, user_sess_length])      

以上步驟相當于實作了模型示意圖中的:

最後,将輸入特征都給 Concat 起來:

## deep_input_emb: [B, 76] 60 + 8 + 8
deep_input_emb = Concatenate()(
    [deep_input_emb, Flatten()(interest_attention_layer), Flatten()(lstm_attention_layer)])

## dense_input.values(): [B, 1], 表示 price
## 此時 deep_input_emb: [B, 77]
if len(dense_input) > 0:  ## 如果存在稠密特征的話
    deep_input_emb = Concatenate()(
        [deep_input_emb] + list(dense_input.values()))      

相當于實作了:

DSIN 深度 Session 興趣網絡介紹及源碼剖析

将 Concat 起來的向量輸入到 DNN 中,實作對點選率的預估:

## output: [B, 80], dnn_hidden_units: [200, 80]
output = DNN(dnn_hidden_units, dnn_activation, l2_reg_dnn,
             dnn_dropout, dnn_use_bn, seed)(deep_input_emb)
output = Dense(1, use_bias=False, activation=None)(output)
## output: [B, 1]
output = PredictionLayer(task)(output)      

即實作:

DSIN 深度 Session 興趣網絡介紹及源碼剖析

總結

繼續閱讀