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 劃分的一個例子:
圖檔表示使用者點選過的商品,圖檔下面的數字為點選時間,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 的網絡結構如下圖所示:
網絡結構圖稍顯複雜, 我們按照從易到難的順序進行介紹, 大緻可以分為 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 是阿裡巴巴提供的一個淘寶展示廣告點選率預估資料集。其主要内容如下:
說明:
-
:原始的樣本骨架,應該是從展現點選日志中擷取的資料,描述了使用者與曝光商品(廣告)之間的關系,比如是否發生了點選等;raw_sample
-
: 描述了廣告的基本資訊,比如廣告 ID,廣告計劃 ID,品牌等;ad_feature
-
: 描述了使用者的基本資訊,如使用者 ID, 年齡,性别等;user_profile
-
: 使用者的行為日志,描述了使用者的曆史行為,行為類型主要包含浏覽、購買、加購、喜歡(收藏);raw_behavior_log
資料預處理 – 采樣使用者
代碼位址:https://github.com/shenweichen/DSIN/blob/master/code/0_gen_sampled_data.py
作者貼心地在給代碼檔案命名時加上了
0_
,
1_
之類的字首,表明了代碼的執行順序,首先我們需要執行
0_gen_sampled_data.py
對使用者進行采樣。代碼中很多内容是檔案的讀取,下面我隻截取出比較核心的部分:
- 采樣使用者 (詳情見注釋)
基本資訊讀取,并對使用者進行下采樣,曆史行為隻考慮浏覽行為。
## 讀取使用者資訊表和原始樣本骨架
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)
- 使用者行為編碼
分别對
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
來産生模型所需要的訓練資料。這部分代碼相對比較複雜,下面将代碼拆分,對各個部分進行分析。
- 讀取
産生的使用者 session 檔案,并統一儲存到1_gen_session.py
字典中。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
- 擷取采樣後的使用者, 儲存到
中sample_sub
sample_sub = pd.read_pickle(
'../sampled_data/raw_sample_' + str(FRAC) + '.pkl')
- 将
中的所有使用者的 session 彙總,儲存到sample_sub
中,每個使用者隻保留sess_input_dict
(定義在DSIN_SESS_COUNT = 5
檔案中)個 Session. 另外使用config.py
儲存每個 session 的真實行為個數,因為在後面的處理過程中,會存在将 session 截斷或補 0 的操作,使所有 Session 的長度統一為sess_input_length_dict
.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': {
.....
},
}
- 繼續介紹主邏輯:下一步讀取使用者資訊以及廣告資訊,并和原始樣本骨架表(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
變量中.
- 将稀疏特征和稠密特征都轉化為 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 等。
- 将所有使用者各個 Session 中的行為個數統一限制為
(10個),如果行為個數不足 10 個,那麼進行補零操作:DSIN_SESS_MAX_LEN
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])
- 構造 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
的行數)
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
按順序來看,正好可以和下圖對應!
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
後,相當于完成了模型示意圖中的左下角部分:
下一步實作 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 輸入後,相當于實作了模型示意圖中的如下部分:
下一步要将其輸入到 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
的核心代碼就不貼了,太長了,知道它輸入輸出大小即可。上述代碼完成了模型結構圖中如下部分:
下一步使用 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)
相當于實作了模型示意圖中的:
在得到 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()))
相當于實作了:
将 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)
即實作: